dotnet / dotnet/runtime

Certificate chain validation fails if CA has no CRL

Open
#124,802 11 comments 0 reactions 0 assignees View on GitHub
area-System.Security discussion
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Description

I'm trying to use [MySqlConnector](https://www.nuget.org/packages/MySqlConnector) with SSL (not mTLS) by setting its [`SslMode`](https://mysqlconnector.net/connection-options/#ssltls-options) connection string parameter to `VerifyFull`. This fails because the CA certificate in our Cloud Foundry environment doesn't contain a Certificate Revocation List.

The reason that `VerifyFull` fails is because [`SslClientAuthenticationOptions.CertificateRevocationCheckMode`](https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslclientauthenticationoptions.certificaterevocationcheckmode?view=net-10.0) gets set to `X509RevocationMode.Online` at https://github.com/mysql-net/MySqlConnector/blob/2.5.0/src/MySqlConnector/Core/ServerSession.cs#L1711:
```c#
var checkCertificateRevocation = cs.SslMode == MySqlSslMode.VerifyFull;
clientAuthenticationOptions.CertificateRevocationCheckMode =
checkCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck;
```
We could ask MySqlConnector to introduce a new connection string parameter to override the `CertificateRevocationCheckMode` value, but then the user needs to know upfront whether certificates in the trust chain contain CRLs. A better solution would be to adapt the chain verification in .NET: if a certificate contains no CRL, skip the CRL check without failure.

This would also match the behavior of the OpenSSL CLI, which succeeds full verification even when no CRL is present.

### Reproduction Steps

## Part 1: generate certificates and verify that OpenSSL CLI validates the chain successfully

1. Generate the CA Private Key:
```bash
openssl genrsa -out ca.key 4096
```
1. Generate the CA Certificate:
```bash
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj "/C=US/ST=State/L=City/O=Testing/CN=MyTestCA"
```
1. Create a Configuration File for the Server Cert: `server.conf`
```ini
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
CN = verifycrl.test.net

[v3_req]
keyUsage = keyEncipherment, dataEncipherment, digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = verifycrl.test.net
```
1. Generate the Server Certificate:
```bash
openssl genrsa -out server.key 2048
```
1. Generate the CSR (using the config file):
```bash
openssl req -new -key server.key -out server.csr -config server.conf
```
1. Sign the Server Cert with the CA:
```bash
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 365 -sha256 -extensions v3_req -extfile server.conf
```
1. Verify the chain:
```bash
openssl verify -CAfile ca.crt server.crt
```
This prints:
```text
server.crt: OK
```
1. Verify CN and SAN
```bash
openssl x509 -in server.crt -noout -text | grep -E "Subject:|DNS"
```
This prints:
```text
Subject: CN = verifycrl.test.net
DNS:verifycrl.test.net
```

## Part 2: Use the generated certificates from a .NET application
1. Create a new .NET 10 console app
1. Copy the files `ca.crt` and `server.crt` into the project
1. Ensure the certificates are copied to the output folder by adding the following to the project file:
```xml


PreserveNewest


```
1. Replace the contents of `Program.cs` with the following:
```c#
using System.Security.Cryptography.X509Certificates;

var caCertificate = new X509Certificate2("ca.crt");
var serverCertificate = new X509Certificate2("server.crt");

var chain = new X509Chain();
//chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.ExtraStore.Add(caCertificate);
bool success = chain.Build(serverCertificate);

if (success)
{
Console.WriteLine("Verification succeeded.");
}
else
{
Console.WriteLine("Verification failed.");
Console.WriteLine($"Chain built with {chain.ChainElements.Count} elements.");

for (int index = 0; index < chain.ChainElements.Count; index++)
{
X509ChainElement element = chain.ChainElements[index];
Console.WriteLine($"Element {index}: {element.Certificate.GetNameInfo(X509NameType.SimpleName, false)}");
Console.WriteLine(" Status:");

foreach (X509ChainStatus status in element.ChainElementStatus)
{
Console.WriteLine($" {status.Status}: {status.StatusInformation}");
}
}
}
```
1. Run the program, which prints:
```text
Verification failed.
Chain built with 2 elements.
Element 0: verifycrl.test.net
Status:
RevocationStatusUnknown: The revocation function was unable to check revocation for the certificate.
OfflineRevocation: The revocation function was unable to check revocation because the revocation server was offline.
Element 1: MyTestCA
Status:
UntrustedRoot: A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider.
```
1. Uncomment the line that sets `RevocationMode` to `NoCheck` and rerun, which prints:
```text
Verification failed.
Chain built with 2 elements.
Element 0: verifycrl.test.net
Status:
Element 1: MyTestCA
Status:
UntrustedRoot: A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider.
```
Note: The UntrustedRoot error is expected; we didn't add the CA certificate to the system store.

