Esempio n. 1
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)

            // 2. Verify x5c is a valid certificate chain starting from the credCert to the Apple WebAuthn root certificate.
            // https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem
            var appleWebAuthnRoots = new string[] {
                "MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w" +
                "HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ" +
                "bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx" +
                "NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG" +
                "A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49" +
                "AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k" +
                "xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/" +
                "pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk" +
                "2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA" +
                "MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3" +
                "jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B" +
                "1bWeT0vT"
            };

            var trustPath = X5c.Values.Select(x => new X509Certificate2(x.GetByteString())).ToArray();

            var appleWebAuthnRootCerts = appleWebAuthnRoots.Select(x => new X509Certificate2(Convert.FromBase64String(x))).ToArray();

            if (_requireValidAttestationRoot)
            {
                if (!ValidateTrustChain(trustPath, appleWebAuthnRootCerts))
                {
                    throw new VerificationException("Invalid certificate chain in Apple attestation");
                }
            }

            // 3. Concatenate authenticatorData and clientDataHash to form nonceToHash.
            var nonceToHash = Data;

            // 4. Perform SHA-256 hash of nonceToHash to produce nonce.
            var nonce = CryptoUtils.GetHasher(HashAlgorithmName.SHA256).ComputeHash(nonceToHash);

            // 5. Verify nonce matches the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert.
            var credCert = trustPath[0];

            if (!nonce.SequenceEqual(AppleAttestationExtensionBytes(credCert.Extensions)))
            {
                throw new VerificationException("Mismatch between nonce and credCert attestation extension in Apple attestation");
            }

            // 6. Verify credential public key matches the Subject Public Key of credCert.
            var coseAlg = CredentialPublicKey[CBORObject.FromObject(COSE.KeyCommonParameter.Alg)].AsInt32();
            var cpk     = new CredentialPublicKey(credCert, coseAlg);

            if (!cpk.GetBytes().SequenceEqual(AuthData.AttestedCredentialData.CredentialPublicKey.GetBytes()))
            {
                throw new VerificationException("Credential public key in Apple attestation does not match subject public key of credCert");
            }

            // 7. If successful, return implementation-specific values representing attestation type Anonymous CA and attestation trust path x5c.
            return; // (AttestationType.Basic, trustPath);
        }
