use std::sync::{Arc, OnceLock, RwLock};
use super::credential::SignedCredential;
#[derive(Debug)]
pub struct OutboundCredentialHolder {
inner: RwLock<Option<Arc<SignedCredential>>>,
intermediates: RwLock<Arc<Vec<SignedCredential>>>,
}
impl OutboundCredentialHolder {
#[must_use]
pub fn new(initial: Option<SignedCredential>) -> Self {
Self {
inner: RwLock::new(initial.map(Arc::new)),
intermediates: RwLock::new(Arc::new(Vec::new())),
}
}
#[must_use]
pub fn with_chain(
initial: Option<SignedCredential>,
intermediates: Vec<SignedCredential>,
) -> Self {
Self {
inner: RwLock::new(initial.map(Arc::new)),
intermediates: RwLock::new(Arc::new(intermediates)),
}
}
#[must_use]
pub fn current(&self) -> Option<Arc<SignedCredential>> {
self.read_guard().clone()
}
#[must_use]
pub fn current_intermediates(&self) -> Arc<Vec<SignedCredential>> {
self.intermediates_read_guard().clone()
}
pub fn store(&self, cred: Option<SignedCredential>) {
*self.write_guard() = cred.map(Arc::new);
}
pub fn store_intermediates(&self, intermediates: Vec<SignedCredential>) {
*self.intermediates_write_guard() = Arc::new(intermediates);
}
fn read_guard(&self) -> std::sync::RwLockReadGuard<'_, Option<Arc<SignedCredential>>> {
self.inner
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write_guard(&self) -> std::sync::RwLockWriteGuard<'_, Option<Arc<SignedCredential>>> {
self.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn intermediates_read_guard(
&self,
) -> std::sync::RwLockReadGuard<'_, Arc<Vec<SignedCredential>>> {
self.intermediates
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn intermediates_write_guard(
&self,
) -> std::sync::RwLockWriteGuard<'_, Arc<Vec<SignedCredential>>> {
self.intermediates
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
fn load_from_env_logged() -> Option<SignedCredential> {
SignedCredential::load_from_env().unwrap_or_else(|e| {
tracing::warn!(target: crate::federation::SIGNING_TRACE_TARGET, error = %e,
"failed to load outbound federation credential; presenting per-message signature only");
None
})
}
fn load_intermediates_from_env_logged() -> Vec<SignedCredential> {
super::chain::load_intermediates_from_env().unwrap_or_else(|e| {
tracing::warn!(target: crate::federation::SIGNING_TRACE_TARGET, error = %e,
"failed to load outbound federation intermediate chain; presenting a direct leaf only");
Vec::new()
})
}
fn global() -> &'static OutboundCredentialHolder {
static GLOBAL: OnceLock<OutboundCredentialHolder> = OnceLock::new();
GLOBAL.get_or_init(|| {
OutboundCredentialHolder::with_chain(
load_from_env_logged(),
load_intermediates_from_env_logged(),
)
})
}
#[must_use]
pub fn shared() -> &'static OutboundCredentialHolder {
global()
}
#[must_use]
pub fn current() -> Option<Arc<SignedCredential>> {
global().current()
}
#[must_use]
pub fn current_intermediates() -> Arc<Vec<SignedCredential>> {
global().current_intermediates()
}
pub fn store(cred: Option<SignedCredential>) {
global().store(cred);
}
pub fn store_intermediates(intermediates: Vec<SignedCredential>) {
global().store_intermediates(intermediates);
}
pub fn reload_from_env() -> std::io::Result<()> {
let next = SignedCredential::load_from_env()?;
global().store(next);
Ok(())
}
pub fn reload_intermediates_from_env() -> std::io::Result<()> {
let next = super::chain::load_intermediates_from_env()?;
global().store_intermediates(next);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::federation::identity::issuer::{FederationIssuer, IssuerConfig};
use ed25519_dalek::SigningKey;
const NOW_UNIX: i64 = 1_900_000_000;
fn signing_key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
fn issuer(seed: u8) -> FederationIssuer {
FederationIssuer::new(
signing_key(seed),
IssuerConfig::new("trust-domain-root", "fleet.example"),
)
}
fn cred_for(subject: &str, subject_seed: u8, issuer_seed: u8) -> SignedCredential {
let subject_key = signing_key(subject_seed);
issuer(issuer_seed)
.issue(subject, &subject_key.verifying_key(), NOW_UNIX)
.expect("mint")
}
#[test]
fn empty_holder_yields_no_credential() {
let holder = OutboundCredentialHolder::new(None);
assert!(holder.current().is_none());
}
#[test]
fn seeded_holder_yields_that_credential() {
let cred = cred_for("region/nyc/node-1", 7, 1);
let holder = OutboundCredentialHolder::new(Some(cred.clone()));
let got = holder.current().expect("present");
assert_eq!(got.credential(), cred.credential());
}
#[test]
fn store_swaps_the_held_credential() {
let holder = OutboundCredentialHolder::new(Some(cred_for("node-old", 7, 1)));
let fresh = cred_for("node-new", 9, 1);
holder.store(Some(fresh.clone()));
let got = holder.current().expect("present");
assert_eq!(got.credential().subject_agent_id, "node-new");
assert_eq!(got.credential(), fresh.credential());
}
#[test]
fn store_none_clears_the_credential() {
let holder = OutboundCredentialHolder::new(Some(cred_for("node-x", 7, 1)));
assert!(holder.current().is_some());
holder.store(None);
assert!(holder.current().is_none());
}
fn intermediate_cert(subject: &str, subject_seed: u8, issuer_seed: u8) -> SignedCredential {
let subject_key = signing_key(subject_seed);
issuer(issuer_seed)
.issue_intermediate(subject, &subject_key.verifying_key(), NOW_UNIX)
.expect("mint intermediate")
}
#[test]
fn new_holder_has_no_intermediates() {
let holder = OutboundCredentialHolder::new(Some(cred_for("node-1", 7, 1)));
assert!(holder.current_intermediates().is_empty());
}
#[test]
fn with_chain_seeds_both_leaf_and_intermediates() {
let leaf = cred_for("region/nyc/node-1", 7, 2);
let inter = intermediate_cert("region/nyc/ca", 2, 1);
let holder = OutboundCredentialHolder::with_chain(Some(leaf), vec![inter.clone()]);
let got = holder.current_intermediates();
assert_eq!(got.len(), 1);
assert_eq!(got[0].credential(), inter.credential());
}
#[test]
fn store_intermediates_swaps_without_touching_leaf() {
let leaf = cred_for("region/nyc/node-1", 7, 2);
let holder = OutboundCredentialHolder::with_chain(Some(leaf.clone()), Vec::new());
let inter = intermediate_cert("region/nyc/ca", 2, 1);
holder.store_intermediates(vec![inter.clone()]);
assert_eq!(holder.current_intermediates().len(), 1);
assert_eq!(
holder.current().expect("leaf present").credential(),
leaf.credential()
);
}
#[test]
fn store_leaf_does_not_clobber_intermediates() {
let inter = intermediate_cert("region/nyc/ca", 2, 1);
let holder = OutboundCredentialHolder::with_chain(
Some(cred_for("node-old", 7, 2)),
vec![inter.clone()],
);
holder.store(Some(cred_for("node-new", 9, 2)));
assert_eq!(
holder
.current()
.expect("present")
.credential()
.subject_agent_id,
"node-new"
);
assert_eq!(holder.current_intermediates().len(), 1);
assert_eq!(
holder.current_intermediates()[0].credential(),
inter.credential()
);
}
#[test]
fn snapshot_taken_before_store_is_unaffected_by_swap() {
let holder = OutboundCredentialHolder::new(Some(cred_for("node-a", 7, 1)));
let snapshot = holder.current().expect("present");
holder.store(Some(cred_for("node-b", 9, 1)));
assert_eq!(snapshot.credential().subject_agent_id, "node-a");
assert_eq!(
holder
.current()
.expect("present")
.credential()
.subject_agent_id,
"node-b"
);
}
}