public override void Verify() { // verify that aaguid is 16 empty bytes (note: required by fido2 conformance testing, could not find this in spec?) if (0 != AuthData.AttestedCredentialData.AaGuid.CompareTo(Guid.Empty)) { throw new Fido2VerificationException("Aaguid was not empty parsing fido-u2f atttestation statement"); } // 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 (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 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()); // 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"); } } } // 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 keyParams = pubKey.ExportParameters(false); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { 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 // 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of [RFC8152]) to CTAP1/U2F public Key format var x = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.X)].GetByteString(); var y = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.Y)].GetByteString(); var publicKeyU2F = new byte[1] { 0x4 }.Concat(x).Concat(y).ToArray(); // 5. Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F) var verificationData = new byte[1] { 0x00 }; verificationData = verificationData .Concat(AuthData.RpIdHash) .Concat(clientDataHash) .Concat(AuthData.AttestedCredentialData.CredentialID) .Concat(publicKeyU2F.ToArray()) .ToArray(); // 6. Verify the sig using verificationData and certificate public key if (null == Sig || CBORType.ByteString != Sig.Type || 0 == Sig.GetByteString().Length) { throw new Fido2VerificationException("Invalid fido-u2f attestation signature"); } byte[] ecsig; try { ecsig = CryptoUtils.SigFromEcDsaSig(Sig.GetByteString(), pubKey.KeySize); } catch (Exception ex) { throw new Fido2VerificationException("Failed to decode fido-u2f attestation signature from ASN.1 encoded form", ex); } var coseAlg = CredentialPublicKey[CBORObject.FromObject(COSE.KeyCommonParameter.Alg)].AsInt32(); var hashAlg = CryptoUtils.algMap[coseAlg]; if (true != pubKey.VerifyData(verificationData, ecsig, hashAlg)) { throw new Fido2VerificationException("Invalid fido-u2f attestation signature"); } }
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 <PubKeyCredParam>() { 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 <Fido2VerificationException>(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); Assert.Equal("CredentialId is not unique to this user", ex.Result.Message); }
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. if (0 == attStmt.Keys.Count || 0 == attStmt.Values.Count) { throw new Fido2VerificationException("Attestation format packed must have attestation statement"); } if (null == Sig || CBORType.ByteString != Sig.Type || 0 == Sig.GetByteString().Length) { throw new Fido2VerificationException("Invalid packed attestation signature"); } if (null == Alg || true != Alg.IsNumber) { throw new Fido2VerificationException("Invalid packed attestation algorithm"); } // 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) { throw new Fido2VerificationException("Malformed x5c array in packed attestation statement"); } var enumerator = X5c.Values.GetEnumerator(); while (enumerator.MoveNext()) { if (null == enumerator || null == enumerator.Current || CBORType.ByteString != enumerator.Current.Type || 0 == enumerator.Current.GetByteString().Length) { throw new Fido2VerificationException("Malformed x5c cert found in packed attestation statement"); } var x5ccert = new X509Certificate2(enumerator.Current.GetByteString()); // X509Certificate2.NotBefore/.NotAfter return LOCAL DateTimes, so // it's correct to compare using DateTime.Now. if (DateTime.Now < x5ccert.NotBefore || DateTime.Now > x5ccert.NotAfter) { throw new Fido2VerificationException("Packed signing certificate expired or not yet valid"); } } // The attestation certificate attestnCert MUST be the first element in the array. var attestnCert = new X509Certificate2(X5c.Values.First().GetByteString()); // 2a. 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 var cpk = new CredentialPublicKey(attestnCert, Alg.AsInt32()); if (true != cpk.Verify(Data, Sig.GetByteString())) { throw new Fido2VerificationException("Invalid full packed signature"); } // Verify that attestnCert meets the requirements in https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements // 2bi. Version MUST be set to 3 if (3 != attestnCert.Version) { throw new Fido2VerificationException("Packed x5c attestation certificate not V3"); } // 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"); } // 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("Attestation 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"); } } // 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(); 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) { 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 packed attestation"); } } // If the authenticator is not listed as one that should produce a basic full attestation, the certificate should be self signed if (!entry?.MetadataStatement?.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_BASIC_FULL) ?? false) { if (trustPath.FirstOrDefault().Subject != trustPath.FirstOrDefault().Issuer) { throw new Fido2VerificationException("Attestation with full attestation from authenticator that does not support full attestation"); } } // Check status resports for authenticator with undesirable status foreach (var report in entry?.StatusReports ?? Enumerable.Empty <StatusReport>()) { if (true == Enum.IsDefined(typeof(UndesiredAuthenticatorStatus), (UndesiredAuthenticatorStatus)report.Status)) { throw new Fido2VerificationException("Authenticator found with undesirable status"); } } } // 3. If ecdaaKeyId is present, then the attestation type is ECDAA else if (null != EcdaaKeyId) { // 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"); // 3b. If successful, return attestation type ECDAA and attestation trust path ecdaaKeyId. // attnType = AttestationType.ECDAA; // trustPath = ecdaaKeyId; } // 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use else { // 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"); } // 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"); } } }
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 VerificationException("Invalid TPM attestation signature"); } if ("2.0" != attStmt["ver"].AsString()) { throw new VerificationException("FIDO2 only supports TPM 2.0"); } // 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"] && CBORType.ByteString == attStmt["pubArea"].Type && 0 != attStmt["pubArea"].GetByteString().Length) { pubArea = new PubArea(attStmt["pubArea"].GetByteString()); } if (null == pubArea || null == pubArea.Unique || 0 == pubArea.Unique.Length) { throw new VerificationException("Missing or malformed pubArea"); } var coseKty = CredentialPublicKey[CBORObject.FromObject(COSE.KeyCommonParameter.KeyType)].AsInt32(); if (3 == coseKty) // RSA { var coseMod = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.N)].GetByteString(); // modulus var coseExp = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.E)].GetByteString(); // exponent if (!coseMod.ToArray().SequenceEqual(pubArea.Unique.ToArray())) { throw new VerificationException("Public key mismatch between pubArea and credentialPublicKey"); } if ((coseExp[0] + (coseExp[1] << 8) + (coseExp[2] << 16)) != pubArea.Exponent) { throw new VerificationException("Public key exponent mismatch between pubArea and credentialPublicKey"); } } else if (2 == coseKty) // ECC { var curve = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.Crv)].AsInt32(); var X = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.X)].GetByteString(); var Y = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.Y)].GetByteString(); if (pubArea.EccCurve != CoseCurveToTpm[curve]) { throw new VerificationException("Curve mismatch between pubArea and credentialPublicKey"); } if (!pubArea.ECPoint.X.SequenceEqual(X)) { throw new VerificationException("X-coordinate mismatch between pubArea and credentialPublicKey"); } if (!pubArea.ECPoint.Y.SequenceEqual(Y)) { throw new VerificationException("Y-coordinate mismatch between pubArea and credentialPublicKey"); } } // Concatenate authenticatorData and clientDataHash to form attToBeSigned. // see data variable // Validate that certInfo is valid CertInfo certInfo = null; if (null != attStmt["certInfo"] && CBORType.ByteString == attStmt["certInfo"].Type && 0 != attStmt["certInfo"].GetByteString().Length) { certInfo = new CertInfo(attStmt["certInfo"].GetByteString()); } if (null == certInfo) { throw new VerificationException("CertInfo invalid parsing TPM format attStmt"); } // 4a. Verify that magic is set to TPM_GENERATED_VALUE // Handled in CertInfo constructor, see CertInfo.Magic // 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) { throw new VerificationException("Invalid TPM attestation algorithm"); } using (var hasher = CryptoUtils.GetHasher(CryptoUtils.HashAlgFromCOSEAlg(Alg.AsInt32()))) { if (!hasher.ComputeHash(Data).SequenceEqual(certInfo.ExtraData)) { throw new VerificationException("Hash value mismatch extraData and attToBeSigned"); } } // 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.HashAlgFromCOSEAlg(certInfo.Alg))) { if (false == hasher.ComputeHash(pubArea.Raw).SequenceEqual(certInfo.AttestedName)) { throw new VerificationException("Hash value mismatch attested and pubArea"); } } // 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 || CBORType.ByteString != X5c.Values.First().Type || 0 == X5c.Values.First().GetByteString().Length) { throw new VerificationException("Malformed x5c in TPM attestation"); } // 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 VerificationException("Bad signature in TPM with aikCert"); } // 5b. Verify that aikCert meets the TPM attestation statement certificate requirements // https://www.w3.org/TR/webauthn/#tpm-cert-requirements // 5bi. Version MUST be set to 3 if (3 != aikCert.Version) { throw new VerificationException("aikCert must be V3"); } // 5bii. Subject field MUST be set to empty - they actually mean subject name if (0 != aikCert.SubjectName.Name.Length) { throw new VerificationException("aikCert subject must be empty"); } // 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); // From https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf // "The issuer MUST include TPM manufacturer, TPM part number and TPM firmware version, using the directoryName // form within the GeneralName structure. The ASN.1 encoding is specified in section 3.1.2 TPM Device // Attributes. In accordance with RFC 5280[11], this extension MUST be critical if subject is empty // and SHOULD be non-critical if subject is non-empty" // Best I can figure to do for now ? // id:49465800 'IFX' Infinion Model and Version are empty if (string.Empty == tpmManufacturer || string.Empty == tpmModel || string.Empty == tpmVersion) // if (string.Empty == tpmManufacturer) { throw new VerificationException("SAN missing TPMManufacturer, TPMModel, or TPMVersion from TPM attestation certificate"); } if (false == TPMManufacturers.Contains(tpmManufacturer)) { throw new VerificationException("Invalid TPM manufacturer found parsing TPM attestation"); } // 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 VerificationException("aikCert EKU missing tcg-kp-AIKCertificate OID"); } // 5biiiii. The Basic Constraints extension MUST have the CA component set to false. if (IsAttnCertCACert(aikCert.Extensions)) { throw new VerificationException("aikCert Basic Constraints extension CA component must be false"); } // 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 VerificationException("AAGUID not found in MDS test metadata"); } if (_requireValidAttestationRoot) { // 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 VerificationException("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())) && (0 != AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData.AaGuid))) { throw new VerificationException(string.Format("aaguid malformed, expected {0}, got {1}", AuthData.AttestedCredentialData.AaGuid, new Guid(aaguid))); } } // If ecdaaKeyId is present, then the attestation type is ECDAA else if (null != EcdaaKeyId) { // Perform ECDAA-Verify on sig to verify that it is a valid signature over certInfo // https://www.w3.org/TR/webauthn/#biblio-fidoecdaaalgorithm throw new VerificationException("ECDAA support for TPM attestation is not yet implemented"); // If successful, return attestation type ECDAA and the identifier of the ECDAA-Issuer public key ecdaaKeyId. //attnType = AttestationType.ECDAA; //trustPath = ecdaaKeyId; } else { throw new VerificationException("Neither x5c nor ECDAA were found in the TPM attestation statement"); } }
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. if (0 == attStmt.Keys.Count || 0 == attStmt.Values.Count) { throw new Fido2VerificationException("Attestation format packed must have attestation statement"); } if (null == Sig || CBORType.ByteString != Sig.Type || 0 == Sig.GetByteString().Length) { throw new Fido2VerificationException("Invalid packed attestation signature"); } if (null == Alg || CBORType.Number != Alg.Type) { throw new Fido2VerificationException("Invalid packed attestation algorithm"); } // 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) { throw new Fido2VerificationException("Malformed x5c array in packed attestation statement"); } var enumerator = X5c.Values.GetEnumerator(); while (enumerator.MoveNext()) { if (null == enumerator || null == enumerator.Current || CBORType.ByteString != enumerator.Current.Type || 0 == enumerator.Current.GetByteString().Length) { throw new Fido2VerificationException("Malformed x5c cert found in packed attestation statement"); } var x5ccert = new X509Certificate2(enumerator.Current.GetByteString()); if (DateTime.UtcNow < x5ccert.NotBefore || DateTime.UtcNow > x5ccert.NotAfter) { throw new Fido2VerificationException("Packed signing certificate expired or not yet valid"); } } // The attestation certificate attestnCert MUST be the first element in the array. var attestnCert = new X509Certificate2(X5c.Values.First().GetByteString()); // 2a. 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 var packedPubKey = (ECDsaCng)attestnCert.GetECDsaPublicKey(); // attestation public key if (false == CryptoUtils.algMap.ContainsKey(Alg.AsInt32())) { throw new Fido2VerificationException("Invalid attestation algorithm"); } var cpk = new CredentialPublicKey(attestnCert, Alg.AsInt32()); if (true != cpk.Verify(Data, Sig.GetByteString())) { 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 if (3 != attestnCert.Version) { throw new Fido2VerificationException("Packed x5c attestation certificate not V3"); } // 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, // 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); if (aaguid != null) { if (0 != AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData.AaGuid)) { throw new Fido2VerificationException("aaguid present in packed attestation 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); var trustPath = X5c.Values .Select(x => new X509Certificate2(x.GetByteString())) .ToArray(); if (null != MetadataService) { var entry = MetadataService.GetEntry(AuthData.AttestedCredentialData.AaGuid); // while conformance testing, we must reject any authenticator that we cannot get metadata for if (true == MetadataService.ConformanceTesting() && null == entry) { throw new Fido2VerificationException("AAGUID not found in MDS test metadata"); } if (null != entry && null != entry.MetadataStatement) { if (entry.Hash != entry.MetadataStatement.Hash) { throw new Fido2VerificationException("Authenticator metadata statement has invalid hash"); } var hasBasicFull = entry.MetadataStatement.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_BASIC_FULL); if (false == hasBasicFull && null != trustPath && trustPath.FirstOrDefault().Subject != trustPath.FirstOrDefault().Issuer) { throw new Fido2VerificationException("Attestation with full attestation from authentictor that does not support full attestation"); } if (true == hasBasicFull && null != trustPath && trustPath.FirstOrDefault().Subject != trustPath.FirstOrDefault().Issuer) { 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 (false == valid) { throw new Fido2VerificationException("Invalid certificate chain in packed attestation"); } } foreach (var report in entry.StatusReports) { if (true == Enum.IsDefined(typeof(UndesiredAuthenticatorStatus), (UndesiredAuthenticatorStatus)report.Status)) { throw new Fido2VerificationException("Authenticator found with undesirable status"); } } } } } // 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 // 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; } // If neither x5c nor ecdaaKeyId is present, self attestation is in use else { // 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 // clientDataHash using the credential public key with alg if (true != AuthData.AttestedCredentialData.CredentialPublicKey.Verify(Data, Sig.GetByteString())) { throw new Fido2VerificationException("Failed to validate signature"); } } }
internal static async Task <AssertionVerificationResult> MakeAssertionResponse(COSE.KeyType kty, COSE.Algorithm alg, COSE.EllipticCurve crv = COSE.EllipticCurve.P256, CredentialPublicKey cpk = null, ushort signCount = 0, ECDsa ecdsa = null, RSA rsa = null, byte[] expandedPrivateKey = null) { const string rp = "fido2.azurewebsites.net"; byte[] rpId = Encoding.UTF8.GetBytes(rp); var rpIdHash = SHA256.Create().ComputeHash(rpId); var flags = AuthenticatorFlags.AT | AuthenticatorFlags.ED | AuthenticatorFlags.UP | AuthenticatorFlags.UV; var aaguid = new Guid("F1D0F1D0-F1D0-F1D0-F1D0-F1D0F1D0F1D0"); var credentialID = new byte[] { 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, }; if (cpk == null) { switch (kty) { case COSE.KeyType.EC2: { if (ecdsa == null) { ecdsa = MakeECDsa(alg, crv); } var ecparams = ecdsa.ExportParameters(true); cpk = MakeCredentialPublicKey(kty, alg, crv, ecparams.Q.X, ecparams.Q.Y); break; } case COSE.KeyType.RSA: { if (rsa == null) { rsa = RSA.Create(); } var rsaparams = rsa.ExportParameters(true); cpk = MakeCredentialPublicKey(kty, alg, rsaparams.Modulus, rsaparams.Exponent); break; } case COSE.KeyType.OKP: { byte[] publicKey = null; if (expandedPrivateKey == null) { MakeEdDSA(out var privateKeySeed, out publicKey, out expandedPrivateKey); } cpk = MakeCredentialPublicKey(kty, alg, COSE.EllipticCurve.Ed25519, publicKey); break; } throw new ArgumentOutOfRangeException(nameof(kty), $"Missing or unknown kty {kty}"); } } var acd = new AttestedCredentialData(aaguid, credentialID, cpk); var extBytes = CBORObject.NewMap().Add("testing", true).EncodeToBytes(); var exts = new Extensions(extBytes); var ad = new AuthenticatorData(rpIdHash, flags, (uint)(signCount + 1), acd, exts); var authData = ad.ToByteArray(); var challenge = new byte[128]; var rng = RandomNumberGenerator.Create(); rng.GetBytes(challenge); var clientData = new { Type = "webauthn.get", Challenge = challenge, Origin = rp, }; var clientDataJson = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(clientData)); var sha = SHA256.Create(); var hashedClientDataJson = sha.ComputeHash(clientDataJson); byte[] data = new byte[authData.Length + hashedClientDataJson.Length]; Buffer.BlockCopy(authData, 0, data, 0, authData.Length); Buffer.BlockCopy(hashedClientDataJson, 0, data, authData.Length, hashedClientDataJson.Length); byte[] signature = SignData(kty, alg, data, ecdsa, rsa, expandedPrivateKey); var userHandle = new byte[16]; rng.GetBytes(userHandle); var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() { AuthenticatorData = authData, Signature = signature, ClientDataJson = clientDataJson, UserHandle = userHandle, }; var lib = new Fido2(new Fido2Configuration() { ServerDomain = rp, ServerName = rp, Origin = rp, }); var existingCredentials = new List <PublicKeyCredentialDescriptor>(); var cred = new PublicKeyCredentialDescriptor { Type = PublicKeyCredentialType.PublicKey, Id = new byte[] { 0xf1, 0xd0 } }; existingCredentials.Add(cred); var options = lib.GetAssertionOptions(existingCredentials, null, null); options.Challenge = challenge; var response = new AuthenticatorAssertionRawResponse() { Response = assertion, Type = PublicKeyCredentialType.PublicKey, Id = new byte[] { 0xf1, 0xd0 }, RawId = new byte[] { 0xf1, 0xd0 }, }; IsUserHandleOwnerOfCredentialIdAsync callback = (args) => { return(Task.FromResult(true)); }; return(await lib.MakeAssertionAsync(response, options, cpk.GetBytes(), signCount, callback)); }