ndn_security/
safe_bag.rs

1//! `SafeBag` — ndn-cxx interop wrapper for transferring an identity
2//! (a certificate plus its password-encrypted private key) between
3//! machines.
4//!
5//! # Wire format
6//!
7//! Pinned to ndn-cxx tag `ndn-cxx-0.9.0`, files
8//! `ndn-cxx/encoding/tlv-security.hpp:34-35` and
9//! `ndn-cxx/security/safe-bag.{hpp,cpp}`. Spec link inside ndn-cxx:
10//! `<a href="../specs/safe-bag.html">`. The wire layout is two nested
11//! TLVs inside a SafeBag outer TLV:
12//!
13//! ```text
14//! SafeBag (TLV 128 = 0x80) {
15//!   Data         (TLV 6 = 0x06) -- the full certificate Data packet
16//!   EncryptedKey (TLV 129 = 0x81) -- PKCS#8 EncryptedPrivateKeyInfo DER
17//! }
18//! ```
19//!
20//! The certificate is stored as the **complete** Data packet wire
21//! encoding including its own outer `0x06` header. The EncryptedKey
22//! body is the raw DER of an `EncryptedPrivateKeyInfo` produced by
23//! the rustcrypto `pkcs8` crate's `encryption` feature, which in turn
24//! uses PBES2 with PBKDF2-HMAC-SHA256 for key derivation and AES-256-
25//! CBC for content encryption — exactly the defaults that OpenSSL's
26//! `i2d_PKCS8PrivateKey_bio` produces on modern releases, which is
27//! what ndn-cxx's `BackEndFile::doExportKey` calls.
28//!
29//! # Algorithm support (path C of the FileTpm design discussion)
30//!
31//! - **RSA** — convert PKCS#1 `RSAPrivateKey` (FileTpm on-disk form)
32//!   to PKCS#8 `PrivateKeyInfo`, then encrypt. Roundtrips with
33//!   `ndnsec export` / `ndnsec import`.
34//! - **ECDSA-P256** — convert SEC1 `ECPrivateKey` to PKCS#8, then
35//!   encrypt. Roundtrips with ndnsec.
36//! - **Ed25519** — already PKCS#8 on disk (sentinel suffix); encrypt
37//!   directly. ndn-rs ↔ ndn-rs interop only — ndn-cxx tpm-file does
38//!   not handle Ed25519 keys regardless of how they're transferred.
39
40use bytes::Bytes;
41use ndn_packet::tlv_type;
42use ndn_tlv::{TlvReader, TlvWriter};
43
44use crate::file_tpm::{FileTpmError, TpmKeyKind};
45
46// SafeBag TLV type codes — pinned from ndn-cxx encoding/tlv-security.hpp.
47const TLV_SAFE_BAG: u64 = 0x80; // 128
48const TLV_ENCRYPTED_KEY: u64 = 0x81; // 129
49
50/// Errors specific to SafeBag encode/decode and PKCS#8 encryption.
51#[derive(Debug, thiserror::Error)]
52pub enum SafeBagError {
53    #[error("malformed SafeBag TLV: {0}")]
54    Malformed(String),
55    #[error("PKCS#8 encryption error: {0}")]
56    Pkcs8(String),
57    #[error("key conversion error: {0}")]
58    KeyConversion(String),
59    #[error("file tpm error: {0}")]
60    Tpm(#[from] FileTpmError),
61    #[error("unsupported algorithm in SafeBag: {0}")]
62    UnsupportedAlgorithm(String),
63}
64
65/// A decoded SafeBag — the certificate Data wire bytes and the
66/// password-encrypted PKCS#8 private key DER.
67#[derive(Clone, Debug)]
68pub struct SafeBag {
69    /// Full wire-encoded certificate Data packet (TLV starting at
70    /// type 0x06). Opaque to SafeBag itself; the caller hands this to
71    /// the PIB or a Data decoder.
72    pub certificate: Bytes,
73    /// `EncryptedPrivateKeyInfo` DER per RFC 5958 / PKCS#8. Use
74    /// [`SafeBag::decrypt_key`] with the export password to recover
75    /// the unencrypted PKCS#8 PrivateKeyInfo.
76    pub encrypted_key: Bytes,
77}
78
79impl SafeBag {
80    /// Encode the SafeBag to its TLV wire form. Output starts with
81    /// `0x80` and is suitable for writing to a file or passing to
82    /// `ndnsec import`.
83    pub fn encode(&self) -> Bytes {
84        let mut w = TlvWriter::new();
85        w.write_nested(TLV_SAFE_BAG, |w| {
86            // Certificate is already a complete TLV (type 0x06 +
87            // length + body); splice it in raw via write_raw rather
88            // than re-wrapping with write_tlv.
89            w.write_raw(&self.certificate);
90            w.write_tlv(TLV_ENCRYPTED_KEY, &self.encrypted_key);
91        });
92        w.finish()
93    }
94
95    /// Decode a SafeBag from its TLV wire form. Tolerates trailing
96    /// bytes after the outer SafeBag TLV (per the TLV spec, anything
97    /// after the encoded length is the next packet).
98    pub fn decode(wire: &[u8]) -> Result<Self, SafeBagError> {
99        let mut outer = TlvReader::new(Bytes::copy_from_slice(wire));
100        let (typ, body) = outer
101            .read_tlv()
102            .map_err(|e| SafeBagError::Malformed(format!("outer TLV: {e:?}")))?;
103        if typ != TLV_SAFE_BAG {
104            return Err(SafeBagError::Malformed(format!(
105                "expected SafeBag (0x80), got 0x{typ:x}"
106            )));
107        }
108
109        // Inside the SafeBag: first the Data certificate (type 0x06
110        // + length + body), then the EncryptedKey TLV (type 0x81).
111        // We need to re-emit the certificate with its outer header
112        // because TlvReader consumed it; capture the type and length
113        // and rebuild.
114        let mut inner = TlvReader::new(body);
115
116        let (cert_type, cert_body) = inner
117            .read_tlv()
118            .map_err(|e| SafeBagError::Malformed(format!("certificate TLV: {e:?}")))?;
119        if cert_type != tlv_type::DATA {
120            return Err(SafeBagError::Malformed(format!(
121                "expected Data (0x06) inside SafeBag, got 0x{cert_type:x}"
122            )));
123        }
124        // Re-emit the Data TLV header + body so callers receive the
125        // full wire-encoded certificate they can pass to a decoder.
126        let mut cert_w = TlvWriter::new();
127        cert_w.write_tlv(tlv_type::DATA, &cert_body);
128        let certificate = cert_w.finish();
129
130        let (ek_type, ek_body) = inner
131            .read_tlv()
132            .map_err(|e| SafeBagError::Malformed(format!("EncryptedKey TLV: {e:?}")))?;
133        if ek_type != TLV_ENCRYPTED_KEY {
134            return Err(SafeBagError::Malformed(format!(
135                "expected EncryptedKey (0x81) inside SafeBag, got 0x{ek_type:x}"
136            )));
137        }
138
139        Ok(Self {
140            certificate,
141            encrypted_key: ek_body,
142        })
143    }
144
145    /// Build a SafeBag by encrypting an unencrypted PKCS#8
146    /// `PrivateKeyInfo` DER with `password`. Uses the rustcrypto
147    /// `pkcs8` crate's default PBES2 parameters: PBKDF2-HMAC-SHA256
148    /// with a random 16-byte salt and AES-256-CBC with a random IV.
149    /// These match the OpenSSL `PKCS8_encrypt` defaults that ndn-cxx
150    /// produces.
151    pub fn encrypt(
152        certificate: Bytes,
153        pkcs8_pki_der: &[u8],
154        password: &[u8],
155    ) -> Result<Self, SafeBagError> {
156        use pkcs8::PrivateKeyInfo;
157        let pki = PrivateKeyInfo::try_from(pkcs8_pki_der)
158            .map_err(|e| SafeBagError::Pkcs8(format!("parse PrivateKeyInfo: {e}")))?;
159        let encrypted = pki
160            .encrypt(rsa::rand_core::OsRng, password)
161            .map_err(|e| SafeBagError::Pkcs8(format!("encrypt: {e}")))?;
162        Ok(Self {
163            certificate,
164            encrypted_key: Bytes::copy_from_slice(encrypted.as_bytes()),
165        })
166    }
167
168    /// Decrypt the SafeBag's encrypted private key with `password`,
169    /// returning the unencrypted PKCS#8 `PrivateKeyInfo` DER. The
170    /// caller dispatches on the embedded algorithm OID.
171    pub fn decrypt_key(&self, password: &[u8]) -> Result<Vec<u8>, SafeBagError> {
172        use pkcs8::EncryptedPrivateKeyInfo;
173        let epki = EncryptedPrivateKeyInfo::try_from(&self.encrypted_key[..])
174            .map_err(|e| SafeBagError::Pkcs8(format!("parse EncryptedPrivateKeyInfo: {e}")))?;
175        let decrypted = epki
176            .decrypt(password)
177            .map_err(|e| SafeBagError::Pkcs8(format!("decrypt: {e}")))?;
178        Ok(decrypted.as_bytes().to_vec())
179    }
180}
181
182// ─── Algorithm-specific PKCS#8 conversion helpers ───────────────────────────
183//
184// FileTpm stores private keys in three algorithm-specific on-disk
185// forms (PKCS#1 for RSA, SEC1 for ECDSA-P256, PKCS#8 for Ed25519).
186// PKCS#8 EncryptedPrivateKeyInfo wraps a PKCS#8 PrivateKeyInfo, so
187// for export we have to convert the on-disk form *to* PKCS#8 first;
188// for import we convert *from* PKCS#8 back to the on-disk form. The
189// PKCS#8 algorithm OID identifies which conversion to apply on the
190// way back in.
191
192/// Convert an RSA PKCS#1 `RSAPrivateKey` DER (the FileTpm on-disk
193/// form for RSA) into a PKCS#8 `PrivateKeyInfo` DER.
194pub(crate) fn rsa_pkcs1_to_pkcs8(pkcs1_der: &[u8]) -> Result<Vec<u8>, SafeBagError> {
195    use pkcs1::DecodeRsaPrivateKey;
196    use rsa::RsaPrivateKey;
197    use rsa::pkcs8::EncodePrivateKey;
198    let sk = RsaPrivateKey::from_pkcs1_der(pkcs1_der)
199        .map_err(|e| SafeBagError::KeyConversion(format!("rsa pkcs1 parse: {e}")))?;
200    let pkcs8_doc = sk
201        .to_pkcs8_der()
202        .map_err(|e| SafeBagError::KeyConversion(format!("rsa to pkcs8: {e}")))?;
203    Ok(pkcs8_doc.as_bytes().to_vec())
204}
205
206/// Convert a PKCS#8 `PrivateKeyInfo` DER carrying an RSA key back
207/// into the PKCS#1 `RSAPrivateKey` form FileTpm stores on disk.
208pub(crate) fn rsa_pkcs8_to_pkcs1(pkcs8_der: &[u8]) -> Result<Vec<u8>, SafeBagError> {
209    use pkcs1::EncodeRsaPrivateKey;
210    use rsa::RsaPrivateKey;
211    use rsa::pkcs8::DecodePrivateKey;
212    let sk = RsaPrivateKey::from_pkcs8_der(pkcs8_der)
213        .map_err(|e| SafeBagError::KeyConversion(format!("rsa pkcs8 parse: {e}")))?;
214    let pkcs1_doc = sk
215        .to_pkcs1_der()
216        .map_err(|e| SafeBagError::KeyConversion(format!("rsa to pkcs1: {e}")))?;
217    Ok(pkcs1_doc.as_bytes().to_vec())
218}
219
220/// Convert a SEC1 `ECPrivateKey` DER (the FileTpm on-disk form for
221/// ECDSA-P256) into a PKCS#8 `PrivateKeyInfo` DER. Bypasses
222/// `SecretKey::from_sec1_der` (which is brittle on missing
223/// AlgorithmIdentifier parameters) by hand-extracting the 32-byte
224/// scalar via [`crate::file_tpm::parse_sec1_p256_priv_scalar`] and
225/// re-constructing the SecretKey from the raw scalar.
226pub(crate) fn ec_sec1_to_pkcs8(sec1_der: &[u8]) -> Result<Vec<u8>, SafeBagError> {
227    use p256_ecdsa::SecretKey;
228    use p256_ecdsa::pkcs8::EncodePrivateKey;
229
230    let scalar = crate::file_tpm::parse_sec1_p256_priv_scalar(sec1_der)?;
231    let secret = SecretKey::from_slice(&scalar)
232        .map_err(|e| SafeBagError::KeyConversion(format!("p256 from scalar: {e}")))?;
233    let pkcs8_doc = secret
234        .to_pkcs8_der()
235        .map_err(|e| SafeBagError::KeyConversion(format!("p256 to pkcs8: {e}")))?;
236    Ok(pkcs8_doc.as_bytes().to_vec())
237}
238
239/// Convert a PKCS#8 `PrivateKeyInfo` DER carrying a P-256 ECDSA key
240/// back into the SEC1 `ECPrivateKey` form FileTpm stores on disk.
241pub(crate) fn ec_pkcs8_to_sec1(pkcs8_der: &[u8]) -> Result<Vec<u8>, SafeBagError> {
242    use p256_ecdsa::SecretKey;
243    use p256_ecdsa::pkcs8::DecodePrivateKey;
244
245    let secret = SecretKey::from_pkcs8_der(pkcs8_der)
246        .map_err(|e| SafeBagError::KeyConversion(format!("p256 pkcs8 parse: {e}")))?;
247    let sec1_doc = secret
248        .to_sec1_der()
249        .map_err(|e| SafeBagError::KeyConversion(format!("p256 to sec1: {e}")))?;
250    Ok(sec1_doc.as_slice().to_vec())
251}
252
253/// Inspect the algorithm OID inside a PKCS#8 PrivateKeyInfo DER and
254/// dispatch to one of the [`TpmKeyKind`] variants. This is how
255/// SafeBag import knows which on-disk form to write.
256pub(crate) fn detect_pkcs8_algorithm(pkcs8_der: &[u8]) -> Result<TpmKeyKind, SafeBagError> {
257    use pkcs8::PrivateKeyInfo;
258    let pki = PrivateKeyInfo::try_from(pkcs8_der)
259        .map_err(|e| SafeBagError::Pkcs8(format!("PrivateKeyInfo parse: {e}")))?;
260    // OIDs from RFC 8017 (RSA), RFC 5480 (EC), RFC 8410 (Ed25519).
261    let oid = pki.algorithm.oid;
262    if oid.to_string() == "1.2.840.113549.1.1.1" {
263        Ok(TpmKeyKind::Rsa)
264    } else if oid.to_string() == "1.2.840.10045.2.1" {
265        // id-ecPublicKey — narrow further by checking parameters.
266        // For SafeBag we only support P-256 (the curve FileTpm
267        // generates), so this is good enough.
268        Ok(TpmKeyKind::EcdsaP256)
269    } else if oid.to_string() == "1.3.101.112" {
270        // id-Ed25519
271        Ok(TpmKeyKind::Ed25519)
272    } else {
273        Err(SafeBagError::UnsupportedAlgorithm(format!(
274            "unknown PKCS#8 algorithm OID {oid}"
275        )))
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    /// A minimal "Data packet" — just `0x06 LL <body>` — that we can
284    /// stuff into a SafeBag for roundtrip tests. SafeBag treats the
285    /// certificate as opaque bytes; nothing in this module parses the
286    /// Data, so a syntactically-valid TLV with arbitrary body is fine.
287    fn fake_cert(body: &[u8]) -> Bytes {
288        let mut w = TlvWriter::new();
289        w.write_tlv(tlv_type::DATA, body);
290        w.finish()
291    }
292
293    #[test]
294    fn safebag_tlv_roundtrip() {
295        let cert = fake_cert(b"fake certificate body");
296        let sb = SafeBag {
297            certificate: cert.clone(),
298            encrypted_key: Bytes::from_static(b"opaque encrypted key bytes"),
299        };
300        let wire = sb.encode();
301        // Outer TLV must start with 0x80.
302        assert_eq!(wire[0], 0x80);
303        let decoded = SafeBag::decode(&wire).unwrap();
304        assert_eq!(decoded.certificate, cert);
305        assert_eq!(decoded.encrypted_key, sb.encrypted_key);
306    }
307
308    #[test]
309    fn safebag_decode_rejects_wrong_outer_type() {
310        // Outer TLV with a non-SafeBag type code (use 0x06 = Data).
311        let mut w = TlvWriter::new();
312        w.write_tlv(tlv_type::DATA, b"oops");
313        let wire = w.finish();
314        match SafeBag::decode(&wire) {
315            Err(SafeBagError::Malformed(_)) => {}
316            other => panic!("expected Malformed, got {other:?}"),
317        }
318    }
319
320    #[test]
321    fn pkcs8_encrypt_decrypt_roundtrip_ed25519() {
322        // Generate an Ed25519 key as raw PKCS#8 (the form FileTpm
323        // already produces for the .privkey-ed25519 sentinel suffix).
324        use ed25519_dalek::SigningKey;
325        use ed25519_dalek::pkcs8::EncodePrivateKey;
326        let mut seed = [0u8; 32];
327        ring::rand::SecureRandom::fill(&ring::rand::SystemRandom::new(), &mut seed).unwrap();
328        let sk = SigningKey::from_bytes(&seed);
329        let pkcs8 = sk.to_pkcs8_der().unwrap();
330
331        let cert = fake_cert(b"ed25519 cert");
332        let pw = b"correct horse battery staple";
333
334        let sb = SafeBag::encrypt(cert.clone(), pkcs8.as_bytes(), pw).unwrap();
335        // Encrypted blob must NOT contain the plain key bytes.
336        assert!(
337            sb.encrypted_key.windows(32).all(|w| w != seed),
338            "encrypted key leaked the seed"
339        );
340        let decrypted = sb.decrypt_key(pw).unwrap();
341        // After decryption we should get back exactly the original
342        // PKCS#8 PrivateKeyInfo DER.
343        assert_eq!(&decrypted[..], pkcs8.as_bytes());
344
345        // Wrong password must fail.
346        assert!(sb.decrypt_key(b"wrong password").is_err());
347
348        // SafeBag wire-roundtrip preserves both fields.
349        let wire = sb.encode();
350        let sb2 = SafeBag::decode(&wire).unwrap();
351        assert_eq!(sb2.decrypt_key(pw).unwrap(), decrypted);
352    }
353
354    #[test]
355    fn rsa_pkcs1_pkcs8_roundtrip() {
356        use pkcs1::EncodeRsaPrivateKey;
357        use rsa::RsaPrivateKey;
358        // Use a tiny key (1024 bits) for test speed; production keys
359        // would be 2048+.
360        let mut rng = rsa::rand_core::OsRng;
361        let sk = RsaPrivateKey::new(&mut rng, 1024).unwrap();
362        let pkcs1 = sk.to_pkcs1_der().unwrap();
363        let pkcs8 = rsa_pkcs1_to_pkcs8(pkcs1.as_bytes()).unwrap();
364        let pkcs1_again = rsa_pkcs8_to_pkcs1(&pkcs8).unwrap();
365        assert_eq!(pkcs1.as_bytes(), pkcs1_again.as_slice());
366    }
367
368    #[test]
369    fn ec_sec1_pkcs8_roundtrip() {
370        use p256_ecdsa::SecretKey;
371        use p256_ecdsa::pkcs8::EncodePrivateKey;
372        // Generate a fresh key as PKCS#8 (which then we need to convert
373        // to SEC1 by hand because to_sec1_der is on a different trait
374        // path that's not always in scope). Use a known scalar for a
375        // deterministic test.
376        let scalar = [
377            0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0,
378            0xF0, 0x01, 0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78, 0x89, 0x9A, 0xAB, 0xBC, 0xCD,
379            0xDE, 0xEF, 0xFE, 0xED,
380        ];
381        let secret = SecretKey::from_slice(&scalar).unwrap();
382        let pkcs8 = secret.to_pkcs8_der().unwrap();
383
384        // PKCS#8 → SEC1 → PKCS#8 should roundtrip cleanly.
385        let sec1 = ec_pkcs8_to_sec1(pkcs8.as_bytes()).unwrap();
386        let pkcs8_again = ec_sec1_to_pkcs8(&sec1).unwrap();
387        assert_eq!(pkcs8.as_bytes(), pkcs8_again.as_slice());
388    }
389
390    #[test]
391    fn detect_pkcs8_algorithm_recognises_each_kind() {
392        // Ed25519
393        {
394            use ed25519_dalek::SigningKey;
395            use ed25519_dalek::pkcs8::EncodePrivateKey;
396            let sk = SigningKey::from_bytes(&[5u8; 32]);
397            let pkcs8 = sk.to_pkcs8_der().unwrap();
398            assert_eq!(
399                detect_pkcs8_algorithm(pkcs8.as_bytes()).unwrap(),
400                TpmKeyKind::Ed25519
401            );
402        }
403        // RSA
404        {
405            use rsa::RsaPrivateKey;
406            use rsa::pkcs8::EncodePrivateKey;
407            let mut rng = rsa::rand_core::OsRng;
408            let sk = RsaPrivateKey::new(&mut rng, 1024).unwrap();
409            let pkcs8 = sk.to_pkcs8_der().unwrap();
410            assert_eq!(
411                detect_pkcs8_algorithm(pkcs8.as_bytes()).unwrap(),
412                TpmKeyKind::Rsa
413            );
414        }
415        // ECDSA-P256
416        {
417            use p256_ecdsa::SecretKey;
418            use p256_ecdsa::pkcs8::EncodePrivateKey;
419            let secret = SecretKey::from_slice(&[7u8; 32]).unwrap();
420            let pkcs8 = secret.to_pkcs8_der().unwrap();
421            assert_eq!(
422                detect_pkcs8_algorithm(pkcs8.as_bytes()).unwrap(),
423                TpmKeyKind::EcdsaP256
424            );
425        }
426    }
427}