Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-core/src/issuer.rs

//! **The server is its own certificate issuer** (the certificate-identity plan).
//!
//! There is no external PKI to run and no second system to keep in step:
//! `gunnar issuer create` mints a CA, and after that *creating a user is issuing
//! their certificate*.
//!
//! ```text
//!   gunnar issuer create                              mint the CA
//!   gunnar issuer import --cert C --key K             adopt one that exists
//!   gunnar issuer show                                what is live
//!   gunnar principal add alice --csr alice.csr        alice's key stays with alice
//!   gunnar principal add alice --server-generated-key the convenient, weaker form
//! ```
//!
//! # Where each half of the issuer lives, and why they are not together
//!
//! | half | where | why |
//! |---|---|---|
//! | the **certificate** | a control document in redb, [`ISSUER_REF`] | it is public, and it is what the trusted-CA list needs |
//! | 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 |
//!
//! An attacker holding the issuer key does not have to *break* authentication.
//! They issue themselves an administrator. So it goes exactly where the SSH host
//! key already goes — through [`crate::seal::SealedSecretStore`], which encrypts
//! on the way in — and **never into redb in the clear**. `tests/issuer.rs`
//! asserts that by reading the control database file back off disk as bytes and
//! searching it, rather than by asking an API that declines to return the key.
//!
//! # Exactly one issuer
//!
//! Not a list, not a default among several. A second issuer is a second answer
//! to *who may mint a user*, and the failure mode of two is that nobody can say
//! which one signed a given client. [`Pki::create`] on a server that has one is
//! **refused** — [`Error::IssuerExists`] — not silently ignored. Replacing it is
//! `issuer rotate`, a different verb, because it invalidates every certificate
//! in the field.
//!
//! # The trusted-CA list stays a list, and the issuer is one entry in it
//!
//! Verifying against many and minting with one are different questions and must
//! not share a field. A rotation needs the old CA and the new one trusted at the
//! same instant, or every client in the field is locked out at the swap. So
//! [`PkiDoc`] has two fields: `issuer`, an `Option` of exactly one, and
//! `trusted`, a list. They ride in **one document** so that "the issuer was
//! created" and "the issuer is trusted" are one compare-and-swap and cannot be
//! interrupted between — the same reason `BootstrapRecord` sits inside
//! `AuthDoc`.
//!
//! # Why `rcgen` and not `cert-helper`
//!
//! The certificate-identity plan names `cert-helper`, and `bench/src/trust.rs`
//! uses it. It cannot be used *here*: `cert-helper` is built on `openssl`, and
//! `openssl`/`openssl-sys` are on `xtask::guard::FORBIDDEN_ANYWHERE` — the
//! dependency budget bans them from both halves of the build, and the appliance
//! is a static musl binary in a distroless image with no system libssl to link
//! against. `bench/` is a detached workspace the guard does not resolve, which
//! is why `trust` may keep its openssl and this may not.
//!
//! That split is worth more than it costs. `trust` and the tests in
//! `tests/issuer.rs` verify what this module mints using **`openssl` the
//! binary**, which shares no code with `rcgen`. A library confirming its own
//! output is the circularity that lets a wrong chain through.
//!
//! # The server decides every field except the public key
//!
//! A CSR is a request, not an instruction. [`Pki::issue_from_csr`] takes exactly
//! one thing out of it — the **subject public key**, which is the whole point of
//! a CSR — and overwrites everything else: the subject name becomes the
//! principal that was asked for, the SANs are dropped, `is_ca` is forced off,
//! and the key usages are set to client authentication. A CSR that claims
//! `CN=admin` while `gunnar principal add alice` is running produces a
//! certificate for **alice**, and `tests/issuer.rs` reads the issued subject
//! back with `openssl x509 -subject` to prove it.

use std::sync::Arc;

use rcgen::{
    BasicConstraints, CertificateParams, CertificateSigningRequestParams, DistinguishedName,
    DnType, DnValue, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SerialNumber,
};
use time::{Duration, OffsetDateTime};

use crate::auth::{certificate_der, fingerprint_of_certificate, Fingerprint};
use crate::control::ControlStore;
use crate::doc::DocRef;
use crate::error::{Error, Result};
use crate::model::{now_ms, UnixMillis};
use gunnar_store::secret::ISSUER_PRIVATE_KEY;
use gunnar_store::{SecretStore, SecretStoreExt};

/// The control document holding the issuer certificate and the trusted-CA list.
///
/// Named `refs/gunnar/…` like [`crate::AUTH_REF`] and
/// [`crate::CATALOG_REF`]: these stopped being git references when the control
/// plane moved to redb (the certificate-identity plan), but they are what every error message and
/// runbook calls these documents.
pub const ISSUER_REF: &str = "refs/gunnar/pki";

/// How long a freshly minted issuer is good for, in days.
///
/// Ten years. An issuer that expires takes every certificate in the field with
/// it, and the recovery is a rotation of the whole estate — so the default is
/// long enough that it is a planned event rather than an outage, and
/// `--days` is there for a deployment with a shorter policy.
pub const DEFAULT_ISSUER_DAYS: u32 = 3650;

