auths_core/trust/
roots_file.rs1use serde::Deserialize;
7use std::path::Path;
8
9use crate::error::TrustError;
10
11#[derive(Debug, Deserialize)]
32pub struct RootsFile {
33 pub version: u32,
35
36 pub roots: Vec<RootEntry>,
38}
39
40#[derive(Debug, Deserialize)]
42pub struct RootEntry {
43 pub did: String,
45
46 pub public_key_hex: String,
48
49 #[serde(default)]
51 pub kel_tip_said: Option<String>,
52
53 #[serde(default)]
55 pub note: Option<String>,
56}
57
58impl RootsFile {
59 pub fn load(path: &Path) -> Result<Self, TrustError> {
69 let content = std::fs::read_to_string(path)?;
70
71 let file: Self = serde_json::from_str(&content)?;
72
73 if file.version != 1 {
74 return Err(TrustError::InvalidData(format!(
75 "Unsupported roots.json version: {}. Expected version 1.",
76 file.version
77 )));
78 }
79
80 for root in &file.roots {
81 let bytes = hex::decode(&root.public_key_hex).map_err(|e| {
82 TrustError::InvalidData(format!(
83 "Invalid public_key_hex for {} in roots.json: {}",
84 root.did, e
85 ))
86 })?;
87 if bytes.len() != 32 {
88 return Err(TrustError::InvalidData(format!(
89 "Invalid key length for {} in roots.json: expected 32 bytes, got {}",
90 root.did,
91 bytes.len()
92 )));
93 }
94 }
95
96 Ok(file)
97 }
98
99 pub fn find(&self, did: &str) -> Option<&RootEntry> {
101 self.roots.iter().find(|r| r.did == did)
102 }
103
104 pub fn dids(&self) -> Vec<&str> {
106 self.roots.iter().map(|r| r.did.as_str()).collect()
107 }
108}
109
110impl RootEntry {
111 pub fn public_key_bytes(&self) -> Result<Vec<u8>, TrustError> {
113 hex::decode(&self.public_key_hex)
114 .map_err(|e| TrustError::InvalidData(format!("Invalid public_key_hex: {}", e)))
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::io::Write;
122
123 fn create_temp_roots_file(content: &str) -> (tempfile::TempDir, std::path::PathBuf) {
124 let dir = tempfile::tempdir().unwrap();
125 let path = dir.path().join("roots.json");
126 let mut file = std::fs::File::create(&path).unwrap();
127 file.write_all(content.as_bytes()).unwrap();
128 (dir, path)
129 }
130
131 #[test]
132 fn test_load_valid_roots_file() {
133 let content = r#"{
134 "version": 1,
135 "roots": [
136 {
137 "did": "did:keri:ETest123",
138 "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
139 "kel_tip_said": "ETip",
140 "note": "Test maintainer"
141 }
142 ]
143 }"#;
144
145 let (_dir, path) = create_temp_roots_file(content);
146 let roots = RootsFile::load(&path).unwrap();
147
148 assert_eq!(roots.version, 1);
149 assert_eq!(roots.roots.len(), 1);
150 assert_eq!(roots.roots[0].did, "did:keri:ETest123");
151 assert_eq!(roots.roots[0].kel_tip_said, Some("ETip".to_string()));
152 assert_eq!(roots.roots[0].note, Some("Test maintainer".to_string()));
153 }
154
155 #[test]
156 fn test_load_minimal_entry() {
157 let content = r#"{
158 "version": 1,
159 "roots": [
160 {
161 "did": "did:keri:ETest",
162 "public_key_hex": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
163 }
164 ]
165 }"#;
166
167 let (_dir, path) = create_temp_roots_file(content);
168 let roots = RootsFile::load(&path).unwrap();
169
170 assert_eq!(roots.roots[0].kel_tip_said, None);
171 assert_eq!(roots.roots[0].note, None);
172 }
173
174 #[test]
175 fn test_load_rejects_wrong_version() {
176 let content = r#"{
177 "version": 2,
178 "roots": []
179 }"#;
180
181 let (_dir, path) = create_temp_roots_file(content);
182 let result = RootsFile::load(&path);
183
184 assert!(result.is_err());
185 assert!(result.unwrap_err().to_string().contains("version"));
186 }
187
188 #[test]
189 fn test_load_rejects_invalid_hex() {
190 let content = r#"{
191 "version": 1,
192 "roots": [
193 {
194 "did": "did:keri:ETest",
195 "public_key_hex": "not-valid-hex"
196 }
197 ]
198 }"#;
199
200 let (_dir, path) = create_temp_roots_file(content);
201 let result = RootsFile::load(&path);
202
203 assert!(result.is_err());
204 assert!(result.unwrap_err().to_string().contains("Invalid"));
205 }
206
207 #[test]
208 fn test_load_rejects_wrong_key_length() {
209 let content = r#"{
210 "version": 1,
211 "roots": [
212 {
213 "did": "did:keri:ETest",
214 "public_key_hex": "0102030405"
215 }
216 ]
217 }"#;
218
219 let (_dir, path) = create_temp_roots_file(content);
220 let result = RootsFile::load(&path);
221
222 assert!(result.is_err());
223 assert!(result.unwrap_err().to_string().contains("32 bytes"));
224 }
225
226 #[test]
227 fn test_find_by_did() {
228 let content = r#"{
229 "version": 1,
230 "roots": [
231 {
232 "did": "did:keri:E111",
233 "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
234 },
235 {
236 "did": "did:keri:E222",
237 "public_key_hex": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
238 }
239 ]
240 }"#;
241
242 let (_dir, path) = create_temp_roots_file(content);
243 let roots = RootsFile::load(&path).unwrap();
244
245 assert!(roots.find("did:keri:E111").is_some());
246 assert!(roots.find("did:keri:E222").is_some());
247 assert!(roots.find("did:keri:E333").is_none());
248 }
249
250 #[test]
251 fn test_dids() {
252 let content = r#"{
253 "version": 1,
254 "roots": [
255 {
256 "did": "did:keri:E111",
257 "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
258 },
259 {
260 "did": "did:keri:E222",
261 "public_key_hex": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
262 }
263 ]
264 }"#;
265
266 let (_dir, path) = create_temp_roots_file(content);
267 let roots = RootsFile::load(&path).unwrap();
268 let dids = roots.dids();
269
270 assert_eq!(dids.len(), 2);
271 assert!(dids.contains(&"did:keri:E111"));
272 assert!(dids.contains(&"did:keri:E222"));
273 }
274
275 #[test]
276 fn test_root_entry_public_key_bytes() {
277 let entry = RootEntry {
278 did: "did:keri:ETest".to_string(),
279 public_key_hex: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
280 .to_string(),
281 kel_tip_said: None,
282 note: None,
283 };
284
285 let bytes = entry.public_key_bytes().unwrap();
286 assert_eq!(bytes.len(), 32);
287 assert_eq!(bytes[0], 0x01);
288 }
289}