1use base64ct::{Base64, Encoding};
4
5use rite_sdk::{
6 CertificateDer, KeyAlgorithm, KeyCheckValue, KeyId, PublicKeyDer, WrapScheme, WrappedKey,
7};
8
9#[derive(Debug)]
11pub enum ArtifactValue {
12 BackendKey {
15 backend_name: String,
17 key_id: KeyId,
19 algorithm: KeyAlgorithm,
21 public_key: Option<PublicKeyDer>,
23 check_value: Option<KeyCheckValue>,
29 },
30 WrappedKey(WrappedKey),
34 PublicKey(PublicKeyDer),
36 Bytes(Vec<u8>),
39
40 Text(String),
43
44 Certificate(CertificateDer),
46}
47
48impl std::fmt::Display for ArtifactValue {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 ArtifactValue::BackendKey {
53 backend_name,
54 key_id,
55 algorithm,
56 public_key,
57 check_value,
58 } => {
59 if let Some(pub_key) = public_key {
61 write!(
62 f,
63 "BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?})\n{}",
64 key_id.as_str(),
65 encode_pem("PUBLIC KEY", pub_key.as_bytes())
66 )
67 } else if let Some(check_value) = check_value {
68 write!(
73 f,
74 "BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?}, kcv={check_value})",
75 key_id.as_str()
76 )
77 } else {
78 write!(
79 f,
80 "BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?}, public_key=not_exportable)",
81 key_id.as_str()
82 )
83 }
84 }
85 ArtifactValue::WrappedKey(wrapped) => match pem_label(wrapped.scheme()) {
86 Some(label) => write!(f, "{}", encode_pem(label, wrapped.data())),
87 None => write!(
90 f,
91 "WrappedKey({}, {} bytes)",
92 wrapped.scheme(),
93 wrapped.data().len()
94 ),
95 },
96 ArtifactValue::PublicKey(key) => {
97 let pem = encode_pem("PUBLIC KEY", key.as_bytes());
98 write!(f, "{pem}")
99 }
100 ArtifactValue::Bytes(bytes) => {
102 let len = bytes.len();
103 write!(f, "Bytes({len} bytes)")
104 }
105 ArtifactValue::Text(text) => {
106 write!(f, "Text({text})")
107 }
108
109 ArtifactValue::Certificate(certificate) => {
110 let pem = encode_pem("CERTIFICATE", certificate.as_bytes());
111 write!(f, "{pem}")
112 }
113 }
114 }
115}
116
117pub(crate) fn base64_encode(data: &[u8]) -> String {
120 Base64::encode_string(data)
121}
122
123fn pem_label(scheme: WrapScheme) -> Option<&'static str> {
129 match scheme {
130 WrapScheme::CmsAes256Gcm => Some("CMS"),
131 _ => None,
132 }
133}
134
135pub(crate) fn encode_pem(label: &str, der_bytes: &[u8]) -> String {
137 let b64 = base64_encode(der_bytes);
138 let lines: Vec<&str> = b64
140 .as_bytes()
141 .chunks(64)
142 .map(|chunk| std::str::from_utf8(chunk).unwrap_or(""))
143 .collect();
144
145 let body = lines.join("\n");
146 format!("-----BEGIN {label}-----\n{body}\n-----END {label}-----")
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct SerializedArtifact {
159 pub bytes: Vec<u8>,
161 pub mime_type: Option<String>,
163 pub extension: &'static str,
165}
166
167impl ArtifactValue {
168 pub fn serialize(&self, format: Option<&str>) -> Result<SerializedArtifact, String> {
190 match self {
191 ArtifactValue::WrappedKey(wrapped) => {
192 let fmt = format.unwrap_or("der");
193 let (mime, ext) = match wrapped.scheme() {
194 WrapScheme::CmsAes256Gcm => ("application/pkcs7-mime", "p7c"),
195 _ => ("application/octet-stream", "bin"),
196 };
197 match fmt {
198 "der" => Ok(SerializedArtifact {
199 bytes: wrapped.data().to_vec(),
200 mime_type: Some(mime.to_string()),
201 extension: ext,
202 }),
203 "pem" => {
204 let label = pem_label(wrapped.scheme()).ok_or_else(|| {
205 format!(
206 "{} produces raw bytes, which have no PEM form. Write it as \
207 'der', which is what a recipient imports.",
208 wrapped.scheme()
209 )
210 })?;
211 Ok(SerializedArtifact {
212 bytes: encode_pem(label, wrapped.data()).into_bytes(),
213 mime_type: Some("application/x-pem-file".to_string()),
214 extension: "pem",
215 })
216 }
217 _ => Err(format!(
218 "Invalid format '{fmt}' for WrappedKey (valid: der, pem)"
219 )),
220 }
221 }
222
223 ArtifactValue::PublicKey(key) => serialize_der(key.as_bytes(), &PUBLIC_KEY, format),
224
225 ArtifactValue::BackendKey { public_key, .. } => match public_key {
226 Some(key) => serialize_der(key.as_bytes(), &PUBLIC_KEY, format),
227 None => Err("Cannot export public key from non-exportable backend key".to_string()),
228 },
229
230 ArtifactValue::Bytes(bytes) => Ok(SerializedArtifact {
231 bytes: bytes.clone(),
232 mime_type: Some("application/octet-stream".to_string()),
233 extension: "bin",
234 }),
235 ArtifactValue::Text(text) => Ok(SerializedArtifact {
236 bytes: text.as_bytes().to_vec(),
237 mime_type: Some("text/plain".to_string()),
238 extension: "txt",
239 }),
240
241 ArtifactValue::Certificate(certificate) => {
242 serialize_der(certificate.as_bytes(), &CERTIFICATE, format)
243 }
244 }
245 }
246}
247
248struct DerArtifact {
254 pem_label: &'static str,
255 der_mime: &'static str,
256 name: &'static str,
257}
258
259const PUBLIC_KEY: DerArtifact = DerArtifact {
261 pem_label: "PUBLIC KEY",
262 der_mime: "application/x-x509-ca-cert",
263 name: "a public key",
264};
265
266const CERTIFICATE: DerArtifact = DerArtifact {
267 pem_label: "CERTIFICATE",
268 der_mime: "application/pkix-cert",
269 name: "a certificate",
270};
271
272fn serialize_der(
274 der: &[u8],
275 artifact: &DerArtifact,
276 format: Option<&str>,
277) -> Result<SerializedArtifact, String> {
278 let fmt = format.unwrap_or("pem");
279 match fmt {
280 "pem" => Ok(SerializedArtifact {
281 bytes: encode_pem(artifact.pem_label, der).into_bytes(),
282 mime_type: Some("application/x-pem-file".to_string()),
283 extension: "pem",
284 }),
285 "der" => Ok(SerializedArtifact {
286 bytes: der.to_vec(),
287 mime_type: Some(artifact.der_mime.to_string()),
288 extension: "der",
289 }),
290 "base64" => Ok(SerializedArtifact {
291 bytes: base64_encode(der).into_bytes(),
292 mime_type: Some("text/plain".to_string()),
293 extension: "txt",
294 }),
295 _ => Err(format!(
296 "Invalid format '{fmt}' for {} (valid: pem, der, base64)",
297 artifact.name
298 )),
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 const P256_SPKI: &[u8] = include_bytes!("testdata/p256.spki.der");
312
313 #[test]
314 fn test_public_key_format_validation() {
315 let public_der = P256_SPKI.to_vec();
316 let public_key =
317 ArtifactValue::PublicKey(PublicKeyDer::new(public_der.clone()).expect("valid SPKI"));
318
319 let serialized = public_key.serialize(None).unwrap();
321 assert_eq!(serialized.extension, "pem");
322 assert_eq!(
323 serialized.mime_type,
324 Some("application/x-pem-file".to_string())
325 );
326
327 let serialized = public_key.serialize(Some("pem")).unwrap();
329 assert_eq!(serialized.extension, "pem");
330 assert_eq!(
331 serialized.mime_type,
332 Some("application/x-pem-file".to_string())
333 );
334
335 let serialized = public_key.serialize(Some("der")).unwrap();
337 assert_eq!(
338 serialized.bytes, public_der,
339 "DER format should output raw key data"
340 );
341 assert_eq!(serialized.extension, "der");
342 assert_eq!(
343 serialized.mime_type,
344 Some("application/x-x509-ca-cert".to_string())
345 );
346
347 let serialized = public_key.serialize(Some("base64")).unwrap();
349 assert_eq!(serialized.extension, "txt");
350 assert_eq!(serialized.mime_type, Some("text/plain".to_string()));
351
352 let result = public_key.serialize(Some("json"));
354 assert!(result.is_err(), "Invalid format should return error");
355 assert!(
356 result.unwrap_err().contains("Invalid format 'json'"),
357 "Error message should mention invalid format"
358 );
359 }
360
361 #[test]
362 fn test_wrapped_key_format_validation() {
363 let cms_data = b"CMS_DATA".to_vec();
364 let description = rite_sdk::WrapDescription::new(
365 rite_sdk::RecipientInfoKind::Ktri,
366 rite_sdk::Oid::new(rite_sdk::oid::RSA_ENCRYPTION).unwrap(),
367 )
368 .with_content_encryption(rite_sdk::Oid::new(rite_sdk::oid::AES_256_GCM).unwrap());
369 let wrapped = ArtifactValue::WrappedKey(
370 WrappedKey::new(WrapScheme::CmsAes256Gcm, description, cms_data.clone()).unwrap(),
371 );
372
373 let serialized = wrapped.serialize(None).unwrap();
375 assert_eq!(
376 serialized.bytes, cms_data,
377 "Default format should output raw CMS data"
378 );
379 assert_eq!(serialized.extension, "p7c");
380 assert_eq!(
381 serialized.mime_type,
382 Some("application/pkcs7-mime".to_string())
383 );
384
385 let serialized = wrapped.serialize(Some("der")).unwrap();
387 assert_eq!(serialized.bytes, cms_data);
388 assert_eq!(serialized.extension, "p7c");
389 assert_eq!(
390 serialized.mime_type,
391 Some("application/pkcs7-mime".to_string())
392 );
393
394 let serialized = wrapped.serialize(Some("pem")).unwrap();
396 let pem_str = String::from_utf8(serialized.bytes).unwrap();
397 assert!(
398 pem_str.contains("-----BEGIN CMS-----"),
399 "PEM should contain CMS header"
400 );
401 assert_eq!(serialized.extension, "pem");
402 assert_eq!(
403 serialized.mime_type,
404 Some("application/x-pem-file".to_string())
405 );
406
407 let result = wrapped.serialize(Some("base64"));
409 assert!(result.is_err(), "Invalid format should return error");
410 assert!(
411 result.unwrap_err().contains("Invalid format 'base64'"),
412 "Error message should mention invalid format"
413 );
414 }
415}