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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//! Secrets: where the SSH host key and the issuer private key live when there
//! is no home directory (the secret-store seam).
//!
//! # The inventory is two keys, and the header used to claim a third
//!
//! Measured 2026-08-10 across every non-test caller in the tree:
//! [`SSH_HOST_KEY`] and [`ISSUER_PRIVATE_KEY`]. That is all of it. This header
//! said "the admin bootstrap material" until then, and it had not been true
//! since the bootstrap admin started leaving with a certificate instead of a
//! stored token.
//!
//! # Where an implementation of this lives now
//!
//! Not behind `RepoStore::secrets()`, which is deleted, and not in a storage
//! engine. The server's implementation is `gunnar_core::RedbSecretStore`, a
//! table in the same `control.redb` as the catalog and the identity table. This
//! crate keeps the **trait** — because it is the vocabulary
//! `gunnar_core::SealedSecretStore` decorates and the conformance suite checks —
//! plus [`crate::mem::MemSecretStore`] as the control and
//! [`crate::arrow_log::ArrowLogSecretStore`] as the legacy store the migration
//! reads out of.
//!
//! The trait stays here rather than moving to `gunnar-core` for one hard
//! reason and one soft one. The hard one: `arrow_log.rs` implements it, and
//! `gunnar-store` cannot depend on `gunnar-core` — that edge runs the other
//! way. The soft one: it is a contract with three implementations and a
//! conformance check, which is what this crate is for.
use crateResult;
/// Key under which the server's persistent SSH host key is stored.
///
/// An appliance that regenerates this on every boot gives every client a
/// host-key-changed warning, which on an escape-hatch product is exactly the
/// wrong first impression. [`SecretStore::get_or_create`] exists to make that
/// impossible.
pub const SSH_HOST_KEY: &str = "ssh/host_ed25519";
/// Key under which **the server's X.509 issuer private key** is stored, PEM
/// encoded (the certificate-identity plan, `gunnar_core::issuer`).
///
/// A key that can mint any user is the one secret whose loss is total — an
/// attacker holding it does not break authentication, they issue themselves an
/// administrator. The control database holds the issuer *certificate*, which is
/// public; this names the half that is not.
///
/// Until 2026-08-10 that separation was also a separation of **files** — the
/// certificate in `control.redb`, the key in an Arrow log inside `control.git`.
/// It is not any more: both are in `control.redb`, in different tables. The
/// property that actually protects the key was never the file boundary, it is
/// the seal: the server writes this only through
/// `gunnar_core::SealedSecretStore`, so what reaches any backend is ciphertext
/// under a KEK that lives in `secrets.kek` beside the database and is not in
/// it. A copied `control.redb` still cannot issue a certificate.
pub const ISSUER_PRIVATE_KEY: &str = "pki/issuer_key";
/// Key under which **the transparency log's signing key** is stored, OpenSSH
/// private-key PEM (`gunnar_core::transparency`).
///
/// Its own key and not the issuer's, deliberately. The issuer signs
/// *certificates*, which say who somebody is; this signs *tree heads*, which say
/// what the log held. Sharing one key would mean that compromising the log's
/// signer also mints administrators — and it would make revoking one of the two
/// jobs impossible without revoking the other.
///
/// ed25519 and `sshsig`, because that is the one signature format in this
/// repository: commits, control-plane challenges and now tree heads. A client
/// can check a tree head with `ssh-keygen -Y verify` and no gunnar at all.
///
/// Sealed under the KEK like every other secret, so a copied `control.redb`
/// cannot sign a tree head that a verifier would believe.
pub const TRANSPARENCY_LOG_KEY: &str = "transparency/log_ed25519";
/// Key under which **the builder's provenance signing key** is stored, OpenSSH
/// private-key PEM (`gunnar_core::provenance`).
///
/// A third key, and a third job. The issuer says who somebody is, the
/// transparency key says what the log held, and this says what a build
/// produced. Three keys means a compromise of one is a compromise of one — and
/// it means the builder can be a different machine from the log, which is what
/// a runner on its own host needs.
pub const PROVENANCE_KEY: &str = "provenance/builder_ed25519";
/// **The outbound webhook signing key.** A fourth key, minted on first use and
/// sealed under the KEK, whose PUBLIC half is what a webhook receiver pins to
/// verify a delivery with `ssh-keygen -Y verify`. Its own key and not the
/// issuer's or the log's: signing a delivery is a different job from issuing a
/// certificate or attesting a build, and a key per job means a compromise of
/// one is a compromise of one.
pub const WEBHOOK_KEY: &str = "webhook/signing_ed25519";
/// **The actor-ticket signing key.** A fifth key, minted on first use by the
/// METERED appliance alone (`ServerStores::actor_ticket_key`) and sealed under
/// the KEK like the rest. Its PUBLIC half is what the appliance advertises over
/// `Billing.ApplianceIdentity` and what the other side of that seam pins to
/// check a ticket the appliance minted.
///
/// # Why it is not the SSH host key
///
/// Reusing the host key would have cost nothing to write and is exactly the
/// kind of thing that is fine until it is not. The host key's signatures are
/// produced inside the SSH protocol's own framing and the `sshsig` envelope,
/// both of which carry a domain separator; a raw Ed25519 signature over a JSON
/// document carries none. One key signing in two formats, one of which has no
/// domain separation at the signature layer, is a cross-protocol confusion
/// waiting for the day somebody makes one format's bytes parseable as the
/// other's. A key per job also means a compromise of one is a compromise of
/// one — the same sentence the four keys above are each here for.
///
/// # Why it costs no installer pin
///
/// It is minted AT RUNTIME into the secret store this box already has, exactly
/// as the webhook and provenance keys are. The alternative that was weighed —
/// shipping the key, or its configuration, in the image — would have put a new
/// value into two separate manifests that pin the installer, and a key that can
/// only be rolled by re-cutting an ISO is a key nobody rolls. Nothing about
/// this one reaches the image.
///
/// The name says what it signs and not what it is for: a store this name lands
/// in is present in every build, and only the metered one ever mints under it.
pub const ACTOR_TICKET_KEY: &str = "actor/ticket_ed25519";
/// **The forge-attestation key** (GHUNT-19): a sixth key, minted on first use
/// (`ServerStores::forge_attest_key`) and sealed under the KEK like the rest.
/// It signs a forge event a person made from an identity-core session that
/// stepped up, which holds no key of theirs: gunnar's word that the session's
/// fresh passkey touch stood behind the event, the actor named. Its own key and
/// not the log's or the issuer's, so a compromise of one is a compromise of one,
/// and it is never a principal's key in the identity table.
pub const FORGE_ATTEST_KEY: &str = "forge/attest_ed25519";
/// Small, durable, mutable key/value storage for things that are not objects
/// and not references.
///
/// The `mk` callback is `&mut dyn FnMut` rather than `impl FnOnce` so the trait
/// stays object-safe. The server holds a `Arc<dyn SecretStore>` and picks the
/// backend at boot. [`SecretStoreExt::get_or_create_with`] gives back the
/// `FnOnce` ergonomics the spec writes, with no loss: the closure is still
/// called at most once.
///
/// A `VaultSecretStore` is *noted, not built*. The trait is shaped so it
/// can be added without a signature change.
/// The ergonomic half of [`SecretStore`], kept off the trait so the trait stays
/// object-safe.