## Additional information
I asked Cursor to analyze the .NET runtime behavior by feeding it the dotnet/runtime repository with the `release/10.0` branch checked out (currently at SHA ad38f95ebb9839f65ca1304fc99f4c137ecb17f6). It proposed code changes, see the transcript for details.

Cursor transcript (using Claude model)

# Certificate chain revocation verification divergence
_Exported on 2/23/2026 at 17:14:50 GMT+1 from Cursor (2.5.20)_

---

**User**

I've opened the source repository for the .NET runtime. The current behavior when verifying a certificate chain, is that a certificate revocation list must be present when using X509RevocationMode.Offline or X509RevocationMode.Online. My certificates don't contain a CRL. Verification succeeds when using X509RevocationMode.NoCheck, but fail with the other modes. When using the OpenSSL command line interface, chain verification succeeds even when certificates don't contain a CRL. Please identify where the .NET runtime diverges from the OpenSSL command line behavior. I'd like to understand how and where the runtime could be changed to succeed CRL verification when using X509RevocationMode.Offline or X509RevocationMode.Online and the certificates don't contain any revocation lists. This applies to Linux only.

---

**Cursor**

I'll help you investigate the .NET runtime's certificate revocation list (CRL) verification behavior on Linux and identify where it diverges from OpenSSL's command-line behavior.

Let me start by exploring the codebase to understand how certificate chain verification and CRL checking are implemented.

Now let me look at the Linux-specific OpenSSL implementation for certificate chain verification:

Now let me look at the OpenSslCrlCache to understand how CRLs are added:

Now let me search for where the actual OpenSSL verification happens and understand the flow:

Now let me look at the Interop layer to understand how these are called:

Let me look at the key section where the CRL check flag is set and understand the flow better:

Now let me understand the critical flow. Let me check how OpenSSL CLI behaves vs the .NET runtime:

Let me look at the actual OpenSSL documentation and understand the key difference:

This is very interesting! The web search suggests that OpenSSL CLI can succeed when certificates don't have CDP extensions. Let me verify this by looking at the actual OpenSSL source behavior and the .NET runtime's implementation more carefully:

Now I see the key issue! Let me search for information about X509_V_FLAG_CRL_CHECK behavior when no CRL is available:

Now I need to understand the exact divergence. Let me look at what happens when the .NET runtime sets the CRL check flag but no CRL is added to the store:

Perfect! Now I can see the issue clearly. Let me search for information about OpenSSL's actual behavior and verify my understanding:

Excellent! Now I understand the issue. Let me verify my understanding by looking at some tests to see if there's any documentation of the expected behavior:

Now I have a clear picture. Let me check if there's any flag or option in OpenSSL that could help:

Perfect! Now I understand the issue completely. Let me create a comprehensive analysis document. The key insight is:

