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