Esempio n. 1
0
        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 VerificationException("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 VerificationException("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 VerificationException("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 VerificationException("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 (false == valid)
                    {
                        throw new VerificationException("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 VerificationException("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 VerificationException("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 VerificationException("Invalid fido-u2f attestation signature");
            }

            var ecsig = CryptoUtils.SigFromEcDsaSig(Sig.GetByteString(), pubKey.KeySize);

            if (null == ecsig)
            {
                throw new VerificationException("Failed to decode fido-u2f attestation signature from ASN.1 encoded form");
            }

            var coseAlg = CredentialPublicKey[CBORObject.FromObject(COSE.KeyCommonParameter.Alg)].AsInt32();
            var hashAlg = CryptoUtils.algMap[coseAlg];

            if (true != pubKey.VerifyData(verificationData, ecsig, hashAlg))
            {
                throw new VerificationException("Invalid fido-u2f attestation signature");
            }
        }
Esempio n. 2
0
        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");

            // 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. 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 attCert = new X509Certificate2(X5c.Values.First().GetByteString());

            // TODO : Check why this variable isn't used. Remove it or use it.
            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 = attCert.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 members of base class AuthenticatorData (AuthData)

            // 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)
            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.HashAlgFromCOSEAlg(coseAlg);

            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");
                    }
                }
            }
        }
Esempio n. 3
0
        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 || true != Alg.IsNumber)
            {
                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 = 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 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);

                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 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);
                    }

                    if (false == valid)
                    {
                        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");
                    }
                }
            }

            // 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");
                }
            }
        }
Esempio n. 4
0
        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)
                {
                    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");
            }
        }
Esempio n. 5
0
        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 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
            // 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");
            }

            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 android-key attestation");
            }

            X509Certificate2 androidKeyCert;
            ECDsa            androidKeyPubKey;

            try
            {
                androidKeyCert   = new X509Certificate2(X5c.Values.First().GetByteString());
                androidKeyPubKey = androidKeyCert.GetECDsaPublicKey(); // attestation public key
            }
            catch (Exception ex)
            {
                throw new Fido2VerificationException("Failed to extract public key from android key: " + ex.Message, ex);
            }

            if (null == Alg || true != Alg.IsNumber || false == CryptoUtils.algMap.ContainsKey(Alg.AsInt32()))
            {
                throw new Fido2VerificationException("Invalid android key attestation algorithm");
            }

            byte[] ecsig;
            try
            {
                ecsig = CryptoUtils.SigFromEcDsaSig(Sig.GetByteString(), androidKeyPubKey.KeySize);
            }
            catch (Exception ex)
            {
                throw new Fido2VerificationException("Failed to decode android key attestation signature from ASN.1 encoded form", ex);
            }

            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.
            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:
            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);
                if (false == clientDataHash.SequenceEqual(attestationChallenge))
                {
                    throw new Fido2VerificationException("Mismatch between attestationChallenge and hashedClientDataJson verifying android key attestation certificate extension");
                }
            }
            catch (Exception)
            {
                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.
            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).
            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).
            if (false == IsPurposeSign(attExtBytes))
            {
                throw new Fido2VerificationException("Found purpose field not set to KM_PURPOSE_SIGN in android key attestation certificate extension");
            }
        }
