Skip to content

Commit

Permalink
Use the stapled OCSP response from TLS on Linux, when available
Browse files Browse the repository at this point in the history
Based on (non-exhaustive) testing, chain builds from a Let's Encrypt issued certificate have
the following characteristics:

* Live OCSP request required (uncached/unstapled): 577ms
* OCSP response retrieved from cache (unstapled): 183ms
* OCSP response utilized from TLS stapling (bypasses cache): 182ms

In both cached and stapled the revocation portion was about 39ms.
(The revocation mode was ExcludeRoot, the CRL pertaining to the intermediate was cached for all three measurements.)

If the OCSP response was stapled (and the math worked out OK on it) then we completely ignore the OCSP cache.
While it could potentially be useful to update the cache if the stapled response was newer, the extra I/O of doing
the "newer" test didn't feel justified at this time.
  • Loading branch information
bartonjs authored Apr 1, 2022
1 parent 6410312 commit 57da7d2
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ internal static X509VerifyStatusCode X509ChainGetCachedOcspStatus(SafeX509StoreC
return response;
}

[LibraryImport(Libraries.CryptoNative)]
private static partial int CryptoNative_X509ChainHasStapledOcsp(SafeX509StoreCtxHandle storeCtx);

internal static bool X509ChainHasStapledOcsp(SafeX509StoreCtxHandle storeCtx)
{
int resp = CryptoNative_X509ChainHasStapledOcsp(storeCtx);

if (resp == 1)
{
return true;
}

Debug.Assert(resp == 0, $"Unexpected response from X509ChainHasStapledOcsp: {resp}");
return false;
}

