1use 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#[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 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 pub fn private_key_pkcs8_der(&self) -> &[u8] {
57 &self.pkcs8_der
58 }
59
60 pub fn private_key_pkcs8_pem(&self) -> &str {
62 &self.pkcs8_pem
63 }
64
65 pub fn public_key_spki_der(&self) -> &[u8] {
67 &self.spki_der
68 }
69
70 pub fn public_key_spki_pem(&self) -> &str {
72 &self.spki_pem
73 }
74
75 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 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 pub fn private_key_pkcs8_pem_corrupt(&self, how: CorruptPem) -> String {
87 corrupt_pem(self.private_key_pkcs8_pem(), how)
88 }
89
90 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 pub fn private_key_pkcs8_der_truncated(&self, len: usize) -> Vec<u8> {
97 truncate_der(self.private_key_pkcs8_der(), len)
98 }
99
100 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 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 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 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}