|
| 1 | +# Running a standalone test instance (CA + one witness cosigner) |
| 2 | + |
| 3 | +This describes a single-process cactus deployment that issues |
| 4 | +certificates carrying **the CA cosignature plus one witness |
| 5 | +cosignature**, so an ACME client can request standalone and |
| 6 | +landmark-relative certificates and you can verify them against the log. |
| 7 | + |
| 8 | +A single cactus process is always a full CA (issuance log + ACME server |
| 9 | ++ CA cosigner). The `mirror` block adds **one** witness/mirror cosigner |
| 10 | +in the same process, and `ca_cosigner_quorum` makes the CA collect that |
| 11 | +witness's cosignature — over loopback — for every subtree it signs. With |
| 12 | +`min_signatures: 1`, each issued standalone certificate ends up with two |
| 13 | +cosignatures: the CA's and the witness's. |
| 14 | + |
| 15 | +> **This is not real transparency.** A witness operated by the CA on the |
| 16 | +> same host proves nothing a relying party should trust — independence is |
| 17 | +> the whole point of a witness. This setup exists to exercise the |
| 18 | +> cosignature wire formats and the relying-party verification paths on a |
| 19 | +> public *test* instance. See [threat-model.md](threat-model.md). |
| 20 | +
|
| 21 | +The sample config is [`config-witness-example.json`](../config-witness-example.json). |
| 22 | + |
| 23 | +## How the pieces wire together |
| 24 | + |
| 25 | +``` |
| 26 | + ┌──────────────────────── one cactus process ───────────────────────┐ |
| 27 | + ACME client ─┼─▶ :14000 ACME ──▶ issuance log ──▶ CA cosigner (id 44363.47.1.99)│ |
| 28 | + │ │ signs each subtree │ |
| 29 | + │ :14080 monitoring (tiles, checkpoint, /landmarks) │ |
| 30 | + │ ▲ │ CA quorum requester │ |
| 31 | + │ │ follows ▼ POST /sign-subtree │ |
| 32 | + │ witness follower ──▶ :14081 witness cosigner (id 44363.47.2.1) │ |
| 33 | + └─────────────────────────────────────────────────────────────────────┘ |
| 34 | +``` |
| 35 | + |
| 36 | +- The witness **follows** the CA's own log over loopback |
| 37 | + (`mirror.upstream.tile_url` = `http://127.0.0.1:14080/1`) and verifies |
| 38 | + the CA's checkpoint cosignatures. |
| 39 | +- On each checkpoint the CA's quorum requester sends a `sign-subtree` |
| 40 | + request — including its own CA cosignature — to the witness over |
| 41 | + loopback (`ca_cosigner_quorum.mirrors[0].url` = |
| 42 | + `http://127.0.0.1:14081/sign-subtree`). `require_ca_signature_on_subtree` |
| 43 | + can stay `true`: the CA supplies that cosignature. |
| 44 | +- All identifiers use the **relative** trust-anchor-ID form (the arcs |
| 45 | + below the `1.3.6.1.4.1` enterprise base). The log ID is the CA ID with |
| 46 | + `.0.<log number>` appended, e.g. `44363.47.1.99.0.1`. |
| 47 | + |
| 48 | +## Setup |
| 49 | + |
| 50 | +```sh |
| 51 | +# 0. Build (cactus needs Go 1.27 / gotip; see README). |
| 52 | +make build # ./bin/cactus, cactus-cli, cactus-keygen |
| 53 | + |
| 54 | +# 1. Pick a data dir and create the keys dir. |
| 55 | +export DATA_DIR=/tmp/cactus-data |
| 56 | +mkdir -p "$DATA_DIR/keys" |
| 57 | + |
| 58 | +# 2. Generate two distinct cosigner seeds — one for the CA, one for the |
| 59 | +# witness. They MUST differ (the config validator enforces this). |
| 60 | +./bin/cactus-keygen -o "$DATA_DIR/keys/ca-cosigner.seed" |
| 61 | +./bin/cactus-keygen -o "$DATA_DIR/keys/witness-cosigner.seed" |
| 62 | + |
| 63 | +# 3. Start from the sample config and point it at $DATA_DIR. |
| 64 | +cp config-witness-example.json config.json |
| 65 | +sed -i "s|/tmp/cactus-data|$DATA_DIR|" config.json |
| 66 | + |
| 67 | +# 4. Fill in the two public keys. `-pub` prints the PEM block whose body |
| 68 | +# is the raw public key — exactly what the config fields take. |
| 69 | +# - ca_cosigner.seed -> mirror.upstream.ca_cosigner_key_pem |
| 70 | +# - witness seed -> ca_cosigner_quorum.mirrors[0].public_key_pem |
| 71 | +./bin/cactus-keygen -pub -o "$DATA_DIR/keys/ca-cosigner.seed" |
| 72 | +./bin/cactus-keygen -pub -o "$DATA_DIR/keys/witness-cosigner.seed" |
| 73 | +# Paste each PEM into the matching field of config.json (a JSON string, |
| 74 | +# newlines escaped as \n, ending in \n). |
| 75 | + |
| 76 | +# 5. Run. |
| 77 | +./bin/cactus -config config.json |
| 78 | +``` |
| 79 | + |
| 80 | +On startup the witness follower briefly logs one `connection refused` |
| 81 | +while the monitoring listener finishes binding, then catches up; that is |
| 82 | +harmless. `curl http://localhost:14090/metrics | grep cactus_mirror` |
| 83 | +shows `cactus_mirror_consistency_failures_total 0` once it is healthy. |
| 84 | + |
| 85 | +### Public exposure |
| 86 | + |
| 87 | +`acme.listen`/`monitoring.listen` are the public surfaces; set their |
| 88 | +`external_url` to whatever clients reach (cactus speaks plaintext HTTP, |
| 89 | +so terminate TLS at a reverse proxy if you need HTTPS, and set |
| 90 | +`external_url` to the public `https://…`). Keep `metrics.listen` and |
| 91 | +`mirror.sign_subtree_listen` on `127.0.0.1` — they are internal. The |
| 92 | +loopback URLs inside the `mirror`/`ca_cosigner_quorum` blocks must stay |
| 93 | +`127.0.0.1` regardless of the public `external_url`. |
| 94 | + |
| 95 | +`challenge_mode: auto-pass` makes every authorization instantly valid, so |
| 96 | +anyone who can reach the ACME port gets a cert for any name — fine for a |
| 97 | +test instance, but switch to `http-01` if that matters. |
| 98 | + |
| 99 | +## Issuing certificates |
| 100 | + |
| 101 | +Use any RFC 8555 client against `<acme external_url>/directory`, e.g.: |
| 102 | + |
| 103 | +```sh |
| 104 | +lego --server http://localhost:14000/directory \ |
| 105 | + --email you@example.com --domains example.test \ |
| 106 | + --accept-tos --pem --path ./certs run |
| 107 | +``` |
| 108 | + |
| 109 | +The order's `certificate` URL is the **standalone** cert (CA + witness |
| 110 | +cosignatures). The `Link: …; rel="alternate"` header on the finalize and |
| 111 | +certificate responses points at the **landmark-relative** variant |
| 112 | +(`/cert/{id}/alternate`), which is signature-free and verifies against |
| 113 | +predistributed landmark subtree hashes. Both cert URLs are POST-as-GET |
| 114 | +resources (RFC 8555 §6.3), so an ACME client — not a plain `curl` — |
| 115 | +retrieves them; the alternate returns `503 + Retry-After` until a |
| 116 | +covering landmark exists. With `time_between_landmarks_ms: 60000` a |
| 117 | +covering landmark appears within ~a minute of issuance; lower it for |
| 118 | +snappier testing (at the cost of more active landmarks). |
| 119 | + |
| 120 | +## Verifying against the log |
| 121 | + |
| 122 | +`cactus-cli` talks to the monitoring base plus the log number — here |
| 123 | +`http://localhost:14080/1`: |
| 124 | + |
| 125 | +```sh |
| 126 | +# Current signed checkpoint (origin / size / root). |
| 127 | +curl http://localhost:14080/1/checkpoint |
| 128 | +./bin/cactus-cli tree show http://localhost:14080/1 |
| 129 | + |
| 130 | +# Walk every tile, recompute the root, compare to the signed checkpoint. |
| 131 | +./bin/cactus-cli tree verify http://localhost:14080/1 |
| 132 | + |
| 133 | +# Inspect one entry (N = tree size - 1 for the most recent). |
| 134 | +./bin/cactus-cli entry http://localhost:14080/1 N |
| 135 | + |
| 136 | +# Full §7.2 verification of an issued cert against the live log: splits |
| 137 | +# the cert, decodes the MTCProof, recomputes the leaf hash, evaluates the |
| 138 | +# inclusion proof, and compares to the log's subtree hash. Prints OK. |
| 139 | +# Works for both the standalone and the landmark-relative certificate |
| 140 | +# (the inclusion-proof check is independent of the cosignatures). |
| 141 | +./bin/cactus-cli cert verify ./certs/example.test.crt http://localhost:14080/1 |
| 142 | + |
| 143 | +# Machine-readable inclusion proof for scripting. |
| 144 | +./bin/cactus-cli prove http://localhost:14080/1 N | jq . |
| 145 | +``` |
| 146 | + |
| 147 | +The standalone certificate's `MTCProof` carries two cosignatures (the CA |
| 148 | +and the witness, sorted by cosigner ID); the landmark-relative one |
| 149 | +carries none. `cactus-cli cert verify` confirms either is correctly |
| 150 | +logged; trusting the *witness* cosignature specifically is a |
| 151 | +relying-party policy decision and requires the witness public key |
| 152 | +(`cactus-keygen -pub -o keys/witness-cosigner.seed`). |
0 commit comments