diff src/event/ngx_event_openssl_stapling.c @ 7653:8409f9df6219

SSL: client certificate validation with OCSP (ticket #1534). OCSP validation for client certificates is enabled by the "ssl_ocsp" directive. OCSP responder can be optionally specified by "ssl_ocsp_responder". When session is reused, peer chain is not available for validation. If the verified chain contains certificates from the peer chain not available at the server, validation will fail.
author Roman Arutyunyan <arut@nginx.com>
date Fri, 22 May 2020 17:30:12 +0300
parents 7cffd81015e7
children b56f725dd4bb
line wrap: on
line diff
--- a/src/event/ngx_event_openssl_stapling.c
+++ b/src/event/ngx_event_openssl_stapling.c
@@ -43,8 +43,35 @@ typedef struct {
 } ngx_ssl_stapling_t;
 
 
+typedef struct {
+    ngx_addr_t                  *addrs;
+    ngx_uint_t                   naddrs;
+
+    ngx_str_t                    host;
+    ngx_str_t                    uri;
+    in_port_t                    port;
+    ngx_uint_t                   depth;
+
+    ngx_resolver_t              *resolver;
+    ngx_msec_t                   resolver_timeout;
+} ngx_ssl_ocsp_conf_t;
+
+
 typedef struct ngx_ssl_ocsp_ctx_s  ngx_ssl_ocsp_ctx_t;
 
+
+struct ngx_ssl_ocsp_s {
+    STACK_OF(X509)              *certs;
+    ngx_uint_t                   ncert;
+
+    int                          cert_status;
+    ngx_int_t                    status;
+
+    ngx_ssl_ocsp_conf_t         *conf;
+    ngx_ssl_ocsp_ctx_t          *ctx;
+};
+
+
 struct ngx_ssl_ocsp_ctx_s {
     SSL_CTX                     *ssl_ctx;
 
@@ -114,7 +141,12 @@ static time_t ngx_ssl_stapling_time(ASN1
 
 static void ngx_ssl_stapling_cleanup(void *data);
 
-static ngx_ssl_ocsp_ctx_t *ngx_ssl_ocsp_start(void);
+static void ngx_ssl_ocsp_validate_next(ngx_connection_t *c);
+static void ngx_ssl_ocsp_handler(ngx_ssl_ocsp_ctx_t *ctx);
+static ngx_int_t ngx_ssl_ocsp_responder(ngx_connection_t *c,
+    ngx_ssl_ocsp_ctx_t *ctx);
+
+static ngx_ssl_ocsp_ctx_t *ngx_ssl_ocsp_start(ngx_log_t *log);
 static void ngx_ssl_ocsp_done(ngx_ssl_ocsp_ctx_t *ctx);
 static void ngx_ssl_ocsp_next(ngx_ssl_ocsp_ctx_t *ctx);
 static void ngx_ssl_ocsp_request(ngx_ssl_ocsp_ctx_t *ctx);
@@ -570,7 +602,7 @@ ngx_ssl_stapling_update(ngx_ssl_stapling
 
     staple->loading = 1;
 
-    ctx = ngx_ssl_ocsp_start();
+    ctx = ngx_ssl_ocsp_start(ngx_cycle->log);
     if (ctx == NULL) {
         return;
     }
@@ -709,14 +741,467 @@ 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_url_t             u;
+    ngx_ssl_ocsp_conf_t  *ocf;
+
+    ocf = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_ocsp_conf_t));
+    if (ocf == NULL) {
+        return NGX_ERROR;
+    }
+
+    ocf->depth = depth;
+
+    if (responder->len) {
+        ngx_memzero(&u, sizeof(ngx_url_t));
+
+        u.url = *responder;
+        u.default_port = 80;
+        u.uri_part = 1;
+
+        if (u.url.len > 7
+            && ngx_strncasecmp(u.url.data, (u_char *) "http://", 7) == 0)
+        {
+            u.url.len -= 7;
+            u.url.data += 7;
+
+        } else {
+            ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
+                          "invalid URL prefix in OCSP responder \"%V\" "
+                          "in \"ssl_ocsp_responder\"", &u.url);
+            return NGX_ERROR;
+        }
+
+        if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
+            if (u.err) {
+                ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
+                              "%s in OCSP responder \"%V\" "
+                              "in \"ssl_ocsp_responder\"", u.err, &u.url);
+            }
+
+            return NGX_ERROR;
+        }
+
+        ocf->addrs = u.addrs;
+        ocf->naddrs = u.naddrs;
+        ocf->host = u.host;
+        ocf->uri = u.uri;
+        ocf->port = u.port;
+    }
+
+    if (SSL_CTX_set_ex_data(ssl->ctx, ngx_ssl_ocsp_index, ocf) == 0) {
+        ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
+                      "SSL_CTX_set_ex_data() failed");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+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_ssl_ocsp_conf_t  *ocf;
+
+    ocf = SSL_CTX_get_ex_data(ssl->ctx, ngx_ssl_ocsp_index);
+    ocf->resolver = resolver;
+    ocf->resolver_timeout = resolver_timeout;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_ssl_ocsp_validate(ngx_connection_t *c)
+{
+    X509                 *cert;
+    SSL_CTX              *ssl_ctx;
+    ngx_int_t             rc;
+    X509_STORE           *store;
+    X509_STORE_CTX       *store_ctx;
+    STACK_OF(X509)       *chain;
+    ngx_ssl_ocsp_t       *ocsp;
+    ngx_ssl_ocsp_conf_t  *ocf;
+
+    if (c->ssl->in_ocsp) {
+        if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        if (ngx_handle_write_event(c->write, 0) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        return NGX_AGAIN;
+    }
+
+    ssl_ctx = SSL_get_SSL_CTX(c->ssl->connection);
+
+    ocf = SSL_CTX_get_ex_data(ssl_ctx, ngx_ssl_ocsp_index);
+    if (ocf == NULL) {
+        return NGX_OK;
+    }
+
+    if (SSL_get_verify_result(c->ssl->connection) != X509_V_OK) {
+        return NGX_OK;
+    }
+
+    cert = SSL_get_peer_certificate(c->ssl->connection);
+    if (cert == NULL) {
+        return NGX_OK;
+    }
+
+    ocsp = ngx_pcalloc(c->pool, sizeof(ngx_ssl_ocsp_t));
+    if (ocsp == NULL) {
+        return NGX_ERROR;
+    }
+
+    c->ssl->ocsp = ocsp;
+
+    ocsp->status = NGX_AGAIN;
+    ocsp->cert_status = V_OCSP_CERTSTATUS_GOOD;
+    ocsp->conf = ocf;
+
+#if (OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined LIBRESSL_VERSION_NUMBER)
+
+    ocsp->certs = SSL_get0_verified_chain(c->ssl->connection);
+
+    if (ocsp->certs) {
+        ocsp->certs = X509_chain_up_ref(ocsp->certs);
+        if (ocsp->certs == NULL) {
+            return NGX_ERROR;
+        }
+    }
+
+#endif
+
+    if (ocsp->certs == NULL) {
+        store = SSL_CTX_get_cert_store(ssl_ctx);
+        if (store == NULL) {
+            ngx_ssl_error(NGX_LOG_ERR, c->log, 0,
+                          "SSL_CTX_get_cert_store() failed");
+            return NGX_ERROR;
+        }
+
+        store_ctx = X509_STORE_CTX_new();
+        if (store_ctx == NULL) {
+            ngx_ssl_error(NGX_LOG_ERR, c->log, 0,
+                          "X509_STORE_CTX_new() failed");
+            return NGX_ERROR;
+        }
+
+        chain = SSL_get_peer_cert_chain(c->ssl->connection);
+
+        if (X509_STORE_CTX_init(store_ctx, store, cert, chain) == 0) {
+            ngx_ssl_error(NGX_LOG_ERR, c->log, 0,
+                          "X509_STORE_CTX_init() failed");
+            X509_STORE_CTX_free(store_ctx);
+            return NGX_ERROR;
+        }
+
+        rc = X509_verify_cert(store_ctx);
+        if (rc <= 0) {
+            ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "X509_verify_cert() failed");
+            X509_STORE_CTX_free(store_ctx);
+            return NGX_ERROR;
+        }
+
+        ocsp->certs = X509_STORE_CTX_get1_chain(store_ctx);
+        if (ocsp->certs == NULL) {
+            ngx_ssl_error(NGX_LOG_ERR, c->log, 0,
+                          "X509_STORE_CTX_get1_chain() failed");
+            X509_STORE_CTX_free(store_ctx);
+            return NGX_ERROR;
+        }
+
+        X509_STORE_CTX_free(store_ctx);
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "ssl ocsp validate, certs:%i", sk_X509_num(ocsp->certs));
+
+    ngx_ssl_ocsp_validate_next(c);
+
+    if (ocsp->status == NGX_AGAIN) {
+        c->ssl->in_ocsp = 1;
+        return NGX_AGAIN;
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_ssl_ocsp_validate_next(ngx_connection_t *c)
+{
+    ngx_uint_t            n;
+    ngx_ssl_ocsp_t       *ocsp;
+    ngx_ssl_ocsp_ctx_t   *ctx;
+    ngx_ssl_ocsp_conf_t  *ocf;
+
+    ocsp = c->ssl->ocsp;
+    ocf = ocsp->conf;
+
+    n = sk_X509_num(ocsp->certs);
+
+    for ( ;; ) {
+
+        if (ocsp->ncert == n - 1 || (ocf->depth == 2 && ocsp->ncert == 1)) {
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "ssl ocsp validated, certs:%ui", ocsp->ncert);
+            goto done;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "ssl ocsp validate cert:%ui", ocsp->ncert);
+
+        ctx = ngx_ssl_ocsp_start(c->log);
+        if (ctx == NULL) {
+            goto failed;
+        }
+
+        ocsp->ctx = ctx;
+
+        ctx->ssl_ctx = SSL_get_SSL_CTX(c->ssl->connection);
+        ctx->cert = sk_X509_value(ocsp->certs, ocsp->ncert);
+        ctx->issuer = sk_X509_value(ocsp->certs, ocsp->ncert + 1);
+        ctx->chain = ocsp->certs;
+
+        ctx->resolver = ocf->resolver;
+        ctx->resolver_timeout = ocf->resolver_timeout;
+
+        ctx->handler = ngx_ssl_ocsp_handler;
+        ctx->data = c;
+
+        ctx->addrs = ocf->addrs;
+        ctx->naddrs = ocf->naddrs;
+        ctx->host = ocf->host;
+        ctx->uri = ocf->uri;
+        ctx->port = ocf->port;
+
+        if (ngx_ssl_ocsp_responder(c, ctx) != NGX_OK) {
+            goto failed;
+        }
+
+        if (ctx->uri.len == 0) {
+            ngx_str_set(&ctx->uri, "/");
+        }
+
+        ocsp->ncert++;
+
+        break;
+    }
+
+    ngx_ssl_ocsp_request(ctx);
+    return;
+
+done:
+
+    ocsp->status = NGX_OK;
+    return;
+
+failed:
+
+    ocsp->status = NGX_ERROR;
+}
+
+
+static void
+ngx_ssl_ocsp_handler(ngx_ssl_ocsp_ctx_t *ctx)
+{
+    ngx_int_t          rc;
+    ngx_ssl_ocsp_t    *ocsp;
+    ngx_connection_t  *c;
+
+    c = ctx->data;
+    ocsp = c->ssl->ocsp;
+    ocsp->ctx = NULL;
+
+    rc = ngx_ssl_ocsp_verify(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;
+        ngx_ssl_ocsp_done(ctx);
+        goto done;
+    }
+
+    ngx_ssl_ocsp_done(ctx);
+
+    ngx_ssl_ocsp_validate_next(c);
+
+done:
+
+    if (ocsp->status == NGX_AGAIN || !c->ssl->in_ocsp) {
+        return;
+    }
+
+    c->ssl->handshaked = 1;
+
+    c->ssl->handler(c);
+}
+
+
+static ngx_int_t
+ngx_ssl_ocsp_responder(ngx_connection_t *c, ngx_ssl_ocsp_ctx_t *ctx)
+{
+    char                      *s;
+    ngx_str_t                  responder;
+    ngx_url_t                  u;
+    STACK_OF(OPENSSL_STRING)  *aia;
+
+    if (ctx->host.len) {
+        return NGX_OK;
+    }
+
+    /* extract OCSP responder URL from certificate */
+
+    aia = X509_get1_ocsp(ctx->cert);
+    if (aia == NULL) {
+        ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                      "no OCSP responder URL in certificate");
+        return NGX_ERROR;
+    }
+
+#if OPENSSL_VERSION_NUMBER >= 0x10000000L
+    s = sk_OPENSSL_STRING_value(aia, 0);
+#else
+    s = sk_value(aia, 0);
+#endif
+    if (s == NULL) {
+        ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                      "no OCSP responder URL in certificate");
+        X509_email_free(aia);
+        return NGX_ERROR;
+    }
+
+    responder.len = ngx_strlen(s);
+    responder.data = ngx_palloc(ctx->pool, responder.len);
+    if (responder.data == NULL) {
+        X509_email_free(aia);
+        return NGX_ERROR;
+    }
+
+    ngx_memcpy(responder.data, s, responder.len);
+    X509_email_free(aia);
+
+    ngx_memzero(&u, sizeof(ngx_url_t));
+
+    u.url = responder;
+    u.default_port = 80;
+    u.uri_part = 1;
+    u.no_resolve = 1;
+
+    if (u.url.len > 7
+        && ngx_strncasecmp(u.url.data, (u_char *) "http://", 7) == 0)
+    {
+        u.url.len -= 7;
+        u.url.data += 7;
+
+    } else {
+        ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                      "invalid URL prefix in OCSP responder \"%V\" "
+                      "in certificate", &u.url);
+        return NGX_ERROR;
+    }
+
+    if (ngx_parse_url(ctx->pool, &u) != NGX_OK) {
+        if (u.err) {
+            ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                          "%s in OCSP responder \"%V\" in certificate",
+                          u.err, &u.url);
+        }
+
+        return NGX_ERROR;
+    }
+
+    if (u.host.len == 0) {
+        ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                      "empty host in OCSP responder in certificate");
+        return NGX_ERROR;
+    }
+
+    ctx->addrs = u.addrs;
+    ctx->naddrs = u.naddrs;
+    ctx->host = u.host;
+    ctx->uri = u.uri;
+    ctx->port = u.port;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_ssl_ocsp_get_status(ngx_connection_t *c, const char **s)
+{
+    ngx_ssl_ocsp_t  *ocsp;
+
+    ocsp = c->ssl->ocsp;
+    if (ocsp == NULL) {
+        return NGX_OK;
+    }
+
+    if (ocsp->status == NGX_ERROR) {
+        *s = "certificate status request failed";
+        return NGX_DECLINED;
+    }
+
+    switch (ocsp->cert_status) {
+
+    case V_OCSP_CERTSTATUS_GOOD:
+        return NGX_OK;
+
+    case V_OCSP_CERTSTATUS_REVOKED:
+        *s = "certificate revoked";
+        break;
+
+    default: /* V_OCSP_CERTSTATUS_UNKNOWN */
+        *s = "certificate status unknown";
+    }
+
+    return NGX_DECLINED;
+}
+
+
+void
+ngx_ssl_ocsp_cleanup(ngx_connection_t *c)
+{
+    ngx_ssl_ocsp_t  *ocsp;
+
+    ocsp = c->ssl->ocsp;
+    if (ocsp == NULL) {
+        return;
+    }
+
+    if (ocsp->ctx) {
+        ngx_ssl_ocsp_done(ocsp->ctx);
+        ocsp->ctx = NULL;
+    }
+
+    if (ocsp->certs) {
+        sk_X509_pop_free(ocsp->certs, X509_free);
+        ocsp->certs = NULL;
+    }
+}
+
+
 static ngx_ssl_ocsp_ctx_t *