/// How long an issued client certificate is good for, in days.
pub const DEFAULT_LEAF_DAYS: u32 = 365;

/// A serial number is 20 bytes of CSPRNG (RFC 5280 allows at most 20).
const SERIAL_LEN: usize = 20;

// ── what is stored ───────────────────────────────────────────────────────────

/// How the live issuer got here.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IssuerOrigin {
    /// `gunnar issuer create` — the server minted it and has never seen another
    /// copy of the key.
    Created,
    /// `gunnar issuer import` — the key arrived from somewhere else, so
    /// somewhere else may still have it.
    Imported,
}

impl std::fmt::Display for IssuerOrigin {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            IssuerOrigin::Created => "created",
            IssuerOrigin::Imported => "imported",
        })
    }
}

/// The live issuer, as [`Pki::issuer`] reports it.
///
/// The **certificate only**. There is deliberately no field here that could
/// carry the private key: the type an operator prints, a gRPC response
/// serialises and a log line might capture must be structurally incapable of
/// holding it.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Issuer {
    /// Its subject common name.
    pub subject: String,
    /// The certificate, PEM-armoured. Public by definition.
    pub cert_pem: String,
    /// [`fingerprint_of_certificate`] over its DER — the same `SHA256:` space
    /// every other credential in gunnar lives in.
    pub fingerprint: Fingerprint,
    /// When this server adopted it.
    pub created: UnixMillis,
    /// Minted here, or handed to us.
    pub origin: IssuerOrigin,
}

/// One entry in the trusted-CA list.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TrustedCa {
    /// An operator-chosen label. Never part of the credential.
    pub label: String,
    /// The CA certificate, PEM-armoured.
    pub cert_pem: String,
    /// Its fingerprint, which is what `untrust` takes.
    pub fingerprint: Fingerprint,
    /// When it was added.
    pub added: UnixMillis,
    /// **This entry is the server's own issuer.** Set by [`Pki::create`] and
    /// [`Pki::import`], and what stops [`Pki::untrust_ca`] from removing the CA
    /// the server is currently minting with.
    pub is_issuer: bool,
}

/// A certificate this server issued.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Issued {
    /// The principal it was issued for. Equal to [`Issued::common_name`] by
    /// construction — the certificate-identity plan takes the identity from the CN.
    pub principal_id: String,
    /// The subject common name that was actually written into the certificate.
    pub common_name: String,
    /// The certificate, PEM-armoured.
    pub cert_pem: String,
    /// Its DER, which is what [`crate::AuthTable::register_certificate`] takes.
    pub cert_der: Vec<u8>,
    /// Its fingerprint.
    pub fingerprint: Fingerprint,
    /// **The private key, and only for `--server-generated-key`.**
    ///
    /// `None` on the CSR path, and that `None` is the security property: on that
    /// path the key never existed here, so there is nothing to hand back, log,
    /// or leak. See [`Pki::issue_with_server_key`] for what taking `Some` costs.
    pub key_pem: Option<String>,
}

/// The stored document: one issuer, and a list of trusted CAs.
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct PkiDoc {
    /// `None` until `issuer create` or `issuer import` has run.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    issuer: Option<Issuer>,
    /// Every CA a client certificate may chain to. The issuer is one of them.
    #[serde(default)]
    trusted: Vec<TrustedCa>,
}

/// No sharded maps, and that is a decision rather than an oversight.
///
/// [`crate::AuthTable`]'s maps are sharded because a forge holds every
/// principal, key and grant on the server in one document and was parsing all
/// of it per connection. This document holds **one** issuer and, in the
/// worst case a rotation produces, two or three CAs. A trie over three rows is
/// a shard probe and a second blob fetch to save nothing.
impl crate::doc::Doc for PkiDoc {
    fn bind(&mut self, _store: &Arc<dyn ControlStore>) {}
    fn flush(&mut self) -> Result<()> {
        Ok(())
    }
}

// ── the issuer ───────────────────────────────────────────────────────────────

/// **The server's certificate authority**: create it, import it, and issue with
/// it.
///
/// Cheap to clone; every clone is the same document and the same secret store.
#[derive(Clone)]
pub struct Pki {
    doc: DocRef<PkiDoc>,
    secrets: Arc<dyn SecretStore>,
}

impl std::fmt::Debug for Pki {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Pki")
    }
}

impl Pki {
    /// Open the PKI held in `control`, with private keys in `secrets`.
    ///
    /// **`secrets` must already be sealed.** Hand it the
    /// [`SealedSecretStore`](crate::seal::SealedSecretStore) the server built at
    /// boot, never a raw backend: this type writes the one key on the machine
    /// that is worth stealing, and it cannot tell from the trait object whether
    /// what it wrote was encrypted. That is why `ServerStores::pki()` is the
    /// only constructor call on the server's boot path.
    pub fn new(control: Arc<dyn ControlStore>, secrets: Arc<dyn SecretStore>) -> Result<Self> {
        Ok(Pki {
            doc: DocRef::new(control, ISSUER_REF)?,
            secrets,
        })
    }