Esempio n. 2
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 (null == X5c || CBORType.Array != X5c.Type || X5c.Count < 2 ||
                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 Apple attestation");
            }

            // 2. Verify x5c is a valid certificate chain starting from the credCert to the Apple WebAuthn root certificate.
            // This happens in AuthenticatorAttestationResponse.VerifyAsync using metadata from MDS3

            var trustPath = X5c.Values
                            .Select(x => new X509Certificate2(x.GetByteString()))
                            .ToArray();

            // credCert is the first certificate in the trust path
            var credCert = trustPath[0];

            // 3. Concatenate authenticatorData and clientDataHash to form nonceToHash.
            var nonceToHash = Data;

            // 4. Perform SHA-256 hash of nonceToHash to produce nonce.
            var nonce = CryptoUtils.GetHasher(HashAlgorithmName.SHA256).ComputeHash(nonceToHash);

            // 5. Verify nonce matches the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert.
            var appleExtensionBytes = GetAppleAttestationExtensionValue(credCert.Extensions);

            if (!nonce.SequenceEqual(appleExtensionBytes))
            {
                throw new VerificationException("Mismatch between nonce and credCert attestation extension in Apple attestation");
            }

            // 6. Verify credential public key matches the Subject Public Key of credCert.
            // First, obtain COSE algorithm being used from credential public key
            var coseAlg = CredentialPublicKey[CBORObject.FromObject(COSE.KeyCommonParameter.Alg)].AsInt32();

            // Next, build temporary CredentialPublicKey for comparison from credCert and COSE algorithm
            var cpk = new CredentialPublicKey(credCert, coseAlg);

            // Finally, compare byte sequence of CredentialPublicKey built from credCert with byte sequence of CredentialPublicKey from AttestedCredentialData from authData
            if (!cpk.GetBytes().SequenceEqual(AuthData.AttestedCredentialData.CredentialPublicKey.GetBytes()))
            {
                throw new VerificationException("Credential public key in Apple attestation does not match subject public key of credCert");
            }

            // 7. If successful, return implementation-specific values representing attestation type Anonymous CA and attestation trust path x5c.
            return(AttestationType.Basic, trustPath);
        }
            internal byte[] SignData(COSE.KeyType kty, COSE.Algorithm alg, COSE.EllipticCurve curve, ECDsa ecdsa = null, RSA rsa = null, Key expandedPrivateKey = null, byte[] publicKey = null)
            {
                switch (kty)
                {
                case COSE.KeyType.EC2:
                {
                    var ecparams = ecdsa.ExportParameters(true);
                    _credentialPublicKey = MakeCredentialPublicKey(kty, alg, curve, ecparams.Q.X, ecparams.Q.Y);
                    var signature = ecdsa.SignData(_attToBeSigned, CryptoUtils.HashAlgFromCOSEAlg((int)alg));
                    return(EcDsaSigFromSig(signature, ecdsa.KeySize));
                }

                case COSE.KeyType.RSA:
                {
                    RSASignaturePadding padding;
                    switch (alg)         // https://www.iana.org/assignments/cose/cose.xhtml#algorithms
                    {
                    case COSE.Algorithm.PS256:
                    case COSE.Algorithm.PS384:
                    case COSE.Algorithm.PS512:
                        padding = RSASignaturePadding.Pss;
                        break;

                    case COSE.Algorithm.RS1:
                    case COSE.Algorithm.RS256:
                    case COSE.Algorithm.RS384:
                    case COSE.Algorithm.RS512:
                        padding = RSASignaturePadding.Pkcs1;
                        break;

                    default:
                        throw new ArgumentOutOfRangeException(nameof(alg), $"Missing or unknown alg {alg}");
                    }

                    var rsaparams = rsa.ExportParameters(true);
                    _credentialPublicKey = MakeCredentialPublicKey(kty, alg, rsaparams.Modulus, rsaparams.Exponent);
                    return(rsa.SignData(_attToBeSigned, CryptoUtils.HashAlgFromCOSEAlg((int)alg), padding));
                }

                case COSE.KeyType.OKP:
                {
                    _credentialPublicKey = MakeCredentialPublicKey(kty, alg, COSE.EllipticCurve.Ed25519, publicKey);
                    return(SignatureAlgorithm.Ed25519.Sign(expandedPrivateKey, _attToBeSigned));
                }

                default:
                    throw new ArgumentOutOfRangeException(nameof(kty), $"Missing or unknown kty {kty}");
                }
            }
        internal static CredentialPublicKey MakeCredentialPublicKey(object[] param)
        {
            var kty = (COSE.KeyType)param[0];
            var alg = (COSE.Algorithm)param[1];
            CredentialPublicKey cpk = null;

            switch (kty)
            {
            case COSE.KeyType.EC2:
            {
                var crv      = (COSE.EllipticCurve)param[2];
                var 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:
            {
                var rsa       = RSA.Create();
                var rsaparams = rsa.ExportParameters(true);
                cpk = MakeCredentialPublicKey(kty, alg, rsaparams.Modulus, rsaparams.Exponent);
                break;
            }

            case COSE.KeyType.OKP:
            {
                byte[] publicKey          = null;
                byte[] 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}");
            }
            return(cpk);
        }
        /// <summary>
        /// Implements alghoritm from https://www.w3.org/TR/webauthn/#verifying-assertion
        /// </summary>
        /// <param name="options">The assertionoptions that was sent to the client</param>
        /// <param name="expectedOrigin">
        /// The expected server origin, used to verify that the signature is sent to the expected server
        /// </param>
        /// <param name="storedPublicKey">The stored public key for this CredentialId</param>
        /// <param name="storedSignatureCounter">The stored counter value for this CredentialId</param>
        /// <param name="isUserHandleOwnerOfCredId">A function that returns <see langword="true"/> if user handle is owned by the credential ID</param>
        /// <param name="requestTokenBindingId"></param>
        public AssertionVerificationResult Verify(
            AssertionOptions options,
            string expectedOrigin,
            byte[] storedPublicKey,
            uint storedSignatureCounter,
            IsUserHandleOwnerOfCredentialId isUserHandleOwnerOfCredId,
            byte[] requestTokenBindingId)
        {
            BaseVerify(expectedOrigin, options.Challenge, requestTokenBindingId);

            if (Raw.Type != PublicKeyCredentialType.PublicKey)
            {
                throw new VerificationException("AssertionResponse Type is not set to public-key");
            }

            if (Raw.Id == null)
            {
                throw new VerificationException("Id is missing");
            }
            if (Raw.RawId == null)
            {
                throw new VerificationException("RawId is missing");
            }

            // 1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.
            if (options.AllowCredentials != null && options.AllowCredentials.Count() > 0)
            {
                // might need to transform x.Id and raw.id as described in https://www.w3.org/TR/webauthn/#publickeycredential
                if (!options.AllowCredentials.Any(x => x.Id.SequenceEqual(Raw.Id)))
                {
                    throw new VerificationException("Invalid");
                }
            }

            // 2. If credential.response.userHandle is present, verify that the user identified by this value is the owner of the public key credential identified by credential.id.
            if (UserHandle != null)
            {
                if (UserHandle.Length == 0)
                {
                    throw new VerificationException("Userhandle was empty DOMString. It should either be null or have a value.");
                }

                if (false == isUserHandleOwnerOfCredId(new IsUserHandleOwnerOfCredentialIdParams(Raw.Id, UserHandle)))
                {
                    throw new VerificationException("User is not owner of the public key identitief by the credential id");
                }
            }

            // 3. Using credential’s id attribute(or the corresponding rawId, if base64url encoding is inappropriate for your use case), look up the corresponding credential public key.
            // public key inserted via parameter.

            // 4. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively.
            //var cData = Raw.Response.ClientDataJson;
            var authData = new AuthenticatorData(Raw.Response.AuthenticatorData);

            //var sig = Raw.Response.Signature;

            // 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
            //var JSONtext = Encoding.UTF8.GetBytes(cData.ToString());


            // 7. Verify that the value of C.type is the string webauthn.get.
            if (Type != "webauthn.get")
            {
                throw new VerificationException();
            }

            // 8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.
            // 9. Verify that the value of C.origin matches the Relying Party's origin.
            // done in base class

            //10. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the attestation was obtained.If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
            // Validated in BaseVerify.
            // todo: Needs testing

            // 11. Verify that the rpIdHash in aData is the SHA - 256 hash of the RP ID expected by the Relying Party.

            byte[] hashedClientDataJson;
            byte[] hashedRpId;
            using (var sha = SHA256.Create())
            {
                // 11
                // https://www.w3.org/TR/webauthn/#sctn-appid-extension
                // FIDO AppID Extension:
                // If true, the AppID was used and thus, when verifying an assertion, the Relying Party MUST expect the rpIdHash to be the hash of the AppID, not the RP ID.
                var rpid = Raw.Extensions?.AppID ?? false ? options.Extensions?.AppID : options.RpId;
                hashedRpId = sha.ComputeHash(Encoding.UTF8.GetBytes(rpid ?? string.Empty));
                // 15
                hashedClientDataJson = sha.ComputeHash(Raw.Response.ClientDataJson);
            }

            if (false == authData.RpIdHash.SequenceEqual(hashedRpId))
            {
                throw new VerificationException("Hash mismatch RPID");
            }
            // 12. Verify that the User Present bit of the flags in authData is set.
            // UNLESS...userVerification is set to preferred or discouraged?
            // See Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData
            // P-5 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "preferred", and check that server succeeds
            // P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds
            //if ((false == authData.UserPresent) && (options.UserVerification != UserVerificationRequirement.Discouraged && options.UserVerification != UserVerificationRequirement.Preferred)) throw new Fido2VerificationException("User Present flag not set in authenticator data");

            // 13 If user verification is required for this assertion, verify that the User Verified bit of the flags in aData is set.
            // UNLESS...userPresent is true?
            // see ee Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData
            // P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds
            if (UserVerificationRequirement.Required == options.UserVerification && false == authData.UserVerified)
            {
                throw new VerificationException("User verification is required");
            }

            // 14. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given as the extensions option in the get() call.In particular, any extension identifier values in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use.
            // todo: Verify this (and implement extensions on options)
            if (true == authData.HasExtensionsData && ((null == authData.Extensions) || (0 == authData.Extensions.Length)))
            {
                throw new VerificationException("Extensions flag present, malformed extensions detected");
            }
            if (false == authData.HasExtensionsData && (null != authData.Extensions))
            {
                throw new VerificationException("Extensions flag not present, but extensions detected");
            }

            // 15.
            // Done earlier, hashedClientDataJson

            // 16. Using the credential public key looked up in step 3, verify that sig is a valid signature over the binary concatenation of aData and hash.
            byte[] data = new byte[Raw.Response.AuthenticatorData.Length + hashedClientDataJson.Length];
            Buffer.BlockCopy(Raw.Response.AuthenticatorData, 0, data, 0, Raw.Response.AuthenticatorData.Length);
            Buffer.BlockCopy(hashedClientDataJson, 0, data, Raw.Response.AuthenticatorData.Length, hashedClientDataJson.Length);

            if (null == storedPublicKey || 0 == storedPublicKey.Length)
            {
                throw new VerificationException("Stored public key is null or empty");
            }
            var cpk = new CredentialPublicKey(storedPublicKey);

            if (true != cpk.Verify(data, Signature))
            {
                throw new VerificationException("Signature did not match");
            }

            // 17.
            if (authData.SignCount > 0 && authData.SignCount <= storedSignatureCounter)
            {
                throw new VerificationException("SignatureCounter was not greater than stored SignatureCounter");
            }

            return(new AssertionVerificationResult()
            {
                Status = "ok",
                ErrorMessage = string.Empty,
                CredentialId = Raw.Id,
                Counter = authData.SignCount,
            });
        }
Esempio n. 6
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. 7
0
        internal async void MakeAssertionResponse(COSE.KeyType kty, COSE.Algorithm alg, COSE.EllipticCurve crv = COSE.EllipticCurve.P256)
        {
            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;
            const ushort signCount    = 0xf1d0;
            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, };

            CredentialPublicKey cpk = null;
            ECDsa ecdsa             = null;
            RSA   rsa = null;

            byte[] expandedPrivateKey = null;
            switch (kty)
            {
            case COSE.KeyType.EC2:
            {
                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:
            {
                rsa = RSA.Create();
                var rsaparams = rsa.ExportParameters(true);
                cpk = MakeCredentialPublicKey(kty, alg, rsaparams.Modulus, rsaparams.Exponent);
                break;
            }

            case COSE.KeyType.OKP:
            {
                MakeEdDSA(out var privateKeySeed, out var 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, signCount, 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 = null;
            switch (kty)
            {
            case COSE.KeyType.EC2:
            {
                signature = ecdsa.SignData(data, CryptoUtils.algMap[(int)alg]);
                break;
            }

            case COSE.KeyType.RSA:
            {
                RSASignaturePadding padding;
                switch (alg)         // https://www.iana.org/assignments/cose/cose.xhtml#algorithms
                {
                case COSE.Algorithm.PS256:
                case COSE.Algorithm.PS384:
                case COSE.Algorithm.PS512:
                    padding = RSASignaturePadding.Pss;
                    break;

                case COSE.Algorithm.RS1:
                case COSE.Algorithm.RS256:
                case COSE.Algorithm.RS384:
                case COSE.Algorithm.RS512:
                    padding = RSASignaturePadding.Pkcs1;
                    break;

                default:
                    throw new ArgumentOutOfRangeException(nameof(alg), $"Missing or unknown alg {alg}");
                }
                signature = rsa.SignData(data, CryptoUtils.algMap[(int)alg], padding);
                break;
            }

            case COSE.KeyType.OKP:
            {
                signature = Ed25519.Sign(data, expandedPrivateKey);
                break;
            }

            default:
                throw new ArgumentOutOfRangeException(nameof(kty), $"Missing or unknown kty {kty}");
            }

            if (kty == COSE.KeyType.EC2)
            {
                signature = EcDsaSigFromSig(signature, ecdsa.KeySize);
            }

            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));
            };
            var res = await lib.MakeAssertionAsync(response, options, cpk.GetBytes(), signCount - 1, callback);
        }
Esempio n. 8
0
        public async Task TestApplePublicKeyMismatch()
        {
            var cpkBytes = new byte[] { 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0x79, 0xfe, 0x59, 0x08, 0xbb, 0x51, 0x29, 0xc8, 0x09, 0x38, 0xb7, 0x54, 0xc0, 0x4d, 0x2b, 0x34, 0x0e, 0xfa, 0x66, 0x15, 0xb9, 0x87, 0x69, 0x8b, 0xf5, 0x9d, 0xa4, 0xe5, 0x3e, 0xa3, 0xe6, 0xfe, 0x22, 0x58, 0x20, 0xfb, 0x03, 0xda, 0xa1, 0x27, 0x0d, 0x58, 0x04, 0xe8, 0xab, 0x61, 0xc1, 0x5a, 0xac, 0xa2, 0x43, 0x5c, 0x7d, 0xbf, 0x36, 0x9d, 0x71, 0xca, 0x15, 0xc5, 0x23, 0xb0, 0x00, 0x4a, 0x1b, 0x75, 0xb7 };

            _credentialPublicKey = new CredentialPublicKey(cpkBytes);

            var authData = new AuthenticatorData(_rpIdHash, _flags, _signCount, _acd, _exts).ToByteArray();

            _attestationObject.Set("authData", new CborByteString(authData));
            var clientData = new
            {
                type      = "webauthn.create",
                challenge = _challenge,
                origin    = "https://www.passwordless.dev",
            };
            var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(clientData);

            var invalidX5cStrings = StackAllocSha256(authData, clientDataJson);

            var trustPath = invalidX5cStrings
                            .Select(x => new X509Certificate2(Convert.FromBase64String(x)))
                            .ToArray();

            var X5c = new CborArray {
                { trustPath[0].RawData },
                { trustPath[1].RawData }
            };

            ((CborMap)_attestationObject["attStmt"]).Set("x5c", X5c);

            var attestationResponse = new AuthenticatorAttestationRawResponse
            {
                Type     = PublicKeyCredentialType.PublicKey,
                Id       = new byte[] { 0xf1, 0xd0 },
                RawId    = new byte[] { 0xf1, 0xd0 },
                Response = new AuthenticatorAttestationRawResponse.ResponseData()
                {
                    AttestationObject = _attestationObject.Encode(),
                    ClientDataJson    = clientDataJson,
                }
            };

            var origChallenge = new CredentialCreateOptions
            {
                Attestation            = AttestationConveyancePreference.Direct,
                AuthenticatorSelection = new AuthenticatorSelection
                {
                    AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
                    RequireResidentKey      = true,
                    UserVerification        = UserVerificationRequirement.Discouraged,
                },
                Challenge        = _challenge,
                ErrorMessage     = "",
                PubKeyCredParams = new List <PubKeyCredParam>()
                {
                    new PubKeyCredParam(COSE.Algorithm.ES256)
                },
                Rp     = new PublicKeyCredentialRpEntity("https://www.passwordless.dev", "6cc3c9e7967a.ngrok.io", ""),
                Status = "ok",
                User   = new Fido2User
                {
                    Name        = "testuser",
                    Id          = Encoding.UTF8.GetBytes("testuser"),
                    DisplayName = "Test User",
                },
                Timeout = 60000,
            };

            IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) =>
            {
                return(Task.FromResult(true));
            };

            IFido2 lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = "6cc3c9e7967a.ngrok.io",
                ServerName   = "6cc3c9e7967a.ngrok.io",
                Origins      = new HashSet <string> {
                    "https://www.passwordless.dev"
                },
            });

            var credentialMakeResult = await lib.MakeNewCredentialAsync(attestationResponse, origChallenge, callback);
        }
