1use 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}