    /// **A [`Pki`] that can read the issuer and the CA list and cannot reach a
    /// key**, for a process that carries no storage engine.
    ///
    /// The whole point of moving the control plane to redb (the certificate-identity plan) is that
    /// the catalog and the identity table are readable without an engine. The
    /// issuer *certificate* is a control document and belongs to that set;
    /// `gunnar issuer show` must therefore work on a client-shaped binary. The
    /// private key does not — it is in the secret log inside the control
    /// repository, which does need an engine.
    ///
    /// So this is a constructor and not a flag: the store it installs refuses
    /// every call, which makes "`show` cannot touch the key" a property of the
    /// value rather than of whoever remembered not to call `create` on it.
    pub fn read_only(control: Arc<dyn ControlStore>) -> Result<Self> {
        Pki::new(control, Arc::new(NoSecrets))
    }

    /// The name this PKI is stored under.
    pub fn ref_name(&self) -> &str {
        self.doc.ref_name()
    }

    /// The live issuer, or `None` on a server that has never had one.
    pub fn issuer(&self) -> Result<Option<Issuer>> {
        Ok(self.doc.load()?.issuer.clone())
    }

    /// Every CA a client certificate may chain to, oldest first.
    pub fn trusted_cas(&self) -> Result<Vec<TrustedCa>> {
        Ok(self.doc.load()?.trusted.clone())
    }

    /// **Mint this server's issuer.** Refused if it already has one.
    ///
    /// # The ordering, and what a crash between the two writes leaves behind
    ///
    /// The private key goes in first, through
    /// [`SecretStore::get_or_create`], which is atomic across racing callers —
    /// so two concurrent `issuer create` calls mint **one** key and both see the
    /// same bytes. Then the certificate is committed with the document's
    /// compare-and-swap, which is where the second caller is refused.
    ///
    /// A crash between the two leaves a key with no certificate. That is the
    /// harmless direction: [`Pki::issuer`] still says `None`, the next `issuer
    /// create` adopts the orphaned key rather than minting a second one, and no
    /// certificate anybody holds was invalidated. The other order — certificate
    /// first — would leave a live issuer whose key cannot be found, and every
    /// `principal add` after it would fail.
    pub fn create(&self, subject: &str, valid_days: u32) -> Result<Issuer> {
        if let Some(existing) = self.issuer()? {
            return Err(Error::IssuerExists {
                subject: existing.subject,
                fingerprint: existing.fingerprint.as_str().to_owned(),
            });
        }

        let key_pem = self.ensure_issuer_key()?;
        let key_pair = parse_key(&key_pem)?;

        let mut params = CertificateParams::default();
        params.distinguished_name = distinguished_name(subject);
        params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
        params.key_usages = vec![
            KeyUsagePurpose::KeyCertSign,
            KeyUsagePurpose::CrlSign,
            KeyUsagePurpose::DigitalSignature,
        ];
        params.serial_number = Some(random_serial()?);
        let (not_before, not_after) = validity(valid_days)?;
        params.not_before = not_before;
        params.not_after = not_after;

        let certificate = params
            .self_signed(&key_pair)
            .map_err(|err| Error::Pki(format!("self-signing the issuer: {err}")))?;
        let cert_pem = certificate.pem();
        let issuer = Issuer {
            subject: subject.to_owned(),
            fingerprint: fingerprint_of_certificate(certificate.der()),
            cert_pem: cert_pem.clone(),
            created: now_ms(),
            origin: IssuerOrigin::Created,
        };

        self.install(issuer)
    }

    /// **Adopt an issuer that already exists.** Refused if this server has one.
    ///
    /// Both halves are checked against each other before anything is written: a
    /// certificate whose SubjectPublicKeyInfo is not this key's public key is
    /// **refused**, because the alternative is a server that accepts the pair,
    /// stores both, and only discovers on the first `principal add` that it can
    /// mint nothing — by which time the operator has moved on.
    pub fn import(&self, cert_pem: &str, key_pem: &str) -> Result<Issuer> {
        if let Some(existing) = self.issuer()? {
            return Err(Error::IssuerExists {
                subject: existing.subject,
                fingerprint: existing.fingerprint.as_str().to_owned(),
            });
        }

        let key_pair = parse_key(key_pem)?;
        let der = certificate_der(cert_pem.as_bytes())?;
        let spki = subject_public_key_info(&der)?;
        if spki != rcgen::PublicKeyData::subject_public_key_info(&key_pair) {
            return Err(Error::Pki(
                "this certificate and this private key are not each other's: the certificate's \
                 SubjectPublicKeyInfo is not the public half of the key. Nothing was stored"
                    .to_owned(),
            ));
        }

        // Parsed so an unusable certificate is refused here rather than on the
        // first `principal add`. `signing_handle` reloads it exactly this way.
        rcgen::Issuer::from_ca_cert_pem(cert_pem, &key_pair)
            .map_err(|err| Error::Pki(format!("reading the imported issuer certificate: {err}")))?;
        let subject =
            certificate_common_name(&der)?.unwrap_or_else(|| "<no common name>".to_owned());

        let issuer = Issuer {
            subject,
            fingerprint: fingerprint_of_certificate(&der),
            cert_pem: cert_pem.to_owned(),
            created: now_ms(),
            origin: IssuerOrigin::Imported,
        };

        // The certificate first here, and the key second, which is the opposite
        // of `create` — because the key is the caller's and `get_or_create`
        // would silently keep an orphan from an interrupted earlier attempt
        // instead of storing what was handed over. The compare-and-swap is what
        // makes "this server has no issuer" true at the instant of the write, so
        // the key that lands afterwards cannot overwrite a live issuer's.
        let installed = self.install(issuer)?;
        self.secrets
            .put(ISSUER_PRIVATE_KEY, key_pem.as_bytes())
            .map_err(Error::from)?;
        Ok(installed)
    }

