ASP.NET Core 5.0 - FIDO2 Attestation Trust

This article will describe the implementation of validating the attestation trust path for the "packed" and "tpm" (Trusted Platform Module) formats in the Users Without Passwords Project (UWPP). I will assume you have downloaded the ASP.NET Core 5.0 - Users Without Passwords Project.

Users Without Passwords Project and Article Series

I have developed two separate projects in the Users Without Passwords Project (UWPP) solution. The Users Without Passwords v4 project supports Bootstrap v4 and the new Users Without Passwords project supports Bootstrap v5. The new version is published at Fido.KenHaggerty.Com. You can register a new user with Windows Hello or a FIDO2 security key. Details, screenshots, and related articles can be found at ASP.NET Core 5.0 - Users Without Passwords Project. The details page includes the version change log.

Early prototypes of the project trusted any authenticator which successfully decoded a Credential Id and a PublicKey. After I purchased a HyperFIDO Titanium PRO key and a second yubico key, I wanted to track the device's Authenticator Attestation Globally Unique ID (AAGUID). I found if the Attestation option is omitted or set to "none", the authData's bytes for the AAGUID were 00000000-0000-0000-0000-000000000000. I also had issues verifying the signature counter. The signature counter should increment with each authentication. I published a prototype of the project to fido.kenhaggerty.com and emailed HyperFIDO support, including the AAGUID I decoded, asking for help. The kind folks at Hypersceu analyzed the published project and replied letting me know I was not decoding big-endian values properly. Even the AAGUID which is big-endian didn't match. I was embarrassed but thankful I had not released that version. I resolved those issues by properly decoding the big-endian values.

AttestationCrypto. DecodeAttestationResponse:
// Signature counter (4 bytes, big-endian unint32)
var counterBytes = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, 4);
Array.Reverse(counterBytes);
var counter = BitConverter.ToUInt32(counterBytes);

// AAGUID (16 bytes, big-endian GUID)
// 00000000-0000-0000-0000-000000000000 when Attestation Option = none
var aaguidBytes = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, 16);
var aaguid = AuthenticatorUtils.GuidFromBigEndian(aaguidBytes);

// CredentialId Length (2 bytes, big-endian uint16)
var credentialIdLengthBytes = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, 2);
Array.Reverse(credentialIdLengthBytes);
var credentialIdLength = BitConverter.ToUInt16(credentialIdLengthBytes);
// Credential ID (CredentialId Length bytes)
var credentialId = AuthenticatorUtils.GetSizedByteArray(authData, ref offset, credentialIdLength);
AuthenticatorUtils.cs:
/// <summary>
/// AAGUID is sent as big endian byte array, this converter is for little endian systems.
/// </summary>
/// <param name="aaguidBytes"></param>
/// <returns><see cref="Guid" /></returns>
public static Guid GuidFromBigEndian(byte[] aaguidBytes)
{
    SwapBytes(aaguidBytes, 0, 3);
    SwapBytes(aaguidBytes, 1, 2);
    SwapBytes(aaguidBytes, 4, 5);
    SwapBytes(aaguidBytes, 6, 7);

    return new Guid(aaguidBytes);
}

private static void SwapBytes(byte[] bytes, int index1, int index2)
{
    var temp = bytes[index1];
    bytes[index1] = bytes[index2];
    bytes[index2] = temp;
}

The Attestation option in CredentialCreationOptions defaults to "none". See Attestation Conveyance Preference Enumeration. This configuration indicates that the Relying Party is not interested in authenticator attestation. "indirect" indicates the RP prefers a verifiable attestation statement but allows the client to decide how to obtain it. "direct" indicates the RP wants to receive the attestation statement. It is recommended that RPs use the "direct" value and store the attestation statement with the credential so they can inspect authenticator models retroactively if policy changes. The attestation statement is used to prove the trustworthiness of the device. The attestation statement contains an attestation trust path or chain of X.509 certificates.

The ASP.NET Core 5.0 - FIDO2 Credential Create article describes decoding a valid Credential Id and a PublicKey without validating the attestation statement. When the Attestation option in CredentialCreationOptions is set to "direct" or "indirect", the attestation statement format should return a "packed" or "tpm" value. The UWPP validates the trust path for the "packed" and "tpm" formats. See Defined Attestation Statement Formats.

AttestationCrypto. DecodeAttestationResponse:
// 7.1.18 Determine the attestation statement format by performing a USASCII case-sensitive match on fmt
// against the set of supported WebAuthn Attestation Statement Format Identifier values.
// If Attestation: none then fmt = none.
// If Attestation: direct then fmt = packed or tpm (Trusted Platform Module).
var fmt = cbor["fmt"].AsString();

// 7.1.21 Assess the attestation trustworthiness using the outputs of the verification procedure in step 19, as follows:            
// a If no attestation was provided, verify that None attestation is acceptable under Relying Party policy.
if (fmt.Equals("none"))
{
    // Firefox v91.0 generates credentials using TPM with Windows Hello - but does not send TPM attestation
    // https://github.com/w3c/webauthn/issues/1620
    // https://bugzilla.mozilla.org/show_bug.cgi?id=1716859
    if (attestation == "none" || isFirefox)
        return new AuthenticationResult
        {
            AaGuidString = aaguid.ToString(),
            CredentialId = Convert.ToBase64String(credentialId.ToArray()),
            PublicKeyJson = publicKeyJson,
            SignatureCounter = counter,
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports,
            SanExtensionJson = string.Empty,
            Verified = true
        };
    else
        return new AuthenticationResult()
        {
            Error = "AttStat fmt = none, expected: packed or tpm.",
            AaGuidString = aaguid.ToString(),
            CredentialId = Convert.ToBase64String(credentialId.ToArray()),
            PublicKeyJson = publicKeyJson,
            SignatureCounter = counter,
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports,
            SanExtensionJson = string.Empty
        };
}

