diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj index 8d8a9ab6..532efc29 100644 --- a/Demo/Demo.csproj +++ b/Demo/Demo.csproj @@ -7,9 +7,9 @@ - + - + diff --git a/Demo/Startup.cs b/Demo/Startup.cs index cbb1eeec..09a4f776 100644 --- a/Demo/Startup.cs +++ b/Demo/Startup.cs @@ -49,7 +49,6 @@ public void ConfigureServices(IServiceCollection services) options.TimestampDriftTolerance = Configuration.GetValue("fido2:timestampDriftTolerance"); options.MDSAccessKey = Configuration["fido2:MDSAccessKey"]; options.MDSCacheDirPath = Configuration["fido2:MDSCacheDirPath"]; - options.RequireValidAttestationRoot = Configuration.GetValue("fido2:requireValidAttestationRoot"); }) .AddCachedMetadataService(config => { diff --git a/Demo/TestController.cs b/Demo/TestController.cs index b21a1b2b..f4e9af8f 100644 --- a/Demo/TestController.cs +++ b/Demo/TestController.cs @@ -36,7 +36,7 @@ public TestController(IOptions fido2Configuration) { ServerDomain = fido2Configuration.Value.ServerDomain, ServerName = fido2Configuration.Value.ServerName, - Origin = _origin + Origin = _origin, }, ConformanceTesting.MetadataServiceInstance( System.IO.Path.Combine(fido2Configuration.Value.MDSCacheDirPath, @"Conformance"), _origin) diff --git a/Demo/appsettings.json b/Demo/appsettings.json index 8a4e8341..627e3bd7 100644 --- a/Demo/appsettings.json +++ b/Demo/appsettings.json @@ -4,7 +4,6 @@ "origin": "https://localhost:44329", "timestampDriftTolerance": 300000, "MDSAccessKey": null, - "requireValidAttestationRoot": false }, "Logging": { "IncludeScopes": false, diff --git a/ExternalLibs/AsnElt/AsnElt/AsnIO.cs b/ExternalLibs/AsnElt/AsnElt/AsnIO.cs index 04883ece..bc67eea8 100644 --- a/ExternalLibs/AsnElt/AsnElt/AsnIO.cs +++ b/ExternalLibs/AsnElt/AsnElt/AsnIO.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -125,7 +125,7 @@ public static byte[] FindBER(byte[] buf, int r = str.IndexOf((char)10, p) + 1; int px = str.IndexOf('-', p); if (px > 0 && px < r && r > 0 && r <= q) { - pemType = string.Copy(str.Substring(p, px - p)); + pemType = new StringBuilder(str.Substring(p, px - p)).ToString(); str = str.Substring(r, q - r); } } diff --git a/Src/Fido2.AspNet/Fido2.AspNet.csproj b/Src/Fido2.AspNet/Fido2.AspNet.csproj index 4aea8708..0ebc1de7 100644 --- a/Src/Fido2.AspNet/Fido2.AspNet.csproj +++ b/Src/Fido2.AspNet/Fido2.AspNet.csproj @@ -8,13 +8,13 @@ - - - - + + + + - - + + diff --git a/Src/Fido2.Models/Fido2Configuration.cs b/Src/Fido2.Models/Fido2Configuration.cs index 044eab1e..dda081d2 100644 --- a/Src/Fido2.Models/Fido2Configuration.cs +++ b/Src/Fido2.Models/Fido2Configuration.cs @@ -15,11 +15,6 @@ public class Fido2Configuration /// public int TimestampDriftTolerance { get; set; } = 0; //Pretty sure 0 will never work - need a better default? - /// - /// When checking attestation, require the attestation to chain to a known root - /// - public bool RequireValidAttestationRoot { get; set; } = false; - /// /// The size of the challenges sent to the client /// diff --git a/Src/Fido2/AttestationFormat/AndroidKey.cs b/Src/Fido2/AttestationFormat/AndroidKey.cs index 13b62c45..f1ce797e 100644 --- a/Src/Fido2/AttestationFormat/AndroidKey.cs +++ b/Src/Fido2/AttestationFormat/AndroidKey.cs @@ -141,13 +141,14 @@ public static bool IsPurposeSign(byte[] attExtBytes) public override void Verify() { - // Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields + // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields + // (handled in base class) if (0 == attStmt.Keys.Count || 0 == attStmt.Values.Count) throw new Fido2VerificationException("Attestation format android-key must have attestation statement"); if (null == Sig || CBORType.ByteString != Sig.Type || 0 == Sig.GetByteString().Length) throw new Fido2VerificationException("Invalid android-key attestation signature"); - // 2a. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash + // 2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash // using the attestation public key in attestnCert with the algorithm specified in alg if (null == X5c || CBORType.Array != X5c.Type || 0 == X5c.Count) throw new Fido2VerificationException("Malformed x5c in android-key attestation"); @@ -185,16 +186,15 @@ public override void Verify() if (true != androidKeyPubKey.VerifyData(Data, ecsig, CryptoUtils.algMap[Alg.AsInt32()])) throw new Fido2VerificationException("Invalid android key attestation signature"); - // Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData. + // 3. Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData. if (true != AuthData.AttestedCredentialData.CredentialPublicKey.Verify(Data, Sig.GetByteString())) throw new Fido2VerificationException("Incorrect credentialPublicKey in android key attestation"); - // Verify that in the attestation certificate extension data: + // 4. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash var attExtBytes = AttestationExtensionBytes(androidKeyCert.Extensions); if (null == attExtBytes) throw new Fido2VerificationException("Android key attestation certificate contains no AttestationRecord extension"); - // 1. The value of the attestationChallenge field is identical to clientDataHash. try { var attestationChallenge = GetAttestationChallenge(attExtBytes); @@ -205,15 +205,18 @@ public override void Verify() { throw new Fido2VerificationException("Malformed android key AttestationRecord extension verifying android key attestation certificate extension"); } - // 2. The AuthorizationList.allApplications field is not present, since PublicKeyCredential MUST be bound to the RP ID. + + // 5. Verify the following using the appropriate authorization list from the attestation certificate extension data + + // 5a. The AuthorizationList.allApplications field is not present, since PublicKeyCredential MUST be bound to the RP ID if (true == FindAllApplicationsField(attExtBytes)) throw new Fido2VerificationException("Found all applications field in android key attestation certificate extension"); - // 3. The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED ( which == 0). + // 5bi. The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED ( which == 0). if (false == IsOriginGenerated(attExtBytes)) throw new Fido2VerificationException("Found origin field not set to KM_ORIGIN_GENERATED in android key attestation certificate extension"); - // 4. The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN (which == 2). + // 5bii. The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN (which == 2). if (false == IsPurposeSign(attExtBytes)) throw new Fido2VerificationException("Found purpose field not set to KM_PURPOSE_SIGN in android key attestation certificate extension"); } diff --git a/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs b/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs index 967131aa..55be6b41 100644 --- a/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs +++ b/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs @@ -37,13 +37,14 @@ private X509Certificate2 GetX509Certificate(string certString) public override void Verify() { - // Verify that attStmt is valid CBOR conforming to the syntax defined above and perform + // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform // CBOR decoding on it to extract the contained fields + // (handled in base class) if ((CBORType.TextString != attStmt["ver"].Type) || (0 == attStmt["ver"].AsString().Length)) throw new Fido2VerificationException("Invalid version in SafetyNet data"); - // Verify that response is a valid SafetyNet response of version ver + // 2. Verify that response is a valid SafetyNet response of version ver var ver = attStmt["ver"].AsString(); if ((CBORType.ByteString != attStmt["response"].Type) || @@ -140,7 +141,7 @@ public override void Verify() throw new Fido2VerificationException(string.Format("SafetyNet timestampMs must be present and between one minute ago and now, got: {0}", timestampMs.ToString())); } - // Verify that the nonce in the response is identical to the SHA-256 hash of the concatenation of authenticatorData and clientDataHash + // 3. Verify that the nonce in the response is identical to the SHA-256 hash of the concatenation of authenticatorData and clientDataHash if ("" == nonce) throw new Fido2VerificationException("Nonce value not found in SafetyNet attestation"); @@ -167,15 +168,17 @@ public override void Verify() ); } - // Verify that the attestation certificate is issued to the hostname "attest.android.com" + // 4. Let attestationCert be the attestation certificate var subject = certs[0].GetNameInfo(X509NameType.DnsName, false); + + // 5. Verify that the attestation certificate is issued to the hostname "attest.android.com" if (false == ("attest.android.com").Equals(subject)) throw new Fido2VerificationException(string.Format("SafetyNet attestation cert DnsName invalid, want {0}, got {1}", "attest.android.com", subject)); + // 6. Verify that the ctsProfileMatch attribute in the payload of response is true if (null == ctsProfileMatch) throw new Fido2VerificationException("SafetyNet response ctsProfileMatch missing"); - - // Verify that the ctsProfileMatch attribute in the payload of response is true + if (true != ctsProfileMatch) throw new Fido2VerificationException("SafetyNet response ctsProfileMatch false"); } diff --git a/Src/Fido2/AttestationFormat/AttestationFormat.cs b/Src/Fido2/AttestationFormat/AttestationFormat.cs index 1b8757dd..9352baf7 100644 --- a/Src/Fido2/AttestationFormat/AttestationFormat.cs +++ b/Src/Fido2/AttestationFormat/AttestationFormat.cs @@ -81,6 +81,34 @@ internal static int U2FTransportsFromAttnCert(X509ExtensionCollection exts) } return u2ftransports; } + + internal bool ValidateTrustChain(X509Certificate2[] trustPath, X509Certificate2[] attestationRootCertificates) + { + foreach (var attestationRootCert in attestationRootCertificates) + { + var chain = new X509Chain(); + chain.ChainPolicy.ExtraStore.Add(attestationRootCert); + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + if (trustPath.Length > 1) + { + foreach (var cert in trustPath.Skip(1).Reverse()) + { + chain.ChainPolicy.ExtraStore.Add(cert); + } + } + var valid = chain.Build(trustPath[0]); + + // because we are using AllowUnknownCertificateAuthority we have to verify that the root matches ourselves + var chainRoot = chain.ChainElements[chain.ChainElements.Count - 1].Certificate; + valid = valid && chainRoot.RawData.SequenceEqual(attestationRootCert.RawData); + + if (true == valid) + return true; + } + return false; + } + public abstract void Verify(); } } diff --git a/Src/Fido2/AttestationFormat/FidoU2f.cs b/Src/Fido2/AttestationFormat/FidoU2f.cs index 3b8f87eb..f210e1c6 100644 --- a/Src/Fido2/AttestationFormat/FidoU2f.cs +++ b/Src/Fido2/AttestationFormat/FidoU2f.cs @@ -11,11 +11,10 @@ namespace Fido2NetLib.AttestationFormat internal class FidoU2f : AttestationFormat { private readonly IMetadataService _metadataService; - private readonly bool _requireValidAttestationRoot; - public FidoU2f(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash, IMetadataService metadataService, bool requireValidAttestationRoot) : base(attStmt, authenticatorData, clientDataHash) + + public FidoU2f(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash, IMetadataService metadataService) : base(attStmt, authenticatorData, clientDataHash) { _metadataService = metadataService; - _requireValidAttestationRoot = requireValidAttestationRoot; } public override void Verify() { @@ -23,64 +22,25 @@ public override void Verify() if (0 != AuthData.AttestedCredentialData.AaGuid.CompareTo(Guid.Empty)) throw new Fido2VerificationException("Aaguid was not empty parsing fido-u2f atttestation statement"); + // https://www.w3.org/TR/webauthn/#fido-u2f-attestation // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields. + // (handled in base class) if (null == X5c || CBORType.Array != X5c.Type || X5c.Count != 1) throw new Fido2VerificationException("Malformed x5c in fido - u2f attestation"); - // 2a. the attestation certificate attestnCert MUST be the first element in the array + // 2a. Check that x5c has exactly one element and let attCert be that element. if (null == X5c.Values || 0 == X5c.Values.Count || CBORType.ByteString != X5c.Values.First().Type || 0 == X5c.Values.First().GetByteString().Length) throw new Fido2VerificationException("Malformed x5c in fido-u2f attestation"); - var cert = new X509Certificate2(X5c.Values.First().GetByteString()); + var attCert = new X509Certificate2(X5c.Values.First().GetByteString()); // TODO : Check why this variable isn't used. Remove it or use it. - var u2ftransports = U2FTransportsFromAttnCert(cert.Extensions); - - var aaguid = AaguidFromAttnCertExts(cert.Extensions); - - if (null != _metadataService && null != aaguid) - { - var guidAaguid = AttestedCredentialData.FromBigEndian(aaguid); - var entry = _metadataService.GetEntry(guidAaguid); - - if (null != entry && null != entry.MetadataStatement) - { - if (entry.Hash != entry.MetadataStatement.Hash) - throw new Fido2VerificationException("Authenticator metadata statement has invalid hash"); - var root = new X509Certificate2(Convert.FromBase64String(entry.MetadataStatement.AttestationRootCertificates.FirstOrDefault())); - - var chain = new X509Chain(); - chain.ChainPolicy.ExtraStore.Add(root); - chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - - var valid = chain.Build(cert); - - if (// the root cert has exactly one status listed against it - chain.ChainElements[chain.ChainElements.Count - 1].ChainElementStatus.Length == 1 && - // and that that status is a status of exactly UntrustedRoot - chain.ChainElements[chain.ChainElements.Count - 1].ChainElementStatus[0].Status == X509ChainStatusFlags.UntrustedRoot) - { - valid = true; - } - - if (_requireValidAttestationRoot) - { - // because we are using AllowUnknownCertificateAuthority we have to verify that the root matches ourselves - var chainRoot = chain.ChainElements[chain.ChainElements.Count - 1].Certificate; - valid = valid && chainRoot.RawData.SequenceEqual(root.RawData); - } - - if (false == valid) - { - throw new Fido2VerificationException("Invalid certificate chain in U2F attestation"); - } - } - } + var u2ftransports = U2FTransportsFromAttnCert(attCert.Extensions); // 2b. If certificate public key is not an Elliptic Curve (EC) public key over the P-256 curve, terminate this algorithm and return an appropriate error - var pubKey = cert.GetECDsaPublicKey(); + var pubKey = attCert.GetECDsaPublicKey(); var keyParams = pubKey.ExportParameters(false); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -88,18 +48,23 @@ public override void Verify() if (!keyParams.Curve.Oid.FriendlyName.Equals(ECCurve.NamedCurves.nistP256.Oid.FriendlyName)) throw new Fido2VerificationException("Attestation certificate public key is not an Elliptic Curve (EC) public key over the P-256 curve"); } - else { if (!keyParams.Curve.Oid.Value.Equals(ECCurve.NamedCurves.nistP256.Oid.Value)) throw new Fido2VerificationException("Attestation certificate public key is not an Elliptic Curve (EC) public key over the P-256 curve"); } + // 3. Extract the claimed rpIdHash from authenticatorData, and the claimed credentialId and credentialPublicKey from authenticatorData - // see rpIdHash, credentialId, and credentialPublicKey variables + // see rpIdHash, credentialId, and credentialPublicKey members of base class AuthenticatorData (AuthData) - // 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of [RFC8152]) to CTAP1/U2F public Key format + // 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of [RFC8152]) to CTAP1/U2F public Key format (Raw ANSI X9.62 public key format) + // 4a. Let x be the value corresponding to the "-2" key (representing x coordinate) in credentialPublicKey, and confirm its size to be of 32 bytes. If size differs or "-2" key is not found, terminate this algorithm and return an appropriate error var x = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.X)].GetByteString(); + + // 4b. Let y be the value corresponding to the "-3" key (representing y coordinate) in credentialPublicKey, and confirm its size to be of 32 bytes. If size differs or "-3" key is not found, terminate this algorithm and return an appropriate error var y = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.Y)].GetByteString(); + + // 4c.Let publicKeyU2F be the concatenation 0x04 || x || y var publicKeyU2F = new byte[1] { 0x4 }.Concat(x).Concat(y).ToArray(); // 5. Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F) @@ -130,6 +95,31 @@ public override void Verify() if (true != pubKey.VerifyData(verificationData, ecsig, hashAlg)) throw new Fido2VerificationException("Invalid fido-u2f attestation signature"); + + // 7. Optionally, inspect x5c and consult externally provided knowledge to determine whether attStmt conveys a Basic or AttCA attestation + var trustPath = X5c.Values + .Select(x => new X509Certificate2(x.GetByteString())) + .ToArray(); + + var aaguid = AaguidFromAttnCertExts(attCert.Extensions); + + if (null != _metadataService && null != aaguid) + { + var guidAaguid = AttestedCredentialData.FromBigEndian(aaguid); + var entry = _metadataService.GetEntry(guidAaguid); + + if (null != entry && null != entry.MetadataStatement) + { + var attestationRootCertificates = entry.MetadataStatement.AttestationRootCertificates + .Select(x => new X509Certificate2(Convert.FromBase64String(x))) + .ToArray(); + + if (false == ValidateTrustChain(trustPath, attestationRootCertificates)) + { + throw new Fido2VerificationException("Invalid certificate chain in U2F attestation"); + } + } + } } } } diff --git a/Src/Fido2/AttestationFormat/Packed.cs b/Src/Fido2/AttestationFormat/Packed.cs index c1f3a329..154a7872 100644 --- a/Src/Fido2/AttestationFormat/Packed.cs +++ b/Src/Fido2/AttestationFormat/Packed.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Fido2NetLib.Objects; using PeterO.Cbor; @@ -19,19 +18,19 @@ internal enum UndesiredAuthenticatorStatus internal enum MetadataAttestationType { ATTESTATION_BASIC_FULL = 0x3e07, - ATTESTATION_BASIC_SURROGATE = 0x3e08 + ATTESTATION_BASIC_SURROGATE = 0x3e08, + ATTESTATION_ATTCA = 0x3e0a, + ATTESTATION_HELLO = 0x3e10 } internal class Packed : AttestationFormat { private readonly IMetadataService _metadataService; - private readonly bool _requireValidAttestationRoot; - public Packed(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash, IMetadataService metadataService, bool requireValidAttestationRoot) + public Packed(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash, IMetadataService metadataService) : base(attStmt, authenticatorData, clientDataHash) { _metadataService = metadataService; - _requireValidAttestationRoot = requireValidAttestationRoot; } public static bool IsValidPackedAttnCertSubject(string attnCertSubj) @@ -49,7 +48,7 @@ public static bool IsValidPackedAttnCertSubject(string attnCertSubj) public override void Verify() { - // Verify that attStmt is valid CBOR conforming to the syntax defined above and + // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and // perform CBOR decoding on it to extract the contained fields. if (0 == attStmt.Keys.Count || 0 == attStmt.Values.Count) throw new Fido2VerificationException("Attestation format packed must have attestation statement"); @@ -60,7 +59,7 @@ public override void Verify() if (null == Alg || true != Alg.IsNumber) throw new Fido2VerificationException("Invalid packed attestation algorithm"); - // If x5c is present, this indicates that the attestation type is not ECDAA + // 2. If x5c is present, this indicates that the attestation type is not ECDAA if (null != X5c) { if (CBORType.Array != X5c.Type || 0 == X5c.Count || null != EcdaaKeyId) @@ -93,31 +92,35 @@ public override void Verify() throw new Fido2VerificationException("Invalid full packed signature"); // Verify that attestnCert meets the requirements in https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements - // 2b. Version MUST be set to 3 + // 2bi. Version MUST be set to 3 if (3 != attestnCert.Version) throw new Fido2VerificationException("Packed x5c attestation certificate not V3"); - // Subject field MUST contain C, O, OU, CN + // 2bii. Subject field MUST contain C, O, OU, CN // OU must match "Authenticator Attestation" if (true != IsValidPackedAttnCertSubject(attestnCert.Subject)) throw new Fido2VerificationException("Invalid attestation cert subject"); - // 2c. If the related attestation root certificate is used for multiple authenticator models, + // 2biii. 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 // verify that the value of this extension matches the aaguid in authenticatorData var aaguid = AaguidFromAttnCertExts(attestnCert.Extensions); + + // 2biiii. The Basic Constraints extension MUST have the CA component set to false + if (IsAttnCertCACert(attestnCert.Extensions)) + throw new Fido2VerificationException("Attestion certificate has CA cert flag present"); + + // 2c. If attestnCert 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 if (aaguid != null) { if (0 != AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData.AaGuid)) throw new Fido2VerificationException("aaguid present in packed attestation cert exts but does not match aaguid from authData"); } - // 2d. The Basic Constraints extension MUST have the CA component set to false - if (IsAttnCertCACert(attestnCert.Extensions)) - throw new Fido2VerificationException("Attestion certificate has CA cert flag present"); // id-fido-u2f-ce-transports var u2ftransports = U2FTransportsFromAttnCert(attestnCert.Extensions); + // 2d. Optionally, inspect x5c and consult externally provided knowledge to determine whether attStmt conveys a Basic or AttCA attestation var trustPath = X5c.Values .Select(x => new X509Certificate2(x.GetByteString())) .ToArray(); @@ -131,28 +134,11 @@ public override void Verify() // If the authenticator is listed as in the metadata as one that should produce a basic full attestation, build and verify the chain if (entry?.MetadataStatement?.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_BASIC_FULL) ?? false) { - var root = new X509Certificate2(Convert.FromBase64String(entry.MetadataStatement.AttestationRootCertificates.FirstOrDefault())); - var chain = new X509Chain(); - chain.ChainPolicy.ExtraStore.Add(root); - chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; - if (trustPath.Length > 1) - { - foreach (var cert in trustPath.Skip(1).Reverse()) - { - chain.ChainPolicy.ExtraStore.Add(cert); - } - } - var valid = chain.Build(trustPath[0]); - - if (_requireValidAttestationRoot) - { - // because we are using AllowUnknownCertificateAuthority we have to verify that the root matches ourselves - var chainRoot = chain.ChainElements[chain.ChainElements.Count - 1].Certificate; - valid = valid && chainRoot.RawData.SequenceEqual(root.RawData); - } + var attestationRootCertificates = entry.MetadataStatement.AttestationRootCertificates + .Select(x => new X509Certificate2(Convert.FromBase64String(x))) + .ToArray(); - if (false == valid) + if (false == ValidateTrustChain(trustPath, attestationRootCertificates)) { throw new Fido2VerificationException("Invalid certificate chain in packed attestation"); } @@ -173,28 +159,27 @@ public override void Verify() } } - // If ecdaaKeyId is present, then the attestation type is ECDAA + // 3. If ecdaaKeyId is present, then the attestation type is ECDAA else if (null != EcdaaKeyId) { - // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash + // 3a. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash // using ECDAA-Verify with ECDAA-Issuer public key identified by ecdaaKeyId // https://www.w3.org/TR/webauthn/#biblio-fidoecdaaalgorithm throw new Fido2VerificationException("ECDAA is not yet implemented"); - // If successful, return attestation type ECDAA and attestation trust path ecdaaKeyId. - //attnType = AttestationType.ECDAA; - //trustPath = ecdaaKeyId; + // 3b. If successful, return attestation type ECDAA and attestation trust path ecdaaKeyId. + // attnType = AttestationType.ECDAA; + // trustPath = ecdaaKeyId; } - // If neither x5c nor ecdaaKeyId is present, self attestation is in use + // 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use else { - // Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData + // 4a. Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData if (false == AuthData.AttestedCredentialData.CredentialPublicKey.IsSameAlg((COSE.Algorithm)Alg.AsInt32())) throw new Fido2VerificationException("Algorithm mismatch between credential public key and authenticator data in self attestation statement"); - // Verify that sig is a valid signature over the concatenation of authenticatorData and + // 4b. Verify that sig is a valid signature over the concatenation of authenticatorData and // clientDataHash using the credential public key with alg - if (true != AuthData.AttestedCredentialData.CredentialPublicKey.Verify(Data, Sig.GetByteString())) throw new Fido2VerificationException("Failed to validate signature"); } diff --git a/Src/Fido2/AttestationFormat/Tpm.cs b/Src/Fido2/AttestationFormat/Tpm.cs index 9f76d6c2..265ba207 100644 --- a/Src/Fido2/AttestationFormat/Tpm.cs +++ b/Src/Fido2/AttestationFormat/Tpm.cs @@ -11,420 +11,53 @@ namespace Fido2NetLib.AttestationFormat { internal class Tpm : AttestationFormat { - private readonly bool _requireValidAttestationRoot; + private readonly IMetadataService _metadataService; - public Tpm(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash, bool requireValidAttestationRoot) + public Tpm(CBORObject attStmt, byte[] authenticatorData, byte[] clientDataHash, IMetadataService metadataService) : base(attStmt, authenticatorData, clientDataHash) { - _requireValidAttestationRoot = requireValidAttestationRoot; + _metadataService = metadataService; } - public static readonly Dictionary TPMManufacturerRootMap = new Dictionary + public static readonly List TPMManufacturers = new List { - {"id:FFFFF1D0", new X509Certificate2[]{ - new X509Certificate2(Convert.FromBase64String( - "MIIGSDCCBDCgAwIBAgIJANLAiKUvCLEqMA0GCSqGSIb3DQEBCwUAMIG/MQswCQYD" + - "VQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDEWMBQGA1UE" + - "CgwNRklETyBBbGxpYW5jZTEMMAoGA1UECwwDQ1dHMTYwNAYDVQQDDC1GSURPIEZh" + - "a2UgVFBNIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTgxMTAvBgkqhkiG" + - "9w0BCQEWImNvbmZvcm1hbmNlLXRvb2xzQGZpZG9hbGxpYW5jZS5vcmcwHhcNMTgw" + - "NTI5MTQzMjU5WhcNNDUxMDE0MTQzMjU5WjCBvzELMAkGA1UEBhMCVVMxCzAJBgNV" + - "BAgMAk1ZMRIwEAYDVQQHDAlXYWtlZmllbGQxFjAUBgNVBAoMDUZJRE8gQWxsaWFu" + - "Y2UxDDAKBgNVBAsMA0NXRzE2MDQGA1UEAwwtRklETyBGYWtlIFRQTSBSb290IENl" + - "cnRpZmljYXRlIEF1dGhvcml0eSAyMDE4MTEwLwYJKoZIhvcNAQkBFiJjb25mb3Jt" + - "YW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMIICIjANBgkqhkiG9w0BAQEFAAOC" + - "Ag8AMIICCgKCAgEAyCtbMw6ckWpylo7ZCboe3khforOB1eUb0DZg4mLsf460nKnZ" + - "JbztZh/3qqLQTUBEb1kxeGW31QiJ5UoiAcPAoo9aHIADVfjJEPvr865fOqt85f/q" + - "O2qsF6ZjVpNk1/zQRP4xPRLZPhawQvZsnmV20vteV8K4KL9kWw/Yjo+m9LKt90OM" + - "1tf7+F/uh1alocxc+WPmfpXxSHDfySTvnq6m8cQySAn3LyjAg1pYnT4P9QC0HbNK" + - "z0KoL+EFylsmvps7wjAeRqNetu0BdmvBLtYC7AMxGpCzAuF5tYl+9/hWMI544QGn" + - "ZrQnhIXfq704brI04NsUtBmCfZ5rEuc+Gzrz/asAPo6JSXyj9OSq+yPiWXen3g98" + - "/BI7f7gZoV6rqrdCojkFlWZVBJgWgHio0JEy7OB4RPO0SIKichjKbvIyTcE+J7oP" + - "Cgz5UCjBbSo94sJ8hs35W2y8aVYriRZ94z5w9IM/T/tZLkZDOzI03uot+PO2d1xX" + - "K8YQ/QVzKnNcxXeve9l3x/CNzgknbp+IiL/NH509Zcn0YiGLfInHLPpEQ3p1PSU5" + - "vtx+mWWpoRWvzwYpQD907skC9exZjm16F1ZKu+cvboeA1AHAHC/tE26Lxema5F/p" + - "KXVFSu2XqK8JS6hO3EauL5ONaWxVIsQX4CIOxFdvS6mdmp8n+9SWr9FOuSMCAwEA" + - "AaNFMEMwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0O" + - "BBYEFEMRFpma7p1QN8JP/uJbFckJMz8yMA0GCSqGSIb3DQEBCwUAA4ICAQBlMRmP" + - "NnAWOxnkQ5L/rFfrNxelNv65g1igxu9ADtLpTvgoDauoT3JdaIeano3jbXxEvOPK" + - "oI0dwDmioYGZoZcTrMFtCLuygotZHVn+m5Lz1M+22mnR/REhrWzifDzqttEy0N1a" + - "OOATF3cc2RMupn1LUci5Tl+Mlx5QzfOL36UduWO6Mt3wRBmMua7vRbxU3GScIUTW" + - "aWUMT3MUdlYIITkMXon5S4zXvc2Z/xn98/Lj0GR/h3VnDlg+mZnIyKdHBJ/racTD" + - "FH1kvlU4LEvY9K6yJIi7GQvlN0JvsL7XDetnOENJrRrq5N8xSu9X9puNFaFBufuA" + - "NmE0EsF7MMybD4YfIhBWE4qSPEgaoa136Paf/pCPXz/BwSTlmCXoRJeybfgJsNoj" + - "K72heXSpJrwGI2RPKRg0UJ2Bw7GRkzubuaAB9apvBVurCngZ8n28bkCG+12v0qMh" + - "UwYKdlPP5mozrxK7shg+y9LBNO2x3b85Uu9hWZl3xys4P7hOtoG3y0IN05aCSvou" + - "l3YCmR+NJ5aK1PePq2qvSaWfBZyZBwNFlWTZb+pxLjXwul+m2Pg/9bMp0oPK7XZt" + - "Ar+IZ5HN+Tld2RL142d5ElizNNpiGDXlFTIqg7YzodejASdKNtn/S1z8yzHUuHE6" + - "ogcYf5q/tvBkp/uRH9i6L+xUSMoGHkXP2Bj9AQ==" - ))}}, - {"id:414D4400", new X509Certificate2[]{ - new X509Certificate2(Convert.FromBase64String( - "MIIEiDCCA3CgAwIBAgIQJk05ojzrXVtJ1hAETuvRITANBgkqhkiG9w0BAQsFADB2" + - "MRQwEgYDVQQLEwtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxEjAQBgNVBAcTCVN1" + - "bm55dmFsZTELMAkGA1UECBMCQ0ExHzAdBgNVBAoTFkFkdmFuY2VkIE1pY3JvIERl" + - "dmljZXMxDzANBgNVBAMTBkFNRFRQTTAeFw0xNDEwMjMxNDM0MzJaFw0zOTEwMjMx" + - "NDM0MzJaMHYxFDASBgNVBAsTC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzESMBAG" + - "A1UEBxMJU3Vubnl2YWxlMQswCQYDVQQIEwJDQTEfMB0GA1UEChMWQWR2YW5jZWQg" + - "TWljcm8gRGV2aWNlczEPMA0GA1UEAxMGQU1EVFBNMIIBIjANBgkqhkiG9w0BAQEF" + - "AAOCAQ8AMIIBCgKCAQEAssnOAYu5nRflQk0bVtsTFcLSAMx9odZ4Ey3n6/MA6FD7" + - "DECIE70RGZgaRIID0eb+dyX3znMrp1TS+lD+GJSw7yDJrKeU4it8cMLqFrqGm4SE" + - "x/X5GBa11sTmL4i60pJ5nDo2T69OiJ+iqYzgBfYJLqHQaeSRN6bBYyn3w1H4JNzP" + - "DNvqKHvkPfYewHjUAFJAI1dShYO8REnNCB8eeolj375nymfAAZzgA8v7zmFX/1tV" + - "LCy7Mm6n7zndT452TB1mek9LC5LkwlnyABwaN2Q8LV4NWpIAzTgr55xbU5VvgcIp" + - "w+/qcbYHmqL6ZzCSeE1gRKQXlsybK+W4phCtQfMgHQIDAQABo4IBEDCCAQwwDgYD" + - "VR0PAQH/BAQDAgEGMCMGCSsGAQQBgjcVKwQWBBRXjFRfeWlRQhIhpKV4rNtfaC+J" + - "yDAdBgNVHQ4EFgQUV4xUX3lpUUISIaSleKzbX2gvicgwDwYDVR0TAQH/BAUwAwEB" + - "/zA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9mdHBtLmFtZC5j" + - "b20vcGtpL29jc3AwLAYDVR0fBCUwIzAhoB+gHYYbaHR0cDovL2Z0cG0uYW1kLmNv" + - "bS9wa2kvY3JsMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRw" + - "czovL2Z0cG0uYW1kLmNvbS9wa2kvY3BzMA0GCSqGSIb3DQEBCwUAA4IBAQCWB9yA" + - "oYYIt5HRY/OqJ5LUacP6rNmsMfPUDTcahXB3iQmY8HpUoGB23lhxbq+kz3vIiGAc" + - "UdKHlpB/epXyhABGTcJrNPMfx9akLqhI7WnMCPBbHDDDzKjjMB3Vm65PFbyuqbLu" + - "jN/sN6kNtc4hL5r5Pr6Mze5H9WXBo2F2Oy+7+9jWMkxNrmUhoUUrF/6YsajTGPeq" + - "7r+i6q84W2nJdd+BoQQv4sk5GeuN2j2u4k1a8DkRPsVPc2I9QTtbzekchTK1GCXW" + - "ki3DKGkZUEuaoaa60Kgw55Q5rt1eK7HKEG5npmR8aEod7BDLWy4CMTNAWR5iabCW" + - "/KX28JbJL6Phau9j"))} }, - {"id:41544D4C", new X509Certificate2[]{ - new X509Certificate2(Convert.FromBase64String( - "MIICKzCCAdCgAwIBAgIUcD8hGhUZbtWr/R0SMo4rBkmgVHgwCgYIKoZIzj0EAwIw" + - "czELMAkGA1UEBhMCVVMxETAPBgNVBAgTCENvbG9yYWRvMRkwFwYDVQQHExBDb2xv" + - "cmFkbyBTcHJpbmdzMQ4wDAYDVQQKEwVBdG1lbDEmMCQGA1UEAxMdQXRtZWwgVFBN" + - "IFJvb3QgU2lnbmluZyBNb2R1bGUwHhcNMTAxMjMxMDAwMDAwWhcNNDAxMjMxMDAw" + - "MDAwWjBzMQswCQYDVQQGEwJVUzERMA8GA1UECBMIQ29sb3JhZG8xGTAXBgNVBAcT" + - "EENvbG9yYWRvIFNwcmluZ3MxDjAMBgNVBAoTBUF0bWVsMSYwJAYDVQQDEx1BdG1l" + - "bCBUUE0gUm9vdCBTaWduaW5nIE1vZHVsZTBZMBMGByqGSM49AgEGCCqGSM49AwEH" + - "A0IABH2Mc2ZzwulHWuF8a+EMpey51ZrMiF78oQywMFzGCmV4CmfpSQVpJqw23np8" + - "QveCQOt7n/zsBMRsqk1bsAfYKwqjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB" + - "Af8EBTADAQH/MB0GA1UdDgQWBBQx0F0Qba/k7RndCq/TW+oio3gcVzAKBggqhkjO" + - "PQQDAgNJADBGAiEAyNu4sBDbRURcVGhKysdHGYidk5H2Bia+yo5mDryJ3hMCIQCs" + - "lDkUE4T1jHHwzSxca6KCXzgtpyui78G742CdYm9W5Q=="))} }, - //{"id:4252434D", "BRCM"}, - //{"id:48504500", "HPE"}, - //{"id:49424d00", "IBM"}, - {"id:49465800", - new X509Certificate2[]{ - new X509Certificate2(Convert.FromBase64String( - "MIIEUDCCAzigAwIBAgIQRyQE4N8hgD99IM2HSOq5WjANBgkqhkiG9w0BAQUFADCB" + - "ljELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTswOQYDVQQL" + - "EzJWZXJpU2lnbiBUcnVzdGVkIENvbXB1dGluZyBDZXJ0aWZpY2F0aW9uIEF1dGhv" + - "cml0eTExMC8GA1UEAxMoVmVyaVNpZ24gVHJ1c3RlZCBQbGF0Zm9ybSBNb2R1bGUg" + - "Um9vdCBDQTAeFw0wNTEwMjUwMDAwMDBaFw0zMDEwMjQyMzU5NTlaMG0xCzAJBgNV" + - "BAYTAkRFMRAwDgYDVQQIEwdCYXZhcmlhMSEwHwYDVQQKExhJbmZpbmVvbiBUZWNo" + - "bm9sb2dpZXMgQUcxDDAKBgNVBAsTA0FJTTEbMBkGA1UEAxMSSUZYIFRQTSBFSyBS" + - "b290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1yZqFFg0PLDo" + - "cW7Fyis2Xe5vERxnJ+KlEMUOQnrw5At9f0/ggovDM8uCVW71T6e24T6HH6kUQZCt" + - "yddtsaf0tebmA3TxjiuBzBAtT6qyns35+sXuL6uZaLnjGKXDv+uByOzpmBXUSwq1" + - "tdSTPQ0wWWQ6v/qwKofZdxAaPCTIBw61G08rkUT42a1hPESmVFrmc5hcnn4AQmJE" + - "cjcOhClwIKE9OQw8TzI+7ncgCZlY3FZFKqHp7NRNnaihpmKbHvn5wXIUnKuvS4iZ" + - "HqSbzGBuZ0ogqJ22ruDJi+JWYUWBmgI1JO85CPJ1Q58t0ME3hM3oWeqV6adWUcIc" + - "IpclkYQWlwIDAQABo4HBMIG+MBIGA1UdEwEB/wQIMAYBAf8CAQEwWAYDVR0gAQH/" + - "BE4wTDBKBgtghkgBhvhFAQcvATA7MDkGCCsGAQUFBwIBFi1odHRwOi8vd3d3LnZl" + - "cmlzaWduLmNvbS9yZXBvc2l0b3J5L2luZGV4Lmh0bWwwDgYDVR0PAQH/BAQDAgIE" + - "MB0GA1UdDgQWBBRW65FEhWPWcrOu1EWWC/eUDlRCpjAfBgNVHSMEGDAWgBQPFPXj" + - "IIhEFsomv40fzjcV6kVvBjANBgkqhkiG9w0BAQUFAAOCAQEAWKL5zsV8p/TZk3mt" + - "9m9NAqXWBDVHBnDgBE+Qphf25s+3s098vkWVLTddH3PtddF3MEYC4W8+dn4tyFe9" + - "mQ+96q8dwJdNabwBokrZy2beL71CXt/4jYNN0j/N9uYO4vIDBFDKRMWCtUO217+w" + - "xQTSOv5+mpgFw7UML/QpgpdmZy2i+eZPxDo8dzT+YJXC5vsHVSooA3rWDDzvnoLC" + - "cmDDiT3pG6AdjAN61MeeHHmoJavV8Tvdoa3g14Sn1lL+TQ1xaznyh520sX0dXPTp" + - "GqZbDzqEMiVbG7vFECqINE96/rwppJlWK91F1MZikGXr7FeF5C0JutGLb0gaYOmv" + - "Yau4DQ==")), - new X509Certificate2(Convert.FromBase64String( - "MIIEUDCCAzigAwIBAgIQRyQE4N8hgD99IM2HSOq5WjANBgkqhkiG9w0BAQUFADCB" + - "ljELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTswOQYDVQQL" + - "EzJWZXJpU2lnbiBUcnVzdGVkIENvbXB1dGluZyBDZXJ0aWZpY2F0aW9uIEF1dGhv" + - "cml0eTExMC8GA1UEAxMoVmVyaVNpZ24gVHJ1c3RlZCBQbGF0Zm9ybSBNb2R1bGUg" + - "Um9vdCBDQTAeFw0wNTEwMjUwMDAwMDBaFw0zMDEwMjQyMzU5NTlaMG0xCzAJBgNV" + - "BAYTAkRFMRAwDgYDVQQIEwdCYXZhcmlhMSEwHwYDVQQKExhJbmZpbmVvbiBUZWNo" + - "bm9sb2dpZXMgQUcxDDAKBgNVBAsTA0FJTTEbMBkGA1UEAxMSSUZYIFRQTSBFSyBS" + - "b290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1yZqFFg0PLDo" + - "cW7Fyis2Xe5vERxnJ+KlEMUOQnrw5At9f0/ggovDM8uCVW71T6e24T6HH6kUQZCt" + - "yddtsaf0tebmA3TxjiuBzBAtT6qyns35+sXuL6uZaLnjGKXDv+uByOzpmBXUSwq1" + - "tdSTPQ0wWWQ6v/qwKofZdxAaPCTIBw61G08rkUT42a1hPESmVFrmc5hcnn4AQmJE" + - "cjcOhClwIKE9OQw8TzI+7ncgCZlY3FZFKqHp7NRNnaihpmKbHvn5wXIUnKuvS4iZ" + - "HqSbzGBuZ0ogqJ22ruDJi+JWYUWBmgI1JO85CPJ1Q58t0ME3hM3oWeqV6adWUcIc" + - "IpclkYQWlwIDAQABo4HBMIG+MBIGA1UdEwEB/wQIMAYBAf8CAQEwWAYDVR0gAQH/" + - "BE4wTDBKBgtghkgBhvhFAQcvATA7MDkGCCsGAQUFBwIBFi1odHRwOi8vd3d3LnZl" + - "cmlzaWduLmNvbS9yZXBvc2l0b3J5L2luZGV4Lmh0bWwwDgYDVR0PAQH/BAQDAgIE" + - "MB0GA1UdDgQWBBRW65FEhWPWcrOu1EWWC/eUDlRCpjAfBgNVHSMEGDAWgBQPFPXj" + - "IIhEFsomv40fzjcV6kVvBjANBgkqhkiG9w0BAQUFAAOCAQEAWKL5zsV8p/TZk3mt" + - "9m9NAqXWBDVHBnDgBE+Qphf25s+3s098vkWVLTddH3PtddF3MEYC4W8+dn4tyFe9" + - "mQ+96q8dwJdNabwBokrZy2beL71CXt/4jYNN0j/N9uYO4vIDBFDKRMWCtUO217+w" + - "xQTSOv5+mpgFw7UML/QpgpdmZy2i+eZPxDo8dzT+YJXC5vsHVSooA3rWDDzvnoLC" + - "cmDDiT3pG6AdjAN61MeeHHmoJavV8Tvdoa3g14Sn1lL+TQ1xaznyh520sX0dXPTp" + - "GqZbDzqEMiVbG7vFECqINE96/rwppJlWK91F1MZikGXr7FeF5C0JutGLb0gaYOmv" + - "Yau4DQ==")), - new X509Certificate2(Convert.FromBase64String( - "MIIFqzCCA5OgAwIBAgIBAzANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJERTEh" + - "MB8GA1UECgwYSW5maW5lb24gVGVjaG5vbG9naWVzIEFHMRswGQYDVQQLDBJPUFRJ" + - "R0EoVE0pIERldmljZXMxKDAmBgNVBAMMH0luZmluZW9uIE9QVElHQShUTSkgUlNB" + - "IFJvb3QgQ0EwHhcNMTMwNzI2MDAwMDAwWhcNNDMwNzI1MjM1OTU5WjB3MQswCQYD" + - "VQQGEwJERTEhMB8GA1UECgwYSW5maW5lb24gVGVjaG5vbG9naWVzIEFHMRswGQYD" + - "VQQLDBJPUFRJR0EoVE0pIERldmljZXMxKDAmBgNVBAMMH0luZmluZW9uIE9QVElH" + - "QShUTSkgUlNBIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC" + - "AQC7E+gc0B5T7awzux66zMMZMTtCkPqGv6a3NVx73ICg2DSwnipFwBiUl9soEodn" + - "25SVVN7pqmvKA2gMTR5QexuYS9PPerfRZrBY00xyFx84V+mIRPg4YqUMLtZBcAwr" + - "R3GO6cffHp20SBH5ITpuqKciwb0v5ueLdtZHYRPq1+jgy58IFY/vACyF/ccWZxUS" + - "JRNSe4ruwBgI7NMWicxiiWQmz1fE3e0mUGQ1tu4M6MpZPxTZxWzN0mMz9noj1oIT" + - "ZUnq/drN54LHzX45l+2b14f5FkvtcXxJ7OCkI7lmWIt8s5fE4HhixEgsR2RX5hzl" + - "8XiHiS7uD3pQhBYSBN5IBbVWREex1IUat5eAOb9AXjnZ7ivxJKiY/BkOmrNgN8k2" + - "7vOS4P81ix1GnXsjyHJ6mOtWRC9UHfvJcvM3U9tuU+3dRfib03NGxSPnKteL4SP1" + - "bdHfiGjV3LIxzFHOfdjM2cvFJ6jXg5hwXCFSdsQm5e2BfT3dWDBSfR4h3Prpkl6d" + - "cAyb3nNtMK3HR5yl6QBuJybw8afHT3KRbwvOHOCR0ZVJTszclEPcM3NQdwFlhqLS" + - "ghIflaKSPv9yHTKeg2AB5q9JSG2nwSTrjDKRab225+zJ0yylH5NwxIBLaVHDyAEu" + - "81af+wnm99oqgvJuDKSQGyLf6sCeuy81wQYO46yNa+xJwQIDAQABo0IwQDAdBgNV" + - "HQ4EFgQU3LtWq/EY/KaadREQZYQSntVBkrkwDgYDVR0PAQH/BAQDAgAGMA8GA1Ud" + - "EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAGHTBUx3ETIXYJsaAgb2pyyN" + - "UltVL2bKzGMVSsnTCrXUU8hKrDQh3jNIMrS0d6dU/fGaGJvehxmmJfjaN/IFWA4M" + - "BdZEnpAe2fJEP8vbLa/QHVfsAVuotLD6QWAqeaC2txpxkerveoV2JAwj1jrprT4y" + - "rkS8SxZuKS05rYdlG30GjOKTq81amQtGf2NlNiM0lBB/SKTt0Uv5TK0jIWbz2WoZ" + - "gGut7mF0md1rHRauWRcoHQdxWSQTCTtgoQzeBj4IS6N3QxQBKV9LL9UWm+CMIT7Y" + - "np8bSJ8oW4UdpSuYWe1ZwSjZyzDiSzpuc4gTS6aHfMmEfoVwC8HN03/HD6B1Lwo2" + - "DvEaqAxkya9IYWrDqkMrEErJO6cqx/vfIcfY/8JYmUJGTmvVlaODJTwYwov/2rjr" + - "la5gR+xrTM7dq8bZimSQTO8h6cdL6u+3c8mGriCQkNZIZEac/Gdn+KwydaOZIcnf" + - "Rdp3SalxsSp6cWwJGE4wpYKB2ClM2QF3yNQoTGNwMlpsxnU72ihDi/RxyaRTz9OR" + - "pubNq8Wuq7jQUs5U00ryrMCZog1cxLzyfZwwCYh6O2CmbvMoydHNy5CU3ygxaLWv" + - "JpgZVHN103npVMR3mLNa3QE+5MFlBlP3Mmystu8iVAKJas39VO5y5jad4dRLkwtM" + - "6sJa8iBpdRjZrBp5sJBI"))} }, - {"id:494E5443", new X509Certificate2[]{ - new X509Certificate2(Convert.FromBase64String( - "MIICdzCCAh6gAwIBAgIUB+dPf7a3IyJGO923z34oQLRP7pwwCgYIKoZIzj0EAwIw" + - "gYcxCzAJBgNVBAYMAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xh" + - "cmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMSEwHwYDVQQLDBhUUE0gRUsg" + - "cm9vdCBjZXJ0IHNpZ25pbmcxFjAUBgNVBAMMDXd3dy5pbnRlbC5jb20wHhcNMTQw" + - "MTE1MDAwMDAwWhcNNDkxMjMxMjM1OTU5WjCBhzELMAkGA1UEBgwCVVMxCzAJBgNV" + - "BAgMAkNBMRQwEgYDVQQHDAtTYW50YSBDbGFyYTEaMBgGA1UECgwRSW50ZWwgQ29y" + - "cG9yYXRpb24xITAfBgNVBAsMGFRQTSBFSyByb290IGNlcnQgc2lnbmluZzEWMBQG" + - "A1UEAwwNd3d3LmludGVsLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJR9" + - "gVEsjUrMb+E/dl19ywJsKZDnghmwVyG16dAfQ0Pftp1bjhtPEGEguvbLGRRopKWH" + - "VscAOlTFnvCHq+6/9/SjZjBkMB8GA1UdIwQYMBaAFOhSBcJP2NLVpSFHFrbODHtb" + - "uncPMB0GA1UdDgQWBBToUgXCT9jS1aUhRxa2zgx7W7p3DzASBgNVHRMBAf8ECDAG" + - "AQH/AgEBMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAgNHADBEAiAldFScWQ6L" + - "PQgW/YT+2GILcATEA2TgzASaCrG+AzL6FgIgLH8ABRzm028hRYR/JZVGkHiomzYX" + - "VILmTjHwSL7uZBU="))} }, - //{"id:4C454E00", "LEN"}, - //{"id:4E534D20", "NSM"}, - //{"id:4E545A00", "NTZ"}, - {"id:4E544300", new X509Certificate2[]{ - new X509Certificate2(Convert.FromBase64String( - "MIIDSjCCAjKgAwIBAgIGAK3jXfbVMA0GCSqGSIb3DQEBBQUAMFIxUDAcBgNVBAMT" + - "FU5UQyBUUE0gRUsgUm9vdCBDQSAwMTAlBgNVBAoTHk51dm90b24gVGVjaG5vbG9n" + - "eSBDb3Jwb3JhdGlvbjAJBgNVBAYTAlRXMB4XDTEyMDcxMTE2MjkzMFoXDTMyMDcx" + - "MTE2MjkzMFowUjFQMBwGA1UEAxMVTlRDIFRQTSBFSyBSb290IENBIDAxMCUGA1UE" + - "ChMeTnV2b3RvbiBUZWNobm9sb2d5IENvcnBvcmF0aW9uMAkGA1UEBhMCVFcwggEi" + - "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDoNqxhtD4yUtXhqKQGGZemoKJy" + - "uj1RnWvmNgzItLeejNU8B6fOnpMQyoS4K72tMhhFRK2jV9RYzyJMSjEwyX0ASTO1" + - "2yMti2UJQS60d36eGwk8WLgrFnnITlemshi01h9t1MOmay3TO1LLH/3/VDKJ+jbd" + - "cbfIO2bBquN8r3/ojYUaNSPj6pK1mmsMoJXF4dGRSEwb/4ozBIw5dugm1MEq4Zj3" + - "GZ0YPg5wyLRugQbt7DkUOX4FGuK5p/C0u5zX8u33EGTrDrRz3ye3zO+aAY1xXF/m" + - "qwEqgxX5M8f0/DXTTO/CfeIksuPeOzujFtXfi5Cy64eeIZ0nAUG3jbtnGjoFAgMB" + - "AAGjJjAkMA4GA1UdDwEB/wQEAwICBDASBgNVHRMBAf8ECDAGAQH/AgEAMA0GCSqG" + - "SIb3DQEBBQUAA4IBAQBBQznOPJAsD4Yvyt/hXtVJSgBX/+rRfoaqbdt3UMbUPJYi" + - "pUoTUgaTx02DVRwommO+hLx7CS++1F2zorWC8qQyvNbg7iffQbbjWitt8NPE6kCr" + - "q0Y5g7M/LkQDd5N3cFfC15uFJOtlj+A2DGzir8dlXU/0qNq9dBFbi+y+Y3rAT+wK" + - "fktmN82UT861wTUzDvnXO+v7H5DYXjUU8kejPW6q+GgsccIbVTOdHNNWbMrcD9yf" + - "oS91nMZ/+/n7IfFWXNN82qERsrvOFCDsbIzUOR30N0IP++oqGfwAbKFfCOCFUz6j" + - "jpXUdJlh22tp12UMsreibmi5bsWYBgybwSbRgvzE")), - new X509Certificate2(Convert.FromBase64String( - "MIIDSjCCAjKgAwIBAgIGAPadBmPZMA0GCSqGSIb3DQEBBQUAMFIxUDAcBgNVBAMT" + - "FU5UQyBUUE0gRUsgUm9vdCBDQSAwMjAlBgNVBAoTHk51dm90b24gVGVjaG5vbG9n" + - "eSBDb3Jwb3JhdGlvbjAJBgNVBAYTAlRXMB4XDTEyMDcxMTE2MzMyNFoXDTMyMDcx" + - "MTE2MzMyNFowUjFQMBwGA1UEAxMVTlRDIFRQTSBFSyBSb290IENBIDAyMCUGA1UE" + - "ChMeTnV2b3RvbiBUZWNobm9sb2d5IENvcnBvcmF0aW9uMAkGA1UEBhMCVFcwggEi" + - "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSagWxaANT1YA2YUSN7sq7yzOT" + - "1ymbIM+WijhE5AGcLwLFoJ9fmaQrYL6fAW2EW/Q3yu97Q9Ysr8yYZ2XCCfxfseEr" + - "Vs80an8Nk6LkTDz8+0Hm0Cct0klvNUAZEIvWpmgHZMvGijXyOcp4z494d8B28Ynb" + - "I7x0JMXZZQQKQi+WfuHtntF+2osYScweocipPrGeONLKU9sngWZ2vnnvw1SBneTa" + - "irxq0Q0SD6Bx9jtxvdf87euk8JzfPhX8jp8GEeAjmLwGR+tnOQrDmczGNmp7YYNN" + - "R+Q7NZVoYWHw5jaoZnNxbouWUXZZxFqDsB/ndCKWtsIzRYPuWcqrFcmUN4SVAgMB" + - "AAGjJjAkMA4GA1UdDwEB/wQEAwICBDASBgNVHRMBAf8ECDAGAQH/AgEAMA0GCSqG" + - "SIb3DQEBBQUAA4IBAQAIkdDSErzPLPYrVthw4lKjW4tRYelUicMPEHKjQeVUAAS5" + - "y9XTzB4DWISDAFsgtQjqHJj0xCG+vpY0Rmn2FCO/0YpP+YBQkdbJOsiyXCdFy9e4" + - "gGjQ24gw1B+rr84+pkI51y952NYBdoQDeb7diPe+24U94f//DYt/JQ8cJua4alr3" + - "2Pohhh5TxCXXfU2EHt67KyqBSxCSy9m4OkCOGLHL2X5nQIdXVj178mw6DSAwyhwR" + - "n3uJo5MvUEoQTFZJKGSXfab619mIgzEr+YHsIQToqf44VfDMDdM+MFiXQ3a5fLii" + - "hEKQ9DhBPtpHAbhFA4jhCiG9HA8FdEplJ+M4uxNz")), - new X509Certificate2(Convert.FromBase64String( - "MIIDWTCCAkGgAwIBAgIJAMklAEG4bgQ6MA0GCSqGSIb3DQEBBQUAMFgxVjAiBgNV" + - "BAMTG05UQyBUUE0gRUsgUm9vdCBDQSBBUlNVRiAwMTAlBgNVBAoTHk51dm90b24g" + - "VGVjaG5vbG9neSBDb3Jwb3JhdGlvbjAJBgNVBAYTAlRXMB4XDTE0MDQwMTE4MzQz" + - "OFoXDTM0MDMyODE4MzQzOFowWDFWMCIGA1UEAxMbTlRDIFRQTSBFSyBSb290IENB" + - "IEFSU1VGIDAxMCUGA1UEChMeTnV2b3RvbiBUZWNobm9sb2d5IENvcnBvcmF0aW9u" + - "MAkGA1UEBhMCVFcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCxcfP" + - "yaNsGhaR28qqisqkrb4Z2OPul7BRNlIEYP8jSFORygyfp4j7bKRyVTTONCUbPq+J" + - "/a4yRcdbEs8dzvzXypQbVUjuC4sOKjPiWLfOhj1Z1yvOn19Xe3Ei4UzMKJm+xpb1" + - "BYR4YfrnuVzL4do/B/lCr2AYs4Fmtn1uzXBp1St8TRJz9HTW1yKJ2ZOqTgW3DX80" + - "6DP//3kIatTuLCZ6Zsdl6fsgMPxJGwrI35ThKBtaUMT93abb/KB/dugvoIgtEi9D" + - "GEC2C0UWsvJEfu0Qi8zoxtYvd9Y2tRlMxMhK75uShXHxRcG+WOGEnm6uVpGphLKg" + - "qxAl1tuFcb94vi7dAgMBAAGjJjAkMA4GA1UdDwEB/wQEAwICBDASBgNVHRMBAf8E" + - "CDAGAQH/AgEAMA0GCSqGSIb3DQEBBQUAA4IBAQB7epeKy2Sa+9huMzK4PnIpjiaX" + - "QrxPx+E8BVGw6VuQqTcTPQRvPhNpc4VF/6/7MA9qb6vDxWEf40tqNi8e/RPNlRFY" + - "Dh4tQ1Hhl69NrZVYZeXl1cU/ometoAAbz79ugq78iFndJ5rHMQ85GRwtW9i/q0p1" + - "VjJ8dLYJ7aRBDTP3hndc35GmZg3q1UX93WD6mM5KuE+mOdv7MXKMtYSrV+dE/iGM" + - "ASrratJf57P6N8BpegPQaSb6UnElwBpwhRxzW7N9qgjQWIqrxe97CfJk41RvtnKu" + - "SePqlm1PtWkygt9bYaInLZYkcknXTD/7BtzAyyS25HtG/YTvuMtKItCp7Z4n")), - new X509Certificate2(Convert.FromBase64String( - "MIIDkjCCAnqgAwIBAgIISN0JfIK6vE0wDQYJKoZIhvcNAQEFBQAwVTFTMB8GA1UE" + - "AxMYTnV2b3RvbiBUUE0gUm9vdCBDQSAxMDEzMCUGA1UEChMeTnV2b3RvbiBUZWNo" + - "bm9sb2d5IENvcnBvcmF0aW9uMAkGA1UEBhMCVFcwHhcNMTUwNTExMDg0MzI1WhcN" + - "MzUwNTA3MDg0MzI1WjBVMVMwHwYDVQQDExhOdXZvdG9uIFRQTSBSb290IENBIDEw" + - "MTMwJQYDVQQKEx5OdXZvdG9uIFRlY2hub2xvZ3kgQ29ycG9yYXRpb24wCQYDVQQG" + - "EwJUVzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALDAta6EZBlhF1MC" + - "Z9GeMXqw8puwZEDI3qR/rwGhEUj2oqhFY/K9zUk2YQCkC6X5lrr/lbWfvZtUGMFC" + - "P4VQlt+bGPTOladGg6zJ/7a6yCd9MqkZbw92niDNhWcXsiB7SRyHYdr/He8tNOoD" + - "mVdNFXxknP8QH3soBPahxckqtrhhk+24Iran04jOAc0959VnP8H0Jyg4BjehIQjj" + - "BGGK+bJWZXHYRFlDj4dRW+epChdOqTpWOulf5GOvwNm3sv4ojU2fJ8cA5TznX81z" + - "+Se6hmw/RF8rUGjf1uiKbsxnbIf3An01mZYgD98FXEHAWAW92vAJUuEQJVBlTest" + - "1YmsaT0CAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8C" + - "AQAwHwYDVR0jBBgwFoAUoNc3KQ4WzyrivucQDPVrLwTF8EMwHQYDVR0OBBYEFKDX" + - "NykOFs8q4r7nEAz1ay8ExfBDMA0GCSqGSIb3DQEBBQUAA4IBAQCOXMzQYz3vr9tg" + - "SiFr6qha2+Jay+EK0iLjSKvNzcTv5yaO8I6pb7tdocvze8394PtM42d2OTOM99lJ" + - "bZogquaJ6fLHMwzO7UEGndtm6YMp6APXk4ecRqUDLqofIWL6PQUVwSEYlAC6RM9k" + - "n4MJqckIxsc6iC38lsjyn4ut8o/E3fIo8UzYDl2P+KK1VkjDcmmgNf6seHmBsOYC" + - "vOc4xYpq0yWuZFfxeyC4wC4mOAKLZX2yLMYrYBmnDd60nc0hgI1/TKb1H/Ew2P7R" + - "UxEDMGe8e3A9YR4M/09FLn8cTTjq7hflRlcqiarpPo6+9Z3dqzmqTQxvVQ/DIVqE" + - "3r3WOnnr")), - new X509Certificate2(Convert.FromBase64String( - "MIICBjCCAaygAwIBAgIIEDiqn2SaqGMwCgYIKoZIzj0EAwIwVTFTMB8GA1UEAxMY" + - "TnV2b3RvbiBUUE0gUm9vdCBDQSAxMTEwMCUGA1UEChMeTnV2b3RvbiBUZWNobm9s" + - "b2d5IENvcnBvcmF0aW9uMAkGA1UEBhMCVFcwHhcNMTUwNTExMDg0MzMzWhcNMzUw" + - "NTA3MDg0MzMzWjBVMVMwHwYDVQQDExhOdXZvdG9uIFRQTSBSb290IENBIDExMTAw" + - "JQYDVQQKEx5OdXZvdG9uIFRlY2hub2xvZ3kgQ29ycG9yYXRpb24wCQYDVQQGEwJU" + - "VzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDVkEOpuyhuviaDH6xQj3faaV2Z4" + - "FvXSdwUkTiB1JjPDgv1PU0SFYtEE1W9VmI1GcOn5FAUi2/QM36DPhmPTd+qjZjBk" + - "MA4GA1UdDwEB/wQEAwICBDASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQV" + - "kdS26vmNAQSGS2kDpI3QAmB30zAfBgNVHSMEGDAWgBQVkdS26vmNAQSGS2kDpI3Q" + - "AmB30zAKBggqhkjOPQQDAgNIADBFAiEAlfxysfHDcxYDed5dmRbvHPKHLEEq9Y9P" + - "wAxoKqH7Q5kCIGfsxiLr2j9nJ9jELwXz0/VWN9PhUNdM3qmsx2JEne6p")), - new X509Certificate2(Convert.FromBase64String( - "MIIDkjCCAnqgAwIBAgIIWAnP9p2CIZcwDQYJKoZIhvcNAQEFBQAwVTFTMB8GA1UE" + - "AxMYTnV2b3RvbiBUUE0gUm9vdCBDQSAyMDEwMCUGA1UEChMeTnV2b3RvbiBUZWNo" + - "bm9sb2d5IENvcnBvcmF0aW9uMAkGA1UEBhMCVFcwHhcNMTUwNDIzMDY1OTE5WhcN" + - "MzUwNDE5MDY1OTE5WjBVMVMwHwYDVQQDExhOdXZvdG9uIFRQTSBSb290IENBIDIw" + - "MTAwJQYDVQQKEx5OdXZvdG9uIFRlY2hub2xvZ3kgQ29ycG9yYXRpb24wCQYDVQQG" + - "EwJUVzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKcE9saVURE582ny" + - "dHsZO7+3xmdMFbOPCdplBda/EJg9cg7n6bZ79Qv7hyymN5qE23SOPNFvm8SAdmCJ" + - "ybmTnk1y+SyiDw5gUpckbXsRYAetTwqtdfBkF4TkFoRJDIraQC8miTdYqXMXfWTo" + - "bhHXf/oV953laOCO/SRlqXzAWzm5d8PwixUBLZTnvcgxM+pXwv6JY6wgXpv55fY1" + - "D3M1hyiNALib+rg0LwazalU0DOryAAIqFzMgkR2IaefkAmpmQ1xpfMJsK+BMixcI" + - "XUCzSGGKKdkc3WUDye/vsyXYQ5zoYuLt3xb7BEZxes31lqbs1gniNz4oD5ptmrS4" + - "8V7Rz/kCAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8C" + - "AQAwHwYDVR0jBBgwFoAUCDAPQ6j0uMjmJKT3Bgz1nnRQFecwHQYDVR0OBBYEFAgw" + - "D0Oo9LjI5iSk9wYM9Z50UBXnMA0GCSqGSIb3DQEBBQUAA4IBAQAE0pMnjz5o3QUd" + - "S3lLQn3+vXkS2xc1EmPxcVFxjPbrJDtnNRMWwglC8zo70VgWu/+ulwzy783zJSiT" + - "nkWPeuszqp3xOtCPWDE4D2sxVbWH3pvel2tgZJv0KJsJH93QE53WbHUwSn2JjHNH" + - "UJiBpq0genUxGD+zBI3NGDGB1iti66aJfCdjn8C0G0gTmQ8jFpZ6AsX1GSvPYeU6" + - "EqN9ynIEYUVcRKwoHQaSmqDd7HVp97fwD+mkOfFYByLVUqC09rNFW81Va4Ze2gw2" + - "HiKz/SVSA5mA/91wfEZSZ6azOgDZNQlbgBo27mZFJ5mR7iJbWgtD+vO4+wRZK8Bc" + - "8yWxV8ri")), - new X509Certificate2(Convert.FromBase64String( - "MIICBjCCAaygAwIBAgIIP5MvnZk8FrswCgYIKoZIzj0EAwIwVTFTMB8GA1UEAxMY" + - "TnV2b3RvbiBUUE0gUm9vdCBDQSAyMTEwMCUGA1UEChMeTnV2b3RvbiBUZWNobm9s" + - "b2d5IENvcnBvcmF0aW9uMAkGA1UEBhMCVFcwHhcNMTUxMDE5MDQzMjAwWhcNMzUx" + - "MDE1MDQzMjAwWjBVMVMwHwYDVQQDExhOdXZvdG9uIFRQTSBSb290IENBIDIxMTAw" + - "JQYDVQQKEx5OdXZvdG9uIFRlY2hub2xvZ3kgQ29ycG9yYXRpb24wCQYDVQQGEwJU" + - "VzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPv9uK2BNm8/nmIyNsc2/aKHV0WR" + - "ptzge3jKAIgUMosQIokl4LE3iopXWD3Hruxjf9vkLMDJrTeK3hWh2ySS4ySjZjBk" + - "MA4GA1UdDwEB/wQEAwICBDASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSf" + - "u3mqD1JieL7RUJKacXHpajW+9zAfBgNVHSMEGDAWgBSfu3mqD1JieL7RUJKacXHp" + - "ajW+9zAKBggqhkjOPQQDAgNIADBFAiEA/jiywhOKpiMOUnTfDmXsXfDFokhKVNTX" + - "B6Xtqm7J8L4CICjT3/Y+rrSnf8zrBXqWeHDh8Wi41+w2ppq6Ev9orZFI"))} }, - //{"id:51434F4D", "QCOM"}, - //{"id:534D5343", "SMSC"}, - {"id:53544D20", new X509Certificate2[]{ - new X509Certificate2(Convert.FromBase64String( - "MIID1zCCAr+gAwIBAgILBAAAAAABIBkJGa4wDQYJKoZIhvcNAQELBQAwgYcxOzA5" + - "BgNVBAsTMkdsb2JhbFNpZ24gVHJ1c3RlZCBDb21wdXRpbmcgQ2VydGlmaWNhdGUg" + - "QXV0aG9yaXR5MRMwEQYDVQQKEwpHbG9iYWxTaWduMTMwMQYDVQQDEypHbG9iYWxT" + - "aWduIFRydXN0ZWQgUGxhdGZvcm0gTW9kdWxlIFJvb3QgQ0EwHhcNMDkwMzE4MTAw" + - "MDAwWhcNNDkwMzE4MTAwMDAwWjCBhzE7MDkGA1UECxMyR2xvYmFsU2lnbiBUcnVz" + - "dGVkIENvbXB1dGluZyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxEzARBgNVBAoTCkds" + - "b2JhbFNpZ24xMzAxBgNVBAMTKkdsb2JhbFNpZ24gVHJ1c3RlZCBQbGF0Zm9ybSBN" + - "b2R1bGUgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPi3" + - "Gi0wHyTT7dq24caFAp31gXFDvALRGJrMiP+TunIYPacYD8eBVSNEiVoCUcVfYxzl" + - "/DPTxmRyGXgQM8CVh9THrxDTW7N2PSAoZ7fvlmjTiBL/IQ7m1F+9wGI/FuaMTphz" + - "w6lBda7HFlIYKTbM/vz24axCHLzJ8Xir2L889D9MMIerBRqouVsDGauH+TIOdw4o" + - "IGKhorqfsDro57JHwViMWlbB1Ogad7PBX5X/e9GDNdZTdo4c0bZnKO+dEtzEgKCh" + - "JmQ53Mxa9y4xPMGRRnjLsyxuM99vkkYXy7rnxctSo7GtGIJJVabNuXZ0peaY9ku0" + - "CUgKAsQndLkTHz8bIh0CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB" + - "/wQFMAMBAf8wHQYDVR0OBBYEFB4jY/CFtfYlTu0awFC+ZXzH1BV6MA0GCSqGSIb3" + - "DQEBCwUAA4IBAQCVb7lI4d49u7EtCX03/rUCCiaZ64NMxxqRmcSVdUx6yRrbl8NN" + - "FNr6ym2kTvwe1+JkTCiDxKzJsOR/jcPczAFiYpFbZQYLA6RK0bzbL9RGcaw5LLhY" + - "o/flqsu3N2/HNesWbekoxLosP6NLGEOnpj1B+R3y7HCQq/08U5l3Ete6TRKTAavc" + - "0mty+uCFtLXf+tirl7xSaIGD0LwcYNdzLEB9g4je6FQSWL0QOXb+zR755QYupZAw" + - "G1PnOgYWfqWowKcQQexFPrKGlzh0ncITV/nBEi++fnnZ7TFiwaKwe+WussrROV1S" + - "DDF29dmoMcbSFDL+DgSMabVT6Qr6Ze1rbmSh")), - new X509Certificate2(Convert.FromBase64String( - "MIICszCCAjqgAwIBAgIORdycjBUV21nQRkudeekwCgYIKoZIzj0EAwMwgYsxOzA5" + - "BgNVBAsTMkdsb2JhbFNpZ24gVHJ1c3RlZCBDb21wdXRpbmcgQ2VydGlmaWNhdGUg" + - "QXV0aG9yaXR5MRMwEQYDVQQKEwpHbG9iYWxTaWduMTcwNQYDVQQDEy5HbG9iYWxT" + - "aWduIFRydXN0ZWQgUGxhdGZvcm0gTW9kdWxlIEVDQyBSb290IENBMB4XDTE0MTEy" + - "NjAwMDAwMFoXDTM4MDExOTAzMTQwN1owgYsxOzA5BgNVBAsTMkdsb2JhbFNpZ24g" + - "VHJ1c3RlZCBDb21wdXRpbmcgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRMwEQYDVQQK" + - "EwpHbG9iYWxTaWduMTcwNQYDVQQDEy5HbG9iYWxTaWduIFRydXN0ZWQgUGxhdGZv" + - "cm0gTW9kdWxlIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAENTps" + - "86FDUD+bep3kd1U5pnita316zBktOVNWxZQ+Ymua0oaR66ItzHrl19zYSGbW6ar0" + - "1V91kktxWDJ6UFl3MyH3yXKsCHS2O5vxMlfmdRp8tpebMorHtIWf9u1+ctNFo2Mw" + - "YTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUYT78" + - "EZkKf7CpW5CgJl4pYUe3MAMwHwYDVR0jBBgwFoAUYT78EZkKf7CpW5CgJl4pYUe3" + - "MAMwCgYIKoZIzj0EAwMDZwAwZAIwd02iAb5aN/pQGWdTJ7/lgMhFCuOLGtQ+ocdV" + - "/xmoxdIWLtggAuq9fFDfsu/vzeJ7AjAGhdk03AjHpLl0dAp7aCI8D8qupwyYTBaL" + - "rSJCZDMHhvNhETbbLu8uEPKt/U6/mGM=")), - new X509Certificate2(Convert.FromBase64String( - "MIIEDDCCAvSgAwIBAgILBAAAAAABIsFs834wDQYJKoZIhvcNAQELBQAwgYcxOzA5" + - "BgNVBAsTMkdsb2JhbFNpZ24gVHJ1c3RlZCBDb21wdXRpbmcgQ2VydGlmaWNhdGUg" + - "QXV0aG9yaXR5MRMwEQYDVQQKEwpHbG9iYWxTaWduMTMwMQYDVQQDEypHbG9iYWxT" + - "aWduIFRydXN0ZWQgUGxhdGZvcm0gTW9kdWxlIFJvb3QgQ0EwHhcNMDkwNzI4MTIw" + - "MDAwWhcNMzkxMjMxMjM1OTU5WjBKMQswCQYDVQQGEwJDSDEeMBwGA1UEChMVU1RN" + - "aWNyb2VsZWN0cm9uaWNzIE5WMRswGQYDVQQDExJTVE0gVFBNIEVLIFJvb3QgQ0Ew" + - "ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDxBLG5wcB9J0MsiJMreoWQ" + - "l21bBN12SSGZPJ3HoPjzcrzAz6SPy+TrFmZ6eUVspsFL/23wdPprqTUtDHi+C2pw" + - "k/3dF3/Rb2t/yHgiPlbCshYpi5f/rJ7nzbQ1ca2LzX3saBe53VfNQQV0zd5uM0DT" + - "SrmAKU1RIAj2WlZFWXoN4NWTyRtqT5suPHa2y8FlCWMZKlS0FiY4pfM20b5YQ+EL" + - "4zqb9zN53u/TdYZegrfSlc30Nl9G13Mgi+8rtPFKwsxx05EBbhVroH7aKVI1djsf" + - "E1MVrUzw62PHik3xlzznXML8OjY//xKeiCWcsApuGCaIAf7TsTRi2l8DNB3rCr1X" + - "AgMBAAGjgbQwgbEwDgYDVR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8CAQEw" + - "HQYDVR0OBBYEFG/mxWwHt2yLCoGSg1zLQR72jtEnMEsGA1UdIAREMEIwQAYJKwYB" + - "BAGgMgFaMDMwMQYIKwYBBQUHAgEWJWh0dHA6Ly93d3cuZ2xvYmFsc2lnbi5uZXQv" + - "cmVwb3NpdG9yeS8wHwYDVR0jBBgwFoAUHiNj8IW19iVO7RrAUL5lfMfUFXowDQYJ" + - "KoZIhvcNAQELBQADggEBAFrKpwFmRh7BGdpPZWc1Y6wIbdTAF6T+q1KwDJcyAjgJ" + - "qThFp3xTAt3tvyVrCRf7T/YARYE24DNa0iFaXsIXeQASDYHJjAZ6LQTslYBeRYLb" + - "C9v8ZE2ocKSCiC8ALYlJWk39Wob0H1Lk6l2zcUo3oKczGiAcRrlmwV496wvGyted" + - "2RBcLZro7yhOOGr9KMabV14fNl0lG+31J1nWI2hgTqh53GXg1QH2YpggD3b7UbVm" + - "c6GZaX37N3z15XfQafuAfHt10kYCNdePzC9tOwirHIsO8lrxoNlzOSxX8SqQGbBI" + - "+kWoe5+SY3gdOGGDQKIdw3W1poMN8bQ5x7XFcgVMwVU=")), - new X509Certificate2(Convert.FromBase64String( - "MIICyDCCAk+gAwIBAgIORyzLp/OdsAvb9r+66LowCgYIKoZIzj0EAwMwgYsxOzA5" + - "BgNVBAsTMkdsb2JhbFNpZ24gVHJ1c3RlZCBDb21wdXRpbmcgQ2VydGlmaWNhdGUg" + - "QXV0aG9yaXR5MRMwEQYDVQQKEwpHbG9iYWxTaWduMTcwNQYDVQQDEy5HbG9iYWxT" + - "aWduIFRydXN0ZWQgUGxhdGZvcm0gTW9kdWxlIEVDQyBSb290IENBMB4XDTE1MTAy" + - "ODAwMDAwMFoXDTM4MDExOTAzMTQwN1owTjELMAkGA1UEBhMCQ0gxHjAcBgNVBAoT" + - "FVNUTWljcm9lbGVjdHJvbmljcyBOVjEfMB0GA1UEAxMWU1RNIFRQTSBFQ0MgUm9v" + - "dCBDQSAwMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABG7/OLXMiprQQHwNnkpT6aqG" + - "zOGLcbbAgUtyjlXOZtuv0GB0ttJ6fwMwgFtt8RKlko8Bwn89/BoZOUcI4ne8ddRS" + - "oqE6StnU3I13qqjalToq3Rnz61Omn6NErK1pxUe3j6OBtTCBsjAOBgNVHQ8BAf8E" + - "BAMCAgQwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUIJJWPAtDqAVyUwMp" + - "BxwH4OvsAwQwHwYDVR0jBBgwFoAUYT78EZkKf7CpW5CgJl4pYUe3MAMwTAYDVR0g" + - "BEUwQzBBBgkrBgEEAaAyAVowNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93d3cuZ2xv" + - "YmFsc2lnbi5jb20vcmVwb3NpdG9yeS8wCgYIKoZIzj0EAwMDZwAwZAIwWnuUAzwy" + - "vHUhHehymKTZ2QcPUwHX0LdcVTac4ohyEL3zcuv/dM0BN62kFxHgBOhWAjAIxt9i" + - "50yAxy0Z/MeV2NTXqKpLwdhWNuzOSFZnzRKsh9MxY3zj8nebDNlHTDGSMR0="))} } - //{"id:534D534E", "SMSN"}, - //{"id:534E5300", "SNS"}, - //{"id:54584E00", "TXN"}, - //{"id:57454300", "WEC"}, - //{"id:524F4343", "ROCC"}, - //{"id:474F4F47", "GOOG"} + "id:FFFFF1D0", // FIDO testing TPM + // From https://trustedcomputinggroup.org/wp-content/uploads/TCG-TPM-Vendor-ID-Registry-Version-1.02-Revision-1.00.pdf + "id:414D4400", // 'AMD' AMD + "id:41544D4C", // 'ATML' Atmel + "id:4252434D", // 'BRCM' Broadcom + "id:4353434F", // 'CSCO' Cisco + "id:464C5953", // 'FLYS' Flyslice Technologies + "id:48504500", // 'HPE' HPE + "id:49424d00", // 'IBM' IBM + "id:49465800", // 'IFX' Infinion + "id:494E5443", // 'INTC' Intel + "id:4C454E00", // 'LEN' Lenovo + "id:4D534654", // 'MSFT' Microsoft + "id:4E534D20", // 'NSM' National Semiconductor + "id:4E545A00", // 'NTZ' Nationz + "id:4E544300", // 'NTC' Nuvoton Technology + "id:51434F4D", // 'QCOM' Qualcomm + "id:534D5343", // 'SMSC' SMSC + "id:53544D20", // 'STM ' ST Microelectronics + "id:534D534E", // 'SMSN' Samsung + "id:534E5300", // 'SNS' Sinosun + "id:54584E00", // 'TXN' Texas Instruments + "id:57454300", // 'WEC' Winbond + "id:524F4343", // 'ROCC' Fuzhou Rockchip + "id:474F4F47", // 'GOOG' Google }; public override void Verify() { + // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields. + // (handled in base class) if (null == Sig || CBORType.ByteString != Sig.Type || 0 == Sig.GetByteString().Length) throw new Fido2VerificationException("Invalid TPM attestation signature"); if ("2.0" != attStmt["ver"].AsString()) throw new Fido2VerificationException("FIDO2 only supports TPM 2.0"); - // Verify that the public key specified by the parameters and unique fields of pubArea + // 2. Verify that the public key specified by the parameters and unique fields of pubArea // is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData PubArea pubArea = null; if (null != attStmt["pubArea"] && @@ -461,10 +94,10 @@ public override void Verify() if (!pubArea.ECPoint.Y.SequenceEqual(Y)) throw new Fido2VerificationException("Y-coordinate mismatch between pubArea and credentialPublicKey"); } - // Concatenate authenticatorData and clientDataHash to form attToBeSigned. - // see data variable + // 3. Concatenate authenticatorData and clientDataHash to form attToBeSigned + // See Data field of base class - // Validate that certInfo is valid + // 4. Validate that certInfo is valid CertInfo certInfo = null; if (null != attStmt["certInfo"] && CBORType.ByteString == attStmt["certInfo"].Type && @@ -476,26 +109,32 @@ public override void Verify() if (null == certInfo) throw new Fido2VerificationException("CertInfo invalid parsing TPM format attStmt"); - // Verify that magic is set to TPM_GENERATED_VALUE and type is set to TPM_ST_ATTEST_CERTIFY - // handled in parser, see CertInfo.Magic + // 4a. Verify that magic is set to TPM_GENERATED_VALUE + // Handled in CertInfo constructor, see CertInfo.Magic - // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg" + // 4b. Verify that type is set to TPM_ST_ATTEST_CERTIFY + // Handled in CertInfo constructor, see CertInfo.Type + + // 4c. Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg" if (null == Alg || true != Alg.IsNumber || false == CryptoUtils.algMap.ContainsKey(Alg.AsInt32())) throw new Fido2VerificationException("Invalid TPM attestation algorithm"); + using(var hasher = CryptoUtils.GetHasher(CryptoUtils.algMap[Alg.AsInt32()])) { if (!hasher.ComputeHash(Data).SequenceEqual(certInfo.ExtraData)) throw new Fido2VerificationException("Hash value mismatch extraData and attToBeSigned"); } - // Verify that attested contains a TPMS_CERTIFY_INFO structure, whose name field contains a valid Name for pubArea, as computed using the algorithm in the nameAlg field of pubArea + // 4d. Verify that attested contains a TPMS_CERTIFY_INFO structure, whose name field contains a valid Name for pubArea, as computed using the algorithm in the nameAlg field of pubArea using(var hasher = CryptoUtils.GetHasher(CryptoUtils.algMap[certInfo.Alg])) { if (false == hasher.ComputeHash(pubArea.Raw).SequenceEqual(certInfo.AttestedName)) throw new Fido2VerificationException("Hash value mismatch attested and pubArea"); } - // If x5c is present, this indicates that the attestation type is not ECDAA + // 4e. 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. + + // 5. If x5c is present, this indicates that the attestation type is not ECDAA if (null != X5c && CBORType.Array == X5c.Type && 0 != X5c.Count) { if (null == X5c.Values || 0 == X5c.Values.Count || @@ -505,24 +144,24 @@ public override void Verify() throw new Fido2VerificationException("Malformed x5c in TPM attestation"); } - // Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg. + // 5a. Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg. var aikCert = new X509Certificate2(X5c.Values.First().GetByteString()); var cpk = new CredentialPublicKey(aikCert, Alg.AsInt32()); if (true != cpk.Verify(certInfo.Raw, Sig.GetByteString())) throw new Fido2VerificationException("Bad signature in TPM with aikCert"); - // Verify that aikCert meets the TPM attestation statement certificate requirements + // 5b. Verify that aikCert meets the TPM attestation statement certificate requirements // https://www.w3.org/TR/webauthn/#tpm-cert-requirements - // Version MUST be set to 3 + // 5bi. Version MUST be set to 3 if (3 != aikCert.Version) throw new Fido2VerificationException("aikCert must be V3"); - // Subject field MUST be set to empty - they actually mean subject name + // 5bii. Subject field MUST be set to empty - they actually mean subject name if (0 != aikCert.SubjectName.Name.Length) throw new Fido2VerificationException("aikCert subject must be empty"); - // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. + // 5biii. The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. // https://www.w3.org/TR/webauthn/#tpm-cert-requirements (string tpmManufacturer, string tpmModel, string tpmVersion) = SANFromAttnCertExts(aikCert.Extensions); @@ -540,56 +179,47 @@ public override void Verify() throw new Fido2VerificationException("SAN missing TPMManufacturer, TPMModel, or TPMVersion from TPM attestation certificate"); } - if (false == TPMManufacturerRootMap.ContainsKey(tpmManufacturer)) + if (false == TPMManufacturers.Contains(tpmManufacturer)) throw new Fido2VerificationException("Invalid TPM manufacturer found parsing TPM attestation"); - var tpmRoots = TPMManufacturerRootMap[tpmManufacturer]; - var valid = false; - var i = 0; - while (valid == false && i < tpmRoots.Length) - { - var chain = new X509Chain(); - chain.ChainPolicy.ExtraStore.Add(tpmRoots[i]); - - chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - if (tpmManufacturer == "id:FFFFF1D0") - { - chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority | X509VerificationFlags.IgnoreInvalidBasicConstraints; - } - else - { - chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; - } - if (X5c.Values.Count > 1) - { - foreach (var cert in X5c.Values.Skip(1).Reverse()) - { - chain.ChainPolicy.ExtraStore.Add(new X509Certificate2(cert.GetByteString())); - } - } - valid = chain.Build(new X509Certificate2(X5c.Values.First().GetByteString())); - if (_requireValidAttestationRoot) - { - // because we are using AllowUnknownCertificateAuthority we have to verify that the root matches ourselves - var chainRoot = chain.ChainElements[chain.ChainElements.Count - 1].Certificate; - valid = valid && chainRoot.RawData.SequenceEqual(tpmRoots[i].RawData); - } - - i++; - } - if (false == valid) - throw new Fido2VerificationException("TPM attestation failed chain validation"); - // The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID. + // 5biiii. The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID. // OID is 2.23.133.8.3 var EKU = EKUFromAttnCertExts(aikCert.Extensions, "2.23.133.8.3"); if (!EKU) throw new Fido2VerificationException("aikCert EKU missing tcg-kp-AIKCertificate OID"); - // The Basic Constraints extension MUST have the CA component set to false. + // 5biiiii. The Basic Constraints extension MUST have the CA component set to false. if (IsAttnCertCACert(aikCert.Extensions)) throw new Fido2VerificationException("aikCert Basic Constraints extension CA component must be false"); - // 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 + // 5biiiiii. An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension [RFC5280] + // are both OPTIONAL as the status of many attestation certificates is available through metadata services. See, for example, the FIDO Metadata Service [FIDOMetadataService]. + var trustPath = X5c.Values + .Select(x => new X509Certificate2(x.GetByteString())) + .ToArray(); + + var entry = _metadataService?.GetEntry(AuthData.AttestedCredentialData.AaGuid); + + // while conformance testing, we must reject any authenticator that we cannot get metadata for + if (_metadataService?.ConformanceTesting() == true && null == entry) + throw new Fido2VerificationException("AAGUID not found in MDS test metadata"); + + // If the authenticator is listed as in the metadata as one that should produce a basic full attestation, build and verify the chain + if ((entry?.MetadataStatement?.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_BASIC_FULL) ?? false) || + (entry?.MetadataStatement?.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_ATTCA) ?? false) || + (entry?.MetadataStatement?.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_HELLO) ?? false)) + { + var attestationRootCertificates = entry.MetadataStatement.AttestationRootCertificates + .Select(x => new X509Certificate2(Convert.FromBase64String(x))) + .ToArray(); + + if (false == ValidateTrustChain(trustPath, attestationRootCertificates)) + { + throw new Fido2VerificationException("TPM attestation failed chain validation"); + } + } + + // 5c. 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 var aaguid = AaguidFromAttnCertExts(aikCert.Extensions); if ((null != aaguid) && (!aaguid.SequenceEqual(Guid.Empty.ToByteArray())) && diff --git a/Src/Fido2/AuthenticatorAssertionResponse.cs b/Src/Fido2/AuthenticatorAssertionResponse.cs index ecea10c1..b80236b5 100644 --- a/Src/Fido2/AuthenticatorAssertionResponse.cs +++ b/Src/Fido2/AuthenticatorAssertionResponse.cs @@ -74,7 +74,7 @@ public async Task VerifyAsync( throw new Fido2VerificationException("Invalid"); } - // 2. If credential.response.userHandle is present, verify that the user identified by this value is the owner of the public key credential identified by credential.id. + // 2. Identify the user being authenticated and verify that this user is the owner of the public key credential source credentialSource identified by credential.id if (UserHandle != null) { if (UserHandle.Length == 0) @@ -87,11 +87,11 @@ public async Task VerifyAsync( } // 3. Using credential’s id attribute(or the corresponding rawId, if base64url encoding is inappropriate for your use case), look up the corresponding credential public key. - // public key inserted via parameter. + // Credential public key passed in via storePublicKey parameter // 4. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively. //var cData = Raw.Response.ClientDataJson; - var authData = new AuthenticatorData(Raw.Response.AuthenticatorData); + var authData = new AuthenticatorData(AuthenticatorData); //var sig = Raw.Response.Signature; // 5. Let JSONtext be the result of running UTF-8 decode on the value of cData. @@ -100,7 +100,7 @@ public async Task VerifyAsync( // 7. Verify that the value of C.type is the string webauthn.get. if (Type != "webauthn.get") - throw new Fido2VerificationException(); + throw new Fido2VerificationException("AssertionResponse is not type webauthn.get"); // 8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the PublicKeyCredentialRequestOptions passed to the get() call. // 9. Verify that the value of C.origin matches the Relying Party's origin. diff --git a/Src/Fido2/AuthenticatorAttestationResponse.cs b/Src/Fido2/AuthenticatorAttestationResponse.cs index 28d7a810..daa91bf5 100644 --- a/Src/Fido2/AuthenticatorAttestationResponse.cs +++ b/Src/Fido2/AuthenticatorAttestationResponse.cs @@ -32,6 +32,7 @@ public static AuthenticatorAttestationResponse Parse(AuthenticatorAttestationRaw if (null == rawResponse.Response.AttestationObject || 0 == rawResponse.Response.AttestationObject.Length) throw new Fido2VerificationException("Missing AttestationObject"); + // 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse structure to obtain the attestation statement format fmt, the authenticator data authData, and the attestation statement attStmt. CBORObject cborAttestation; try { @@ -39,7 +40,7 @@ public static AuthenticatorAttestationResponse Parse(AuthenticatorAttestationRaw } catch (CBORException ex) { - throw new Fido2VerificationException("Malformed AttestationObject", ex); + throw new Fido2VerificationException("AttestationObject invalid CBOR", ex); } if (null == cborAttestation["fmt"] @@ -65,33 +66,31 @@ public static AuthenticatorAttestationResponse Parse(AuthenticatorAttestationRaw public async Task VerifyAsync(CredentialCreateOptions originalOptions, Fido2Configuration config, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, IMetadataService metadataService, byte[] requestTokenBindingId) { - BaseVerify(config.Origin, originalOptions.Challenge, requestTokenBindingId); - // verify challenge is same as we expected - // verify origin - // done in baseclass + // https://www.w3.org/TR/webauthn/#registering-a-new-credential + // 1. Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON. + // 2. Let C, the client data claimed as collected during the credential creation, be the result of running an implementation-specific JSON parser on JSONtext. + // Note: C may be any implementation-specific data structure representation, as long as C’s components are referenceable, as required by this algorithm. + // Above handled in base class constructor + // 3. Verify that the value of C.type is webauthn.create if (Type != "webauthn.create") throw new Fido2VerificationException("AttestationResponse is not type webauthn.create"); + // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call. + // 5. Verify that the value of C.origin matches the Relying Party's origin. + // 6. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained. + // If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection. + BaseVerify(config.Origin, originalOptions.Challenge, requestTokenBindingId); + if (Raw.Id == null || Raw.Id.Length == 0) throw new Fido2VerificationException("AttestationResponse is missing Id"); if (Raw.Type != PublicKeyCredentialType.PublicKey) throw new Fido2VerificationException("AttestationResponse is missing type with value 'public-key'"); - if (null == AttestationObject.AuthData || 0 == AttestationObject.AuthData.Length) - throw new Fido2VerificationException("Missing or malformed authData"); - var authData = new AuthenticatorData(AttestationObject.AuthData); - // 6 - //todo: Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection - // over which the assertion was obtained.If Token Binding was used on that TLS connection, - // also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection. - // This is done in BaseVerify. - // TODO: test that implmentation - - // 7 - // Compute the hash of response.clientDataJSON using SHA - 256. + + // 7. Compute the hash of response.clientDataJSON using SHA-256. byte[] clientDataHash, rpIdHash; using (var sha = CryptoUtils.GetHasher(HashAlgorithmName.SHA256)) { @@ -99,35 +98,39 @@ public async Task VerifyAsync(CredentialCreateOp rpIdHash = sha.ComputeHash(Encoding.UTF8.GetBytes(originalOptions.Rp.Id)); } - // 9 - // Verify that the RP ID hash in authData is indeed the SHA - 256 hash of the RP ID expected by the RP. + // 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse structure to obtain the attestation statement format fmt, the authenticator data authData, and the attestation statement attStmt. + // Handled in AuthenticatorAttestationResponse::Parse() + + // 9. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party if (false == authData.RpIdHash.SequenceEqual(rpIdHash)) throw new Fido2VerificationException("Hash mismatch RPID"); - // 10 - // Verify that the User Present bit of the flags in authData is set. + // 10. Verify that the User Present bit of the flags in authData is set. if (false == authData.UserPresent) throw new Fido2VerificationException("User Present flag not set in authenticator data"); - // 11 - // If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set. + // 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set. // see authData.UserVerified + // TODO: Make this a configurable option and add check to require - // 12 - // Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected - // todo: Implement sort of like this: ClientExtensions.Keys.Any(x => options.extensions.contains(x); + // 12. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, + // considering the client extension input values that were given as the extensions option in the create() call. In particular, any extension identifier values + // in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., + // no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use. + + // TODO?: Implement sort of like this: ClientExtensions.Keys.Any(x => options.extensions.contains(x); if (false == authData.HasAttestedCredentialData) throw new Fido2VerificationException("Attestation flag not set on attestation data"); - // 13 - // Determine the attestation statement format by performing a US ASCII case-sensitive match on fmt against the set of supported WebAuthn Attestation Statement Format Identifier values. The up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the in the IANA registry of the same name [WebAuthn-Registries]. + // 13. 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. + // An up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the IANA registry of the same name // https://www.w3.org/TR/webauthn/#defined-attestation-formats AttestationFormat.AttestationFormat verifier; switch (AttestationObject.Fmt) { - // 14 - // validate the attStmt + // 14. 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 the hash of the serialized client data computed in step 7 case "none": // https://www.w3.org/TR/webauthn/#none-attestation verifier = new None(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash); @@ -135,7 +138,7 @@ public async Task VerifyAsync(CredentialCreateOp case "tpm": // https://www.w3.org/TR/webauthn/#tpm-attestation - verifier = new Tpm(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, config.RequireValidAttestationRoot); + verifier = new Tpm(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, metadataService); break; case "android-key": @@ -150,57 +153,36 @@ public async Task VerifyAsync(CredentialCreateOp case "fido-u2f": // https://www.w3.org/TR/webauthn/#fido-u2f-attestation - verifier = new FidoU2f(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, metadataService, config.RequireValidAttestationRoot); + verifier = new FidoU2f(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, metadataService); break; case "packed": // https://www.w3.org/TR/webauthn/#packed-attestation - verifier = new Packed(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, metadataService, config.RequireValidAttestationRoot); + verifier = new Packed(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, metadataService); break; default: throw new Fido2VerificationException("Missing or unknown attestation type"); } verifier.Verify(); - /* - * 15 - * 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. - * */ - - /* - * 16 - * Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, as follows: https://www.w3.org/TR/webauthn/#registering-a-new-credential - * */ - // use aaguid (authData.AttData.Aaguid) to find root certs in metadata - // use root plus trustPath to build trust chain - // implemented for AttestationObject.Fmt == "packed" in packed specific verifier - - /* - * 17 - * Check that the credentialId is not yet registered to any other user. - * If registration is requested for a credential that is already registered to a different user, the Relying Party SHOULD fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting the older registration. - * */ + // 15. 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. + // Done for "fido-u2f", "packed", and "tpm" inside format-specific verifier above + + // 16. Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, as follows: + // If self attestation was used, check if self attestation is acceptable under Relying Party policy. + // If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in the set of acceptable trust anchors obtained in step 15. + // Otherwise, use the X.509 certificates returned by the verification procedure to verify that the attestation public key correctly chains up to an acceptable root certificate. + + // 17. Check that the credentialId is not yet registered to any other user. + // If registration is requested for a credential that is already registered to a different user, the Relying Party SHOULD fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting the older registration if (false == await isCredentialIdUniqueToUser(new IsCredentialIdUniqueToUserParams(authData.AttestedCredentialData.CredentialID, originalOptions.User))) { throw new Fido2VerificationException("CredentialId is not unique to this user"); } - /* - * 18 - * If the attestation statement attStmt verified successfully and is found to be trustworthy, then register the new credential with the account that was denoted in the options.user passed to create(), by associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as appropriate for the Relying Party's system. - * */ - // This is handled by code att call site and result object. - - - /* - * 19 - * If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above, the Relying Party SHOULD fail the registration ceremony. - * NOTE: However, if permitted by policy, the Relying Party MAY register the credential ID and credential public key but treat the credential as one with self attestation (see §6.3.3 Attestation Types). If doing so, the Relying Party is asserting there is no cryptographic proof that the public key credential has been generated by a particular authenticator model. See [FIDOSecRef] and [UAFProtocol] for a more detailed discussion. - * */ - + // 18. If the attestation statement attStmt verified successfully and is found to be trustworthy, then register the new credential with the account that was denoted in the options.user passed to create(), + // by associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as appropriate for the Relying Party's system. var result = new AttestationVerificationSuccess() { CredentialId = authData.AttestedCredentialData.CredentialID, @@ -212,6 +194,8 @@ public async Task VerifyAsync(CredentialCreateOp }; return result; + // 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above, the Relying Party SHOULD fail the registration ceremony. + // This implementation throws if the outputs are not trustworthy for a particular attestation type. } /// diff --git a/Src/Fido2/AuthenticatorResponse.cs b/Src/Fido2/AuthenticatorResponse.cs index b5be4ca6..334eaeca 100644 --- a/Src/Fido2/AuthenticatorResponse.cs +++ b/Src/Fido2/AuthenticatorResponse.cs @@ -14,12 +14,16 @@ protected AuthenticatorResponse(byte[] clientDataJson) { if (null == clientDataJson) throw new Fido2VerificationException("clientDataJson cannot be null"); - var stringx = Encoding.UTF8.GetString(clientDataJson); + // 1. Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON + var JSONtext = Encoding.UTF8.GetString(clientDataJson); + // 2. Let C, the client data claimed as collected during the credential creation, be the result of running an implementation-specific JSON parser on JSONtext + // Note: C may be any implementation-specific data structure representation, as long as C’s components are referenceable, as required by this algorithm. + // We call this AuthenticatorResponse AuthenticatorResponse response; try { - response = JsonConvert.DeserializeObject(stringx); + response = JsonConvert.DeserializeObject(JSONtext); } catch (Exception e) when (e is JsonReaderException || e is JsonSerializationException) { @@ -53,19 +57,22 @@ private AuthenticatorResponse() protected void BaseVerify(string expectedOrigin, byte[] originalChallenge, byte[] requestTokenBindingId) { + if (Type != "webauthn.create" && Type != "webauthn.get") + throw new Fido2VerificationException($"Type not equal to 'webauthn.create' or 'webauthn.get'. Was: '{Type}'"); + if (null == Challenge) throw new Fido2VerificationException("Challenge cannot be null"); - // verify challenge is same + // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call if (!Challenge.SequenceEqual(originalChallenge)) throw new Fido2VerificationException("Challenge not equal to original challenge"); + // 5. Verify that the value of C.origin matches the Relying Party's origin. if (Origin != expectedOrigin) throw new Fido2VerificationException($"Origin {Origin} not equal to original origin {expectedOrigin}"); - if (Type != "webauthn.create" && Type != "webauthn.get") - throw new Fido2VerificationException($"Type not equal to 'webauthn.create' or 'webauthn.get'. Was: '{Type}'"); - + // 6. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained. + // If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection. if (TokenBinding != null) { TokenBinding.Verify(requestTokenBindingId); diff --git a/Src/Fido2/Fido2.csproj b/Src/Fido2/Fido2.csproj index bbe15732..12183d7d 100644 --- a/Src/Fido2/Fido2.csproj +++ b/Src/Fido2/Fido2.csproj @@ -17,8 +17,8 @@ - - + + diff --git a/Src/Fido2/Fido2NetLib.cs b/Src/Fido2/Fido2NetLib.cs index fcd19009..b37c86f0 100644 --- a/Src/Fido2/Fido2NetLib.cs +++ b/Src/Fido2/Fido2NetLib.cs @@ -51,7 +51,6 @@ public CredentialCreateOptions RequestNewCredential( AttestationConveyancePreference attestationPreference, AuthenticationExtensionsClientInputs extensions = null) { - // note: I have no idea if this crypto is ok... var challenge = new byte[_config.ChallengeSize]; _crypto.GetBytes(challenge); diff --git a/Src/Fido2/Metadata/StaticMetadataRepository.cs b/Src/Fido2/Metadata/StaticMetadataRepository.cs index 7a1f0fca..950486df 100644 --- a/Src/Fido2/Metadata/StaticMetadataRepository.cs +++ b/Src/Fido2/Metadata/StaticMetadataRepository.cs @@ -178,10 +178,60 @@ public async Task GetToc() { AttestationTypes = new ushort[] { - (ushort)MetadataAttestationType.ATTESTATION_BASIC_FULL + (ushort)MetadataAttestationType.ATTESTATION_BASIC_SURROGATE }, Hash = "", - Description = "Windows Hello software authenticator" + Description = "Windows Hello Software Authenticator", + AuthenticatorVersion = 1, + ProtocolFamily = "fido2", + Upv = new UafVersion[] + { + new UafVersion() + { + Major = 1, + Minor = 0 + } + }, + AssertionScheme = "FIDOV2", + AuthenticationAlgorithm = 12, + PublicKeyAlgAndEncoding = 260, + UserVerificationDetails = new VerificationMethodDescriptor[][] + { + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 2 // USER_VERIFY_FINGERPRINT_INTERNAL + } + }, + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 4 // USER_VERIFY_PASSCODE_INTERNAL + } + }, + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 16 // USER_VERIFY_FACEPRINT_INTERNAL + } + }, + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 64 // USER_VERIFY_EYEPRINT_INTERNAL + } + } + }, + KeyProtection = 2, // KEY_PROTECTION_HARDWARE + MatcherProtection = 1, // MATCHER_PROTECTION_SOFTWARE + AttachmentHint = 1, // ATTACHMENT_HINT_INTERNAL + IsSecondFactorOnly = false, + TcDisplay = 0, + Icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC" } }; _entries.Add(new Guid(msftWhfbSoftware.AaGuid), msftWhfbSoftware); @@ -222,10 +272,64 @@ public async Task GetToc() { AttestationTypes = new ushort[] { - (ushort)MetadataAttestationType.ATTESTATION_BASIC_FULL + 15888 }, Hash = "", - Description = "Windows Hello hardware authenticator" + Description = "Windows Hello Hardware Authenticator", + AttestationRootCertificates = new string[] + { + "MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=", + }, + AuthenticatorVersion = 1, + ProtocolFamily = "fido2", + Upv = new UafVersion[] + { + new UafVersion() + { + Major = 1, + Minor = 0 + } + }, + AssertionScheme = "FIDOV2", + AuthenticationAlgorithm = 12, + PublicKeyAlgAndEncoding = 260, + UserVerificationDetails = new VerificationMethodDescriptor[][] + { + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 2 // USER_VERIFY_FINGERPRINT_INTERNAL + } + }, + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 4 // USER_VERIFY_PASSCODE_INTERNAL + } + }, + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 16 // USER_VERIFY_FACEPRINT_INTERNAL + } + }, + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 64 // USER_VERIFY_EYEPRINT_INTERNAL + } + } + }, + KeyProtection = 2, // KEY_PROTECTION_HARDWARE + MatcherProtection = 1, // MATCHER_PROTECTION_SOFTWARE + AttachmentHint = 1, // ATTACHMENT_HINT_INTERNAL + IsSecondFactorOnly = false, + TcDisplay = 0, + Icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC" } }; _entries.Add(new Guid(msftWhfbHardware.AaGuid), msftWhfbHardware); @@ -247,7 +351,61 @@ public async Task GetToc() (ushort)MetadataAttestationType.ATTESTATION_BASIC_FULL }, Hash = "", - Description = "Windows Hello VBS hardware authenticator" + Description = "Windows Hello VBS Hardware Authenticator", + AttestationRootCertificates = new string[] + { + "MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=", + }, + AuthenticatorVersion = 1, + ProtocolFamily = "fido2", + Upv = new UafVersion[] + { + new UafVersion() + { + Major = 1, + Minor = 0 + } + }, + AssertionScheme = "FIDOV2", + AuthenticationAlgorithm = 12, + PublicKeyAlgAndEncoding = 260, + UserVerificationDetails = new VerificationMethodDescriptor[][] + { + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 2 // USER_VERIFY_FINGERPRINT_INTERNAL + } + }, + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 4 // USER_VERIFY_PASSCODE_INTERNAL + } + }, + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 16 // USER_VERIFY_FACEPRINT_INTERNAL + } + }, + new VerificationMethodDescriptor[] + { + new VerificationMethodDescriptor() + { + UserVerification = 64 // USER_VERIFY_EYEPRINT_INTERNAL + } + } + }, + KeyProtection = 6, // KEY_PROTECTION_HARDWARE & KEY_PROTECTION_TEE + MatcherProtection = 2, // MATCHER_PROTECTION_TEE + AttachmentHint = 1, // ATTACHMENT_HINT_INTERNAL + IsSecondFactorOnly = false, + TcDisplay = 0, + Icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC" } }; _entries.Add(new Guid(msftWhfbHardwareVbs.AaGuid), msftWhfbHardwareVbs); diff --git a/Src/Fido2/Objects/AttestationType.cs b/Src/Fido2/Objects/AttestationType.cs deleted file mode 100644 index 8f60a459..00000000 --- a/Src/Fido2/Objects/AttestationType.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Fido2NetLib.Objects -{ - public class AttestationType : TypedString - { - public static readonly AttestationType None = new AttestationType("none"); - public static readonly AttestationType Basic = new AttestationType("basic"); - public static readonly AttestationType Self = new AttestationType("self"); - public static readonly AttestationType AttCa = new AttestationType("attca"); - public static readonly AttestationType ECDAA = new AttestationType("ecdaa"); - - private AttestationType(string value) : base(value) - { - } - } -} diff --git a/Test/AuthenticatorResponse.cs b/Test/AuthenticatorResponse.cs new file mode 100644 index 00000000..70c49e63 --- /dev/null +++ b/Test/AuthenticatorResponse.cs @@ -0,0 +1,856 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Fido2NetLib; +using Fido2NetLib.Objects; +using Newtonsoft.Json; +using NSec.Cryptography; +using PeterO.Cbor; +using Xunit; + +namespace Test +{ + public class AuthenticatorResponse + { + [Fact] + public void TestAuthenticatorAttestationRawResponse() + { + var challenge = RandomGenerator.Default.GenerateBytes(128); + var clientDataJson = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject + ( + new + { + Type = "webauthn.create", + Challenge = challenge, + Origin = "fido2.azurewebsites.net", + } + ) + ); + var rawResponse = new AuthenticatorAttestationRawResponse + { + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = CBORObject.NewMap().EncodeToBytes(), + ClientDataJson = clientDataJson + }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = true, + AuthenticatorSelection = true, + BiometricAuthenticatorPerformanceBounds = true, + GenericTransactionAuthorization = new byte[] { 0xf1, 0xd0 }, + SimpleTransactionAuthorization = "test", + Extensions = new string[] { "foo", "bar" }, + Example = "test", + Location = new GeoCoordinatePortable.GeoCoordinate(42.523714, -71.040860), + UserVerificationIndex = new byte[] { 0xf1, 0xd0 }, + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + Assert.Equal(PublicKeyCredentialType.PublicKey, rawResponse.Type); + Assert.True(rawResponse.Id.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.True(rawResponse.RawId.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.True(rawResponse.Response.AttestationObject.SequenceEqual(new byte[] { 0xa0 })); + Assert.True(rawResponse.Response.ClientDataJson.SequenceEqual(clientDataJson)); + Assert.True(rawResponse.Extensions.AppID); + Assert.True(rawResponse.Extensions.AuthenticatorSelection); + Assert.True(rawResponse.Extensions.BiometricAuthenticatorPerformanceBounds); + Assert.True(rawResponse.Extensions.GenericTransactionAuthorization.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.Equal("test", rawResponse.Extensions.SimpleTransactionAuthorization); + Assert.Equal(rawResponse.Extensions.Extensions, new string[] { "foo", "bar" }); + Assert.Equal("test", rawResponse.Extensions.Example); + Assert.Equal(42.523714, rawResponse.Extensions.Location.Latitude); + Assert.Equal(-71.040860, rawResponse.Extensions.Location.Longitude); + Assert.True(rawResponse.Extensions.UserVerificationIndex.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.Equal((ulong)4, rawResponse.Extensions.UserVerificationMethod[0][0]); + } + + [Fact] + public void TestAuthenticatorAttestationRawResponseNull() + { + var ex = Assert.Throws(() => AuthenticatorAttestationResponse.Parse(null)); + Assert.Equal("Expected rawResponse, got null", ex.Message); + } + + [Fact] + public void TestAuthenticatorAttestationResponseNull() + { + var rawResponse = new AuthenticatorAttestationRawResponse + { + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Response = null, + }; + var ex = Assert.Throws(() => AuthenticatorAttestationResponse.Parse(rawResponse)); + Assert.Equal("Expected rawResponse, got null", ex.Message); + } + + [Theory] + [InlineData(null)] + [InlineData(new byte[0])] + public void TestAuthenticatorAttestationReponseAttestationObjectNull(byte[] value) + { + var rawResponse = new AuthenticatorAttestationRawResponse + { + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = value, + } + }; + var ex = Assert.Throws(() => AuthenticatorAttestationResponse.Parse(rawResponse)); + Assert.Equal("Missing AttestationObject", ex.Message); + } + + [Theory] + [InlineData(new byte[] { 0x66, 0x6f, 0x6f })] + public void TestAuthenticatorAttestationObjectBadCBOR(byte[] value) + { + var rawResponse = new AuthenticatorAttestationRawResponse + { + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = value, + } + }; + + var ex = Assert.Throws(() => AuthenticatorAttestationResponse.Parse(rawResponse)); + Assert.Equal("AttestationObject invalid CBOR", ex.Message); + } + + [Theory] + [InlineData(new byte[] { 0xa1, 0x63, 0x66, 0x6d, 0x74, 0xf6 })] // "fmt", null + [InlineData(new byte[] { 0xa1, 0x63, 0x66, 0x6d, 0x74, 0x18, 0x2a })] // "fmt", 42 + [InlineData(new byte[] { 0xa1, 0x67, 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74, 0xf6 })] // "attStmt", null + [InlineData(new byte[] { 0xa1, 0x67, 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74, 0x67, 0x61, 0x74, 0x74, 0x53, 0x74, 0x6d, 0x74 })] // "attStmt", "attStmt" + [InlineData(new byte[] { 0xa1, 0x68, 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, 0xf6 })] // "authData", null + [InlineData(new byte[] { 0xa1, 0x68, 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, 0x68, 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61 })] // "authData", "authData" + public void TestAuthenticatorAttestationObjectMalformed(byte[] value) + { + var rawResponse = new AuthenticatorAttestationRawResponse + { + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = value, + } + }; + + var ex = Assert.Throws(() => AuthenticatorAttestationResponse.Parse(rawResponse)); + Assert.Equal("Malformed AttestationObject", ex.Message); + } + + [Fact] + public void TestAuthenticatorAttestationResponseInvalidType() + { + var challenge = RandomGenerator.Default.GenerateBytes(128); + var rp = "fido2.azurewebsites.net"; + var clientDataJson = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject + ( + new + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + } + ) + ); + var rawResponse = new AuthenticatorAttestationRawResponse + { + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = CBORObject.NewMap().Add("fmt", "testing").Add("attStmt", CBORObject.NewMap()).Add("authData", new byte[0]).EncodeToBytes(), + ClientDataJson = clientDataJson + }, + }; + + var origChallenge = new CredentialCreateOptions + { + Attestation = AttestationConveyancePreference.Direct, + AuthenticatorSelection = new AuthenticatorSelection + { + AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, + RequireResidentKey = true, + UserVerification = UserVerificationRequirement.Required, + }, + Challenge = challenge, + ErrorMessage = "", + PubKeyCredParams = new List() + { + new PubKeyCredParam + { + Alg = -7, + Type = PublicKeyCredentialType.PublicKey, + } + }, + Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), + Status = "ok", + User = new Fido2User + { + Name = "testuser", + Id = Encoding.UTF8.GetBytes("testuser"), + DisplayName = "Test User", + }, + Timeout = 60000, + }; + + IsCredentialIdUniqueToUserAsyncDelegate callback = (args) => + { + return Task.FromResult(true); + }; + + var lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origin = rp, + }); + + var ex = Assert.ThrowsAsync(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); + Assert.Equal("AttestationResponse is not type webauthn.create", ex.Result.Message); + } + + [Theory] + [InlineData(null)] + [InlineData(new byte[0])] + public void TestAuthenticatorAttestationResponseInvalidRawId(byte[] value) + { + var challenge = RandomGenerator.Default.GenerateBytes(128); + var rp = "fido2.azurewebsites.net"; + var clientDataJson = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject + ( + new + { + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, + } + ) + ); + var rawResponse = new AuthenticatorAttestationRawResponse + { + Type = PublicKeyCredentialType.PublicKey, + Id = value, + RawId = value, + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = CBORObject.NewMap().Add("fmt", "testing").Add("attStmt", CBORObject.NewMap()).Add("authData", new byte[0]).EncodeToBytes(), + ClientDataJson = clientDataJson + }, + }; + + var origChallenge = new CredentialCreateOptions + { + Attestation = AttestationConveyancePreference.Direct, + AuthenticatorSelection = new AuthenticatorSelection + { + AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, + RequireResidentKey = true, + UserVerification = UserVerificationRequirement.Required, + }, + Challenge = challenge, + ErrorMessage = "", + PubKeyCredParams = new List() + { + new PubKeyCredParam + { + Alg = -7, + Type = PublicKeyCredentialType.PublicKey, + } + }, + Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), + Status = "ok", + User = new Fido2User + { + Name = "testuser", + Id = Encoding.UTF8.GetBytes("testuser"), + DisplayName = "Test User", + }, + Timeout = 60000, + }; + + IsCredentialIdUniqueToUserAsyncDelegate callback = (args) => + { + return Task.FromResult(true); + }; + + var lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origin = rp, + }); + + var ex = Assert.ThrowsAsync(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); + Assert.Equal("AttestationResponse is missing Id", ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAttestationResponseInvalidRawType() + { + var challenge = RandomGenerator.Default.GenerateBytes(128); + var rp = "fido2.azurewebsites.net"; + var clientDataJson = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject + ( + new + { + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, + } + ) + ); + var rawResponse = new AuthenticatorAttestationRawResponse + { + Type = null, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = CBORObject.NewMap().Add("fmt", "testing").Add("attStmt", CBORObject.NewMap()).Add("authData", new byte[0]).EncodeToBytes(), + ClientDataJson = clientDataJson + }, + }; + + var origChallenge = new CredentialCreateOptions + { + Attestation = AttestationConveyancePreference.Direct, + AuthenticatorSelection = new AuthenticatorSelection + { + AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, + RequireResidentKey = true, + UserVerification = UserVerificationRequirement.Required, + }, + Challenge = challenge, + ErrorMessage = "", + PubKeyCredParams = new List() + { + new PubKeyCredParam + { + Alg = -7, + Type = PublicKeyCredentialType.PublicKey, + } + }, + Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), + Status = "ok", + User = new Fido2User + { + Name = "testuser", + Id = Encoding.UTF8.GetBytes("testuser"), + DisplayName = "Test User", + }, + Timeout = 60000, + }; + + IsCredentialIdUniqueToUserAsyncDelegate callback = (args) => + { + return Task.FromResult(true); + }; + + var lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origin = rp, + }); + + var ex = Assert.ThrowsAsync(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); + Assert.Equal("AttestationResponse is missing type with value 'public-key'", ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAttestationResponseRpidMismatch() + { + var challenge = RandomGenerator.Default.GenerateBytes(128); + var rp = "fido2.azurewebsites.net"; + var authData = new AuthenticatorData( + SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes("passwordless.dev")), + AuthenticatorFlags.UV, + 0, + null, + null + ).ToByteArray(); + var clientDataJson = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject + ( + new + { + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, + } + ) + ); + var rawResponse = new AuthenticatorAttestationRawResponse + { + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = CBORObject.NewMap().Add("fmt", "testing").Add("attStmt", CBORObject.NewMap()).Add("authData", authData).EncodeToBytes(), + ClientDataJson = clientDataJson + }, + }; + + var origChallenge = new CredentialCreateOptions + { + Attestation = AttestationConveyancePreference.Direct, + AuthenticatorSelection = new AuthenticatorSelection + { + AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, + RequireResidentKey = true, + UserVerification = UserVerificationRequirement.Required, + }, + Challenge = challenge, + ErrorMessage = "", + PubKeyCredParams = new List() + { + new PubKeyCredParam + { + Alg = -7, + Type = PublicKeyCredentialType.PublicKey, + } + }, + Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), + Status = "ok", + User = new Fido2User + { + Name = "testuser", + Id = Encoding.UTF8.GetBytes("testuser"), + DisplayName = "Test User", + }, + Timeout = 60000, + }; + + IsCredentialIdUniqueToUserAsyncDelegate callback = (args) => + { + return Task.FromResult(true); + }; + + var lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origin = rp, + }); + + var ex = Assert.ThrowsAsync(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); + Assert.Equal("Hash mismatch RPID", ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAttestationResponseNotUserPresent() + { + var challenge = RandomGenerator.Default.GenerateBytes(128); + var rp = "fido2.azurewebsites.net"; + var authData = new AuthenticatorData( + SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(rp)), + AuthenticatorFlags.UV, + 0, + null, + null + ).ToByteArray(); + var clientDataJson = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject + ( + new + { + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, + } + ) + ); + var rawResponse = new AuthenticatorAttestationRawResponse + { + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = CBORObject.NewMap().Add("fmt", "testing").Add("attStmt", CBORObject.NewMap()).Add("authData", authData).EncodeToBytes(), + ClientDataJson = clientDataJson + }, + }; + + var origChallenge = new CredentialCreateOptions + { + Attestation = AttestationConveyancePreference.Direct, + AuthenticatorSelection = new AuthenticatorSelection + { + AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, + RequireResidentKey = true, + UserVerification = UserVerificationRequirement.Required, + }, + Challenge = challenge, + ErrorMessage = "", + PubKeyCredParams = new List() + { + new PubKeyCredParam + { + Alg = -7, + Type = PublicKeyCredentialType.PublicKey, + } + }, + Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), + Status = "ok", + User = new Fido2User + { + Name = "testuser", + Id = Encoding.UTF8.GetBytes("testuser"), + DisplayName = "Test User", + }, + Timeout = 60000, + }; + + IsCredentialIdUniqueToUserAsyncDelegate callback = (args) => + { + return Task.FromResult(true); + }; + + var lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origin = rp, + }); + + var ex = Assert.ThrowsAsync(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); + Assert.Equal("User Present flag not set in authenticator data", ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAttestationResponseNoAttestedCredentialData() + { + var challenge = RandomGenerator.Default.GenerateBytes(128); + var rp = "fido2.azurewebsites.net"; + var authData = new AuthenticatorData( + SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(rp)), + AuthenticatorFlags.UP | AuthenticatorFlags.UV, + 0, + null, + null + ).ToByteArray(); + var clientDataJson = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject + ( + new + { + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, + } + ) + ); + var rawResponse = new AuthenticatorAttestationRawResponse + { + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = CBORObject.NewMap().Add("fmt", "testing").Add("attStmt", CBORObject.NewMap()).Add("authData", authData).EncodeToBytes(), + ClientDataJson = clientDataJson + }, + }; + + var origChallenge = new CredentialCreateOptions + { + Attestation = AttestationConveyancePreference.Direct, + AuthenticatorSelection = new AuthenticatorSelection + { + AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, + RequireResidentKey = true, + UserVerification = UserVerificationRequirement.Required, + }, + Challenge = challenge, + ErrorMessage = "", + PubKeyCredParams = new List() + { + new PubKeyCredParam + { + Alg = -7, + Type = PublicKeyCredentialType.PublicKey, + } + }, + Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), + Status = "ok", + User = new Fido2User + { + Name = "testuser", + Id = Encoding.UTF8.GetBytes("testuser"), + DisplayName = "Test User", + }, + Timeout = 60000, + }; + + IsCredentialIdUniqueToUserAsyncDelegate callback = (args) => + { + return Task.FromResult(true); + }; + + var lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origin = rp, + }); + + var ex = Assert.ThrowsAsync(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); + Assert.Equal("Attestation flag not set on attestation data", ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAttestationResponseUnknownAttestationType() + { + var challenge = RandomGenerator.Default.GenerateBytes(128); + var rp = "fido2.azurewebsites.net"; + var acd = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray()); + var authData = new AuthenticatorData( + SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(rp)), + AuthenticatorFlags.AT | AuthenticatorFlags.UP | AuthenticatorFlags.UV, + 0, + acd, + null + ).ToByteArray(); + var clientDataJson = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject + ( + new + { + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, + } + ) + ); + var rawResponse = new AuthenticatorAttestationRawResponse + { + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = CBORObject.NewMap().Add("fmt", "testing").Add("attStmt", CBORObject.NewMap()).Add("authData", authData).EncodeToBytes(), + ClientDataJson = clientDataJson + }, + }; + + var origChallenge = new CredentialCreateOptions + { + Attestation = AttestationConveyancePreference.Direct, + AuthenticatorSelection = new AuthenticatorSelection + { + AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, + RequireResidentKey = true, + UserVerification = UserVerificationRequirement.Required, + }, + Challenge = challenge, + ErrorMessage = "", + PubKeyCredParams = new List() + { + new PubKeyCredParam + { + Alg = -7, + Type = PublicKeyCredentialType.PublicKey, + } + }, + Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), + Status = "ok", + User = new Fido2User + { + Name = "testuser", + Id = Encoding.UTF8.GetBytes("testuser"), + DisplayName = "Test User", + }, + Timeout = 60000, + }; + + IsCredentialIdUniqueToUserAsyncDelegate callback = (args) => + { + return Task.FromResult(true); + }; + + var lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origin = rp, + }); + + var ex = Assert.ThrowsAsync(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); + Assert.Equal("Missing or unknown attestation type", ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAttestationResponseNotUniqueCredId() + { + var challenge = RandomGenerator.Default.GenerateBytes(128); + var rp = "fido2.azurewebsites.net"; + var acd = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray()); + var authData = new AuthenticatorData( + SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(rp)), + AuthenticatorFlags.AT | AuthenticatorFlags.UP | AuthenticatorFlags.UV, + 0, + acd, + null + ).ToByteArray(); + var clientDataJson = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject + ( + new + { + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, + } + ) + ); + var rawResponse = new AuthenticatorAttestationRawResponse + { + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Response = new AuthenticatorAttestationRawResponse.ResponseData() + { + AttestationObject = CBORObject.NewMap().Add("fmt", "none").Add("attStmt", CBORObject.NewMap()).Add("authData", authData).EncodeToBytes(), + ClientDataJson = clientDataJson + }, + }; + + var origChallenge = new CredentialCreateOptions + { + Attestation = AttestationConveyancePreference.Direct, + AuthenticatorSelection = new AuthenticatorSelection + { + AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform, + RequireResidentKey = true, + UserVerification = UserVerificationRequirement.Required, + }, + Challenge = challenge, + ErrorMessage = "", + PubKeyCredParams = new List() + { + new PubKeyCredParam + { + Alg = -7, + Type = PublicKeyCredentialType.PublicKey, + } + }, + Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), + Status = "ok", + User = new Fido2User + { + Name = "testuser", + Id = Encoding.UTF8.GetBytes("testuser"), + DisplayName = "Test User", + }, + Timeout = 60000, + }; + + IsCredentialIdUniqueToUserAsyncDelegate callback = (args) => + { + return Task.FromResult(false); + }; + + var lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origin = rp, + }); + + var ex = Assert.ThrowsAsync(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); + Assert.Equal("CredentialId is not unique to this user", ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionRawResponse() + { + var challenge = RandomGenerator.Default.GenerateBytes(128); + var clientDataJson = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject + ( + new + { + Type = "webauthn.get", + Challenge = challenge, + Origin = "fido2.azurewebsites.net", + } + ) + ); + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new byte[] { 0xf1, 0xd0 }, + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = true, + AuthenticatorSelection = true, + BiometricAuthenticatorPerformanceBounds = true, + GenericTransactionAuthorization = new byte[] { 0xf1, 0xd0 }, + SimpleTransactionAuthorization = "test", + Extensions = new string[] { "foo", "bar" }, + Example = "test", + Location = new GeoCoordinatePortable.GeoCoordinate(42.523714, -71.040860), + UserVerificationIndex = new byte[] { 0xf1, 0xd0 }, + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + Assert.Equal(PublicKeyCredentialType.PublicKey, assertionResponse.Type); + Assert.True(assertionResponse.Id.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.True(assertionResponse.RawId.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.True(assertionResponse.Response.AuthenticatorData.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.True(assertionResponse.Response.Signature.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.True(assertionResponse.Response.ClientDataJson.SequenceEqual(clientDataJson)); + Assert.True(assertionResponse.Response.UserHandle.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.True(assertionResponse.Extensions.AppID); + Assert.True(assertionResponse.Extensions.AuthenticatorSelection); + Assert.True(assertionResponse.Extensions.BiometricAuthenticatorPerformanceBounds); + Assert.True(assertionResponse.Extensions.GenericTransactionAuthorization.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.Equal("test", assertionResponse.Extensions.SimpleTransactionAuthorization); + Assert.Equal(assertionResponse.Extensions.Extensions, new string[] { "foo", "bar" }); + Assert.Equal("test", assertionResponse.Extensions.Example); + Assert.Equal(42.523714, assertionResponse.Extensions.Location.Latitude); + Assert.Equal(-71.040860, assertionResponse.Extensions.Location.Longitude); + Assert.True(assertionResponse.Extensions.UserVerificationIndex.SequenceEqual(new byte[] { 0xf1, 0xd0 })); + Assert.Equal((ulong)4, assertionResponse.Extensions.UserVerificationMethod[0][0]); + } + } +} diff --git a/Test/Fido2Tests.cs b/Test/Fido2Tests.cs index 8c40b6e1..395dfa06 100644 --- a/Test/Fido2Tests.cs +++ b/Test/Fido2Tests.cs @@ -17,7 +17,6 @@ using Asn1; using System.Security.Cryptography.X509Certificates; using Fido2NetLib.AttestationFormat; -using Asn1; namespace fido2_net_lib.Test { @@ -204,6 +203,7 @@ public Attestation() idFidoGenCeAaguidExt = new X509Extension(oidIdFidoGenCeAaguid, _asnEncodedAaguid, false); } + public async Task MakeAttestationResponse() { _attestationObject.Set("authData", _authData); @@ -217,6 +217,25 @@ public Attestation() { AttestationObject = _attestationObject.EncodeToBytes(), ClientDataJson = _clientDataJson, + }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = true, + AuthenticatorSelection = true, + BiometricAuthenticatorPerformanceBounds = true, + GenericTransactionAuthorization = new byte[] { 0xf1, 0xd0 }, + SimpleTransactionAuthorization = "test", + Extensions = new string[] { "foo", "bar" }, + Example = "test", + Location = new GeoCoordinatePortable.GeoCoordinate(42.523714, -71.040860), + UserVerificationIndex = new byte[] { 0xf1, 0xd0 }, + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, } }; diff --git a/Test/Test.csproj b/Test/Test.csproj index 9c2b1e05..1d3027e5 100644 --- a/Test/Test.csproj +++ b/Test/Test.csproj @@ -18,7 +18,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,13 +26,13 @@ - - - + + + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/azure-pipelines.yml b/azure-pipelines.yml index edfce893..07978022 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,6 +22,8 @@ jobs: displayName: dotnet restore inputs: command: restore + includeNuGetOrg: true + noCache: true # Build the main library - job: lib