    /// Commit an issuer into the document, and add it to the trusted-CA list in
    /// the same compare-and-swap.
    fn install(&self, issuer: Issuer) -> Result<Issuer> {
        let entry = TrustedCa {
            label: issuer.subject.clone(),
            cert_pem: issuer.cert_pem.clone(),
            fingerprint: issuer.fingerprint.clone(),
            added: issuer.created,
            is_issuer: true,
        };
        self.doc.update(|doc| {
            // Re-checked INSIDE the transaction. The check at the top of
            // `create` is the good error message; this one is the guarantee.
            // `DocRef::update` re-reads and re-runs on a lost race, so a second
            // caller that got past the first check finds the winner here.
            if let Some(existing) = &doc.issuer {
                return Err(Error::IssuerExists {
                    subject: existing.subject.clone(),
                    fingerprint: existing.fingerprint.as_str().to_owned(),
                });
            }
            doc.issuer = Some(issuer.clone());
            doc.trusted.retain(|ca| ca.fingerprint != entry.fingerprint);
            doc.trusted.push(entry.clone());
            Ok(issuer.clone())
        })
    }

    /// Trust a CA this server does not mint with.
    ///
    /// The other half of a rotation: old and new both trusted while clients are
    /// re-issued. Re-adding one already in the list replaces its label rather
    /// than duplicating the entry.
    pub fn trust_ca(&self, label: &str, cert_pem: &str) -> Result<TrustedCa> {
        let der = certificate_der(cert_pem.as_bytes())?;
        x509_parser::parse_x509_certificate(&der)
            .map_err(|err| Error::Pki(format!("reading the CA certificate: {err}")))?;
        let entry = TrustedCa {
            label: label.to_owned(),
            cert_pem: cert_pem.to_owned(),
            fingerprint: fingerprint_of_certificate(&der),
            added: now_ms(),
            is_issuer: false,
        };
        self.doc.update(|doc| {
            let is_issuer = doc
                .issuer
                .as_ref()
                .is_some_and(|i| i.fingerprint == entry.fingerprint);
            let mut entry = entry.clone();
            entry.is_issuer = is_issuer;
            doc.trusted.retain(|ca| ca.fingerprint != entry.fingerprint);
            doc.trusted.push(entry.clone());
            Ok(entry)
        })
    }

    /// Stop trusting a CA. `Ok(None)` if it was not in the list.
    ///
    /// **Refuses to remove the live issuer's own entry.** A server that mints
    /// with a CA it does not trust issues certificates it will then refuse, and
    /// nothing about the failure would point back here. Rotate first, which
    /// changes which entry is the issuer, and then remove the old one.
    pub fn untrust_ca(&self, fingerprint: &Fingerprint) -> Result<Option<TrustedCa>> {
        self.doc.update(|doc| {
            let Some(at) = doc
                .trusted
                .iter()
                .position(|ca| &ca.fingerprint == fingerprint)
            else {
                return Ok(None);
            };
            if doc.trusted[at].is_issuer {
                return Err(Error::Pki(format!(
                    "{fingerprint} is this server's own issuer; removing it from the trusted \
                     list would leave a server minting certificates it refuses. Rotate the \
                     issuer first"
                )));
            }
            Ok(Some(doc.trusted.remove(at)))
        })
    }

    /// **Issue a certificate from a CSR.** The default, and the strong form.
    ///
    /// `common_name` is the principal the operator asked for, and it is what
    /// lands in the subject — **not** whatever the CSR claimed. See the module
    /// documentation: the only thing taken out of the request is the public key.
    ///
    /// The private key never existed on this server, so [`Issued::key_pem`] is
    /// `None` and there is nothing here to leak.
    pub fn issue_from_csr(
        &self,
        common_name: &str,
        csr_pem: &str,
        valid_days: u32,
    ) -> Result<Issued> {
        // Parsing VERIFIES the request's self-signature (rcgen's
        // `CertificateSigningRequestParams::from_der` calls `verify_signature`),
        // which is the proof of possession: a CSR carrying somebody else's
        // public key does not sign, and does not get here.
        let mut request = CertificateSigningRequestParams::from_pem(csr_pem).map_err(|err| {
            Error::Pki(format!(
                "this is not a usable certificate signing request: {err}. A CSR that does not \
                 verify against its own public key proves nothing about who holds the private \
                 half, which is the only thing it exists to prove"
            ))
        })?;

        harden(&mut request.params, common_name, valid_days)?;

        let issuer = self.signing_handle()?;
        let certificate = request
            .signed_by(&issuer)
            .map_err(|err| Error::Pki(format!("signing the request for {common_name}: {err}")))?;

        Ok(self.issued(
            common_name,
            certificate.pem(),
            certificate.der().to_vec(),
            None,
        ))
    }

