1use base64ct::{Base64, Encoding};
4
5use rite_sdk::{
6 CertificateDer, EncryptedData, KeyAlgorithm, KeyCheckValue, KeyId, PublicKeyDer, WrapScheme,
7 WrappedKey,
8};
9use secrecy::{ExposeSecret, SecretBox};
10
11#[derive(Debug)]
13pub enum ArtifactValue {
14 BackendKey {
17 backend_name: String,
19 key_id: KeyId,
21 algorithm: KeyAlgorithm,
23 public_key: Option<PublicKeyDer>,
25 check_value: Option<KeyCheckValue>,
31 },
32 WrappedKey(WrappedKey),
36 EncryptedData(EncryptedData),
44 PublicKey(PublicKeyDer),
46 Bytes(Vec<u8>),
49 Secret(SecretBox<Vec<u8>>),
59 Shares(ShareSet),
70
71 Text(String),
74
75 Certificate(CertificateDer),
77}
78
79pub struct ShareSet {
87 threshold: u8,
88 shares: Vec<Share>,
89}
90
91pub struct Share {
94 threshold: u8,
95 index: u8,
96 y: SecretBox<Vec<u8>>,
97}
98
99impl Share {
100 pub fn new(threshold: u8, index: u8, y: Vec<u8>) -> Self {
102 Self {
103 threshold,
104 index,
105 y: SecretBox::new(Box::new(y)),
106 }
107 }
108
109 pub fn threshold(&self) -> u8 {
111 self.threshold
112 }
113
114 pub fn index(&self) -> u8 {
116 self.index
117 }
118
119 pub fn y(&self) -> &[u8] {
121 self.y.expose_secret()
122 }
123}
124
125impl std::fmt::Debug for Share {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 write!(
128 f,
129 "Share(index={}, threshold={}, {} bytes)",
130 self.index,
131 self.threshold,
132 self.y.expose_secret().len()
133 )
134 }
135}
136
137impl ShareSet {
138 pub fn new(threshold: u8, shares: impl IntoIterator<Item = Share>) -> Self {
141 Self {
142 threshold,
143 shares: shares.into_iter().collect(),
144 }
145 }
146
147 pub fn threshold(&self) -> u8 {
149 self.threshold
150 }
151
152 pub fn count(&self) -> u8 {
154 u8::try_from(self.shares.len()).unwrap_or(u8::MAX)
155 }
156
157 pub fn share(&self, index: u8) -> Option<&Share> {
159 self.shares.iter().find(|share| share.index == index)
160 }
161
162 pub fn only(&self) -> Option<&Share> {
165 match self.shares.as_slice() {
166 [share] => Some(share),
167 _ => None,
168 }
169 }
170}
171
172impl std::fmt::Debug for ShareSet {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 write!(
175 f,
176 "ShareSet({}-of-{}, {} bytes each)",
177 self.threshold,
178 self.shares.len(),
179 self.shares
180 .first()
181 .map_or(0, |share| share.y.expose_secret().len())
182 )
183 }
184}
185
186impl std::fmt::Display for ArtifactValue {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 ArtifactValue::BackendKey {
191 backend_name,
192 key_id,
193 algorithm,
194 public_key,
195 check_value,
196 } => {
197 if let Some(pub_key) = public_key {
199 write!(
200 f,
201 "BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?})\n{}",
202 key_id.as_str(),
203 encode_pem("PUBLIC KEY", pub_key.as_bytes())
204 )
205 } else if let Some(check_value) = check_value {
206 write!(
211 f,
212 "BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?}, kcv={check_value})",
213 key_id.as_str()
214 )
215 } else {
216 write!(
217 f,
218 "BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?}, public_key=not_exportable)",
219 key_id.as_str()
220 )
221 }
222 }
223 ArtifactValue::WrappedKey(wrapped) => match pem_label(wrapped.scheme()) {
224 Some(label) => write!(f, "{}", encode_pem(label, wrapped.data())),
225 None => write!(
228 f,
229 "WrappedKey({}, {} bytes)",
230 wrapped.scheme(),
231 wrapped.data().len()
232 ),
233 },
234 ArtifactValue::EncryptedData(encrypted) => write!(
235 f,
236 "EncryptedData({}, {} bytes)",
237 encrypted.scheme(),
238 encrypted.data().len()
239 ),
240 ArtifactValue::PublicKey(key) => {
241 let pem = encode_pem("PUBLIC KEY", key.as_bytes());
242 write!(f, "{pem}")
243 }
244 ArtifactValue::Bytes(bytes) => {
246 let len = bytes.len();
247 write!(f, "Bytes({len} bytes)")
248 }
249 ArtifactValue::Secret(bytes) => {
250 let len = bytes.expose_secret().len();
251 write!(f, "Secret({len} bytes)")
252 }
253 ArtifactValue::Shares(set) => write!(f, "{set:?}"),
254 ArtifactValue::Text(text) => {
255 write!(f, "Text({text})")
256 }
257
258 ArtifactValue::Certificate(certificate) => {
259 let pem = encode_pem("CERTIFICATE", certificate.as_bytes());
260 write!(f, "{pem}")
261 }
262 }
263 }
264}
265
266pub(crate) fn base64_encode(data: &[u8]) -> String {
269 Base64::encode_string(data)
270}
271
272fn pem_label(scheme: WrapScheme) -> Option<&'static str> {
278 match scheme {
279 WrapScheme::CmsAes256Gcm => Some("CMS"),
280 _ => None,
281 }
282}
283
284fn serialize_container(
290 scheme: WrapScheme,
291 data: &[u8],
292 format: Option<&str>,
293 kind: &str,
294) -> Result<SerializedArtifact, String> {
295 let (mime, ext) = match scheme {
296 WrapScheme::CmsAes256Gcm => ("application/pkcs7-mime", "p7c"),
297 _ => ("application/octet-stream", "bin"),
298 };
299 match format.unwrap_or("der") {
300 "der" => Ok(SerializedArtifact {
301 bytes: data.to_vec(),
302 mime_type: Some(mime.to_string()),
303 extension: ext,
304 }),
305 "pem" => {
306 let label = pem_label(scheme).ok_or_else(|| {
307 format!(
308 "{scheme} produces raw bytes, which have no PEM form. Write it as 'der', \
309 which is what a recipient imports."
310 )
311 })?;
312 Ok(SerializedArtifact {
313 bytes: encode_pem(label, data).into_bytes(),
314 mime_type: Some("application/x-pem-file".to_string()),
315 extension: "pem",
316 })
317 }
318 other => Err(format!(
319 "Invalid format '{other}' for {kind} (valid: der, pem)"
320 )),
321 }
322}
323
324pub(crate) fn encode_pem(label: &str, der_bytes: &[u8]) -> String {
326 let b64 = base64_encode(der_bytes);
327 let lines: Vec<&str> = b64
329 .as_bytes()
330 .chunks(64)
331 .map(|chunk| std::str::from_utf8(chunk).unwrap_or(""))
332 .collect();
333
334 let body = lines.join("\n");
335 format!("-----BEGIN {label}-----\n{body}\n-----END {label}-----")
336}
337
338#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct SerializedArtifact {
348 pub bytes: Vec<u8>,
350 pub mime_type: Option<String>,
352 pub extension: &'static str,
354}
355
356impl ArtifactValue {
357 pub fn serialize(&self, format: Option<&str>) -> Result<SerializedArtifact, String> {
379 match self {
380 ArtifactValue::WrappedKey(wrapped) => {
381 serialize_container(wrapped.scheme(), wrapped.data(), format, "WrappedKey")
382 }
383 ArtifactValue::EncryptedData(encrypted) => serialize_container(
384 encrypted.scheme(),
385 encrypted.data(),
386 format,
387 "EncryptedData",
388 ),
389
390 ArtifactValue::PublicKey(key) => serialize_der(key.as_bytes(), &PUBLIC_KEY, format),
391
392 ArtifactValue::BackendKey { public_key, .. } => match public_key {
393 Some(key) => serialize_der(key.as_bytes(), &PUBLIC_KEY, format),
394 None => Err("Cannot export public key from non-exportable backend key".to_string()),
395 },
396
397 ArtifactValue::Bytes(bytes) => Ok(SerializedArtifact {
398 bytes: bytes.clone(),
399 mime_type: Some("application/octet-stream".to_string()),
400 extension: "bin",
401 }),
402 ArtifactValue::Secret(bytes) => Ok(SerializedArtifact {
403 bytes: bytes.expose_secret().clone(),
404 mime_type: Some("application/octet-stream".to_string()),
405 extension: "bin",
406 }),
407 ArtifactValue::Shares(_) => Err(
410 "A share set is not written to disk; each share is shown to its custodian \
411 by a step that reads it"
412 .to_string(),
413 ),
414 ArtifactValue::Text(text) => Ok(SerializedArtifact {
415 bytes: text.as_bytes().to_vec(),
416 mime_type: Some("text/plain".to_string()),
417 extension: "txt",
418 }),
419
420 ArtifactValue::Certificate(certificate) => {
421 serialize_der(certificate.as_bytes(), &CERTIFICATE, format)
422 }
423 }
424 }
425}
426
427struct DerArtifact {
433 pem_label: &'static str,
434 der_mime: &'static str,
435 name: &'static str,
436}
437
438const PUBLIC_KEY: DerArtifact = DerArtifact {
440 pem_label: "PUBLIC KEY",
441 der_mime: "application/x-x509-ca-cert",
442 name: "a public key",
443};
444
445const CERTIFICATE: DerArtifact = DerArtifact {
446 pem_label: "CERTIFICATE",
447 der_mime: "application/pkix-cert",
448 name: "a certificate",
449};
450
451fn serialize_der(
453 der: &[u8],
454 artifact: &DerArtifact,
455 format: Option<&str>,
456) -> Result<SerializedArtifact, String> {
457 let fmt = format.unwrap_or("pem");
458 match fmt {
459 "pem" => Ok(SerializedArtifact {
460 bytes: encode_pem(artifact.pem_label, der).into_bytes(),
461 mime_type: Some("application/x-pem-file".to_string()),
462 extension: "pem",
463 }),
464 "der" => Ok(SerializedArtifact {
465 bytes: der.to_vec(),
466 mime_type: Some(artifact.der_mime.to_string()),
467 extension: "der",
468 }),
469 "base64" => Ok(SerializedArtifact {
470 bytes: base64_encode(der).into_bytes(),
471 mime_type: Some("text/plain".to_string()),
472 extension: "txt",
473 }),
474 _ => Err(format!(
475 "Invalid format '{fmt}' for {} (valid: pem, der, base64)",
476 artifact.name
477 )),
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 const P256_SPKI: &[u8] = include_bytes!("testdata/p256.spki.der");
491
492 #[test]
493 fn a_public_key_serializes_as_pem_by_default_and_der_on_request() {
494 let public_der = P256_SPKI.to_vec();
495 let public_key =
496 ArtifactValue::PublicKey(PublicKeyDer::new(public_der.clone()).expect("valid SPKI"));
497
498 let serialized = public_key.serialize(None).unwrap();
500 assert_eq!(serialized.extension, "pem");
501 assert_eq!(
502 serialized.mime_type,
503 Some("application/x-pem-file".to_string())
504 );
505
506 let serialized = public_key.serialize(Some("pem")).unwrap();
508 assert_eq!(serialized.extension, "pem");
509 assert_eq!(
510 serialized.mime_type,
511 Some("application/x-pem-file".to_string())
512 );
513
514 let serialized = public_key.serialize(Some("der")).unwrap();
516 assert_eq!(
517 serialized.bytes, public_der,
518 "DER format should output raw key data"
519 );
520 assert_eq!(serialized.extension, "der");
521 assert_eq!(
522 serialized.mime_type,
523 Some("application/x-x509-ca-cert".to_string())
524 );
525
526 let serialized = public_key.serialize(Some("base64")).unwrap();
528 assert_eq!(serialized.extension, "txt");
529 assert_eq!(serialized.mime_type, Some("text/plain".to_string()));
530
531 let result = public_key.serialize(Some("json"));
533 assert!(result.is_err(), "Invalid format should return error");
534 assert!(
535 result.unwrap_err().contains("Invalid format 'json'"),
536 "Error message should mention invalid format"
537 );
538 }
539
540 #[test]
543 fn a_share_set_is_never_written_to_disk() {
544 let set = ShareSet::new(
545 2,
546 [
547 Share::new(2, 1, vec![0xCD; 8]),
548 Share::new(2, 2, vec![0xEF; 8]),
549 ],
550 );
551 let shares = ArtifactValue::Shares(set);
552 for format in [None, Some("bin"), Some("pem")] {
553 let error = shares
554 .serialize(format)
555 .expect_err("shares have no file form");
556 assert!(error.contains("not written to disk"), "{error}");
557 }
558 }
559
560 #[test]
561 fn a_wrapped_key_serializes_as_der_by_default_and_pem_on_request() {
562 let cms_data = b"CMS_DATA".to_vec();
563 let description = rite_sdk::WrapDescription::new(
564 rite_sdk::RecipientInfoKind::Ktri,
565 rite_sdk::Oid::new(rite_sdk::oid::RSA_ENCRYPTION).unwrap(),
566 )
567 .with_content_encryption(rite_sdk::Oid::new(rite_sdk::oid::AES_256_GCM).unwrap());
568 let wrapped = ArtifactValue::WrappedKey(
569 WrappedKey::new(WrapScheme::CmsAes256Gcm, description, cms_data.clone()).unwrap(),
570 );
571
572 let serialized = wrapped.serialize(None).unwrap();
574 assert_eq!(
575 serialized.bytes, cms_data,
576 "Default format should output raw CMS data"
577 );
578 assert_eq!(serialized.extension, "p7c");
579 assert_eq!(
580 serialized.mime_type,
581 Some("application/pkcs7-mime".to_string())
582 );
583
584 let serialized = wrapped.serialize(Some("der")).unwrap();
586 assert_eq!(serialized.bytes, cms_data);
587 assert_eq!(serialized.extension, "p7c");
588 assert_eq!(
589 serialized.mime_type,
590 Some("application/pkcs7-mime".to_string())
591 );
592
593 let serialized = wrapped.serialize(Some("pem")).unwrap();
595 let pem_str = String::from_utf8(serialized.bytes).unwrap();
596 assert!(
597 pem_str.contains("-----BEGIN CMS-----"),
598 "PEM should contain CMS header"
599 );
600 assert_eq!(serialized.extension, "pem");
601 assert_eq!(
602 serialized.mime_type,
603 Some("application/x-pem-file".to_string())
604 );
605
606 let result = wrapped.serialize(Some("base64"));
608 assert!(result.is_err(), "Invalid format should return error");
609 assert!(
610 result.unwrap_err().contains("Invalid format 'base64'"),
611 "Error message should mention invalid format"
612 );
613 }
614}