Skip to main content

rite_runtime/
artifact_resolver.rs

1//! Artifact resolution for ceremony execution.
2//!
3//! This module provides utilities for resolving artifacts stored in the `ExecutionState`
4//! to their byte content or backend key metadata. Artifact references are pre-parsed
5//! at resolution time (see `StepInputs` in rite-model), so no `${...}` parsing
6//! happens here.
7//!
8//! ## Key Functions
9//!
10//! - [`resolve_artifact_bytes`] - Extract byte content from an artifact
11//! - [`resolve_backend_key`] - Get backend key metadata for backend operations
12
13use crate::actions::ArtifactValue;
14use crate::executor::ExecutionError;
15use rite_model::ArtifactId;
16use rite_sdk::{KeyAlgorithm, KeyId, PublicKeyDer};
17use std::collections::HashMap;
18use std::hash::BuildHasher;
19
20/// Resolve an artifact reference to byte content.
21///
22/// # Arguments
23/// * `artifacts` - The artifact store from `ActionContext`
24/// * `artifact_id` - The artifact ID
25/// * `property` - Optional subproperty (e.g., "private" or "public")
26///
27/// # Returns
28/// The resolved bytes, or an error if resolution fails.
29///
30/// # Supported references
31/// - `artifact_id` → full artifact content
32/// - `artifact_id` + "private" → private key from keypair
33/// - `artifact_id` + "public" → public key from keypair
34pub fn resolve_artifact_bytes<S: BuildHasher>(
35    artifacts: &HashMap<ArtifactId, ArtifactValue, S>,
36    artifact_id: &ArtifactId,
37    property: Option<&str>,
38) -> Result<Vec<u8>, ExecutionError> {
39    let artifact = artifacts.get(artifact_id).ok_or_else(|| {
40        ExecutionError::InvalidParams(format!("Artifact '{artifact_id}' not found"))
41    })?;
42
43    match (artifact, property) {
44        // Backend-managed key - only public key is accessible
45        (
46            ArtifactValue::BackendKey {
47                public_key: Some(pub_key),
48                ..
49            },
50            Some("public"),
51        ) => Ok(pub_key.as_bytes().to_vec()),
52        (
53            ArtifactValue::BackendKey {
54                public_key: None, ..
55            },
56            Some("public"),
57        ) => {
58            let id = artifact_id.as_str();
59            Err(ExecutionError::InvalidParams(format!(
60                "Public key for '{id}' is not exportable from backend"
61            )))
62        }
63        (ArtifactValue::BackendKey { .. }, Some("private")) => {
64            let id = artifact_id.as_str();
65            Err(ExecutionError::InvalidParams(format!(
66                "Cannot access private key from backend-managed key '{id}'"
67            )))
68        }
69
70        // Real public key
71        (ArtifactValue::PublicKey(key), None) => Ok(key.as_bytes().to_vec()),
72
73        // Real wrapped key
74        (ArtifactValue::WrappedKey { data, .. }, None) => Ok(data.clone()),
75
76        // Materials (loaded from files or inline)
77        (ArtifactValue::Bytes(bytes), None) => Ok(bytes.clone()),
78        (ArtifactValue::Text(text), None) => Ok(text.as_bytes().to_vec()),
79
80        // X.509 certificate: the whole certificate, not the key inside it.
81        // `issue_certificate` reads an issuer certificate through here.
82        (ArtifactValue::Certificate(certificate), None) => Ok(certificate.as_bytes().to_vec()),
83
84        // Invalid combinations
85        _ => Err(ExecutionError::InvalidParams(format!(
86            "Cannot extract bytes from artifact '{artifact_id}' with property '{property:?}'"
87        ))),
88    }
89}
90
91/// What an action needs to know about a backend-managed key.
92///
93/// Borrowed from the artifact store rather than cloned: a caller that needs an
94/// owned `KeyId` or key past the borrow clones the field it needs.
95#[derive(Debug, Clone, Copy)]
96#[non_exhaustive]
97pub struct BackendKeyMeta<'a> {
98    /// Backend that owns the key. A step running on another one is an error.
99    pub backend_name: &'a str,
100    /// The reference that backend answers to.
101    pub key_id: &'a KeyId,
102    /// Algorithm, for choosing a signature scheme and for the transcript.
103    pub algorithm: KeyAlgorithm,
104    /// Public half, absent for a key the backend does not export.
105    pub public_key: Option<&'a PublicKeyDer>,
106}
107
108/// Resolve an artifact to a backend-managed key reference.
109///
110/// Used when an action operates on a key the backend holds, which is every
111/// action that needs a private key.
112///
113/// # Errors
114/// Returns an error if:
115/// - The artifact doesn't exist
116/// - The artifact is not a `BackendKey` variant
117pub fn resolve_backend_key<'a, S: BuildHasher>(
118    artifacts: &'a HashMap<ArtifactId, ArtifactValue, S>,
119    artifact_id: &ArtifactId,
120) -> Result<BackendKeyMeta<'a>, ExecutionError> {
121    let id = artifact_id.as_str();
122    let artifact = artifacts
123        .get(artifact_id)
124        .ok_or_else(|| ExecutionError::InvalidParams(format!("Artifact '{id}' not found")))?;
125
126    match artifact {
127        ArtifactValue::BackendKey {
128            backend_name,
129            key_id,
130            algorithm,
131            public_key,
132        } => Ok(BackendKeyMeta {
133            backend_name: backend_name.as_str(),
134            key_id,
135            algorithm: *algorithm,
136            public_key: public_key.as_ref(),
137        }),
138        _ => Err(ExecutionError::InvalidParams(format!(
139            "Artifact '{id}' is not a backend-managed key"
140        ))),
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_resolve_bytes() {
150        let mut artifacts = HashMap::new();
151        artifacts.insert(
152            ArtifactId::new("ksr"),
153            ArtifactValue::Bytes(b"test content".to_vec()),
154        );
155
156        let result = resolve_artifact_bytes(&artifacts, &ArtifactId::new("ksr"), None).unwrap();
157        assert_eq!(result, b"test content");
158    }
159
160    #[test]
161    fn test_resolve_text() {
162        let mut artifacts = HashMap::new();
163        artifacts.insert(
164            ArtifactId::new("usb_drive"),
165            ArtifactValue::Text("USB Drive".to_string()),
166        );
167
168        let result =
169            resolve_artifact_bytes(&artifacts, &ArtifactId::new("usb_drive"), None).unwrap();
170        assert_eq!(result, b"USB Drive");
171    }
172
173    #[test]
174    fn test_resolve_not_found() {
175        let artifacts: HashMap<ArtifactId, ArtifactValue> = HashMap::new();
176        let result = resolve_artifact_bytes(&artifacts, &ArtifactId::new("missing"), None);
177        assert!(result.is_err());
178    }
179}