var attStmt = cbor["attStmt"];

// 7.1.19 Verify that attStmt is a correct attestation statement, conveying a valid attestation signature,
// by using the attestation statement format fmt’s verification procedure given attStmt, authData and hash.
// Note: Each attestation statement format specifies its own verification procedure.
var attStmtSignature = attStmt["sig"];
if (attStmtSignature == null || attStmtSignature.Type != CBORType.ByteString ||
    attStmtSignature.GetByteString().Length == 0)
    return new AuthenticationResult()
    {
        Error = "7.1.19 Invalid signature, expected: valid, detected: invalid.",
        AaGuidString = aaguid.ToString(),
        CredentialId = Convert.ToBase64String(credentialId.ToArray()),
        PublicKeyJson = publicKeyJson,
        SignatureCounter = counter,
        ClientExtensionResultsJson = clientExtensionResultsJson,
        Transports = transports,
        SanExtensionJson = string.Empty
    };

// 7.1.20 MetadataService is not implemented

// 7.1.21 c Otherwise, use the X.509 certificates returned as the attestation trust path from the verification
// procedure to verify that the attestation public key either correctly chains up to an acceptable root certificate,
// or is itself an acceptable certificate (i.e., it and the root certificate obtained in Step 20 may be the same).
var x5c = attStmt["x5c"];

// 8.2.1 Packed Attestation Statement Certificate Requirements
// https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements
// 8.3.1 TPM Attestation Statement Certificate Requirements
// https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements

X509Certificate2 attestnCert = null;
//if (null != x5c && CBORType.Array == x5c.Type && 0 != x5c.Count)
if (x5c != null && x5c.Type == CBORType.Array && x5c.Count > 0)
{
    if (x5c.Values == null ||
        x5c.Values.Count == 0 ||
        x5c.Values.First().Type != CBORType.ByteString ||
        x5c.Values.First().GetByteString().Length == 0)
        return new AuthenticationResult()
        {
            Error = "8.2.1 & 8.3.1 Invalid x5c in attestation statement, expected: valid, detected: invalid.",
            AaGuidString = aaguid.ToString(),
            CredentialId = Convert.ToBase64String(credentialId.ToArray()),
            PublicKeyJson = publicKeyJson,
            SignatureCounter = counter,
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports,
            SanExtensionJson = string.Empty
        };

    // Attestation Identity Key
    attestnCert = new X509Certificate2(x5c.Values.First().GetByteString());
    if (DateTime.Now < attestnCert.NotBefore || DateTime.Now > attestnCert.NotAfter)
        return new AuthenticationResult()
        {
            Error = "8.2.1 & 8.3.1 Invalid Attestation Identity Key certificate, expired or not yet valid.",
            AaGuidString = aaguid.ToString(),
            CredentialId = Convert.ToBase64String(credentialId.ToArray()),
            PublicKeyJson = publicKeyJson,
            SignatureCounter = counter,
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports,
            SanExtensionJson = string.Empty
        };

    // 8.2.1 a & 8.3.1 a Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).
    if (attestnCert.Version != 3)
        return new AuthenticationResult()
        {
            Error = $"8.2.1 a && 8.3.1 a Invalid Attestation Identity Key certificate version, expected: 3 = (ASN1 2), detected: {attestnCert.Version}.",
            AaGuidString = aaguid.ToString(),
            CredentialId = Convert.ToBase64String(credentialId.ToArray()),
            PublicKeyJson = publicKeyJson,
            SignatureCounter = counter,
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports,
            SanExtensionJson = string.Empty
        };

    // 8.3.1 e The Basic Constraints extension MUST have the CA component set to false.
    if (AuthenticatorUtils.IsAttnCertCACert(attestnCert.Extensions))
        return new AuthenticationResult()
        {
            Error = "8.3.1 e Invalid Attestation Identity Key certificate basic constraints extension, CertificateAuthority " +
            "must be false.",
            AaGuidString = aaguid.ToString(),
            CredentialId = Convert.ToBase64String(credentialId.ToArray()),
            PublicKeyJson = publicKeyJson,
            SignatureCounter = counter,
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports,
            SanExtensionJson = string.Empty
        };

    // 8.2.1 c If the related attestation root certificate is used for multiple authenticator models,
    // the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing
    // the AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical.
    // 8.3 Verification 5 certInfo i If aikCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4
    // (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData.
    // Windows Hello = null
    var attestnCertAaguid = AuthenticatorUtils.AaguidBytesFromAttnCertExts(attestnCert.Extensions);
    if ((attestnCertAaguid != null) && (!attestnCertAaguid.SequenceEqual(Guid.Empty.ToByteArray())) &&
        AuthenticatorUtils.GuidFromBigEndian(attestnCertAaguid).CompareTo(aaguid) != 0)
        return new AuthenticationResult()
        {
            Error = $"8.3 Verification 5 certInfo i Invalid Attestation Identity Key certificate aaguid value, expected: {aaguid}, " +
            $"detected: {AuthenticatorUtils.GuidFromBigEndian(attestnCertAaguid)}.",
            AaGuidString = aaguid.ToString(),
            CredentialId = Convert.ToBase64String(credentialId.ToArray()),
            PublicKeyJson = publicKeyJson,
            SignatureCounter = counter,
            ClientExtensionResultsJson = clientExtensionResultsJson,
            Transports = transports,
            SanExtensionJson = string.Empty
        };

}

var ecdaaKeyId = attStmt["ecdaaKeyId"];
if (ecdaaKeyId != null && ecdaaKeyId.Count > 0)
{
    //https://github.com/webauthn-open-source/fido2-lib/blob/6ab981a91912303ecefe65cee3e03eb2b6c1968f/lib/attestations/packed.js#L78-L88
    return new AuthenticationResult()
    {
        Error = "ECDAA Attestation is not implemented.",
        AaGuidString = aaguid.ToString(),
        CredentialId = Convert.ToBase64String(credentialId.ToArray()),
        PublicKeyJson = publicKeyJson,
        SignatureCounter = counter,
        ClientExtensionResultsJson = clientExtensionResultsJson,
        Transports = transports,
        SanExtensionJson = string.Empty
    };
}

