1use base64ct::{Base64, Encoding};
4
5use rite_sdk::{KeyAlgorithm, KeyId, WrapAlgorithm};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum KeyFormat {
10 Der,
12 Pem,
14}
15
16#[derive(Debug)]
18pub enum ArtifactValue {
19 BackendKey {
22 backend_name: String,
24 key_id: KeyId,
26 algorithm: KeyAlgorithm,
28 public_key: Option<Vec<u8>>,
30 },
31 WrappedKey {
33 data: Vec<u8>,
35 algorithm: WrapAlgorithm,
37 },
38 PublicKey {
40 key_data: Vec<u8>,
42 format: KeyFormat,
44 },
45 Bytes(Vec<u8>),
48
49 Text(String),
52
53 Certificate {
56 der: Vec<u8>,
58 },
59}
60
61impl std::fmt::Display for ArtifactValue {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 ArtifactValue::BackendKey {
66 backend_name,
67 key_id,
68 algorithm,
69 public_key,
70 ..
71 } => {
72 if let Some(pub_key) = public_key {
74 write!(
75 f,
76 "BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?})\n{}",
77 key_id.as_str(),
78 encode_pem("PUBLIC KEY", pub_key)
79 )
80 } else {
81 write!(
82 f,
83 "BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?}, public_key=not_exportable)",
84 key_id.as_str()
85 )
86 }
87 }
88 ArtifactValue::WrappedKey { data, algorithm } => {
89 let label = match algorithm {
90 WrapAlgorithm::CmsRsaCbc | WrapAlgorithm::CmsRsaGcm => "CMS",
91 _ => "WRAPPED KEY",
92 };
93 let pem = encode_pem(label, data);
94 write!(f, "{pem}")
95 }
96 ArtifactValue::PublicKey {
97 key_data,
98 format: KeyFormat::Pem,
99 } => {
100 let pem = encode_pem("PUBLIC KEY", key_data);
101 write!(f, "{pem}")
102 }
103 ArtifactValue::PublicKey {
104 key_data,
105 format: KeyFormat::Der,
106 } => {
107 let encoded = base64_encode(key_data);
109 write!(f, "{encoded}")
110 }
111 ArtifactValue::Bytes(bytes) => {
113 let len = bytes.len();
114 write!(f, "Bytes({len} bytes)")
115 }
116 ArtifactValue::Text(text) => {
117 write!(f, "Text({text})")
118 }
119
120 ArtifactValue::Certificate { der } => {
121 let pem = encode_pem("CERTIFICATE", der);
122 write!(f, "{pem}")
123 }
124 }
125 }
126}
127
128pub(crate) fn base64_encode(data: &[u8]) -> String {
131 Base64::encode_string(data)
132}
133
134pub(crate) fn encode_pem(label: &str, der_bytes: &[u8]) -> String {
136 let b64 = base64_encode(der_bytes);
137 let lines: Vec<&str> = b64
139 .as_bytes()
140 .chunks(64)
141 .map(|chunk| std::str::from_utf8(chunk).unwrap_or(""))
142 .collect();
143
144 let body = lines.join("\n");
145 format!("-----BEGIN {label}-----\n{body}\n-----END {label}-----")
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct SerializedArtifact {
158 pub bytes: Vec<u8>,
160 pub mime_type: Option<String>,
162 pub extension: &'static str,
164}
165
166impl ArtifactValue {
167 #[allow(clippy::too_many_lines)]
189 pub fn serialize(&self, format: Option<&str>) -> Result<SerializedArtifact, String> {
190 match self {
191 ArtifactValue::WrappedKey { data, algorithm } => {
192 let fmt = format.unwrap_or("der");
193 let (mime, ext) = match algorithm {
194 WrapAlgorithm::CmsRsaCbc | WrapAlgorithm::CmsRsaGcm => {
195 ("application/pkcs7-mime", "p7c")
196 }
197 _ => ("application/octet-stream", "bin"),
198 };
199 match fmt {
200 "der" => Ok(SerializedArtifact {
201 bytes: data.clone(),
202 mime_type: Some(mime.to_string()),
203 extension: ext,
204 }),
205 "pem" => {
206 let label = match algorithm {
207 WrapAlgorithm::CmsRsaCbc | WrapAlgorithm::CmsRsaGcm => "CMS",
208 _ => "WRAPPED KEY",
209 };
210 Ok(SerializedArtifact {
211 bytes: encode_pem(label, data).into_bytes(),
212 mime_type: Some("application/x-pem-file".to_string()),
213 extension: "pem",
214 })
215 }
216 _ => Err(format!(
217 "Invalid format '{fmt}' for WrappedKey (valid: der, pem)"
218 )),
219 }
220 }
221
222 ArtifactValue::PublicKey { key_data, .. } => {
223 let fmt = format.unwrap_or("pem");
224 match fmt {
225 "pem" => Ok(SerializedArtifact {
226 bytes: encode_pem("PUBLIC KEY", key_data).into_bytes(),
227 mime_type: Some("application/x-pem-file".to_string()),
228 extension: "pem",
229 }),
230 "der" => Ok(SerializedArtifact {
231 bytes: key_data.clone(),
232 mime_type: Some("application/x-x509-ca-cert".to_string()),
235 extension: "der",
236 }),
237 "base64" => Ok(SerializedArtifact {
238 bytes: base64_encode(key_data).into_bytes(),
239 mime_type: Some("text/plain".to_string()),
240 extension: "txt",
241 }),
242 _ => Err(format!(
243 "Invalid format '{fmt}' for PublicKey (valid: pem, der, base64)"
244 )),
245 }
246 }
247
248 ArtifactValue::BackendKey { public_key, .. } => {
249 if let Some(pub_key) = public_key {
250 let fmt = format.unwrap_or("pem");
252 match fmt {
253 "pem" => Ok(SerializedArtifact {
254 bytes: encode_pem("PUBLIC KEY", pub_key).into_bytes(),
255 mime_type: Some("application/x-pem-file".to_string()),
256 extension: "pem",
257 }),
258 "der" => Ok(SerializedArtifact {
259 bytes: pub_key.clone(),
260 mime_type: Some("application/x-x509-ca-cert".to_string()),
261 extension: "der",
262 }),
263 "base64" => Ok(SerializedArtifact {
264 bytes: base64_encode(pub_key).into_bytes(),
265 mime_type: Some("text/plain".to_string()),
266 extension: "txt",
267 }),
268 _ => Err(format!(
269 "Invalid format '{fmt}' for BackendKey (valid: pem, der, base64)"
270 )),
271 }
272 } else {
273 Err("Cannot export public key from non-exportable backend key".to_string())
274 }
275 }
276
277 ArtifactValue::Bytes(bytes) => Ok(SerializedArtifact {
278 bytes: bytes.clone(),
279 mime_type: Some("application/octet-stream".to_string()),
280 extension: "bin",
281 }),
282 ArtifactValue::Text(text) => Ok(SerializedArtifact {
283 bytes: text.as_bytes().to_vec(),
284 mime_type: Some("text/plain".to_string()),
285 extension: "txt",
286 }),
287
288 ArtifactValue::Certificate { der } => {
289 let fmt = format.unwrap_or("pem");
290 match fmt {
291 "pem" => Ok(SerializedArtifact {
292 bytes: encode_pem("CERTIFICATE", der).into_bytes(),
293 mime_type: Some("application/x-pem-file".to_string()),
294 extension: "pem",
295 }),
296 "der" => Ok(SerializedArtifact {
297 bytes: der.clone(),
298 mime_type: Some("application/pkix-cert".to_string()),
299 extension: "der",
300 }),
301 _ => Err(format!(
302 "Invalid format '{fmt}' for Certificate (valid: pem, der)"
303 )),
304 }
305 }
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_public_key_format_validation() {
316 let public_der = b"PUBLIC_KEY_DER_DATA".to_vec();
317 let public_key = ArtifactValue::PublicKey {
318 key_data: public_der.clone(),
319 format: KeyFormat::Pem,
320 };
321
322 let serialized = public_key.serialize(None).unwrap();
324 assert_eq!(serialized.extension, "pem");
325 assert_eq!(
326 serialized.mime_type,
327 Some("application/x-pem-file".to_string())
328 );
329
330 let serialized = public_key.serialize(Some("pem")).unwrap();
332 assert_eq!(serialized.extension, "pem");
333 assert_eq!(
334 serialized.mime_type,
335 Some("application/x-pem-file".to_string())
336 );
337
338 let serialized = public_key.serialize(Some("der")).unwrap();
340 assert_eq!(
341 serialized.bytes, public_der,
342 "DER format should output raw key data"
343 );
344 assert_eq!(serialized.extension, "der");
345 assert_eq!(
346 serialized.mime_type,
347 Some("application/x-x509-ca-cert".to_string())
348 );
349
350 let serialized = public_key.serialize(Some("base64")).unwrap();
352 assert_eq!(serialized.extension, "txt");
353 assert_eq!(serialized.mime_type, Some("text/plain".to_string()));
354
355 let result = public_key.serialize(Some("json"));
357 assert!(result.is_err(), "Invalid format should return error");
358 assert!(
359 result.unwrap_err().contains("Invalid format 'json'"),
360 "Error message should mention invalid format"
361 );
362 }
363
364 #[test]
365 fn test_wrapped_key_format_validation() {
366 let cms_data = b"CMS_DATA".to_vec();
367 let wrapped = ArtifactValue::WrappedKey {
368 data: cms_data.clone(),
369 algorithm: WrapAlgorithm::CmsRsaGcm,
370 };
371
372 let serialized = wrapped.serialize(None).unwrap();
374 assert_eq!(
375 serialized.bytes, cms_data,
376 "Default format should output raw CMS data"
377 );
378 assert_eq!(serialized.extension, "p7c");
379 assert_eq!(
380 serialized.mime_type,
381 Some("application/pkcs7-mime".to_string())
382 );
383
384 let serialized = wrapped.serialize(Some("der")).unwrap();
386 assert_eq!(serialized.bytes, cms_data);
387 assert_eq!(serialized.extension, "p7c");
388 assert_eq!(
389 serialized.mime_type,
390 Some("application/pkcs7-mime".to_string())
391 );
392
393 let serialized = wrapped.serialize(Some("pem")).unwrap();
395 let pem_str = String::from_utf8(serialized.bytes).unwrap();
396 assert!(
397 pem_str.contains("-----BEGIN CMS-----"),
398 "PEM should contain CMS header"
399 );
400 assert_eq!(serialized.extension, "pem");
401 assert_eq!(
402 serialized.mime_type,
403 Some("application/x-pem-file".to_string())
404 );
405
406 let result = wrapped.serialize(Some("base64"));
408 assert!(result.is_err(), "Invalid format should return error");
409 assert!(
410 result.unwrap_err().contains("Invalid format 'base64'"),
411 "Error message should mention invalid format"
412 );
413 }
414}