-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
Background and Motivation
A service can use SubjectName/Issuer authentication (see https://learn.microsoft.com/en-us/entra/msal/javascript/node/sni, AzureAD/microsoft-authentication-library-for-python#60) to trust the client making an API call.
The service only accepts certain issuers which have strict association of subject name and requestor.
An SubjectName/Issuer call done by a client looks like
var authority = $"https://login.microsoftonline.com/{tenantId}";
var scopes = new[] { "https://graph.microsoft.com/.default" };
X509Certificate2 certificate; /* client's certificate used for mTLS*/
var app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithAuthority(authority)
.WithCertificate(certificate)
.Build();
// Acquire token
var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();In security-sensitive environments certificate validation must be performed in a controlled and deterministic manner to ensure trust and prevent vulnerabilities.
Therefore, for SN/Issuer services often operate under strict issuer constraints (e.g., only accepting certificates from pre-approved PKIs), using only offline chain validation to maintain integrity and compliance with organizational or tenant-level trust policies.
In such cases there is no intent on relying on the CRL Distribution Point (CDP) and Authority Information Access (AIA) extensions embedded in client certificates. CDP and AIA may point to untrusted or dynamic endpoints, services must explicitly manage certificate chain building and revocation checking. Thus this approach of offline client certificate validation mitigates risks such as server-side request forgery (SSRF) and ensures that CRLs are retrieved only from vetted, secure sources.
For these services to perform certificate chain building offline during client certificate validation to maintain full control over the trust anchors and intermediate certificates involved in the process. it is possible thanks to the API exposed by .NET as in the following example
bool TryOfflineBuildChian(X509Certificate clientCertificate, out X509Chain chain)
{
chain = new X509Chain
{
ChainPolicy = {
DisableCertificateDownloads = true,
TrustMode = X509ChainTrustMode.CustomRootTrust,
RevocationMode = X509RevocationMode.NoCheck
}
};
chain.ChainPolicy.CustomTrustStore.Add(rootCert);
chain.ChainPolicy.ExtraStore.Add(intermediateCert);
return chain.Build(clientCertificate);
}However, while it allows for greater control over chain construction, .NET currently lacks built-in APIs for manually validating CRLs outside the X509Chain and without depending on Bouncy Castle or platform-specific libraries like Windows CAPI. Both Bouncy Castle and Windows CAPI offers essential features such building a CRL from bytes, enumerating revoked serial numbers, verifying CRL signatures with a public key, and obtain CRL's files such as ThisUpdate and NextUpdate , capabilities crucial for services that need to conduct revocation checks in a strictly controlled and platform-independent manner. By introducing similar functionalities in .NET for manual CRL validation, services would gain the ability to maintain strict control over certificate trust decisions without relying on external libraries or foreign function interfaces which are a bottleneck for the performances.
Using Bouncy Castle it is possible to write the following code
X509Certificate2 issuerCert; /* issuer CA certificate */
Uri crlEndpoint= GetTrustedCrlDistributionPoint(issuerCert);
byte[] crlBytes = DownloadCrl(crlEndpoint); /* CRL bytes */
X509CrlParser parser = new X509CrlParser();
X509Crl crl = parser.ReadCrl(crlBytes);
// Validate issuer and signature
var issuerBc = DotNetUtilities.FromX509Certificate(issuerCert2);
if (!crl.Verify(issuerBc.GetPublicKey()))
throw new CryptographicException("CRL is not issued by the given certificate (DN/signature mismatch).");
// Enumerate revoked
foreach (X509CrlEntry entry in crl.GetRevokedCertificates())
{
Console.WriteLine(entry);
}Additionally, exposing properties such as ThisUpdate, NextUpdate, and the list of revoked certificates empowers services handling high RPS traffic, requiring SubjectName/Issuer validation on each request, to implement efficient certificate revocation verification mechanisms.
For example, instead of relying on standard CRL validation par of the X509Chian during each request, which typically involves downloading and parsing CRLs from remote endpoints, these services could perform rapid in-memory lookups using optimized data structures and caches. These caches are indexed to quickly identify whether a client certificate has been revoked, significantly reducing latency and eliminating network dependencies after the initial CRL download.
The logic executed by these processes can be summarized as:
bool IsCertificateValid(X509Certificate clientCertificate)
{
if (TryOfflineBuildChian(clientCertificate, out X509Chain chain))
{
foreach (var element in chain.ChainElements)
{
var issuer = element.Certificate.Issuer;
var crl = CrlCache.Get(issuer);
if (crl == null)
{
var url = GetTrustedCrlDistributionPoint(element.Certificate);
var crlBytes = DownloadCrl(url);
crl = ParseCrlAndValidateSignature(crlBytes, issuer);
CrlCache.Set(issuer, crl);
}
if (crl.IsRevoked(element.Certificate.SerialNumber))
return false;
}
return true;
}
return false;
}Furthermore, given the nature of high-volume services, it's essential for developers to have control over throttling and retry mechanisms when downloading CRLs. For example, they may need to define how the service behaves when a CRL distribution endpoint is unavailable, or implement throttling strategies to avoid unintentionally overwhelming the endpoint, potentially causing a denial-of-service scenarios. These considerations are especially important in distributed systems and cloud-native architectures, where reliability, scalability, and fault tolerance are critical and every services weights differently each of this characteristics. Providing developers with fine-grained control over the CRL allows them to build robust and predictable revocation checking under varying network and load conditions.
Proposed API
Introduce a the following objects X509CertificateRevocationList, X509CertificateRevocationListRevocationEntry, X509CertificateRevocationListLoader.
public class CertificateRevocationListBuilder
{
public static CertificateRevocationListBuilder Load(byte[] crl, out BigInteger crlNumber)
+ public static CertificateRevocationListBuilder Load(byte[] crl,
+ X509Certificate2 issuerCertificate,
+ out BigInteger crlNumber,
+ out DateTimeOffset thisUpdate,
+ out DateTimeOffset nextUpdate,
+ out DateTimeOffset? nextPublish = default);
public static CertificateRevocationListBuilder LoadPem(byte[] currentCrl, out BigInteger crlNumber)
+ public static CertificateRevocationListBuilder LoadPem(byte[] currentCrl,
+ X509Certificate2 issuerCertificate,
+ out BigInteger crlNumber,
+ out DateTimeOffset thisUpdate,
+ out DateTimeOffset nextUpdate,
+ out DateTimeOffset? nextPublish = default);
+ public X509CertificateRevocationList ToX509CertificateRevocationList(X509Certificate2 issuerCertificate,
+ BigInteger crlNumber,
+ DateTimeOffset nextUpdate,
+ HashAlgorithmName hashAlgorithm,
+ RSASignaturePadding RSASignaturePadding? rsaSignaturePadding = default,
+ DateTimeOffset? thisUpdate = default);
+ public X509CertificateRevocationList ToX509CertificateRevocationList(X500DistinguishedName issuerName,
+ X509SignatureGenerator generator,
+ BigInteger crlNumber,
+ DateTimeOffset nextUpdate,
+ HashAlgorithmName hashAlgorithm,
+ X509AuthorityKeyIdentifierExtension authorityKeyIdentifier,
+ DateTimeOffset? thisUpdate = default);
}+ public static class X509CertificateRevocationListLoader
+ {
+ public static X509CertificateRevocationList LoadCertificate(byte[] data);
+ }
+ public sealed class X509CertificateRevocationList
+ {
+ /// <summary>
+ /// Gets the raw DER-encoded CRL data.
+ /// </summary>
+ public byte[] RawData { get; }
+
+ /// <summary>
+ /// Gets the distinguished name of the CRL issuer.
+ /// </summary>
+ public X500DistinguishedName IssuerName { get; }
+
+ /// <summary>
+ /// Gets the date when the CRL was issued.
+ /// </summary>
+ public DateTime ThisUpdate { get; }
+
+ /// <summary>
+ /// Gets the date when the CRL expires, if present.
+ /// </summary>
+ public DateTime NextUpdate { get; }
+
+ /// <summary>
+ /// Gets the date when the next version of the CRL will be issued, if present.
+ /// </summary>
+ public DateTime? NextPublish { get; }
+
+ /// <summary>
+ /// Gets the CRL number, if present.
+ /// </summary>
+ public BigInteger? CrlNumber { get; }
+
+ /// <summary>
+ /// Gets the CRL version.
+ /// </summary>
+ public int Version { get; }
+
+ /// <summary>
+ /// Signature bytes.
+ /// </summary>
+ public byte[] Signature { get; }
+
+ /// <summary>
+ /// Gets the signature algorithm OID or friendly name.
+ /// </summary>
+ public Oid SignatureAlgorithmOid { get; }
+
+ /// <summary>
+ /// Gets the CRL extensions.
+ /// </summary>
+ public X509ExtensionCollection Extensions { get; }
+
+ /// <summary>
+ /// Gets the collection of revoked certificates.
+ /// </summary>
+ public IReadOnlyCollection<X509CertificateRevocationListRevocationEntry> RevokedCertificates { get; }
+
+ /// <summary>
+ /// Verifies the CRL signature using the specified issuer certificate.
+ /// </summary>
+ /// <param name="issuerCert">The issuer certificate.</param>
+ /// <returns>True if the signature is valid; otherwise, false.</returns>
+ public bool VerifySignature(X509Certificate2 issuerCert);
+
+ /// <summary>
+ /// Determines whether a certificate with the specified serial number is listed as revoked in this CRL.
+ /// </summary>
+ /// <param name="certificate">The certificate to check.</param>
+ /// <returns>True if revoked; otherwise, false.</returns>
+ public bool IsRevoked(X509Certificate2 certificate);
+
+ /// <summary>
+ /// Determines whether a certificate with the specified serial number is listed as revoked in this CRL.
+ /// </summary>
+ /// <param name="serialNumber"> The serial number of the certificate to check.</param>
+ /// <returns> True if the certificate with the given serial number is revoked; otherwise, false. </returns>
+ public bool IsRevoked(string SerialNumber);
+
+ /// <summary>
+ /// Attempts to retrieve the revoked certificate entry for the given serial number.
+ /// </summary>
+ /// <param name="serialNumber">The serial number to look up.</param>
+ /// <param name="revocationEntry">The revoked certificate entry if found; otherwise, null.</param>
+ /// <returns>True if the entry exists; otherwise, false.</returns>
+ public bool TryGetRevokedCertificate(string serialNumber, out X509CertificateRevocationListRevocationEntry? revocationEntry);
+
+ /// <summary>
+ /// Attempts to retrieve the revoked certificate entry for the specified certificate.
+ /// </summary>
+ /// <param name="certificate">The certificate to look up.</param>
+ /// <param name="revocationEntry">The revoked certificate entry if found; otherwise, null.</param>
+ /// <returns>True if the certificate has been revoked; otherwise, false.</returns>
+ public bool TryGetRevokedCertificate(X509Certificate2 certificate, out X509CertificateRevocationListRevocationEntry? revocationEntry);
+}
+
+ /// <summary>
+ /// Represents a revoked certificate entry in a CRL.
+ /// </summary>
+ public sealed class X509CertificateRevocationListRevocationEntry
+ {
+ /// <summary>
+ /// Gets the serial number of the revoked certificate.
+ /// </summary>
+ public string SerialNumber { get; }
+
+ /// <summary>
+ /// Gets the date when the certificate was revoked.
+ /// </summary>
+ public DateTimeOffset RevocationTime { get; }
+
+ /// <summary>
+ /// Gets the reason for revocation, if present.
+ /// </summary>
+ public string Reason { get; }
+
+ /// <summary>
+ /// Gets the extensions.
+ /// </summary>
+ public X509ExtensionCollection Extensions { get; }
+
+ /// <summary>
+ /// Gets the raw DER-encoded data.
+ /// </summary>
+ public byte[] RawData { get; }
+}Usage Examples
bool IsCertificateValid(X509Certificate clientCertificate)
{
if (TryBuildChain(clientCertificate, out X509Chain chain))
{
foreach (var element in chain.ChainElements)
{
var issuer = element.Certificate.Issuer;
X509CertificateRevocationList? crl = CrlCache.Get(issuer);
if (crl == null)
{
var url = GetTrustedCrlDistributionPoint(issuer);
byte[] crlBytes = DownloadCrl(url);
crl = X509CertificateRevocationListLoader.Load(crlBytes);
if (!crl.VerifySignature(issuer))
{
throw new CryptographicException($"CRL {url} is not issued by the certificate {issuer.SubjectName}");
}
CrlCache.Set(issuer, crl);
}
if (crl.IsRevoked(element.Certificate.SerialNumber))
return false;
}
return true;
}
return false;
}