var alg = attStmt["alg"];
// Windows Hello: AuthData algorithm = -257 (RS256), AttStmt algorithm = -65535 (RS1)
var algorithm = (AuthenticatorUtils.Algorithm)alg.AsInt32();

// 8.2 Signing 1 && 8.3 Signing 1 Let authenticatorData denote the authenticator data for the attestation,
// and let clientDataHash denote the hash of the serialized client data.
// 8.3 Signing 2 Concatenate authenticatorData and clientDataHash to form attToBeSigned.
var dataBytes = new byte[authData.Length + clientDataHash.Length];
authData.CopyTo(dataBytes, 0);
clientDataHash.CopyTo(dataBytes, authData.Length);

// The verify attestation methods set Verified true or Error = failure description
var result = new AuthenticationResult();
switch (fmt)
{
    case "packed":
        // 8.2 Verification 1 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform
        // CBOR decoding on it to extract the contained fields.
        result = VerifyPackedAttestation(algorithm, attestnCert, attStmtSignature, dataBytes, publicKeyJson);
        result.AaGuidString = aaguid.ToString();
        result.CredentialId = Convert.ToBase64String(credentialId.ToArray());
        result.PublicKeyJson = publicKeyJson;
        result.SignatureCounter = counter;
        result.ClientExtensionResultsJson = clientExtensionResultsJson;
        result.Transports = transports;
        result.SanExtensionJson = string.Empty;
        break;
    case "tpm":
        result = VerifyTpmAttestation(attStmt, cborAuthDataPublicKey, algorithm, attestnCert, attStmtSignature, dataBytes);
        result.AaGuidString = aaguid.ToString();
        result.CredentialId = Convert.ToBase64String(credentialId.ToArray());
        result.PublicKeyJson = publicKeyJson;
        result.SignatureCounter = counter;
        result.ClientExtensionResultsJson = clientExtensionResultsJson;
        result.Transports = transports;
        // SanExtensionJson set by VerifyTpmAttestation
        break;
    default:
        result.Error = $"Unsupported Attestation fmt, expected: packed or tpm, detected: {fmt}.";
        result.AaGuidString = aaguid.ToString();
        result.CredentialId = Convert.ToBase64String(credentialId.ToArray());
        result.PublicKeyJson = publicKeyJson;
        result.SignatureCounter = counter;
        result.ClientExtensionResultsJson = clientExtensionResultsJson;
        result.Transports = transports;
        result.SanExtensionJson = string.Empty;
        break;
}
return result;
VerifyPackedAttestation:
/// <summary>
/// 8.2 https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation
/// </summary>
private static AuthenticationResult VerifyPackedAttestation(AuthenticatorUtils.Algorithm algorithm,
    X509Certificate2 attestnCert, CBORObject attStmtSignature, byte[] dataBytes, string publicKeyJson)
{
    if (algorithm != AuthenticatorUtils.Algorithm.ES256 &&
        algorithm != AuthenticatorUtils.Algorithm.ES384 &&
        algorithm != AuthenticatorUtils.Algorithm.ES512)
        return new AuthenticationResult()
        {
            Error = $"Packed Attestation verification: Unsupported algorithm value in packed attestation, " +
            $"expected: ES256, ES384, or ES512, detected: {(AuthenticatorUtils.Algorithm)algorithm}."
        };

    // 8.2 Verification 2 If x5c is present:
    if (attestnCert != null)
    {
        // Return a AuthenticationResult with exception message.
        try
        {
            // 8.2.1 b Subject field MUST be set to:
            //  Subject - C
            //      ISO 3166 code specifying the country where the Authenticator vendor is incorporated(PrintableString)
            //  Subject - O
            //      Legal name of the Authenticator vendor(UTF8String)
            //  Subject - OU
            //      Literal string “Authenticator Attestation” (UTF8String)
            //  Subject - CN
            //      A UTF8String of the vendor’s choosing
            var certSubject = attestnCert.Subject;
            var dictSubject = certSubject.Split(new string[] { ", " }, StringSplitOptions.None)
                                        .Select(part => part.Split('='))
                                        .ToDictionary(split => split[0], split => split[1]);
            if (dictSubject["C"].Length != 2 && dictSubject["O"].Length != 0 && dictSubject["OU"].Length != 0 &&
                dictSubject["OU"].ToString() == "Authenticator Attestation" && dictSubject["CN"].Length != 0)
                return new AuthenticationResult()
                {
                    Error = "8.2.1 b Packed FULL Attestation verification: Invalid Attestation Identity Key certificate subject, " +
                    "expected: valid, detected: invalid."
                };

            var ecDsaPublicKey = attestnCert.GetECDsaPublicKey();
            var keyParams = ecDsaPublicKey.ExportParameters(false);
            var keyParamsNamedCurve = ECCurve.NamedCurves.nistP256;
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                if (keyParams.Curve.Oid.FriendlyName.Equals(ECCurve.NamedCurves.nistP256.Oid.FriendlyName))
                    keyParamsNamedCurve = ECCurve.NamedCurves.nistP256;

                if (keyParams.Curve.Oid.FriendlyName.Equals(ECCurve.NamedCurves.nistP384.Oid.FriendlyName))
                    keyParamsNamedCurve = ECCurve.NamedCurves.nistP384;

                if (keyParams.Curve.Oid.FriendlyName.Equals(ECCurve.NamedCurves.nistP521.Oid.FriendlyName))
                    keyParamsNamedCurve = ECCurve.NamedCurves.nistP521;
            }
            else
            {
                if (keyParams.Curve.Oid.Value.Equals(ECCurve.NamedCurves.nistP256.Oid.Value))
                    keyParamsNamedCurve = ECCurve.NamedCurves.nistP256;

                if (keyParams.Curve.Oid.Value.Equals(ECCurve.NamedCurves.nistP384.Oid.Value))
                    keyParamsNamedCurve = ECCurve.NamedCurves.nistP384;

                if (keyParams.Curve.Oid.Value.Equals(ECCurve.NamedCurves.nistP521.Oid.Value))
                    keyParamsNamedCurve = ECCurve.NamedCurves.nistP521;
            }

            //  Elliptic Curve Cryptography
            var ecDsa = ECDsa.Create(new ECParameters
            {
                Curve = keyParamsNamedCurve,
                Q = new ECPoint
                {
                    X = keyParams.Q.X,
                    Y = keyParams.Q.Y
                }
            });

            var signature = AuthenticatorUtils.SigFromEcDsaSig(attStmtSignature.GetByteString(), ecDsa.KeySize);

            var hashAlgorithmName = HashAlgorithmName.SHA256;
            if (algorithm.Equals((int)AuthenticatorUtils.Algorithm.ES384))
                hashAlgorithmName = HashAlgorithmName.SHA384;
            if (algorithm.Equals((int)AuthenticatorUtils.Algorithm.ES512))
                hashAlgorithmName = HashAlgorithmName.SHA512;

            // 8.2 Signing 2 If Basic or AttCA attestation is in use, the authenticator produces the sig by concatenating
            // authenticatorData and clientDataHash, and signing the result using an attestation private key selected
            // through an authenticator-specific mechanism. It sets x5c to attestnCert followed by the related certificate
            // chain (if any). It sets alg to the algorithm of the attestation private key.
            if (!ecDsa.VerifyData(dataBytes, signature, hashAlgorithmName))
                return new AuthenticationResult()
                {
                    Error = "8.2 Signing 2 Packed FULL Attestation verification: failed, expected: true, detected: false."
                };
        }
        catch (Exception ex)
        {
            return new AuthenticationResult()
            {
                Error = $"Packed FULL Attestation verification: Exception {ex.Message}."
            };
        }

    }
    else
    {
        //https://github.com/webauthn-open-source/fido2-lib/blob/6ab981a91912303ecefe65cee3e03eb2b6c1968f/lib/attestations/packed.js#L78-L88
        //return new AuthenticationResult()
        //{
        //    Error = "Packed SELF(SURROGATE) Attestation verification is not implemented. Attempts do not verify."
        //};

        // Return a AuthenticationResult with exception message.
        try
        {
            //  Elliptic Curve Cryptography
            ECCModel shaPublicKey = JsonSerializer.Deserialize<ECCModel>(publicKeyJson);
            var ecDsa = ECDsa.Create(new ECParameters
            {
                Curve = ECCurve.NamedCurves.nistP256,
                Q = new ECPoint
                {
                    X = Base64UrlTextEncoder.Decode(shaPublicKey.X),
                    Y = Base64UrlTextEncoder.Decode(shaPublicKey.Y)
                }
            });

            var signature = AuthenticatorUtils.SigFromEcDsaSig(attStmtSignature.GetByteString(), ecDsa.KeySize);
            if (!ecDsa.VerifyData(dataBytes, signature, HashAlgorithmName.SHA256))
                return new AuthenticationResult()
                {
                    Error = "Packed SELF(SURROGATE) Attestation verification: failed, expected: true, detected: false."
                };
        }
        catch (Exception ex)
        {
            return new AuthenticationResult()
            {
                Error = $"Packed SELF(SURROGATE) Attestation verification: Exception {ex.Message}."
            };
        }
    }

    return new AuthenticationResult
    {
        Verified = true
    };
}
VerifyTpmAttestation:
/// <summary>
/// 8.3 https://www.w3.org/TR/webauthn-2/#sctn-tpm-attestation
/// </summary>
private static AuthenticationResult VerifyTpmAttestation(CBORObject attStmt, CBORObject cborAuthDataPublicKey,
    AuthenticatorUtils.Algorithm algorithm, X509Certificate2 aikCert, CBORObject attStmtSignature,
    byte[] dataBytes)
{
    if (algorithm != AuthenticatorUtils.Algorithm.RS1 &&
        algorithm != AuthenticatorUtils.Algorithm.RS256 &&
        algorithm != AuthenticatorUtils.Algorithm.RS384 &&
        algorithm != AuthenticatorUtils.Algorithm.RS512)
        return new AuthenticationResult()
        {
            Error = $"TPM Attestation verification: Unsupported algorithm value in TPM attestation, expected: RS1, RS256, RS384, or RS512," +
            $" detected: {(AuthenticatorUtils.Algorithm)algorithm}."
        };

    string sanExtensionJson;
    // Return a AuthenticationResult with exception message.
    try
    {
        var attStmtVersion = attStmt["ver"].AsString();
        if (!attStmtVersion.Equals("2.0"))
            return new AuthenticationResult()
            {
                Error = $"TPM Attestation verification: Unsupported attestation statement version, expected: (string)2.0, detected: {attStmtVersion}."
            };

        // 8.3 Verification 2 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform
        // CBOR decoding on it to extract the contained fields.
        PubArea pubArea = null;
        if (attStmt["pubArea"] != null &&
            attStmt["pubArea"].Type == CBORType.ByteString &&
            attStmt["pubArea"].GetByteString().Length != 0)
            pubArea = new PubArea(attStmt["pubArea"].GetByteString());

        if (pubArea == null || pubArea.Unique == null || pubArea.Unique.Length == 0)
            return new AuthenticationResult()
            {
                Error = "8.3 Verification 2 TPM Attestation verification: Missing or malformed pubArea, expected: valid, detected: invalid."
            };

        // 8.3 Signing 3 Generate a signature using the procedure specified in [TPMv2-Part3] Section 18.2,
        // using the attestation private key and setting the extraData parameter to the digest of attToBeSigned
        // using the hash algorithm corresponding to the "alg" signature algorithm.
        // (For the "RS256" algorithm, this would be a SHA-256 digest.)
        var credentialPublicKey = new CredentialPublicKey(aikCert, (int)algorithm);
        var cborCredentialPublicKey = credentialPublicKey.GetCBORObject();

        //https://www.w3.org/TR/webauthn-2/#sctn-tpm-attestation
        // 8.3 Verification 3 Verify that the public key specified by the parameters and unique fields of pubArea
        // is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData.
        var coseKty = cborCredentialPublicKey[CBORObject.FromObject(AuthenticatorUtils.KeyCommonParameter.KeyType)].AsInt32();
        if (coseKty == 3) // RSA
        {
            var coseMod = cborCredentialPublicKey[CBORObject.FromObject(AuthenticatorUtils.KeyTypeParameter.N)].GetByteString(); // modulus 
            var coseExp = cborCredentialPublicKey[CBORObject.FromObject(AuthenticatorUtils.KeyTypeParameter.E)].GetByteString(); // exponent

            var authDataPublicKeyMod = cborAuthDataPublicKey[CBORObject.FromObject(AuthenticatorUtils.KeyTypeParameter.N)].GetByteString().ToArray();
            var authDataPublicKeyExp = cborAuthDataPublicKey[CBORObject.FromObject(AuthenticatorUtils.KeyTypeParameter.E)].GetByteString().ToArray(); // exponent

            // Windows Hello passes
            if (!authDataPublicKeyMod.SequenceEqual(pubArea.Unique))
                return new AuthenticationResult()
                {
                    Error = "8.3 Verification 3 TPM Attestation verification: AuthData PublicKey Modulus mismatch with pubArea.Unique."
                };

            // Windows Hello fails
            //if (!coseMod.SequenceEqual(authDataPublicKeyMod))
            //    return new AuthenticationResult()
            //    {
            //        Error = "8.3 Verification 3 CredentialPublicKey mismatch between authData PublicKey Modulus.",
            //        AaGuidString = aaguid.ToString()
            //    };
            //if (!coseMod.ToArray().SequenceEqual(pubArea.Unique.ToArray()))
            //    return new AuthenticationResult()
            //    {
            //        Error = "8.3 Verification 3 Public key mismatch between pubArea and credentialPublicKey.",
            //        AaGuidString = aaguid.ToString()
            //    };

            if (!coseExp.SequenceEqual(authDataPublicKeyExp))
                return new AuthenticationResult()
                {
                    Error = "8.3 Verification 3 TPM Attestation verification: CredentialPublicKey Exponent mismatch with authData PublicKey ."
                };

            if ((coseExp[0] + (coseExp[1] << 8) + (coseExp[2] << 16)) != pubArea.Exponent)
                return new AuthenticationResult()
                {
                    Error = "8.3 Verification 3 TPM Attestation verification: CredentialPublicKey Exponent mismatch with pubArea.Exponent."
                };
        }
        else if (coseKty == 2) // ECC
        {
            var curve = cborCredentialPublicKey[CBORObject.FromObject(AuthenticatorUtils.KeyTypeParameter.Crv)].AsInt32();
            var X = cborCredentialPublicKey[CBORObject.FromObject(AuthenticatorUtils.KeyTypeParameter.X)].GetByteString();
            var Y = cborCredentialPublicKey[CBORObject.FromObject(AuthenticatorUtils.KeyTypeParameter.Y)].GetByteString();

            if (pubArea.EccCurve != AuthenticatorUtils.CoseCurveToTpm[curve])
                return new AuthenticationResult()
                {
                    Error = "8.3 Verification 3 TPM Attestation verification: CredentialPublicKey Curve mismatch with pubArea.EccCurve."
                };

            if (!pubArea.ECPoint.X.SequenceEqual(X))
                return new AuthenticationResult()
                {
                    Error = "8.3 Verification 3 TPM Attestation verification: CredentialPublicKey X-coordinate mismatch with pubArea.ECPoint.X."
                };
            if (!pubArea.ECPoint.Y.SequenceEqual(Y))
                return new AuthenticationResult()
                {
                    Error = "8.3 Verification 3 TPM Attestation verification: CredentialPublicKey Y-coordinate mismatch with pubArea.ECPoint.Y."
                };
        }

        // 8.3 Verification 2 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform
        // CBOR decoding on it to extract the contained fields.
        CertInfo certInfo = null;
        if (attStmt["certInfo"] != null &&
            attStmt["certInfo"].Type == CBORType.ByteString &&
            attStmt["certInfo"].GetByteString().Length != 0)
            certInfo = new CertInfo(attStmt["certInfo"].GetByteString());

        if (certInfo == null)
            return new AuthenticationResult()
            {
                Error = "8.3 Verification 2 TPM Attestation verification: Invalid certInfo, detected: certInfo = null."
            };

        // 8.3 Verification 5 certInfo a Verify that magic is set to TPM_GENERATED_VALUE.
        if (BitConverter.ToUInt32(certInfo.Magic.ToArray().Reverse().ToArray(), 0) !=
            (uint)AuthenticatorUtils.TpmGenerated.TPM_GENERATED_VALUE)
            return new AuthenticationResult()
            {
                Error = $"8.3 Verification 5 certInfo a TPM Attestation verification: Invalid certInfo magic number, " +
                $"expected: {(uint)AuthenticatorUtils.TpmGenerated.TPM_GENERATED_VALUE}, " +
                $"detected: {BitConverter.ToString(certInfo.Magic).Replace("-", "")}."
            };

        // 8.3 Verification 5 certInfo b Verify that type is set to TPM_ST_ATTEST_CERTIFY.
        if (BitConverter.ToUInt16(certInfo.Type.ToArray().Reverse().ToArray(), 0) !=
            (ushort)AuthenticatorUtils.TpmStructureTags.TPM_ST_ATTEST_CERTIFY)
            return new AuthenticationResult()
            {
                Error = $"8.3 Verification 5 certInfo b TPM Attestation verification: Invalid certInfo type, " +
                $"expected: {(ushort)AuthenticatorUtils.TpmStructureTags.TPM_ST_ATTEST_CERTIFY}, " +
                $"detected: {BitConverter.ToUInt16(certInfo.Type.ToArray().Reverse().ToArray(), 0)}."
            };

        // 8.3 Verification 5 certInfo c Verify that extraData is set to the hash of attToBeSigned using the hash
        // algorithm employed in "alg".
        if (certInfo.ExtraData == null || certInfo.ExtraData.Length == 0)
            return new AuthenticationResult()
            {
                Error = $"8.3 Verification 5 certInfo c TPM Attestation verification: Invalid certInfo extraData, " +
                $"expected: ExtraData.Length > 0," +
                $" detected: ExtraData = null || ExtraData.Length = 0."
            };

        using (var hasher = AuthenticatorUtils.GetHasher(AuthenticatorUtils.HashAlgorithmNameFromCOSEAlg((int)algorithm)))
        {
            if (!hasher.ComputeHash(dataBytes).SequenceEqual(certInfo.ExtraData))
                return new AuthenticationResult()
                {
                    Error = "8.3 Verification 5 certInfo c TPM Attestation verification: Invalid certInfo.ExtraData, hash value mismatch with dataBytes."
                };
        }

        // 8.3 Verification 5 certInfo d Verify that attested contains a TPMS_CERTIFY_INFO structure as specified
        // in [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea, as computed
        // using the algorithm in the nameAlg field of pubArea using the procedure specified in [TPMv2-Part1] section 16.
        using (var hasher = AuthenticatorUtils.GetHasher(AuthenticatorUtils.HashAlgorithmNameFromCOSEAlg(certInfo.Alg)))
        {
            if (!hasher.ComputeHash(pubArea.Raw).SequenceEqual(certInfo.AttestedName))
                return new AuthenticationResult()
                {
                    Error = "8.3 Verification 5 certInfo d TPM Attestation verification: Invalid pubArea.Raw, hash value mismatch with certInfo.AttestedName."
                };
        }

        // 8.3 Verification 5 certInfo e Verify that x5c is present.
        // Attestation Identity Key certificate
        if (aikCert == null)
            return new AuthenticationResult()
            {
                Error = "8.3 Verification 5 certInfo e TPM Attestation verification: Invalid Attestation Identity Key certificate, detected: aikCert = null."
            };

        // 8.3 Verification 5 certInfo f Note that the remaining fields in the "Standard Attestation Structure"
        // [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion are ignored.
        // These fields MAY be used as an input to risk engines.

        // 8.3 Verification 5 certInfo g Verify the sig is a valid signature over certInfo using the attestation public key
        // in aikCert with the algorithm specified in alg.
        var cpk = new CredentialPublicKey(aikCert, (int)algorithm);
        if (!cpk.Verify(certInfo.Raw, attStmtSignature.GetByteString()))
            return new AuthenticationResult()
            {
                Error = "8.3 Verification 5 certInfo g TPM Attestation verification: Invalid signature over certInfo using the aikCert attestation public key."
            };

        // 8.3 Verification 5 certInfo h Verify that aikCert meets the requirements in
        // 8.3.1 TPM Attestation Statement Certificate Requirements.

        // 8.3.1 b Subject field MUST be set to empty.
        if (aikCert.SubjectName.Name.Length > 0)
            return new AuthenticationResult()
            {
                Error = "8.3.1 b TPM Attestation verification: Invalid aikCert, subject must be empty."
            };

        // 8.3.1 c The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9.
        string tpmManufacturer = string.Empty;
        string tpmModel = string.Empty;
        string tpmVersion = string.Empty;
        try
        {
            (tpmManufacturer, tpmModel, tpmVersion) = AuthenticatorUtils.SANFromAttnCertExtensions(aikCert.Extensions);
        }
        catch (Exception ex)
        {
            return new AuthenticationResult()
            {
                Error = $"8.3.1 c TPM Attestation verification: Invalid Subject Alternative Name extension, Exception: {ex.Message}."
            };
        }

        // From https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf
        // "The issuer MUST include TPM manufacturer, TPM part number and TPM firmware version,
        // using the directoryName form within the GeneralName structure. The ASN.1 encoding is specified
        // in section 3.1.2 TPM Device Attributes. In accordance with RFC 5280[11], this extension MUST be
        // critical if subject is empty and SHOULD be non-critical if subject is non-empty"

        if (string.IsNullOrEmpty(tpmManufacturer) || string.IsNullOrEmpty(tpmModel) || string.IsNullOrEmpty(tpmVersion))
            return new AuthenticationResult()
            {
                Error = $"TPM Attestation verification: Invalid Subject Alternative Name extension, " +
                $"detected: TPMManufacturer = {tpmManufacturer}, TPMModel = {tpmModel}, TPMVersion = {tpmVersion}."
            };

        sanExtensionJson = JsonSerializer.Serialize(new { Manufacturer = tpmManufacturer, Model = tpmModel, Version = tpmVersion });

        // TCG TPM Vendor ID Registry - Not required by WebAuthn specification
        if (false == AuthenticatorUtils.TPMManufacturers.Contains(tpmManufacturer))
            return new AuthenticationResult()
            {
                Error = $"TPM Attestation verification: Invalid TPM manufacturer not found in Vendor ID Registry, " +
                $"detected: TPMManufacturer = {tpmManufacturer}."
            };

        // 8.3.1 d The Extended Key Usage extension MUST contain the OID 2.23.133.8.3 ("joint-iso-itu-t(2)
        // internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)").
        var EKU = AuthenticatorUtils.EKUFromAttnCertExts(aikCert.Extensions, "2.23.133.8.3");
        if (!EKU)
            return new AuthenticationResult()
            {
                Error = $"8.3.1 d TPM Attestation verification: Invalid aikCert, EKU missing tcg-kp-AIKCertificate OID."
            };

        // If successful, return implementation-specific values representing attestation type AttCA and attestation
        // trust path x5c.

        // If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates
        // or ECDAA-Issuer public keys) for that attestation type and attestation statement format fmt, from
        // a trusted source or from policy. For example, the FIDO Metadata Service [FIDOMetadataService]
        // provides one way to obtain such information, using the aaguid in the attestedCredentialData in authData.
        // TODO: Implement FIDO metadata service

    }
    catch (Exception ex)
    {
        return new AuthenticationResult()
        {
            Error = $"TPM Attestation verification: Exception {ex.Message}."
        };
    }

    return new AuthenticationResult
    {
        SanExtensionJson = sanExtensionJson,
        Verified = true
    };
}
3.2.9 Subject Alternative Name
Trusted Computing Group - TCG EK Credential Profile For TPM Family 2.0; Level 0 10 December 2018 Specification Version 2.1 Revision 13