Esempio n. 9
0
        public Apple()
        {
            validX5cStrings = new[] {
                "MIICRDCCAcmgAwIBAgIGAXUCfWGDMAoGCCqGSM49BAMCMEgxHDAaBgNVBAMME0FwcGxlIFdlYkF1dGhuIENBIDExEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwHhcNMjAxMDA3MDk0NjEyWhcNMjAxMDA4MDk1NjEyWjCBkTFJMEcGA1UEAwxANjEyNzZmYzAyZDNmZThkMTZiMzNiNTU0OWQ4MTkyMzZjODE3NDZhODNmMmU5NGE2ZTRiZWUxYzcwZjgxYjViYzEaMBgGA1UECwwRQUFBIENlcnRpZmljYXRpb24xEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAR5/lkIu1EpyAk4t1TATSs0DvpmFbmHaYv1naTlPqPm/vsD2qEnDVgE6KthwVqsokNcfb82nXHKFcUjsABKG3W3o1UwUzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIE8DAzBgkqhkiG92NkCAIEJjAkoSIEIJxgAhVAs+GYNN/jfsYkRcieGylPeSzka5QTwyMO84aBMAoGCCqGSM49BAMCA2kAMGYCMQDaHBjrI75xAF7SXzyF5zSQB/Lg9PjTdyye+w7stiqy84K6lmo8d3fIptYjLQx81bsCMQCvC8MSN+aewiaU0bMsdxRbdDerCJJj3xJb3KZwloevJ3daCmCcrZrAPYfLp2kDOsg=",
                "MIICNDCCAbqgAwIBAgIQViVTlcen+0Dr4ijYJghTtjAKBggqhkjOPQQDAzBLMR8wHQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MzgwMVoXDTMwMDMxMzAwMDAwMFowSDEcMBoGA1UEAwwTQXBwbGUgV2ViQXV0aG4gQ0EgMTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIMuhy8mFJGBAiW59fzWu2N4tfVfP8sEW8c1mTR1/VSQRN+b/hkhF2XGmh3aBQs41FCDQBpDT7JNES1Ww+HPv8uYkf7AaWCBvvlsvHfIjd2vRqWu4d1RW1r6q5O+nAsmkaNmMGQwEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBQm12TZxXjCWmfRp95rEtAbY/HG1zAdBgNVHQ4EFgQU666CxP+hrFtR1M8kYQUAvmO9d4gwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMQDdixo0gaX62du052V7hB4UTCe3W4dqQYbCsUdXUDNyJ+/lVEV+9kiVDGMuXEg+cMECMCyKYETcIB/P5ZvDTSkwwUh4Udlg7Wp18etKyr44zSW4l9DIBb7wx/eLB6VxxugOBw=="
            };
            _attestationObject = new CborMap {
                { "fmt", "apple" }
            };
            var(type, alg, crv) = Fido2Tests._validCOSEParameters[0];
            X509Certificate2 root, attestnCert;
            DateTimeOffset   notBefore = DateTimeOffset.UtcNow;
            DateTimeOffset   notAfter  = notBefore.AddDays(2);
            var attDN = new X500DistinguishedName("CN=attest.apple.com, OU=Apple Authenticator Attestation, O=FIDO2-NET-LIB, C=US");

            using (var ecdsaRoot = ECDsa.Create())
            {
                var rootRequest = new CertificateRequest(rootDN, ecdsaRoot, HashAlgorithmName.SHA256);
                rootRequest.CertificateExtensions.Add(caExt);

                ECCurve eCCurve = ECCurve.NamedCurves.nistP256;
                using (root = rootRequest.CreateSelfSigned(
                           notBefore,
                           notAfter))

                    using (var ecdsaAtt = ECDsa.Create(eCCurve))
                    {
                        var attRequest = new CertificateRequest(attDN, ecdsaAtt, HashAlgorithmName.SHA256);

                        byte[] serial = new byte[12];
                        RandomNumberGenerator.Fill(serial);

                        using (X509Certificate2 publicOnly = attRequest.Create(
                                   root,
                                   notBefore,
                                   notAfter,
                                   serial))
                        {
                            attestnCert = publicOnly.CopyWithPrivateKey(ecdsaAtt);
                        }

                        var ecparams = ecdsaAtt.ExportParameters(true);

                        var cpk = new CborMap
                        {
                            { COSE.KeyCommonParameter.KeyType, type },
                            { COSE.KeyCommonParameter.Alg, alg },
                            { COSE.KeyTypeParameter.X, ecparams.Q.X },
                            { COSE.KeyTypeParameter.Y, ecparams.Q.Y },
                            { COSE.KeyTypeParameter.Crv, crv }
                        };

                        var x = (byte[])cpk[COSE.KeyTypeParameter.X];
                        var y = (byte[])cpk[COSE.KeyTypeParameter.Y];

                        _credentialPublicKey = new CredentialPublicKey(cpk);

                        var X5c = new CborArray {
                            attestnCert.RawData,
                            root.RawData
                        };

                        _attestationObject.Add("attStmt", new CborMap {
                            { "x5c", X5c }
                        });
                    }
            }
        }
Esempio n. 10
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. 11
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);
            }
        }
        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 = "https://www.passwordless.dev";

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