//! The mTLS agreement: where an agent's own identity **at the store** lives, //! and what the object at that path holds. //! //! The sibling of [`crate::matrix`] and [`crate::queue`], and the one that is //! about reaching the store rather than about something kept inside it. An //! agent's client certificate is minted at swarm level //! (`swarm-controller::agent_identity`) and published here; the agent's hive //! collects it under the **hive's** own certificate and hands it into the //! container. //! //! 🔑 The recursion this shape looks like it has — *a credential fetched from //! the store that is what opens the store* — is broken by who reads it. The //! reader is the hive, never the agent, and a hive already holds its own leaf. //! Nothing has to already be an agent in order to obtain an agent's identity. use serde::{Deserialize, Serialize}; use crate::{ Error, path::{Kind, principal_prefix}, }; /// The path holding `agent`'s client certificate for the store itself. /// /// One path per agent with no further segment below it, unlike /// [`crate::matrix::account_path`]: an agent has exactly one identity, and a /// second one under a name would be a second principal wearing that name. /// /// # Errors /// [`Error::PathSegment`] when `agent` contains anything but `[A-Za-z0-9_-]`, /// which is what keeps one agent's name from addressing another's identity. pub fn identity_path(agent: &str) -> Result { let prefix = principal_prefix(Kind::Agent, agent)?; Ok(format!("{prefix}/bao-mtls")) } /// What the path holds: the leaf, its private key, and the authority the leaf /// was issued from. /// /// All three, because a reader has to reconstruct a usable identity from the /// store alone — the same requirement [`crate::queue::Credential`] states for /// carrying its client id. The authority rides along so the cert-auth role /// and the certificate cannot be delivered from two different sources and /// silently disagree; it is public material, unlike the other two fields. /// /// ⚠️ **No `Debug` derive.** See the hand-written impl below: one of these /// fields is a private key, and a derived `Debug` would put it in any log line /// that ever formatted a node's payload. #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Credential { /// The agent's client certificate, PEM. The nix-side reader in an agent's /// hive spells `bao kv get -field=cert`, so this name is load-bearing for /// a reader this crate does not control. pub cert: String, /// The private key for [`Credential::cert`], PEM. `bao kv get -field=key` /// on the reading side, and `0600` the moment it lands on disk there. pub key: String, /// The authority [`Credential::cert`] was issued from, PEM. Public /// material: it is also what the store keeps inside the agent's cert-auth /// role, by value (see [`crate::client::SecretStore::write_cert_role`]). pub ca: String, } impl std::fmt::Debug for Credential { /// Redacts the key and summarises the two public fields by length. /// /// Hand-written rather than derived because the derive is the failure: /// this type is carried through a job-graph node and an `anyhow` context /// chain, both of which format whatever they are given. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Credential") .field("cert", &format_args!("{} bytes of PEM", self.cert.len())) .field("key", &"") .field("ca", &format_args!("{} bytes of PEM", self.ca.len())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn an_agent_name_lands_under_its_own_principal_prefix() { assert_eq!( identity_path("atlas").expect("a plain name is legal"), "swarm/agents/atlas/bao-mtls" ); } #[test] fn a_traversal_in_the_agent_name_is_refused() { let e = identity_path("../argus").expect_err("a traversal is not"); assert!(matches!(e, Error::PathSegment { kind: "agent", .. }), "{e}"); } #[test] fn two_agents_never_share_a_path() { assert_ne!( identity_path("atlas").expect("legal"), identity_path("argus").expect("legal") ); } /// The path sits under the same prefix an agent's policy grants /// (`policy::render_agent`'s stanza is `swarm/agents//*`), which is /// what makes an agent able to read its own identity back. #[test] fn an_agents_identity_is_inside_its_own_policy_stanza() { let path = identity_path("atlas").expect("legal"); let document = crate::policy::render_agent("atlas").expect("legal"); let prefix = format!("{}/data/swarm/agents/atlas/", crate::path::MOUNT); assert!( path.starts_with("swarm/agents/atlas/"), "the identity must sit under the agent's own prefix, got {path}" ); assert!( document.contains(&format!("{prefix}*")), "the agent's document must cover {prefix}*, got:\n{document}" ); } #[test] fn the_object_round_trips_through_the_store_representation() { let c = Credential { cert: "-----BEGIN CERTIFICATE-----\n".to_owned(), key: "-----BEGIN PRIVATE KEY-----\n".to_owned(), ca: "-----BEGIN CERTIFICATE-----\n".to_owned(), }; let json = serde_json::to_string(&c).expect("serialises"); assert_eq!( serde_json::from_str::(&json).expect("deserialises"), c ); } #[test] fn the_field_names_the_nix_reader_asks_for_are_the_ones_written() { let json = serde_json::to_value(Credential { cert: "leaf".to_owned(), key: "private".to_owned(), ca: "authority".to_owned(), }) .expect("serialises"); assert_eq!(json["cert"], "leaf"); assert_eq!(json["key"], "private"); assert_eq!(json["ca"], "authority"); } /// The property the hand-written `Debug` exists for: a key that reaches a /// log line is a key on a disk somebody else owns. #[test] fn formatting_the_credential_does_not_reveal_the_key() { let rendered = format!( "{:?}", Credential { cert: "leaf".to_owned(), key: "SUPER-SECRET-KEY-MATERIAL".to_owned(), ca: "authority".to_owned(), } ); assert!( !rendered.contains("SUPER-SECRET-KEY-MATERIAL"), "the key must not survive formatting, got {rendered}" ); assert!( rendered.contains(""), "and the reader must be told it was withheld, got {rendered}" ); } }