ndn_security/
pib.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use bytes::Bytes;
5use thiserror::Error;
6
7use ndn_packet::{Name, NameComponent};
8
9use crate::{TrustError, cert_cache::Certificate, signer::Ed25519Signer};
10
11// ─── Error ────────────────────────────────────────────────────────────────────
12
13#[derive(Debug, Error)]
14pub enum PibError {
15    #[error("I/O error: {0}")]
16    Io(#[from] std::io::Error),
17    #[error("key not found in PIB: {0}")]
18    KeyNotFound(String),
19    #[error("certificate not found in PIB: {0}")]
20    CertNotFound(String),
21    #[error("corrupt PIB data: {0}")]
22    Corrupt(String),
23    #[error("invalid name")]
24    InvalidName,
25}
26
27impl From<PibError> for TrustError {
28    fn from(e: PibError) -> Self {
29        TrustError::KeyStore(e.to_string())
30    }
31}
32
33// ─── FilePib ──────────────────────────────────────────────────────────────────
34
35/// File-based Public Info Base (PIB) for persistent key and certificate storage.
36///
37/// # Directory layout
38/// ```text
39/// <root>/
40///   keys/<sha256>/
41///     name.uri          # NDN name in URI form (human-readable)
42///     private.key       # 32-byte raw Ed25519 seed
43///     cert.ndnc         # NDNC-format certificate (optional)
44///   anchors/<sha256>/
45///     name.uri
46///     cert.ndnc
47/// ```
48///
49/// Key directories are named by the SHA-256 of the canonical name bytes to
50/// avoid filesystem special-character issues.  The `name.uri` file provides
51/// the human-readable name for `list` operations.
52///
53/// # Certificate format (NDNC v1)
54/// ```text
55/// [4]  magic "NDNC"
56/// [1]  version = 1
57/// [8]  valid_from  (u64 be, nanoseconds since Unix epoch)
58/// [8]  valid_until (u64 be, nanoseconds since Unix epoch; u64::MAX = never)
59/// [4]  pk_len      (u32 be)
60/// [pk_len] public key bytes
61/// ```
62pub struct FilePib {
63    root: PathBuf,
64}
65
66impl FilePib {
67    /// Create or open a PIB at `root`, creating the directory tree if needed.
68    pub fn new(root: impl Into<PathBuf>) -> Result<Self, PibError> {
69        let root = root.into();
70        std::fs::create_dir_all(root.join("keys"))?;
71        std::fs::create_dir_all(root.join("anchors"))?;
72        Ok(Self { root })
73    }
74
75    /// Open an existing PIB without creating it.  Returns an error if `root`
76    /// does not contain an initialised PIB.
77    pub fn open(root: impl Into<PathBuf>) -> Result<Self, PibError> {
78        let root = root.into();
79        if !root.join("keys").exists() {
80            return Err(PibError::Io(std::io::Error::new(
81                std::io::ErrorKind::NotFound,
82                format!(
83                    "PIB not found at {} (run `ndn-sec keygen` to create one)",
84                    root.display()
85                ),
86            )));
87        }
88        Ok(Self { root })
89    }
90
91    /// Return the root directory of this PIB.
92    pub fn root(&self) -> &Path {
93        &self.root
94    }
95
96    // ─── Keys ─────────────────────────────────────────────────────────────────
97
98    /// Generate a new Ed25519 key using a cryptographically random seed and
99    /// persist it to the PIB.  Returns the signer so the caller can immediately
100    /// issue a certificate without re-reading from disk.
101    pub fn generate_ed25519(&self, key_name: &Name) -> Result<Ed25519Signer, PibError> {
102        let seed = random_seed();
103        let signer = Ed25519Signer::from_seed(&seed, key_name.clone());
104        let dir = self.key_dir(key_name)?;
105        std::fs::write(dir.join("private.key"), seed)?;
106        std::fs::write(dir.join("name.uri"), name_to_uri(key_name))?;
107        Ok(signer)
108    }
109
110    /// Load the signer for `key_name` from the PIB.
111    pub fn get_signer(&self, key_name: &Name) -> Result<Ed25519Signer, PibError> {
112        let dir = self
113            .existing_key_dir(key_name)
114            .ok_or_else(|| PibError::KeyNotFound(name_to_uri(key_name)))?;
115        let seed_bytes = std::fs::read(dir.join("private.key"))?;
116        if seed_bytes.len() != 32 {
117            return Err(PibError::Corrupt(
118                "private.key must be exactly 32 bytes".into(),
119            ));
120        }
121        let mut seed = [0u8; 32];
122        seed.copy_from_slice(&seed_bytes);
123        Ok(Ed25519Signer::from_seed(&seed, key_name.clone()))
124    }
125
126    /// Delete a key and its associated certificate from the PIB.
127    pub fn delete_key(&self, key_name: &Name) -> Result<(), PibError> {
128        if let Some(dir) = self.existing_key_dir(key_name) {
129            std::fs::remove_dir_all(dir)?;
130        }
131        Ok(())
132    }
133
134    /// List all key names stored in the PIB.
135    pub fn list_keys(&self) -> Result<Vec<Name>, PibError> {
136        list_names_in(&self.root.join("keys"))
137    }
138
139    // ─── Certificates ─────────────────────────────────────────────────────────
140
141    /// Persist a certificate for `key_name` in its key directory.
142    pub fn store_cert(&self, key_name: &Name, cert: &Certificate) -> Result<(), PibError> {
143        let dir = self.key_dir(key_name)?;
144        std::fs::write(dir.join("cert.ndnc"), encode_cert(cert))?;
145        Ok(())
146    }
147
148    /// Load the certificate for `key_name`.
149    pub fn get_cert(&self, key_name: &Name) -> Result<Certificate, PibError> {
150        let dir = self
151            .existing_key_dir(key_name)
152            .ok_or_else(|| PibError::CertNotFound(name_to_uri(key_name)))?;
153        let data = std::fs::read(dir.join("cert.ndnc"))
154            .map_err(|_| PibError::CertNotFound(name_to_uri(key_name)))?;
155        decode_cert(Arc::new(key_name.clone()), &data)
156    }
157
158    // ─── Trust anchors ────────────────────────────────────────────────────────
159
160    /// Persist a certificate as a trust anchor.
161    pub fn add_trust_anchor(&self, key_name: &Name, cert: &Certificate) -> Result<(), PibError> {
162        let dir = self.anchor_dir(key_name)?;
163        std::fs::write(dir.join("cert.ndnc"), encode_cert(cert))?;
164        std::fs::write(dir.join("name.uri"), name_to_uri(key_name))?;
165        Ok(())
166    }
167
168    /// Remove a trust anchor from the PIB.
169    pub fn remove_trust_anchor(&self, key_name: &Name) -> Result<(), PibError> {
170        let dir = self.root.join("anchors").join(name_hash(key_name));
171        if dir.exists() {
172            std::fs::remove_dir_all(dir)?;
173        }
174        Ok(())
175    }
176
177    /// Load all trust anchor certificates from the PIB.
178    pub fn trust_anchors(&self) -> Result<Vec<Certificate>, PibError> {
179        let anchors_root = self.root.join("anchors");
180        if !anchors_root.exists() {
181            return Ok(vec![]);
182        }
183        let mut certs = Vec::new();
184        for entry in std::fs::read_dir(&anchors_root)? {
185            let entry = entry?;
186            let path = entry.path();
187            if !path.is_dir() {
188                continue;
189            }
190            let name_uri = std::fs::read_to_string(path.join("name.uri")).unwrap_or_default();
191            let name = name_from_uri(name_uri.trim()).unwrap_or_else(|_| Name::root());
192            if let Ok(data) = std::fs::read(path.join("cert.ndnc"))
193                && let Ok(cert) = decode_cert(Arc::new(name), &data)
194            {
195                certs.push(cert);
196            }
197        }
198        Ok(certs)
199    }
200
201    /// List all trust anchor names stored in the PIB.
202    pub fn list_anchors(&self) -> Result<Vec<Name>, PibError> {
203        list_names_in(&self.root.join("anchors"))
204    }
205
206    // ─── Internal helpers ─────────────────────────────────────────────────────
207
208    /// Return the key directory for `name`, creating it (and `name.uri`) if
209    /// it does not already exist.
210    fn key_dir(&self, name: &Name) -> Result<PathBuf, PibError> {
211        let dir = self.root.join("keys").join(name_hash(name));
212        std::fs::create_dir_all(&dir)?;
213        Ok(dir)
214    }
215
216    /// Return the key directory only if it already exists on disk.
217    fn existing_key_dir(&self, name: &Name) -> Option<PathBuf> {
218        let dir = self.root.join("keys").join(name_hash(name));
219        if dir.exists() { Some(dir) } else { None }
220    }
221
222    fn anchor_dir(&self, name: &Name) -> Result<PathBuf, PibError> {
223        let dir = self.root.join("anchors").join(name_hash(name));
224        std::fs::create_dir_all(&dir)?;
225        Ok(dir)
226    }
227}
228
229// ─── Certificate encoding ─────────────────────────────────────────────────────
230
231const NDNC_MAGIC: &[u8; 4] = b"NDNC";
232const NDNC_VERSION: u8 = 1;
233
234fn encode_cert(cert: &Certificate) -> Vec<u8> {
235    let pk = cert.public_key.as_ref();
236    let mut buf = Vec::with_capacity(25 + pk.len());
237    buf.extend_from_slice(NDNC_MAGIC);
238    buf.push(NDNC_VERSION);
239    buf.extend_from_slice(&cert.valid_from.to_be_bytes());
240    buf.extend_from_slice(&cert.valid_until.to_be_bytes());
241    buf.extend_from_slice(&(pk.len() as u32).to_be_bytes());
242    buf.extend_from_slice(pk);
243    buf
244}
245
246/// Decode an NDNC-format certificate from raw bytes.
247pub fn decode_cert(name: Arc<Name>, data: &[u8]) -> Result<Certificate, PibError> {
248    if data.len() < 25 {
249        return Err(PibError::Corrupt("cert too short".into()));
250    }
251    if &data[..4] != NDNC_MAGIC {
252        return Err(PibError::Corrupt("invalid magic bytes".into()));
253    }
254    // data[4] = version (reserved for future format changes)
255    let valid_from = u64::from_be_bytes(data[5..13].try_into().unwrap());
256    let valid_until = u64::from_be_bytes(data[13..21].try_into().unwrap());
257    let pk_len = u32::from_be_bytes(data[21..25].try_into().unwrap()) as usize;
258    if data.len() < 25 + pk_len {
259        return Err(PibError::Corrupt("cert data truncated".into()));
260    }
261    let pk = Bytes::copy_from_slice(&data[25..25 + pk_len]);
262    Ok(Certificate {
263        name,
264        public_key: pk,
265        valid_from,
266        valid_until,
267        issuer: None,
268        signed_region: None,
269        sig_value: None,
270    })
271}
272
273// ─── Name helpers ─────────────────────────────────────────────────────────────
274
275/// Compute a hex-encoded SHA-256 of the canonical name bytes for use as a
276/// stable, filesystem-safe directory name.
277fn name_hash(name: &Name) -> String {
278    use ring::digest;
279    let mut bytes: Vec<u8> = Vec::new();
280    for comp in name.components() {
281        let len = comp.value.len() as u32;
282        bytes.extend_from_slice(&len.to_be_bytes());
283        bytes.extend_from_slice(&comp.value);
284    }
285    let hash = digest::digest(&digest::SHA256, &bytes);
286    hex_encode(hash.as_ref())
287}
288
289fn hex_encode(bytes: &[u8]) -> String {
290    bytes.iter().map(|b| format!("{:02x}", b)).collect()
291}
292
293/// Convert a `Name` to its NDN URI representation.
294///
295/// Component bytes that are not URI-safe (alphanumeric, `-`, `.`, `_`, `~`)
296/// are percent-encoded as `%XX`.
297pub fn name_to_uri(name: &Name) -> String {
298    if name.components().is_empty() {
299        return "/".to_string();
300    }
301    name.components()
302        .iter()
303        .map(|c| {
304            let mut s = String::from("/");
305            for &b in c.value.iter() {
306                if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~') {
307                    s.push(b as char);
308                } else {
309                    s.push_str(&format!("%{:02X}", b));
310                }
311            }
312            s
313        })
314        .collect()
315}
316
317/// Parse an NDN URI such as `/ndn/router1` or `/ndn/KEY/%08abc` into a `Name`.
318pub fn name_from_uri(uri: &str) -> Result<Name, PibError> {
319    if uri == "/" || uri.is_empty() {
320        return Ok(Name::root());
321    }
322    let comps: Result<Vec<NameComponent>, PibError> = uri
323        .split('/')
324        .filter(|s| !s.is_empty())
325        .map(|seg| {
326            let mut bytes: Vec<u8> = Vec::new();
327            let seg = seg.as_bytes();
328            let mut i = 0;
329            while i < seg.len() {
330                if seg[i] == b'%' && i + 2 < seg.len() {
331                    let hi = hex_digit(seg[i + 1]).ok_or(PibError::InvalidName)?;
332                    let lo = hex_digit(seg[i + 2]).ok_or(PibError::InvalidName)?;
333                    bytes.push((hi << 4) | lo);
334                    i += 3;
335                } else {
336                    bytes.push(seg[i]);
337                    i += 1;
338                }
339            }
340            Ok(NameComponent::generic(Bytes::from(bytes)))
341        })
342        .collect();
343    Ok(Name::from_components(comps?))
344}
345
346fn hex_digit(b: u8) -> Option<u8> {
347    match b {
348        b'0'..=b'9' => Some(b - b'0'),
349        b'a'..=b'f' => Some(b - b'a' + 10),
350        b'A'..=b'F' => Some(b - b'A' + 10),
351        _ => None,
352    }
353}
354
355fn list_names_in(dir: &Path) -> Result<Vec<Name>, PibError> {
356    if !dir.exists() {
357        return Ok(vec![]);
358    }
359    let mut names = Vec::new();
360    for entry in std::fs::read_dir(dir)? {
361        let entry = entry?;
362        let path = entry.path();
363        if !path.is_dir() {
364            continue;
365        }
366        let uri_path = path.join("name.uri");
367        if uri_path.exists() {
368            let uri = std::fs::read_to_string(&uri_path)?;
369            if let Ok(name) = name_from_uri(uri.trim()) {
370                names.push(name);
371            }
372        }
373    }
374    Ok(names)
375}
376
377// ─── Randomness ───────────────────────────────────────────────────────────────
378
379fn random_seed() -> [u8; 32] {
380    use ring::rand::{SecureRandom, SystemRandom};
381    let rng = SystemRandom::new();
382    let mut seed = [0u8; 32];
383    rng.fill(&mut seed).expect("system RNG unavailable");
384    seed
385}
386
387// ─── Tests ────────────────────────────────────────────────────────────────────
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use crate::Signer;
393    use bytes::Bytes;
394    use ndn_packet::NameComponent;
395    use std::time::{SystemTime, UNIX_EPOCH};
396
397    fn key_name(s: &str) -> Name {
398        Name::from_components([NameComponent::generic(Bytes::copy_from_slice(s.as_bytes()))])
399    }
400
401    fn tmp_pib() -> (tempfile::TempDir, FilePib) {
402        let dir = tempfile::tempdir().unwrap();
403        let pib = FilePib::new(dir.path()).unwrap();
404        (dir, pib)
405    }
406
407    fn now_ns() -> u64 {
408        SystemTime::now()
409            .duration_since(UNIX_EPOCH)
410            .unwrap()
411            .as_nanos() as u64
412    }
413
414    #[test]
415    fn create_pib_creates_dirs() {
416        let dir = tempfile::tempdir().unwrap();
417        FilePib::new(dir.path()).unwrap();
418        assert!(dir.path().join("keys").exists());
419        assert!(dir.path().join("anchors").exists());
420    }
421
422    #[test]
423    fn open_nonexistent_pib_errors() {
424        let r = FilePib::open("/tmp/ndn_pib_nonexistent_xyz_abc");
425        assert!(r.is_err());
426    }
427
428    #[test]
429    fn generate_and_retrieve_signer() {
430        let (_dir, pib) = tmp_pib();
431        let name = key_name("mykey");
432        pib.generate_ed25519(&name).unwrap();
433        let signer = pib.get_signer(&name).unwrap();
434        assert_eq!(signer.key_name(), &name);
435    }
436
437    #[test]
438    fn get_signer_missing_key_errors() {
439        let (_dir, pib) = tmp_pib();
440        let name = key_name("missing");
441        assert!(matches!(
442            pib.get_signer(&name),
443            Err(PibError::KeyNotFound(_))
444        ));
445    }
446
447    #[test]
448    fn delete_key_removes_it() {
449        let (_dir, pib) = tmp_pib();
450        let name = key_name("delkey");
451        pib.generate_ed25519(&name).unwrap();
452        pib.delete_key(&name).unwrap();
453        assert!(matches!(
454            pib.get_signer(&name),
455            Err(PibError::KeyNotFound(_))
456        ));
457    }
458
459    #[test]
460    fn list_keys_returns_all() {
461        let (_dir, pib) = tmp_pib();
462        let n1 = key_name("key1");
463        let n2 = key_name("key2");
464        pib.generate_ed25519(&n1).unwrap();
465        pib.generate_ed25519(&n2).unwrap();
466        let mut keys = pib.list_keys().unwrap();
467        keys.sort_by_key(|a| a.to_string());
468        assert_eq!(keys.len(), 2);
469    }
470
471    #[test]
472    fn store_and_get_cert() {
473        let (_dir, pib) = tmp_pib();
474        let name = key_name("certkey");
475        let signer = pib.generate_ed25519(&name).unwrap();
476        let pk = Bytes::copy_from_slice(&signer.public_key_bytes());
477        let now = now_ns();
478        let cert = Certificate {
479            name: Arc::new(name.clone()),
480            public_key: pk.clone(),
481            valid_from: now,
482            valid_until: now + 365 * 24 * 3600 * 1_000_000_000u64,
483            issuer: None,
484            signed_region: None,
485            sig_value: None,
486        };
487        pib.store_cert(&name, &cert).unwrap();
488        let loaded = pib.get_cert(&name).unwrap();
489        assert_eq!(loaded.public_key, pk);
490        assert_eq!(loaded.valid_from, now);
491    }
492
493    #[test]
494    fn get_cert_missing_errors() {
495        let (_dir, pib) = tmp_pib();
496        let name = key_name("nocert");
497        pib.generate_ed25519(&name).unwrap();
498        assert!(matches!(
499            pib.get_cert(&name),
500            Err(PibError::CertNotFound(_))
501        ));
502    }
503
504    #[test]
505    fn trust_anchor_roundtrip() {
506        let (_dir, pib) = tmp_pib();
507        let name = key_name("anchor");
508        let cert = Certificate {
509            name: Arc::new(name.clone()),
510            public_key: Bytes::from_static(&[0xAB; 32]),
511            valid_from: 0,
512            valid_until: u64::MAX,
513            issuer: None,
514            signed_region: None,
515            sig_value: None,
516        };
517        pib.add_trust_anchor(&name, &cert).unwrap();
518        let anchors = pib.trust_anchors().unwrap();
519        assert_eq!(anchors.len(), 1);
520        assert_eq!(anchors[0].public_key.as_ref(), &[0xABu8; 32]);
521    }
522
523    #[test]
524    fn list_anchors_returns_names() {
525        let (_dir, pib) = tmp_pib();
526        let name = key_name("ta");
527        let cert = Certificate {
528            name: Arc::new(name.clone()),
529            public_key: Bytes::from_static(&[1u8; 32]),
530            valid_from: 0,
531            valid_until: u64::MAX,
532            issuer: None,
533            signed_region: None,
534            sig_value: None,
535        };
536        pib.add_trust_anchor(&name, &cert).unwrap();
537        let names = pib.list_anchors().unwrap();
538        assert_eq!(names.len(), 1);
539    }
540
541    #[test]
542    fn name_uri_roundtrip_ascii() {
543        let name = Name::from_components([
544            NameComponent::generic(Bytes::from_static(b"ndn")),
545            NameComponent::generic(Bytes::from_static(b"router1")),
546        ]);
547        let uri = name_to_uri(&name);
548        assert_eq!(uri, "/ndn/router1");
549        let back = name_from_uri(&uri).unwrap();
550        assert_eq!(back, name);
551    }
552
553    #[test]
554    fn name_uri_roundtrip_binary() {
555        let name = Name::from_components([
556            NameComponent::generic(Bytes::from_static(b"ndn")),
557            NameComponent::generic(Bytes::from(vec![0x08, 0x01, 0xFF])),
558        ]);
559        let uri = name_to_uri(&name);
560        let back = name_from_uri(&uri).unwrap();
561        assert_eq!(back, name);
562    }
563
564    #[test]
565    fn root_name_uri() {
566        let uri = name_to_uri(&Name::root());
567        assert_eq!(uri, "/");
568        let back = name_from_uri(&uri).unwrap();
569        assert_eq!(back, Name::root());
570    }
571
572    #[test]
573    fn cert_encode_decode_roundtrip() {
574        let name = Arc::new(key_name("enc"));
575        let cert = Certificate {
576            name: Arc::clone(&name),
577            public_key: Bytes::from_static(&[0x55; 32]),
578            valid_from: 1_000_000,
579            valid_until: 9_999_999,
580            issuer: None,
581            signed_region: None,
582            sig_value: None,
583        };
584        let encoded = encode_cert(&cert);
585        let decoded = decode_cert(Arc::clone(&name), &encoded).unwrap();
586        assert_eq!(decoded.public_key, cert.public_key);
587        assert_eq!(decoded.valid_from, cert.valid_from);
588        assert_eq!(decoded.valid_until, cert.valid_until);
589    }
590
591    #[test]
592    fn corrupt_cert_errors() {
593        let name = Arc::new(key_name("bad"));
594        assert!(decode_cert(name.clone(), b"").is_err());
595        assert!(decode_cert(name.clone(), b"BADC\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00").is_err());
596    }
597}