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, Share};
14use crate::executor::ExecutionError;
15use rite_model::ArtifactId;
16use rite_sdk::{KeyAlgorithm, KeyCheckValue, KeyId, PublicKeyDer};
17use secrecy::ExposeSecret;
18use std::collections::HashMap;
19use std::hash::BuildHasher;
20
21/// Resolve an artifact reference to byte content.
22///
23/// # Arguments
24/// * `artifacts` - The artifact store from `ActionContext`
25/// * `artifact_id` - The artifact ID
26/// * `property` - Optional subproperty (e.g., "private" or "public")
27///
28/// # Returns
29/// The bytes, borrowed from the store. A reader that only looks at them makes
30/// no copy, which is what keeps opened content from being duplicated on every
31/// read; one that needs to keep them copies on its own account.
32///
33/// # Supported references
34/// - `artifact_id` → full artifact content
35/// - `artifact_id` + "private" → private key from keypair
36/// - `artifact_id` + "public" → public key from keypair
37/// - `artifact_id` + "kcv" → key check value of a symmetric key
38pub fn resolve_artifact_bytes<'a, S: BuildHasher>(
39    artifacts: &'a HashMap<ArtifactId, ArtifactValue, S>,
40    artifact_id: &ArtifactId,
41    property: Option<&str>,
42) -> Result<&'a [u8], ExecutionError> {
43    let artifact = artifacts.get(artifact_id).ok_or_else(|| {
44        ExecutionError::InvalidParams(format!("Artifact '{artifact_id}' not found"))
45    })?;
46
47    match (artifact, property) {
48        // Backend-managed key - only public key is accessible
49        (
50            ArtifactValue::BackendKey {
51                public_key: Some(pub_key),
52                ..
53            },
54            Some("public"),
55        ) => Ok(pub_key.as_bytes()),
56        (
57            ArtifactValue::BackendKey {
58                public_key: None, ..
59            },
60            Some("public"),
61        ) => {
62            let id = artifact_id.as_str();
63            Err(ExecutionError::InvalidParams(format!(
64                "Public key for '{id}' is not exportable from backend"
65            )))
66        }
67        (ArtifactValue::BackendKey { .. }, Some("private")) => {
68            let id = artifact_id.as_str();
69            Err(ExecutionError::InvalidParams(format!(
70                "Cannot access private key from backend-managed key '{id}'"
71            )))
72        }
73
74        // The check value of a symmetric key, as its three raw bytes, so
75        // `${artifact.kek.kcv | hex}` reads the way an operator says it.
76        (
77            ArtifactValue::BackendKey {
78                check_value: Some(kcv),
79                ..
80            },
81            Some("kcv"),
82        ) => Ok(kcv.as_bytes()),
83        (
84            ArtifactValue::BackendKey {
85                check_value: None,
86                algorithm,
87                ..
88            },
89            Some("kcv"),
90        ) => {
91            let id = artifact_id.as_str();
92            Err(ExecutionError::InvalidParams(format!(
93                "'{id}' is a {algorithm} key, which is named by its public half \
94                 rather than by a check value"
95            )))
96        }
97
98        // Real public key
99        (ArtifactValue::PublicKey(key), None) => Ok(key.as_bytes()),
100
101        // Real wrapped key
102        (ArtifactValue::WrappedKey(wrapped), None) => Ok(wrapped.data()),
103
104        // The container, as it would be written to media. What is inside it
105        // needs the key, which is `decrypt_data`'s job and not this one's.
106        (ArtifactValue::EncryptedData(encrypted), None) => Ok(encrypted.data()),
107
108        // Materials (loaded from files or inline), and content a step opened.
109        // The second reads like the first: what a ceremony does with opened
110        // content is what it does with any bytes, and only the write to disk
111        // is gated.
112        (ArtifactValue::Bytes(bytes), None) => Ok(bytes),
113        (ArtifactValue::Secret(bytes), None) => Ok(bytes.expose_secret()),
114        (ArtifactValue::Text(text), None) => Ok(text.as_bytes()),
115
116        // A share has no byte form to borrow; laying one out is a container's
117        // job. A step that reads a share asks for the share.
118        (ArtifactValue::Shares(_), _) => Err(ExecutionError::InvalidParams(format!(
119            "'{artifact_id}' holds shares of a secret, which are read as shares and not as \
120             bytes; only a step that shows or combines shares can name one"
121        ))),
122
123        // X.509 certificate: the whole certificate, not the key inside it.
124        // `issue_certificate` reads an issuer certificate through here.
125        (ArtifactValue::Certificate(certificate), None) => Ok(certificate.as_bytes()),
126
127        // Invalid combinations
128        _ => Err(ExecutionError::InvalidParams(format!(
129            "Cannot extract bytes from artifact '{artifact_id}' with property '{property:?}'"
130        ))),
131    }
132}
133
134/// Resolve a reference to one share.
135///
136/// `shares.share_N` names the share evaluated at `N` in a set a split made.
137/// A set holding exactly one share, as a typed-back share arrives, is named
138/// without a property. Borrowed, like every read.
139pub fn resolve_share<'a, S: BuildHasher>(
140    artifacts: &'a HashMap<ArtifactId, ArtifactValue, S>,
141    artifact_id: &ArtifactId,
142    property: Option<&str>,
143) -> Result<&'a Share, ExecutionError> {
144    let artifact = artifacts.get(artifact_id).ok_or_else(|| {
145        ExecutionError::InvalidParams(format!("Artifact '{artifact_id}' not found"))
146    })?;
147    let ArtifactValue::Shares(set) = artifact else {
148        return Err(ExecutionError::InvalidParams(format!(
149            "'{artifact_id}' is not a share; a share is one of a set 'split_secret' made, or \
150             one a custodian typed back"
151        )));
152    };
153    match property {
154        Some(property) => {
155            let index = share_index(property).ok_or_else(|| {
156                ExecutionError::InvalidParams(format!(
157                    "'{property}' is not a share of '{artifact_id}'; a share is named \
158                     'share_1' through 'share_{}'",
159                    set.count()
160                ))
161            })?;
162            set.share(index).ok_or_else(|| {
163                ExecutionError::InvalidParams(format!(
164                    "'{artifact_id}' has shares 1 through {}, and share_{index} is not one of them",
165                    set.count()
166                ))
167            })
168        }
169        None => set.only().ok_or_else(|| {
170            ExecutionError::InvalidParams(format!(
171                "'{artifact_id}' is a set of {} shares; name one as '{artifact_id}.share_1' \
172                 through '{artifact_id}.share_{}'",
173                set.count(),
174                set.count()
175            ))
176        }),
177    }
178}
179
180/// The `N` of a `share_N` property, or `None` for anything else.
181///
182/// `N` is one or more ASCII digits, no sign, no leading zero, from 1 to
183/// 255: the one spelling of each index, so a name is a share or is not.
184fn share_index(property: &str) -> Option<u8> {
185    let digits = property.strip_prefix("share_")?;
186    if digits.is_empty() || digits.starts_with('0') || !digits.bytes().all(|b| b.is_ascii_digit()) {
187        return None;
188    }
189    digits.parse::<u8>().ok()
190}
191
192/// What an action needs to know about a backend-managed key.
193///
194/// Borrowed from the artifact store rather than cloned: a caller that needs an
195/// owned `KeyId` or key past the borrow clones the field it needs.
196#[derive(Debug, Clone, Copy)]
197#[non_exhaustive]
198pub struct BackendKeyMeta<'a> {
199    /// Backend that owns the key. A step running on another one is an error.
200    pub backend_name: &'a str,
201    /// The reference that backend answers to.
202    pub key_id: &'a KeyId,
203    /// Algorithm, for choosing a signature scheme and for the transcript.
204    pub algorithm: KeyAlgorithm,
205    /// Public half, absent for a key the backend does not export.
206    pub public_key: Option<&'a PublicKeyDer>,
207    /// Check value, present for a symmetric key and absent for a keypair.
208    ///
209    /// What names a key with no public half, so a step recording which key it
210    /// operated on has something to record for either form.
211    pub check_value: Option<&'a KeyCheckValue>,
212}
213
214/// Resolve an artifact to a backend-managed key reference.
215///
216/// Used when an action operates on a key the backend holds, which is every
217/// action that needs a private key.
218///
219/// # Errors
220/// Returns an error if:
221/// - The artifact doesn't exist
222/// - The artifact is not a `BackendKey` variant
223pub fn resolve_backend_key<'a, S: BuildHasher>(
224    artifacts: &'a HashMap<ArtifactId, ArtifactValue, S>,
225    artifact_id: &ArtifactId,
226) -> Result<BackendKeyMeta<'a>, ExecutionError> {
227    let id = artifact_id.as_str();
228    let artifact = artifacts
229        .get(artifact_id)
230        .ok_or_else(|| ExecutionError::InvalidParams(format!("Artifact '{id}' not found")))?;
231
232    match artifact {
233        ArtifactValue::BackendKey {
234            backend_name,
235            key_id,
236            algorithm,
237            public_key,
238            check_value,
239        } => Ok(BackendKeyMeta {
240            backend_name: backend_name.as_str(),
241            key_id,
242            algorithm: *algorithm,
243            public_key: public_key.as_ref(),
244            check_value: check_value.as_ref(),
245        }),
246        _ => Err(ExecutionError::InvalidParams(format!(
247            "Artifact '{id}' is not a backend-managed key"
248        ))),
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::actions::ShareSet;
256
257    #[test]
258    fn resolving_a_bytes_artifact_returns_its_content() {
259        let mut artifacts = HashMap::new();
260        artifacts.insert(
261            ArtifactId::new("ksr"),
262            ArtifactValue::Bytes(b"test content".to_vec()),
263        );
264
265        let result = resolve_artifact_bytes(&artifacts, &ArtifactId::new("ksr"), None).unwrap();
266        assert_eq!(result, b"test content");
267    }
268
269    #[test]
270    fn resolving_a_text_artifact_returns_its_utf8_bytes() {
271        let mut artifacts = HashMap::new();
272        artifacts.insert(
273            ArtifactId::new("usb_drive"),
274            ArtifactValue::Text("USB Drive".to_string()),
275        );
276
277        let result =
278            resolve_artifact_bytes(&artifacts, &ArtifactId::new("usb_drive"), None).unwrap();
279        assert_eq!(result, b"USB Drive");
280    }
281
282    fn two_shares() -> HashMap<ArtifactId, ArtifactValue> {
283        let set = ShareSet::new(
284            2,
285            [
286                Share::new(2, 1, vec![0x11; 4]),
287                Share::new(2, 2, vec![0x22; 4]),
288            ],
289        );
290        HashMap::from([(ArtifactId::new("shares"), ArtifactValue::Shares(set))])
291    }
292
293    #[test]
294    fn a_share_set_has_no_byte_form() {
295        let artifacts = two_shares();
296        for property in [None, Some("share_1")] {
297            let error = resolve_artifact_bytes(&artifacts, &ArtifactId::new("shares"), property)
298                .expect_err("shares are read as shares")
299                .to_string();
300            assert!(error.contains("not as bytes"), "{error}");
301        }
302    }
303
304    #[test]
305    fn a_share_is_named_by_index_and_a_set_of_one_needs_no_name() {
306        let artifacts = two_shares();
307        let id = ArtifactId::new("shares");
308        assert_eq!(
309            resolve_share(&artifacts, &id, Some("share_2"))
310                .unwrap()
311                .index(),
312            2
313        );
314
315        let missing = resolve_share(&artifacts, &id, Some("share_3"))
316            .unwrap_err()
317            .to_string();
318        assert!(missing.contains("share_3 is not one of them"), "{missing}");
319        let misspelt = resolve_share(&artifacts, &id, Some("share_02"))
320            .unwrap_err()
321            .to_string();
322        assert!(misspelt.contains("'share_02' is not a share"), "{misspelt}");
323        let unnamed = resolve_share(&artifacts, &id, None)
324            .unwrap_err()
325            .to_string();
326        assert!(unnamed.contains("is a set of 2 shares"), "{unnamed}");
327
328        let one = ArtifactId::new("typed_back");
329        let mut artifacts = artifacts;
330        artifacts.insert(
331            one.clone(),
332            ArtifactValue::Shares(ShareSet::new(2, [Share::new(2, 3, vec![0x33; 4])])),
333        );
334        assert_eq!(resolve_share(&artifacts, &one, None).unwrap().index(), 3);
335
336        let bytes = ArtifactId::new("bytes");
337        artifacts.insert(bytes.clone(), ArtifactValue::Bytes(vec![1]));
338        let not_a_share = resolve_share(&artifacts, &bytes, None)
339            .unwrap_err()
340            .to_string();
341        assert!(not_a_share.contains("is not a share"), "{not_a_share}");
342    }
343
344    #[test]
345    fn a_share_property_is_digits_from_one_with_no_sign_and_no_leading_zero() {
346        assert_eq!(share_index("share_1"), Some(1));
347        assert_eq!(share_index("share_255"), Some(255));
348        assert_eq!(share_index("share_0"), None);
349        assert_eq!(share_index("share_01"), None);
350        assert_eq!(share_index("share_+1"), None);
351        assert_eq!(share_index("share_256"), None);
352        assert_eq!(share_index("share_"), None);
353        assert_eq!(share_index("share1"), None);
354        assert_eq!(share_index("shares_1"), None);
355    }
356
357    #[test]
358    fn resolving_an_unknown_artifact_fails() {
359        let artifacts: HashMap<ArtifactId, ArtifactValue> = HashMap::new();
360        let result = resolve_artifact_bytes(&artifacts, &ArtifactId::new("missing"), None);
361        assert!(result.is_err());
362    }
363}