[LibraryImport(Libraries.CryptoNative, StringMarshalling = StringMarshalling.Utf8)]
private static partial int CryptoNative_X509ChainVerifyOcsp(
SafeX509StoreCtxHandle ctx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ internal sealed class OpenSslX509ChainEventSource : EventSource
private const int EventId_RevocationCheckStart = 45;
private const int EventId_RevocationCheckStop = 46;
private const int EventId_CrlIdentifiersDetermined = 47;
private const int EventId_StapledOcspPresent = 48;

private static string GetCertificateSubject(SafeX509Handle certHandle)
{
Expand Down Expand Up @@ -745,5 +746,17 @@ private void CrlIdentifiersDetermined(string subjectName, string crlDistribution
{
WriteEvent(EventId_CrlIdentifiersDetermined, subjectName, crlDistributionPoint, cacheFileName);
}

[Event(
EventId_StapledOcspPresent,
Level = EventLevel.Verbose,
Message = "The target certificate has a stapled OCSP request, skipping the CRL check.")]
internal void StapledOcspPresent()
{
if (IsEnabled())
{
WriteEvent(EventId_StapledOcspPresent);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -388,15 +388,25 @@ internal void ProcessRevocation(
{
for (int i = 0; i < revocationSize; i++)
{
using (SafeX509Handle cert =
Interop.Crypto.X509UpRef(Interop.Crypto.GetX509StackField(chainStack, i)))
if (i == 0 && Interop.Crypto.X509ChainHasStapledOcsp(_storeCtx))
{
OpenSslCrlCache.AddCrlForCertificate(
cert,
_store,
revocationMode,
_verificationTime,
_downloadTimeout);
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);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ static const Entry s_cryptoNative[] =
DllImportEntry(CryptoNative_UpRefEvpPkey)
DllImportEntry(CryptoNative_X509ChainBuildOcspRequest)
DllImportEntry(CryptoNative_X509ChainGetCachedOcspStatus)
DllImportEntry(CryptoNative_X509ChainHasStapledOcsp)
DllImportEntry(CryptoNative_X509ChainNew)
DllImportEntry(CryptoNative_X509ChainVerifyOcsp)
DllImportEntry(CryptoNative_X509CheckPurpose)
Expand Down
97 changes: 93 additions & 4 deletions src/native/libs/System.Security.Cryptography.Native/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "pal_types.h"
#include "pal_utilities.h"
#include "pal_safecrt.h"
#include "pal_x509.h"
#include "openssl.h"

#ifdef FEATURE_DISTRO_AGNOSTIC_SSL
Expand All @@ -19,6 +20,12 @@
#include <time.h>
#include <unistd.h>

#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_1_1_0_RTM
c_static_assert(CRYPTO_EX_INDEX_X509 == 3);
#else
c_static_assert(CRYPTO_EX_INDEX_X509 == 10);
#endif

// See X509NameType.SimpleName
#define NAME_TYPE_SIMPLE 0
// See X509NameType.EmailName
Expand Down Expand Up @@ -1172,6 +1179,70 @@ int64_t CryptoNative_OpenSslVersionNumber()
return (int64_t)OpenSSL_version_num();
}

static void ExDataFree(
void* parent,
void* ptr,
CRYPTO_EX_DATA* ad,
int idx,
long argl,
void* argp)
{
(void)parent;
(void)ad;
(void)idx;
(void)argl;
(void)argp;

if (ptr != NULL)
{
if (idx == g_x509_ocsp_index)
{
OCSP_RESPONSE_free((OCSP_RESPONSE*)ptr);
}
}
}

// In the OpenSSL 1.0.2 headers, the `from` argument is not const (became const in 1.1.0)
// In the OpenSSL 3 headers, `from_d` changed from (void*) to (void**).
static int ExDataDup(
CRYPTO_EX_DATA* to,
#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_1_1_0_RTM
const CRYPTO_EX_DATA* from,
#else
CRYPTO_EX_DATA* from,
#endif
#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_3_0_RTM
void** from_d,
#else
void* from_d,
#endif
int idx,
long argl,
void* argp)
{
(void)to;
(void)from;
(void)idx;
(void)argl;
(void)argp;

// From the docs (https://www.openssl.org/docs/man1.1.1/man3/CRYPTO_get_ex_new_index.html):
// "The from_d parameter needs to be cast to a void **pptr as the API has currently the wrong signature ..."
void** pptr = (void**)from_d;

if (pptr != NULL)
{
if (idx == g_x509_ocsp_index)
{
*pptr = NULL;
}
}

// If the dup_func() returns 0 the whole CRYPTO_dup_ex_data() will fail.
// So, return 1 unless we returned 0 already.
return 1;
}

void CryptoNative_RegisterLegacyAlgorithms()
{
#ifdef NEED_OPENSSL_3_0
Expand Down Expand Up @@ -1304,6 +1375,9 @@ static int32_t EnsureOpenSsl10Initialized()
// Ensure that the error message table is loaded.
ERR_load_crypto_strings();

// In OpenSSL 1.0.2-, CRYPTO_EX_INDEX_X509 is 10.
g_x509_ocsp_index = CRYPTO_get_ex_new_index(10, 0, NULL, NULL, ExDataDup, ExDataFree);

done:
if (ret != 0)
{
Expand Down Expand Up @@ -1368,6 +1442,9 @@ static int32_t EnsureOpenSsl11Initialized()
// atexit handler, so we will indicate that we're in the shutdown state
// and stop asking problematic questions from other threads.
atexit(HandleShutdown);

// In OpenSSL 1.1.0+, CRYPTO_EX_INDEX_X509 is 3.
g_x509_ocsp_index = CRYPTO_get_ex_new_index(3, 0, NULL, NULL, ExDataDup, ExDataFree);
return 0;
}

Expand All @@ -1385,9 +1462,12 @@ int32_t CryptoNative_OpenSslAvailable()
}

static int32_t g_initStatus = 1;
int g_x509_ocsp_index = -1;

static int32_t EnsureOpenSslInitializedCore()
{
int ret = 0;

// If portable then decide which OpenSSL we are, and call the right one.
// If 1.0, call the 1.0 one.
// Otherwise call the 1.1 one.
Expand All @@ -1396,17 +1476,26 @@ static int32_t EnsureOpenSslInitializedCore()

if (API_EXISTS(SSL_state))
{
return EnsureOpenSsl10Initialized();
ret = EnsureOpenSsl10Initialized();
}
else
{
return EnsureOpenSsl11Initialized();
ret = EnsureOpenSsl11Initialized();
}
#elif OPENSSL_VERSION_NUMBER < OPENSSL_VERSION_1_1_0_RTM
return EnsureOpenSsl10Initialized();
ret = EnsureOpenSsl10Initialized();
#else
return EnsureOpenSsl11Initialized();
ret = EnsureOpenSsl11Initialized();
#endif

if (ret == 0)
{
// On OpenSSL 1.0.2 our expected index is 0.
// On OpenSSL 1.1.0+ 0 is a reserved value and we expect 1.
assert(g_x509_ocsp_index != -1);
}

return ret;
}

static void EnsureOpenSslInitializedOnce()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ const EVP_CIPHER* EVP_chacha20_poly1305(void);
REQUIRED_FUNCTION(BN_num_bits) \
REQUIRED_FUNCTION(BN_set_word) \
LEGACY_FUNCTION(CRYPTO_add_lock) \
REQUIRED_FUNCTION(CRYPTO_get_ex_new_index) \
LEGACY_FUNCTION(CRYPTO_num_locks) \
LEGACY_FUNCTION(CRYPTO_set_locking_callback) \
REQUIRED_FUNCTION(d2i_ASN1_BIT_STRING) \
Expand Down Expand Up @@ -548,6 +549,7 @@ const EVP_CIPHER* EVP_chacha20_poly1305(void);
REQUIRED_FUNCTION(X509_get_default_cert_dir_env) \
REQUIRED_FUNCTION(X509_get_default_cert_file) \
REQUIRED_FUNCTION(X509_get_default_cert_file_env) \
REQUIRED_FUNCTION(X509_get_ex_data) \
REQUIRED_FUNCTION(X509_get_ext) \
REQUIRED_FUNCTION(X509_get_ext_by_NID) \
REQUIRED_FUNCTION(X509_get_ext_count) \
Expand Down Expand Up @@ -575,6 +577,7 @@ const EVP_CIPHER* EVP_chacha20_poly1305(void);
REQUIRED_FUNCTION(X509_new) \
REQUIRED_FUNCTION(X509_PUBKEY_get) \
FALLBACK_FUNCTION(X509_PUBKEY_get0_param) \
REQUIRED_FUNCTION(X509_set_ex_data) \
REQUIRED_FUNCTION(X509_set_pubkey) \
REQUIRED_FUNCTION(X509_sign) \
REQUIRED_FUNCTION(X509_subject_name_hash) \
Expand Down Expand Up @@ -666,6 +669,7 @@ FOR_ALL_OPENSSL_FUNCTIONS
#define BN_num_bits BN_num_bits_ptr
#define BN_set_word BN_set_word_ptr
#define CRYPTO_add_lock CRYPTO_add_lock_ptr
#define CRYPTO_get_ex_new_index CRYPTO_get_ex_new_index_ptr
#define CRYPTO_num_locks CRYPTO_num_locks_ptr
#define CRYPTO_set_locking_callback CRYPTO_set_locking_callback_ptr
#define d2i_ASN1_BIT_STRING d2i_ASN1_BIT_STRING_ptr
Expand Down Expand Up @@ -1028,6 +1032,7 @@ FOR_ALL_OPENSSL_FUNCTIONS
#define X509_get_default_cert_dir_env X509_get_default_cert_dir_env_ptr
#define X509_get_default_cert_file X509_get_default_cert_file_ptr
#define X509_get_default_cert_file_env X509_get_default_cert_file_env_ptr
#define X509_get_ex_data X509_get_ex_data_ptr
#define X509_get_ext X509_get_ext_ptr
#define X509_get_ext_by_NID X509_get_ext_by_NID_ptr
#define X509_get_ext_count X509_get_ext_count_ptr
Expand All @@ -1049,6 +1054,7 @@ FOR_ALL_OPENSSL_FUNCTIONS
#define X509_new X509_new_ptr
#define X509_PUBKEY_get0_param X509_PUBKEY_get0_param_ptr
#define X509_PUBKEY_get X509_PUBKEY_get_ptr
#define X509_set_ex_data X509_set_ex_data_ptr
#define X509_set_pubkey X509_set_pubkey_ptr
#define X509_subject_name_hash X509_subject_name_hash_ptr
#define X509_sign X509_sign_ptr
Expand Down
28 changes: 27 additions & 1 deletion src/native/libs/System.Security.Cryptography.Native/pal_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ c_static_assert(PAL_SSL_ERROR_WANT_READ == SSL_ERROR_WANT_READ);
c_static_assert(PAL_SSL_ERROR_WANT_WRITE == SSL_ERROR_WANT_WRITE);
c_static_assert(PAL_SSL_ERROR_SYSCALL == SSL_ERROR_SYSCALL);
c_static_assert(PAL_SSL_ERROR_ZERO_RETURN == SSL_ERROR_ZERO_RETURN);
c_static_assert(SSL_CTRL_SET_TLSEXT_STATUS_REQ_TYPE == 65);
c_static_assert(TLSEXT_STATUSTYPE_ocsp == 1);

#define DOTNET_DEFAULT_CIPHERSTRING \
"ECDHE-ECDSA-AES256-GCM-SHA384:" \
Expand Down Expand Up @@ -234,6 +236,12 @@ SSL_CTX* CryptoNative_SslCtxCreate(const SSL_METHOD* method)
return NULL;
}
}

// Opportunistically request the server present a stapled OCSP response.
if (SSL_CTX_ctrl(ctx, SSL_CTRL_SET_TLSEXT_STATUS_REQ_TYPE, TLSEXT_STATUSTYPE_ocsp, NULL) != 1)
{
ERR_clear_error();
}
}

return ctx;
Expand Down Expand Up @@ -559,8 +567,26 @@ int32_t CryptoNative_IsSslStateOK(SSL* ssl)

X509* CryptoNative_SslGetPeerCertificate(SSL* ssl)
{
const uint8_t* data = NULL;
long len = SSL_get_tlsext_status_ocsp_resp(ssl, &data);
X509* cert = SSL_get1_peer_certificate(ssl);

if (len > 0 && cert != NULL)
{
OCSP_RESPONSE* ocspResp = d2i_OCSP_RESPONSE(NULL, &data, len);

if (ocspResp == NULL)
{
ERR_clear_error();
}
else
{
X509_set_ex_data(cert, g_x509_ocsp_index, ocspResp);
}
}

// No error queue impact.
return SSL_get1_peer_certificate(ssl);
return cert;
}

X509Stack* CryptoNative_SslGetPeerCertChain(SSL* ssl)
Expand Down
Loading

0 comments on commit 57da7d2

Please sign in to comment.