changeset 7654:b56f725dd4bb

OCSP: certificate status cache. When enabled, certificate status is stored in cache and is used to validate the certificate in future requests. New directive ssl_ocsp_cache is added to configure the cache.
author Roman Arutyunyan <arut@nginx.com>
date Fri, 22 May 2020 17:25:27 +0300
parents 8409f9df6219
children bd4d1b9db0ee
files src/event/ngx_event_openssl.h src/event/ngx_event_openssl_stapling.c src/http/modules/ngx_http_ssl_module.c src/http/modules/ngx_http_ssl_module.h
diffstat 4 files changed, 401 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/src/event/ngx_event_openssl.h
+++ b/src/event/ngx_event_openssl.h
@@ -187,12 +187,13 @@ ngx_int_t ngx_ssl_stapling(ngx_conf_t *c
 ngx_int_t ngx_ssl_stapling_resolver(ngx_conf_t *cf, ngx_ssl_t *ssl,
     ngx_resolver_t *resolver, ngx_msec_t resolver_timeout);
 ngx_int_t ngx_ssl_ocsp(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *responder,
-    ngx_uint_t depth);
+    ngx_uint_t depth, ngx_shm_zone_t *shm_zone);
 ngx_int_t ngx_ssl_ocsp_resolver(ngx_conf_t *cf, ngx_ssl_t *ssl,
     ngx_resolver_t *resolver, ngx_msec_t resolver_timeout);
 ngx_int_t ngx_ssl_ocsp_validate(ngx_connection_t *c);
 ngx_int_t ngx_ssl_ocsp_get_status(ngx_connection_t *c, const char **s);
 void ngx_ssl_ocsp_cleanup(ngx_connection_t *c);
+ngx_int_t ngx_ssl_ocsp_cache_init(ngx_shm_zone_t *shm_zone, void *data);
 RSA *ngx_ssl_rsa512_key_callback(ngx_ssl_conn_t *ssl_conn, int is_export,
     int key_length);
 ngx_array_t *ngx_ssl_read_password_file(ngx_conf_t *cf, ngx_str_t *file);
--- a/src/event/ngx_event_openssl_stapling.c
+++ b/src/event/ngx_event_openssl_stapling.c
@@ -52,11 +52,28 @@ typedef struct {
     in_port_t                    port;
     ngx_uint_t                   depth;
 
+    ngx_shm_zone_t              *shm_zone;
+
     ngx_resolver_t              *resolver;
     ngx_msec_t                   resolver_timeout;
 } ngx_ssl_ocsp_conf_t;
 
 
+typedef struct {
+    ngx_rbtree_t                 rbtree;
+    ngx_rbtree_node_t            sentinel;
+    ngx_queue_t                  expire_queue;
+} ngx_ssl_ocsp_cache_t;
+
+
+typedef struct {
+    ngx_str_node_t               node;
+    ngx_queue_t                  queue;
+    int                          status;
+    time_t                       valid;
+} ngx_ssl_ocsp_cache_node_t;
+
+
 typedef struct ngx_ssl_ocsp_ctx_s  ngx_ssl_ocsp_ctx_t;
 
 