    /// **Issue a certificate whose private key this server generated.**
    ///
    /// Named `--server-generated-key` on the command line rather than being what
    /// an operator gets by not thinking about it, because it is the weaker
    /// shape and the weakness is not visible from the outside:
    ///
    /// * the key existed here and travelled over the wire, so the server could
    ///   have kept a copy and nothing in the protocol proves it did not;
    /// * it is in a log, a core dump or a proxy's buffer if anything upstream is
    ///   careless;
    /// * *"only alice could have signed this"* stops being true.
    ///
    /// It exists because a machine account being bootstrapped, or a human who
    /// cannot run the tooling, genuinely needs it.
    pub fn issue_with_server_key(&self, common_name: &str, valid_days: u32) -> Result<Issued> {
        let key_pair = KeyPair::generate()
            .map_err(|err| Error::Pki(format!("minting a key for {common_name}: {err}")))?;

        let mut params = CertificateParams::default();
        harden(&mut params, common_name, valid_days)?;

        let issuer = self.signing_handle()?;
        let certificate = params
            .signed_by(&key_pair, &issuer)
            .map_err(|err| Error::Pki(format!("signing a certificate for {common_name}: {err}")))?;

        Ok(self.issued(
            common_name,
            certificate.pem(),
            certificate.der().to_vec(),
            Some(key_pair.serialize_pem()),
        ))
    }

    /// Mint a **server** identity for the smart-HTTP listener: leaf certificate
    /// with `hosts` as subject alternative names, signed by this issuer, key
    /// generated here and never stored.
    ///
    /// Deliberately NOT [`harden`], which is the CLIENT shape — it clears SANs
    /// and pins the EKU to `ClientAuth` precisely so a minted client
    /// certificate can never be presented as a server for a host it does not
    /// own. A server identity is the opposite trade: SANs are the whole point
    /// (a TLS client matches the name it dialled against them) and the EKU is
    /// `ServerAuth` and nothing else, so this certificate cannot come back in
    /// a client's role either. `is_ca` stays forced off.
    ///
    /// This is what makes gunnar a **single executable with its own TLS**: the
    /// appliance already mints its issuer on first start, clients already pin
    /// that issuer (`gunnar issuer show --pem`), and this leaf closes the
    /// chain without an operator ever touching a PEM.
    pub fn issue_server_identity(&self, hosts: &[String], valid_days: u32) -> Result<Issued> {
        if hosts.is_empty() {
            return Err(Error::Pki(
                "a server certificate needs at least one host name or address for its subject \
                 alternative names; a SAN-less server certificate matches nothing a client dials"
                    .to_owned(),
            ));
        }
        let key_pair = KeyPair::generate()
            .map_err(|err| Error::Pki(format!("minting a server key: {err}")))?;
        let common_name = &hosts[0];
        let mut params = CertificateParams::new(hosts.to_vec())
            .map_err(|err| Error::Pki(format!("server certificate SANs {hosts:?}: {err}")))?;
        params.distinguished_name = distinguished_name(common_name);
        params.is_ca = IsCa::ExplicitNoCa;
        params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
        params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
        params.use_authority_key_identifier_extension = true;
        params.serial_number = Some(random_serial()?);
        let now = OffsetDateTime::now_utc();
        params.not_before = now - NOT_BEFORE_SLACK;
        params.not_after = now + Duration::days(i64::from(valid_days));

        let issuer = self.signing_handle()?;
        let certificate = params
            .signed_by(&key_pair, &issuer)
            .map_err(|err| Error::Pki(format!("signing the server certificate: {err}")))?;
        Ok(self.issued(
            common_name,
            certificate.pem(),
            certificate.der().to_vec(),
            Some(key_pair.serialize_pem()),
        ))
    }

    fn issued(
        &self,
        common_name: &str,
        cert_pem: String,
        cert_der: Vec<u8>,
        key_pem: Option<String>,
    ) -> Issued {
        Issued {
            principal_id: common_name.to_owned(),
            common_name: common_name.to_owned(),
            fingerprint: fingerprint_of_certificate(&cert_der),
            cert_pem,
            cert_der,
            key_pem,
        }
    }

    /// The issuer certificate and its private key, ready to sign with.
    ///
    /// The certificate is rebuilt from the stored PEM rather than kept in
    /// memory: `signed_by` reads the issuer's distinguished name, key usages and
    /// key-identifier method off it, and those must come from what is on disk —
    /// otherwise a certificate issued after a restart could chain to something
    /// slightly different from what the CA list hands out.
    fn signing_handle(&self) -> Result<rcgen::Issuer<'static, KeyPair>> {
        let issuer = self.issuer()?.ok_or(Error::NoIssuer)?;
        let key_pem = self
            .secrets
            .get(ISSUER_PRIVATE_KEY)
            .map_err(Error::from)?
            .ok_or_else(|| {
                Error::Pki(format!(
                    "this server has an issuer certificate ({}) but no private key for it at \
                     {ISSUER_PRIVATE_KEY}. It can verify certificates and issue none",
                    issuer.subject
                ))
            })?;
        let key_pem = String::from_utf8(key_pem)
            .map_err(|_| Error::Pki("the stored issuer key is not utf-8 PEM".to_owned()))?;
        let key_pair = parse_key(&key_pem)?;