Esempio n. 6
0
        public override (AttestationType, X509Certificate2[]) 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 VerificationException("Attestation format packed must have attestation statement");
            }

            if (null == Sig || CBORType.ByteString != Sig.Type || 0 == Sig.GetByteString().Length)
            {
                throw new VerificationException("Invalid packed attestation signature");
            }

            if (null == Alg || true != Alg.IsNumber)
            {
                throw new VerificationException("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 VerificationException("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 VerificationException("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 VerificationException("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 VerificationException("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 VerificationException("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 VerificationException("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 VerificationException("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 VerificationException("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();

                return(AttestationType.AttCa, trustPath);
            }

            // 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 VerificationException("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 VerificationException("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 VerificationException("Failed to validate signature");
                }

                return(AttestationType.Self, null);
            }
        }
Esempio n. 7
0
        public override AttestationFormatVerificationResult Verify()
        {
            // verify that aaguid is 16 empty bytes (note: required by fido2 conformance testing, could not find this in spec?)
            if (false == AuthData.AttData.Aaguid.SequenceEqual(Guid.Empty.ToByteArray()))
            {
                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());

            // 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 = (ECDsaCng)cert.GetECDsaPublicKey();

            if (CngAlgorithm.ECDsaP256 != pubKey.Key.Algorithm)
            {
                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 publicKeyU2F = CryptoUtils.U2FKeyFromCOSEKey(CredentialPublicKey);

            // 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.AttData.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");
            }

            var ecsig = CryptoUtils.SigFromEcDsaSig(Sig.GetByteString());

            if (null == ecsig)
            {
                throw new Fido2VerificationException("Failed to decode fido-u2f attestation signature from ASN.1 encoded form");
            }

            if (true != pubKey.VerifyData(verificationData, ecsig, CryptoUtils.algMap[CredentialPublicKey[CBORObject.FromObject(3)].AsInt32()]))
            {
                throw new Fido2VerificationException("Invalid fido-u2f attestation signature");
            }

            return(new AttestationFormatVerificationResult()
            {
                attnType = AttestationType.None,
                trustPath = null
            });
        }
Esempio n. 8
0
        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");
            }
            // 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
            if (null == X5c && CBORType.Array != X5c.Type && 0 == X5c.Count)
            {
                throw new Fido2VerificationException("Malformed x5c in android-key attestation");
            }

            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 android-key attestation");
            }

            X509Certificate2 androidKeyCert   = null;
            ECDsaCng         androidKeyPubKey = null;

            try
            {
                androidKeyCert   = new X509Certificate2(X5c.Values.First().GetByteString());
                androidKeyPubKey = (ECDsaCng)androidKeyCert.GetECDsaPublicKey(); // attestation public key
            }
            catch (Exception ex)
            {
                throw new Fido2VerificationException("Failed to extract public key from android key" + ex.Message);
            }

            if (null == Alg || CBORType.Number != Alg.Type || false == CryptoUtils.algMap.ContainsKey(Alg.AsInt32()))
            {
                throw new Fido2VerificationException("Invalid attestation algorithm");
            }
            if (true != androidKeyPubKey.VerifyData(Data, CryptoUtils.SigFromEcDsaSig(Sig.GetByteString(), androidKeyPubKey.KeySize), CryptoUtils.algMap[Alg.AsInt32()]))
            {
                throw new Fido2VerificationException("Invalid android key signature");
            }

            var credentialPublicKey = CBORObject.DecodeFromBytes(AuthData.AttData.CredentialPublicKey);
            var cng = ECDsa.Create(new ECParameters
            {
                Curve = ECCurve.NamedCurves.nistP256,
                Q     = new ECPoint
                {
                    X = credentialPublicKey[CBORObject.FromObject(-2)].GetByteString(),
                    Y = credentialPublicKey[CBORObject.FromObject(-3)].GetByteString()
                }
            });

            // Verify that the public key in the first certificate in in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
            if (true != cng.VerifyData(Data, CryptoUtils.SigFromEcDsaSig(Sig.GetByteString(), cng.KeySize), CryptoUtils.algMap[Alg.AsInt32()]))
            {
                throw new Fido2VerificationException("Invalid android key signature");
            }

            // Verify that in the attestation certificate extension data:
            var attExtBytes = AttestationExtensionBytes(androidKeyCert.Extensions);

            // 1. The value of the attestationChallenge field is identical to clientDataHash.
            var attestationChallenge = GetAttestationChallenge(attExtBytes);

            if (false == clientDataHash.SequenceEqual(attestationChallenge))
            {
                throw new Fido2VerificationException("Mismatched between attestationChallenge and hashedClientDataJson verifying android key attestation certificate extension");
            }

            // 2. 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).
            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).
            if (false == IsPurposeSign(attExtBytes))
            {
                throw new Fido2VerificationException("Found purpose field not set to KM_PURPOSE_SIGN in android key attestation certificate extension");
            }
        }
