1use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22pub use greentic_distributor_client::signing::{
27 SigningError, TrustRoot, TrustedKey, key_id_for_public_key_pem,
28};
29
30pub const TRUST_ROOT_SCHEMA_V1: &str = "greentic.trust-root.v1";
32
33#[derive(Debug, Error)]
36pub enum TrustRootDocError {
37 #[error("trust-root schema `{found}` is not the expected `{TRUST_ROOT_SCHEMA_V1}`")]
38 BadSchema { found: String },
39 #[error("trust-root key validation: {0}")]
40 Key(#[from] SigningError),
41 #[error(
42 "trust-root key_id `{supplied}` does not match the derivation from the public key (`{derived}`)"
43 )]
44 KeyIdMismatch { supplied: String, derived: String },
45 #[error("trust-root key_id `{0}` must be a non-empty hex string")]
46 EmptyKeyId(String),
47}
48
49#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
52pub struct TrustRootDocument {
53 pub schema: String,
54 pub keys: Vec<TrustedKey>,
55}
56
57impl TrustRootDocument {
58 pub fn v1(keys: Vec<TrustedKey>) -> Self {
60 Self {
61 schema: TRUST_ROOT_SCHEMA_V1.to_string(),
62 keys,
63 }
64 }
65
66 pub fn into_trust_root(self) -> Result<TrustRoot, TrustRootDocError> {
68 if self.schema != TRUST_ROOT_SCHEMA_V1 {
69 return Err(TrustRootDocError::BadSchema { found: self.schema });
70 }
71 Ok(TrustRoot::new(self.keys))
72 }
73}
74
75pub fn validate_trusted_key(key: TrustedKey) -> Result<TrustedKey, TrustRootDocError> {
79 if key.key_id.trim().is_empty() {
80 return Err(TrustRootDocError::EmptyKeyId(key.key_id));
81 }
82 let derived = key_id_for_public_key_pem(&key.public_key_pem)?;
83 if !key.key_id.eq_ignore_ascii_case(&derived) {
84 return Err(TrustRootDocError::KeyIdMismatch {
85 supplied: key.key_id,
86 derived,
87 });
88 }
89 Ok(TrustedKey {
90 key_id: derived,
91 public_key_pem: key.public_key_pem,
92 })
93}
94
95pub fn apply_add(keys: &mut Vec<TrustedKey>, key: TrustedKey) {
99 keys.retain(|k| !k.key_id.eq_ignore_ascii_case(&key.key_id));
100 keys.push(key);
101}
102
103pub fn apply_remove(keys: &mut Vec<TrustedKey>, key_id: &str) -> bool {
108 let before = keys.len();
109 keys.retain(|k| !k.key_id.eq_ignore_ascii_case(key_id));
110 keys.len() != before
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::test_support::keypair;
117
118 #[test]
119 fn validate_accepts_canonical_entry() {
120 let (pem, id) = keypair(1);
121 let key = validate_trusted_key(TrustedKey {
122 key_id: id.clone(),
123 public_key_pem: pem,
124 })
125 .unwrap();
126 assert_eq!(key.key_id, id);
127 }
128
129 #[test]
130 fn validate_canonicalizes_uppercase_key_id() {
131 let (pem, id) = keypair(2);
132 let key = validate_trusted_key(TrustedKey {
133 key_id: id.to_uppercase(),
134 public_key_pem: pem,
135 })
136 .unwrap();
137 assert_eq!(key.key_id, id, "stored id must be canonical lowercase");
138 }
139
140 #[test]
141 fn validate_rejects_mismatched_key_id() {
142 let (pem_a, _id_a) = keypair(3);
143 let (_pem_b, id_b) = keypair(4);
144 let err = validate_trusted_key(TrustedKey {
145 key_id: id_b,
146 public_key_pem: pem_a,
147 })
148 .expect_err("mismatched id must be rejected");
149 assert!(matches!(err, TrustRootDocError::KeyIdMismatch { .. }));
150 }
151
152 #[test]
153 fn validate_rejects_empty_and_whitespace_key_id() {
154 let (pem, _) = keypair(5);
155 let err = validate_trusted_key(TrustedKey {
156 key_id: " ".into(),
157 public_key_pem: pem,
158 })
159 .expect_err("empty id must be rejected");
160 assert!(matches!(err, TrustRootDocError::EmptyKeyId(_)));
161 }
162
163 #[test]
164 fn validate_rejects_malformed_pem() {
165 let err = validate_trusted_key(TrustedKey {
166 key_id: "abcdef0123456789abcdef0123456789".into(),
167 public_key_pem: "not-a-pem".into(),
168 })
169 .expect_err("bad pem must be rejected");
170 assert!(matches!(err, TrustRootDocError::Key(_)));
171 }
172
173 #[test]
174 fn apply_add_replaces_same_key_id_case_insensitively() {
175 let (pem, id) = keypair(6);
176 let mut keys = vec![TrustedKey {
177 key_id: id.to_uppercase(),
178 public_key_pem: "old".into(),
179 }];
180 apply_add(
181 &mut keys,
182 TrustedKey {
183 key_id: id.clone(),
184 public_key_pem: pem.clone(),
185 },
186 );
187 assert_eq!(keys.len(), 1, "duplicate key_id must dedup");
188 assert_eq!(keys[0].public_key_pem, pem, "PEM must be replaced");
189 }
190
191 #[test]
192 fn apply_add_keeps_distinct_keys() {
193 let (pem_a, id_a) = keypair(7);
194 let (pem_b, id_b) = keypair(8);
195 let mut keys = Vec::new();
196 apply_add(
197 &mut keys,
198 TrustedKey {
199 key_id: id_a,
200 public_key_pem: pem_a,
201 },
202 );
203 apply_add(
204 &mut keys,
205 TrustedKey {
206 key_id: id_b,
207 public_key_pem: pem_b,
208 },
209 );
210 assert_eq!(keys.len(), 2);
211 }
212
213 #[test]
214 fn apply_remove_drops_only_matching_key_and_reports_change() {
215 let (pem_a, id_a) = keypair(9);
216 let (pem_b, id_b) = keypair(10);
217 let mut keys = vec![
218 TrustedKey {
219 key_id: id_a.clone(),
220 public_key_pem: pem_a,
221 },
222 TrustedKey {
223 key_id: id_b.clone(),
224 public_key_pem: pem_b,
225 },
226 ];
227 assert!(apply_remove(&mut keys, &id_a.to_uppercase()));
228 assert_eq!(keys.len(), 1);
229 assert_eq!(keys[0].key_id, id_b);
230 assert!(
231 !apply_remove(&mut keys, &id_a),
232 "absent key must report no change"
233 );
234 }
235
236 #[test]
237 fn document_round_trips_and_rejects_unknown_schema() {
238 let (pem, id) = keypair(11);
239 let doc = TrustRootDocument::v1(vec![TrustedKey {
240 key_id: id.clone(),
241 public_key_pem: pem,
242 }]);
243 let json = serde_json::to_string(&doc).unwrap();
244 let parsed: TrustRootDocument = serde_json::from_str(&json).unwrap();
245 let root = parsed.into_trust_root().unwrap();
246 assert_eq!(root.keys.len(), 1);
247 assert_eq!(root.keys[0].key_id, id);
248
249 let bad: TrustRootDocument =
250 serde_json::from_str(r#"{"schema":"greentic.trust-root.v999","keys":[]}"#).unwrap();
251 let err = bad.into_trust_root().expect_err("bad schema must reject");
252 assert!(matches!(err, TrustRootDocError::BadSchema { .. }));
253 }
254}