use base64ct::{Base64, Encoding};
use rite_sdk::{
CertificateDer, KeyAlgorithm, KeyCheckValue, KeyId, PublicKeyDer, WrapScheme, WrappedKey,
};
#[derive(Debug)]
pub enum ArtifactValue {
BackendKey {
backend_name: String,
key_id: KeyId,
algorithm: KeyAlgorithm,
public_key: Option<PublicKeyDer>,
check_value: Option<KeyCheckValue>,
},
WrappedKey(WrappedKey),
PublicKey(PublicKeyDer),
Bytes(Vec<u8>),
Text(String),
Certificate(CertificateDer),
}
impl std::fmt::Display for ArtifactValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArtifactValue::BackendKey {
backend_name,
key_id,
algorithm,
public_key,
check_value,
} => {
if let Some(pub_key) = public_key {
write!(
f,
"BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?})\n{}",
key_id.as_str(),
encode_pem("PUBLIC KEY", pub_key.as_bytes())
)
} else if let Some(check_value) = check_value {
write!(
f,
"BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?}, kcv={check_value})",
key_id.as_str()
)
} else {
write!(
f,
"BackendKey(backend={backend_name}, key_id={}, algorithm={algorithm:?}, public_key=not_exportable)",
key_id.as_str()
)
}
}
ArtifactValue::WrappedKey(wrapped) => match pem_label(wrapped.scheme()) {
Some(label) => write!(f, "{}", encode_pem(label, wrapped.data())),
None => write!(
f,
"WrappedKey({}, {} bytes)",
wrapped.scheme(),
wrapped.data().len()
),
},
ArtifactValue::PublicKey(key) => {
let pem = encode_pem("PUBLIC KEY", key.as_bytes());
write!(f, "{pem}")
}
ArtifactValue::Bytes(bytes) => {
let len = bytes.len();
write!(f, "Bytes({len} bytes)")
}
ArtifactValue::Text(text) => {
write!(f, "Text({text})")
}
ArtifactValue::Certificate(certificate) => {
let pem = encode_pem("CERTIFICATE", certificate.as_bytes());
write!(f, "{pem}")
}
}
}
}
pub(crate) fn base64_encode(data: &[u8]) -> String {
Base64::encode_string(data)
}
fn pem_label(scheme: WrapScheme) -> Option<&'static str> {
match scheme {
WrapScheme::CmsAes256Gcm => Some("CMS"),
_ => None,
}
}
pub(crate) fn encode_pem(label: &str, der_bytes: &[u8]) -> String {
let b64 = base64_encode(der_bytes);
let lines: Vec<&str> = b64
.as_bytes()
.chunks(64)
.map(|chunk| std::str::from_utf8(chunk).unwrap_or(""))
.collect();
let body = lines.join("\n");
format!("-----BEGIN {label}-----\n{body}\n-----END {label}-----")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SerializedArtifact {
pub bytes: Vec<u8>,
pub mime_type: Option<String>,
pub extension: &'static str,
}
impl ArtifactValue {
pub fn serialize(&self, format: Option<&str>) -> Result<SerializedArtifact, String> {
match self {
ArtifactValue::WrappedKey(wrapped) => {
let fmt = format.unwrap_or("der");
let (mime, ext) = match wrapped.scheme() {
WrapScheme::CmsAes256Gcm => ("application/pkcs7-mime", "p7c"),
_ => ("application/octet-stream", "bin"),
};
match fmt {
"der" => Ok(SerializedArtifact {
bytes: wrapped.data().to_vec(),
mime_type: Some(mime.to_string()),
extension: ext,
}),
"pem" => {
let label = pem_label(wrapped.scheme()).ok_or_else(|| {
format!(
"{} produces raw bytes, which have no PEM form. Write it as \
'der', which is what a recipient imports.",
wrapped.scheme()
)
})?;
Ok(SerializedArtifact {
bytes: encode_pem(label, wrapped.data()).into_bytes(),
mime_type: Some("application/x-pem-file".to_string()),
extension: "pem",
})
}
_ => Err(format!(
"Invalid format '{fmt}' for WrappedKey (valid: der, pem)"
)),
}
}
ArtifactValue::PublicKey(key) => serialize_der(key.as_bytes(), &PUBLIC_KEY, format),
ArtifactValue::BackendKey { public_key, .. } => match public_key {
Some(key) => serialize_der(key.as_bytes(), &PUBLIC_KEY, format),
None => Err("Cannot export public key from non-exportable backend key".to_string()),
},
ArtifactValue::Bytes(bytes) => Ok(SerializedArtifact {
bytes: bytes.clone(),
mime_type: Some("application/octet-stream".to_string()),
extension: "bin",
}),
ArtifactValue::Text(text) => Ok(SerializedArtifact {
bytes: text.as_bytes().to_vec(),
mime_type: Some("text/plain".to_string()),
extension: "txt",
}),
ArtifactValue::Certificate(certificate) => {
serialize_der(certificate.as_bytes(), &CERTIFICATE, format)
}
}
}
}
struct DerArtifact {
pem_label: &'static str,
der_mime: &'static str,
name: &'static str,
}
const PUBLIC_KEY: DerArtifact = DerArtifact {
pem_label: "PUBLIC KEY",
der_mime: "application/x-x509-ca-cert",
name: "a public key",
};
const CERTIFICATE: DerArtifact = DerArtifact {
pem_label: "CERTIFICATE",
der_mime: "application/pkix-cert",
name: "a certificate",
};
fn serialize_der(
der: &[u8],
artifact: &DerArtifact,
format: Option<&str>,
) -> Result<SerializedArtifact, String> {
let fmt = format.unwrap_or("pem");
match fmt {
"pem" => Ok(SerializedArtifact {
bytes: encode_pem(artifact.pem_label, der).into_bytes(),
mime_type: Some("application/x-pem-file".to_string()),
extension: "pem",
}),
"der" => Ok(SerializedArtifact {
bytes: der.to_vec(),
mime_type: Some(artifact.der_mime.to_string()),
extension: "der",
}),
"base64" => Ok(SerializedArtifact {
bytes: base64_encode(der).into_bytes(),
mime_type: Some("text/plain".to_string()),
extension: "txt",
}),
_ => Err(format!(
"Invalid format '{fmt}' for {} (valid: pem, der, base64)",
artifact.name
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
const P256_SPKI: &[u8] = include_bytes!("testdata/p256.spki.der");
#[test]
fn test_public_key_format_validation() {
let public_der = P256_SPKI.to_vec();
let public_key =
ArtifactValue::PublicKey(PublicKeyDer::new(public_der.clone()).expect("valid SPKI"));
let serialized = public_key.serialize(None).unwrap();
assert_eq!(serialized.extension, "pem");
assert_eq!(
serialized.mime_type,
Some("application/x-pem-file".to_string())
);
let serialized = public_key.serialize(Some("pem")).unwrap();
assert_eq!(serialized.extension, "pem");
assert_eq!(
serialized.mime_type,
Some("application/x-pem-file".to_string())
);
let serialized = public_key.serialize(Some("der")).unwrap();
assert_eq!(
serialized.bytes, public_der,
"DER format should output raw key data"
);
assert_eq!(serialized.extension, "der");
assert_eq!(
serialized.mime_type,
Some("application/x-x509-ca-cert".to_string())
);
let serialized = public_key.serialize(Some("base64")).unwrap();
assert_eq!(serialized.extension, "txt");
assert_eq!(serialized.mime_type, Some("text/plain".to_string()));
let result = public_key.serialize(Some("json"));
assert!(result.is_err(), "Invalid format should return error");
assert!(
result.unwrap_err().contains("Invalid format 'json'"),
"Error message should mention invalid format"
);
}
#[test]
fn test_wrapped_key_format_validation() {
let cms_data = b"CMS_DATA".to_vec();
let description = rite_sdk::WrapDescription::new(
rite_sdk::RecipientInfoKind::Ktri,
rite_sdk::Oid::new(rite_sdk::oid::RSA_ENCRYPTION).unwrap(),
)
.with_content_encryption(rite_sdk::Oid::new(rite_sdk::oid::AES_256_GCM).unwrap());
let wrapped = ArtifactValue::WrappedKey(
WrappedKey::new(WrapScheme::CmsAes256Gcm, description, cms_data.clone()).unwrap(),
);
let serialized = wrapped.serialize(None).unwrap();
assert_eq!(
serialized.bytes, cms_data,
"Default format should output raw CMS data"
);
assert_eq!(serialized.extension, "p7c");
assert_eq!(
serialized.mime_type,
Some("application/pkcs7-mime".to_string())
);
let serialized = wrapped.serialize(Some("der")).unwrap();
assert_eq!(serialized.bytes, cms_data);
assert_eq!(serialized.extension, "p7c");
assert_eq!(
serialized.mime_type,
Some("application/pkcs7-mime".to_string())
);
let serialized = wrapped.serialize(Some("pem")).unwrap();
let pem_str = String::from_utf8(serialized.bytes).unwrap();
assert!(
pem_str.contains("-----BEGIN CMS-----"),
"PEM should contain CMS header"
);
assert_eq!(serialized.extension, "pem");
assert_eq!(
serialized.mime_type,
Some("application/x-pem-file".to_string())
);
let result = wrapped.serialize(Some("base64"));
assert!(result.is_err(), "Invalid format should return error");
assert!(
result.unwrap_err().contains("Invalid format 'base64'"),
"Error message should mention invalid format"
);
}
}