Skip to main content

runx_runtime/receipts/
signing.rs

1// rust-style-allow: large-file because signer material parsing, production
2// validation, and verifier behavior are audited as one receipt boundary.
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use base64::Engine;
7use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
8use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
9use runx_contracts::{
10    ReceiptIssuer, ReceiptIssuerType, ReceiptSignature, SignatureAlgorithm, sha256_prefixed,
11};
12use runx_receipts::{SignatureVerificationFailure, SignatureVerifier};
13use thiserror::Error;
14
15use super::seal::RuntimeReceiptSignaturePolicy;
16
17pub const RECEIPT_SIGNATURE_BASE64_PREFIX: &str = "base64:";
18pub const RUNX_RECEIPT_SIGN_KID_ENV: &str = "RUNX_RECEIPT_SIGN_KID";
19pub const RUNX_RECEIPT_SIGN_ED25519_SEED_BASE64_ENV: &str = "RUNX_RECEIPT_SIGN_ED25519_SEED_BASE64";
20pub const RUNX_RECEIPT_SIGN_ISSUER_TYPE_ENV: &str = "RUNX_RECEIPT_SIGN_ISSUER_TYPE";
21
22pub(crate) fn is_receipt_signing_env_name(name: &str) -> bool {
23    name.starts_with("RUNX_RECEIPT_SIGN_")
24}
25
26pub(crate) fn strip_receipt_signing_env(env: &mut BTreeMap<String, String>) {
27    env.retain(|name, _| !is_receipt_signing_env_name(name));
28}
29
30pub trait RuntimeReceiptSigner {
31    fn issuer(&self) -> ReceiptIssuer;
32    fn sign_receipt_body(
33        &self,
34        body_digest: &str,
35    ) -> Result<ReceiptSignature, RuntimeReceiptSigningError>;
36}
37
38#[derive(Clone, Debug, Error, PartialEq, Eq)]
39pub enum RuntimeReceiptSigningError {
40    #[error("production receipt signing requires a signer")]
41    MissingSigner,
42    #[error("production receipt signing requires a verifier")]
43    MissingVerifier,
44    #[error("production receipt signer key id is missing")]
45    MissingKeyId,
46    #[error("production receipt signer public key hash is missing")]
47    MissingPublicKeySha256,
48    #[error("production receipt signer public key hash is malformed")]
49    MalformedPublicKeySha256,
50    #[error("production receipt signer issuer type is missing")]
51    MissingIssuerType,
52    #[error("production receipt signer issuer type is unsupported")]
53    UnsupportedIssuerType,
54    #[error("production receipt signer returned an unsupported signature algorithm")]
55    UnsupportedAlgorithm,
56    #[error("production receipt signer returned a local pseudo signature")]
57    PseudoSignature,
58    #[error("production receipt signer key material is malformed")]
59    MalformedSignerKey,
60    #[error("production receipt verifier key material is malformed")]
61    MalformedVerifierKey,
62    #[error(
63        "production receipt signing requires {RUNX_RECEIPT_SIGN_KID_ENV}, {RUNX_RECEIPT_SIGN_ED25519_SEED_BASE64_ENV}, and {RUNX_RECEIPT_SIGN_ISSUER_TYPE_ENV} to be set together"
64    )]
65    IncompleteSigningEnv,
66    #[error(
67        "governed runtime receipt signing requires {RUNX_RECEIPT_SIGN_KID_ENV}, {RUNX_RECEIPT_SIGN_ED25519_SEED_BASE64_ENV}, and {RUNX_RECEIPT_SIGN_ISSUER_TYPE_ENV}"
68    )]
69    MissingSigningEnv,
70    #[error("production receipt signature did not verify: {0:?}")]
71    SignatureVerification(SignatureVerificationFailure),
72}
73
74#[derive(Clone, Default)]
75pub struct RuntimeReceiptSignatureConfig {
76    production: Option<Arc<ProductionReceiptSignatureMaterial>>,
77}
78
79struct ProductionReceiptSignatureMaterial {
80    signer: Ed25519ReceiptSigner,
81    verifier: Ed25519ReceiptVerifier,
82}
83
84impl std::fmt::Debug for RuntimeReceiptSignatureConfig {
85    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        formatter
87            .debug_struct("RuntimeReceiptSignatureConfig")
88            .field("production_configured", &self.production.is_some())
89            .finish()
90    }
91}
92
93impl RuntimeReceiptSignatureConfig {
94    #[must_use]
95    pub fn local_development() -> Self {
96        Self { production: None }
97    }
98
99    pub fn production_signing(
100        signer: Ed25519ReceiptSigner,
101        verifier: Ed25519ReceiptVerifier,
102    ) -> Self {
103        Self {
104            production: Some(Arc::new(ProductionReceiptSignatureMaterial {
105                signer,
106                verifier,
107            })),
108        }
109    }
110
111    pub fn from_env(env: &BTreeMap<String, String>) -> Result<Self, RuntimeReceiptSigningError> {
112        let kid = non_empty_env(env, RUNX_RECEIPT_SIGN_KID_ENV);
113        let seed = non_empty_env(env, RUNX_RECEIPT_SIGN_ED25519_SEED_BASE64_ENV);
114        let issuer_type = non_empty_env(env, RUNX_RECEIPT_SIGN_ISSUER_TYPE_ENV);
115        match (kid, seed, issuer_type) {
116            (None, None, None) => Err(RuntimeReceiptSigningError::MissingSigningEnv),
117            (Some(kid), Some(seed), Some(issuer_type)) => {
118                let issuer_type = parse_production_issuer_type(issuer_type)?;
119                let signer = Ed25519ReceiptSigner::from_seed_base64(kid, issuer_type, seed)?;
120                let verifier = Ed25519ReceiptVerifier::new([signer.production_key()]);
121                Ok(Self::production_signing(signer, verifier))
122            }
123            (Some(_), Some(_), None) => Err(RuntimeReceiptSigningError::MissingIssuerType),
124            _ => Err(RuntimeReceiptSigningError::IncompleteSigningEnv),
125        }
126    }
127
128    #[must_use]
129    pub fn signature_policy(&self) -> RuntimeReceiptSignaturePolicy<'_> {
130        match self.production.as_ref() {
131            Some(production) => RuntimeReceiptSignaturePolicy::production_signing(
132                &production.signer,
133                &production.verifier,
134            ),
135            None => RuntimeReceiptSignaturePolicy::local_development(),
136        }
137    }
138
139    #[must_use]
140    pub fn production_key_for_kid(&self, kid: &str) -> Option<ProductionReceiptKey> {
141        self.production
142            .as_ref()
143            .map(|production| production.signer.production_key())
144            .filter(|key| key.kid() == kid)
145    }
146}
147
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct ProductionReceiptKey {
150    kid: String,
151    public_key: Vec<u8>,
152}
153
154impl ProductionReceiptKey {
155    #[must_use]
156    pub fn new(kid: impl Into<String>, public_key: impl Into<Vec<u8>>) -> Self {
157        Self {
158            kid: kid.into(),
159            public_key: public_key.into(),
160        }
161    }
162
163    #[must_use]
164    pub fn kid(&self) -> &str {
165        &self.kid
166    }
167
168    #[must_use]
169    pub fn public_key(&self) -> &[u8] {
170        &self.public_key
171    }
172
173    #[must_use]
174    pub fn public_key_sha256(&self) -> String {
175        sha256_prefixed(&self.public_key)
176    }
177}
178
179pub struct Ed25519ReceiptSigner {
180    issuer: ReceiptIssuer,
181    key_pair: Ed25519KeyPair,
182}
183
184impl Ed25519ReceiptSigner {
185    pub fn from_seed(
186        kid: impl Into<String>,
187        issuer_type: ReceiptIssuerType,
188        seed: &[u8],
189    ) -> Result<Self, RuntimeReceiptSigningError> {
190        let key_pair = Ed25519KeyPair::from_seed_unchecked(seed)
191            .map_err(|_| RuntimeReceiptSigningError::MalformedSignerKey)?;
192        let kid = kid.into();
193        let issuer = ReceiptIssuer {
194            issuer_type,
195            kid: kid.into(),
196            public_key_sha256: sha256_prefixed(key_pair.public_key().as_ref()).into(),
197        };
198        validate_production_issuer(&issuer)?;
199        Ok(Self { issuer, key_pair })
200    }
201
202    pub fn from_seed_base64(
203        kid: impl Into<String>,
204        issuer_type: ReceiptIssuerType,
205        seed: &str,
206    ) -> Result<Self, RuntimeReceiptSigningError> {
207        let seed = decode_key_material(seed)
208            .map_err(|_| RuntimeReceiptSigningError::MalformedSignerKey)?;
209        Self::from_seed(kid, issuer_type, &seed)
210    }
211
212    #[must_use]
213    pub fn production_key(&self) -> ProductionReceiptKey {
214        ProductionReceiptKey::new(
215            self.issuer.kid.to_string(),
216            self.key_pair.public_key().as_ref().to_vec(),
217        )
218    }
219
220    #[must_use]
221    pub fn public_key(&self) -> &[u8] {
222        self.key_pair.public_key().as_ref()
223    }
224}
225
226impl RuntimeReceiptSigner for Ed25519ReceiptSigner {
227    fn issuer(&self) -> ReceiptIssuer {
228        self.issuer.clone()
229    }
230
231    fn sign_receipt_body(
232        &self,
233        body_digest: &str,
234    ) -> Result<ReceiptSignature, RuntimeReceiptSigningError> {
235        let signature = self.key_pair.sign(body_digest.as_bytes());
236        Ok(ReceiptSignature {
237            alg: SignatureAlgorithm::Ed25519,
238            value: format!(
239                "{RECEIPT_SIGNATURE_BASE64_PREFIX}{}",
240                URL_SAFE_NO_PAD.encode(signature.as_ref())
241            )
242            .into(),
243        })
244    }
245}
246
247#[derive(Clone, Debug, Default, PartialEq, Eq)]
248pub struct Ed25519ReceiptVerifier {
249    keys: Vec<ProductionReceiptKey>,
250}
251
252impl Ed25519ReceiptVerifier {
253    #[must_use]
254    pub fn new(keys: impl IntoIterator<Item = ProductionReceiptKey>) -> Self {
255        Self {
256            keys: keys.into_iter().collect(),
257        }
258    }
259
260    #[must_use]
261    pub fn from_public_key(kid: impl Into<String>, public_key: impl Into<Vec<u8>>) -> Self {
262        Self::new([ProductionReceiptKey::new(kid, public_key)])
263    }
264
265    pub fn from_public_key_base64(
266        kid: impl Into<String>,
267        public_key: &str,
268    ) -> Result<Self, RuntimeReceiptSigningError> {
269        let public_key = decode_key_material(public_key)
270            .map_err(|_| RuntimeReceiptSigningError::MalformedVerifierKey)?;
271        Ok(Self::from_public_key(kid, public_key))
272    }
273
274    #[must_use]
275    pub fn keys(&self) -> &[ProductionReceiptKey] {
276        &self.keys
277    }
278
279    fn resolve_key(
280        &self,
281        issuer: &ReceiptIssuer,
282    ) -> Result<&ProductionReceiptKey, SignatureVerificationFailure> {
283        if matches!(
284            issuer.issuer_type,
285            ReceiptIssuerType::Local | ReceiptIssuerType::Verifier
286        ) {
287            return Err(SignatureVerificationFailure::UnsupportedIssuer);
288        }
289        if issuer.kid.trim().is_empty() {
290            return Err(SignatureVerificationFailure::MissingKey);
291        }
292        self.keys
293            .iter()
294            .find(|key| key.kid == issuer.kid)
295            .ok_or(SignatureVerificationFailure::MissingKey)
296    }
297}
298
299impl SignatureVerifier for Ed25519ReceiptVerifier {
300    fn verify(
301        &self,
302        issuer: &ReceiptIssuer,
303        signature: &ReceiptSignature,
304        body_digest: &str,
305    ) -> Result<(), SignatureVerificationFailure> {
306        let key = self.resolve_key(issuer)?;
307        if key.public_key.len() != 32 {
308            return Err(SignatureVerificationFailure::MalformedKey);
309        }
310        if issuer.public_key_sha256 != key.public_key_sha256() {
311            return Err(SignatureVerificationFailure::KeyHashMismatch);
312        }
313        let signature_bytes = decode_signature_value(&signature.value)?;
314        if signature_bytes.len() != 64 {
315            return Err(SignatureVerificationFailure::MalformedSignature);
316        }
317        UnparsedPublicKey::new(&ED25519, &key.public_key)
318            .verify(body_digest.as_bytes(), &signature_bytes)
319            .map_err(|_| SignatureVerificationFailure::SignatureMismatch)
320    }
321}
322
323pub(crate) fn validate_production_issuer(
324    issuer: &ReceiptIssuer,
325) -> Result<(), RuntimeReceiptSigningError> {
326    if matches!(
327        issuer.issuer_type,
328        ReceiptIssuerType::Local | ReceiptIssuerType::Verifier
329    ) {
330        return Err(RuntimeReceiptSigningError::UnsupportedIssuerType);
331    }
332    if issuer.kid.trim().is_empty() {
333        return Err(RuntimeReceiptSigningError::MissingKeyId);
334    }
335    if issuer.public_key_sha256.trim().is_empty() {
336        return Err(RuntimeReceiptSigningError::MissingPublicKeySha256);
337    }
338    if !is_well_formed_sha256(&issuer.public_key_sha256) {
339        return Err(RuntimeReceiptSigningError::MalformedPublicKeySha256);
340    }
341    Ok(())
342}
343
344fn parse_production_issuer_type(
345    value: &str,
346) -> Result<ReceiptIssuerType, RuntimeReceiptSigningError> {
347    match value {
348        "hosted" => Ok(ReceiptIssuerType::Hosted),
349        "ci" => Ok(ReceiptIssuerType::Ci),
350        "local" | "verifier" => Err(RuntimeReceiptSigningError::UnsupportedIssuerType),
351        _ => Err(RuntimeReceiptSigningError::UnsupportedIssuerType),
352    }
353}
354
355pub(crate) fn is_local_pseudo_signature(value: &str) -> bool {
356    value.starts_with("sig:")
357}
358
359fn decode_signature_value(value: &str) -> Result<Vec<u8>, SignatureVerificationFailure> {
360    let Some(encoded) = value.strip_prefix(RECEIPT_SIGNATURE_BASE64_PREFIX) else {
361        return Err(SignatureVerificationFailure::MalformedSignature);
362    };
363    URL_SAFE_NO_PAD
364        .decode(encoded)
365        .or_else(|_| STANDARD.decode(encoded))
366        .map_err(|_| SignatureVerificationFailure::MalformedSignature)
367}
368
369fn decode_key_material(value: &str) -> Result<Vec<u8>, ()> {
370    URL_SAFE_NO_PAD
371        .decode(value)
372        .or_else(|_| STANDARD.decode(value))
373        .map_err(|_| ())
374}
375
376fn non_empty_env<'a>(env: &'a BTreeMap<String, String>, key: &str) -> Option<&'a str> {
377    env.get(key)
378        .map(String::as_str)
379        .map(str::trim)
380        .filter(|value| !value.is_empty())
381}
382
383fn is_well_formed_sha256(value: &str) -> bool {
384    let Some(hex) = value.strip_prefix("sha256:") else {
385        return false;
386    };
387    hex.len() == 64 && hex.bytes().all(|byte| byte.is_ascii_hexdigit())
388}