Skip to main content

uselesskey_core/srp/
keypair_material.rs

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