本文分析 nginx ocsp cache 的原理,用于回答相关业务的解惑
缓存时间 OCSP 的缓存时间和证书 OCSP Response 的 Next Update 时间相关.如果证书未提供 Next Update 时间,过期时间=当前时间+3600s.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // nextupdate 存在时设置为 nextupdate ,不存在时设置为 NGX_MAX_TIME_T_VALUE //ngx_ssl_ocsp_verify if (nextupdate) { ctx->valid = ngx_ssl_stapling_time(nextupdate); if (ctx->valid == (time_t) NGX_ERROR) { ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "invalid nextUpdate time in certificate status"); goto error; } } else { ctx->valid = NGX_MAX_TIME_T_VALUE; } // 在存储到缓存中时,如果时间为 NGX_MAX_TIME_T_VALUE ,则是当前时间 +3600s // ngx_ssl_ocsp_cache_store ocsp cache 存储 valid = ctx->valid; now = ngx_time(); // 过期时间小于当前,不缓存,直接返回 if (valid < now) { return NGX_OK; } // 过期时间未默认值是设置为 3600s if (valid == NGX_MAX_TIME_T_VALUE) { valid = now + 3600; }
Thisupdate 和 nextupdate 的时间由 OCSP_resp_find_status 从证书中获取, OCSP_resp_find_status 是 openssl 提供的函数.
https://github.com/openssl/openssl/blob/e3d897d3fa3b48bb835fab0665a435469beea7ae/crypto/ocsp/ocsp_cl.c#L248
single = OCSP_resp_get0(bs, i); // 负责获取证书信息 .
调用
1 2 3 4 5 6 7 8 #define sk_OCSP_SINGLERESP_value( st, i )SKM_sk_value(OCSP_SINGLERESP, (st), (i)) #defineSKM_sk_value(type, st, i) ((type *)sk_value(CHECKED_STACK_OF(type, st), i))
OCSP_single_get0_status 获取状态后直接返回值,并没有做对应的值转换或者判断.
存储的 key 和谁有关 Ocsp 缓存使用使用证书的 Serial Number 作为key
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // ngx_ssl_ocsp_create_key static ngx_int_t ngx_ssl_ocsp_create_key(ngx_ssl_ocsp_ctx_t *ctx) { u_char *p; ...... ctx->key.data = p; // 设置p 为key 默认设置长度 60 ctx->key.len = 60; ...... // 获取 serial number serial = X509_get_serialNumber(ctx->cert); if (serial->length > 20) { return NGX_ERROR; } // 将证书得 serial 复制到p中 p = ngx_cpymem(p, serial->data, serial->length); ngx_memzero(p, 20 - serial->length); ...... return NGX_OK; }
Ocsp 存储的大小 Ocsp 的存储使用 nginx 的共享内存来存储,关于 nginx 共享内存可以看 OpenResty 和 Nginx 如何分配和管理内存
在OCSP 的数据在存储到红黑树中只存储了数据的状态和过期时间,并不存储整张证书.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // ngx_ssl_ocsp_cache_store ocsp cache 存储 // node 分配的内存块 node->node.str.len = ctx->key.len; node->node.str.data = (u_char *) node + sizeof(ngx_ssl_ocsp_cache_node_t); // 将OCSP ctx->key 存储到 node.str ngx_memcpy(node->node.str.data, ctx->key.data, ctx->key.len); // node.key 为 ctx->key 的 hash 值 node->node.node.key = hash; // 证书的状态 node->status = ctx->status; // 证书的过期时间 node->valid = valid; // 将node插入到树中 ngx_rbtree_insert(&cache->rbtree, &node->node.node); // 将数据加入到 LRU 队列 ngx_queue_insert_head(&cache->expire_queue, &node->queue);
由于无法直接读取到 nginx 创建的共享内存空间,我们在 lua 里面创建一个共享对象来验证占用.
1 2 3 4 5 6 7 8 typedef struct { ngx_str_node_t node; // 16 bytes ngx_queue_t queue; // 32 bytes int status; // 4 bytes time_t valid; // 8 bytes } ngx_ssl_ocsp_cache_node_t; size_t size = sizeof(ngx_ssl_ocsp_cache_node_t); // 60 bytes
写入30w 个key value 长度为 60 的字符串,内存占用 79.078125 M , 当把字符串长度扩展到80时,内存占用为 79.078125 M, 和 60 是一致的. (实际分配了 128 bytes 的内存)
以上的验证可能并不精确,但是可以大概描述 ocsp cache 在内存中的存储的大致量. 当 cache 数为 500M 时 ,大致可以缓存 180 w 个.
缓存满了的淘汰策略 淘汰策略使用 LRU (最近最少) .
当 cache 缓存满了后, 会自动将新的添加到队列的头部,删除尾部的数据.
Ocsp stapling 只会下发 good 的状态. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 static void ngx_ssl_stapling_ocsp_handler(ngx_ssl_ocsp_ctx_t *ctx) { time_t now; ngx_str_t response; ngx_ssl_stapling_t *staple; staple = ctx->data; now = ngx_time(); if (ngx_ssl_ocsp_verify(ctx) != NGX_OK) { goto error ; } **if (ctx->status != V_OCSP_CERTSTATUS_GOOD) { ngx_log_error(NGX_LOG_ERR, ctx->log , 0 , "certificate status \"%s\" in the OCSP response" , OCSP_cert_status_str(ctx->status )); goto error ; }** /* copy the response to memory not in ctx->pool */ response.len = ctx->response->last - ctx->response->pos; response.data = ngx_alloc(response.len , ctx->log ); ...