uselesskey_core_keypair_material/
lib.rs1#![forbid(unsafe_code)]
2use 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#[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 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 pub fn private_key_pkcs8_der(&self) -> &[u8] {
56 &self.pkcs8_der
57 }
58
59 pub fn private_key_pkcs8_pem(&self) -> &str {
61 &self.pkcs8_pem
62 }
63
64 pub fn public_key_spki_der(&self) -> &[u8] {
66 &self.spki_der
67 }
68
69 pub fn public_key_spki_pem(&self) -> &str {
71 &self.spki_pem
72 }
73
74 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 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 pub fn private_key_pkcs8_pem_corrupt(&self, how: CorruptPem) -> String {
86 corrupt_pem(self.private_key_pkcs8_pem(), how)
87 }
88
89 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 pub fn private_key_pkcs8_der_truncated(&self, len: usize) -> Vec<u8> {
96 truncate_der(self.private_key_pkcs8_der(), len)
97 }
98
99 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 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 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 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}