Esempio n. 9
0
        public override AttestationFormatVerificationResult Verify()
        {
            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
            // 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 Fido2VerificationException("Missing or malformed pubArea");
            }

            var coseKty = CredentialPublicKey[CBORObject.FromObject(1)].AsInt32();

            if (3 == coseKty)                                                                 // RSA
            {
                var coseMod = CredentialPublicKey[CBORObject.FromObject(-1)].GetByteString(); // modulus
                var coseExp = CredentialPublicKey[CBORObject.FromObject(-2)].GetByteString(); // exponent

                if (!coseMod.ToArray().SequenceEqual(pubArea.Unique.ToArray()))
                {
                    throw new Fido2VerificationException("Public key mismatch between pubArea and credentialPublicKey");
                }
                if ((coseExp[0] + (coseExp[1] << 8) + (coseExp[2] << 16)) != pubArea.Exponent)
                {
                    throw new Fido2VerificationException("Public key exponent mismatch between pubArea and credentialPublicKey");
                }
            }
            else if (2 == coseKty) // ECC
            {
                var curve = CredentialPublicKey[CBORObject.FromObject(-1)].AsInt32();
                var X     = CredentialPublicKey[CBORObject.FromObject(-2)].GetByteString();
                var Y     = CredentialPublicKey[CBORObject.FromObject(-3)].GetByteString();

                if (pubArea.EccCurve != CoseCurveToTpm[curve])
                {
                    throw new Fido2VerificationException("Curve mismatch between pubArea and credentialPublicKey");
                }
                if (!pubArea.ECPoint.X.SequenceEqual(X))
                {
                    throw new Fido2VerificationException("X-coordinate mismatch between pubArea and credentialPublicKey");
                }
                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

            // 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 || null == certInfo.ExtraData || 0 == certInfo.ExtraData.Length)
            {
                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

            // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg"
            if (null == Alg || CBORType.Number != Alg.Type || false == CryptoUtils.algMap.ContainsKey(Alg.AsInt32()))
            {
                throw new Fido2VerificationException("Invalid TPM attestation algorithm");
            }
            if (!CryptoUtils.GetHasher(CryptoUtils.algMap[Alg.AsInt32()]).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
            if (false == CryptoUtils.GetHasher(CryptoUtils.algMap[certInfo.Alg]).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
            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 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.
                var aikCert = new X509Certificate2(X5c.Values.First().GetByteString());

                var coseKey = CryptoUtils.CoseKeyFromCertAndAlg(aikCert, Alg.AsInt32());

                if (true != CryptoUtils.VerifySigWithCoseKey(certInfo.Raw, coseKey, Sig.GetByteString()))
                {
                    throw new Fido2VerificationException("Bad signature in TPM with aikCert");
                }

                // 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
                if (3 != aikCert.Version)
                {
                    throw new Fido2VerificationException("aikCert must be V3");
                }

                // 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.
                // https://www.w3.org/TR/webauthn/#tpm-cert-requirements
                var SAN = SANFromAttnCertExts(aikCert.Extensions);
                if (null == SAN || 0 == SAN.Length)
                {
                    throw new Fido2VerificationException("SAN missing from TPM attestation certificate");
                }

                // 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?
                if (false == SAN.Contains("TPMManufacturer") || false == SAN.Contains("TPMModel") ||
                    false == SAN.Contains("TPMVersion"))
                {
                    throw new Fido2VerificationException("SAN missing TPMManufacturer, TPMModel, or TPMVersopm from TPM attestation certificate");
                }

                // 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);
                if (null == EKU || 0 != EKU.CompareTo("Attestation Identity Key Certificate (2.23.133.8.3)"))
                {
                    throw new Fido2VerificationException("Invalid EKU on AIK certificate");
                }

                // 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
                var aaguid = AaguidFromAttnCertExts(aikCert.Extensions);
                if ((null != aaguid) && (!aaguid.SequenceEqual(Guid.Empty.ToByteArray())) && (!aaguid.SequenceEqual(AuthData.AttData.Aaguid.ToArray())))
                {
                    throw new Fido2VerificationException("aaguid malformed");
                }

                // If successful, return attestation type AttCA and attestation trust path x5c.
                return(new AttestationFormatVerificationResult()
                {
                    attnType = AttestationType.AttCa,
                    trustPath = X5c.Values
                                .Select(x => new X509Certificate2(x.GetByteString()))
                                .ToArray()
                });
            }
            // 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 Fido2VerificationException("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 Fido2VerificationException("Neither x5c nor ECDAA were found in the TPM attestation statement");
            }
        }
Esempio n. 10
0
        public override AttestationFormatVerificationResult 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 coseKey = CryptoUtils.CoseKeyFromCertAndAlg(attestnCert, Alg.AsInt32());

                if (true != CryptoUtils.VerifySigWithCoseKey(Data, coseKey, 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 && !aaguid.SequenceEqual(AuthData.AttData.Aaguid.ToArray()))
                {
                    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);

                return(new AttestationFormatVerificationResult()
                {
                    attnType = AttestationType.Basic,
                    trustPath = X5c.Values
                                .Select(x => new X509Certificate2(x.GetByteString()))
                                .ToArray()
                });
            }
            // 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
                var credentialPublicKey = CBORObject.DecodeFromBytes(AuthData.AttData.CredentialPublicKey);
                var coseAlg             = credentialPublicKey[CBORObject.FromObject(3)].AsInt32();
                if (Alg.AsInt32() != coseAlg)
                {
                    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 != CryptoUtils.VerifySigWithCoseKey(Data, credentialPublicKey, Sig.GetByteString()))
                {
                    throw new Fido2VerificationException("Failed to validate signature");
                }

                // If successful, return attestation type Self and empty attestation trust path.
                return(new AttestationFormatVerificationResult()
                {
                    attnType = AttestationType.Self,
                    trustPath = null
                });
            }
        }