public async Task <MetadataStatement> GetMetadataStatement(MetadataTOCPayloadEntry entry)
        {
            var statementBase64Url = await DownloadStringAsync(entry.Url + "/?token=" + _token);

            var tocAlg = await GetTocAlg();

            var statementBytes  = Base64Url.Decode(statementBase64Url);
            var statementString = System.Text.Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
            var statement       = JsonConvert.DeserializeObject <MetadataStatement>(statementString);

            statement.Hash = Base64Url.Encode(CryptoUtils.GetHasher(new HashAlgorithmName(tocAlg)).ComputeHash(System.Text.Encoding.UTF8.GetBytes(statementBase64Url)));

            if (!HashesAreEqual(entry.Hash, statement.Hash))
            {
                throw new Fido2VerificationException("TOC entry and statement hashes do not match");
            }

            return(statement);
        }
Beispiel #2
0
        public async Task <MetadataStatement> GetMetadataStatement(MetadataTOCPayload toc, MetadataTOCPayloadEntry entry)
        {
            var statementBase64Url = await DownloadStringAsync(entry.Url + "/?token=" + WebUtility.UrlEncode(_token));

            var statementBytes  = Base64Url.Decode(statementBase64Url);
            var statementString = Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
            var statement       = Newtonsoft.Json.JsonConvert.DeserializeObject <MetadataStatement>(statementString);

            using (HashAlgorithm hasher = CryptoUtils.GetHasher(new HashAlgorithmName(toc.JwtAlg)))
            {
                statement.Hash = Base64Url.Encode(hasher.ComputeHash(Encoding.UTF8.GetBytes(statementBase64Url)));
            }

            if (!HashesAreEqual(entry.Hash, statement.Hash))
            {
                throw new Fido2VerificationException("TOC entry and statement hashes do not match");
            }

            return(statement);
        }
        public void AddConformanceTOC()
        {
            var endpoints = new string[] {
                "https://fidoalliance.co.nz/mds/execute/20c027c091eba81d2e92c6581bf42c68776dc3910cf48840b73a035e5d70f956",
                "https://fidoalliance.co.nz/mds/execute/3e0be36ab70cdf5f32ae858b8610fcb7bf6e4f1aa47c7e53afcda5c822f5a346",
                "https://fidoalliance.co.nz/mds/execute/55a6301b9d7a7a45dc27dceeddc9b0ae4396c7d9ea8f46757018dd865dda24c5",
                "https://fidoalliance.co.nz/mds/execute/62c8ba89cf4f991e6890f442a606bb0b6f31f9a05946031846c4af1113046900",
                "https://fidoalliance.co.nz/mds/execute/d352b77e801de7b0d7d9842b02721c3e708c82405353235d2c04081fff8a302a"
            };

            var client = new System.Net.WebClient();

            foreach (var tocURL in endpoints)
            {
                var rawTOC = client.DownloadString(tocURL);
                //var jwtToken = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(rawTOC);
                //var tocPayload = (jwtToken).Payload.SerializeToJson();
                MetadataTOCPayload toc = null;
                try { toc = ValidatedTOCFromJwtSecurityToken(rawTOC, true); }
                catch { Exception ex; continue; }

                foreach (var entry in toc.Entries)
                {
                    if (null != entry.AaGuid)
                    {
                        var rawStatement      = client.DownloadString(entry.Url);
                        var statementBytes    = Base64Url.Decode(rawStatement);
                        var statement         = System.Text.Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
                        var metadataStatement = JsonConvert.DeserializeObject <MetadataStatement>(statement);
                        metadataStatement.Hash  = Base64Url.Encode(CryptoUtils.GetHasher(new HashAlgorithmName(tocAlg)).ComputeHash(System.Text.Encoding.UTF8.GetBytes(rawStatement)));
                        entry.MetadataStatement = metadataStatement;
                        payload.Add(new Guid(entry.AaGuid), entry);
                    }
                }
            }
        }
