krabka_audit/operator_keys/trust_set.rs
1//! The loaded trust set: the keys it holds, and the check a signature passes.
2//!
3//! [`OperatorKeys::load`] validates every entry before it keeps one, so a set
4//! that loads holds one key per `key_id` and one key per principal. A lookup
5//! is by `key_id` alone. [`OperatorKeys::verify`] adds the principal binding
6//! to the signature check, so one operator's key never signs in another's
7//! name.
8
9use {
10 super::{OperatorKeyEntry, OperatorKeyError, key_file::read_public_key},
11 crate::signing::verify_signature,
12 std::collections::BTreeMap,
13};
14
15/// One loaded operator key and the principal it is bound to.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct OperatorKey {
18 key_id: String,
19 principal: String,
20 public_key: Vec<u8>,
21}
22
23impl OperatorKey {
24 /// Stable identifier that a signed record names.
25 #[must_use]
26 pub fn key_id(&self) -> &str { &self.key_id }
27
28 /// The principal this key speaks for.
29 ///
30 /// A record that claims another author is refused even when its signature
31 /// verifies, so one operator's key cannot sign in another's name.
32 #[must_use]
33 pub fn principal(&self) -> &str { &self.principal }
34
35 /// The raw 32-byte Ed25519 public key.
36 #[must_use]
37 pub fn public_key(&self) -> &[u8] { &self.public_key }
38}
39
40/// The loaded, validated operator key trust set.
41///
42/// An empty set is the default and means no operator key is provisioned. The
43/// file-config layer refuses a configuration that demands a signature against
44/// an empty set.
45#[derive(Debug, Clone, Default, PartialEq, Eq)]
46pub struct OperatorKeys {
47 by_id: BTreeMap<String, OperatorKey>,
48}
49
50impl OperatorKeys {
51 /// Read every configured public key file and build the trust set.
52 ///
53 /// # Errors
54 ///
55 /// Returns [`OperatorKeyError`] when an entry has a blank `key_id` or
56 /// `principal`, when its `public_key_path` cannot be read, when the file
57 /// does not hold a 32-byte Ed25519 public key, or when two entries share a
58 /// `key_id` or a `principal`.
59 pub fn load(entries: &[OperatorKeyEntry]) -> Result<Self, OperatorKeyError> {
60 let mut by_id: BTreeMap<String, OperatorKey> = BTreeMap::new();
61 let mut by_principal: BTreeMap<String, String> = BTreeMap::new();
62 for (index, entry) in entries.iter().enumerate() {
63 if entry.key_id.trim().is_empty() {
64 return Err(OperatorKeyError::BlankField {
65 index,
66 field: "key_id",
67 });
68 }
69 if entry.principal.trim().is_empty() {
70 return Err(OperatorKeyError::BlankField {
71 index,
72 field: "principal",
73 });
74 }
75 if by_id.contains_key(&entry.key_id) {
76 return Err(OperatorKeyError::DuplicateKeyId {
77 key_id: entry.key_id.clone(),
78 });
79 }
80 if let Some(first) = by_principal.get(&entry.principal) {
81 return Err(OperatorKeyError::DuplicatePrincipal {
82 principal: entry.principal.clone(),
83 first: first.clone(),
84 second: entry.key_id.clone(),
85 });
86 }
87 let public_key = read_public_key(&entry.key_id, &entry.public_key_path)?;
88 by_principal.insert(entry.principal.clone(), entry.key_id.clone());
89 by_id.insert(entry.key_id.clone(), OperatorKey {
90 key_id: entry.key_id.clone(),
91 principal: entry.principal.clone(),
92 public_key,
93 });
94 }
95 Ok(Self { by_id })
96 }
97
98 /// The key registered under `key_id`, with the principal it is bound to.
99 #[must_use]
100 pub fn get(&self, key_id: &str) -> Option<&OperatorKey> { self.by_id.get(key_id) }
101
102 /// How many keys the trust set holds.
103 #[must_use]
104 pub fn len(&self) -> usize { self.by_id.len() }
105
106 /// Whether no operator key is provisioned.
107 #[must_use]
108 pub fn is_empty(&self) -> bool { self.by_id.is_empty() }
109
110 /// Verify `signature` over `message` as made by `key_id` on behalf of
111 /// `principal`.
112 ///
113 /// The principal binding is part of the check: a signature made by Alice's
114 /// key over a record that claims Bob wrote it does not verify. The caller
115 /// supplies the canonical bytes of its own signed payload, under its own
116 /// domain separator.
117 #[must_use]
118 pub fn verify(&self, key_id: &str, principal: &str, message: &[u8], signature: &[u8]) -> bool {
119 self.get(key_id).is_some_and(|key| {
120 key.principal == principal && verify_signature(&key.public_key, message, signature)
121 })
122 }
123}