This contains the alternative name of the entity associated with this certificate. The issuer MUST include TPM manufacturer, TPM part number and TPM firmware version, using the directoryNameform within the GeneralName structure. The ASN.1 encoding is specified in section 3.1.2 TPM Device Attributes. In accordance with RFC 5280[11], this extension MUST be critical if subject is empty and SHOULD be non-critical if subject is non-empty.

  • The TPM manufacturer identifies the manufacturer of the TPM. This value MUST be the vendor ID defined in the TCG Vendor ID Registry[3] . It MUST match the value reported by the command TPM2_GetCapability(property = TPM_PT_MANUFACTURER).
  • The TPM part number is encoded as a string and is manufacturer-specific. A manufacturer MUST provide a way to the user to retrieve the part number physically or logically. This information could be e.g. provided as part of the vendor string in the command TPM2_GetCapability(property = TPM_PT_VENDOR_STRING_x; x=1…4).
  • The TPM firmware version is a manufacturer-specific implementation version of the TPM. This value SHOULD match the version reported by the command TPM2_GetCapability (property = TPM_PT_FIRMWARE_VERSION_1).
NOTE This representation of subject alternative name is maintained to provide consistency with TPM 1.2

The AuthenticatorUtils. SANFromAttnCertExtensions function derived from the SANFromAttnCertExts at GitHub - passwordless-lib / fido2-net-lib / Src/ Fido2/ AttestationFormat/ Tpm.cs has been updated to use the MSDocs - AsnReader Class, new in .NET 5.0.