Beispiel #4
0
        protected async Task <MetadataTOCPayload> DeserializeAndValidateToc(string rawTocJwt)
        {
            if (string.IsNullOrWhiteSpace(rawTocJwt))
            {
                throw new ArgumentNullException(nameof(rawTocJwt));
            }

            var jwtParts = rawTocJwt.Split('.');

            if (jwtParts.Length != 3)
            {
                throw new ArgumentException("The JWT does not have the 3 expected components");
            }

            var tocHeaderString = jwtParts.First();
            var tocHeader       = JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(tocHeaderString)));

            var tocAlg = tocHeader["alg"]?.Value <string>();

            if (tocAlg == null)
            {
                throw new ArgumentNullException("No alg value was present in the TOC header.");
            }

            var x5cArray = tocHeader["x5c"] as JArray;

            if (x5cArray == null)
            {
                throw new Exception("No x5c array was present in the TOC header.");
            }

            var keyStrings = x5cArray.Values <string>().ToList();

            if (keyStrings.Count == 0)
            {
                throw new ArgumentException("No keys were present in the TOC header.");
            }

            var rootCert = GetX509Certificate(ROOT_CERT);
            var tocCerts = keyStrings.Select(o => GetX509Certificate(o)).ToArray();

            var keys = new List <SecurityKey>();

            foreach (var certString in keyStrings)
            {
                var cert = GetX509Certificate(certString);

                var ecdsaPublicKey = cert.GetECDsaPublicKey();
                if (ecdsaPublicKey != null)
                {
                    keys.Add(new ECDsaSecurityKey(ecdsaPublicKey));
                    continue;
                }

                var rsaPublicKey = cert.GetRSAPublicKey();
                if (rsaPublicKey != null)
                {
                    keys.Add(new RsaSecurityKey(rsaPublicKey));
                    continue;
                }
                throw new Fido2MetadataException("Unknown certificate algorithm");
            }
            var tocPublicKeys = keys.ToArray();

            var certChain = new X509Chain();

            certChain.ChainPolicy.ExtraStore.Add(rootCert);
            certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;

            var validationParameters = new TokenValidationParameters
            {
                ValidateIssuer           = false,
                ValidateAudience         = false,
                ValidateLifetime         = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKeys        = tocPublicKeys,
            };

            var tokenHandler = new JwtSecurityTokenHandler();

            tokenHandler.ValidateToken(
                rawTocJwt,
                validationParameters,
                out var validatedToken);

            if (tocCerts.Length > 1)
            {
                certChain.ChainPolicy.ExtraStore.AddRange(tocCerts.Skip(1).ToArray());
            }

            var certChainIsValid = certChain.Build(tocCerts.First());

            // if the root is trusted in the context we are running in, valid should be true here
            if (!certChainIsValid)
            {
                foreach (var element in certChain.ChainElements)
                {
                    if (element.Certificate.Issuer != element.Certificate.Subject)
                    {
                        var cdp     = CryptoUtils.CDPFromCertificateExts(element.Certificate.Extensions);
                        var crlFile = await DownloadDataAsync(cdp);

                        if (true == CryptoUtils.IsCertInCRL(crlFile, element.Certificate))
                        {
                            throw new Fido2VerificationException(string.Format("Cert {0} found in CRL {1}", element.Certificate.Subject, cdp));
                        }
                    }
                }

                // otherwise we have to manually validate that the root in the chain we are testing is the root we downloaded
                if (rootCert.Thumbprint == certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate.Thumbprint &&
                    // and that the number of elements in the chain accounts for what was in x5c plus the root we added
                    certChain.ChainElements.Count == (keyStrings.Count + 1) &&
                    // and that the root cert has exactly one status listed against it
                    certChain.ChainElements[certChain.ChainElements.Count - 1].ChainElementStatus.Length == 1 &&
                    // and that that status is a status of exactly UntrustedRoot
                    certChain.ChainElements[certChain.ChainElements.Count - 1].ChainElementStatus[0].Status == X509ChainStatusFlags.UntrustedRoot)
                {
                    // if we are good so far, that is a good sign
                    certChainIsValid = true;
                    for (var i = 0; i < certChain.ChainElements.Count - 1; i++)
                    {
                        // check each non-root cert to verify zero status listed against it, otherwise, invalidate chain
                        if (0 != certChain.ChainElements[i].ChainElementStatus.Length)
                        {
                            certChainIsValid = false;
                        }
                    }
                }
            }

            if (!certChainIsValid)
            {
                throw new Fido2VerificationException("Failed to validate cert chain while parsing TOC");
            }

            var tocPayload = ((JwtSecurityToken)validatedToken).Payload.SerializeToJson();

            var toc = Newtonsoft.Json.JsonConvert.DeserializeObject <MetadataTOCPayload>(tocPayload);

            toc.JwtAlg = tocAlg;
            return(toc);
        }
