From ab5b0d11dae565f9bdd7ec2a0d0a45e95489170c Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 21:54:13 +0200 Subject: [PATCH] The certificate bed orders the same certificate from a second authority, the catalogue's own Issue 020 could not tell a Pebble interop detail from a fault of the proxy's. The bed now raises step-ca as the catalogue pins it and orders again through the same proxy and the same challenge path (novox/hq 04-ISSUES/020). --- test/integration/certificates.test.ts | 63 ++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/test/integration/certificates.test.ts b/test/integration/certificates.test.ts index 1fe8e64..eaadfd0 100644 --- a/test/integration/certificates.test.ts +++ b/test/integration/certificates.test.ts @@ -41,6 +41,14 @@ const ACME = "/var/lib/acme"; */ const AUTHORITY = "ghcr.io/letsencrypt/pebble:2.5.0"; +/** + * The second implementation, for the same order (novox/hq 04-ISSUES/020): the certificate + * authority the catalogue itself runs, pinned as the catalogue pins it. If the order, the challenge + * and the handshake agree here as well as against Pebble, the one thing 020 could not rule out — a + * Pebble interop detail — is ruled out; and if they disagree, which side differs is in view. + */ +const SECOND_AUTHORITY = "smallstep/step-ca@sha256:a2b17872915c193259b75a5474c398326f41bd199f0842093e52cf4182bc8270"; + let instanceId = ""; function shellQuote(s: string): string { @@ -185,6 +193,59 @@ test("a public name is served with a certificate the mesh did not issue", { assert.match(issuer, new RegExp(NAME), `the certificate is not for the name asked for:\n${issuer}`); }); +test("the same order against a second authority: the catalogue's own certificate authority", { + skip, timeout: 900_000, +}, async () => { + // The proxy that served the first test goes; its cache with it, or the certificate Pebble issued + // would be served again and nothing would have been ordered here. + await must(`pkill -f mesh-route-proxy || true; sleep 1; mkdir -p ${ACME}/cache2`); + + // The catalogue's authority, as the catalogue runs it: ACME on, listening on its own port, a + // root and an intermediate made at first start. It resolves the name through the machine's + // resolver, which reads the hosts entry the first test wrote. + await must( + `docker run -d --name stepca --network host ` + + `-e DOCKER_STEPCA_INIT_NAME="Lab CA" -e DOCKER_STEPCA_INIT_DNS_NAMES=localhost,127.0.0.1 ` + + `-e DOCKER_STEPCA_INIT_ACME=true -e DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT=false ` + + `-e DOCKER_STEPCA_INIT_PASSWORD=lab-only-password ${SECOND_AUTHORITY}`, + ); + let ready = false; + for (let i = 0; i < 90 && !ready; i++) { + ({ ok: ready } = await on(`docker exec stepca test -s /home/step/certs/root_ca.crt && curl -sk https://127.0.0.1:9000/health -o /dev/null`)); + if (!ready) await new Promise((r) => setTimeout(r, 2000)); + } + assert.ok(ready, `the second authority never came up:\n${(await on(`docker logs stepca 2>&1 | tail -30`)).out}`); + await must(`docker exec stepca cat /home/step/certs/root_ca.crt > ${ACME}/stepca-root.pem`); + + await must( + `ROUTES=${ACME}/routes.json LISTEN=:80 TLS_LISTEN=:443 ` + + `ACME_CACHE=${ACME}/cache2 ` + + `ACME_DIRECTORY=https://127.0.0.1:9000/acme/acme/directory ` + + `ACME_CA_BUNDLE=${ACME}/stepca-root.pem ` + + `nohup /usr/local/bin/mesh-route-proxy >${ACME}/proxy2.log 2>&1 & sleep 3`, + ); + + let served = { out: "", ok: false }; + for (let i = 0; i < 40 && !served.ok; i++) { + served = await on(`curl -sf --cacert ${ACME}/stepca-root.pem https://${NAME}/ `); + if (!served.ok) await new Promise((r) => setTimeout(r, 2000)); + } + if (!served.ok) { + const proxyLog = (await on(`cat ${ACME}/proxy2.log`)).out; + const authority = (await on(`docker logs stepca 2>&1 | tail -40`)).out; + assert.fail( + `the name was never served over TLS from the second authority: ${served.out}\n\n` + + `── the proxy tried:\n${proxyLog}\n── the authority heard:\n${authority}\n`); + } + assert.match(served.out, /hello/); + const issuer = await must( + `echo | openssl s_client -connect ${NAME}:443 -servername ${NAME} 2>/dev/null ` + + `| openssl x509 -noout -issuer -subject`, + ); + assert.match(issuer, /Lab CA/, `the certificate was not issued by the second authority:\n${issuer}`); + assert.match(issuer, new RegExp(NAME), `the certificate is not for the name asked for:\n${issuer}`); +}); + test("no certificate is ordered for a name the mesh does not route", { skip, timeout: 300_000, }, async () => { @@ -193,6 +254,6 @@ test("no certificate is ordered for a name the mesh does not route", { const { out } = await on( `echo | openssl s_client -connect 127.0.0.1:443 -servername nobody-asked-for-this.example 2>&1 | head -20`, ); - assert.doesNotMatch(out, /Pebble/i, + assert.doesNotMatch(out, /Pebble|Lab CA/i, `a certificate was obtained for a name nothing routes here:\n${out}`); });