From e15f8e91c6bb05df05acfcf2689616056ec9d2d1 Mon Sep 17 00:00:00 2001 From: Timothy Copeland Date: Wed, 12 Aug 2026 19:46:45 +1000 Subject: [PATCH] Don't verify the cert chain --- README.md | 19 ++ app/main.c | 292 ++++++++++++++++++++++------- benchmark/certs/mkcerts.sh | 8 +- doc/ARCHITECTURE.md | 22 ++- example-docker/config/tlsproxy.yml | 4 +- example/chain.pem | 207 -------------------- example/default.yml | 41 +++- test/CMakeLists.txt | 9 +- test/configs/goodconf1.yml | 1 - test/integration/reload_fds.sh | 1 - test/integration/reload_lock.sh | 1 - test/integration/reload_reports.sh | 1 - test/integration/selfsigned.sh | 120 ++++++++---- test/integration/signals.sh | 1 - 14 files changed, 390 insertions(+), 337 deletions(-) diff --git a/README.md b/README.md index 720df5e..01c4634 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,25 @@ beside it. Every timeout is in seconds. `nworkers` wants to be the number of hardware threads you are giving it. Half that leaves half the machine idle, and measurably so. +What a listener sends is the server certificate and the intermediates above it, +with the root left out, since a client that doesn't already trust the root gains +nothing from a copy of it and one that does has it already. Named individually +in `cacerts` the intermediates are put in order for you and anything not on the +path up from `servcert` draws a warning and stays behind. One certificate per +file there, since we read the first one in each and stop, so a bundle needs +splitting up before it goes in the list and nothing will tell you if it didn't. +Given instead as one `cert-chain` file they go out as written, leaf first and +each issuer after the certificate it signed, since that path sends the file +rather than sorting it. + +Nothing verifies any of that. No path is built against a trust store, no +signature is checked and no expiry is looked at, so a chain with a link missing +starts here and is refused at the client instead. The one check at startup is +OpenSSL's, that `servkey` belongs to the certificate being served, and it is a +refusal to start. A self-signed certificate on its own in `cert-chain` is +therefore an ordinary configuration rather than a special case, and is how to +run this without a CA at all. + **`SIGHUP` reloads the configuration.** The master re-reads and re-validates the file, and only then tells the workers to cycle, so a file with a bad port, an `nworkers` of zero, a `tcp-keep*` above what the kernel accepts, or a diff --git a/app/main.c b/app/main.c index a7c96f4..1af95aa 100644 --- a/app/main.c +++ b/app/main.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -70,9 +71,12 @@ void init_shmem(void); int init_logger(tpx_config_t *config); SSL_CTX *init_openssl(const tpx_listen_conf_t *config, int logfd); -int load_servcert(const tpx_listen_conf_t *config, SSL_CTX *ctx, int logfd); -int load_cacerts(const tpx_listen_conf_t *config, SSL_CTX *ctx, int logfd); +X509 *load_servcert(const tpx_listen_conf_t *config, int logfd); +STACK_OF(X509) *load_cacerts(const tpx_listen_conf_t *config, int logfd); +int load_chain_file(const tpx_listen_conf_t *config, int logfd, + STACK_OF(X509) **ca_certs, X509 **leaf); int load_servkey(const tpx_listen_conf_t *config, SSL_CTX *ctx, int logfd); +int build_chain(const tpx_listen_conf_t *config, SSL_CTX *ctx, int logfd); int handle_reload(tpx_config_t **config, int *logfd, pid_t **pids); @@ -628,9 +632,8 @@ SSL_CTX *init_openssl(const tpx_listen_conf_t *config, int logfd) { // TODO: make the ciphersuites and accepted TLS versions configurable if (!SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION)) { - SSL_CTX_free(ctx); _fatal(logfd, "Couldn't set minimum TLS protocol version", TPX_ERR_OSSL); - return NULL; + goto cleanup_fail; } uint64_t opts = @@ -646,44 +649,11 @@ SSL_CTX *init_openssl(const tpx_listen_conf_t *config, int logfd) { // later on (issue #44) SSL_CTX_set_num_tickets(ctx, 1); - if (config->cacerts != NULL) { - if (load_servcert(config, ctx, logfd) == 0) - goto cleanup_fail; - if (load_cacerts(config, ctx, logfd) == 0) - goto cleanup_fail; - } else if (config->cert_chain != NULL) { - if (SSL_CTX_use_certificate_chain_file(ctx, config->cert_chain) != 1) { - _fatal(logfd, "Couldn't load cert chain", TPX_ERR_OSSL); - goto cleanup_fail; - } - - STACK_OF(X509) *certs; - SSL_CTX_get0_chain_certs(ctx, &certs); - for (int i=0; icacerts == NULL - ? SSL_BUILD_CHAIN_FLAG_UNTRUSTED | - SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR - : 0; - - int chain_ret = SSL_CTX_build_cert_chain(ctx, flags); - if (chain_ret == 0) { - _fatal(logfd, "Failed to build cert chain", TPX_ERR_OSSL); + if (build_chain(config, ctx, logfd) == 0) { + _fatal(logfd, "Couldn't load cert chain", TPX_ERR_PLAIN); goto cleanup_fail; - } else if (chain_ret == 2) { - // This is what happens if we built a cert chain with ignored errors - log_system_err_m_ex(logfd, LL_WARN, "Building cert chain", "Ignored cert errors"); } - if (load_servkey(config, ctx, logfd) == 0) - goto cleanup_fail; - // No mTLS SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); @@ -695,12 +665,12 @@ SSL_CTX *init_openssl(const tpx_listen_conf_t *config, int logfd) { } /** @brief Load the server certificate into the SSL_CTX */ -int load_servcert(const tpx_listen_conf_t *config, SSL_CTX *ctx, int logfd) { +X509 *load_servcert(const tpx_listen_conf_t *config, int logfd) { BIO *leaf_bio = BIO_new_file(config->servcert, "r"); if (leaf_bio == NULL) { _fatal(logfd, "Failed to open server cert file", TPX_ERR_OSSL); - return 0; + return NULL; } X509 *leaf = NULL; @@ -708,48 +678,106 @@ int load_servcert(const tpx_listen_conf_t *config, SSL_CTX *ctx, int logfd) { BIO_free(leaf_bio); X509_free(leaf); _fatal(logfd, "Failed to load server cert", TPX_ERR_OSSL); - return 0; + return NULL; } BIO_free(leaf_bio); log_cert_load(logfd, LL_INFO, leaf, 0); - if (SSL_CTX_use_certificate(ctx, leaf) != 1) { - X509_free(leaf); - _fatal(logfd, "Failed to add server certificate to CTX", TPX_ERR_OSSL); - return 0; + return leaf; +} + +/** @brief Load the CA certificates into a stack + * + * @param[in] config The configuration information + * @param[in] logfd The fd which receives log messages + * @return 1 for success, 0 for failure + **/ +STACK_OF(X509) *load_cacerts(const tpx_listen_conf_t *config, int logfd) { + STACK_OF(X509) *ca_certs = sk_X509_new_null(); + + for (size_t i=0; icacerts_count; ++i) { + BIO *bio = BIO_new_file(config->cacerts[i], "rb"); + if (!bio) { + _fatal(logfd, "Couldn't allocate BIO", TPX_ERR_OSSL); + goto cleanup; + } + + X509 *cert = PEM_read_bio_X509(bio, NULL, NULL, NULL); + BIO_free(bio); + if (!cert) { + _fatal(logfd, "Couldn't load cert file", TPX_ERR_OSSL); + goto cleanup; + } + + if (sk_X509_push(ca_certs, cert) == 0) { + _fatal(logfd, "Couldn't push cert to stack", TPX_ERR_OSSL); + X509_free(cert); + goto cleanup; + } + + log_cert_load(logfd, LL_INFO, cert, 0); } - // We can "free" leaf here because it's refcounted, and we lose our ref - X509_free(leaf); - return 1; + return ca_certs; +cleanup: + sk_X509_pop_free(ca_certs, X509_free); + return NULL; } -/** @brief Load the CA certificates into the SSL_CTX */ -int load_cacerts(const tpx_listen_conf_t *config, SSL_CTX *ctx, int logfd) { - X509_STORE *store = X509_STORE_new(); - if (store == NULL) { - _fatal(logfd, "Couldn't allocate X509 store", TPX_ERR_OSSL); - return 0; - } +/** @brief Load the CA certificates into a stack + * + * @param[in] config The configuration information + * @param[in] logfd The fd which receives log messages + * @param[out] ca_certs CA certificates will be loaded into here + * @param[out] leaf The leaf certificate will be loaded into here + * @return 1 for success, 0 for failure. In case of failure, caller doesn't + * need to free ca_certs or leaf + **/ +int load_chain_file(const tpx_listen_conf_t *config, int logfd, + STACK_OF(X509) **ca_certs, X509 **leaf) { + *ca_certs = sk_X509_new_null(); + *leaf = NULL; + + int is_leaf = 1; + + BIO *bio = BIO_new_file(config->cert_chain, "rb"); + X509 *cert = NULL; + while ((cert = PEM_read_bio_X509(bio, NULL, NULL, NULL)) != 0) { + log_cert_load(logfd, LL_INFO, cert, 0); + + if (is_leaf) { + *leaf = cert; + is_leaf = 0; + continue; + } - X509_LOOKUP *lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file()); - for (size_t i=0; icacerts_count; ++i) { - if (!X509_LOOKUP_load_file(lookup, config->cacerts[i], - X509_FILETYPE_PEM)) { - X509_STORE_free(store); - _fatal(logfd, "Couldn't load CA certificate", TPX_ERR_OSSL); - return 0; + if (sk_X509_push(*ca_certs, cert) == 0) { + _fatal(logfd, "Couldn't push cert to stack", TPX_ERR_OSSL); + X509_free(cert); + goto cleanup; } } + BIO_free(bio); + bio = NULL; - SSL_CTX_set_cert_store(ctx, store); - - STACK_OF(X509) *certs; - SSL_CTX_get0_chain_certs(ctx, &certs); - for (int i=0; icacerts != NULL) { + if ((ca_certs = load_cacerts(config, logfd)) == NULL) { + _fatal(logfd, "Couldn't load CA certs", TPX_ERR_PLAIN); + goto cleanup; + } + if ((leaf = load_servcert(config, logfd)) == NULL) { + _fatal(logfd, "Couldn't load server cert", TPX_ERR_PLAIN); + goto cleanup; + } + + // We'll fill the chain ourselves + chain = sk_X509_new_null(); + } else if (config->cert_chain != NULL) { + if (!load_chain_file(config, logfd, &ca_certs, &leaf)) { + // Can't have these freed, they're guaranteed to be freed already if + // load_chain_file returns 0 + ca_certs = NULL; + leaf = NULL; + _fatal(logfd, "Couldn't load certificate chain from file", + TPX_ERR_PLAIN); + goto cleanup; + } + chain = ca_certs; + } else { + _fatal(logfd, "Config contains neither cert-chain nor cacerts", + TPX_ERR_PLAIN); + goto cleanup; + } + + if (SSL_CTX_use_certificate(ctx, leaf) == 0) { + _fatal(logfd, "Couldn't insert leaf cert into OpenSSL context", + TPX_ERR_OSSL); + goto cleanup; + } + + if (load_servkey(config, ctx, logfd) == 0) { + _fatal(logfd, "Couldn't load server key into OpenSSL context", + TPX_ERR_PLAIN); + goto cleanup; + } + + if (chain != ca_certs) { + X509 *cur = leaf; + int found; + for (;;) { + found = 0; + for (size_t i=0; i SUBJ_MAX ? "..." : ""); + OPENSSL_free(subj); + + log_system_err_m_ex(logfd, LL_WARN, "Certificate isn't in chain", + ignored); + fprintf(stderr, "%s\n", ignored); + } + #undef SUBJ_MAX + + sk_X509_pop_free(ca_certs, X509_free); + } + + // In either case, we no longer need the leaf, we've given it to the SSL_CTX + X509_free(leaf); + // Set them to NULL so they're not double freed + leaf = NULL; + // If we're using cacerts, this is already free. If we're using the + // cert-chain, set this to NULL so that only chain is freed in cleanup + ca_certs = NULL; + + if (SSL_CTX_set0_chain(ctx, chain) == 0) { + _fatal(logfd, "Couldn't insert cert chain into OpenSSL context", + TPX_ERR_OSSL); + goto cleanup; + } + + return 1; + +cleanup: + X509_free(leaf); + + sk_X509_pop_free(ca_certs, X509_free); + sk_X509_pop_free(chain, X509_free); + + return 0; +} + int handle_reload(tpx_config_t **config, int *logfd, pid_t **pids) { assert(config_fname); tpx_config_t *new_config; diff --git a/benchmark/certs/mkcerts.sh b/benchmark/certs/mkcerts.sh index 1a33bf6..5e44bc8 100755 --- a/benchmark/certs/mkcerts.sh +++ b/benchmark/certs/mkcerts.sh @@ -3,9 +3,11 @@ # leaf signed by it. All three subjects serve the same two-certificate chain, # so the handshake carries the same bytes whichever one is under test. # -# A lone self-signed leaf is not usable here: SSL_CTX_build_cert_chain() -# returns 2 for one, and app/main.c:init_openssl() treats anything but 1 as -# fatal, so tlsproxy will not start with one in cert-chain. +# That second certificate is the CA itself, so this is the one place we put a +# root in a chain, against the advice in example/default.yml. The CSVs in +# benchmark/baseline/ were measured against these bytes, and taking a +# certificate out of the handshake would leave every later run incomparable +# with them. set -eu here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index e4fc3a2..0b56dca 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -89,19 +89,27 @@ connect-timeout: 5 listen-ip: 0.0.0.0 listen-port: 8443 -## The certificate chain offered to clients +## The certificate chain offered to clients. List the intermediates between +## servcert and the root, one certificate per file since we read the first in +## each and stop, and leave the root out: a client that doesn't already trust +## it gains nothing from a copy, and one that does has it already. A cacert +## that isn't in the path upwards from servcert is warned about and left out +## of what we send. cacerts: -- cacert.pem - intcert.pem servcert: servcert.pem servkey: servkey.pem #servkeypass: test -## Alternatively, provide all certs (including server cert) in a single file: +## Alternatively, provide the server certificate and the intermediates above +## it in a single file, leaf first and each issuer after the certificate it +## signed, stopping one short of the root. That file is sent as written: # cert-chain: chain.pem # servkey: servkey.pem # servkeypass: test ## cacerts and servcert can't be used together with cert-chain. +## A self-signed server certificate goes in cert-chain on its own, which +## nothing treats as an error, since we verify none of this. ## If we implement mTLS: # trusted-certs: @@ -156,6 +164,12 @@ The important decision regarding the proxy context (`proxy_t` in `inc/proxy.h`) The listener context just contains the listen socket and the peer address, used for connecting new sockets to the remote host once we accept a new connection. Basically, when a listener accepts a connection, it starts a new pending connection to the peer address. This is stored here so that we can easily support listening on multiple sockets and forwarding to different backend servers. Used with `SO_REUSEPORT`, this could also achieve load balancing: create two listen sockets both on the same port, but pointing to different backends, and the kernel will load balance for us. +### Certificates + +`build_chain()` in `app/main.c` decides what a listener offers, and the two config forms reach it differently. A `cert-chain` file is read straight through with `PEM_read_bio_X509()`: the first certificate becomes the leaf and the rest become the chain in the order the file has them, so that path sends what the operator wrote and neither sorts nor drops anything. Non-certificate PEM blocks are skipped by OpenSSL's own reader, so a file that also carries the private key works. With `cacerts` the leaf comes from `servcert` and the listed files are candidates: we start at the leaf and repeatedly take the first candidate for which `X509_check_issued()` says it issued the certificate we are holding, removing it from the candidates as we go so that a cross-certified pair cannot make the walk run forever. What is left over never joined the chain, so it is named in a `WARN` on stderr and in the log, and it is not sent. Each `cacerts` file gives us one certificate and one only, since `load_cacerts()` reads the first and closes the file: a bundle holding several contributes its first and nothing else, and the ones after it are never read, so nothing warns about them either. Splitting a bundle up is the operator's job, and `example/default.yml` says so. + +Both forms then hand the leaf to `SSL_CTX_use_certificate()`, the key to `SSL_CTX_use_PrivateKey()`, which is where a key that does not belong to the leaf is caught, and the chain to `SSL_CTX_set0_chain()`, which applies the security level to every CA in it. Nothing verifies the result, deliberately for now: we do not build a path against a trust store, check signatures, or look at expiry, since the operator's own chain has no reason to verify against a store we never populate, and #89 has the reporting that would replace it. A self-signed leaf is therefore an ordinary configuration rather than an error, which is what makes a proxy with no CA at all possible; what a client makes of it stays the client's decision. + ## Log messages @@ -197,7 +211,7 @@ This is called whenever the configuration file is (re)loaded. It will contain th - `certchain`: (Optional) The cert chain file. - `cacerts`: (Optional) A list of CA certificate files separated by ':'. - `servcert`: (Optional) The server certificate file. -- `servkey`: The server certificate file. +- `servkey`: The server private key file. ### Cert Loaded Event diff --git a/example-docker/config/tlsproxy.yml b/example-docker/config/tlsproxy.yml index 2b5869d..30629ff 100644 --- a/example-docker/config/tlsproxy.yml +++ b/example-docker/config/tlsproxy.yml @@ -12,8 +12,10 @@ listeners: listen-ip: 0.0.0.0 listen-port: 8443 + ## Intermediates only, since the root is no use to a client that doesn't + ## already have it and redundant for one that does, and one certificate + ## per file, since we read the first in each and stop cacerts: - - /etc/tlsproxy/cacert.pem - /etc/tlsproxy/intcert.pem servcert: /etc/tlsproxy/servcert.pem servkey: /etc/tlsproxy/servkey.pem diff --git a/example/chain.pem b/example/chain.pem index 0da20fa..5cfeb28 100644 --- a/example/chain.pem +++ b/example/chain.pem @@ -1,89 +1,3 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 2 (0x2) - Signature Algorithm: sha256WithRSAEncryption - Issuer: C=MX, ST=Durango, L=Durango, O=Dingo, OU=Development, CN=Test Intermediate - Validity - Not Before: Dec 15 14:18:32 2025 GMT - Not After : Sep 14 14:18:32 2035 GMT - Subject: C=MX, ST=Durango, L=Durango, O=Dingo, OU=Development, CN=Test Server - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (4096 bit) - Modulus: - 00:e7:4c:82:ee:b9:06:2b:f0:18:d6:ad:84:0d:0b: - 1d:59:f6:32:c0:07:23:98:3d:cb:e3:c5:83:ee:ad: - 37:7b:dc:76:16:cb:f0:8d:f6:f2:c0:6e:54:2f:a6: - 2a:30:80:65:e8:97:35:37:ad:80:b6:7b:d6:7c:40: - 39:35:71:8b:4a:b6:03:c6:02:bb:72:b1:f5:0e:b9: - 16:8d:5a:43:5a:ec:7b:f5:03:5b:6b:53:74:62:a6: - e6:63:5e:25:01:fd:2d:2c:87:8c:e2:de:de:48:a0: - 75:fe:b3:65:61:4c:12:30:88:ac:53:0c:15:93:22: - f8:ba:a0:03:c0:31:af:67:b6:98:8e:08:c7:68:ca: - 1f:da:f1:2c:ad:e4:d8:d9:a2:1d:b7:75:ba:b2:35: - e3:5b:3f:da:c1:eb:da:50:53:25:22:99:af:0a:68: - 6d:31:ff:fb:43:6e:8f:93:1b:c4:90:e8:94:bf:8b: - 78:b0:db:03:71:cb:e4:61:0f:1d:a0:f1:d7:c3:d7: - 19:f0:59:86:2c:99:65:6d:6e:e3:74:f4:10:0a:b5: - ca:3f:d1:5a:b4:78:8f:6c:a0:24:87:de:e8:83:d6: - f3:e2:12:9f:27:1b:e9:33:f4:a1:a2:40:e0:2a:ce: - d1:0e:3a:58:2d:36:82:97:1d:c7:b2:0e:d5:9d:c9: - 8e:57:84:15:f9:a8:41:07:04:52:76:5c:79:98:47: - 28:8e:cf:20:c5:57:bd:60:5a:ca:b2:d9:97:7c:67: - 43:f8:43:17:14:d2:8b:8c:f9:8b:8a:e8:c4:f7:9f: - df:bd:77:63:7d:1c:6c:d1:58:43:a5:87:74:12:f6: - c1:f1:26:8d:56:fa:a4:b6:74:3d:6b:84:ff:95:67: - 17:c4:01:de:71:2a:cf:af:35:0c:3b:63:1b:25:c8: - 1f:ad:8e:d9:6e:a5:f2:ac:2f:81:6e:9b:14:27:e8: - 5e:24:69:75:1a:0c:1b:ed:b9:2b:46:b2:66:87:41: - ea:08:12:d8:aa:78:6a:35:b6:3d:6c:a0:27:6d:57: - 2d:d7:06:02:a0:bf:79:a5:14:b0:d3:2b:6d:86:de: - 09:93:27:53:16:e0:14:97:38:20:95:42:b3:00:c1: - ad:5b:3a:d1:56:6d:4c:6a:ba:fb:be:81:f0:64:cd: - 35:0c:a8:b7:5a:f4:18:d5:44:74:65:46:87:34:9a: - e6:e6:75:0f:ee:82:54:7d:aa:f8:54:5e:70:04:4c: - 8f:18:c3:d7:b3:5c:57:17:8b:dd:84:47:d7:e7:5a: - 8b:29:af:d9:11:15:1d:b3:87:35:fe:ac:ca:b3:be: - 3a:83:d3:32:ed:3a:ef:17:34:57:ce:3b:43:91:09: - e9:50:d9 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - F0:C4:1C:60:A0:47:98:80:02:E7:AD:BA:47:DB:BA:74:43:88:58:EB - X509v3 Authority Key Identifier: - 72:94:15:74:24:C9:0A:2E:53:BD:47:DC:8C:FF:A4:FC:9E:D1:DC:65 - Signature Algorithm: sha256WithRSAEncryption - Signature Value: - ae:28:55:68:41:14:4d:fd:eb:8d:9d:fd:c9:26:65:c9:3e:7e: - 62:0d:54:9c:0d:b2:ea:33:27:36:23:55:90:34:8e:35:1b:d4: - d1:2a:d8:5e:85:e1:3b:f2:cd:7f:5f:f9:b8:bb:b3:fe:2f:7e: - fb:ac:8a:eb:28:8a:a1:97:99:97:b3:78:89:51:d9:73:55:2d: - d4:55:e1:d5:91:4f:4c:e3:d5:71:1f:a6:4c:c4:3e:11:8c:3f: - c3:b1:80:de:e5:6f:9a:b3:6b:ee:4a:46:fa:81:50:7c:80:ab: - 3b:ad:63:86:f1:ac:a3:d5:75:f2:f3:09:01:48:0c:34:34:1c: - 23:1a:0a:5c:59:24:c3:e6:eb:11:72:a4:9d:92:25:45:db:18: - 1c:99:76:3a:1e:68:6f:5d:e2:ce:59:45:ce:84:25:97:03:f5: - 9c:03:18:51:8f:bb:12:4b:b2:fd:26:b7:88:0b:0e:e7:c0:fc: - fb:73:75:77:c8:28:aa:ba:30:6d:a2:93:bd:aa:08:53:02:a7: - 1e:3e:45:44:37:d5:41:26:1d:09:64:83:f1:53:5b:29:28:42: - 4b:b9:28:3e:65:69:c7:fc:99:55:8c:01:13:15:96:4f:04:0c: - 4b:de:e1:e1:d3:93:aa:20:e6:89:52:9c:62:90:66:ce:fe:9d: - e7:98:f0:06:f0:03:79:42:02:86:db:22:b9:6f:92:38:59:9d: - 8b:35:52:dd:5c:b9:89:58:b7:1a:52:1c:bd:48:7c:17:aa:b9: - 16:08:fc:66:28:9d:e9:f3:10:16:77:5d:c4:9d:77:e9:d6:57: - 4b:d3:15:3c:ec:c6:1a:96:43:88:3f:a8:38:44:80:5c:01:bb: - 70:e1:c1:a3:85:83:cc:d8:a8:da:17:20:f3:ae:5a:2b:b0:fa: - 4c:fc:76:ee:32:71:88:16:8c:ec:d6:98:c5:36:8e:ad:d2:30: - 66:96:43:44:4b:a8:16:8f:6a:6b:8e:78:34:6b:9c:62:00:f6: - b5:e2:bf:e1:31:85:67:7f:03:43:ef:2b:74:d1:3d:d7:ec:cc: - 12:a0:82:83:ba:f2:40:de:5a:23:87:3b:ae:8a:a5:da:5d:51: - 4a:fa:01:67:ce:78:96:4e:2f:5e:0e:37:d4:a0:59:73:79:64: - 09:45:e0:7a:5d:45:50:9c:5e:9d:e7:32:32:3f:cd:50:75:41: - 34:f1:1f:2f:59:5d:30:f9:c9:94:b5:ae:f4:5b:d1:03:ca:37: - a0:63:3f:ec:e1:70:58:20:7a:d6:a8:50:d2:b0:40:5e:f6:cc: - 15:53:ec:54:b7:95:38:3e:fd:3c:cc:c7:72:d6:38:b2:4a:ce: - 6a:25:05:57:ca:71:99:2d -----BEGIN CERTIFICATE----- MIIFnTCCA4WgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBzMQswCQYDVQQGEwJNWDEQ MA4GA1UECAwHRHVyYW5nbzEQMA4GA1UEBwwHRHVyYW5nbzEOMAwGA1UECgwFRGlu @@ -117,94 +31,6 @@ j2prjng0a5xiAPa14r/hMYVnfwND7yt00T3X7MwSoIKDuvJA3lojhzuuiqXaXVFK W9EDyjegYz/s4XBYIHrWqFDSsEBe9swVU+xUt5U4Pv08zMdy1jiySs5qJQVXynGZ LQ== -----END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 1 (0x1) - Signature Algorithm: sha256WithRSAEncryption - Issuer: C=MX, ST=Durango, L=Durango, O=Dingo, OU=Development, CN=Test CA - Validity - Not Before: Dec 15 14:18:32 2025 GMT - Not After : Sep 14 14:18:32 2035 GMT - Subject: C=MX, ST=Durango, L=Durango, O=Dingo, OU=Development, CN=Test Intermediate - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (4096 bit) - Modulus: - 00:ca:84:16:33:e7:57:04:cc:45:88:17:be:e9:cb: - a0:43:ef:5b:6a:e6:3f:a4:86:b6:f7:3f:2f:b6:03: - ec:ae:89:37:93:46:12:bc:21:b5:76:00:57:1a:03: - 17:d5:31:1b:29:3f:59:00:62:50:d6:8c:a4:cb:21: - 97:59:7e:77:e3:c2:bb:83:a6:2b:6f:4d:a8:95:28: - 7d:93:40:4e:91:2d:c2:9c:aa:96:71:1a:89:fd:88: - 52:93:a0:d7:18:4f:99:cc:e5:48:21:81:19:95:9b: - 70:30:0e:cd:ca:08:5a:44:06:70:6d:88:b2:8a:de: - 20:75:e6:5a:cf:24:9e:48:6c:5f:8e:03:bc:ab:d1: - b8:6d:62:ec:4a:7d:ee:60:67:ff:27:23:65:48:6e: - a2:9e:7a:3d:c8:aa:27:11:d0:f2:92:07:85:25:5f: - 6c:20:74:d0:74:47:58:85:cf:66:c5:26:d8:cd:8e: - 9c:74:f8:24:f4:52:86:40:ec:07:ab:a8:d2:90:02: - b3:7a:bc:db:93:e1:da:a2:64:aa:6d:f8:fb:3c:c1: - 31:7d:73:54:dc:56:09:ce:d6:cb:26:63:79:3d:37: - 05:ae:58:9d:96:fb:2c:a2:df:70:5a:6b:65:d7:02: - fa:3e:85:63:1a:e0:ad:bb:2f:f2:5c:e3:7f:c4:cc: - a3:cc:8f:b3:e3:c7:8b:11:83:5c:f5:54:50:71:6c: - b3:b0:84:cc:18:62:cd:be:77:00:b5:18:b4:cc:d7: - 9d:bc:4c:56:31:6f:b9:b3:90:f6:94:72:58:a9:b6: - d1:95:fb:3a:f5:44:8f:cc:e2:41:2c:4c:59:93:0a: - 62:3c:3a:30:7d:33:97:84:39:df:30:04:f1:83:1e: - 89:29:5e:b2:08:11:ac:26:28:78:9f:5f:90:80:80: - 54:58:e2:9c:91:e8:e0:33:13:0c:c6:4d:46:9b:1d: - 56:da:47:86:07:2a:b7:92:3e:81:8e:ab:e8:db:af: - b6:c0:c0:fe:9c:b7:b6:dc:87:e4:3c:6e:9e:d9:6d: - f7:87:18:28:4a:ca:92:62:0e:4e:25:8b:47:94:98: - bc:0c:17:5e:2d:c6:b5:0d:95:e4:80:7a:a9:bc:58: - 28:f9:42:f1:2e:49:05:ca:3a:2e:ea:6e:dd:5c:2a: - 78:68:fc:af:9b:55:89:a7:a8:4b:6e:d6:b3:83:d2: - a9:94:e6:c1:a3:9c:db:1b:92:b0:6c:26:91:7b:2c: - 32:28:64:a8:e8:60:f6:ff:f5:8c:d1:b6:04:58:ef: - 19:9e:54:35:40:31:10:fc:ff:8e:ac:3f:41:f8:79: - 9a:ff:88:39:d5:45:07:d4:a9:33:03:f6:84:67:32: - 1b:34:a7 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: - CA:TRUE - X509v3 Subject Key Identifier: - 72:94:15:74:24:C9:0A:2E:53:BD:47:DC:8C:FF:A4:FC:9E:D1:DC:65 - X509v3 Authority Key Identifier: - 3A:6A:8A:F0:31:1C:26:75:FC:9A:E3:7E:72:D2:32:A2:BA:7B:5D:C5 - Signature Algorithm: sha256WithRSAEncryption - Signature Value: - c6:2b:9d:90:3a:02:69:8b:5a:f0:b9:ca:9f:c4:56:74:c2:c8: - e5:a7:13:aa:e8:15:b8:86:04:b6:b3:7e:03:b6:bc:88:06:62: - 27:70:13:5a:cd:ff:13:75:c4:f0:9b:20:69:28:b0:ac:03:31: - 99:8f:fe:eb:41:96:75:14:43:16:24:d4:27:72:64:5c:66:37: - 01:e3:09:66:2d:0d:98:fb:20:2e:08:ba:68:e8:45:51:80:bd: - 80:07:73:57:9a:0f:40:f1:ff:de:c0:61:aa:88:3c:7b:1c:bc: - 89:be:40:2c:73:de:69:1c:80:a9:99:5e:53:c6:35:63:a4:cb: - db:65:ea:a6:5c:5b:ca:62:06:c1:35:f4:1c:c4:54:eb:02:f6: - 40:7a:1a:98:4a:e6:f2:24:72:77:cb:f0:05:14:54:72:ce:10: - 28:a8:47:61:ef:13:d1:ae:0f:87:06:f5:46:b7:a1:47:91:77: - 84:82:16:45:31:84:98:3c:88:8d:9f:3f:a7:e7:cb:61:25:eb: - 91:2c:6b:91:e6:c9:ba:7a:90:eb:7a:92:60:3a:cf:53:9e:87: - e5:a5:6b:01:5c:56:b4:13:fd:9f:d2:d5:ac:28:89:f3:23:67: - 29:d0:cc:da:b1:fe:41:ea:0f:19:06:49:f9:fc:55:a6:a1:63: - 40:9e:79:0c:f4:af:ba:d8:4a:4f:ef:d5:4a:2e:c1:51:89:2d: - 06:90:67:3b:d9:b5:82:4f:f5:a2:09:61:49:1c:a0:04:93:e1: - 73:8a:71:54:ff:e6:5b:03:02:ed:06:ec:ca:26:29:10:5f:db: - 92:ea:a5:f6:f2:02:46:86:c9:1b:1f:80:ab:0a:55:76:be:01: - f8:aa:21:ba:86:0b:ec:ed:f6:53:f6:ba:72:ac:41:7a:fe:3d: - 2e:a4:c7:32:70:54:e1:7b:d8:90:e4:81:ec:fc:fe:f2:b5:fd: - 83:68:92:6c:5b:33:ae:2f:e3:db:d7:0e:fa:4e:17:24:12:bf: - 3d:a1:9e:e3:09:11:8f:05:1a:8f:87:fe:4c:d6:45:3c:35:74: - 71:90:bc:c0:5b:bb:7b:7b:2a:23:ae:07:3f:50:ce:64:6d:74: - 36:ec:7d:a4:a5:07:bf:5e:d5:84:6b:63:b0:89:1e:e4:a8:1e: - 8c:42:00:0f:20:8d:1c:52:52:bc:3c:90:c5:ee:36:f8:f1:c8: - 20:f0:ce:50:3d:9f:46:9e:72:fe:1c:54:c7:be:b0:53:51:6d: - 61:dc:13:23:84:f6:1d:f9:15:c6:95:59:1d:90:48:9f:f1:c7: - 2c:c5:07:00:22:1f:99:ae:42:22:13:5b:c2:06:2c:8f:b5:1c: - 17:28:12:b5:f0:4b:74:fd -----BEGIN CERTIFICATE----- MIIFpzCCA4+gAwIBAgIBATANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJNWDEQ MA4GA1UECAwHRHVyYW5nbzEQMA4GA1UEBwwHRHVyYW5nbzEOMAwGA1UECgwFRGlu @@ -238,36 +64,3 @@ I64HP1DOZG10Nux9pKUHv17VhGtjsIke5KgejEIADyCNHFJSvDyQxe42+PHIIPDO UD2fRp5y/hxUx76wU1FtYdwTI4T2HfkVxpVZHZBIn/HHLMUHACIfma5CIhNbwgYs j7UcFygStfBLdP0= -----END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIFszCCA5ugAwIBAgIUf9d8X/YRCU5n5AvzG9UkfqSGPR0wDQYJKoZIhvcNAQEL -BQAwaTELMAkGA1UEBhMCTVgxEDAOBgNVBAgMB0R1cmFuZ28xEDAOBgNVBAcMB0R1 -cmFuZ28xDjAMBgNVBAoMBURpbmdvMRQwEgYDVQQLDAtEZXZlbG9wbWVudDEQMA4G -A1UEAwwHVGVzdCBDQTAeFw0yNTEyMTUxNDE4MzFaFw00MjA1MjAxNDE4MzFaMGkx -CzAJBgNVBAYTAk1YMRAwDgYDVQQIDAdEdXJhbmdvMRAwDgYDVQQHDAdEdXJhbmdv -MQ4wDAYDVQQKDAVEaW5nbzEUMBIGA1UECwwLRGV2ZWxvcG1lbnQxEDAOBgNVBAMM -B1Rlc3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDxBXxOVL1G -ETYQletNJi/7Jm3BSwxWHB8zvGgRYjyTX8K5J7eKDWBi0rq/m6LXtb2ZvHN040u3 -fnftYheDIrzn716yGM1M4KzUfNPCwGJqc8Um/KlGYENIyhE+EKECVgQeXtAgTuR9 -7RoTR54zP7/NCJ5S2ujSUGjZUB7P4la0PZ6XD4d6ObxXqgzSabXKTSDnkjqpV87q -dN8mbvlsfZBW1CTP/kRUIBoljTqEygy8vbevJrEAg0tut8xdqh2Mv7A1mSfDNsXK -Q77YTKhgHj0RJcaeJFNMRhOYUb2CZn58aDQBETGzECB0bBtm8wSqYa5baYrtUos9 -x2cB8I+3CxNd/6N53Aq4/5JzPmvBdlCCq67ZUX4f1xcAVSrB8ZXjUn6sPX2hxt2I -BNi/g3ortKDyfvY1nB9WinfuAehpro8nxIw6lkcv2ZZUjc2KzK1ibXX8x6NCMDBA -Ret7LGZ29OWuyNZQYD1CJGRlUAGScayaoJrCzXVYh2fuEYzRPdQl8nN6ilgm+LlX -g4pQmog5EsOtLoYKFKXIawQ3WbkjFrKLG5bwxTQ5U1r9dG1exbxFzH1RTbtK9uBW -43uoKF+5DhrBwI5+WDrYygO0b3c0RPTPCAVCjlXKEG1B72YD/Rh9qrFexV3tS3ym -ygm0oMqkKAJlP6bVgqzDNrAmXiDpN5s2xQIDAQABo1MwUTAdBgNVHQ4EFgQUOmqK -8DEcJnX8muN+ctIyorp7XcUwHwYDVR0jBBgwFoAUOmqK8DEcJnX8muN+ctIyorp7 -XcUwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEA01iPhpxezVE5 -kZ4Yis1ZzGZFnQrT5yXoeHEzmEn4ZPyHZbzbVZq+4fakA8DgYMe0kLVmmCzZa93g -KH0RkmOOM5qCPhZAL+skPp6tVS6qNEuSmjW9nUmY+w41n/Tk2ISeVGeInfAVtsBM -leEBymFOqKTApjDqtgH3oi9yWSkTd1rilYw3NGiPjMqeOFEpSQ04bHmD69+995qM -BXQywEsX5dCMtoKIJ5wY+zVMtXdLGNEi/N08kvwToRTb64THbf4keNpTPsT2VARh -SoJ8YjbdZPtPbevceEax9SehKJLgElLVBDvDq78UiyGqXwRI7MkwMgxK1XL87mWc -IleAULvuk8qwmpJ2xb9JKzVUnvFTSh9kZLFZ18Aa/K/1jlI9pAKr5N0h0lozQj7h -yqWYWQkUk8wT7DRRdsFzlfR+UfUktlMAaUfw1+D+Pia7W8lIORhwz0gk0axQQVVd -uW/lr2Dn8O5UB318p/GTFy6TRueQnDquaU7pN452zfUvZPGG/O0eNbIwf6Q0dZIe -kdcG8BfVgNE3aC5+liuRe/nQ1V/f+CDCGWO0GTsKyldpuo/86x7VuTw/VjQ7r682 -TgMe9VN+aDe2juGG+voJkuE6DeLAkhvFWpFlwdCSbSiepSBC6COmeW3UhVXdDj8Q -MkYnRqy5y31l5FMHS3CAEYOeGDcehjU= ------END CERTIFICATE----- diff --git a/example/default.yml b/example/default.yml index 3fc8bca..febdbe5 100644 --- a/example/default.yml +++ b/example/default.yml @@ -47,11 +47,35 @@ listeners: listen-ip: 0.0.0.0 listen-port: 8443 - ## The certificate chain offered to clients + ## The certificate chain offered to clients. Nothing here is verified: + ## we don't build a path against a trust store, check a signature or look + ## at an expiry date, so a chain with a gap in it starts and fails at the + ## client instead. The one check is OpenSSL's, that servkey belongs to + ## the certificate we're serving, and that one refuses to start. + ## + ## servcert is the server's own certificate and cacerts holds the + ## intermediates above it. We work upwards from servcert, taking the + ## listed certificate whose subject matches the issuer we're looking for, + ## so the order of the list doesn't matter, and anything left over is + ## named in a warning and not sent. + ## + ## Two things to know before writing the list. One file, one certificate: + ## we read the first certificate in each of these files and stop there, so + ## a bundle holding several has to be split up first, and since the ones + ## after the first are never read, nothing warns that they were skipped. + ## And leave the root out, since a client that doesn't already trust it + ## has no use for a copy and one that does has it already, so sending it + ## costs a certificate's worth of handshake and buys nothing. The root is + ## on the path upwards, so if you do list it, it goes out. + ## ## If all you have is a self-signed server certificate, put it in - ## cert-chain instead of servcert, and leave servcert empty + ## cert-chain instead of servcert, and leave servcert and cacerts empty. + ## That is a supported way to run: the certificate goes out on its own + ## and the client decides what to make of it, which for openssl s_client + ## is "18 (self-signed certificate)" unless it has been given the same + ## file. Listing that certificate in cacerts as well sends it twice, + ## since it is its own issuer. cacerts: - - cacert.pem - intcert.pem servcert: servcert.pem ## Anything group or other can read, write or execute here draws a warning @@ -73,7 +97,14 @@ listeners: # listen-ip: 0.0.0.0 # listen-port: 8443 - # ## The certificate chain offered to clients + # ## The certificate chain offered to clients, as one file holding the + # ## server certificate first, then its issuer, then that certificate's + # ## issuer, and so on, stopping one short of the root. We send the file + # ## as written rather than sorting it, so a file out of order reaches + # ## the client out of order, a root left in is sent for no benefit, and + # ## an unrelated certificate in there is sent too. A file whose first + # ## certificate isn't the server's own is caught only when servkey then + # ## doesn't match it, which is a refusal to start. # cert-chain: chain.pem # servkey: servkey.pem @@ -87,6 +118,6 @@ listeners: # listen-ip: 0.0.0.0 # listen-port: 42023 - # ## The certificate chain offered to clients + # ## Leaf first, then each issuer in turn, stopping before the root # cert-chain: chain.pem # servkey: servkey.pem diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a77b8c6..43a700e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -64,11 +64,14 @@ add_test(NAME integration_signals ${TLSProxy_SOURCE_DIR}/example) set_tests_properties(integration_signals PROPERTIES TIMEOUT 60) +# This one makes its own certificate rather than taking one from example/, +# which has no self-signed leaf with a key beside it, so it needs openssl(1) +# and reports 77 when it cannot find it. add_test(NAME integration_selfsigned COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/integration/selfsigned.sh - $ - ${TLSProxy_SOURCE_DIR}/example) -set_tests_properties(integration_selfsigned PROPERTIES TIMEOUT 60) + $) +set_tests_properties(integration_selfsigned PROPERTIES TIMEOUT 60 + SKIP_RETURN_CODE 77) add_test(NAME integration_pid1 COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/integration/pid1.sh diff --git a/test/configs/goodconf1.yml b/test/configs/goodconf1.yml index b35c2de..6350e6e 100644 --- a/test/configs/goodconf1.yml +++ b/test/configs/goodconf1.yml @@ -21,7 +21,6 @@ listeners: ## The certificate chain offered to clients cacerts: - - cacert.pem - intcert.pem servcert: servcert.pem servkey: servkey.pem diff --git a/test/integration/reload_fds.sh b/test/integration/reload_fds.sh index e24fd52..c0542b1 100755 --- a/test/integration/reload_fds.sh +++ b/test/integration/reload_fds.sh @@ -45,7 +45,6 @@ listeners: listen-ip: 127.0.0.1 listen-port: $1 cacerts: - - $CERTS/cacert.pem - $CERTS/intcert.pem servcert: $CERTS/servcert.pem servkey: $2 diff --git a/test/integration/reload_lock.sh b/test/integration/reload_lock.sh index 4cb015c..7d063c5 100755 --- a/test/integration/reload_lock.sh +++ b/test/integration/reload_lock.sh @@ -46,7 +46,6 @@ listeners: listen-ip: 127.0.0.1 listen-port: $1 cacerts: - - $CERTS/cacert.pem - $CERTS/intcert.pem servcert: $CERTS/servcert.pem servkey: $2 diff --git a/test/integration/reload_reports.sh b/test/integration/reload_reports.sh index 0f9c3d2..8092e17 100755 --- a/test/integration/reload_reports.sh +++ b/test/integration/reload_reports.sh @@ -44,7 +44,6 @@ listeners: listen-ip: 127.0.0.1 listen-port: $1 cacerts: - - $CERTS/cacert.pem - $CERTS/intcert.pem servcert: $CERTS/servcert.pem servkey: $2 diff --git a/test/integration/selfsigned.sh b/test/integration/selfsigned.sh index e823ced..949ee8b 100755 --- a/test/integration/selfsigned.sh +++ b/test/integration/selfsigned.sh @@ -1,44 +1,65 @@ #!/usr/bin/env bash # -# Integration test for #68: a listener whose cert-chain holds a leaf with no -# issuer above it must start, since that is what -# SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR was asked for. +# A listener whose cert-chain holds one self-signed certificate starts, +# accepts, and sends that certificate and nothing else. Nothing verifies what +# the operator named, so a leaf with no issuer above it is not an error here; +# what a client makes of it is the client's business, and openssl(1) reports +# 18, self-signed certificate, unless it has been handed the same file. # -# Usage: selfsigned.sh +# Usage: selfsigned.sh # # init_openssl() lives in app/main.c, which is linked into the executable and -# not into the library the cmocka binaries use, so the only way to see what it -# does with a chain of one is to run the program. +# not into the library the cmocka binaries use, so running the program is the +# only way to see what it does with a chain of one. # -# The fixture is the example leaf on its own rather than a generated -# self-signed certificate, which keeps the openssl(1) binary out of the -# dependencies. Both take the same path: X509_verify_cert() fails, the flag -# turns that into a success, and the chain left over the leaf is empty, so -# SSL_CTX_build_cert_chain() returns 2 rather than 1. A chain that does carry -# an intermediate returns 1 even when errors were ignored, since the security -# level loop in ssl_build_cert_chain() overwrites the 2. +# openssl(1) does three jobs here: it makes the certificate, since example/ +# has no self-signed leaf with a key beside it; it holds the backend port +# open, since the proxy defers the client handshake until its own connect to +# the backend has finished, so a refused backend leaves nothing to read; and +# it reads back what came over the wire. Without it the test exits 77 and +# CTest records a skip. set -u BIN=$1 -CERTS=$2 + +command -v openssl >/dev/null 2>&1 || { + echo "SKIP: openssl(1) is not on PATH" + exit 77 +} RUN=$(mktemp -d) LOG=$RUN/tlsproxy.log MASTER= +BACKEND= + +cleanup() { + [ -n "$MASTER" ] && kill -KILL "$MASTER" 2>/dev/null + [ -n "$BACKEND" ] && kill -KILL "$BACKEND" 2>/dev/null + wait 2>/dev/null + rm -rf "$RUN" +} +trap cleanup EXIT fail() { echo "FAIL: $*" - [ -n "$MASTER" ] && kill -KILL "$MASTER" 2>/dev/null [ -f "$LOG" ] && { echo "--- log ---"; cat "$LOG"; } [ -f "$RUN/stderr.txt" ] \ && { echo "--- stderr ---"; cat "$RUN/stderr.txt"; } exit 1 } -# $1 is the listen port. The schema rejects listen-port 0, so we cannot ask -# the kernel for a free one; start on a port derived from the pid and walk -# upwards, which keeps concurrent runs off each other. +# An EC key so that generating one costs nothing worth measuring, and a day of +# validity so the fixture cannot be the reason a later run fails +openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ + -keyout "$RUN/servkey.pem" -out "$RUN/servcert.pem" -days 1 \ + -subj "/CN=selfsigned.test" >/dev/null 2>&1 \ + || fail "openssl could not generate the fixture certificate" +chmod 600 "$RUN/servkey.pem" + +# $1 is the listen port and $2 the backend's. The schema rejects listen-port 0, +# so we cannot ask the kernel for a free one; start on a port derived from the +# pid and walk upwards, which keeps concurrent runs off each other. write_config() { cat > "$RUN/tlsproxy.yml" </dev/null 2>&1 & + BACKEND=$! + ( cd "$RUN" && exec "$BIN" tlsproxy.yml ) \ >"$RUN/stdout.txt" 2>"$RUN/stderr.txt" & MASTER=$! # The listen event is logged by a worker, so seeing it in the file means # the master got past init_openssl() and into parent_loop() draining the - # ring. Before #68 was fixed the master died in init_openssl() instead and - # this loop ran out with the process gone. + # ring. A master that refused the certificate dies in init_openssl() + # instead and this loop runs out with the process gone. started= for _ in $(seq 50); do grep -q 'event=listen' "$LOG" 2>/dev/null && { started=1; break; } @@ -75,22 +105,15 @@ for try in 0 1 2 3 4 5 6 7 8 9; do sleep 0.1 done [ -n "$started" ] && break + kill -KILL "$MASTER" 2>/dev/null - wait "$MASTER" 2>/dev/null + kill -KILL "$BACKEND" 2>/dev/null + wait "$MASTER" "$BACKEND" 2>/dev/null MASTER= + BACKEND= done [ -n "$MASTER" ] || fail "the proxy would not start on any candidate port" -echo "master $MASTER on port $PORT" - -grep -q 'Failed to build cert chain' "$LOG" \ - && fail "a chain of one was treated as a failure" - -# The 2 is worth a line of its own, since the chain we are about to offer is -# shorter than the file the operator named implied. The message is matched -# rather than the level, since the example key's mode draws a WARN of its own -# here and either one would satisfy a check on the level alone. -grep -q 'level=WARN.*error_msg="Building cert chain"' "$LOG" \ - || fail "nothing warned that the chain was built with errors ignored" +echo "master $MASTER on port $PORT, backend on $BACK" # Logging the listener and accepting on it are different things, and only the # second one is what the operator asked for. @@ -98,13 +121,34 @@ grep -q 'level=WARN.*error_msg="Building cert chain"' "$LOG" \ || fail "listener on port $PORT is not accepting" exec 3>&- +WIRE=$(timeout 10 openssl s_client -connect "127.0.0.1:$PORT" -showcerts \ + /dev/null) +NCERTS=$(printf '%s' "$WIRE" | grep -c 'BEGIN CERTIFICATE') +[ "$NCERTS" = 1 ] \ + || fail "expected the one certificate the file holds, got $NCERTS" + +# With one certificate in the reply the range covers exactly that certificate +printf '%s' "$WIRE" | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' \ + > "$RUN/wire.pem" +SENT=$(openssl x509 -in "$RUN/wire.pem" -noout -fingerprint -sha256) +NAMED=$(openssl x509 -in "$RUN/servcert.pem" -noout -fingerprint -sha256) +[ "$SENT" = "$NAMED" ] \ + || fail "the certificate sent is not the one named: $SENT vs $NAMED" + +# A chain of one is a configuration we accept rather than one we tolerate, so +# it should have produced nothing to report at all. +grep -q 'level=ERROR\|level=FATAL' "$LOG" \ + && fail "a self-signed leaf was reported as an error" + kill -TERM "$MASTER" 2>/dev/null for _ in $(seq 50); do kill -0 "$MASTER" 2>/dev/null || break sleep 0.1 done kill -KILL "$MASTER" 2>/dev/null -wait "$MASTER" 2>/dev/null +kill -KILL "$BACKEND" 2>/dev/null +wait "$MASTER" "$BACKEND" 2>/dev/null +MASTER= +BACKEND= -rm -rf "$RUN" echo "PASS" diff --git a/test/integration/signals.sh b/test/integration/signals.sh index ef1e3ca..4198ab6 100755 --- a/test/integration/signals.sh +++ b/test/integration/signals.sh @@ -42,7 +42,6 @@ listeners: listen-ip: 127.0.0.1 listen-port: $1 cacerts: - - $CERTS/cacert.pem - $CERTS/intcert.pem servcert: $CERTS/servcert.pem servkey: $CERTS/servkey.pem