Beispiel #5
0
        public async Task <MetadataBLOBPayload> DeserializeAndValidateBlob(string rawBLOBJwt, CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(rawBLOBJwt))
            {
                throw new ArgumentNullException(nameof(rawBLOBJwt));
            }

            var jwtParts = rawBLOBJwt.Split('.');

            if (jwtParts.Length != 3)
            {
                throw new ArgumentException("The JWT does not have the 3 expected components");
            }

            var blobHeader = jwtParts[0];

            using var jsonDoc = JsonDocument.Parse(Base64Url.Decode(blobHeader));
            var tokenHeader = jsonDoc.RootElement;

            var blobAlg = tokenHeader.TryGetProperty("alg", out var algEl)
                ? algEl.GetString() !
                : throw new ArgumentNullException("No alg value was present in the BLOB header.");

            var blobCertStrings = tokenHeader.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array
                ? x5cEl.ToStringArray()
                : throw new ArgumentException("No x5c array was present in the BLOB header.");

            var rootCert         = GetX509Certificate(ROOT_CERT);
            var blobCertificates = new X509Certificate2[blobCertStrings.Length];
            var blobPublicKeys   = new List <SecurityKey>();

            for (int i = 0; i < blobCertStrings.Length; i++)
            {
                var cert = GetX509Certificate(blobCertStrings[i]);
                blobCertificates[i] = cert;

                if (cert.GetECDsaPublicKey() is ECDsa ecdsaPublicKey)
                {
                    blobPublicKeys.Add(new ECDsaSecurityKey(ecdsaPublicKey));
                }

                else if (cert.GetRSAPublicKey() is RSA rsa)
                {
                    blobPublicKeys.Add(new RsaSecurityKey(rsa));
                }
            }

            var certChain = new X509Chain();

            certChain.ChainPolicy.ExtraStore.Add(rootCert);
            certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;

            var validationParameters = new TokenValidationParameters
            {
                ValidateIssuer           = false,
                ValidateAudience         = false,
                ValidateLifetime         = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKeys        = blobPublicKeys,
            };

            var tokenHandler = new JwtSecurityTokenHandler()
            {
                // 250k isn't enough bytes for conformance test tool
                // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1097
                MaximumTokenSizeInBytes = rawBLOBJwt.Length
            };

            tokenHandler.ValidateToken(
                rawBLOBJwt,
                validationParameters,
                out var validatedToken);

            if (blobCertificates.Length > 1)
            {
                certChain.ChainPolicy.ExtraStore.AddRange(blobCertificates.Skip(1).ToArray());
            }

            var certChainIsValid = certChain.Build(blobCertificates[0]);

            // if the root is trusted in the context we are running in, valid should be true here
            if (!certChainIsValid)
            {
                foreach (var element in certChain.ChainElements)
                {
                    if (element.Certificate.Issuer != element.Certificate.Subject)
                    {
                        var cdp     = CryptoUtils.CDPFromCertificateExts(element.Certificate.Extensions);
                        var crlFile = await DownloadDataAsync(cdp, cancellationToken);

                        if (CryptoUtils.IsCertInCRL(crlFile, element.Certificate))
                        {
                            throw new Fido2VerificationException($"Cert {element.Certificate.Subject} found in CRL {cdp}");
                        }
                    }
                }

                // otherwise we have to manually validate that the root in the chain we are testing is the root we downloaded
                if (rootCert.Thumbprint == certChain.ChainElements[^ 1].Certificate.Thumbprint &&
                    // and that the number of elements in the chain accounts for what was in x5c plus the root we added
                    certChain.ChainElements.Count == (blobCertStrings.Length + 1) &&
                    // and that the root cert has exactly one status listed against it
                    certChain.ChainElements[^ 1].ChainElementStatus.Length == 1 &&
                    // and that that status is a status of exactly UntrustedRoot
                    certChain.ChainElements[^ 1].ChainElementStatus[0].Status == X509ChainStatusFlags.UntrustedRoot)
                {
                    // if we are good so far, that is a good sign
                    certChainIsValid = true;
                    for (var i = 0; i < certChain.ChainElements.Count - 1; i++)
                    {
                        // check each non-root cert to verify zero status listed against it, otherwise, invalidate chain
                        if (0 != certChain.ChainElements[i].ChainElementStatus.Length)
                        {
                            certChainIsValid = false;
                        }
                    }
                }
            }

            if (!certChainIsValid)
            {
                throw new Fido2VerificationException("Failed to validate cert chain while parsing BLOB");
            }

            var blobPayload = ((JwtSecurityToken)validatedToken).Payload.SerializeToJson();

            var blob = JsonSerializer.Deserialize <MetadataBLOBPayload>(blobPayload) !;

            blob.JwtAlg = blobAlg;
            return(blob);
        }
