Skip to main content

uselesskey_core_keypair_material/
lib.rs

1#![forbid(unsafe_code)]
2//! PKCS#8 / SPKI key-material helpers shared by key fixture crates.
3//!
4//! Provides the `Pkcs8SpkiKeyMaterial` trait and related types for consistent
5//! access to private (PKCS#8) and public (SPKI) key encodings in PEM and DER
6//! formats, plus corrupt PEM/DER negative fixture support.
7
8use std::fmt;
9use std::sync::Arc;
10
11use uselesskey_core::Error;
12use uselesskey_core::negative::{
13    CorruptPem, corrupt_der_deterministic, corrupt_pem, corrupt_pem_deterministic, truncate_der,
14};
15use uselesskey_core::sink::TempArtifact;
16use uselesskey_core_kid::kid_from_bytes;
17
18/// Common PKCS#8/SPKI key material shared by multiple fixture crates.
19#[derive(Clone)]
20pub struct Pkcs8SpkiKeyMaterial {
21    pkcs8_der: Arc<[u8]>,
22    pkcs8_pem: String,
23    spki_der: Arc<[u8]>,
24    spki_pem: String,
25}
26
27impl fmt::Debug for Pkcs8SpkiKeyMaterial {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.debug_struct("Pkcs8SpkiKeyMaterial")
30            .field("pkcs8_der_len", &self.pkcs8_der.len())
31            .field("pkcs8_pem_len", &self.pkcs8_pem.len())
32            .field("spki_der_len", &self.spki_der.len())
33            .field("spki_pem_len", &self.spki_pem.len())
34            .finish_non_exhaustive()
35    }
36}
37
38impl Pkcs8SpkiKeyMaterial {
39    /// Build a material container from PKCS#8 and SPKI forms.
40    pub fn new(
41        pkcs8_der: impl Into<Arc<[u8]>>,
42        pkcs8_pem: impl Into<String>,
43        spki_der: impl Into<Arc<[u8]>>,
44        spki_pem: impl Into<String>,
45    ) -> Self {
46        Self {
47            pkcs8_der: pkcs8_der.into(),
48            pkcs8_pem: pkcs8_pem.into(),
49            spki_der: spki_der.into(),
50            spki_pem: spki_pem.into(),
51        }
52    }
53
54    /// PKCS#8 DER-encoded private key bytes.
55    pub fn private_key_pkcs8_der(&self) -> &[u8] {
56        &self.pkcs8_der
57    }
58
59    /// PKCS#8 PEM-encoded private key.
60    pub fn private_key_pkcs8_pem(&self) -> &str {
61        &self.pkcs8_pem
62    }
63
64    /// SPKI DER-encoded public key bytes.
65    pub fn public_key_spki_der(&self) -> &[u8] {
66        &self.spki_der
67    }
68
69    /// SPKI PEM-encoded public key.
70    pub fn public_key_spki_pem(&self) -> &str {
71        &self.spki_pem
72    }
73
74    /// Write the PKCS#8 PEM private key to a tempfile and return the handle.
75    pub fn write_private_key_pkcs8_pem(&self) -> Result<TempArtifact, Error> {
76        TempArtifact::new_string("uselesskey-", ".pkcs8.pem", self.private_key_pkcs8_pem())
77    }
78
79    /// Write the SPKI PEM public key to a tempfile and return the handle.
80    pub fn write_public_key_spki_pem(&self) -> Result<TempArtifact, Error> {
81        TempArtifact::new_string("uselesskey-", ".spki.pem", self.public_key_spki_pem())
82    }
83
84    /// Produce a corrupted variant of the PKCS#8 PEM.
85    pub fn private_key_pkcs8_pem_corrupt(&self, how: CorruptPem) -> String {
86        corrupt_pem(self.private_key_pkcs8_pem(), how)
87    }
88
89    /// Produce a deterministic corrupted PKCS#8 PEM using a variant string.
90    pub fn private_key_pkcs8_pem_corrupt_deterministic(&self, variant: &str) -> String {
91        corrupt_pem_deterministic(self.private_key_pkcs8_pem(), variant)
92    }
93
94    /// Produce a truncated variant of the PKCS#8 DER.
95    pub fn private_key_pkcs8_der_truncated(&self, len: usize) -> Vec<u8> {
96        truncate_der(self.private_key_pkcs8_der(), len)
97    }
98
99    /// Produce a deterministic corrupted PKCS#8 DER using a variant string.
100    pub fn private_key_pkcs8_der_corrupt_deterministic(&self, variant: &str) -> Vec<u8> {
101        corrupt_der_deterministic(self.private_key_pkcs8_der(), variant)
102    }
103
104    /// A stable key identifier derived from the SPKI bytes.
105    pub fn kid(&self) -> String {
106        kid_from_bytes(self.public_key_spki_der())
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::Pkcs8SpkiKeyMaterial;
113    use uselesskey_core::negative::CorruptPem;
114
115    fn sample_material() -> Pkcs8SpkiKeyMaterial {
116        Pkcs8SpkiKeyMaterial::new(
117            vec![0x30, 0x82, 0x01, 0x22],
118            "-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n".to_string(),
119            vec![0x30, 0x59, 0x30, 0x13],
120            "-----BEGIN PUBLIC KEY-----\nBBBB\n-----END PUBLIC KEY-----\n".to_string(),
121        )
122    }
123
124    #[test]
125    fn accessors_expose_material() {
126        let material = sample_material();
127
128        assert_eq!(material.private_key_pkcs8_der(), &[0x30, 0x82, 0x01, 0x22]);
129        assert!(
130            material
131                .private_key_pkcs8_pem()
132                .contains("BEGIN PRIVATE KEY")
133        );
134        assert_eq!(material.public_key_spki_der(), &[0x30, 0x59, 0x30, 0x13]);
135        assert!(material.public_key_spki_pem().contains("BEGIN PUBLIC KEY"));
136    }
137
138    #[test]
139    fn debug_does_not_include_key_pem() {
140        let material = sample_material();
141        let dbg = format!("{material:?}");
142        assert!(dbg.contains("Pkcs8SpkiKeyMaterial"));
143        assert!(!dbg.contains("BEGIN PRIVATE KEY"));
144    }
145
146    #[test]
147    fn private_key_pkcs8_pem_corrupt() {
148        let material = sample_material();
149        let corrupted = material.private_key_pkcs8_pem_corrupt(CorruptPem::BadHeader);
150        assert_ne!(corrupted, material.private_key_pkcs8_pem());
151        assert!(corrupted.contains("CORRUPTED KEY"));
152    }
153
154    #[test]
155    fn deterministic_corruption_is_stable() {
156        let material = sample_material();
157        let a = material.private_key_pkcs8_pem_corrupt_deterministic("core-keypair:v1");
158        let b = material.private_key_pkcs8_pem_corrupt_deterministic("core-keypair:v1");
159        assert_eq!(a, b);
160        assert_ne!(a, material.private_key_pkcs8_pem());
161        // Must still look like (corrupted) PEM, not a constant like "" or "xyzzy"
162        assert!(a.contains("-----"));
163    }
164
165    #[test]
166    fn truncation_respects_requested_length() {
167        let material = sample_material();
168        let truncated = material.private_key_pkcs8_der_truncated(2);
169        assert_eq!(truncated.len(), 2);
170        assert_eq!(truncated, &material.private_key_pkcs8_der()[..2]);
171    }
172
173    #[test]
174    fn private_key_pkcs8_der_corrupt_deterministic() {
175        let material = sample_material();
176        let a = material.private_key_pkcs8_der_corrupt_deterministic("variant-a");
177        let b = material.private_key_pkcs8_der_corrupt_deterministic("variant-a");
178        assert_eq!(a, b);
179        assert_ne!(a, material.private_key_pkcs8_der());
180        // Different variants must produce different corruption — a constant return can't satisfy this
181        let c = material.private_key_pkcs8_der_corrupt_deterministic("variant-b");
182        assert_ne!(a, c);
183    }
184
185    #[test]
186    fn kid_is_deterministic() {
187        let material = sample_material();
188        let a = material.kid();
189        let b = material.kid();
190        assert_eq!(a, b);
191        assert!(!a.is_empty());
192    }
193
194    #[test]
195    fn kid_depends_on_spki_bytes() {
196        let m1 = sample_material();
197        let m2 = Pkcs8SpkiKeyMaterial::new(
198            vec![0x30, 0x82, 0x01, 0x22],
199            "-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n",
200            vec![0xFF, 0xFE, 0xFD, 0xFC],
201            "-----BEGIN PUBLIC KEY-----\nCCCC\n-----END PUBLIC KEY-----\n",
202        );
203        assert_ne!(m1.kid(), m2.kid());
204    }
205
206    #[test]
207    fn tempfile_writers_round_trip_content() {
208        let material = sample_material();
209
210        let private = material
211            .write_private_key_pkcs8_pem()
212            .expect("write private");
213        let public = material.write_public_key_spki_pem().expect("write public");
214
215        let private_text = private.read_to_string().expect("read private");
216        let public_text = public.read_to_string().expect("read public");
217
218        assert!(private_text.contains("BEGIN PRIVATE KEY"));
219        assert!(public_text.contains("BEGIN PUBLIC KEY"));
220    }
221
222    mod property {
223        use super::Pkcs8SpkiKeyMaterial;
224        use super::sample_material;
225
226        use proptest::prelude::*;
227
228        fn sample_material_with_der(der: Vec<u8>) -> Pkcs8SpkiKeyMaterial {
229            Pkcs8SpkiKeyMaterial::new(
230                der,
231                sample_material().private_key_pkcs8_pem(),
232                sample_material().public_key_spki_der(),
233                sample_material().public_key_spki_pem(),
234            )
235        }
236
237        proptest! {
238            #![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })]
239
240            #[test]
241            fn truncation_len_is_capped(
242                der in prop::collection::vec(any::<u8>(), 0..128),
243                request in 0usize..256,
244            ) {
245                let material = sample_material_with_der(der.clone());
246                let truncated = material.private_key_pkcs8_der_truncated(request);
247                assert_eq!(truncated.len(), request.min(der.len()));
248            }
249
250            #[test]
251            fn deterministic_pem_corruption_is_reproducible(
252                seed in "[a-zA-Z0-9]{1,24}",
253            ) {
254                let material = sample_material();
255                let a = material.private_key_pkcs8_pem_corrupt_deterministic(&seed);
256                let b = material.private_key_pkcs8_pem_corrupt_deterministic(&seed);
257                assert_eq!(a, b);
258            }
259
260            #[test]
261            fn kid_stable_for_fixed_spki(
262                private_pem in "[A-Z ]{0,64}",
263            ) {
264                let material = Pkcs8SpkiKeyMaterial::new(
265                    vec![0x30, 0x82, 0x01, 0x22],
266                    private_pem,
267                    vec![0x30, 0x59, 0x30, 0x13],
268                    "-----BEGIN PUBLIC KEY-----\nBBBB\n-----END PUBLIC KEY-----\n".to_string(),
269                );
270                let a = material.kid();
271                let b = material.kid();
272                prop_assert!(!a.is_empty());
273                assert_eq!(a, b);
274            }
275        }
276    }
277}