Skip to main content

rite_runtime/actions/
types.rs

1//! Core types for action handling.
2
3use base64ct::{Base64, Encoding};
4
5use rite_sdk::{CertificateDer, KeyAlgorithm, KeyId, PublicKeyDer, WrapAlgorithm};
6
7/// Runtime representation of an artifact.
8#[derive(Debug)]
9pub enum ArtifactValue {
10    /// Cryptographic key managed by a backend (software, HSM, `YubiKey`).
11    /// The private key never leaves the backend - only references are stored.
12    BackendKey {
13        /// Name of the backend that owns this key (e.g., "software", "yubikey1").
14        backend_name: String,
15        /// Backend-specific key identifier (opaque, e.g., UUID or "`slot_9c`").
16        key_id: KeyId,
17        /// Algorithm for type checking and evidence.
18        algorithm: KeyAlgorithm,
19        /// Public key (None for non-exportable HSM keys).
20        public_key: Option<PublicKeyDer>,
21    },
22    /// Wrapped key (CMS `EnvelopedData`, AES Key Wrap, or RSA-OAEP).
23    WrappedKey {
24        /// Wrapped key bytes (format depends on algorithm).
25        data: Vec<u8>,
26        /// Wrapping algorithm used.
27        algorithm: WrapAlgorithm,
28    },
29    /// Exported public key.
30    PublicKey(PublicKeyDer),
31    /// Binary content from files or inline data (documents, crypto materials).
32    /// Used for hashing, cryptographic operations, and verification.
33    Bytes(Vec<u8>),
34
35    /// Display text for physical item references (USB drives, tamper bags, etc.).
36    /// Used in messages and prompts when referencing physical objects.
37    Text(String),
38
39    /// X.509 certificate. Stored as DER; displayed and serialized as PEM.
40    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            // Real cryptographic artifacts
47            ArtifactValue::BackendKey {
48                backend_name,
49                key_id,
50                algorithm,
51                public_key,
52                ..
53            } => {
54                // Output backend key metadata and public key if available
55                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            // Materials
83            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
99/// Encode bytes as base64 (standard alphabet with padding).
100/// Uses constant-time encoding from `RustCrypto` `base64ct`.
101pub(crate) fn base64_encode(data: &[u8]) -> String {
102    Base64::encode_string(data)
103}
104
105/// Encode DER bytes as PEM with given label.
106pub(crate) fn encode_pem(label: &str, der_bytes: &[u8]) -> String {
107    let b64 = base64_encode(der_bytes);
108    // Split into 64-character lines (PEM standard)
109    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/// A serialized artifact ready for output.
120///
121/// Contains both the serialized bytes and metadata about the content.
122/// Produced by calling `artifact.serialize(format)`.
123///
124/// Metadata includes:
125/// - MIME type (see <https://pki-tutorial.readthedocs.io/en/latest/mime.html>)
126/// - File extension appropriate for the format
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct SerializedArtifact {
129    /// Serialized content bytes.
130    pub bytes: Vec<u8>,
131    /// MIME type for the artifact in the specified format.
132    pub mime_type: Option<String>,
133    /// File extension appropriate for the format (without leading dot).
134    pub extension: &'static str,
135}
136
137impl ArtifactValue {
138    /// Serialize artifact to bytes with metadata.
139    ///
140    /// Returns a `SerializedArtifact` containing both the serialized bytes
141    /// and metadata (MIME type, file extension) appropriate for the format.
142    ///
143    /// Applies sensible defaults if format is None, then validates strictly.
144    ///
145    /// Supported formats by artifact type:
146    /// - `WrappedKey`: "der" (default), "pem"
147    /// - `PublicKey`: "pem" (default), "der", "base64"
148    /// - `RsaKeypair`: "pem" (default), "der" (private key in PKCS#8)
149    /// - Other artifacts: text representation (format ignored)
150    ///
151    /// MIME type references:
152    /// - application/pkcs7-mime: CMS/PKCS#7 (RFC 5273)
153    /// - application/pkcs8: PKCS#8 private keys (RFC 5958)
154    /// - application/x-pem-file: PEM-encoded files
155    /// - application/x-x509-ca-cert: X.509 certificates (Netscape)
156    /// - text/plain: Text artifacts
157    ///
158    /// Returns an error if an unsupported format is specified.
159    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
217/// How one kind of DER artifact renders.
218///
219/// Public keys and certificates take the same three formats and differ only in
220/// their PEM label, their DER media type, and how they are named when a format
221/// is refused, so [`serialize_der`] holds the ladder once.
222struct DerArtifact {
223    pem_label: &'static str,
224    der_mime: &'static str,
225    name: &'static str,
226}
227
228/// x509-ca-cert by convention: no official media type exists for bare SPKI DER.
229const 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
241/// Render DER material in the format a ceremony asked for, PEM by default.
242fn 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    /// A P-256 public key in SPKI DER, so the artifact holds a real one.
276    ///
277    /// Reproduce with:
278    /// `openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 |
279    ///  openssl pkey -pubout -outform DER`
280    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        // Test default format (PEM)
289        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        // Test explicit PEM format
297        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        // Test DER format
305        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        // Test base64 format
317        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        // Invalid format
322        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        // Test default format (DER)
339        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        // Test explicit DER format
351        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        // Test PEM format
360        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        // Invalid format
373        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}