        let der = certificate_der(issuer.cert_pem.as_bytes())?;
        if subject_public_key_info(&der)?
            != rcgen::PublicKeyData::subject_public_key_info(&key_pair)
        {
            return Err(Error::Pki(
                "the stored issuer certificate and the stored issuer key do not go together"
                    .to_owned(),
            ));
        }
        rcgen::Issuer::from_ca_cert_pem(&issuer.cert_pem, key_pair)
            .map_err(|err| Error::Pki(format!("reading the stored issuer certificate: {err}")))
    }

    /// The issuer's private key, minting one on first use.
    ///
    /// Atomic across racing callers, exactly as
    /// [`crate::host_key::ensure_host_key`] is: two `issuer create` calls at
    /// once produce one key, not two.
    fn ensure_issuer_key(&self) -> Result<String> {
        let bytes = self
            .secrets
            .get_or_create_with(ISSUER_PRIVATE_KEY, || {
                tracing::info!(
                    key = ISSUER_PRIVATE_KEY,
                    "no issuer private key stored; minting one. It is sealed under the KEK and \
                     never enters the control database"
                );
                let pair = KeyPair::generate()
                    .map_err(|err| gunnar_store::Error::backend(format!("{err}")))?;
                Ok(pair.serialize_pem().into_bytes())
            })
            .map_err(Error::from)?;
        String::from_utf8(bytes)
            .map_err(|_| Error::Pki("the stored issuer key is not utf-8 PEM".to_owned()))
    }
}

/// The secret store [`Pki::read_only`] installs: it refuses everything.
///
/// Not a store that answers `None`. `None` means *no issuer key is stored*,
/// which a caller would reasonably act on by minting one — and a `create` that
/// silently succeeded against a store that discards writes would leave a server
/// holding a certificate it can never sign with.
struct NoSecrets;

impl NoSecrets {
    fn refuse() -> gunnar_store::Error {
        gunnar_store::Error::backend(
            "this handle can read the issuer certificate and the trusted-CA list and cannot \
             reach a private key. Issuing needs the sealed secret store, and this handle was \
             built without one on purpose",
        )
    }
}

impl SecretStore for NoSecrets {
    fn get(&self, _key: &str) -> gunnar_store::Result<Option<Vec<u8>>> {
        Err(Self::refuse())
    }
    fn put(&self, _key: &str, _value: &[u8]) -> gunnar_store::Result<()> {
        Err(Self::refuse())
    }
    fn delete(&self, _key: &str) -> gunnar_store::Result<()> {
        Err(Self::refuse())
    }
    fn get_or_create(
        &self,
        _key: &str,
        _mk: &mut dyn FnMut() -> gunnar_store::Result<Vec<u8>>,
    ) -> gunnar_store::Result<Vec<u8>> {
        Err(Self::refuse())
    }
}

// ── the parts that decide what a certificate says ────────────────────────────

/// Overwrite everything a CSR may have asked for except the public key.
///
/// This is the whole of "the server decides". Every field set here is one an
/// unchecked CSR could otherwise have chosen for itself:
///
/// * **the subject** becomes `common_name`, so a CSR claiming `CN=admin` is
///   issued as whoever the operator named;
/// * **subject alternative names are dropped**, because a `dNSName` SAN is how a
///   client certificate becomes usable as a *server* certificate for a host it
///   does not own;
/// * **`is_ca` is forced off** — a CSR carrying `basicConstraints CA:TRUE` would
///   otherwise ask for the right to mint users;
/// * **key usage** is digital signature and client authentication and nothing
///   else, so a stolen client certificate cannot be presented in another role.
fn harden(params: &mut CertificateParams, common_name: &str, valid_days: u32) -> Result<()> {
    if common_name.is_empty() {
        return Err(Error::Pki(
            "a certificate needs a common name; the certificate-identity scheme \
             takes the identity from it"
                .to_owned(),
        ));
    }
    params.distinguished_name = distinguished_name(common_name);
    params.subject_alt_names.clear();
    params.is_ca = IsCa::ExplicitNoCa;
    params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
    params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
    params.name_constraints = None;
    params.custom_extensions.clear();
    params.crl_distribution_points.clear();
    params.use_authority_key_identifier_extension = true;
    params.serial_number = Some(random_serial()?);
    let (not_before, not_after) = validity(valid_days)?;
    params.not_before = not_before;
    params.not_after = not_after;
    Ok(())
}

/// A distinguished name carrying one common name, and nothing else.
fn distinguished_name(common_name: &str) -> DistinguishedName {
    let mut dn = DistinguishedName::new();
    dn.push(DnType::CommonName, common_name);
    dn
}

