private async Task <MetadataBLOBPayload> DeserializeAndValidateBlobAsync(string rawBLOBJwt, CancellationToken cancellationToken) { 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 blobHeaderString = jwtParts[0]; using var blobHeaderDoc = JsonDocument.Parse(Base64Url.Decode(blobHeaderString)); var blobHeader = blobHeaderDoc.RootElement; string blobAlg = blobHeader.TryGetProperty("alg", out var algEl) ? algEl.GetString() ! : throw new ArgumentNullException("No alg value was present in the BLOB header."); string[] keyStrings = blobHeader.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array ? x5cEl.ToStringArray() : throw new ArgumentNullException("No x5c array was present in the BLOB header."); if (keyStrings.Length is 0) { throw new ArgumentException("No keys were present in the BLOB header."); } var rootCert = GetX509Certificate(ROOT_CERT); var blobCerts = new X509Certificate2[keyStrings.Length]; var keys = new SecurityKey[keyStrings.Length]; for (int i = 0; i < blobCerts.Length; i++) { var cert = GetX509Certificate(keyStrings[i]); blobCerts[i] = cert; if (cert.GetECDsaPublicKey() is ECDsa ecdsaPublicKey) { keys[i] = new ECDsaSecurityKey(ecdsaPublicKey); } else if (cert.GetRSAPublicKey() is RSA rsaPublicKey) { keys[i] = new RsaSecurityKey(rsaPublicKey); } else { throw new Fido2MetadataException("Unknown certificate algorithm"); } } var blobPublicKeys = keys.ToArray(); // defensive copy 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 (blobCerts.Length > 1) { certChain.ChainPolicy.ExtraStore.AddRange(blobCerts.Skip(1).ToArray()); } var certChainIsValid = certChain.Build(blobCerts[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 == (keyStrings.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); }