1. **OpenSSL CLI behavior**: When you run `openssl verify` WITHOUT the `-crl_check` flag, it succeeds because it doesn't perform CRL checking at all
2. **.NET Runtime behavior**: When using `X509RevocationMode.Offline` or `X509RevocationMode.Online`, the runtime:
- Tries to find a CDP URL in the certificate
- If no CDP URL exists, it returns early (doesn't add any CRL to the store)
- Then sets the `X509_V_FLAG_CRL_CHECK` flag on the store
- Calls `X509_verify_cert()` which fails with `X509_V_ERR_UNABLE_TO_GET_CRL` because the flag is set but no CRL is in the store

The divergence is that .NET **always sets the CRL check flag** when revocation mode is not NoCheck, even when no CRLs could be found or added to the store.

Let me examine the exact flow more carefully to provide you with a complete analysis:

Now let me look at whether there's a way to conditionally set the CRL check flag:

Let me check if there's a way to track whether any CRLs were added to the store:

Now I have a complete picture. Let me create a comprehensive analysis for you. Let me also check if there are any tests that document this behavior:

Perfect! Now let me look at the event source to understand the logging better and then provide you with a comprehensive analysis:

Excellent! Now I have a complete understanding of the issue. Let me provide you with a comprehensive analysis:

## Analysis: .NET Runtime CRL Verification Divergence from OpenSSL CLI

I've identified the key divergence between the .NET runtime's certificate chain verification behavior and the OpenSSL command-line interface when certificates lack CRL Distribution Points (CDP).

### The Core Issue

**Location of the divergence:**

The issue occurs in the certificate revocation checking logic, specifically in these files:

1. **`OpenSslCrlCache.cs`** - Lines 28-73 (the `AddCrlForCertificate` method)
2. **`OpenSslX509ChainProcessor.cs`** - Lines 384-412 (the `ProcessRevocation` method)
3. **`pal_x509.c`** - Lines 242-253 (the `CryptoNative_X509StoreSetRevocationFlag` function)

### Current .NET Runtime Behavior

When `X509RevocationMode.Offline` or `X509RevocationMode.Online` is used:

1. **Step 1**: For each certificate in the chain, `AddCrlForCertificate()` is called

```42:46:c:\Source\Repos\runtime\src\libraries\System.Security.Cryptography\src\System\Security\Cryptography\X509Certificates\OpenSslCrlCache.cs
string? url = GetCdpUrl(cert);

if (url == null)
{
return;
}
```

2. **Step 2**: If no CDP URL is found (certificate lacks CRL Distribution Point extension), the method returns early **without adding any CRL to the store**

3. **Step 3**: After attempting to add CRLs for all certificates, the runtime **unconditionally** sets the `X509_V_FLAG_CRL_CHECK` flag:

```411:412:c:\Source\Repos\runtime\src\libraries\System.Security.Cryptography\src\System\Security\Cryptography\X509Certificates\OpenSslX509ChainProcessor.cs
Interop.Crypto.X509StoreSetRevocationFlag(_store, revocationFlag);
Interop.Crypto.X509StoreCtxRebuildChain(_storeCtx);
```

4. **Step 4**: The native code sets the flag unconditionally:

```242:253:c:\Source\Repos\runtime\src\native\libs\System.Security.Cryptography.Native\pal_x509.c
int32_t CryptoNative_X509StoreSetRevocationFlag(X509_STORE* ctx, X509RevocationFlag revocationFlag)
{
unsigned long verifyFlags = X509_V_FLAG_CRL_CHECK;

if (revocationFlag != EndCertificateOnly)
{
verifyFlags |= X509_V_FLAG_CRL_CHECK_ALL;
}

// Just a field mutator, no error queue interactions apply.
return X509_STORE_set_flags(ctx, verifyFlags);
}
```

5. **Step 5**: OpenSSL's `X509_verify_cert()` is called with `X509_V_FLAG_CRL_CHECK` set but **no CRLs in the store**, resulting in `X509_V_ERR_UNABLE_TO_GET_CRL` error

### OpenSSL CLI Behavior

When you run `openssl verify` without the `-crl_check` flag:
- OpenSSL **does not set** the `X509_V_FLAG_CRL_CHECK` flag
- Verification succeeds because CRL checking is not performed at all
- The certificate chain is validated for signatures, trust, and validity periods only

When you run `openssl verify -crl_check`:
- OpenSSL sets the `X509_V_FLAG_CRL_CHECK` flag
- If no CRL is available in the store, it **fails** with `X509_V_ERR_UNABLE_TO_GET_CRL`
- This is the same behavior as the .NET runtime

### The Key Divergence

The .NET runtime diverges from OpenSSL CLI in this way:

**OpenSSL CLI**: You have control over whether to enable CRL checking. If you don't specify `-crl_check`, verification succeeds without CRL validation.

**.NET Runtime**: When you specify `X509RevocationMode.Offline` or `X509RevocationMode.Online`, the runtime **always enables CRL checking** via `X509_V_FLAG_CRL_CHECK`, even when:
- No certificates in the chain have CDP extensions
- No CRLs could be downloaded or cached
- The store is completely empty of CRLs

### Proposed Solutions

To make the .NET runtime succeed like OpenSSL CLI when certificates lack CRLs, you have several options:

#### **Option 1: Conditionally Set CRL Check Flag (Most Compatible)**

Modify the code to only set `X509_V_FLAG_CRL_CHECK` if at least one CRL was successfully added to the store.

**Changes needed:**

1. Modify `AddCrlForCertificate()` to return a boolean indicating whether a CRL was added
2. Track whether any CRLs were added during the loop in `ProcessRevocation()`
3. Only call `X509StoreSetRevocationFlag()` if at least one CRL was added

**Location**: `OpenSslX509ChainProcessor.cs` lines 384-412 and `OpenSslCrlCache.cs` lines 28-73

#### **Option 2: Add X509_V_FLAG_EXTENDED_CRL_SUPPORT (If Available)**

Some OpenSSL versions support extended CRL features that may handle missing CRLs more gracefully. However, this flag may not be available in all OpenSSL versions.

#### **Option 3: Suppress UNABLE_TO_GET_CRL Errors for Certificates Without CDP**

Modify the error handling in `FinishRevocation()` to treat `X509_V_ERR_UNABLE_TO_GET_CRL` as a non-error when the certificate doesn't have a CDP extension.

**Location**: `OpenSslX509ChainProcessor.cs` lines 435-633

#### **Option 4: Use Verification Callback to Skip CRL Check**

Modify the verification callback to ignore `X509_V_ERR_UNABLE_TO_GET_CRL` errors when no CDP is present.

**Location**: `OpenSslX509ChainProcessor.cs` lines 1359-1403 (the `VerifyCallback` method in `WorkingChain`)

### Recommended Approach

**Option 1** is the cleanest and most semantically correct approach. It aligns with the principle that if no CRLs are available (because certificates don't advertise where to get them), then CRL checking shouldn't be enforced by OpenSSL. The runtime would still attempt OCSP checking as a fallback, which is already implemented.

This would make the behavior match: "If certificates don't tell us where to get CRLs, don't require CRLs for verification to succeed."

Would you like me to show you the specific code changes needed to implement Option 1, or would you prefer to explore one of the other options?

---

**User**

Show me the changes needed to implement option 1.

---

**Cursor**

I'll show you the specific code changes needed to implement Option 1, which conditionally sets the CRL check flag only when at least one CRL was successfully added to the store.

Now let me show you the complete set of changes needed:

## Changes Required for Option 1

Here are the specific code changes needed to implement Option 1:

### Change 1: Modify `OpenSslCrlCache.AddCrlForCertificate()` to return bool

**File**: `src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslCrlCache.cs`

Change the method signature from `void` to `bool` and track whether a CRL was successfully added:

```28:73:c:\Source\Repos\runtime\src\libraries\System.Security.Cryptography\src\System\Security\Cryptography\X509Certificates\OpenSslCrlCache.cs
public static void AddCrlForCertificate(
SafeX509Handle cert,
SafeX509StoreHandle store,
X509RevocationMode revocationMode,
DateTime verificationTime,
TimeSpan downloadTimeout)
{
// In Offline mode, accept any cached CRL we have.
// "CRL is Expired" is a better match for Offline than "Could not find CRL"
if (revocationMode != X509RevocationMode.Online)
{
verificationTime = DateTime.MinValue;
}

string? url = GetCdpUrl(cert);

if (url == null)
{
return;
}

string crlFileName = GetCrlFileName(cert, url);

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.CrlIdentifiersDetermined(cert, url, crlFileName);
}

if (AddCachedCrl(crlFileName, store, verificationTime))
{
return;
}

// Don't do any work if we're prohibited from fetching new CRLs
if (revocationMode != X509RevocationMode.Online)
{
if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.CrlCheckOffline();
}

return;
}

DownloadAndAddCrl(url, crlFileName, store, downloadTimeout);
}
```

**Modified version:**

```csharp
public static bool AddCrlForCertificate(
SafeX509Handle cert,
SafeX509StoreHandle store,
X509RevocationMode revocationMode,
DateTime verificationTime,
TimeSpan downloadTimeout)
{
// In Offline mode, accept any cached CRL we have.
// "CRL is Expired" is a better match for Offline than "Could not find CRL"
if (revocationMode != X509RevocationMode.Online)
{
verificationTime = DateTime.MinValue;
}

string? url = GetCdpUrl(cert);

if (url == null)
{
return false;
}

string crlFileName = GetCrlFileName(cert, url);

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.CrlIdentifiersDetermined(cert, url, crlFileName);
}

if (AddCachedCrl(crlFileName, store, verificationTime))
{
return true;
}

// Don't do any work if we're prohibited from fetching new CRLs
if (revocationMode != X509RevocationMode.Online)
{
if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.CrlCheckOffline();
}

return false;
}

return DownloadAndAddCrl(url, crlFileName, store, downloadTimeout);
}
```

### Change 2: Modify `DownloadAndAddCrl()` to return bool

**File**: Same file (`OpenSslCrlCache.cs`)

```203:260:c:\Source\Repos\runtime\src\libraries\System.Security.Cryptography\src\System\Security\Cryptography\X509Certificates\OpenSslCrlCache.cs
private static void DownloadAndAddCrl(
string url,
string crlFileName,
SafeX509StoreHandle store,
TimeSpan downloadTimeout)
{
// X509_STORE_add_crl will increase the refcount on the CRL object, so we should still
// dispose our copy.
using (SafeX509CrlHandle? crl = OpenSslCertificateAssetDownloader.DownloadCrl(url, downloadTimeout))
{
// null is a valid return (e.g. no remainingDownloadTime)
if (crl != null && !crl.IsInvalid)
{
if (!Interop.Crypto.X509StoreAddCrl(store, crl))
{
// Ignore error "cert already in store", throw on anything else. In any case the error queue will be cleared.
if (X509_R_CERT_ALREADY_IN_HASH_TABLE == Interop.Crypto.ErrPeekLastError())
{
Interop.Crypto.ErrClearError();
}
else
{
throw Interop.Crypto.CreateOpenSslCryptographicException();
}
}

// Saving the CRL to the disk is just a performance optimization for later requests to not
// need to use the network again, so failure to save shouldn't throw an exception or mark
// the chain as invalid.
try
{
string crlFile = GetCachedCrlPath(crlFileName, mkDir: true);

using (SafeBioHandle bio = Interop.Crypto.BioNewFile(crlFile, "wb"))
{
if (bio.IsInvalid || Interop.Crypto.PemWriteBioX509Crl(bio, crl) == 0)
{
// No bio, or write failed

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.CrlCacheWriteFailed(crlFile);
}

Interop.Crypto.ErrClearError();
}
}
}
catch (UnauthorizedAccessException) { }
catch (IOException) { }

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.CrlCacheWriteSucceeded();
}
}
}
}
```

**Modified version:**

```csharp
private static bool DownloadAndAddCrl(
string url,
string crlFileName,
SafeX509StoreHandle store,
TimeSpan downloadTimeout)
{
// X509_STORE_add_crl will increase the refcount on the CRL object, so we should still
// dispose our copy.
using (SafeX509CrlHandle? crl = OpenSslCertificateAssetDownloader.DownloadCrl(url, downloadTimeout))
{
// null is a valid return (e.g. no remainingDownloadTime)
if (crl != null && !crl.IsInvalid)
{
if (!Interop.Crypto.X509StoreAddCrl(store, crl))
{
// Ignore error "cert already in store", throw on anything else. In any case the error queue will be cleared.
if (X509_R_CERT_ALREADY_IN_HASH_TABLE == Interop.Crypto.ErrPeekLastError())
{
Interop.Crypto.ErrClearError();
}
else
{
throw Interop.Crypto.CreateOpenSslCryptographicException();
}
}

// Saving the CRL to the disk is just a performance optimization for later requests to not
// need to use the network again, so failure to save shouldn't throw an exception or mark
// the chain as invalid.
try
{
string crlFile = GetCachedCrlPath(crlFileName, mkDir: true);

using (SafeBioHandle bio = Interop.Crypto.BioNewFile(crlFile, "wb"))
{
if (bio.IsInvalid || Interop.Crypto.PemWriteBioX509Crl(bio, crl) == 0)
{
// No bio, or write failed

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.CrlCacheWriteFailed(crlFile);
}

Interop.Crypto.ErrClearError();
}
}
}
catch (UnauthorizedAccessException) { }
catch (IOException) { }

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.CrlCacheWriteSucceeded();
}

return true;
}
}

return false;
}
```

### Change 3: Modify `ProcessRevocation()` to track CRL additions

**File**: `src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509ChainProcessor.cs`

```354:433:c:\Source\Repos\runtime\src\libraries\System.Security.Cryptography\src\System\Security\Cryptography\X509Certificates\OpenSslX509ChainProcessor.cs
internal void ProcessRevocation(
X509RevocationMode revocationMode,
X509RevocationFlag revocationFlag)
{
int chainSize;
int revocationSize;

using (SafeX509StackHandle chainStack = Interop.Crypto.X509StoreCtxGetChain(_storeCtx))
{
chainSize = Interop.Crypto.GetX509StackFieldCount(chainStack);

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.RevocationCheckStart(revocationMode, revocationFlag, chainSize);
}

switch (revocationFlag)
{
case X509RevocationFlag.EndCertificateOnly:
revocationSize = 1;
break;
case X509RevocationFlag.ExcludeRoot:
revocationSize = chainSize - 1;
break;
default:
Debug.Assert(revocationFlag == X509RevocationFlag.EntireChain);
revocationSize = chainSize;
break;
}

if (revocationMode != X509RevocationMode.NoCheck)
{
for (int i = 0; i < revocationSize; i++)
{
if (i == 0 && Interop.Crypto.X509ChainHasStapledOcsp(_storeCtx))
{
if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.StapledOcspPresent();
}
}
else
{
using (SafeX509Handle cert =
Interop.Crypto.X509UpRef(Interop.Crypto.GetX509StackField(chainStack, i)))
{
OpenSslCrlCache.AddCrlForCertificate(
cert,
_store,
revocationMode,
_verificationTime,
_downloadTimeout);
}
}
}
}

Interop.Crypto.X509StoreSetRevocationFlag(_store, revocationFlag);
Interop.Crypto.X509StoreCtxRebuildChain(_storeCtx);
}

Interop.Crypto.X509VerifyStatusCode errorCode = Interop.Crypto.X509StoreCtxGetError(_storeCtx);

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.CrlChainFinished(errorCode);
}

// If anything is wrong, move see if we need to try OCSP,
// or clearing an unwanted root revocation flag.
if (errorCode != Interop.Crypto.X509VerifyStatusCode.X509_V_OK)
{
FinishRevocation(revocationMode, revocationFlag, chainSize);
}

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.RevocationCheckStop();
}
}
```

**Modified version:**

```csharp
internal void ProcessRevocation(
X509RevocationMode revocationMode,
X509RevocationFlag revocationFlag)
{
int chainSize;
int revocationSize;
bool anyCrlAdded = false;

using (SafeX509StackHandle chainStack = Interop.Crypto.X509StoreCtxGetChain(_storeCtx))
{
chainSize = Interop.Crypto.GetX509StackFieldCount(chainStack);

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.RevocationCheckStart(revocationMode, revocationFlag, chainSize);
}

switch (revocationFlag)
{
case X509RevocationFlag.EndCertificateOnly:
revocationSize = 1;
break;
case X509RevocationFlag.ExcludeRoot:
revocationSize = chainSize - 1;
break;
default:
Debug.Assert(revocationFlag == X509RevocationFlag.EntireChain);
revocationSize = chainSize;
break;
}

if (revocationMode != X509RevocationMode.NoCheck)
{
for (int i = 0; i < revocationSize; i++)
{
if (i == 0 && Interop.Crypto.X509ChainHasStapledOcsp(_storeCtx))
{
if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.StapledOcspPresent();
}
}
else
{
using (SafeX509Handle cert =
Interop.Crypto.X509UpRef(Interop.Crypto.GetX509StackField(chainStack, i)))
{
bool crlAdded = OpenSslCrlCache.AddCrlForCertificate(
cert,
_store,
revocationMode,
_verificationTime,
_downloadTimeout);

anyCrlAdded |= crlAdded;
}
}
}
}

// Only set the CRL check flag if at least one CRL was successfully added to the store.
// This matches OpenSSL CLI behavior where verification succeeds when certificates
// don't have CRL Distribution Points and no -crl_check flag is specified.
if (anyCrlAdded)
{
Interop.Crypto.X509StoreSetRevocationFlag(_store, revocationFlag);
}

Interop.Crypto.X509StoreCtxRebuildChain(_storeCtx);
}

Interop.Crypto.X509VerifyStatusCode errorCode = Interop.Crypto.X509StoreCtxGetError(_storeCtx);

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.CrlChainFinished(errorCode);
}

// If anything is wrong, move see if we need to try OCSP,
// or clearing an unwanted root revocation flag.
if (errorCode != Interop.Crypto.X509VerifyStatusCode.X509_V_OK)
{
FinishRevocation(revocationMode, revocationFlag, chainSize);
}

if (OpenSslX509ChainEventSource.Log.IsEnabled())
{
OpenSslX509ChainEventSource.Log.RevocationCheckStop();
}
}
```

### Summary of Changes

**Key modifications:**

1. **`AddCrlForCertificate()`**: Changed return type from `void` to `bool`
- Returns `false` when no CDP URL is found (line 44-47)
- Returns `true` when a cached CRL is successfully added (line 56-59)
- Returns `false` in Offline mode when no cached CRL exists (line 62-70)
- Returns the result of `DownloadAndAddCrl()` in Online mode (line 72)

2. **`DownloadAndAddCrl()`**: Changed return type from `void` to `bool`
- Returns `true` when a CRL is successfully downloaded and added (line 214-258)
- Returns `false` when download fails or CRL is invalid (line 260)

3. **`ProcessRevocation()`**: Track whether any CRLs were added
- Added `bool anyCrlAdded = false` variable (line 357)
- Capture return value from `AddCrlForCertificate()` and OR it into `anyCrlAdded` (lines 400-407)
- **Conditionally** call `X509StoreSetRevocationFlag()` only if `anyCrlAdded` is true (lines 411-417)

### Impact

With these changes:

- **When certificates have CDP extensions**: CRLs are added to the store, the flag is set, and CRL checking occurs (existing behavior preserved)
- **When certificates lack CDP extensions**: No CRLs are added, the flag is NOT set, and OpenSSL verification succeeds without CRL checking (matches OpenSSL CLI behavior)
- **OCSP checking**: Still occurs as a fallback in `FinishRevocation()` for certificates that have OCSP endpoints, providing revocation checking even without CRLs

This approach is semantically correct: "Don't require CRL checking if the certificates don't tell us where to find CRLs."

Would you like me to create a patch file with these changes, or would you like to discuss any aspects of this approach?

### Expected behavior

No `RevocationStatusUnknown`/`OfflineRevocation` errors when a certificate doesn't contain a CRL (similar to how the OpenSSL CLI works).

### Actual behavior

When using a mode other than `NoCheck`, chain verification fails when the CA certificate doesn't contain a CRL.

### Regression?

_No response_

### Known Workarounds

_No response_

### Configuration

.NET SDK: 10.0.103, runtime: 10.0.3
OS: Linux (using the OpenSSL API)
Specific to that configuration: no, the same happens on Windows

### Other information

_No response_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.