/// The common name out of a distinguished name, if it has one.
///
/// Public because the *claimed* name in a CSR is a thing a caller may want to
/// log or refuse on — `gunnar-cli` prints it when it differs from the principal
/// being issued, so an operator can see that a request asked for something else.
pub fn common_name(dn: &DistinguishedName) -> Option<String> {
    match dn.get(&DnType::CommonName)? {
        DnValue::Utf8String(text) => Some(text.clone()),
        DnValue::PrintableString(text) => Some(text.as_str().to_owned()),
        DnValue::Ia5String(text) => Some(text.as_str().to_owned()),
        DnValue::TeletexString(text) => Some(String::from_utf8_lossy(text.as_bytes()).into_owned()),
        // BMPString and UniversalString are UCS-2 and UTF-32; gunnar never
        // writes either, and a principal id is not expressible in them without
        // a transcoding step that would be a second definition of "the name".
        // `None` here means "this certificate does not name a principal gunnar
        // can match", which is the refusing direction.
        _ => None,
    }
}

/// The common name a certificate signing request claims, for the caller that
/// wants to report it. Never what gets issued — see [`harden`].
pub fn csr_claimed_common_name(csr_pem: &str) -> Result<Option<String>> {
    let request = CertificateSigningRequestParams::from_pem(csr_pem)
        .map_err(|err| Error::Pki(format!("this is not a usable request: {err}")))?;
    Ok(common_name(&request.params.distinguished_name))
}

/// `[now, now + days)`, from the wall clock.
/// How far back a certificate's `not_before` is set (see
/// `not_before_tests`): a day, so a box whose clock is stepped back after it
/// minted still holds valid certificates.
pub const NOT_BEFORE_SLACK: Duration = Duration::hours(24);

fn validity(valid_days: u32) -> Result<(OffsetDateTime, OffsetDateTime)> {
    if valid_days == 0 {
        return Err(Error::Pki(
            "a certificate valid for zero days is expired the moment it is signed".to_owned(),
        ));
    }
    let now = OffsetDateTime::now_utc();
    let until = now
        .checked_add(Duration::days(i64::from(valid_days)))
        .ok_or_else(|| Error::Pki(format!("{valid_days} days from now is not a date")))?;
    Ok((now - NOT_BEFORE_SLACK, until))
}

/// 20 bytes from the system CSPRNG.
///
/// rcgen's default is the SHA-256 of the subject public key, which is stable
/// **per key** — so re-issuing the same principal from the same CSR would
/// produce two certificates sharing one serial, and a serial is what a CRL
/// names. Random is what makes revocation able to name one of them.
fn random_serial() -> Result<SerialNumber> {
    use aws_lc_rs::rand::{SecureRandom as _, SystemRandom};

    let mut bytes = [0u8; SERIAL_LEN];
    SystemRandom::new().fill(&mut bytes).map_err(|_| {
        Error::Pki("the system CSPRNG refused to produce a serial number".to_owned())
    })?;
    // The most significant bit clear, so it encodes as a positive 20-byte
    // integer rather than as 21 bytes with a leading zero (RFC 5280 §4.1.2.2
    // caps a serial at 20 octets).
    bytes[0] &= 0x7f;
    Ok(SerialNumber::from_slice(&bytes))
}

fn parse_key(pem: &str) -> Result<KeyPair> {
    KeyPair::from_pem(pem)
        .map_err(|err| Error::Pki(format!("this is not a usable private key: {err}")))
}

/// The common name a certificate's subject carries (DER), if any.
fn certificate_common_name(der: &[u8]) -> Result<Option<String>> {
    let (_rest, cert) = x509_parser::parse_x509_certificate(der)
        .map_err(|err| Error::Pki(format!("this is not a usable X.509 certificate: {err}")))?;
    let name = cert
        .subject()
        .iter_common_name()
        .next()
        .and_then(|cn| cn.as_str().ok())
        .map(str::to_owned);
    Ok(name)
}

/// The SubjectPublicKeyInfo DER out of a certificate, byte-for-byte as it was
/// encoded, so it can be compared with a key's (`rcgen::PublicKeyData`).
fn subject_public_key_info(der: &[u8]) -> Result<Vec<u8>> {
    let (_rest, cert) = x509_parser::parse_x509_certificate(der)
        .map_err(|err| Error::Pki(format!("this is not a usable X.509 certificate: {err}")))?;
    Ok(cert.tbs_certificate.subject_pki.raw.to_vec())
}

/// **Mint a key pair and a certificate signing request for it, HERE.**
///
/// The other half of [`Pki::issue_from_csr`], for a box that has to obtain a
/// client certificate from ANOTHER box's issuer — the twin, which is installed
/// from the same medium as its primary and so carries nothing per box: its
/// certificate has to be minted at pairing, and its private key has to be born
/// on the machine that will use it and never cross the wire. What crosses is
/// this request, which carries the public half and a self-signature that
/// proves possession; `issue_from_csr` verifies that signature and overrides
/// the subject, so `common_name` is a courtesy and never an authority.
///
/// Returns `(private key PEM, request PEM)`. ECDSA P-256 — rcgen's default and
/// what the issuer's own leaves use, so the two ends negotiate one signature
/// scheme.
pub fn mint_client_request(common_name: &str) -> Result<(String, String)> {
    let key =
        KeyPair::generate().map_err(|err| Error::Pki(format!("minting a key pair: {err}")))?;
    let mut params = CertificateParams::new(Vec::<String>::new())
        .map_err(|err| Error::Pki(format!("building a signing request: {err}")))?;
    params.distinguished_name = DistinguishedName::new();
    params
        .distinguished_name
        .push(DnType::CommonName, common_name);
    let request = params
        .serialize_request(&key)
        .map_err(|err| Error::Pki(format!("signing the request with its own key: {err}")))?;
    let pem = request
        .pem()
        .map_err(|err| Error::Pki(format!("armouring the signing request: {err}")))?;
    Ok((key.serialize_pem(), pem))
}

