ndn_security/
sqlite_pib.rs

1//! SQLite-backed Public Info Base (PIB), wire-compatible with
2//! `ndn-cxx`'s `pib-sqlite3` backend.
3//!
4//! # Compatibility
5//!
6//! An ndn-rs binary using `SqlitePib` should be able to open a `pib.db`
7//! created by `ndnsec` (the ndn-cxx CLI) and operate on it without
8//! corruption, and vice versa. To make that work, this module replicates
9//! the ndn-cxx schema **bit-for-bit** — same tables, same indexes, same
10//! triggers, same column types, same `wireEncode()`-based name storage.
11//! Diverging from the schema in any way (adding `PRAGMA user_version`,
12//! storing names as URI strings, omitting a trigger, ...) would silently
13//! make the resulting database incompatible.
14//!
15//! Pinned to ndn-cxx tag `ndn-cxx-0.9.0`, commit
16//! `0751bba88021b745c1a0ab7198efd279756c9a3c`, file
17//! `ndn-cxx/security/pib/impl/pib-sqlite3.cpp` lines 33–186 (`DB_INIT`).
18//!
19//! # Storage conventions (MUST match ndn-cxx)
20//!
21//! - Default DB path: `$HOME/.ndn/pib.db` (with `TEST_HOME` and CWD
22//!   fallbacks; see [`SqlitePib::open_default`]).
23//! - All `Name` columns hold the **TLV wire encoding** of the Name
24//!   (outer type `0x07` + length + components), not URI strings.
25//! - The `key_bits` column holds raw public-key bytes — for
26//!   ndn-cxx-issued keys, this is a DER-encoded `SubjectPublicKeyInfo`.
27//! - The `certificate_data` column holds the full Data-packet wire
28//!   encoding of the certificate.
29//! - `tpm_locator` is stored as a UTF-8 string in a `BLOB` column.
30//! - `PRAGMA foreign_keys=ON` is set at every connection open. Without
31//!   it the `ON DELETE CASCADE` rules become no-ops, leaking orphan
32//!   rows that `ndnsec` will then trip over.
33//! - Default-row invariants are maintained by triggers, not by Rust
34//!   code. `add_*` calls just `INSERT` and let the triggers do the
35//!   rest. Mutating a row's `is_default` is also delegated.
36
37use std::path::{Path, PathBuf};
38use std::sync::Mutex;
39
40use bytes::Bytes;
41use ndn_packet::{Name, NameComponent, tlv_type};
42use ndn_tlv::{TlvReader, TlvWriter};
43use rusqlite::{Connection, OpenFlags, OptionalExtension, params};
44
45use crate::pib::PibError;
46
47// ─── Schema (verbatim from ndn-cxx pib-sqlite3.cpp DB_INIT) ───────────────────
48
49/// Schema embedded character-for-character from ndn-cxx 0.9.0
50/// `ndn-cxx/security/pib/impl/pib-sqlite3.cpp` lines 33–186. **Do not edit.**
51/// Any divergence from this string will cause silent incompatibility with
52/// `ndnsec` and other ndn-cxx tools that share the same `pib.db`.
53const DB_INIT: &str = r#"
54CREATE TABLE IF NOT EXISTS
55  tpmInfo(
56    tpm_locator           BLOB
57  );
58
59CREATE TABLE IF NOT EXISTS
60  identities(
61    id                    INTEGER PRIMARY KEY,
62    identity              BLOB NOT NULL,
63    is_default            INTEGER DEFAULT 0
64  );
65
66CREATE UNIQUE INDEX IF NOT EXISTS
67  identityIndex ON identities(identity);
68
69CREATE TRIGGER IF NOT EXISTS
70  identity_default_before_insert_trigger
71  BEFORE INSERT ON identities
72  FOR EACH ROW
73  WHEN NEW.is_default=1
74  BEGIN
75    UPDATE identities SET is_default=0;
76  END;
77
78CREATE TRIGGER IF NOT EXISTS
79  identity_default_after_insert_trigger
80  AFTER INSERT ON identities
81  FOR EACH ROW
82  WHEN NOT EXISTS
83    (SELECT id
84       FROM identities
85       WHERE is_default=1)
86  BEGIN
87    UPDATE identities
88      SET is_default=1
89      WHERE identity=NEW.identity;
90  END;
91
92CREATE TRIGGER IF NOT EXISTS
93  identity_default_update_trigger
94  BEFORE UPDATE ON identities
95  FOR EACH ROW
96  WHEN NEW.is_default=1 AND OLD.is_default=0
97  BEGIN
98    UPDATE identities SET is_default=0;
99  END;
100
101CREATE TABLE IF NOT EXISTS
102  keys(
103    id                    INTEGER PRIMARY KEY,
104    identity_id           INTEGER NOT NULL,
105    key_name              BLOB NOT NULL,
106    key_bits              BLOB NOT NULL,
107    is_default            INTEGER DEFAULT 0,
108    FOREIGN KEY(identity_id)
109      REFERENCES identities(id)
110      ON DELETE CASCADE
111      ON UPDATE CASCADE
112  );
113
114CREATE UNIQUE INDEX IF NOT EXISTS
115  keyIndex ON keys(key_name);
116
117CREATE TRIGGER IF NOT EXISTS
118  key_default_before_insert_trigger
119  BEFORE INSERT ON keys
120  FOR EACH ROW
121  WHEN NEW.is_default=1
122  BEGIN
123    UPDATE keys
124      SET is_default=0
125      WHERE identity_id=NEW.identity_id;
126  END;
127
128CREATE TRIGGER IF NOT EXISTS
129  key_default_after_insert_trigger
130  AFTER INSERT ON keys
131  FOR EACH ROW
132  WHEN NOT EXISTS
133    (SELECT id
134       FROM keys
135       WHERE is_default=1
136         AND identity_id=NEW.identity_id)
137  BEGIN
138    UPDATE keys
139      SET is_default=1
140      WHERE key_name=NEW.key_name;
141  END;
142
143CREATE TRIGGER IF NOT EXISTS
144  key_default_update_trigger
145  BEFORE UPDATE ON keys
146  FOR EACH ROW
147  WHEN NEW.is_default=1 AND OLD.is_default=0
148  BEGIN
149    UPDATE keys
150      SET is_default=0
151      WHERE identity_id=NEW.identity_id;
152  END;
153
154CREATE TABLE IF NOT EXISTS
155  certificates(
156    id                    INTEGER PRIMARY KEY,
157    key_id                INTEGER NOT NULL,
158    certificate_name      BLOB NOT NULL,
159    certificate_data      BLOB NOT NULL,
160    is_default            INTEGER DEFAULT 0,
161    FOREIGN KEY(key_id)
162      REFERENCES keys(id)
163      ON DELETE CASCADE
164      ON UPDATE CASCADE
165  );
166
167CREATE UNIQUE INDEX IF NOT EXISTS
168  certIndex ON certificates(certificate_name);
169
170CREATE TRIGGER IF NOT EXISTS
171  cert_default_before_insert_trigger
172  BEFORE INSERT ON certificates
173  FOR EACH ROW
174  WHEN NEW.is_default=1
175  BEGIN
176    UPDATE certificates
177      SET is_default=0
178      WHERE key_id=NEW.key_id;
179  END;
180
181CREATE TRIGGER IF NOT EXISTS
182  cert_default_after_insert_trigger
183  AFTER INSERT ON certificates
184  FOR EACH ROW
185  WHEN NOT EXISTS
186    (SELECT id
187       FROM certificates
188       WHERE is_default=1
189         AND key_id=NEW.key_id)
190  BEGIN
191    UPDATE certificates
192      SET is_default=1
193      WHERE certificate_name=NEW.certificate_name;
194  END;
195
196CREATE TRIGGER IF NOT EXISTS
197  cert_default_update_trigger
198  BEFORE UPDATE ON certificates
199  FOR EACH ROW
200  WHEN NEW.is_default=1 AND OLD.is_default=0
201  BEGIN
202    UPDATE certificates
203      SET is_default=0
204      WHERE key_id=NEW.key_id;
205  END;
206"#;
207
208// ─── Name <-> wire-format BLOB ────────────────────────────────────────────────
209
210/// Encode a `Name` to its canonical TLV wire form (outer type `0x07` +
211/// length + components), as ndn-cxx's `Name::wireEncode()` produces.
212/// This is the byte sequence stored in the `identity`, `key_name`, and
213/// `certificate_name` BLOB columns of the SQLite PIB.
214fn name_wire_encode(name: &Name) -> Vec<u8> {
215    let mut w = TlvWriter::new();
216    w.write_nested(tlv_type::NAME, |w| {
217        for c in name.components() {
218            w.write_tlv(c.typ, &c.value);
219        }
220    });
221    w.finish().to_vec()
222}
223
224/// Decode a Name from a wire-format BLOB read from the SQLite PIB.
225/// The blob is expected to be `[type=0x07] [length] [components]`.
226fn name_wire_decode(blob: &[u8]) -> Result<Name, PibError> {
227    let mut reader = TlvReader::new(Bytes::copy_from_slice(blob));
228    let (typ, value) = reader
229        .read_tlv()
230        .map_err(|e| PibError::Corrupt(format!("name TLV: {e:?}")))?;
231    if typ != tlv_type::NAME {
232        return Err(PibError::Corrupt(format!(
233            "expected Name TLV (0x07), got 0x{typ:x}"
234        )));
235    }
236    Name::decode(value).map_err(|e| PibError::Corrupt(format!("name body: {e:?}")))
237}
238
239// ─── SqlitePib ────────────────────────────────────────────────────────────────
240
241/// SQLite-backed PIB, wire-compatible with ndn-cxx `pib-sqlite3`.
242///
243/// All public methods take `&self` and serialise through an internal
244/// `Mutex<Connection>`. SQLite handles its own concurrency control, but
245/// `rusqlite::Connection` is not `Sync` so we wrap it.
246pub struct SqlitePib {
247    conn: Mutex<Connection>,
248    path: PathBuf,
249}
250
251impl SqlitePib {
252    /// Open or create a `pib.db` at `path`, initialising the schema if
253    /// absent. Equivalent to ndn-cxx's `PibSqlite3(location)` constructor
254    /// when `location` points at a directory; here we expect the full
255    /// file path.
256    pub fn open(path: impl AsRef<Path>) -> Result<Self, PibError> {
257        let path = path.as_ref().to_path_buf();
258        if let Some(parent) = path.parent() {
259            std::fs::create_dir_all(parent)?;
260        }
261        let conn = Connection::open_with_flags(
262            &path,
263            OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
264        )
265        .map_err(map_sqlite_err)?;
266
267        // MANDATORY for ndn-cxx compatibility — without it, the cascade
268        // rules on the `keys` and `certificates` foreign keys silently
269        // become no-ops and orphan rows accumulate. ndn-cxx sets this on
270        // every open (`pib-sqlite3.cpp:223`); we must too.
271        conn.execute_batch("PRAGMA foreign_keys=ON;")
272            .map_err(map_sqlite_err)?;
273
274        // `IF NOT EXISTS` everywhere makes this idempotent — running
275        // against an existing ndn-cxx-created DB is a no-op.
276        conn.execute_batch(DB_INIT).map_err(map_sqlite_err)?;
277
278        Ok(Self {
279            conn: Mutex::new(conn),
280            path,
281        })
282    }
283
284    /// Open the default PIB at `$HOME/.ndn/pib.db`, mirroring
285    /// `PibSqlite3()` with an empty location argument. Honours
286    /// `TEST_HOME` first (for parity with ndn-cxx test harnesses), then
287    /// `HOME`, then the current working directory.
288    pub fn open_default() -> Result<Self, PibError> {
289        let dir = if let Ok(p) = std::env::var("TEST_HOME") {
290            PathBuf::from(p).join(".ndn")
291        } else if let Ok(p) = std::env::var("HOME") {
292            PathBuf::from(p).join(".ndn")
293        } else {
294            std::env::current_dir()?.join(".ndn")
295        };
296        Self::open(dir.join("pib.db"))
297    }
298
299    /// Path to the underlying database file.
300    pub fn path(&self) -> &Path {
301        &self.path
302    }
303
304    // ── tpmInfo ──────────────────────────────────────────────────────────────
305
306    /// Return the TPM locator string the PIB was last associated with,
307    /// e.g. `"tpm-file:"` or `"tpm-file:/custom/path"`. `None` if no
308    /// locator has ever been set on this DB.
309    pub fn get_tpm_locator(&self) -> Result<Option<String>, PibError> {
310        let conn = self.conn.lock().expect("sqlite mutex poisoned");
311        let row = conn
312            .query_row("SELECT tpm_locator FROM tpmInfo", [], |row| {
313                row.get::<_, Vec<u8>>(0)
314            })
315            .optional()
316            .map_err(map_sqlite_err)?;
317        Ok(row.map(|bytes| String::from_utf8_lossy(&bytes).into_owned()))
318    }
319
320    /// Set the TPM locator string. Mirrors ndn-cxx's update-then-insert
321    /// dance (no SQLite UPSERT was available when the schema was
322    /// designed): try `UPDATE`, and if no row was affected, `INSERT`.
323    pub fn set_tpm_locator(&self, locator: &str) -> Result<(), PibError> {
324        let conn = self.conn.lock().expect("sqlite mutex poisoned");
325        let updated = conn
326            .execute(
327                "UPDATE tpmInfo SET tpm_locator=?",
328                params![locator.as_bytes()],
329            )
330            .map_err(map_sqlite_err)?;
331        if updated == 0 {
332            conn.execute(
333                "INSERT INTO tpmInfo (tpm_locator) VALUES (?)",
334                params![locator.as_bytes()],
335            )
336            .map_err(map_sqlite_err)?;
337        }
338        Ok(())
339    }
340
341    // ── identities ───────────────────────────────────────────────────────────
342
343    /// Add `identity` to the PIB. Idempotent: re-adding an existing
344    /// identity is a no-op (the unique index on `identity` would otherwise
345    /// reject it).
346    pub fn add_identity(&self, identity: &Name) -> Result<(), PibError> {
347        let conn = self.conn.lock().expect("sqlite mutex poisoned");
348        let blob = name_wire_encode(identity);
349        let existing: Option<i64> = conn
350            .query_row(
351                "SELECT id FROM identities WHERE identity=?",
352                params![blob],
353                |row| row.get(0),
354            )
355            .optional()
356            .map_err(map_sqlite_err)?;
357        if existing.is_none() {
358            conn.execute(
359                "INSERT INTO identities (identity) VALUES (?)",
360                params![blob],
361            )
362            .map_err(map_sqlite_err)?;
363        }
364        Ok(())
365    }
366
367    /// Return `true` if the named identity is in the PIB.
368    pub fn has_identity(&self, identity: &Name) -> Result<bool, PibError> {
369        let conn = self.conn.lock().expect("sqlite mutex poisoned");
370        let blob = name_wire_encode(identity);
371        Ok(conn
372            .query_row(
373                "SELECT id FROM identities WHERE identity=?",
374                params![blob],
375                |row| row.get::<_, i64>(0),
376            )
377            .optional()
378            .map_err(map_sqlite_err)?
379            .is_some())
380    }
381
382    /// Delete an identity and (via `ON DELETE CASCADE`) all keys and
383    /// certificates rooted at it.
384    pub fn delete_identity(&self, identity: &Name) -> Result<(), PibError> {
385        let conn = self.conn.lock().expect("sqlite mutex poisoned");
386        let blob = name_wire_encode(identity);
387        conn.execute("DELETE FROM identities WHERE identity=?", params![blob])
388            .map_err(map_sqlite_err)?;
389        Ok(())
390    }
391
392    /// List all identities in the PIB, in insertion order.
393    pub fn list_identities(&self) -> Result<Vec<Name>, PibError> {
394        let conn = self.conn.lock().expect("sqlite mutex poisoned");
395        let mut stmt = conn
396            .prepare("SELECT identity FROM identities")
397            .map_err(map_sqlite_err)?;
398        let rows = stmt
399            .query_map([], |row| row.get::<_, Vec<u8>>(0))
400            .map_err(map_sqlite_err)?;
401        let mut out = Vec::new();
402        for row in rows {
403            let blob = row.map_err(map_sqlite_err)?;
404            out.push(name_wire_decode(&blob)?);
405        }
406        Ok(out)
407    }
408
409    /// Mark `identity` as the default. The trigger
410    /// `identity_default_update_trigger` clears the previous default in
411    /// the same operation, so callers do not need to do it manually.
412    pub fn set_default_identity(&self, identity: &Name) -> Result<(), PibError> {
413        let conn = self.conn.lock().expect("sqlite mutex poisoned");
414        let blob = name_wire_encode(identity);
415        let updated = conn
416            .execute(
417                "UPDATE identities SET is_default=1 WHERE identity=?",
418                params![blob],
419            )
420            .map_err(map_sqlite_err)?;
421        if updated == 0 {
422            return Err(PibError::KeyNotFound(name_to_string(identity)));
423        }
424        Ok(())
425    }
426
427    /// Return the current default identity, if one is set.
428    pub fn get_default_identity(&self) -> Result<Option<Name>, PibError> {
429        let conn = self.conn.lock().expect("sqlite mutex poisoned");
430        let blob: Option<Vec<u8>> = conn
431            .query_row(
432                "SELECT identity FROM identities WHERE is_default=1",
433                [],
434                |row| row.get(0),
435            )
436            .optional()
437            .map_err(map_sqlite_err)?;
438        Ok(match blob {
439            Some(b) => Some(name_wire_decode(&b)?),
440            None => None,
441        })
442    }
443
444    // ── keys ─────────────────────────────────────────────────────────────────
445
446    /// Add a key under an existing identity. The identity must already
447    /// exist; ndn-cxx's `addKey` adds it implicitly via a subquery, so
448    /// we do the same here. `key_bits` is the raw public-key bytes (for
449    /// ndn-cxx-issued keys this is a DER `SubjectPublicKeyInfo`).
450    pub fn add_key(
451        &self,
452        identity: &Name,
453        key_name: &Name,
454        key_bits: &[u8],
455    ) -> Result<(), PibError> {
456        let conn = self.conn.lock().expect("sqlite mutex poisoned");
457        let id_blob = name_wire_encode(identity);
458        let key_blob = name_wire_encode(key_name);
459        // Ensure the identity exists first so the subquery in INSERT
460        // resolves to a valid id (rather than NULL → NOT NULL violation).
461        let existing: Option<i64> = conn
462            .query_row(
463                "SELECT id FROM identities WHERE identity=?",
464                params![id_blob],
465                |row| row.get(0),
466            )
467            .optional()
468            .map_err(map_sqlite_err)?;
469        if existing.is_none() {
470            conn.execute(
471                "INSERT INTO identities (identity) VALUES (?)",
472                params![id_blob],
473            )
474            .map_err(map_sqlite_err)?;
475        }
476
477        // Existing key with same name → UPDATE the bits in place (matches
478        // ndn-cxx behaviour at pib-sqlite3.cpp:376–377).
479        let key_exists: Option<i64> = conn
480            .query_row(
481                "SELECT id FROM keys WHERE key_name=?",
482                params![key_blob],
483                |row| row.get(0),
484            )
485            .optional()
486            .map_err(map_sqlite_err)?;
487        if key_exists.is_some() {
488            conn.execute(
489                "UPDATE keys SET key_bits=? WHERE key_name=?",
490                params![key_bits, key_blob],
491            )
492            .map_err(map_sqlite_err)?;
493        } else {
494            conn.execute(
495                "INSERT INTO keys (identity_id, key_name, key_bits) \
496                 VALUES ((SELECT id FROM identities WHERE identity=?), ?, ?)",
497                params![id_blob, key_blob, key_bits],
498            )
499            .map_err(map_sqlite_err)?;
500        }
501        Ok(())
502    }
503
504    /// Return the raw `key_bits` (public-key BLOB) for a key, if present.
505    pub fn get_key_bits(&self, key_name: &Name) -> Result<Option<Vec<u8>>, PibError> {
506        let conn = self.conn.lock().expect("sqlite mutex poisoned");
507        let blob = name_wire_encode(key_name);
508        conn.query_row(
509            "SELECT key_bits FROM keys WHERE key_name=?",
510            params![blob],
511            |row| row.get(0),
512        )
513        .optional()
514        .map_err(map_sqlite_err)
515    }
516
517    /// Delete a key and (via cascade) all certificates issued under it.
518    pub fn delete_key(&self, key_name: &Name) -> Result<(), PibError> {
519        let conn = self.conn.lock().expect("sqlite mutex poisoned");
520        let blob = name_wire_encode(key_name);
521        conn.execute("DELETE FROM keys WHERE key_name=?", params![blob])
522            .map_err(map_sqlite_err)?;
523        Ok(())
524    }
525
526    /// List all keys under `identity`, in insertion order.
527    pub fn list_keys(&self, identity: &Name) -> Result<Vec<Name>, PibError> {
528        let conn = self.conn.lock().expect("sqlite mutex poisoned");
529        let blob = name_wire_encode(identity);
530        let mut stmt = conn
531            .prepare(
532                "SELECT key_name FROM keys \
533                 JOIN identities ON keys.identity_id=identities.id \
534                 WHERE identities.identity=?",
535            )
536            .map_err(map_sqlite_err)?;
537        let rows = stmt
538            .query_map(params![blob], |row| row.get::<_, Vec<u8>>(0))
539            .map_err(map_sqlite_err)?;
540        let mut out = Vec::new();
541        for row in rows {
542            let kb = row.map_err(map_sqlite_err)?;
543            out.push(name_wire_decode(&kb)?);
544        }
545        Ok(out)
546    }
547
548    /// Mark `key_name` as the default key for its parent identity.
549    pub fn set_default_key(&self, key_name: &Name) -> Result<(), PibError> {
550        let conn = self.conn.lock().expect("sqlite mutex poisoned");
551        let blob = name_wire_encode(key_name);
552        let updated = conn
553            .execute(
554                "UPDATE keys SET is_default=1 WHERE key_name=?",
555                params![blob],
556            )
557            .map_err(map_sqlite_err)?;
558        if updated == 0 {
559            return Err(PibError::KeyNotFound(name_to_string(key_name)));
560        }
561        Ok(())
562    }
563
564    /// Return the default key for `identity`, if one is set.
565    pub fn get_default_key(&self, identity: &Name) -> Result<Option<Name>, PibError> {
566        let conn = self.conn.lock().expect("sqlite mutex poisoned");
567        let blob = name_wire_encode(identity);
568        let row: Option<Vec<u8>> = conn
569            .query_row(
570                "SELECT key_name FROM keys \
571                 JOIN identities ON keys.identity_id=identities.id \
572                 WHERE identities.identity=? AND keys.is_default=1",
573                params![blob],
574                |row| row.get(0),
575            )
576            .optional()
577            .map_err(map_sqlite_err)?;
578        Ok(match row {
579            Some(b) => Some(name_wire_decode(&b)?),
580            None => None,
581        })
582    }
583
584    // ── certificates ─────────────────────────────────────────────────────────
585
586    /// Add a certificate under an existing key. The key must already
587    /// exist (we don't auto-create it; ndn-cxx fails the foreign key
588    /// constraint here too).
589    pub fn add_certificate(
590        &self,
591        key_name: &Name,
592        cert_name: &Name,
593        cert_data: &[u8],
594    ) -> Result<(), PibError> {
595        let conn = self.conn.lock().expect("sqlite mutex poisoned");
596        let key_blob = name_wire_encode(key_name);
597        let cert_blob = name_wire_encode(cert_name);
598        let existing: Option<i64> = conn
599            .query_row(
600                "SELECT id FROM certificates WHERE certificate_name=?",
601                params![cert_blob],
602                |row| row.get(0),
603            )
604            .optional()
605            .map_err(map_sqlite_err)?;
606        if existing.is_some() {
607            conn.execute(
608                "UPDATE certificates SET certificate_data=? WHERE certificate_name=?",
609                params![cert_data, cert_blob],
610            )
611            .map_err(map_sqlite_err)?;
612        } else {
613            conn.execute(
614                "INSERT INTO certificates (key_id, certificate_name, certificate_data) \
615                 VALUES ((SELECT id FROM keys WHERE key_name=?), ?, ?)",
616                params![key_blob, cert_blob, cert_data],
617            )
618            .map_err(map_sqlite_err)?;
619        }
620        Ok(())
621    }
622
623    /// Return the full Data-wire bytes of `cert_name`, if present.
624    pub fn get_certificate(&self, cert_name: &Name) -> Result<Option<Vec<u8>>, PibError> {
625        let conn = self.conn.lock().expect("sqlite mutex poisoned");
626        let blob = name_wire_encode(cert_name);
627        conn.query_row(
628            "SELECT certificate_data FROM certificates WHERE certificate_name=?",
629            params![blob],
630            |row| row.get(0),
631        )
632        .optional()
633        .map_err(map_sqlite_err)
634    }
635
636    /// Delete a certificate by name.
637    pub fn delete_certificate(&self, cert_name: &Name) -> Result<(), PibError> {
638        let conn = self.conn.lock().expect("sqlite mutex poisoned");
639        let blob = name_wire_encode(cert_name);
640        conn.execute(
641            "DELETE FROM certificates WHERE certificate_name=?",
642            params![blob],
643        )
644        .map_err(map_sqlite_err)?;
645        Ok(())
646    }
647
648    /// List all certificate names issued under `key_name`.
649    pub fn list_certificates(&self, key_name: &Name) -> Result<Vec<Name>, PibError> {
650        let conn = self.conn.lock().expect("sqlite mutex poisoned");
651        let blob = name_wire_encode(key_name);
652        let mut stmt = conn
653            .prepare(
654                "SELECT certificate_name FROM certificates \
655                 JOIN keys ON certificates.key_id=keys.id \
656                 WHERE keys.key_name=?",
657            )
658            .map_err(map_sqlite_err)?;
659        let rows = stmt
660            .query_map(params![blob], |row| row.get::<_, Vec<u8>>(0))
661            .map_err(map_sqlite_err)?;
662        let mut out = Vec::new();
663        for row in rows {
664            let cb = row.map_err(map_sqlite_err)?;
665            out.push(name_wire_decode(&cb)?);
666        }
667        Ok(out)
668    }
669
670    /// Mark `cert_name` as the default cert for its parent key.
671    pub fn set_default_certificate(&self, cert_name: &Name) -> Result<(), PibError> {
672        let conn = self.conn.lock().expect("sqlite mutex poisoned");
673        let blob = name_wire_encode(cert_name);
674        let updated = conn
675            .execute(
676                "UPDATE certificates SET is_default=1 WHERE certificate_name=?",
677                params![blob],
678            )
679            .map_err(map_sqlite_err)?;
680        if updated == 0 {
681            return Err(PibError::CertNotFound(name_to_string(cert_name)));
682        }
683        Ok(())
684    }
685
686    /// Return the default certificate Data-wire for `key_name`, if any.
687    pub fn get_default_certificate(&self, key_name: &Name) -> Result<Option<Vec<u8>>, PibError> {
688        let conn = self.conn.lock().expect("sqlite mutex poisoned");
689        let blob = name_wire_encode(key_name);
690        conn.query_row(
691            "SELECT certificate_data FROM certificates \
692             JOIN keys ON certificates.key_id=keys.id \
693             WHERE certificates.is_default=1 AND keys.key_name=?",
694            params![blob],
695            |row| row.get(0),
696        )
697        .optional()
698        .map_err(map_sqlite_err)
699    }
700}
701
702// ─── Error mapping ────────────────────────────────────────────────────────────
703
704fn map_sqlite_err(e: rusqlite::Error) -> PibError {
705    PibError::Corrupt(format!("sqlite: {e}"))
706}
707
708fn name_to_string(name: &Name) -> String {
709    let mut s = String::new();
710    for c in name.components() {
711        s.push('/');
712        // Best-effort URI-ish — only used in error messages.
713        for &b in c.value.iter() {
714            if b.is_ascii_graphic() && b != b'/' {
715                s.push(b as char);
716            } else {
717                s.push_str(&format!("%{b:02X}"));
718            }
719        }
720    }
721    if s.is_empty() { "/".into() } else { s }
722}
723
724// Suppress unused-import warning for NameComponent on builds that only
725// hit the public surface. The tests below use it.
726#[allow(dead_code)]
727fn _force_use(_c: NameComponent) {}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732    use tempfile::tempdir;
733
734    fn comp(s: &'static str) -> NameComponent {
735        NameComponent::generic(Bytes::from_static(s.as_bytes()))
736    }
737    fn name(parts: &[&'static str]) -> Name {
738        Name::from_components(parts.iter().map(|p| comp(p)))
739    }
740
741    #[test]
742    fn wire_roundtrip_through_name_helpers() {
743        let n = name(&["alice", "KEY", "k1"]);
744        let blob = name_wire_encode(&n);
745        // Outer type=0x07, length, then 3 generic-component TLVs.
746        assert_eq!(blob[0], 0x07);
747        let decoded = name_wire_decode(&blob).unwrap();
748        assert_eq!(decoded, n);
749    }
750
751    #[test]
752    fn open_creates_schema() {
753        let dir = tempdir().unwrap();
754        let pib = SqlitePib::open(dir.path().join("pib.db")).unwrap();
755        // Verify all four tables exist by reading sqlite_master.
756        let conn = pib.conn.lock().unwrap();
757        let tables: Vec<String> = conn
758            .prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
759            .unwrap()
760            .query_map([], |row| row.get(0))
761            .unwrap()
762            .map(|r| r.unwrap())
763            .collect();
764        assert_eq!(
765            tables,
766            vec!["certificates", "identities", "keys", "tpmInfo"]
767        );
768    }
769
770    #[test]
771    fn identity_add_list_delete() {
772        let dir = tempdir().unwrap();
773        let pib = SqlitePib::open(dir.path().join("pib.db")).unwrap();
774        let alice = name(&["alice"]);
775        let bob = name(&["bob"]);
776        pib.add_identity(&alice).unwrap();
777        pib.add_identity(&bob).unwrap();
778        pib.add_identity(&alice).unwrap(); // idempotent
779        let listed = pib.list_identities().unwrap();
780        assert_eq!(listed.len(), 2);
781        assert!(listed.contains(&alice));
782        assert!(listed.contains(&bob));
783        pib.delete_identity(&alice).unwrap();
784        let listed = pib.list_identities().unwrap();
785        assert_eq!(listed, vec![bob]);
786    }
787
788    #[test]
789    fn first_identity_becomes_default_via_trigger() {
790        let dir = tempdir().unwrap();
791        let pib = SqlitePib::open(dir.path().join("pib.db")).unwrap();
792        let alice = name(&["alice"]);
793        pib.add_identity(&alice).unwrap();
794        // The `identity_default_after_insert_trigger` should have promoted
795        // the only row to default automatically.
796        let def = pib.get_default_identity().unwrap();
797        assert_eq!(def, Some(alice));
798    }
799
800    #[test]
801    fn set_default_identity_clears_previous() {
802        let dir = tempdir().unwrap();
803        let pib = SqlitePib::open(dir.path().join("pib.db")).unwrap();
804        let alice = name(&["alice"]);
805        let bob = name(&["bob"]);
806        pib.add_identity(&alice).unwrap();
807        pib.add_identity(&bob).unwrap();
808        // Alice was first → default. Promote bob and ensure alice is no
809        // longer marked default (the update trigger handles this).
810        pib.set_default_identity(&bob).unwrap();
811        assert_eq!(pib.get_default_identity().unwrap(), Some(bob));
812    }
813
814    #[test]
815    fn key_under_identity_with_cert() {
816        let dir = tempdir().unwrap();
817        let pib = SqlitePib::open(dir.path().join("pib.db")).unwrap();
818        let alice = name(&["alice"]);
819        let key1 = name(&["alice", "KEY", "k1"]);
820        let cert1 = name(&["alice", "KEY", "k1", "self", "v=1"]);
821        pib.add_identity(&alice).unwrap();
822        pib.add_key(&alice, &key1, &[0xAA; 32]).unwrap();
823        pib.add_certificate(&key1, &cert1, &[0xCC; 64]).unwrap();
824
825        assert_eq!(pib.get_key_bits(&key1).unwrap().unwrap(), vec![0xAA; 32]);
826        assert_eq!(pib.list_keys(&alice).unwrap(), vec![key1.clone()]);
827        assert_eq!(pib.list_certificates(&key1).unwrap(), vec![cert1.clone()]);
828        assert_eq!(pib.get_default_key(&alice).unwrap(), Some(key1.clone()));
829        assert_eq!(
830            pib.get_default_certificate(&key1).unwrap().unwrap(),
831            vec![0xCC; 64]
832        );
833    }
834
835    #[test]
836    fn delete_identity_cascades_to_keys_and_certs() {
837        let dir = tempdir().unwrap();
838        let pib = SqlitePib::open(dir.path().join("pib.db")).unwrap();
839        let alice = name(&["alice"]);
840        let key1 = name(&["alice", "KEY", "k1"]);
841        let cert1 = name(&["alice", "KEY", "k1", "self", "v=1"]);
842        pib.add_identity(&alice).unwrap();
843        pib.add_key(&alice, &key1, &[0xAA; 32]).unwrap();
844        pib.add_certificate(&key1, &cert1, &[0xCC; 64]).unwrap();
845        pib.delete_identity(&alice).unwrap();
846        // Cascade should have wiped the key and the cert.
847        assert!(pib.get_key_bits(&key1).unwrap().is_none());
848        assert!(pib.get_certificate(&cert1).unwrap().is_none());
849    }
850
851    #[test]
852    fn tpm_locator_roundtrip() {
853        let dir = tempdir().unwrap();
854        let pib = SqlitePib::open(dir.path().join("pib.db")).unwrap();
855        assert_eq!(pib.get_tpm_locator().unwrap(), None);
856        pib.set_tpm_locator("tpm-file:").unwrap();
857        assert_eq!(pib.get_tpm_locator().unwrap(), Some("tpm-file:".into()));
858        // Update path: setting again replaces (single-row table).
859        pib.set_tpm_locator("tpm-file:/custom/path").unwrap();
860        assert_eq!(
861            pib.get_tpm_locator().unwrap(),
862            Some("tpm-file:/custom/path".into())
863        );
864    }
865
866    #[test]
867    fn reopen_persists_state() {
868        let dir = tempdir().unwrap();
869        let path = dir.path().join("pib.db");
870        let alice = name(&["alice"]);
871        let key1 = name(&["alice", "KEY", "k1"]);
872        {
873            let pib = SqlitePib::open(&path).unwrap();
874            pib.add_identity(&alice).unwrap();
875            pib.add_key(&alice, &key1, &[0xBB; 32]).unwrap();
876            pib.set_tpm_locator("tpm-file:").unwrap();
877        }
878        let pib = SqlitePib::open(&path).unwrap();
879        assert_eq!(pib.list_identities().unwrap(), vec![alice]);
880        assert_eq!(pib.get_key_bits(&key1).unwrap().unwrap(), vec![0xBB; 32]);
881        assert_eq!(pib.get_tpm_locator().unwrap(), Some("tpm-file:".into()));
882    }
883
884    #[test]
885    fn delete_key_cascades_to_certs() {
886        let dir = tempdir().unwrap();
887        let pib = SqlitePib::open(dir.path().join("pib.db")).unwrap();
888        let alice = name(&["alice"]);
889        let key1 = name(&["alice", "KEY", "k1"]);
890        let cert1 = name(&["alice", "KEY", "k1", "self", "v=1"]);
891        pib.add_identity(&alice).unwrap();
892        pib.add_key(&alice, &key1, &[0; 32]).unwrap();
893        pib.add_certificate(&key1, &cert1, &[0; 64]).unwrap();
894        pib.delete_key(&key1).unwrap();
895        assert!(pib.get_certificate(&cert1).unwrap().is_none());
896    }
897
898    #[test]
899    fn re_add_key_updates_bits() {
900        let dir = tempdir().unwrap();
901        let pib = SqlitePib::open(dir.path().join("pib.db")).unwrap();
902        let alice = name(&["alice"]);
903        let key1 = name(&["alice", "KEY", "k1"]);
904        pib.add_identity(&alice).unwrap();
905        pib.add_key(&alice, &key1, &[0x11; 32]).unwrap();
906        pib.add_key(&alice, &key1, &[0x22; 32]).unwrap();
907        assert_eq!(pib.get_key_bits(&key1).unwrap().unwrap(), vec![0x22; 32]);
908    }
909}