@@ -100,10 +117,13 @@ struct ngx_ssl_ocsp_ctx_s {
     void                       (*handler)(ngx_ssl_ocsp_ctx_t *ctx);
     void                        *data;
 
+    ngx_str_t                    key;
     ngx_buf_t                   *request;
     ngx_buf_t                   *response;
     ngx_peer_connection_t        peer;
 
+    ngx_shm_zone_t              *shm_zone;
+
     ngx_int_t                  (*process)(ngx_ssl_ocsp_ctx_t *ctx);
 
     ngx_uint_t                   state;
@@ -164,6 +184,10 @@ static ngx_int_t ngx_ssl_ocsp_parse_head
 static ngx_int_t ngx_ssl_ocsp_process_body(ngx_ssl_ocsp_ctx_t *ctx);
 static ngx_int_t ngx_ssl_ocsp_verify(ngx_ssl_ocsp_ctx_t *ctx);
 
+static ngx_int_t ngx_ssl_ocsp_cache_lookup(ngx_ssl_ocsp_ctx_t *ctx);
+static ngx_int_t ngx_ssl_ocsp_cache_store(ngx_ssl_ocsp_ctx_t *ctx);
+static ngx_int_t ngx_ssl_ocsp_create_key(ngx_ssl_ocsp_ctx_t *ctx);
+
 static u_char *ngx_ssl_ocsp_log_error(ngx_log_t *log, u_char *buf, size_t len);
 
 
@@ -743,7 +767,7 @@ ngx_ssl_stapling_cleanup(void *data)
 
 ngx_int_t
 ngx_ssl_ocsp(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *responder,
-    ngx_uint_t depth)
+    ngx_uint_t depth, ngx_shm_zone_t *shm_zone)
 {
     ngx_url_t             u;
     ngx_ssl_ocsp_conf_t  *ocf;
@@ -754,6 +778,7 @@ ngx_ssl_ocsp(ngx_conf_t *cf, ngx_ssl_t *
     }
 
     ocf->depth = depth;
+    ocf->shm_zone = shm_zone;
 
     if (responder->len) {
         ngx_memzero(&u, sizeof(ngx_url_t));
@@ -939,6 +964,7 @@ ngx_ssl_ocsp_validate(ngx_connection_t *
 static void
 ngx_ssl_ocsp_validate_next(ngx_connection_t *c)
 {
+    ngx_int_t             rc;
     ngx_uint_t            n;
     ngx_ssl_ocsp_t       *ocsp;
     ngx_ssl_ocsp_ctx_t   *ctx;
@@ -978,6 +1004,8 @@ ngx_ssl_ocsp_validate_next(ngx_connectio
         ctx->handler = ngx_ssl_ocsp_handler;
         ctx->data = c;
 
+        ctx->shm_zone = ocf->shm_zone;
+
         ctx->addrs = ocf->addrs;
         ctx->naddrs = ocf->naddrs;
         ctx->host = ocf->host;
@@ -994,7 +1022,28 @@ ngx_ssl_ocsp_validate_next(ngx_connectio
 
         ocsp->ncert++;
 
-        break;
+        rc = ngx_ssl_ocsp_cache_lookup(ctx);
+
+        if (rc == NGX_ERROR) {
+            goto failed;
+        }
+
+        if (rc == NGX_DECLINED) {
+            break;
+        }
+
+        /* rc == NGX_OK */
+
+        if (ctx->status != V_OCSP_CERTSTATUS_GOOD) {
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, ctx->log, 0,
+                           "ssl ocsp cached status \"%s\"",
+                           OCSP_cert_status_str(ctx->status));
+            ocsp->cert_status = ctx->status;
+            goto done;
+        }
+
+        ocsp->ctx = NULL;
+        ngx_ssl_ocsp_done(ctx);
     }
 
     ngx_ssl_ocsp_request(ctx);
@@ -1029,6 +1078,13 @@ ngx_ssl_ocsp_handler(ngx_ssl_ocsp_ctx_t 
         goto done;
     }
 
+    rc = ngx_ssl_ocsp_cache_store(ctx);
+    if (rc != NGX_OK) {
+        ocsp->status = rc;
+        ngx_ssl_ocsp_done(ctx);
+        goto done;
+    }
+
     if (ctx->status != V_OCSP_CERTSTATUS_GOOD) {
         ocsp->cert_status = ctx->status;
         ocsp->status = NGX_OK;
@@ -2374,6 +2430,245 @@ error:
 }
 
 
+ngx_int_t
+ngx_ssl_ocsp_cache_init(ngx_shm_zone_t *shm_zone, void *data)
+{
+    size_t                 len;
+    ngx_slab_pool_t       *shpool;
+    ngx_ssl_ocsp_cache_t  *cache;
+
+    if (data) {
+        shm_zone->data = data;
+        return NGX_OK;
+    }
+
+    shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
+
+    if (shm_zone->shm.exists) {
+        shm_zone->data = shpool->data;
+        return NGX_OK;
+    }
+
+    cache = ngx_slab_alloc(shpool, sizeof(ngx_ssl_ocsp_cache_t));
+    if (cache == NULL) {
+        return NGX_ERROR;
+    }
+
+    shpool->data = cache;
+    shm_zone->data = cache;
+
+    ngx_rbtree_init(&cache->rbtree, &cache->sentinel,
+                    ngx_str_rbtree_insert_value);
+
+    ngx_queue_init(&cache->expire_queue);
+
+    len = sizeof(" in OCSP cache \"\"") + shm_zone->shm.name.len;
+
+    shpool->log_ctx = ngx_slab_alloc(shpool, len);
+    if (shpool->log_ctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    ngx_sprintf(shpool->log_ctx, " in OCSP cache \"%V\"%Z",
+                &shm_zone->shm.name);
+
+    shpool->log_nomem = 0;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_ssl_ocsp_cache_lookup(ngx_ssl_ocsp_ctx_t *ctx)
+{
+    uint32_t                    hash;
+    ngx_shm_zone_t             *shm_zone;
+    ngx_slab_pool_t            *shpool;
+    ngx_ssl_ocsp_cache_t       *cache;
+    ngx_ssl_ocsp_cache_node_t  *node;
+
+    shm_zone = ctx->shm_zone;
+
+    if (shm_zone == NULL) {
+        return NGX_DECLINED;
+    }
+
+    if (ngx_ssl_ocsp_create_key(ctx) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ctx->log, 0, "ssl ocsp cache lookup");
+
+    cache = shm_zone->data;
+    shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
+    hash = ngx_hash_key(ctx->key.data, ctx->key.len);
+
+    ngx_shmtx_lock(&shpool->mutex);
+
+    node = (ngx_ssl_ocsp_cache_node_t *)
+               ngx_str_rbtree_lookup(&cache->rbtree, &ctx->key, hash);
+
+    if (node) {
+        if (node->valid > ngx_time()) {
+            ctx->status = node->status;
+            ngx_shmtx_unlock(&shpool->mutex);
+
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, ctx->log, 0,
+                           "ssl ocsp cache hit, %s",
+                           OCSP_cert_status_str(ctx->status));
+
+            return NGX_OK;
+        }
+
+        ngx_queue_remove(&node->queue);
+        ngx_rbtree_delete(&cache->rbtree, &node->node.node);
+        ngx_slab_free_locked(shpool, node);
+
+        ngx_shmtx_unlock(&shpool->mutex);
+
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ctx->log, 0,
+                       "ssl ocsp cache expired");
+
+        return NGX_DECLINED;
+    }
+
+    ngx_shmtx_unlock(&shpool->mutex);
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ctx->log, 0, "ssl ocsp cache miss");
+
+    return NGX_DECLINED;
+}
+
+
+static ngx_int_t
+ngx_ssl_ocsp_cache_store(ngx_ssl_ocsp_ctx_t *ctx)
+{
+    time_t                      now, valid;
+    uint32_t                    hash;
+    ngx_queue_t                *q;
+    ngx_shm_zone_t             *shm_zone;
+    ngx_slab_pool_t            *shpool;
+    ngx_ssl_ocsp_cache_t       *cache;
+    ngx_ssl_ocsp_cache_node_t  *node;
+
+    shm_zone = ctx->shm_zone;
+
+    if (shm_zone == NULL) {
+        return NGX_OK;
+    }
+
+    valid = ctx->valid;
+
+    now = ngx_time();
+
+    if (valid < now) {
+        return NGX_OK;
+    }
+
+    if (valid == NGX_MAX_TIME_T_VALUE) {
+        valid = now + 3600;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, ctx->log, 0,
+                   "ssl ocsp cache store, valid:%T", valid - now);
+
+    cache = shm_zone->data;
+    shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
+    hash = ngx_hash_key(ctx->key.data, ctx->key.len);
+
+    ngx_shmtx_lock(&shpool->mutex);
+
+    node = ngx_slab_calloc_locked(shpool,
+                             sizeof(ngx_ssl_ocsp_cache_node_t) + ctx->key.len);
+    if (node == NULL) {
+
+        if (!ngx_queue_empty(&cache->expire_queue)) {
+            q = ngx_queue_last(&cache->expire_queue);
+            node = ngx_queue_data(q, ngx_ssl_ocsp_cache_node_t, queue);
+
+            ngx_rbtree_delete(&cache->rbtree, &node->node.node);
+            ngx_queue_remove(q);
+            ngx_slab_free_locked(shpool, node);
+
+            node = ngx_slab_alloc_locked(shpool,
+                             sizeof(ngx_ssl_ocsp_cache_node_t) + ctx->key.len);
+        }
+
+        if (node == NULL) {
+            ngx_shmtx_unlock(&shpool->mutex);
+            ngx_log_error(NGX_LOG_ALERT, ctx->log, 0,
+                          "could not allocate new entry%s", shpool->log_ctx);
+            return NGX_ERROR;
+        }
+    }
+
+    node->node.str.len = ctx->key.len;
+    node->node.str.data = (u_char *) node + sizeof(ngx_ssl_ocsp_cache_node_t);
+    ngx_memcpy(node->node.str.data, ctx->key.data, ctx->key.len);
+    node->node.node.key = hash;
+    node->status = ctx->status;
+    node->valid = valid;
+
+    ngx_rbtree_insert(&cache->rbtree, &node->node.node);
+    ngx_queue_insert_head(&cache->expire_queue, &node->queue);
+
+    ngx_shmtx_unlock(&shpool->mutex);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_ssl_ocsp_create_key(ngx_ssl_ocsp_ctx_t *ctx)
+{
+    u_char        *p;
+    X509_NAME     *name;
+    ASN1_INTEGER  *serial;
+
+    p = ngx_pnalloc(ctx->pool, 60);
+    if (p == NULL) {
+        return NGX_ERROR;
+    }
+
+    ctx->key.data = p;
+    ctx->key.len = 60;
+
+    name = X509_get_subject_name(ctx->issuer);
+    if (X509_NAME_digest(name, EVP_sha1(), p, NULL) == 0) {
+        return NGX_ERROR;
+    }
+
+    p += 20;
+
+    if (X509_pubkey_digest(ctx->issuer, EVP_sha1(), p, NULL) == 0) {
+        return NGX_ERROR;
+    }
+
+    p += 20;
+
+    serial = X509_get_serialNumber(ctx->cert);
+    if (serial->length > 20) {
+        return NGX_ERROR;
+    }
+
+    p = ngx_cpymem(p, serial->data, serial->length);
+    ngx_memzero(p, 20 - serial->length);
+
+#if (NGX_DEBUG)
+    {
+        u_char  buf[120];
+
+        ngx_hex_dump(buf, ctx->key.data, ctx->key.len);
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ctx->log, 0,
+                       "ssl ocsp key %*s", 120, buf);
+    }
+#endif
+
+    return NGX_OK;
+}
+
+
 static u_char *
 ngx_ssl_ocsp_log_error(ngx_log_t *log, u_char *buf, size_t len)
 {
@@ -2436,7 +2731,7 @@ ngx_ssl_stapling_resolver(ngx_conf_t *cf
 
 ngx_int_t
 ngx_ssl_ocsp(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *responder,
-    ngx_uint_t depth)
+    ngx_uint_t depth, ngx_shm_zone_t *shm_zone)
 {
     ngx_log_error(NGX_LOG_EMERG, ssl->log, 0,
                   "\"ssl_ocsp\" is not supported on this platform");
@@ -2473,4 +2768,11 @@ ngx_ssl_ocsp_cleanup(ngx_connection_t *c
 }
 
 
+ngx_int_t
+ngx_ssl_ocsp_cache_init(ngx_shm_zone_t *shm_zone, void *data)
+{
+    return NGX_OK;
+}
+
+
 #endif
--- a/src/http/modules/ngx_http_ssl_module.c
+++ b/src/http/modules/ngx_http_ssl_module.c
@@ -50,6 +50,8 @@ static char *ngx_http_ssl_password_file(
     void *conf);
 static char *ngx_http_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
+static char *ngx_http_ssl_ocsp_cache(ngx_conf_t *cf, ngx_command_t *cmd,
+    void *conf);
 
 static ngx_int_t ngx_http_ssl_init(ngx_conf_t *cf);
 
@@ -236,6 +238,13 @@ static ngx_command_t  ngx_http_ssl_comma
       offsetof(ngx_http_ssl_srv_conf_t, ocsp_responder),
       NULL },
 
+    { ngx_string("ssl_ocsp_cache"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_http_ssl_ocsp_cache,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      0,
+      NULL },
+
     { ngx_string("ssl_stapling"),
       NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
       ngx_conf_set_flag_slot,
@@ -602,6 +611,7 @@ ngx_http_ssl_create_srv_conf(ngx_conf_t 
     sscf->session_tickets = NGX_CONF_UNSET;
     sscf->session_ticket_keys = NGX_CONF_UNSET_PTR;
     sscf->ocsp = NGX_CONF_UNSET_UINT;
+    sscf->ocsp_cache_zone = NGX_CONF_UNSET_PTR;
     sscf->stapling = NGX_CONF_UNSET;
     sscf->stapling_verify = NGX_CONF_UNSET;
 
@@ -667,6 +677,8 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *
 
     ngx_conf_merge_uint_value(conf->ocsp, prev->ocsp, 0);
     ngx_conf_merge_str_value(conf->ocsp_responder, prev->ocsp_responder, "");
+    ngx_conf_merge_ptr_value(conf->ocsp_cache_zone,
+                         prev->ocsp_cache_zone, NULL);
 
     ngx_conf_merge_value(conf->stapling, prev->stapling, 0);
     ngx_conf_merge_value(conf->stapling_verify, prev->stapling_verify, 0);
@@ -838,7 +850,8 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *
             return NGX_CONF_ERROR;
         }
 
-        if (ngx_ssl_ocsp(cf, &conf->ssl, &conf->ocsp_responder, conf->ocsp)
+        if (ngx_ssl_ocsp(cf, &conf->ssl, &conf->ocsp_responder, conf->ocsp,
+                         conf->ocsp_cache_zone)
             != NGX_OK)
         {
             return NGX_CONF_ERROR;
@@ -1143,6 +1156,85 @@ invalid:
 }
 
 
+static char *
+ngx_http_ssl_ocsp_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
+{
+    ngx_http_ssl_srv_conf_t *sscf = conf;
+
+    size_t       len;
+    ngx_int_t    n;
+    ngx_str_t   *value, name, size;
+    ngx_uint_t   j;
+
+    if (sscf->ocsp_cache_zone != NGX_CONF_UNSET_PTR) {
+        return "is duplicate";
+    }
+
+    value = cf->args->elts;
+
+    if (ngx_strcmp(value[1].data, "off") == 0) {
+        sscf->ocsp_cache_zone = NULL;
+        return NGX_CONF_OK;
+    }
+
+    if (value[1].len <= sizeof("shared:") - 1
+        || ngx_strncmp(value[1].data, "shared:", sizeof("shared:") - 1) != 0)
+    {
+        goto invalid;
+    }
+
+    len = 0;
+
+    for (j = sizeof("shared:") - 1; j < value[1].len; j++) {
+        if (value[1].data[j] == ':') {
+            break;
+        }
+
+        len++;
+    }
+
+    if (len == 0) {
+        goto invalid;
+    }
+
+    name.len = len;
+    name.data = value[1].data + sizeof("shared:") - 1;
+
+    size.len = value[1].len - j - 1;
+    size.data = name.data + len + 1;
+
+    n = ngx_parse_size(&size);
+
+    if (n == NGX_ERROR) {
+        goto invalid;
+    }
+
+    if (n < (ngx_int_t) (8 * ngx_pagesize)) {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                           "OCSP cache \"%V\" is too small", &value[1]);
+
+        return NGX_CONF_ERROR;
+    }
+
+    sscf->ocsp_cache_zone = ngx_shared_memory_add(cf, &name, n,
+                                                  &ngx_http_ssl_module_ctx);
+    if (sscf->ocsp_cache_zone == NULL) {
+        return NGX_CONF_ERROR;
+    }
+
+    sscf->ocsp_cache_zone->init = ngx_ssl_ocsp_cache_init;
+
+    return NGX_CONF_OK;
+
+invalid:
+
+    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                       "invalid OCSP cache \"%V\"", &value[1]);
+
+    return NGX_CONF_ERROR;
+}
+
+
 static ngx_int_t
 ngx_http_ssl_init(ngx_conf_t *cf)
 {
--- a/src/http/modules/ngx_http_ssl_module.h
+++ b/src/http/modules/ngx_http_ssl_module.h
@@ -56,6 +56,7 @@ typedef struct {
 
     ngx_uint_t                      ocsp;
     ngx_str_t                       ocsp_responder;
+    ngx_shm_zone_t                 *ocsp_cache_zone;
 
     ngx_flag_t                      stapling;
     ngx_flag_t                      stapling_verify;