/// The SubjectPublicKeyInfo of a PEM private key, DER — to prove a certificate
/// handed to this box was issued for THIS box's key.
pub fn public_key_info_of_key(key_pem: &str) -> Result<Vec<u8>> {
    Ok(rcgen::PublicKeyData::subject_public_key_info(&parse_key(
        key_pem,
    )?))
}

/// The SubjectPublicKeyInfo a PEM certificate carries, DER.
pub fn public_key_info_of_cert(cert_pem: &str) -> Result<Vec<u8>> {
    let (_rest, block) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
        .map_err(|err| Error::Pki(format!("this is not a usable PEM certificate: {err}")))?;
    subject_public_key_info(&block.contents)
}

/// The SubjectPublicKeyInfo a DER certificate carries, DER — which KEY a
/// registered certificate row (`AuthTable`'s `x509` rows keep the DER) was
/// issued for.
pub fn public_key_info_of_der(cert_der: &[u8]) -> Result<Vec<u8>> {
    subject_public_key_info(cert_der)
}

/// The DER inside the first PEM block of `pem`.
pub fn pem_to_der(pem: &str) -> Result<Vec<u8>> {
    let (_rest, block) = x509_parser::pem::parse_x509_pem(pem.as_bytes())
        .map_err(|err| Error::Pki(format!("this is not a usable PEM certificate: {err}")))?;
    Ok(block.contents)
}

/// Did the CA in `ca_pem` issue the certificate in `cert_pem`, **by name**?
///
/// A name comparison, not a signature check: it exists to refuse a CA/leaf
/// pair that was handed over mismatched, which would otherwise surface months
/// later as a handshake failure naming neither. The signature is checked where
/// it matters — by rustls, at every handshake, against the pinned CA.
/// `Err` carries both names so the refusal can print them.
pub fn issued_by(
    cert_pem: &str,
    ca_pem: &str,
) -> Result<std::result::Result<(), (String, String)>> {
    let (cert_der, ca_der) = (pem_to_der(cert_pem)?, pem_to_der(ca_pem)?);
    let parse = |der: &[u8], what: &str| {
        x509_parser::parse_x509_certificate(der)
            .map(|(_, c)| {
                (
                    c.issuer().as_raw().to_vec(),
                    c.issuer().to_string(),
                    c.subject().as_raw().to_vec(),
                    c.subject().to_string(),
                )
            })
            .map_err(|err| Error::Pki(format!("{what} is not a usable X.509 certificate: {err}")))
    };
    let (leaf_issuer, leaf_issuer_name, _, _) = parse(&cert_der, "the certificate")?;
    let (_, _, ca_subject, ca_subject_name) = parse(&ca_der, "the issuer")?;
    Ok(if leaf_issuer == ca_subject {
        Ok(())
    } else {
        Err((leaf_issuer_name, ca_subject_name))
    })
}

/// `notAfter` of a DER certificate, Unix milliseconds.
pub fn not_after_ms(der: &[u8]) -> Result<UnixMillis> {
    let (_rest, cert) = x509_parser::parse_x509_certificate(der)
        .map_err(|err| Error::Pki(format!("this is not a usable X.509 certificate: {err}")))?;
    Ok(cert.validity().not_after.timestamp() * 1000)
}

#[cfg(test)]
mod not_before_tests {
    use super::*;

    /// **An estate's boxes mint their issuer and server identity at FIRST
    /// OPEN, before the estate clock steps them** (MEASURED on mock-upcloud-kvm
    /// 2026-09-29, topic recharge: guests boot +2 h like UpCloud without
    /// kvmclock; main minted its issuer at 10:33, the front's clock stepped it
    /// to 08:34, and the twin refused the primary's replication listener
    /// `certificate not valid yet` for good). A certificate is valid from a day
    /// before it is signed — the lifetime is unchanged — as Skidbladnir
    /// nordisk-twin's NOT_BEFORE_SLACK (4613616) does for holger.
    #[test]
    fn a_certificate_minted_before_the_clock_stepped_back_is_already_valid() {
        let (nb, na) = validity(1).unwrap();
        let now = OffsetDateTime::now_utc();
        assert!(
            nb <= now - Duration::hours(23),
            "not_before {nb} is not a day back from {now}"
        );
        assert!(
            na >= now + Duration::hours(23),
            "the lifetime is unchanged: {na}"
        );
        assert_eq!(NOT_BEFORE_SLACK, Duration::hours(24));
    }
}