-ngx_ssl_ocsp_start(void)
+ngx_ssl_ocsp_start(ngx_log_t *log)
 {
-    ngx_log_t           *log;
     ngx_pool_t          *pool;
     ngx_ssl_ocsp_ctx_t  *ctx;
 
-    pool = ngx_create_pool(2048, ngx_cycle->log);
+    pool = ngx_create_pool(2048, log);
     if (pool == NULL) {
         return NULL;
     }
@@ -828,6 +1313,14 @@ ngx_ssl_ocsp_request(ngx_ssl_ocsp_ctx_t 
         }
 
         if (resolve == NGX_NO_RESOLVER) {
+            if (ctx->naddrs == 0) {
+                ngx_log_error(NGX_LOG_ERR, ctx->log, 0,
+                              "no resolver defined to resolve %V", &ctx->host);
+
+                ngx_ssl_ocsp_error(ctx);
+                return;
+            }
+
             ngx_log_error(NGX_LOG_WARN, ctx->log, 0,
                           "no resolver defined to resolve %V", &ctx->host);
             goto connect;
@@ -979,8 +1472,10 @@ ngx_ssl_ocsp_connect(ngx_ssl_ocsp_ctx_t 
 
     ctx->process = ngx_ssl_ocsp_process_status_line;
 
-    ngx_add_timer(ctx->peer.connection->read, ctx->timeout);
-    ngx_add_timer(ctx->peer.connection->write, ctx->timeout);
+    if (ctx->timeout) {
+        ngx_add_timer(ctx->peer.connection->read, ctx->timeout);
+        ngx_add_timer(ctx->peer.connection->write, ctx->timeout);
+    }
 
     if (rc == NGX_OK) {
         ngx_ssl_ocsp_write_handler(ctx->peer.connection->write);
@@ -1036,7 +1531,7 @@ ngx_ssl_ocsp_write_handler(ngx_event_t *
         }
     }
 
-    if (!wev->timer_set) {
+    if (!wev->timer_set && ctx->timeout) {
         ngx_add_timer(wev, ctx->timeout);
     }
 }
@@ -1939,4 +2434,43 @@ 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_log_error(NGX_LOG_EMERG, ssl->log, 0,
+                  "\"ssl_ocsp\" is not supported on this platform");
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_ssl_ocsp_resolver(ngx_conf_t *cf, ngx_ssl_t *ssl,
+    ngx_resolver_t *resolver, ngx_msec_t resolver_timeout)
+{
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_ssl_ocsp_validate(ngx_connection_t *c)
+{
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_ssl_ocsp_get_status(ngx_connection_t *c, const char **s)
+{
+    return NGX_OK;
+}
+
+
+void
+ngx_ssl_ocsp_cleanup(ngx_connection_t *c)
+{
+}
+
+
 #endif