gunnar_core/issuer.rs
1//! **The server is its own certificate issuer** (the certificate-identity plan).
2//!
3//! There is no external PKI to run and no second system to keep in step:
4//! `gunnar issuer create` mints a CA, and after that *creating a user is issuing
5//! their certificate*.
6//!
7//! ```text
8//! gunnar issuer create mint the CA
9//! gunnar issuer import --cert C --key K adopt one that exists
10//! gunnar issuer show what is live
11//! gunnar principal add alice --csr alice.csr alice's key stays with alice
12//! gunnar principal add alice --server-generated-key the convenient, weaker form
13//! ```
14//!
15//! # Where each half of the issuer lives, and why they are not together
16//!
17//! | half | where | why |
18//! |---|---|---|
19//! | the **certificate** | a control document in redb, [`ISSUER_REF`] | it is public, and it is what the trusted-CA list needs |
20//! | the **private key** | the [`SecretStore`], sealed under the KEK, at [`gunnar_store::secret::ISSUER_PRIVATE_KEY`] | a key that can mint any user is the one secret whose loss is total |
21//!
22//! An attacker holding the issuer key does not have to *break* authentication.
23//! They issue themselves an administrator. So it goes exactly where the SSH host
24//! key already goes — through [`crate::seal::SealedSecretStore`], which encrypts
25//! on the way in — and **never into redb in the clear**. `tests/issuer.rs`
26//! asserts that by reading the control database file back off disk as bytes and
27//! searching it, rather than by asking an API that declines to return the key.
28//!
29//! # Exactly one issuer
30//!
31//! Not a list, not a default among several. A second issuer is a second answer
32//! to *who may mint a user*, and the failure mode of two is that nobody can say
33//! which one signed a given client. [`Pki::create`] on a server that has one is
34//! **refused** — [`Error::IssuerExists`] — not silently ignored. Replacing it is
35//! `issuer rotate`, a different verb, because it invalidates every certificate
36//! in the field.
37//!
38//! # The trusted-CA list stays a list, and the issuer is one entry in it
39//!
40//! Verifying against many and minting with one are different questions and must
41//! not share a field. A rotation needs the old CA and the new one trusted at the
42//! same instant, or every client in the field is locked out at the swap. So
43//! [`PkiDoc`] has two fields: `issuer`, an `Option` of exactly one, and
44//! `trusted`, a list. They ride in **one document** so that "the issuer was
45//! created" and "the issuer is trusted" are one compare-and-swap and cannot be
46//! interrupted between — the same reason `BootstrapRecord` sits inside
47//! `AuthDoc`.
48//!
49//! # Why `rcgen` and not `cert-helper`
50//!
51//! The certificate-identity plan names `cert-helper`, and `bench/src/trust.rs`
52//! uses it. It cannot be used *here*: `cert-helper` is built on `openssl`, and
53//! `openssl`/`openssl-sys` are on `xtask::guard::FORBIDDEN_ANYWHERE` — the
54//! dependency budget bans them from both halves of the build, and the appliance
55//! is a static musl binary in a distroless image with no system libssl to link
56//! against. `bench/` is a detached workspace the guard does not resolve, which
57//! is why `trust` may keep its openssl and this may not.
58//!
59//! That split is worth more than it costs. `trust` and the tests in
60//! `tests/issuer.rs` verify what this module mints using **`openssl` the
61//! binary**, which shares no code with `rcgen`. A library confirming its own
62//! output is the circularity that lets a wrong chain through.
63//!
64//! # The server decides every field except the public key
65//!
66//! A CSR is a request, not an instruction. [`Pki::issue_from_csr`] takes exactly
67//! one thing out of it — the **subject public key**, which is the whole point of
68//! a CSR — and overwrites everything else: the subject name becomes the
69//! principal that was asked for, the SANs are dropped, `is_ca` is forced off,
70//! and the key usages are set to client authentication. A CSR that claims
71//! `CN=admin` while `gunnar principal add alice` is running produces a
72//! certificate for **alice**, and `tests/issuer.rs` reads the issued subject
73//! back with `openssl x509 -subject` to prove it.
74
75use std::sync::Arc;
76
77use rcgen::{
78 BasicConstraints, CertificateParams, CertificateSigningRequestParams, DistinguishedName,
79 DnType, DnValue, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SerialNumber,
80};
81use time::{Duration, OffsetDateTime};
82
83use crate::auth::{certificate_der, fingerprint_of_certificate, Fingerprint};
84use crate::control::ControlStore;
85use crate::doc::DocRef;
86use crate::error::{Error, Result};
87use crate::model::{now_ms, UnixMillis};
88use gunnar_store::secret::ISSUER_PRIVATE_KEY;
89use gunnar_store::{SecretStore, SecretStoreExt};
90
91/// The control document holding the issuer certificate and the trusted-CA list.
92///
93/// Named `refs/gunnar/…` like [`crate::AUTH_REF`] and
94/// [`crate::CATALOG_REF`]: these stopped being git references when the control
95/// plane moved to redb (the certificate-identity plan), but they are what every error message and
96/// runbook calls these documents.
97pub const ISSUER_REF: &str = "refs/gunnar/pki";
98
99/// How long a freshly minted issuer is good for, in days.
100///
101/// Ten years. An issuer that expires takes every certificate in the field with
102/// it, and the recovery is a rotation of the whole estate — so the default is
103/// long enough that it is a planned event rather than an outage, and
104/// `--days` is there for a deployment with a shorter policy.
105pub const DEFAULT_ISSUER_DAYS: u32 = 3650;
106
107/// How long an issued client certificate is good for, in days.
108pub const DEFAULT_LEAF_DAYS: u32 = 365;
109
110/// A serial number is 20 bytes of CSPRNG (RFC 5280 allows at most 20).
111const SERIAL_LEN: usize = 20;
112
113// ── what is stored ───────────────────────────────────────────────────────────
114
115/// How the live issuer got here.
116#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum IssuerOrigin {
119 /// `gunnar issuer create` — the server minted it and has never seen another
120 /// copy of the key.
121 Created,
122 /// `gunnar issuer import` — the key arrived from somewhere else, so
123 /// somewhere else may still have it.
124 Imported,
125}
126
127impl std::fmt::Display for IssuerOrigin {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 f.write_str(match self {
130 IssuerOrigin::Created => "created",
131 IssuerOrigin::Imported => "imported",
132 })
133 }
134}
135
136/// The live issuer, as [`Pki::issuer`] reports it.
137///
138/// The **certificate only**. There is deliberately no field here that could
139/// carry the private key: the type an operator prints, a gRPC response
140/// serialises and a log line might capture must be structurally incapable of
141/// holding it.
142#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
143#[serde(deny_unknown_fields)]
144pub struct Issuer {
145 /// Its subject common name.
146 pub subject: String,
147 /// The certificate, PEM-armoured. Public by definition.
148 pub cert_pem: String,
149 /// [`fingerprint_of_certificate`] over its DER — the same `SHA256:` space
150 /// every other credential in gunnar lives in.
151 pub fingerprint: Fingerprint,
152 /// When this server adopted it.
153 pub created: UnixMillis,
154 /// Minted here, or handed to us.
155 pub origin: IssuerOrigin,
156}
157
158/// One entry in the trusted-CA list.
159#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
160#[serde(deny_unknown_fields)]
161pub struct TrustedCa {
162 /// An operator-chosen label. Never part of the credential.
163 pub label: String,
164 /// The CA certificate, PEM-armoured.
165 pub cert_pem: String,
166 /// Its fingerprint, which is what `untrust` takes.
167 pub fingerprint: Fingerprint,
168 /// When it was added.
169 pub added: UnixMillis,
170 /// **This entry is the server's own issuer.** Set by [`Pki::create`] and
171 /// [`Pki::import`], and what stops [`Pki::untrust_ca`] from removing the CA
172 /// the server is currently minting with.
173 pub is_issuer: bool,
174}
175
176/// A certificate this server issued.
177#[derive(Clone, PartialEq, Eq, Debug)]
178pub struct Issued {
179 /// The principal it was issued for. Equal to [`Issued::common_name`] by
180 /// construction — the certificate-identity plan takes the identity from the CN.
181 pub principal_id: String,
182 /// The subject common name that was actually written into the certificate.
183 pub common_name: String,
184 /// The certificate, PEM-armoured.
185 pub cert_pem: String,
186 /// Its DER, which is what [`crate::AuthTable::register_certificate`] takes.
187 pub cert_der: Vec<u8>,
188 /// Its fingerprint.
189 pub fingerprint: Fingerprint,
190 /// **The private key, and only for `--server-generated-key`.**
191 ///
192 /// `None` on the CSR path, and that `None` is the security property: on that
193 /// path the key never existed here, so there is nothing to hand back, log,
194 /// or leak. See [`Pki::issue_with_server_key`] for what taking `Some` costs.
195 pub key_pem: Option<String>,
196}
197
198/// The stored document: one issuer, and a list of trusted CAs.
199#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
200#[serde(deny_unknown_fields)]
201struct PkiDoc {
202 /// `None` until `issuer create` or `issuer import` has run.
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 issuer: Option<Issuer>,
205 /// Every CA a client certificate may chain to. The issuer is one of them.
206 #[serde(default)]
207 trusted: Vec<TrustedCa>,
208}
209
210/// No sharded maps, and that is a decision rather than an oversight.
211///
212/// [`crate::AuthTable`]'s maps are sharded because a forge holds every
213/// principal, key and grant on the server in one document and was parsing all
214/// of it per connection. This document holds **one** issuer and, in the
215/// worst case a rotation produces, two or three CAs. A trie over three rows is
216/// a shard probe and a second blob fetch to save nothing.
217impl crate::doc::Doc for PkiDoc {
218 fn bind(&mut self, _store: &Arc<dyn ControlStore>) {}
219 fn flush(&mut self) -> Result<()> {
220 Ok(())
221 }
222}
223
224// ── the issuer ───────────────────────────────────────────────────────────────
225
226/// **The server's certificate authority**: create it, import it, and issue with
227/// it.
228///
229/// Cheap to clone; every clone is the same document and the same secret store.
230#[derive(Clone)]
231pub struct Pki {
232 doc: DocRef<PkiDoc>,
233 secrets: Arc<dyn SecretStore>,
234}
235
236impl std::fmt::Debug for Pki {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 f.write_str("Pki")
239 }
240}
241
242impl Pki {
243 /// Open the PKI held in `control`, with private keys in `secrets`.
244 ///
245 /// **`secrets` must already be sealed.** Hand it the
246 /// [`SealedSecretStore`](crate::seal::SealedSecretStore) the server built at
247 /// boot, never a raw backend: this type writes the one key on the machine
248 /// that is worth stealing, and it cannot tell from the trait object whether
249 /// what it wrote was encrypted. That is why `ServerStores::pki()` is the
250 /// only constructor call on the server's boot path.
251 pub fn new(control: Arc<dyn ControlStore>, secrets: Arc<dyn SecretStore>) -> Result<Self> {
252 Ok(Pki {
253 doc: DocRef::new(control, ISSUER_REF)?,
254 secrets,
255 })
256 }
257
258 /// **A [`Pki`] that can read the issuer and the CA list and cannot reach a
259 /// key**, for a process that carries no storage engine.
260 ///
261 /// The whole point of moving the control plane to redb (the certificate-identity plan) is that
262 /// the catalog and the identity table are readable without an engine. The
263 /// issuer *certificate* is a control document and belongs to that set;
264 /// `gunnar issuer show` must therefore work on a client-shaped binary. The
265 /// private key does not — it is in the secret log inside the control
266 /// repository, which does need an engine.
267 ///
268 /// So this is a constructor and not a flag: the store it installs refuses
269 /// every call, which makes "`show` cannot touch the key" a property of the
270 /// value rather than of whoever remembered not to call `create` on it.
271 pub fn read_only(control: Arc<dyn ControlStore>) -> Result<Self> {
272 Pki::new(control, Arc::new(NoSecrets))
273 }
274
275 /// The name this PKI is stored under.
276 pub fn ref_name(&self) -> &str {
277 self.doc.ref_name()
278 }
279
280 /// The live issuer, or `None` on a server that has never had one.
281 pub fn issuer(&self) -> Result<Option<Issuer>> {
282 Ok(self.doc.load()?.issuer.clone())
283 }
284
285 /// Every CA a client certificate may chain to, oldest first.
286 pub fn trusted_cas(&self) -> Result<Vec<TrustedCa>> {
287 Ok(self.doc.load()?.trusted.clone())
288 }
289
290 /// **Mint this server's issuer.** Refused if it already has one.
291 ///
292 /// # The ordering, and what a crash between the two writes leaves behind
293 ///
294 /// The private key goes in first, through
295 /// [`SecretStore::get_or_create`], which is atomic across racing callers —
296 /// so two concurrent `issuer create` calls mint **one** key and both see the
297 /// same bytes. Then the certificate is committed with the document's
298 /// compare-and-swap, which is where the second caller is refused.
299 ///
300 /// A crash between the two leaves a key with no certificate. That is the
301 /// harmless direction: [`Pki::issuer`] still says `None`, the next `issuer
302 /// create` adopts the orphaned key rather than minting a second one, and no
303 /// certificate anybody holds was invalidated. The other order — certificate
304 /// first — would leave a live issuer whose key cannot be found, and every
305 /// `principal add` after it would fail.
306 pub fn create(&self, subject: &str, valid_days: u32) -> Result<Issuer> {
307 if let Some(existing) = self.issuer()? {
308 return Err(Error::IssuerExists {
309 subject: existing.subject,
310 fingerprint: existing.fingerprint.as_str().to_owned(),
311 });
312 }
313
314 let key_pem = self.ensure_issuer_key()?;
315 let key_pair = parse_key(&key_pem)?;
316
317 let mut params = CertificateParams::default();
318 params.distinguished_name = distinguished_name(subject);
319 params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
320 params.key_usages = vec![
321 KeyUsagePurpose::KeyCertSign,
322 KeyUsagePurpose::CrlSign,
323 KeyUsagePurpose::DigitalSignature,
324 ];
325 params.serial_number = Some(random_serial()?);
326 let (not_before, not_after) = validity(valid_days)?;
327 params.not_before = not_before;
328 params.not_after = not_after;
329
330 let certificate = params
331 .self_signed(&key_pair)
332 .map_err(|err| Error::Pki(format!("self-signing the issuer: {err}")))?;
333 let cert_pem = certificate.pem();
334 let issuer = Issuer {
335 subject: subject.to_owned(),
336 fingerprint: fingerprint_of_certificate(certificate.der()),
337 cert_pem: cert_pem.clone(),
338 created: now_ms(),
339 origin: IssuerOrigin::Created,
340 };
341
342 self.install(issuer)
343 }
344
345 /// **Adopt an issuer that already exists.** Refused if this server has one.
346 ///
347 /// Both halves are checked against each other before anything is written: a
348 /// certificate whose SubjectPublicKeyInfo is not this key's public key is
349 /// **refused**, because the alternative is a server that accepts the pair,
350 /// stores both, and only discovers on the first `principal add` that it can
351 /// mint nothing — by which time the operator has moved on.
352 pub fn import(&self, cert_pem: &str, key_pem: &str) -> Result<Issuer> {
353 if let Some(existing) = self.issuer()? {
354 return Err(Error::IssuerExists {
355 subject: existing.subject,
356 fingerprint: existing.fingerprint.as_str().to_owned(),
357 });
358 }
359
360 let key_pair = parse_key(key_pem)?;
361 let der = certificate_der(cert_pem.as_bytes())?;
362 let spki = subject_public_key_info(&der)?;
363 if spki != rcgen::PublicKeyData::subject_public_key_info(&key_pair) {
364 return Err(Error::Pki(
365 "this certificate and this private key are not each other's: the certificate's \
366 SubjectPublicKeyInfo is not the public half of the key. Nothing was stored"
367 .to_owned(),
368 ));
369 }
370
371 // Parsed so an unusable certificate is refused here rather than on the
372 // first `principal add`. `signing_handle` reloads it exactly this way.
373 rcgen::Issuer::from_ca_cert_pem(cert_pem, &key_pair)
374 .map_err(|err| Error::Pki(format!("reading the imported issuer certificate: {err}")))?;
375 let subject =
376 certificate_common_name(&der)?.unwrap_or_else(|| "<no common name>".to_owned());
377
378 let issuer = Issuer {
379 subject,
380 fingerprint: fingerprint_of_certificate(&der),
381 cert_pem: cert_pem.to_owned(),
382 created: now_ms(),
383 origin: IssuerOrigin::Imported,
384 };
385
386 // The certificate first here, and the key second, which is the opposite
387 // of `create` — because the key is the caller's and `get_or_create`
388 // would silently keep an orphan from an interrupted earlier attempt
389 // instead of storing what was handed over. The compare-and-swap is what
390 // makes "this server has no issuer" true at the instant of the write, so
391 // the key that lands afterwards cannot overwrite a live issuer's.
392 let installed = self.install(issuer)?;
393 self.secrets
394 .put(ISSUER_PRIVATE_KEY, key_pem.as_bytes())
395 .map_err(Error::from)?;
396 Ok(installed)
397 }
398
399 /// Commit an issuer into the document, and add it to the trusted-CA list in
400 /// the same compare-and-swap.
401 fn install(&self, issuer: Issuer) -> Result<Issuer> {
402 let entry = TrustedCa {
403 label: issuer.subject.clone(),
404 cert_pem: issuer.cert_pem.clone(),
405 fingerprint: issuer.fingerprint.clone(),
406 added: issuer.created,
407 is_issuer: true,
408 };
409 self.doc.update(|doc| {
410 // Re-checked INSIDE the transaction. The check at the top of
411 // `create` is the good error message; this one is the guarantee.
412 // `DocRef::update` re-reads and re-runs on a lost race, so a second
413 // caller that got past the first check finds the winner here.
414 if let Some(existing) = &doc.issuer {
415 return Err(Error::IssuerExists {
416 subject: existing.subject.clone(),
417 fingerprint: existing.fingerprint.as_str().to_owned(),
418 });
419 }
420 doc.issuer = Some(issuer.clone());
421 doc.trusted.retain(|ca| ca.fingerprint != entry.fingerprint);
422 doc.trusted.push(entry.clone());
423 Ok(issuer.clone())
424 })
425 }
426
427 /// Trust a CA this server does not mint with.
428 ///
429 /// The other half of a rotation: old and new both trusted while clients are
430 /// re-issued. Re-adding one already in the list replaces its label rather
431 /// than duplicating the entry.
432 pub fn trust_ca(&self, label: &str, cert_pem: &str) -> Result<TrustedCa> {
433 let der = certificate_der(cert_pem.as_bytes())?;
434 x509_parser::parse_x509_certificate(&der)
435 .map_err(|err| Error::Pki(format!("reading the CA certificate: {err}")))?;
436 let entry = TrustedCa {
437 label: label.to_owned(),
438 cert_pem: cert_pem.to_owned(),
439 fingerprint: fingerprint_of_certificate(&der),
440 added: now_ms(),
441 is_issuer: false,
442 };
443 self.doc.update(|doc| {
444 let is_issuer = doc
445 .issuer
446 .as_ref()
447 .is_some_and(|i| i.fingerprint == entry.fingerprint);
448 let mut entry = entry.clone();
449 entry.is_issuer = is_issuer;
450 doc.trusted.retain(|ca| ca.fingerprint != entry.fingerprint);
451 doc.trusted.push(entry.clone());
452 Ok(entry)
453 })
454 }
455
456 /// Stop trusting a CA. `Ok(None)` if it was not in the list.
457 ///
458 /// **Refuses to remove the live issuer's own entry.** A server that mints
459 /// with a CA it does not trust issues certificates it will then refuse, and
460 /// nothing about the failure would point back here. Rotate first, which
461 /// changes which entry is the issuer, and then remove the old one.
462 pub fn untrust_ca(&self, fingerprint: &Fingerprint) -> Result<Option<TrustedCa>> {
463 self.doc.update(|doc| {
464 let Some(at) = doc
465 .trusted
466 .iter()
467 .position(|ca| &ca.fingerprint == fingerprint)
468 else {
469 return Ok(None);
470 };
471 if doc.trusted[at].is_issuer {
472 return Err(Error::Pki(format!(
473 "{fingerprint} is this server's own issuer; removing it from the trusted \
474 list would leave a server minting certificates it refuses. Rotate the \
475 issuer first"
476 )));
477 }
478 Ok(Some(doc.trusted.remove(at)))
479 })
480 }
481
482 /// **Issue a certificate from a CSR.** The default, and the strong form.
483 ///
484 /// `common_name` is the principal the operator asked for, and it is what
485 /// lands in the subject — **not** whatever the CSR claimed. See the module
486 /// documentation: the only thing taken out of the request is the public key.
487 ///
488 /// The private key never existed on this server, so [`Issued::key_pem`] is
489 /// `None` and there is nothing here to leak.
490 pub fn issue_from_csr(
491 &self,
492 common_name: &str,
493 csr_pem: &str,
494 valid_days: u32,
495 ) -> Result<Issued> {
496 // Parsing VERIFIES the request's self-signature (rcgen's
497 // `CertificateSigningRequestParams::from_der` calls `verify_signature`),
498 // which is the proof of possession: a CSR carrying somebody else's
499 // public key does not sign, and does not get here.
500 let mut request = CertificateSigningRequestParams::from_pem(csr_pem).map_err(|err| {
501 Error::Pki(format!(
502 "this is not a usable certificate signing request: {err}. A CSR that does not \
503 verify against its own public key proves nothing about who holds the private \
504 half, which is the only thing it exists to prove"
505 ))
506 })?;
507
508 harden(&mut request.params, common_name, valid_days)?;
509
510 let issuer = self.signing_handle()?;
511 let certificate = request
512 .signed_by(&issuer)
513 .map_err(|err| Error::Pki(format!("signing the request for {common_name}: {err}")))?;
514
515 Ok(self.issued(
516 common_name,
517 certificate.pem(),
518 certificate.der().to_vec(),
519 None,
520 ))
521 }
522
523 /// **Issue a certificate whose private key this server generated.**
524 ///
525 /// Named `--server-generated-key` on the command line rather than being what
526 /// an operator gets by not thinking about it, because it is the weaker
527 /// shape and the weakness is not visible from the outside:
528 ///
529 /// * the key existed here and travelled over the wire, so the server could
530 /// have kept a copy and nothing in the protocol proves it did not;
531 /// * it is in a log, a core dump or a proxy's buffer if anything upstream is
532 /// careless;
533 /// * *"only alice could have signed this"* stops being true.
534 ///
535 /// It exists because a machine account being bootstrapped, or a human who
536 /// cannot run the tooling, genuinely needs it.
537 pub fn issue_with_server_key(&self, common_name: &str, valid_days: u32) -> Result<Issued> {
538 let key_pair = KeyPair::generate()
539 .map_err(|err| Error::Pki(format!("minting a key for {common_name}: {err}")))?;
540
541 let mut params = CertificateParams::default();
542 harden(&mut params, common_name, valid_days)?;
543
544 let issuer = self.signing_handle()?;
545 let certificate = params
546 .signed_by(&key_pair, &issuer)
547 .map_err(|err| Error::Pki(format!("signing a certificate for {common_name}: {err}")))?;
548
549 Ok(self.issued(
550 common_name,
551 certificate.pem(),
552 certificate.der().to_vec(),
553 Some(key_pair.serialize_pem()),
554 ))
555 }
556
557 /// Mint a **server** identity for the smart-HTTP listener: leaf certificate
558 /// with `hosts` as subject alternative names, signed by this issuer, key
559 /// generated here and never stored.
560 ///
561 /// Deliberately NOT [`harden`], which is the CLIENT shape — it clears SANs
562 /// and pins the EKU to `ClientAuth` precisely so a minted client
563 /// certificate can never be presented as a server for a host it does not
564 /// own. A server identity is the opposite trade: SANs are the whole point
565 /// (a TLS client matches the name it dialled against them) and the EKU is
566 /// `ServerAuth` and nothing else, so this certificate cannot come back in
567 /// a client's role either. `is_ca` stays forced off.
568 ///
569 /// This is what makes gunnar a **single executable with its own TLS**: the
570 /// appliance already mints its issuer on first start, clients already pin
571 /// that issuer (`gunnar issuer show --pem`), and this leaf closes the
572 /// chain without an operator ever touching a PEM.
573 pub fn issue_server_identity(&self, hosts: &[String], valid_days: u32) -> Result<Issued> {
574 if hosts.is_empty() {
575 return Err(Error::Pki(
576 "a server certificate needs at least one host name or address for its subject \
577 alternative names; a SAN-less server certificate matches nothing a client dials"
578 .to_owned(),
579 ));
580 }
581 let key_pair = KeyPair::generate()
582 .map_err(|err| Error::Pki(format!("minting a server key: {err}")))?;
583 let common_name = &hosts[0];
584 let mut params = CertificateParams::new(hosts.to_vec())
585 .map_err(|err| Error::Pki(format!("server certificate SANs {hosts:?}: {err}")))?;
586 params.distinguished_name = distinguished_name(common_name);
587 params.is_ca = IsCa::ExplicitNoCa;
588 params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
589 params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
590 params.use_authority_key_identifier_extension = true;
591 params.serial_number = Some(random_serial()?);
592 let now = OffsetDateTime::now_utc();
593 params.not_before = now - NOT_BEFORE_SLACK;
594 params.not_after = now + Duration::days(i64::from(valid_days));
595
596 let issuer = self.signing_handle()?;
597 let certificate = params
598 .signed_by(&key_pair, &issuer)
599 .map_err(|err| Error::Pki(format!("signing the server certificate: {err}")))?;
600 Ok(self.issued(
601 common_name,
602 certificate.pem(),
603 certificate.der().to_vec(),
604 Some(key_pair.serialize_pem()),
605 ))
606 }
607
608 fn issued(
609 &self,
610 common_name: &str,
611 cert_pem: String,
612 cert_der: Vec<u8>,
613 key_pem: Option<String>,
614 ) -> Issued {
615 Issued {
616 principal_id: common_name.to_owned(),
617 common_name: common_name.to_owned(),
618 fingerprint: fingerprint_of_certificate(&cert_der),
619 cert_pem,
620 cert_der,
621 key_pem,
622 }
623 }
624
625 /// The issuer certificate and its private key, ready to sign with.
626 ///
627 /// The certificate is rebuilt from the stored PEM rather than kept in
628 /// memory: `signed_by` reads the issuer's distinguished name, key usages and
629 /// key-identifier method off it, and those must come from what is on disk —
630 /// otherwise a certificate issued after a restart could chain to something
631 /// slightly different from what the CA list hands out.
632 fn signing_handle(&self) -> Result<rcgen::Issuer<'static, KeyPair>> {
633 let issuer = self.issuer()?.ok_or(Error::NoIssuer)?;
634 let key_pem = self
635 .secrets
636 .get(ISSUER_PRIVATE_KEY)
637 .map_err(Error::from)?
638 .ok_or_else(|| {
639 Error::Pki(format!(
640 "this server has an issuer certificate ({}) but no private key for it at \
641 {ISSUER_PRIVATE_KEY}. It can verify certificates and issue none",
642 issuer.subject
643 ))
644 })?;
645 let key_pem = String::from_utf8(key_pem)
646 .map_err(|_| Error::Pki("the stored issuer key is not utf-8 PEM".to_owned()))?;
647 let key_pair = parse_key(&key_pem)?;
648
649 let der = certificate_der(issuer.cert_pem.as_bytes())?;
650 if subject_public_key_info(&der)?
651 != rcgen::PublicKeyData::subject_public_key_info(&key_pair)
652 {
653 return Err(Error::Pki(
654 "the stored issuer certificate and the stored issuer key do not go together"
655 .to_owned(),
656 ));
657 }
658 rcgen::Issuer::from_ca_cert_pem(&issuer.cert_pem, key_pair)
659 .map_err(|err| Error::Pki(format!("reading the stored issuer certificate: {err}")))
660 }
661
662 /// The issuer's private key, minting one on first use.
663 ///
664 /// Atomic across racing callers, exactly as
665 /// [`crate::host_key::ensure_host_key`] is: two `issuer create` calls at
666 /// once produce one key, not two.
667 fn ensure_issuer_key(&self) -> Result<String> {
668 let bytes = self
669 .secrets
670 .get_or_create_with(ISSUER_PRIVATE_KEY, || {
671 tracing::info!(
672 key = ISSUER_PRIVATE_KEY,
673 "no issuer private key stored; minting one. It is sealed under the KEK and \
674 never enters the control database"
675 );
676 let pair = KeyPair::generate()
677 .map_err(|err| gunnar_store::Error::backend(format!("{err}")))?;
678 Ok(pair.serialize_pem().into_bytes())
679 })
680 .map_err(Error::from)?;
681 String::from_utf8(bytes)
682 .map_err(|_| Error::Pki("the stored issuer key is not utf-8 PEM".to_owned()))
683 }
684}
685
686/// The secret store [`Pki::read_only`] installs: it refuses everything.
687///
688/// Not a store that answers `None`. `None` means *no issuer key is stored*,
689/// which a caller would reasonably act on by minting one — and a `create` that
690/// silently succeeded against a store that discards writes would leave a server
691/// holding a certificate it can never sign with.
692struct NoSecrets;
693
694impl NoSecrets {
695 fn refuse() -> gunnar_store::Error {
696 gunnar_store::Error::backend(
697 "this handle can read the issuer certificate and the trusted-CA list and cannot \
698 reach a private key. Issuing needs the sealed secret store, and this handle was \
699 built without one on purpose",
700 )
701 }
702}
703
704impl SecretStore for NoSecrets {
705 fn get(&self, _key: &str) -> gunnar_store::Result<Option<Vec<u8>>> {
706 Err(Self::refuse())
707 }
708 fn put(&self, _key: &str, _value: &[u8]) -> gunnar_store::Result<()> {
709 Err(Self::refuse())
710 }
711 fn delete(&self, _key: &str) -> gunnar_store::Result<()> {
712 Err(Self::refuse())
713 }
714 fn get_or_create(
715 &self,
716 _key: &str,
717 _mk: &mut dyn FnMut() -> gunnar_store::Result<Vec<u8>>,
718 ) -> gunnar_store::Result<Vec<u8>> {
719 Err(Self::refuse())
720 }
721}
722
723// ── the parts that decide what a certificate says ────────────────────────────
724
725/// Overwrite everything a CSR may have asked for except the public key.
726///
727/// This is the whole of "the server decides". Every field set here is one an
728/// unchecked CSR could otherwise have chosen for itself:
729///
730/// * **the subject** becomes `common_name`, so a CSR claiming `CN=admin` is
731/// issued as whoever the operator named;
732/// * **subject alternative names are dropped**, because a `dNSName` SAN is how a
733/// client certificate becomes usable as a *server* certificate for a host it
734/// does not own;
735/// * **`is_ca` is forced off** — a CSR carrying `basicConstraints CA:TRUE` would
736/// otherwise ask for the right to mint users;
737/// * **key usage** is digital signature and client authentication and nothing
738/// else, so a stolen client certificate cannot be presented in another role.
739fn harden(params: &mut CertificateParams, common_name: &str, valid_days: u32) -> Result<()> {
740 if common_name.is_empty() {
741 return Err(Error::Pki(
742 "a certificate needs a common name; the certificate-identity scheme \
743 takes the identity from it"
744 .to_owned(),
745 ));
746 }
747 params.distinguished_name = distinguished_name(common_name);
748 params.subject_alt_names.clear();
749 params.is_ca = IsCa::ExplicitNoCa;
750 params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
751 params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
752 params.name_constraints = None;
753 params.custom_extensions.clear();
754 params.crl_distribution_points.clear();
755 params.use_authority_key_identifier_extension = true;
756 params.serial_number = Some(random_serial()?);
757 let (not_before, not_after) = validity(valid_days)?;
758 params.not_before = not_before;
759 params.not_after = not_after;
760 Ok(())
761}
762
763/// A distinguished name carrying one common name, and nothing else.
764fn distinguished_name(common_name: &str) -> DistinguishedName {
765 let mut dn = DistinguishedName::new();
766 dn.push(DnType::CommonName, common_name);
767 dn
768}
769
770/// The common name out of a distinguished name, if it has one.
771///
772/// Public because the *claimed* name in a CSR is a thing a caller may want to
773/// log or refuse on — `gunnar-cli` prints it when it differs from the principal
774/// being issued, so an operator can see that a request asked for something else.
775pub fn common_name(dn: &DistinguishedName) -> Option<String> {
776 match dn.get(&DnType::CommonName)? {
777 DnValue::Utf8String(text) => Some(text.clone()),
778 DnValue::PrintableString(text) => Some(text.as_str().to_owned()),
779 DnValue::Ia5String(text) => Some(text.as_str().to_owned()),
780 DnValue::TeletexString(text) => Some(String::from_utf8_lossy(text.as_bytes()).into_owned()),
781 // BMPString and UniversalString are UCS-2 and UTF-32; gunnar never
782 // writes either, and a principal id is not expressible in them without
783 // a transcoding step that would be a second definition of "the name".
784 // `None` here means "this certificate does not name a principal gunnar
785 // can match", which is the refusing direction.
786 _ => None,
787 }
788}
789
790/// The common name a certificate signing request claims, for the caller that
791/// wants to report it. Never what gets issued — see [`harden`].
792pub fn csr_claimed_common_name(csr_pem: &str) -> Result<Option<String>> {
793 let request = CertificateSigningRequestParams::from_pem(csr_pem)
794 .map_err(|err| Error::Pki(format!("this is not a usable request: {err}")))?;
795 Ok(common_name(&request.params.distinguished_name))
796}
797
798/// `[now, now + days)`, from the wall clock.
799/// How far back a certificate's `not_before` is set (see
800/// `not_before_tests`): a day, so a box whose clock is stepped back after it
801/// minted still holds valid certificates.
802pub const NOT_BEFORE_SLACK: Duration = Duration::hours(24);
803
804fn validity(valid_days: u32) -> Result<(OffsetDateTime, OffsetDateTime)> {
805 if valid_days == 0 {
806 return Err(Error::Pki(
807 "a certificate valid for zero days is expired the moment it is signed".to_owned(),
808 ));
809 }
810 let now = OffsetDateTime::now_utc();
811 let until = now
812 .checked_add(Duration::days(i64::from(valid_days)))
813 .ok_or_else(|| Error::Pki(format!("{valid_days} days from now is not a date")))?;
814 Ok((now - NOT_BEFORE_SLACK, until))
815}
816
817/// 20 bytes from the system CSPRNG.
818///
819/// rcgen's default is the SHA-256 of the subject public key, which is stable
820/// **per key** — so re-issuing the same principal from the same CSR would
821/// produce two certificates sharing one serial, and a serial is what a CRL
822/// names. Random is what makes revocation able to name one of them.
823fn random_serial() -> Result<SerialNumber> {
824 use aws_lc_rs::rand::{SecureRandom as _, SystemRandom};
825
826 let mut bytes = [0u8; SERIAL_LEN];
827 SystemRandom::new().fill(&mut bytes).map_err(|_| {
828 Error::Pki("the system CSPRNG refused to produce a serial number".to_owned())
829 })?;
830 // The most significant bit clear, so it encodes as a positive 20-byte
831 // integer rather than as 21 bytes with a leading zero (RFC 5280 §4.1.2.2
832 // caps a serial at 20 octets).
833 bytes[0] &= 0x7f;
834 Ok(SerialNumber::from_slice(&bytes))
835}
836
837fn parse_key(pem: &str) -> Result<KeyPair> {
838 KeyPair::from_pem(pem)
839 .map_err(|err| Error::Pki(format!("this is not a usable private key: {err}")))
840}
841
842/// The common name a certificate's subject carries (DER), if any.
843fn certificate_common_name(der: &[u8]) -> Result<Option<String>> {
844 let (_rest, cert) = x509_parser::parse_x509_certificate(der)
845 .map_err(|err| Error::Pki(format!("this is not a usable X.509 certificate: {err}")))?;
846 let name = cert
847 .subject()
848 .iter_common_name()
849 .next()
850 .and_then(|cn| cn.as_str().ok())
851 .map(str::to_owned);
852 Ok(name)
853}
854
855/// The SubjectPublicKeyInfo DER out of a certificate, byte-for-byte as it was
856/// encoded, so it can be compared with a key's (`rcgen::PublicKeyData`).
857fn subject_public_key_info(der: &[u8]) -> Result<Vec<u8>> {
858 let (_rest, cert) = x509_parser::parse_x509_certificate(der)
859 .map_err(|err| Error::Pki(format!("this is not a usable X.509 certificate: {err}")))?;
860 Ok(cert.tbs_certificate.subject_pki.raw.to_vec())
861}
862
863/// **Mint a key pair and a certificate signing request for it, HERE.**
864///
865/// The other half of [`Pki::issue_from_csr`], for a box that has to obtain a
866/// client certificate from ANOTHER box's issuer — the twin, which is installed
867/// from the same medium as its primary and so carries nothing per box: its
868/// certificate has to be minted at pairing, and its private key has to be born
869/// on the machine that will use it and never cross the wire. What crosses is
870/// this request, which carries the public half and a self-signature that
871/// proves possession; `issue_from_csr` verifies that signature and overrides
872/// the subject, so `common_name` is a courtesy and never an authority.
873///
874/// Returns `(private key PEM, request PEM)`. ECDSA P-256 — rcgen's default and
875/// what the issuer's own leaves use, so the two ends negotiate one signature
876/// scheme.
877pub fn mint_client_request(common_name: &str) -> Result<(String, String)> {
878 let key =
879 KeyPair::generate().map_err(|err| Error::Pki(format!("minting a key pair: {err}")))?;
880 let mut params = CertificateParams::new(Vec::<String>::new())
881 .map_err(|err| Error::Pki(format!("building a signing request: {err}")))?;
882 params.distinguished_name = DistinguishedName::new();
883 params
884 .distinguished_name
885 .push(DnType::CommonName, common_name);
886 let request = params
887 .serialize_request(&key)
888 .map_err(|err| Error::Pki(format!("signing the request with its own key: {err}")))?;
889 let pem = request
890 .pem()
891 .map_err(|err| Error::Pki(format!("armouring the signing request: {err}")))?;
892 Ok((key.serialize_pem(), pem))
893}
894
895/// The SubjectPublicKeyInfo of a PEM private key, DER — to prove a certificate
896/// handed to this box was issued for THIS box's key.
897pub fn public_key_info_of_key(key_pem: &str) -> Result<Vec<u8>> {
898 Ok(rcgen::PublicKeyData::subject_public_key_info(&parse_key(
899 key_pem,
900 )?))
901}
902
903/// The SubjectPublicKeyInfo a PEM certificate carries, DER.
904pub fn public_key_info_of_cert(cert_pem: &str) -> Result<Vec<u8>> {
905 let (_rest, block) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
906 .map_err(|err| Error::Pki(format!("this is not a usable PEM certificate: {err}")))?;
907 subject_public_key_info(&block.contents)
908}
909
910/// The SubjectPublicKeyInfo a DER certificate carries, DER — which KEY a
911/// registered certificate row (`AuthTable`'s `x509` rows keep the DER) was
912/// issued for.
913pub fn public_key_info_of_der(cert_der: &[u8]) -> Result<Vec<u8>> {
914 subject_public_key_info(cert_der)
915}
916
917/// The DER inside the first PEM block of `pem`.
918pub fn pem_to_der(pem: &str) -> Result<Vec<u8>> {
919 let (_rest, block) = x509_parser::pem::parse_x509_pem(pem.as_bytes())
920 .map_err(|err| Error::Pki(format!("this is not a usable PEM certificate: {err}")))?;
921 Ok(block.contents)
922}
923
924/// Did the CA in `ca_pem` issue the certificate in `cert_pem`, **by name**?
925///
926/// A name comparison, not a signature check: it exists to refuse a CA/leaf
927/// pair that was handed over mismatched, which would otherwise surface months
928/// later as a handshake failure naming neither. The signature is checked where
929/// it matters — by rustls, at every handshake, against the pinned CA.
930/// `Err` carries both names so the refusal can print them.
931pub fn issued_by(
932 cert_pem: &str,
933 ca_pem: &str,
934) -> Result<std::result::Result<(), (String, String)>> {
935 let (cert_der, ca_der) = (pem_to_der(cert_pem)?, pem_to_der(ca_pem)?);
936 let parse = |der: &[u8], what: &str| {
937 x509_parser::parse_x509_certificate(der)
938 .map(|(_, c)| {
939 (
940 c.issuer().as_raw().to_vec(),
941 c.issuer().to_string(),
942 c.subject().as_raw().to_vec(),
943 c.subject().to_string(),
944 )
945 })
946 .map_err(|err| Error::Pki(format!("{what} is not a usable X.509 certificate: {err}")))
947 };
948 let (leaf_issuer, leaf_issuer_name, _, _) = parse(&cert_der, "the certificate")?;
949 let (_, _, ca_subject, ca_subject_name) = parse(&ca_der, "the issuer")?;
950 Ok(if leaf_issuer == ca_subject {
951 Ok(())
952 } else {
953 Err((leaf_issuer_name, ca_subject_name))
954 })
955}
956
957/// `notAfter` of a DER certificate, Unix milliseconds.
958pub fn not_after_ms(der: &[u8]) -> Result<UnixMillis> {
959 let (_rest, cert) = x509_parser::parse_x509_certificate(der)
960 .map_err(|err| Error::Pki(format!("this is not a usable X.509 certificate: {err}")))?;
961 Ok(cert.validity().not_after.timestamp() * 1000)
962}
963
964#[cfg(test)]
965mod not_before_tests {
966 use super::*;
967
968 /// **An estate's boxes mint their issuer and server identity at FIRST
969 /// OPEN, before the estate clock steps them** (MEASURED on mock-upcloud-kvm
970 /// 2026-09-29, topic recharge: guests boot +2 h like UpCloud without
971 /// kvmclock; main minted its issuer at 10:33, the front's clock stepped it
972 /// to 08:34, and the twin refused the primary's replication listener
973 /// `certificate not valid yet` for good). A certificate is valid from a day
974 /// before it is signed — the lifetime is unchanged — as Skidbladnir
975 /// nordisk-twin's NOT_BEFORE_SLACK (4613616) does for holger.
976 #[test]
977 fn a_certificate_minted_before_the_clock_stepped_back_is_already_valid() {
978 let (nb, na) = validity(1).unwrap();
979 let now = OffsetDateTime::now_utc();
980 assert!(
981 nb <= now - Duration::hours(23),
982 "not_before {nb} is not a day back from {now}"
983 );
984 assert!(
985 na >= now + Duration::hours(23),
986 "the lifetime is unchanged: {na}"
987 );
988 assert_eq!(NOT_BEFORE_SLACK, Duration::hours(24));
989 }
990}