1use base64ct::{Base64, Encoding};
4
5use rite_sdk::{CertificateDer, KeyAlgorithm, KeyId, PublicKeyDer, WrapAlgorithm};
6
7#[derive(Debug)]
9pub enum ArtifactValue {
10 BackendKey {
13 backend_name: String,
15 key_id: KeyId,
17 algorithm: KeyAlgorithm,
19 public_key: Option<PublicKeyDer>,
21 },
22 WrappedKey {
24 data: Vec<u8>,
26 algorithm: WrapAlgorithm,
28 },
29 PublicKey(PublicKeyDer),
31 Bytes(Vec<u8>),
34
35 Text(String),
38
39 Certificate(CertificateDer),
41}
42
43impl std::fmt::Display for ArtifactValue {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 ArtifactValue::BackendKey {
48 backend_name,
49 key_id,
50 algorithm,
51 public_key,
52 ..
53 } => {
54 if let Some(pub_key) = public_key {
56 write!(
57 f,
58 "BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?})\n{}",
59 key_id.as_str(),
60 encode_pem("PUBLIC KEY", pub_key.as_bytes())
61 )
62 } else {
63 write!(
64 f,
65 "BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?}, public_key=not_exportable)",
66 key_id.as_str()
67 )
68 }
69 }
70 ArtifactValue::WrappedKey { data, algorithm } => {
71 let label = match algorithm {
72 WrapAlgorithm::CmsRsaCbc | WrapAlgorithm::CmsRsaGcm => "CMS",
73 _ => "WRAPPED KEY",
74 };
75 let pem = encode_pem(label, data);
76 write!(f, "{pem}")
77 }
78 ArtifactValue::PublicKey(key) => {
79 let pem = encode_pem("PUBLIC KEY", key.as_bytes());
80 write!(f, "{pem}")
81 }
82 ArtifactValue::Bytes(bytes) => {
84 let len = bytes.len();
85 write!(f, "Bytes({len} bytes)")
86 }
87 ArtifactValue::Text(text) => {
88 write!(f, "Text({text})")
89 }
90
91 ArtifactValue::Certificate(certificate) => {
92 let pem = encode_pem("CERTIFICATE", certificate.as_bytes());
93 write!(f, "{pem}")
94 }
95 }
96 }
97}
98
99pub(crate) fn base64_encode(data: &[u8]) -> String {
102 Base64::encode_string(data)
103}
104
105pub(crate) fn encode_pem(label: &str, der_bytes: &[u8]) -> String {
107 let b64 = base64_encode(der_bytes);
108 let lines: Vec<&str> = b64
110 .as_bytes()
111 .chunks(64)
112 .map(|chunk| std::str::from_utf8(chunk).unwrap_or(""))
113 .collect();
114
115 let body = lines.join("\n");
116 format!("-----BEGIN {label}-----\n{body}\n-----END {label}-----")
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SerializedArtifact {
129 pub bytes: Vec<u8>,
131 pub mime_type: Option<String>,
133 pub extension: &'static str,
135}
136
137impl ArtifactValue {
138 pub fn serialize(&self, format: Option<&str>) -> Result<SerializedArtifact, String> {
160 match self {
161 ArtifactValue::WrappedKey { data, algorithm } => {
162 let fmt = format.unwrap_or("der");
163 let (mime, ext) = match algorithm {
164 WrapAlgorithm::CmsRsaCbc | WrapAlgorithm::CmsRsaGcm => {
165 ("application/pkcs7-mime", "p7c")
166 }
167 _ => ("application/octet-stream", "bin"),
168 };
169 match fmt {
170 "der" => Ok(SerializedArtifact {
171 bytes: data.clone(),
172 mime_type: Some(mime.to_string()),
173 extension: ext,
174 }),
175 "pem" => {
176 let label = match algorithm {
177 WrapAlgorithm::CmsRsaCbc | WrapAlgorithm::CmsRsaGcm => "CMS",
178 _ => "WRAPPED KEY",
179 };
180 Ok(SerializedArtifact {
181 bytes: encode_pem(label, data).into_bytes(),
182 mime_type: Some("application/x-pem-file".to_string()),
183 extension: "pem",
184 })
185 }
186 _ => Err(format!(
187 "Invalid format '{fmt}' for WrappedKey (valid: der, pem)"
188 )),
189 }
190 }
191
192 ArtifactValue::PublicKey(key) => serialize_der(key.as_bytes(), &PUBLIC_KEY, format),
193
194 ArtifactValue::BackendKey { public_key, .. } => match public_key {
195 Some(key) => serialize_der(key.as_bytes(), &PUBLIC_KEY, format),
196 None => Err("Cannot export public key from non-exportable backend key".to_string()),
197 },
198
199 ArtifactValue::Bytes(bytes) => Ok(SerializedArtifact {
200 bytes: bytes.clone(),
201 mime_type: Some("application/octet-stream".to_string()),
202 extension: "bin",
203 }),
204 ArtifactValue::Text(text) => Ok(SerializedArtifact {
205 bytes: text.as_bytes().to_vec(),
206 mime_type: Some("text/plain".to_string()),
207 extension: "txt",
208 }),
209
210 ArtifactValue::Certificate(certificate) => {
211 serialize_der(certificate.as_bytes(), &CERTIFICATE, format)
212 }
213 }
214 }
215}
216
217struct DerArtifact {
223 pem_label: &'static str,
224 der_mime: &'static str,
225 name: &'static str,
226}
227
228const PUBLIC_KEY: DerArtifact = DerArtifact {
230 pem_label: "PUBLIC KEY",
231 der_mime: "application/x-x509-ca-cert",
232 name: "a public key",
233};
234
235const CERTIFICATE: DerArtifact = DerArtifact {
236 pem_label: "CERTIFICATE",
237 der_mime: "application/pkix-cert",
238 name: "a certificate",
239};
240
241fn serialize_der(
243 der: &[u8],
244 artifact: &DerArtifact,
245 format: Option<&str>,
246) -> Result<SerializedArtifact, String> {
247 let fmt = format.unwrap_or("pem");
248 match fmt {
249 "pem" => Ok(SerializedArtifact {
250 bytes: encode_pem(artifact.pem_label, der).into_bytes(),
251 mime_type: Some("application/x-pem-file".to_string()),
252 extension: "pem",
253 }),
254 "der" => Ok(SerializedArtifact {
255 bytes: der.to_vec(),
256 mime_type: Some(artifact.der_mime.to_string()),
257 extension: "der",
258 }),
259 "base64" => Ok(SerializedArtifact {
260 bytes: base64_encode(der).into_bytes(),
261 mime_type: Some("text/plain".to_string()),
262 extension: "txt",
263 }),
264 _ => Err(format!(
265 "Invalid format '{fmt}' for {} (valid: pem, der, base64)",
266 artifact.name
267 )),
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 const P256_SPKI: &[u8] = include_bytes!("testdata/p256.spki.der");
281
282 #[test]
283 fn test_public_key_format_validation() {
284 let public_der = P256_SPKI.to_vec();
285 let public_key =
286 ArtifactValue::PublicKey(PublicKeyDer::new(public_der.clone()).expect("valid SPKI"));
287
288 let serialized = public_key.serialize(None).unwrap();
290 assert_eq!(serialized.extension, "pem");
291 assert_eq!(
292 serialized.mime_type,
293 Some("application/x-pem-file".to_string())
294 );
295
296 let serialized = public_key.serialize(Some("pem")).unwrap();
298 assert_eq!(serialized.extension, "pem");
299 assert_eq!(
300 serialized.mime_type,
301 Some("application/x-pem-file".to_string())
302 );
303
304 let serialized = public_key.serialize(Some("der")).unwrap();
306 assert_eq!(
307 serialized.bytes, public_der,
308 "DER format should output raw key data"
309 );
310 assert_eq!(serialized.extension, "der");
311 assert_eq!(
312 serialized.mime_type,
313 Some("application/x-x509-ca-cert".to_string())
314 );
315
316 let serialized = public_key.serialize(Some("base64")).unwrap();
318 assert_eq!(serialized.extension, "txt");
319 assert_eq!(serialized.mime_type, Some("text/plain".to_string()));
320
321 let result = public_key.serialize(Some("json"));
323 assert!(result.is_err(), "Invalid format should return error");
324 assert!(
325 result.unwrap_err().contains("Invalid format 'json'"),
326 "Error message should mention invalid format"
327 );
328 }
329
330 #[test]
331 fn test_wrapped_key_format_validation() {
332 let cms_data = b"CMS_DATA".to_vec();
333 let wrapped = ArtifactValue::WrappedKey {
334 data: cms_data.clone(),
335 algorithm: WrapAlgorithm::CmsRsaGcm,
336 };
337
338 let serialized = wrapped.serialize(None).unwrap();
340 assert_eq!(
341 serialized.bytes, cms_data,
342 "Default format should output raw CMS data"
343 );
344 assert_eq!(serialized.extension, "p7c");
345 assert_eq!(
346 serialized.mime_type,
347 Some("application/pkcs7-mime".to_string())
348 );
349
350 let serialized = wrapped.serialize(Some("der")).unwrap();
352 assert_eq!(serialized.bytes, cms_data);
353 assert_eq!(serialized.extension, "p7c");
354 assert_eq!(
355 serialized.mime_type,
356 Some("application/pkcs7-mime".to_string())
357 );
358
359 let serialized = wrapped.serialize(Some("pem")).unwrap();
361 let pem_str = String::from_utf8(serialized.bytes).unwrap();
362 assert!(
363 pem_str.contains("-----BEGIN CMS-----"),
364 "PEM should contain CMS header"
365 );
366 assert_eq!(serialized.extension, "pem");
367 assert_eq!(
368 serialized.mime_type,
369 Some("application/x-pem-file".to_string())
370 );
371
372 let result = wrapped.serialize(Some("base64"));
374 assert!(result.is_err(), "Invalid format should return error");
375 assert!(
376 result.unwrap_err().contains("Invalid format 'base64'"),
377 "Error message should mention invalid format"
378 );
379 }
380}