Skip to main content

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}