AuthenticatorUtils. SANFromAttnCertExtensions:
/// <summary>
/// 8.3.1 c TPM Attestation Statement Certificate Requirements
/// The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9.
/// </summary>
/// <param name="extensions"></param>
/// <returns><see cref="string, string, string" /></returns>
public static (string, string, string) SANFromAttnCertExtensions(X509ExtensionCollection extensions)
{
    string tpmManufacturer = string.Empty, tpmModel = string.Empty, tpmVersion = string.Empty;
            
    // UniversalTags https://www.obj-sys.com/asn1tutorial/node124.html 
    // General Name Structure
    //  SEQUENCE
    //      SET
    //          SEQUENCE
    //              OBJECT IDENTIFER tcg-at-tpmManufacturer (2.23.133.2.1)
    //              UTF8 STRING id:54434700 (TCG)
    //      SET
    //          SEQUENCE
    //              OBJECT IDENTIFER tcg-at-tpmModel (2.23.133.2.2)
    //              UTF8 STRING ABCDEF123456
    //      SET
    //          SEQUENCE
    //              OBJECT IDENTIFER tcg-at-tpmVersion (2.23.133.2.3)
    //              UTF8 STRING id:00010023

    var foundSAN = false;
    foreach (var extension in extensions)
    {
        if (extension.Oid.Value.Equals("2.5.29.17"))
        {
            if (0 == extension.RawData.Length)
                throw new InvalidOperationException("SAN missing from TPM attestation certificate");

            foundSAN = true;
            // Subject Alternative Name
            AsnReader asnReader = new(extension.RawData, AsnEncodingRules.DER);
            var sanTag = asnReader.PeekTag();
            if (!sanTag.IsConstructed)
                throw new InvalidOperationException("SANFromAttnCertExts, the extension tag is not constructed.");
            if (sanTag != Asn1Tag.Sequence)
                throw new InvalidOperationException("SANFromAttnCertExts, the extension tag is not a Sequence.");

            var sanSeqAsnReader = asnReader.ReadSequence();
            // General Name - new Asn1Tag(TagClass.ContextSpecific, 4)
            var sanGeneralNameAsnReader = sanSeqAsnReader.ReadSetOf(new Asn1Tag(TagClass.ContextSpecific, 4));
            var sanGeneralNameTag = sanGeneralNameAsnReader.PeekTag();
            if (sanGeneralNameTag.TagClass != TagClass.Universal || sanGeneralNameTag != Asn1Tag.Sequence)
                throw new InvalidOperationException("SANFromAttnCertExts, the general name tag is not a Sequence.");

            // SEQ
            var sanGeneralNameSeqAsnReader = sanGeneralNameAsnReader.ReadSequence();
            var sanGeneralNameSeqTag = sanGeneralNameSeqAsnReader.PeekTag();
            if (!sanGeneralNameSeqTag.IsConstructed || sanGeneralNameSeqTag != Asn1Tag.SetOf)
                throw new InvalidOperationException("SANFromAttnCertExts, the general name sequence tag is not a SetOf.");

            // SEQ - SET - Manufacturer Device Attributes
            var sanGeneralNameSeqFirstSetAsnReader = sanGeneralNameSeqAsnReader.ReadSetOf();
            var sanGeneralNameSeqFirstSetTag = sanGeneralNameSeqFirstSetAsnReader.PeekTag();
            if (!sanGeneralNameSeqFirstSetTag.IsConstructed || sanGeneralNameSeqFirstSetTag != Asn1Tag.Sequence)
                throw new InvalidOperationException("SANFromAttnCertExts, the first device attributes tag is not a Sequence.");
            // SEQ - SET - SEQ
            var sanGeneralNameSeqFirstSetSeqAsnReader = sanGeneralNameSeqFirstSetAsnReader.ReadSequence();
            var sanGeneralNameSeqFirstSetSeqOidTag = sanGeneralNameSeqFirstSetSeqAsnReader.PeekTag();
            if (sanGeneralNameSeqFirstSetSeqOidTag.IsConstructed ||
                sanGeneralNameSeqFirstSetSeqOidTag != Asn1Tag.ObjectIdentifier)
                throw new InvalidOperationException("SANFromAttnCertExts, the first device attributes oid tag is not a ObjectIdentifier.");
            // SEQ - SET - SEQ - OID
            var tpmManufacturerOid = sanGeneralNameSeqFirstSetSeqAsnReader.ReadObjectIdentifier();
            var sanGeneralNameSeqFirstSetSeqManufacturerTag = sanGeneralNameSeqFirstSetSeqAsnReader.PeekTag();
            if (sanGeneralNameSeqFirstSetSeqManufacturerTag != new Asn1Tag(TagClass.Universal, 12))
                throw new InvalidOperationException("SANFromAttnCertExts, the first device attributes manufacturer tag is not a Universal:12.");
            // SEQ - SET - SEQ - ManufacturerValue
            var tpmManufacturerValue = sanGeneralNameSeqFirstSetSeqAsnReader.ReadCharacterString(UniversalTagNumber.UTF8String);
            if (tpmManufacturerOid == "2.23.133.2.1")
                tpmManufacturer = tpmManufacturerValue;

            // SEQ - SET - Model Device Attributes
            var sanGeneralNameSeqSecondSetAsnReader = sanGeneralNameSeqAsnReader.ReadSetOf();
            // SEQ - SET - SEQ
            var sanGeneralNameSeqSecondSetSeqAsnReader = sanGeneralNameSeqSecondSetAsnReader.ReadSequence();
            var sanGeneralNameSeqSecondSetSeqOidTag = sanGeneralNameSeqSecondSetSeqAsnReader.PeekTag();
            if (sanGeneralNameSeqSecondSetSeqOidTag.IsConstructed ||
                sanGeneralNameSeqSecondSetSeqOidTag != Asn1Tag.ObjectIdentifier)
                throw new InvalidOperationException("SANFromAttnCertExts, the second device attributes oid tag is not a ObjectIdentifier.");
            // SEQ - SET - SEQ - OID
            var tpmModelOid = sanGeneralNameSeqSecondSetSeqAsnReader.ReadObjectIdentifier();
            var sanGeneralNameSeqSecondSetSeqModelTag = sanGeneralNameSeqSecondSetSeqAsnReader.PeekTag();
            if (sanGeneralNameSeqSecondSetSeqModelTag != new Asn1Tag(TagClass.Universal, 12))
                throw new InvalidOperationException("SANFromAttnCertExts, the second device attributes model tag is not a Universal:12.");
            // SEQ - SET - SEQ - ModelValue
            var tpmModelValue = sanGeneralNameSeqSecondSetSeqAsnReader.ReadCharacterString(UniversalTagNumber.UTF8String);
            if (tpmModelOid == "2.23.133.2.2")
                tpmModel = tpmModelValue;

            // SEQ - SET - Version Device Attributes
            var sanGeneralNameSeqThirdSetAsnReader = sanGeneralNameSeqAsnReader.ReadSetOf();
            // SEQ - SET - SEQ
            var sanGeneralNameSeqThirdSetSeqAsnReader = sanGeneralNameSeqThirdSetAsnReader.ReadSequence();
            var sanGeneralNameSeqThirdSetSeqOidTag = sanGeneralNameSeqThirdSetSeqAsnReader.PeekTag();
            if (sanGeneralNameSeqThirdSetSeqOidTag.IsConstructed ||
                sanGeneralNameSeqThirdSetSeqOidTag != Asn1Tag.ObjectIdentifier)
                throw new InvalidOperationException("SANFromAttnCertExts, the third device attributes oid tag is not a ObjectIdentifier.");
            // SEQ - SET - SEQ - OID
            var tpmVersionOid = sanGeneralNameSeqThirdSetSeqAsnReader.ReadObjectIdentifier();
            var sanGeneralNameSeqThirdSetSeqVersionTag = sanGeneralNameSeqThirdSetSeqAsnReader.PeekTag();
            if (sanGeneralNameSeqThirdSetSeqVersionTag != new Asn1Tag(TagClass.Universal, 12))
                throw new InvalidOperationException("SANFromAttnCertExts, the third device attributes version tag is not a Universal:12.");
            // SEQ - SET - SEQ - VersionValue
            var tpmVersionValue = sanGeneralNameSeqThirdSetSeqAsnReader.ReadCharacterString(UniversalTagNumber.UTF8String);
            if (tpmVersionOid == "2.23.133.2.3")
                tpmVersion = tpmVersionValue;

            break;
        }
    }

    if (!foundSAN)
        throw new InvalidOperationException("SAN missing from TPM attestation certificate");

    return (tpmManufacturer, tpmModel, tpmVersion);
}
Ken Haggerty
Created 08/16/21
Updated 08/16/21 04:01 GMT

Log In or Reset Quota to read more.

Article Tags:

FIDO Validation
Successfully completed. Thank you for contributing.
Contribute to enjoy content without advertisments.
Something went wrong. Please try again.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?