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