Beispiel #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
            // (handled in base class)
            if ((CBORType.TextString != attStmt["ver"].Type) ||
                (0 == attStmt["ver"].AsString().Length))
            {
                throw new Fido2VerificationException("Invalid version in SafetyNet data");
            }

            // 2. Verify that response is a valid SafetyNet response of version ver
            var ver = attStmt["ver"].AsString();

            if ((CBORType.ByteString != attStmt["response"].Type) ||
                (0 == attStmt["response"].GetByteString().Length))
            {
                throw new Fido2VerificationException("Invalid response in SafetyNet data");
            }

            var response    = attStmt["response"].GetByteString();
            var responseJWT = Encoding.UTF8.GetString(response);

            if (string.IsNullOrWhiteSpace(responseJWT))
            {
                throw new Fido2VerificationException("SafetyNet response null or whitespace");
            }

            var jwtParts = responseJWT.Split('.');

            if (jwtParts.Length != 3)
            {
                throw new Fido2VerificationException("SafetyNet response JWT does not have the 3 expected components");
            }

            var jwtHeaderString = jwtParts.First();
            var jwtHeaderJSON   = JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(jwtHeaderString)));

            var x5cArray = jwtHeaderJSON["x5c"] as JArray;

            if (x5cArray == null)
            {
                throw new Fido2VerificationException("SafetyNet response JWT header missing x5c");
            }
            var x5cStrings = x5cArray.Values <string>().ToList();

            if (x5cStrings.Count == 0)
            {
                throw new Fido2VerificationException("No keys were present in the TOC header in SafetyNet response JWT");
            }

            var certs = new List <X509Certificate2>();
            var keys  = new List <SecurityKey>();

            foreach (var certString in x5cStrings)
            {
                var cert = GetX509Certificate(certString);
                certs.Add(cert);

                var ecdsaPublicKey = cert.GetECDsaPublicKey();
                if (ecdsaPublicKey != null)
                {
                    keys.Add(new ECDsaSecurityKey(ecdsaPublicKey));
                }

                var rsaPublicKey = cert.GetRSAPublicKey();
                if (rsaPublicKey != null)
                {
                    keys.Add(new RsaSecurityKey(rsaPublicKey));
                }
            }

            var validationParameters = new TokenValidationParameters
            {
                ValidateIssuer           = false,
                ValidateAudience         = false,
                ValidateLifetime         = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKeys        = keys
            };

            var           tokenHandler   = new JwtSecurityTokenHandler();
            SecurityToken validatedToken = null;

            try
            {
                tokenHandler.ValidateToken(
                    responseJWT,
                    validationParameters,
                    out validatedToken);
            }
            catch (SecurityTokenException ex)
            {
                throw new Fido2VerificationException("SafetyNet response security token validation failed", ex);
            }

            var  nonce           = "";
            bool?ctsProfileMatch = null;
            var  timestampMs     = DateTimeHelper.UnixEpoch;

            var jwtToken = validatedToken as JwtSecurityToken;

            foreach (var claim in jwtToken.Claims)
            {
                if (("nonce" == claim.Type) && ("http://www.w3.org/2001/XMLSchema#string" == claim.ValueType) && (0 != claim.Value.Length))
                {
                    nonce = claim.Value;
                }
                if (("ctsProfileMatch" == claim.Type) && ("http://www.w3.org/2001/XMLSchema#boolean" == claim.ValueType))
                {
                    ctsProfileMatch = bool.Parse(claim.Value);
                }
                if (("timestampMs" == claim.Type) && ("http://www.w3.org/2001/XMLSchema#integer64" == claim.ValueType))
                {
                    timestampMs = DateTimeHelper.UnixEpoch.AddMilliseconds(double.Parse(claim.Value));
                }
            }

            var notAfter  = DateTime.UtcNow.AddMilliseconds(_driftTolerance);
            var notBefore = DateTime.UtcNow.AddMinutes(-1).AddMilliseconds(-(_driftTolerance));

            if ((notAfter < timestampMs) || ((notBefore) > timestampMs))
            {
                throw new Fido2VerificationException(string.Format("SafetyNet timestampMs must be present and between one minute ago and now, got: {0}", timestampMs.ToString()));
            }

            // 3. Verify that the nonce in the response is identical to the SHA-256 hash of the concatenation of authenticatorData and clientDataHash
            if ("" == nonce)
            {
                throw new Fido2VerificationException("Nonce value not found in SafetyNet attestation");
            }

            byte[] nonceHash = null;
            try
            {
                nonceHash = Convert.FromBase64String(nonce);
            }
            catch (Exception ex)
            {
                throw new Fido2VerificationException("Nonce value not base64string in SafetyNet attestation", ex);
            }

            using (var hasher = CryptoUtils.GetHasher(HashAlgorithmName.SHA256))
            {
                var dataHash = hasher.ComputeHash(Data);
                if (false == dataHash.SequenceEqual(nonceHash))
                {
                    throw new Fido2VerificationException(
                              string.Format(
                                  "SafetyNet response nonce / hash value mismatch, nonce {0}, hash {1}",
                                  BitConverter.ToString(nonceHash).Replace("-", ""),
                                  BitConverter.ToString(dataHash).Replace("-", "")
                                  )
                              );
                }
            }

            // 4. Let attestationCert be the attestation certificate
            var attestationCert = certs[0];
            var subject         = attestationCert.GetNameInfo(X509NameType.DnsName, false);

            // 5. Verify that the attestation certificate is issued to the hostname "attest.android.com"
            if (false == ("attest.android.com").Equals(subject))
            {
                throw new Fido2VerificationException(string.Format("SafetyNet attestation cert DnsName invalid, want {0}, got {1}", "attest.android.com", subject));
            }

            // 6. Verify that the ctsProfileMatch attribute in the payload of response is true
            if (null == ctsProfileMatch)
            {
                throw new Fido2VerificationException("SafetyNet response ctsProfileMatch missing");
            }

            if (true != ctsProfileMatch)
            {
                throw new Fido2VerificationException("SafetyNet response ctsProfileMatch false");
            }

            return(AttestationType.Basic, new X509Certificate2[] { attestationCert });
        }
 public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
 {
     writer.WriteStringValue(Base64Url.Encode(value));
 }
 public override void WriteJson(JsonWriter writer, byte[] value, JsonSerializer serializer)
 {
     writer.WriteValue(Base64Url.Encode(value));
 }