1use anyhow::{anyhow, Context, Result};
4use base64::engine::general_purpose::{STANDARD as BASE64, URL_SAFE_NO_PAD as BASE64_URL_SAFE};
5use base64::Engine;
6use chrono::{DateTime, Utc};
7use ed25519_dalek::{
8 Signature as Ed25519Signature, Signer as _, SigningKey as Ed25519SigningKey,
9 VerifyingKey as Ed25519VerifyingKey,
10};
11use fleetforge_common::licensing::{ensure_feature_allowed, LicensedFeature};
12use hmac::{Hmac, Mac};
13#[cfg(any(
14 feature = "kms-cli-aws",
15 feature = "kms-cli-gcp",
16 feature = "kms-cli-azure"
17))]
18use once_cell::sync::OnceCell;
19use p256::ecdsa::{Signature as P256Signature, VerifyingKey as P256VerifyingKey};
20use p384::ecdsa::{Signature as P384Signature, VerifyingKey as P384VerifyingKey};
21use p521::ecdsa::{Signature as P521Signature, VerifyingKey as P521VerifyingKey};
22use process::{ProcessError, ProcessRunner};
23#[cfg(any(feature = "jwk-aws", feature = "jwk-gcp"))]
24use rsa::pkcs1::DecodeRsaPublicKey;
25use rsa::pkcs1v15;
26use rsa::pkcs8::{AssociatedOid, DecodePublicKey};
27use rsa::pss;
28use rsa::traits::PublicKeyParts;
29use rsa::{BigUint, RsaPublicKey};
30use serde::{Deserialize, Serialize};
31use serde_json::{Map, Value};
32use sha2::{Digest, Sha256, Sha384, Sha512};
33use signature::{digest::FixedOutputReset, Verifier};
34#[cfg(any(
35 feature = "kms-cli-aws",
36 feature = "kms-cli-gcp",
37 feature = "jwk-gcp",
38 feature = "jwk-aws"
39))]
40use spki::{der::Decode, ObjectIdentifier, SubjectPublicKeyInfoOwned};
41use std::borrow::Cow;
42use std::collections::BTreeMap;
43#[cfg(any(feature = "kms-cli-gcp", feature = "kms-cli-aws"))]
44use std::io::Write;
45use std::sync::Arc;
46use std::{env, fs, path::Path};
47#[cfg(any(feature = "kms-cli-gcp", feature = "kms-cli-aws"))]
48use tempfile::NamedTempFile;
49use uuid::Uuid;
50use which::which;
51
52mod c2pa;
53mod capability;
54mod process;
55mod scitt;
56mod vault;
57mod vault_object_store;
58mod serde_helpers {
59 pub mod base64_bytes {
60 use base64::engine::general_purpose::STANDARD as BASE64;
61 use base64::Engine;
62 use serde::{Deserialize, Deserializer, Serializer};
63
64 pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
65 where
66 S: Serializer,
67 {
68 serializer.serialize_str(&BASE64.encode(bytes))
69 }
70
71 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
72 where
73 D: Deserializer<'de>,
74 {
75 let encoded = String::deserialize(deserializer)?;
76 BASE64
77 .decode(encoded.as_bytes())
78 .map_err(serde::de::Error::custom)
79 }
80 }
81}
82
83pub fn normalize_ecdsa_signature(
84 signature: &[u8],
85 algorithm: &SigningAlgorithm,
86) -> Result<Vec<u8>> {
87 if !algorithm.is_ecdsa() {
88 return Ok(signature.to_vec());
89 }
90 if signature.first().copied() == Some(0x30) {
91 return Ok(signature.to_vec());
92 }
93 if signature.len() % 2 != 0 {
94 return Err(anyhow!(
95 "invalid ECDSA signature length {}; expected even number of bytes",
96 signature.len()
97 ));
98 }
99 let half = signature.len() / 2;
100 raw_ecdsa_to_der(&signature[..half], &signature[half..])
101}
102
103fn raw_ecdsa_to_der(r: &[u8], s: &[u8]) -> Result<Vec<u8>> {
104 fn encode_integer(bytes: &[u8]) -> Vec<u8> {
105 let mut value = bytes
106 .iter()
107 .skip_while(|b| **b == 0)
108 .copied()
109 .collect::<Vec<_>>();
110 if value.is_empty() {
111 value.push(0);
112 }
113 if value[0] & 0x80 != 0 {
114 value.insert(0, 0);
115 }
116 let mut encoded = vec![0x02, value.len() as u8];
117 encoded.extend(value);
118 encoded
119 }
120
121 let mut r_enc = encode_integer(r);
122 let mut s_enc = encode_integer(s);
123 let total_len = (r_enc.len() + s_enc.len()) as u8;
124 let mut der = vec![0x30, total_len];
125 der.append(&mut r_enc);
126 der.append(&mut s_enc);
127 Ok(der)
128}
129
130pub fn digest_for_algorithm(algorithm: &SigningAlgorithm, payload: &[u8]) -> Result<Vec<u8>> {
131 let digest = match algorithm.as_str() {
132 "ES256" | "PS256" | "RS256" => Sha256::digest(payload).to_vec(),
133 "ES384" | "PS384" | "RS384" => Sha384::digest(payload).to_vec(),
134 "ES512" | "PS512" | "RS512" => Sha512::digest(payload).to_vec(),
135 other => {
136 return Err(anyhow!(
137 "algorithm '{}' is not supported by digest_for_algorithm",
138 other
139 ))
140 }
141 };
142 Ok(digest)
143}
144
145fn decode_base64_url(value: &str) -> Result<Vec<u8>> {
146 BASE64_URL_SAFE
147 .decode(value.as_bytes())
148 .map_err(|err| anyhow!("failed to decode base64url field: {err}"))
149}
150
151fn jwk_str<'a>(jwk: &'a Jwk, field: &str) -> Result<&'a str> {
152 jwk.fields
153 .get(field)
154 .and_then(Value::as_str)
155 .ok_or_else(|| anyhow!("JWK missing '{}'", field))
156}
157
158fn pad_coordinate(bytes: &[u8], len: usize) -> Result<Vec<u8>> {
159 if bytes.len() > len {
160 return Err(anyhow!(
161 "coordinate length {} exceeds expected {} bytes",
162 bytes.len(),
163 len
164 ));
165 }
166 if bytes.len() == len {
167 return Ok(bytes.to_vec());
168 }
169 let mut padded = vec![0u8; len];
170 padded[len - bytes.len()..].copy_from_slice(bytes);
171 Ok(padded)
172}
173
174fn uncompressed_point(x: &[u8], y: &[u8]) -> Vec<u8> {
175 let mut encoded = Vec::with_capacity(1 + x.len() + y.len());
176 encoded.push(0x04);
177 encoded.extend_from_slice(x);
178 encoded.extend_from_slice(y);
179 encoded
180}
181
182fn try_verify_with_jwk(
183 algorithm: &SigningAlgorithm,
184 jwk: &Jwk,
185 payload: &[u8],
186 signature: &[u8],
187) -> Result<Option<bool>> {
188 let kty = jwk_str(jwk, "kty")?;
189 match kty {
190 "OKP" => verify_okp_jwk(algorithm, jwk, payload, signature),
191 "EC" => verify_ec_jwk(algorithm, jwk, payload, signature),
192 "RSA" => verify_rsa_jwk(algorithm, jwk, payload, signature),
193 _ => Ok(None),
194 }
195}
196
197fn verify_okp_jwk(
198 algorithm: &SigningAlgorithm,
199 jwk: &Jwk,
200 payload: &[u8],
201 signature: &[u8],
202) -> Result<Option<bool>> {
203 if !algorithm.is_edwards() {
204 return Ok(None);
205 }
206 let crv = jwk_str(jwk, "crv")?;
207 if crv != "Ed25519" {
208 return Err(anyhow!(
209 "unsupported OKP curve '{}' for algorithm '{}'",
210 crv,
211 algorithm.as_str()
212 ));
213 }
214 let x = decode_base64_url(jwk_str(jwk, "x")?)?;
215 if x.len() != 32 {
216 return Err(anyhow!(
217 "Ed25519 public key must be 32 bytes; got {} bytes",
218 x.len()
219 ));
220 }
221 if signature.len() != 64 {
222 return Ok(Some(false));
223 }
224 let key_bytes: [u8; 32] = x
225 .as_slice()
226 .try_into()
227 .map_err(|_| anyhow!("invalid Ed25519 public key length"))?;
228 let verifying_key = Ed25519VerifyingKey::from_bytes(&key_bytes)
229 .map_err(|err| anyhow!("invalid Ed25519 public key: {err}"))?;
230 let mut sig_bytes = [0u8; 64];
231 sig_bytes.copy_from_slice(signature);
232 let sig = Ed25519Signature::from_bytes(&sig_bytes);
233 Ok(Some(verifying_key.verify(payload, &sig).is_ok()))
234}
235
236fn verify_ec_jwk(
237 algorithm: &SigningAlgorithm,
238 jwk: &Jwk,
239 payload: &[u8],
240 signature: &[u8],
241) -> Result<Option<bool>> {
242 if !algorithm.is_ecdsa() {
243 return Ok(None);
244 }
245 let crv = jwk_str(jwk, "crv")?;
246 let x = decode_base64_url(jwk_str(jwk, "x")?)?;
247 let y = decode_base64_url(jwk_str(jwk, "y")?)?;
248 match (crv, algorithm.as_str()) {
249 ("P-256", "ES256") => {
250 let x = pad_coordinate(&x, 32)?;
251 let y = pad_coordinate(&y, 32)?;
252 let encoded = uncompressed_point(&x, &y);
253 let verifying_key = P256VerifyingKey::from_sec1_bytes(&encoded)
254 .map_err(|err| anyhow!("invalid P-256 public key: {err}"))?;
255 let sig = P256Signature::from_der(signature)
256 .map_err(|_| anyhow!("invalid P-256 ECDSA signature encoding"))?;
257 Ok(Some(verifying_key.verify(payload, &sig).is_ok()))
258 }
259 ("P-384", "ES384") => {
260 let x = pad_coordinate(&x, 48)?;
261 let y = pad_coordinate(&y, 48)?;
262 let encoded = uncompressed_point(&x, &y);
263 let verifying_key = P384VerifyingKey::from_sec1_bytes(&encoded)
264 .map_err(|err| anyhow!("invalid P-384 public key: {err}"))?;
265 let sig = P384Signature::from_der(signature)
266 .map_err(|_| anyhow!("invalid P-384 ECDSA signature encoding"))?;
267 Ok(Some(verifying_key.verify(payload, &sig).is_ok()))
268 }
269 ("P-521", "ES512") => {
270 let x = pad_coordinate(&x, 66)?;
271 let y = pad_coordinate(&y, 66)?;
272 let encoded = uncompressed_point(&x, &y);
273 let verifying_key = P521VerifyingKey::from_sec1_bytes(&encoded)
274 .map_err(|err| anyhow!("invalid P-521 public key: {err}"))?;
275 let sig = P521Signature::from_der(signature)
276 .map_err(|_| anyhow!("invalid P-521 ECDSA signature encoding"))?;
277 Ok(Some(verifying_key.verify(payload, &sig).is_ok()))
278 }
279 _ => Err(anyhow!(
280 "EC curve '{}' is not compatible with algorithm '{}'",
281 crv,
282 algorithm.as_str()
283 )),
284 }
285}
286
287fn verify_rsa_jwk(
288 algorithm: &SigningAlgorithm,
289 jwk: &Jwk,
290 payload: &[u8],
291 signature: &[u8],
292) -> Result<Option<bool>> {
293 if !algorithm.is_rsa() {
294 return Ok(None);
295 }
296 let n = decode_base64_url(jwk_str(jwk, "n")?)?;
297 let e = decode_base64_url(jwk_str(jwk, "e")?)?;
298 let modulus = BigUint::from_bytes_be(&n);
299 let exponent = BigUint::from_bytes_be(&e);
300 let public_key = RsaPublicKey::new(modulus, exponent).context("invalid RSA key components")?;
301 let result = match algorithm.as_str() {
302 "RS256" => verify_rsa_pkcs1::<Sha256>(&public_key, payload, signature)?,
303 "RS384" => verify_rsa_pkcs1::<Sha384>(&public_key, payload, signature)?,
304 "RS512" => verify_rsa_pkcs1::<Sha512>(&public_key, payload, signature)?,
305 "PS256" => verify_rsa_pss::<Sha256>(&public_key, payload, signature)?,
306 "PS384" => verify_rsa_pss::<Sha384>(&public_key, payload, signature)?,
307 "PS512" => verify_rsa_pss::<Sha512>(&public_key, payload, signature)?,
308 other => return Err(anyhow!("unsupported RSA algorithm '{}'", other)),
309 };
310 Ok(Some(result))
311}
312
313fn verify_rsa_pkcs1<D>(public_key: &RsaPublicKey, payload: &[u8], signature: &[u8]) -> Result<bool>
314where
315 D: Digest + Default + Clone + AssociatedOid,
316{
317 let verifier = pkcs1v15::VerifyingKey::<D>::new(public_key.clone());
318 let sig = pkcs1v15::Signature::try_from(signature)
319 .map_err(|_| anyhow!("invalid RSA signature length"))?;
320 Ok(verifier.verify(payload, &sig).is_ok())
321}
322
323fn verify_rsa_pss<D>(public_key: &RsaPublicKey, payload: &[u8], signature: &[u8]) -> Result<bool>
324where
325 D: Digest + Default + Clone + FixedOutputReset,
326{
327 let verifier = pss::VerifyingKey::<D>::new(public_key.clone());
328 let sig = pss::Signature::try_from(signature)
329 .map_err(|_| anyhow!("invalid RSA-PSS signature length"))?;
330 Ok(verifier.verify(payload, &sig).is_ok())
331}
332
333fn verify_rsa_pkcs1v15_sha256(spki_der: &[u8], payload: &[u8], signature: &[u8]) -> Result<bool> {
334 let verifier = pkcs1v15::VerifyingKey::<Sha256>::from_public_key_der(spki_der)
335 .context("invalid RSA SPKI")?;
336 let sig = pkcs1v15::Signature::try_from(signature)
337 .map_err(|_| anyhow!("invalid RSA signature length"))?;
338 Ok(verifier.verify(payload, &sig).is_ok())
339}
340
341fn build_oct_jwk(key: &[u8]) -> Result<(Jwk, String)> {
342 let encoded = BASE64_URL_SAFE.encode(key);
343 let mut fields = Map::new();
344 fields.insert("kty".into(), Value::String("oct".into()));
345 fields.insert("k".into(), Value::String(encoded));
346 finalize_jwk(fields)
347}
348
349fn build_ed25519_jwk(public_key: &[u8]) -> Result<(Jwk, String)> {
350 let encoded = BASE64_URL_SAFE.encode(public_key);
351 let mut fields = Map::new();
352 fields.insert("kty".into(), Value::String("OKP".into()));
353 fields.insert("crv".into(), Value::String("Ed25519".into()));
354 fields.insert("x".into(), Value::String(encoded));
355 finalize_jwk(fields)
356}
357
358fn finalize_jwk(mut fields: Map<String, Value>) -> Result<(Jwk, String)> {
359 let jwk = Jwk {
360 fields: fields.clone(),
361 };
362 let kid = jwk_thumbprint(&jwk)?;
363 fields.insert("kid".into(), Value::String(kid.clone()));
364 Ok((Jwk { fields }, kid))
365}
366
367fn jwk_thumbprint(jwk: &Jwk) -> Result<String> {
368 let kty = jwk
369 .fields
370 .get("kty")
371 .and_then(Value::as_str)
372 .ok_or_else(|| anyhow!("JWK missing 'kty'"))?;
373 let canonical = match kty {
374 "oct" => {
375 let k = jwk
376 .fields
377 .get("k")
378 .and_then(Value::as_str)
379 .ok_or_else(|| anyhow!("octet JWK missing 'k'"))?;
380 canonical_thumbprint(&[("kty", "oct"), ("k", k)])
381 }
382 "OKP" => {
383 let crv = jwk
384 .fields
385 .get("crv")
386 .and_then(Value::as_str)
387 .ok_or_else(|| anyhow!("OKP JWK missing 'crv'"))?;
388 let x = jwk
389 .fields
390 .get("x")
391 .and_then(Value::as_str)
392 .ok_or_else(|| anyhow!("OKP JWK missing 'x'"))?;
393 canonical_thumbprint(&[("crv", crv), ("kty", "OKP"), ("x", x)])
394 }
395 other => {
396 return Err(anyhow!(
397 "unsupported JWK kty '{}' for thumbprint computation",
398 other
399 ))
400 }
401 }?;
402 let digest = Sha256::digest(canonical.as_bytes());
403 Ok(BASE64_URL_SAFE.encode(digest))
404}
405
406fn canonical_thumbprint(fields: &[(&str, &str)]) -> Result<String> {
407 let mut map = BTreeMap::new();
408 for (key, value) in fields {
409 map.insert(key.to_string(), Value::String(value.to_string()));
410 }
411 serde_json::to_string(&map).map_err(|err| anyhow!("failed to serialise thumbprint map: {err}"))
412}
413
414fn base64_url(bytes: &[u8]) -> String {
415 BASE64_URL_SAFE.encode(bytes)
416}
417
418fn trim_leading_zeroes(bytes: &[u8]) -> &[u8] {
419 let mut idx = 0;
420 while idx < bytes.len() && bytes[idx] == 0 {
421 idx += 1;
422 }
423 &bytes[idx..]
424}
425
426fn decode_pem(pem: &str) -> Result<Vec<u8>> {
427 let body: String = pem
428 .lines()
429 .filter_map(|line| {
430 let trimmed = line.trim();
431 if trimmed.starts_with("-----") || trimmed.is_empty() {
432 None
433 } else {
434 Some(trimmed)
435 }
436 })
437 .collect();
438 BASE64
439 .decode(body.as_bytes())
440 .map_err(|err| anyhow!("failed to decode PEM body: {err}"))
441}
442
443#[cfg(feature = "jwk-gcp")]
444pub fn jwk_from_gcp_public_key(
445 pem: &str,
446 kid: &str,
447 algorithm: &SigningAlgorithm,
448 hint: &str,
449) -> Result<Jwk> {
450 let der = decode_pem(pem)?;
451 let spki =
452 SubjectPublicKeyInfoOwned::from_der(&der).context("failed to parse GCP public key")?;
453 if algorithm.is_edwards() {
454 return jwk_from_ed25519_spki(&spki, kid);
455 }
456 if algorithm.is_ecdsa() {
457 let (curve, coord_len) = match algorithm.as_str() {
458 "ES256" => ("P-256", 32),
459 "ES384" => ("P-384", 48),
460 "ES512" => ("P-521", 66),
461 other => {
462 return Err(anyhow!(
463 "unsupported ECDSA algorithm '{}' for automatic public key parsing; {}",
464 other,
465 hint
466 ))
467 }
468 };
469 return jwk_from_ec_spki(&spki, curve, coord_len, kid);
470 }
471 if algorithm.is_rsa() {
472 return jwk_from_rsa_spki(&spki, kid);
473 }
474 Err(anyhow!(
475 "automatic public key parsing is not available for algorithm '{}'; {}",
476 algorithm.as_str(),
477 hint
478 ))
479}
480
481#[cfg(any(feature = "jwk-aws", feature = "jwk-gcp"))]
482fn jwk_from_ed25519_spki(spki: &SubjectPublicKeyInfoOwned, kid: &str) -> Result<Jwk> {
483 let ed_oid = ObjectIdentifier::new("1.3.101.112")
484 .map_err(|err| anyhow!("failed to construct Ed25519 OID: {err}"))?;
485 if spki.algorithm.oid != ed_oid {
486 return Err(anyhow!("public key is not Ed25519"));
487 }
488 let pk = spki.subject_public_key.raw_bytes();
489 if pk.len() != 32 {
490 return Err(anyhow!(
491 "Ed25519 public key must be 32 bytes; got {}",
492 pk.len()
493 ));
494 }
495 let (jwk, _) = build_ed25519_jwk(pk)?;
496 Ok(attach_kid(jwk, kid))
497}
498
499type HmacSha256 = Hmac<Sha256>;
500type HmacSha384 = Hmac<Sha384>;
501type HmacSha512 = Hmac<Sha512>;
502
503const TRUST_SIGNING_KEY_ENV: &str = "FLEETFORGE_TRUST_SIGNING_KEY";
504const TRUST_SIGNING_KEY_PATH_ENV: &str = "FLEETFORGE_TRUST_SIGNING_KEY_PATH";
505const SCITT_SIGNING_KEY_ENV: &str = "FLEETFORGE_SCITT_KEY";
506const SCITT_SIGNING_KEY_PATH_ENV: &str = "FLEETFORGE_SCITT_KEY_PATH";
507const CAPABILITY_SIGNING_KEY_ENV: &str = "FLEETFORGE_CAPABILITY_KEY";
508const CAPABILITY_SIGNING_KEY_PATH_ENV: &str = "FLEETFORGE_CAPABILITY_KEY_PATH";
509const C2PA_SIGNING_KEY_ENV: &str = "FLEETFORGE_C2PA_KEY";
510const C2PA_SIGNING_KEY_PATH_ENV: &str = "FLEETFORGE_C2PA_KEY_PATH";
511const TRUST_SIGNER_BACKEND_ENV: &str = "FLEETFORGE_TRUST_SIGNER_BACKEND";
512const TRUST_SIGNER_ALGORITHM_ENV: &str = "FLEETFORGE_TRUST_SIGNER_ALGORITHM";
513const TRUST_SIGNER_PUBLIC_KEY_ENV: &str = "FLEETFORGE_TRUST_SIGNER_PUBLIC_KEY";
514const TRUST_SIGNER_PUBLIC_KEY_PATH_ENV: &str = "FLEETFORGE_TRUST_SIGNER_PUBLIC_KEY_PATH";
515const TRUST_SIGNER_AWS_KEY_ENV: &str = "FLEETFORGE_TRUST_SIGNER_AWS_KEY_ID";
516const TRUST_SIGNER_AWS_PROFILE_ENV: &str = "FLEETFORGE_TRUST_SIGNER_AWS_PROFILE";
517const TRUST_SIGNER_AWS_REGION_ENV: &str = "FLEETFORGE_TRUST_SIGNER_AWS_REGION";
518const TRUST_SIGNER_GCP_KEY_ENV: &str = "FLEETFORGE_TRUST_SIGNER_GCP_KEY_VERSION";
519const TRUST_SIGNER_GCP_DIGEST_ENV: &str = "FLEETFORGE_TRUST_SIGNER_GCP_DIGEST";
520const TRUST_SIGNER_AZURE_KEY_ENV: &str = "FLEETFORGE_TRUST_SIGNER_AZURE_KEY_ID";
521const TRUST_SIGNER_CLI_BIN_ENV: &str = "FLEETFORGE_TRUST_SIGNER_CLI";
522const SCITT_SIGNER_BACKEND_ENV: &str = "FLEETFORGE_SCITT_SIGNER_BACKEND";
523const SCITT_SIGNER_ALGORITHM_ENV: &str = "FLEETFORGE_SCITT_SIGNER_ALGORITHM";
524const SCITT_SIGNER_PUBLIC_KEY_ENV: &str = "FLEETFORGE_SCITT_SIGNER_PUBLIC_KEY";
525const SCITT_SIGNER_PUBLIC_KEY_PATH_ENV: &str = "FLEETFORGE_SCITT_SIGNER_PUBLIC_KEY_PATH";
526const SCITT_SIGNER_AWS_KEY_ENV: &str = "FLEETFORGE_SCITT_SIGNER_AWS_KEY_ID";
527const SCITT_SIGNER_AWS_PROFILE_ENV: &str = "FLEETFORGE_SCITT_SIGNER_AWS_PROFILE";
528const SCITT_SIGNER_AWS_REGION_ENV: &str = "FLEETFORGE_SCITT_SIGNER_AWS_REGION";
529const SCITT_SIGNER_GCP_KEY_ENV: &str = "FLEETFORGE_SCITT_SIGNER_GCP_KEY_VERSION";
530const SCITT_SIGNER_GCP_DIGEST_ENV: &str = "FLEETFORGE_SCITT_SIGNER_GCP_DIGEST";
531const SCITT_SIGNER_AZURE_KEY_ENV: &str = "FLEETFORGE_SCITT_SIGNER_AZURE_KEY_ID";
532const SCITT_SIGNER_CLI_BIN_ENV: &str = "FLEETFORGE_SCITT_SIGNER_CLI";
533const CAPABILITY_SIGNER_BACKEND_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_BACKEND";
534const CAPABILITY_SIGNER_ALGORITHM_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_ALGORITHM";
535const CAPABILITY_SIGNER_PUBLIC_KEY_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_PUBLIC_KEY";
536const CAPABILITY_SIGNER_PUBLIC_KEY_PATH_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_PUBLIC_KEY_PATH";
537const CAPABILITY_SIGNER_AWS_KEY_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_AWS_KEY_ID";
538const CAPABILITY_SIGNER_AWS_PROFILE_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_AWS_PROFILE";
539const CAPABILITY_SIGNER_AWS_REGION_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_AWS_REGION";
540const CAPABILITY_SIGNER_GCP_KEY_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_GCP_KEY_VERSION";
541const CAPABILITY_SIGNER_GCP_DIGEST_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_GCP_DIGEST";
542const CAPABILITY_SIGNER_AZURE_KEY_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_AZURE_KEY_ID";
543const CAPABILITY_SIGNER_CLI_BIN_ENV: &str = "FLEETFORGE_CAPABILITY_SIGNER_CLI";
544const C2PA_SIGNER_BACKEND_ENV: &str = "FLEETFORGE_C2PA_SIGNER_BACKEND";
545const C2PA_SIGNER_ALGORITHM_ENV: &str = "FLEETFORGE_C2PA_SIGNER_ALGORITHM";
546const C2PA_SIGNER_PUBLIC_KEY_ENV: &str = "FLEETFORGE_C2PA_SIGNER_PUBLIC_KEY";
547const C2PA_SIGNER_PUBLIC_KEY_PATH_ENV: &str = "FLEETFORGE_C2PA_SIGNER_PUBLIC_KEY_PATH";
548const C2PA_SIGNER_AWS_KEY_ENV: &str = "FLEETFORGE_C2PA_SIGNER_AWS_KEY_ID";
549const C2PA_SIGNER_AWS_PROFILE_ENV: &str = "FLEETFORGE_C2PA_SIGNER_AWS_PROFILE";
550const C2PA_SIGNER_AWS_REGION_ENV: &str = "FLEETFORGE_C2PA_SIGNER_AWS_REGION";
551const C2PA_SIGNER_GCP_KEY_ENV: &str = "FLEETFORGE_C2PA_SIGNER_GCP_KEY_VERSION";
552const C2PA_SIGNER_GCP_DIGEST_ENV: &str = "FLEETFORGE_C2PA_SIGNER_GCP_DIGEST";
553const C2PA_SIGNER_AZURE_KEY_ENV: &str = "FLEETFORGE_C2PA_SIGNER_AZURE_KEY_ID";
554const C2PA_SIGNER_CLI_BIN_ENV: &str = "FLEETFORGE_C2PA_SIGNER_CLI";
555
556struct SignerSlot {
557 label: &'static str,
558 backend_env: &'static str,
559 algorithm_env: &'static str,
560 key_env: &'static str,
561 key_path_env: &'static str,
562 public_key_env: &'static str,
563 public_key_path_env: &'static str,
564 aws_key_env: &'static str,
565 aws_profile_env: &'static str,
566 aws_region_env: &'static str,
567 gcp_key_env: &'static str,
568 gcp_digest_env: &'static str,
569 azure_key_env: &'static str,
570 cli_bin_env: &'static str,
571}
572
573const TRUST_SIGNER_SLOT: SignerSlot = SignerSlot {
574 label: "trust",
575 backend_env: TRUST_SIGNER_BACKEND_ENV,
576 algorithm_env: TRUST_SIGNER_ALGORITHM_ENV,
577 key_env: TRUST_SIGNING_KEY_ENV,
578 key_path_env: TRUST_SIGNING_KEY_PATH_ENV,
579 public_key_env: TRUST_SIGNER_PUBLIC_KEY_ENV,
580 public_key_path_env: TRUST_SIGNER_PUBLIC_KEY_PATH_ENV,
581 aws_key_env: TRUST_SIGNER_AWS_KEY_ENV,
582 aws_profile_env: TRUST_SIGNER_AWS_PROFILE_ENV,
583 aws_region_env: TRUST_SIGNER_AWS_REGION_ENV,
584 gcp_key_env: TRUST_SIGNER_GCP_KEY_ENV,
585 gcp_digest_env: TRUST_SIGNER_GCP_DIGEST_ENV,
586 azure_key_env: TRUST_SIGNER_AZURE_KEY_ENV,
587 cli_bin_env: TRUST_SIGNER_CLI_BIN_ENV,
588};
589
590const SCITT_SIGNER_SLOT: SignerSlot = SignerSlot {
591 label: "scitt",
592 backend_env: SCITT_SIGNER_BACKEND_ENV,
593 algorithm_env: SCITT_SIGNER_ALGORITHM_ENV,
594 key_env: SCITT_SIGNING_KEY_ENV,
595 key_path_env: SCITT_SIGNING_KEY_PATH_ENV,
596 public_key_env: SCITT_SIGNER_PUBLIC_KEY_ENV,
597 public_key_path_env: SCITT_SIGNER_PUBLIC_KEY_PATH_ENV,
598 aws_key_env: SCITT_SIGNER_AWS_KEY_ENV,
599 aws_profile_env: SCITT_SIGNER_AWS_PROFILE_ENV,
600 aws_region_env: SCITT_SIGNER_AWS_REGION_ENV,
601 gcp_key_env: SCITT_SIGNER_GCP_KEY_ENV,
602 gcp_digest_env: SCITT_SIGNER_GCP_DIGEST_ENV,
603 azure_key_env: SCITT_SIGNER_AZURE_KEY_ENV,
604 cli_bin_env: SCITT_SIGNER_CLI_BIN_ENV,
605};
606
607const CAPABILITY_SIGNER_SLOT: SignerSlot = SignerSlot {
608 label: "capability",
609 backend_env: CAPABILITY_SIGNER_BACKEND_ENV,
610 algorithm_env: CAPABILITY_SIGNER_ALGORITHM_ENV,
611 key_env: CAPABILITY_SIGNING_KEY_ENV,
612 key_path_env: CAPABILITY_SIGNING_KEY_PATH_ENV,
613 public_key_env: CAPABILITY_SIGNER_PUBLIC_KEY_ENV,
614 public_key_path_env: CAPABILITY_SIGNER_PUBLIC_KEY_PATH_ENV,
615 aws_key_env: CAPABILITY_SIGNER_AWS_KEY_ENV,
616 aws_profile_env: CAPABILITY_SIGNER_AWS_PROFILE_ENV,
617 aws_region_env: CAPABILITY_SIGNER_AWS_REGION_ENV,
618 gcp_key_env: CAPABILITY_SIGNER_GCP_KEY_ENV,
619 gcp_digest_env: CAPABILITY_SIGNER_GCP_DIGEST_ENV,
620 azure_key_env: CAPABILITY_SIGNER_AZURE_KEY_ENV,
621 cli_bin_env: CAPABILITY_SIGNER_CLI_BIN_ENV,
622};
623
624const C2PA_SIGNER_SLOT: SignerSlot = SignerSlot {
625 label: "c2pa",
626 backend_env: C2PA_SIGNER_BACKEND_ENV,
627 algorithm_env: C2PA_SIGNER_ALGORITHM_ENV,
628 key_env: C2PA_SIGNING_KEY_ENV,
629 key_path_env: C2PA_SIGNING_KEY_PATH_ENV,
630 public_key_env: C2PA_SIGNER_PUBLIC_KEY_ENV,
631 public_key_path_env: C2PA_SIGNER_PUBLIC_KEY_PATH_ENV,
632 aws_key_env: C2PA_SIGNER_AWS_KEY_ENV,
633 aws_profile_env: C2PA_SIGNER_AWS_PROFILE_ENV,
634 aws_region_env: C2PA_SIGNER_AWS_REGION_ENV,
635 gcp_key_env: C2PA_SIGNER_GCP_KEY_ENV,
636 gcp_digest_env: C2PA_SIGNER_GCP_DIGEST_ENV,
637 azure_key_env: C2PA_SIGNER_AZURE_KEY_ENV,
638 cli_bin_env: C2PA_SIGNER_CLI_BIN_ENV,
639};
640
641fn configured_algorithm(slot: &SignerSlot) -> Result<Option<SigningAlgorithm>> {
642 match env::var(slot.algorithm_env) {
643 Ok(value) => {
644 let trimmed = value.trim();
645 if trimmed.is_empty() {
646 Ok(None)
647 } else {
648 SigningAlgorithm::parse(trimmed).map(Some)
649 }
650 }
651 Err(_) => Ok(None),
652 }
653}
654
655fn resolve_signer(slot: &SignerSlot, strict: bool) -> Result<Arc<dyn Signer>> {
656 let backend_value = env::var(slot.backend_env).unwrap_or_else(|_| "env-hmac".to_string());
657 let backend = backend_value.trim().to_ascii_lowercase();
658 match backend.as_str() {
659 "" | "env" | "env-hmac" => resolve_hmac_signer(slot, strict),
660 "env-ed25519" | "env-edwards" => resolve_ed25519_signer(slot, strict),
661 "aws-kms-cli" => {
662 ensure_feature_allowed(LicensedFeature::ExternalKms)?;
663 #[cfg(feature = "kms-cli-aws")]
664 {
665 AwsKmsCliSigner::new(slot).map(|signer| Arc::new(signer) as Arc<dyn Signer>)
666 }
667 #[cfg(not(feature = "kms-cli-aws"))]
668 {
669 Err(anyhow!(
670 "{} signer backend 'aws-kms-cli' requires enabling the 'kms-cli-aws' feature",
671 slot.label
672 ))
673 }
674 }
675 "gcp-kms-cli" => {
676 ensure_feature_allowed(LicensedFeature::ExternalKms)?;
677 #[cfg(feature = "kms-cli-gcp")]
678 {
679 GcpKmsCliSigner::new(slot).map(|signer| Arc::new(signer) as Arc<dyn Signer>)
680 }
681 #[cfg(not(feature = "kms-cli-gcp"))]
682 {
683 Err(anyhow!(
684 "{} signer backend 'gcp-kms-cli' requires enabling the 'kms-cli-gcp' feature",
685 slot.label
686 ))
687 }
688 }
689 "azure-kv-cli" => {
690 ensure_feature_allowed(LicensedFeature::ExternalKms)?;
691 #[cfg(feature = "kms-cli-azure")]
692 {
693 AzureKvCliSigner::new(slot).map(|signer| Arc::new(signer) as Arc<dyn Signer>)
694 }
695 #[cfg(not(feature = "kms-cli-azure"))]
696 {
697 Err(anyhow!(
698 "{} signer backend 'azure-kv-cli' requires enabling the 'kms-cli-azure' feature",
699 slot.label
700 ))
701 }
702 }
703 other => Err(anyhow!(
704 "unsupported signer backend '{}' for {} signer",
705 other,
706 slot.label
707 )),
708 }
709}
710
711fn cli_path_exists(binary: &str) -> bool {
712 let path = Path::new(binary);
713 if path.is_absolute() || path.components().count() > 1 {
714 path.exists()
715 } else {
716 which(binary).is_ok()
717 }
718}
719
720const AWS_CLI_GUIDANCE: &str = "Install the AWS CLI (https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and run `aws configure` (or export AWS_PROFILE/AWS_REGION) before using this backend.";
721const GCP_CLI_GUIDANCE: &str = "Install the Google Cloud CLI (https://cloud.google.com/sdk/docs/install) and run `gcloud auth application-default login` so the signer can call Cloud KMS.";
722const AZURE_CLI_GUIDANCE: &str = "Install the Azure CLI (https://learn.microsoft.com/cli/azure/install-azure-cli) and run `az login` (or configure managed identity) before using this backend.";
723
724fn require_cli(binary: &str, context: &str, guidance: &str) -> Result<()> {
725 if cli_path_exists(binary) {
726 Ok(())
727 } else {
728 Err(anyhow!(
729 "{} requires the '{}' CLI but it was not found. {}",
730 context,
731 binary,
732 guidance
733 ))
734 }
735}
736
737fn map_cli_error(action: &str, binary: &str, guidance: &str, err: ProcessError) -> anyhow::Error {
738 match err {
739 ProcessError::Spawn { source, .. } => anyhow!(
740 "{} failed to start '{}': {}. {}",
741 action,
742 binary,
743 source,
744 guidance
745 ),
746 ProcessError::Timeout {
747 timeout,
748 stdout,
749 stderr,
750 ..
751 } => anyhow!(
752 "{} via '{}' timed out after {:?}. stderr: {} stdout: {}. {}",
753 action,
754 binary,
755 timeout,
756 summarize_cli_output(&stderr),
757 summarize_cli_output(&stdout),
758 guidance
759 ),
760 ProcessError::NonZeroExit {
761 code,
762 stdout,
763 stderr,
764 ..
765 } => anyhow!(
766 "{} via '{}' exited with status {:?}. stderr: {} stdout: {}. {}",
767 action,
768 binary,
769 code,
770 summarize_cli_output(&stderr),
771 summarize_cli_output(&stdout),
772 guidance
773 ),
774 ProcessError::Wait { source, .. } => anyhow!(
775 "{} via '{}' failed while waiting for completion: {}. {}",
776 action,
777 binary,
778 source,
779 guidance
780 ),
781 ProcessError::Output { source, .. } => anyhow!(
782 "{} via '{}' failed to collect CLI output: {}. {}",
783 action,
784 binary,
785 source,
786 guidance
787 ),
788 ProcessError::ReaderPanicked { .. } => anyhow!(
789 "{} via '{}' aborted while reading CLI output. {}",
790 action,
791 binary,
792 guidance
793 ),
794 ProcessError::Json { stdout, source, .. } => anyhow!(
795 "{} via '{}' returned invalid JSON: {} (stdout: {}). {}",
796 action,
797 binary,
798 source,
799 summarize_cli_output(&stdout),
800 guidance
801 ),
802 }
803}
804
805fn summarize_cli_output(output: &str) -> String {
806 let trimmed = output.trim();
807 if trimmed.is_empty() {
808 return "n/a".to_string();
809 }
810 let max_len = 240;
811 if trimmed.len() > max_len {
812 format!("{}…", &trimmed[..max_len])
813 } else {
814 trimmed.to_string()
815 }
816}
817
818fn resolve_hmac_signer(slot: &SignerSlot, strict: bool) -> Result<Arc<dyn Signer>> {
819 let algorithm = configured_algorithm(slot)?;
820 if let Some(ref alg) = algorithm {
821 if !alg.is_hmac() {
822 return Err(anyhow!(
823 "{} signer backend 'env-hmac' only supports HS256/HS384/HS512 algorithms",
824 slot.label
825 ));
826 }
827 }
828 match load_key_material(slot.key_path_env, slot.key_env, slot.label, strict)? {
829 Some((key, _)) => Ok(Arc::new(TrustSigner::from_secret_with_algorithm(
830 key,
831 algorithm.clone(),
832 )?)),
833 None => {
834 if strict {
835 Err(anyhow!(
836 "{} signer requires key material (set {} or {})",
837 slot.label,
838 slot.key_env,
839 slot.key_path_env
840 ))
841 } else {
842 Ok(Arc::new(TrustSigner::for_trust_with_algorithm(
843 algorithm.clone(),
844 )))
845 }
846 }
847 }
848}
849
850fn resolve_ed25519_signer(slot: &SignerSlot, strict: bool) -> Result<Arc<dyn Signer>> {
851 if let Some(alg) = configured_algorithm(slot)? {
852 if !alg.is_edwards() {
853 return Err(anyhow!(
854 "{} signer backend 'env-ed25519' only supports EdDSA",
855 slot.label
856 ));
857 }
858 }
859 match load_key_material(slot.key_path_env, slot.key_env, slot.label, strict)? {
860 Some((secret, _)) => {
861 let signer = Ed25519Signer::from_bytes(&secret)?;
862 Ok(Arc::new(signer))
863 }
864 None => Err(anyhow!(
865 "{} signer requires key material for Ed25519 (set {} or {})",
866 slot.label,
867 slot.key_env,
868 slot.key_path_env
869 )),
870 }
871}
872
873#[cfg(feature = "kms-cli-aws")]
874struct AwsKmsCliSigner {
875 key_id: String,
876 kid: String,
877 algorithm: SigningAlgorithm,
878 aws_algorithm: &'static str,
879 binary: String,
880 profile: Option<String>,
881 region: Option<String>,
882 public_key: Option<Jwk>,
883 fetched_public_key: OnceCell<Jwk>,
884}
885
886#[cfg(feature = "kms-cli-aws")]
887impl AwsKmsCliSigner {
888 fn new(slot: &SignerSlot) -> Result<Self> {
889 let algorithm =
890 configured_algorithm(slot)?.unwrap_or_else(|| SigningAlgorithm::new("PS256"));
891 let aws_algorithm = map_aws_algorithm(&algorithm)?;
892 let key_id = env::var(slot.aws_key_env).map_err(|_| {
893 anyhow!(
894 "{} signer backend 'aws-kms-cli' requires {} to be set",
895 slot.label,
896 slot.aws_key_env
897 )
898 })?;
899 let kid = format!("aws-kms:{}", key_id);
900 let binary = env::var(slot.cli_bin_env).unwrap_or_else(|_| "aws".into());
901 require_cli(
902 &binary,
903 &format!("{} signer backend 'aws-kms-cli'", slot.label),
904 AWS_CLI_GUIDANCE,
905 )?;
906 let profile = env::var(slot.aws_profile_env).ok();
907 let region = env::var(slot.aws_region_env).ok();
908 let public_key = load_jwk_from_env(slot.public_key_env, slot.public_key_path_env)?
909 .map(|jwk| attach_kid(jwk, &kid));
910 Ok(Self {
911 key_id,
912 kid,
913 algorithm,
914 aws_algorithm,
915 binary,
916 profile,
917 region,
918 public_key,
919 fetched_public_key: OnceCell::new(),
920 })
921 }
922
923 fn fetch_public_key(&self) -> Result<Jwk> {
924 #[derive(Deserialize)]
925 struct AwsPublicKeyResponse {
926 #[serde(rename = "PublicKey")]
927 public_key: String,
928 #[serde(rename = "KeySpec")]
929 key_spec: String,
930 }
931 let resp: AwsPublicKeyResponse = ProcessRunner::new(&self.binary)
932 .args([
933 "kms",
934 "get-public-key",
935 "--key-id",
936 &self.key_id,
937 "--output",
938 "json",
939 ])
940 .env_opt("AWS_PROFILE", self.profile.clone())
941 .env_opt("AWS_REGION", self.region.clone())
942 .run_json()
943 .map_err(|err| {
944 map_cli_error(
945 "aws kms get-public-key",
946 &self.binary,
947 AWS_CLI_GUIDANCE,
948 err,
949 )
950 })?;
951 let der = BASE64
952 .decode(resp.public_key.as_bytes())
953 .context("failed to decode AWS public key")?;
954 jwk_from_aws_public_key(&der, &resp.key_spec, &self.kid)
955 }
956}
957
958#[cfg(feature = "kms-cli-aws")]
959impl Signer for AwsKmsCliSigner {
960 fn algorithm(&self) -> SigningAlgorithm {
961 self.algorithm.clone()
962 }
963
964 fn key_id(&self) -> &str {
965 &self.kid
966 }
967
968 fn public_key_jwk(&self) -> Result<Jwk> {
969 if let Some(jwk) = &self.public_key {
970 return Ok(jwk.clone());
971 }
972 if let Some(jwk) = self.fetched_public_key.get() {
973 return Ok(jwk.clone());
974 }
975 let jwk = self.fetch_public_key()?;
976 let _ = self.fetched_public_key.set(jwk.clone());
977 Ok(jwk)
978 }
979
980 fn sign(&self, payload: &[u8]) -> Result<SignatureEnvelope> {
981 let mut input_file = NamedTempFile::new().context("failed to create temp file")?;
982 input_file
983 .write_all(payload)
984 .context("failed to write payload to temp file")?;
985 let message_arg = format!("fileb://{}", input_file.path().display());
986 let public_key = Some(self.public_key_jwk()?);
987 #[derive(Deserialize)]
988 struct AwsSignResponse {
989 #[serde(rename = "Signature")]
990 signature: String,
991 #[serde(rename = "KeyId")]
992 key_id: String,
993 }
994 let resp: AwsSignResponse = ProcessRunner::new(&self.binary)
995 .args([
996 "kms",
997 "sign",
998 "--key-id",
999 &self.key_id,
1000 "--message",
1001 &message_arg,
1002 "--message-type",
1003 "RAW",
1004 "--signing-algorithm",
1005 self.aws_algorithm,
1006 "--output",
1007 "json",
1008 ])
1009 .env_opt("AWS_PROFILE", self.profile.clone())
1010 .env_opt("AWS_REGION", self.region.clone())
1011 .run_json()
1012 .map_err(|err| map_cli_error("aws kms sign", &self.binary, AWS_CLI_GUIDANCE, err))?;
1013 let mut signature = BASE64
1014 .decode(resp.signature.as_bytes())
1015 .context("invalid signature output")?;
1016 signature = normalize_ecdsa_signature(&signature, &self.algorithm)?;
1017 Ok(SignatureEnvelope {
1018 algorithm: self.algorithm(),
1019 signature,
1020 key_id: format!("aws-kms:{}", resp.key_id),
1021 public_key,
1022 })
1023 }
1024
1025 fn verify_via_backend(&self, payload: &[u8], signature: &[u8]) -> Result<bool> {
1026 let mut input_file = NamedTempFile::new().context("failed to create temp file")?;
1027 input_file
1028 .write_all(payload)
1029 .context("failed to write payload to temp file")?;
1030 let message_arg = format!("fileb://{}", input_file.path().display());
1031 let signature_b64 = BASE64.encode(signature);
1032 #[derive(Deserialize)]
1033 struct AwsVerifyResponse {
1034 #[serde(rename = "SignatureValid")]
1035 signature_valid: bool,
1036 }
1037 let resp: AwsVerifyResponse = ProcessRunner::new(&self.binary)
1038 .args([
1039 "kms",
1040 "verify",
1041 "--key-id",
1042 &self.key_id,
1043 "--message",
1044 &message_arg,
1045 "--message-type",
1046 "RAW",
1047 "--signature",
1048 &signature_b64,
1049 "--signing-algorithm",
1050 self.aws_algorithm,
1051 "--output",
1052 "json",
1053 ])
1054 .env_opt("AWS_PROFILE", self.profile.clone())
1055 .env_opt("AWS_REGION", self.region.clone())
1056 .run_json()
1057 .map_err(|err| map_cli_error("aws kms verify", &self.binary, AWS_CLI_GUIDANCE, err))?;
1058 Ok(resp.signature_valid)
1059 }
1060}
1061
1062#[cfg(feature = "kms-cli-gcp")]
1063struct GcpKmsCliSigner {
1064 key_resource: String,
1065 kid: String,
1066 algorithm: SigningAlgorithm,
1067 digest_algorithm: Option<String>,
1068 signing_algorithm_flag: Option<String>,
1069 binary: String,
1070 public_key: Option<Jwk>,
1071 public_key_hint: String,
1072 fetched_public_key: OnceCell<Jwk>,
1073}
1074
1075#[cfg(feature = "kms-cli-gcp")]
1076impl GcpKmsCliSigner {
1077 fn new(slot: &SignerSlot) -> Result<Self> {
1078 let algorithm =
1079 configured_algorithm(slot)?.unwrap_or_else(|| SigningAlgorithm::new("PS256"));
1080 match algorithm.as_str() {
1081 "ES256" | "PS256" | "RS256" | "EdDSA" => {}
1082 other => return Err(anyhow!("unsupported GCP signing algorithm '{}'", other)),
1083 }
1084 let (digest_algorithm, signing_algorithm_flag) = if algorithm.is_edwards() {
1085 (None, Some("ec-sign-ed25519".to_string()))
1086 } else {
1087 let mut digest = env::var(slot.gcp_digest_env).unwrap_or_else(|_| "SHA256".into());
1088 if digest.trim().is_empty() {
1089 digest = "SHA256".into();
1090 }
1091 (Some(digest), None)
1092 };
1093 let key_resource = env::var(slot.gcp_key_env).map_err(|_| {
1094 anyhow!(
1095 "{} signer backend 'gcp-kms-cli' requires {} to be set",
1096 slot.label,
1097 slot.gcp_key_env
1098 )
1099 })?;
1100 let kid = format!("gcp-kms:{}", key_resource);
1101 let binary = env::var(slot.cli_bin_env).unwrap_or_else(|_| "gcloud".into());
1102 require_cli(
1103 &binary,
1104 &format!("{} signer backend 'gcp-kms-cli'", slot.label),
1105 GCP_CLI_GUIDANCE,
1106 )?;
1107 let public_key = load_jwk_from_env(slot.public_key_env, slot.public_key_path_env)?
1108 .map(|jwk| attach_kid(jwk, &kid));
1109 let public_key_hint = format!(
1110 "set {} or {}",
1111 slot.public_key_env, slot.public_key_path_env
1112 );
1113 Ok(Self {
1114 key_resource,
1115 kid,
1116 algorithm,
1117 digest_algorithm,
1118 signing_algorithm_flag,
1119 binary,
1120 public_key,
1121 public_key_hint,
1122 fetched_public_key: OnceCell::new(),
1123 })
1124 }
1125
1126 fn fetch_public_key(&self) -> Result<Jwk> {
1127 #[derive(Deserialize)]
1128 struct GcpPublicKeyResponse {
1129 pem: String,
1130 }
1131 let resp: GcpPublicKeyResponse = ProcessRunner::new(&self.binary)
1132 .args([
1133 "kms",
1134 "keys",
1135 "versions",
1136 "get-public-key",
1137 "--name",
1138 &self.key_resource,
1139 "--format",
1140 "json",
1141 "--quiet",
1142 ])
1143 .run_json()
1144 .map_err(|err| {
1145 map_cli_error(
1146 "gcloud kms keys versions get-public-key",
1147 &self.binary,
1148 GCP_CLI_GUIDANCE,
1149 err,
1150 )
1151 })?;
1152 jwk_from_gcp_public_key(&resp.pem, &self.kid, &self.algorithm, &self.public_key_hint)
1153 }
1154}
1155
1156#[cfg(feature = "kms-cli-gcp")]
1157impl Signer for GcpKmsCliSigner {
1158 fn algorithm(&self) -> SigningAlgorithm {
1159 self.algorithm.clone()
1160 }
1161
1162 fn key_id(&self) -> &str {
1163 &self.kid
1164 }
1165
1166 fn public_key_jwk(&self) -> Result<Jwk> {
1167 if let Some(jwk) = &self.public_key {
1168 return Ok(jwk.clone());
1169 }
1170 if let Some(jwk) = self.fetched_public_key.get() {
1171 return Ok(jwk.clone());
1172 }
1173 let jwk = self.fetch_public_key()?;
1174 let _ = self.fetched_public_key.set(jwk.clone());
1175 Ok(jwk)
1176 }
1177
1178 fn sign(&self, payload: &[u8]) -> Result<SignatureEnvelope> {
1179 let mut input_file = NamedTempFile::new().context("failed to create temp file")?;
1180 input_file
1181 .write_all(payload)
1182 .context("failed to write payload to temp file")?;
1183 #[derive(Deserialize)]
1184 struct GcpSignResponse {
1185 signature: String,
1186 }
1187 let mut runner = ProcessRunner::new(&self.binary).args([
1188 "kms",
1189 "keys",
1190 "versions",
1191 "asymmetric-sign",
1192 "--name",
1193 &self.key_resource,
1194 "--input-file",
1195 input_file.path().to_str().unwrap(),
1196 "--format",
1197 "json",
1198 "--quiet",
1199 ]);
1200 if let Some(sign_alg) = &self.signing_algorithm_flag {
1201 runner = runner.args(["--signing-algorithm", sign_alg]);
1202 } else if let Some(digest) = &self.digest_algorithm {
1203 runner = runner.args(["--digest-algorithm", digest]);
1204 }
1205 let public_key = Some(self.public_key_jwk()?);
1206 let resp: GcpSignResponse = runner.run_json().map_err(|err| {
1207 map_cli_error(
1208 "gcloud kms keys versions asymmetric-sign",
1209 &self.binary,
1210 GCP_CLI_GUIDANCE,
1211 err,
1212 )
1213 })?;
1214 let mut signature = BASE64
1215 .decode(resp.signature.as_bytes())
1216 .context("invalid gcloud signature output")?;
1217 signature = normalize_ecdsa_signature(&signature, &self.algorithm)?;
1218 Ok(SignatureEnvelope {
1219 algorithm: self.algorithm(),
1220 signature,
1221 key_id: self.kid.clone(),
1222 public_key,
1223 })
1224 }
1225
1226 fn verify_via_backend(&self, payload: &[u8], signature: &[u8]) -> Result<bool> {
1227 let mut input_file = NamedTempFile::new().context("failed to create temp file")?;
1228 input_file
1229 .write_all(payload)
1230 .context("failed to write payload to temp file")?;
1231 let mut signature_file = NamedTempFile::new().context("failed to create signature file")?;
1232 signature_file
1233 .write_all(&BASE64.encode(signature).into_bytes())
1234 .context("failed to write signature to temp file")?;
1235 #[derive(Deserialize)]
1236 struct GcpVerifyResponse {
1237 verified: bool,
1238 }
1239 let mut runner = ProcessRunner::new(&self.binary).args([
1240 "kms",
1241 "keys",
1242 "versions",
1243 "asymmetric-verify",
1244 "--name",
1245 &self.key_resource,
1246 "--input-file",
1247 input_file.path().to_str().unwrap(),
1248 "--signature-file",
1249 signature_file.path().to_str().unwrap(),
1250 "--format",
1251 "json",
1252 "--quiet",
1253 ]);
1254 if let Some(sign_alg) = &self.signing_algorithm_flag {
1255 runner = runner.args(["--signing-algorithm", sign_alg]);
1256 } else if let Some(digest) = &self.digest_algorithm {
1257 runner = runner.args(["--digest-algorithm", digest]);
1258 }
1259 let resp: GcpVerifyResponse = runner.run_json().map_err(|err| {
1260 map_cli_error(
1261 "gcloud kms keys versions asymmetric-verify",
1262 &self.binary,
1263 GCP_CLI_GUIDANCE,
1264 err,
1265 )
1266 })?;
1267 Ok(resp.verified)
1268 }
1269}
1270
1271#[cfg(feature = "kms-cli-azure")]
1272struct AzureKvCliSigner {
1273 key_id: String,
1274 kid: String,
1275 algorithm: SigningAlgorithm,
1276 azure_algorithm: String,
1277 binary: String,
1278 public_key: Option<Jwk>,
1279 fetched_public_key: OnceCell<Jwk>,
1280}
1281
1282#[cfg(feature = "kms-cli-azure")]
1283impl AzureKvCliSigner {
1284 fn new(slot: &SignerSlot) -> Result<Self> {
1285 let algorithm =
1286 configured_algorithm(slot)?.unwrap_or_else(|| SigningAlgorithm::new("PS256"));
1287 let azure_algorithm = map_azure_algorithm(&algorithm)?;
1288 let key_id = env::var(slot.azure_key_env).map_err(|_| {
1289 anyhow!(
1290 "{} signer backend 'azure-kv-cli' requires {} to be set",
1291 slot.label,
1292 slot.azure_key_env
1293 )
1294 })?;
1295 let kid = format!("azure-kv:{}", key_id);
1296 let binary = env::var(slot.cli_bin_env).unwrap_or_else(|_| "az".into());
1297 require_cli(
1298 &binary,
1299 &format!("{} signer backend 'azure-kv-cli'", slot.label),
1300 AZURE_CLI_GUIDANCE,
1301 )?;
1302 let public_key = load_jwk_from_env(slot.public_key_env, slot.public_key_path_env)?
1303 .map(|jwk| attach_kid(jwk, &kid));
1304 Ok(Self {
1305 key_id,
1306 kid,
1307 algorithm,
1308 azure_algorithm,
1309 binary,
1310 public_key,
1311 fetched_public_key: OnceCell::new(),
1312 })
1313 }
1314
1315 fn fetch_public_key(&self) -> Result<Jwk> {
1316 let value: Value = ProcessRunner::new(&self.binary)
1317 .args([
1318 "keyvault",
1319 "key",
1320 "show",
1321 "--id",
1322 &self.key_id,
1323 "--output",
1324 "json",
1325 ])
1326 .run_json()
1327 .map_err(|err| {
1328 map_cli_error(
1329 "az keyvault key show",
1330 &self.binary,
1331 AZURE_CLI_GUIDANCE,
1332 err,
1333 )
1334 })?;
1335 let key_value = value
1336 .get("key")
1337 .cloned()
1338 .ok_or_else(|| anyhow!("Azure key show response missing 'key' object"))?;
1339 let jwk =
1340 Jwk::from_value(key_value).context("Azure key payload is not a valid JWK shape")?;
1341 Ok(attach_kid(jwk, &self.kid))
1342 }
1343}
1344
1345#[cfg(feature = "kms-cli-azure")]
1346impl Signer for AzureKvCliSigner {
1347 fn algorithm(&self) -> SigningAlgorithm {
1348 self.algorithm.clone()
1349 }
1350
1351 fn key_id(&self) -> &str {
1352 &self.kid
1353 }
1354
1355 fn public_key_jwk(&self) -> Result<Jwk> {
1356 if let Some(jwk) = &self.public_key {
1357 return Ok(jwk.clone());
1358 }
1359 self.fetched_public_key
1360 .get_or_try_init(|| self.fetch_public_key())
1361 .map(|jwk| jwk.clone())
1362 }
1363
1364 fn sign(&self, payload: &[u8]) -> Result<SignatureEnvelope> {
1365 let digest = digest_for_algorithm(&self.algorithm, payload)?;
1366 let digest_b64 = BASE64.encode(&digest);
1367 let public_key = Some(self.public_key_jwk()?);
1368 #[derive(Deserialize)]
1369 struct AzureSignResponse {
1370 result: String,
1371 }
1372 let resp: AzureSignResponse = ProcessRunner::new(&self.binary)
1373 .args([
1374 "keyvault",
1375 "key",
1376 "sign",
1377 "--id",
1378 &self.key_id,
1379 "--algorithm",
1380 &self.azure_algorithm,
1381 "--digest",
1382 &digest_b64,
1383 "--output",
1384 "json",
1385 ])
1386 .run_json()
1387 .map_err(|err| {
1388 map_cli_error(
1389 "az keyvault key sign",
1390 &self.binary,
1391 AZURE_CLI_GUIDANCE,
1392 err,
1393 )
1394 })?;
1395 let mut signature = BASE64
1396 .decode(resp.result.as_bytes())
1397 .context("invalid Azure signature output")?;
1398 signature = normalize_ecdsa_signature(&signature, &self.algorithm)?;
1399 Ok(SignatureEnvelope {
1400 algorithm: self.algorithm(),
1401 signature,
1402 key_id: self.kid.clone(),
1403 public_key,
1404 })
1405 }
1406
1407 fn verify_via_backend(&self, payload: &[u8], signature: &[u8]) -> Result<bool> {
1408 let digest = digest_for_algorithm(&self.algorithm, payload)?;
1409 let digest_b64 = BASE64.encode(&digest);
1410 let signature_b64 = BASE64.encode(signature);
1411 #[derive(Deserialize)]
1412 struct AzureVerifyResponse {
1413 value: bool,
1414 }
1415 let resp: AzureVerifyResponse = ProcessRunner::new(&self.binary)
1416 .args([
1417 "keyvault",
1418 "key",
1419 "verify",
1420 "--id",
1421 &self.key_id,
1422 "--algorithm",
1423 &self.azure_algorithm,
1424 "--digest",
1425 &digest_b64,
1426 "--value",
1427 &signature_b64,
1428 "--output",
1429 "json",
1430 ])
1431 .run_json()
1432 .map_err(|err| {
1433 map_cli_error(
1434 "az keyvault key verify",
1435 &self.binary,
1436 AZURE_CLI_GUIDANCE,
1437 err,
1438 )
1439 })?;
1440 Ok(resp.value)
1441 }
1442}
1443
1444#[cfg(feature = "kms-cli-aws")]
1445fn map_aws_algorithm(alg: &SigningAlgorithm) -> Result<&'static str> {
1446 match alg.as_str() {
1447 "ES256" => Ok("ECDSA_SHA_256"),
1448 "ES384" => Ok("ECDSA_SHA_384"),
1449 "PS256" => Ok("RSASSA_PSS_SHA_256"),
1450 "PS384" => Ok("RSASSA_PSS_SHA_384"),
1451 "RS256" => Ok("RSASSA_PKCS1_V1_5_SHA_256"),
1452 other => Err(anyhow!(
1453 "unsupported AWS signing algorithm '{}' (supported: ES256, ES384, PS256, PS384, RS256)",
1454 other
1455 )),
1456 }
1457}
1458
1459#[cfg(feature = "kms-cli-azure")]
1460fn map_azure_algorithm(alg: &SigningAlgorithm) -> Result<String> {
1461 match alg.as_str() {
1462 "ES256" | "ES384" | "ES512" | "PS256" | "PS384" | "PS512" | "RS256" | "RS384" | "RS512" => {
1463 Ok(alg.as_str().to_string())
1464 }
1465 other => Err(anyhow!("unsupported Azure signing algorithm '{}'", other)),
1466 }
1467}
1468
1469#[cfg(feature = "jwk-aws")]
1470pub fn jwk_from_aws_public_key(der: &[u8], key_spec: &str, kid: &str) -> Result<Jwk> {
1471 let spki =
1472 SubjectPublicKeyInfoOwned::from_der(der).context("failed to parse AWS public key SPKI")?;
1473 match key_spec {
1474 "ECC_NIST_P256" => jwk_from_ec_spki(&spki, "P-256", 32, kid),
1475 "ECC_NIST_P384" => jwk_from_ec_spki(&spki, "P-384", 48, kid),
1476 "ECC_NIST_P521" => jwk_from_ec_spki(&spki, "P-521", 66, kid),
1477 "RSA_2048" | "RSA_3072" | "RSA_4096" => jwk_from_rsa_spki(&spki, kid),
1478 other => Err(anyhow!(
1479 "unsupported AWS KMS KeySpec '{}' for public key export",
1480 other
1481 )),
1482 }
1483}
1484
1485#[cfg(any(feature = "jwk-aws", feature = "jwk-gcp"))]
1486fn jwk_from_ec_spki(
1487 spki: &SubjectPublicKeyInfoOwned,
1488 curve: &str,
1489 coord_len: usize,
1490 kid: &str,
1491) -> Result<Jwk> {
1492 let point = spki.subject_public_key.raw_bytes();
1493 if point.len() != 1 + (coord_len * 2) || point.first().copied() != Some(0x04) {
1494 return Err(anyhow!("unsupported EC public key format from AWS KMS"));
1495 }
1496 let x = &point[1..1 + coord_len];
1497 let y = &point[1 + coord_len..];
1498 let mut fields = Map::new();
1499 fields.insert("kty".into(), Value::String("EC".into()));
1500 fields.insert("crv".into(), Value::String(curve.into()));
1501 fields.insert("x".into(), Value::String(base64_url(x)));
1502 fields.insert("y".into(), Value::String(base64_url(y)));
1503 Ok(attach_kid(Jwk { fields }, kid))
1504}
1505
1506#[cfg(any(feature = "jwk-aws", feature = "jwk-gcp"))]
1507fn jwk_from_rsa_spki(spki: &SubjectPublicKeyInfoOwned, kid: &str) -> Result<Jwk> {
1508 let spk_bytes = spki
1509 .subject_public_key
1510 .as_bytes()
1511 .ok_or_else(|| anyhow!("subjectPublicKey BIT STRING not octet-aligned"))?;
1512 let rsa = rsa::RsaPublicKey::from_pkcs1_der(spk_bytes)
1513 .context("failed to parse RSA public key from AWS KMS")?;
1514 let n_bytes = rsa.n().to_bytes_be();
1515 let e_bytes = rsa.e().to_bytes_be();
1516 let n = trim_leading_zeroes(&n_bytes);
1517 let e = trim_leading_zeroes(&e_bytes);
1518 let mut fields = Map::new();
1519 fields.insert("kty".into(), Value::String("RSA".into()));
1520 fields.insert("n".into(), Value::String(base64_url(n)));
1521 fields.insert("e".into(), Value::String(base64_url(e)));
1522 Ok(attach_kid(Jwk { fields }, kid))
1523}
1524
1525pub const TRUST_MESH_ALPHA_FLAG: &str = "TRUST_MESH_ALPHA";
1527
1528#[derive(Clone, Debug, PartialEq, Eq)]
1529pub struct SigningAlgorithm(Cow<'static, str>);
1530
1531impl SigningAlgorithm {
1532 pub fn new(value: impl Into<Cow<'static, str>>) -> Self {
1533 Self(value.into())
1534 }
1535
1536 pub fn as_str(&self) -> &str {
1537 &self.0
1538 }
1539
1540 pub fn parse(value: &str) -> Result<Self> {
1541 let normalized = value.trim().to_ascii_uppercase();
1542 if normalized.is_empty() {
1543 return Err(anyhow!("signing algorithm value is empty"));
1544 }
1545 Ok(SigningAlgorithm::new(normalized))
1546 }
1547
1548 pub fn is_ecdsa(&self) -> bool {
1549 matches!(self.as_str(), "ES256" | "ES384" | "ES512")
1550 }
1551
1552 pub fn is_rsa(&self) -> bool {
1553 matches!(
1554 self.as_str(),
1555 "PS256" | "PS384" | "PS512" | "RS256" | "RS384" | "RS512"
1556 )
1557 }
1558
1559 pub fn is_edwards(&self) -> bool {
1560 self.as_str() == "EDDSA"
1561 }
1562
1563 pub fn is_hmac(&self) -> bool {
1564 matches!(self.as_str(), "HS256" | "HS384" | "HS512")
1565 }
1566}
1567
1568impl Default for SigningAlgorithm {
1569 fn default() -> Self {
1570 SigningAlgorithm::new("HS256")
1571 }
1572}
1573
1574impl Serialize for SigningAlgorithm {
1575 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1576 where
1577 S: serde::Serializer,
1578 {
1579 serializer.serialize_str(self.as_str())
1580 }
1581}
1582
1583impl<'de> Deserialize<'de> for SigningAlgorithm {
1584 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1585 where
1586 D: serde::Deserializer<'de>,
1587 {
1588 let value = String::deserialize(deserializer)?;
1589 SigningAlgorithm::parse(&value)
1590 .map_err(|err| serde::de::Error::custom(format!("invalid signing algorithm: {err}")))
1591 }
1592}
1593
1594#[derive(Clone, Debug, Serialize, Deserialize)]
1595pub struct Jwk {
1596 #[serde(flatten)]
1597 pub fields: Map<String, Value>,
1598}
1599
1600impl Jwk {
1601 pub fn from_value(value: Value) -> Result<Self> {
1602 let map = value
1603 .as_object()
1604 .cloned()
1605 .ok_or_else(|| anyhow!("JWK must be a JSON object"))?;
1606 Ok(Self { fields: map })
1607 }
1608}
1609
1610#[derive(Clone, Debug, Serialize, Deserialize)]
1611pub struct SignatureEnvelope {
1612 pub algorithm: SigningAlgorithm,
1613 #[serde(with = "crate::serde_helpers::base64_bytes")]
1614 pub signature: Vec<u8>,
1615 pub key_id: String,
1616 #[serde(skip_serializing_if = "Option::is_none")]
1617 pub public_key: Option<Jwk>,
1618}
1619
1620impl SignatureEnvelope {
1621 pub fn verify_offline(&self, payload: &[u8]) -> Result<Option<bool>> {
1622 match &self.public_key {
1623 Some(jwk) => try_verify_with_jwk(&self.algorithm, jwk, payload, &self.signature),
1624 None => Ok(None),
1625 }
1626 }
1627}
1628
1629pub fn verify_signature_envelope<F>(
1630 payload: &[u8],
1631 envelope: &SignatureEnvelope,
1632 signer_lookup: F,
1633) -> Result<bool>
1634where
1635 F: Fn(&str) -> Option<Arc<dyn Signer>>,
1636{
1637 if let Some(result) = envelope.verify_offline(payload)? {
1638 return Ok(result);
1639 }
1640 if let Some(signer) = signer_lookup(&envelope.key_id) {
1641 return signer.verify(payload, &envelope.signature);
1642 }
1643 Ok(false)
1644}
1645
1646pub trait Signer: Send + Sync {
1647 fn algorithm(&self) -> SigningAlgorithm;
1648 fn key_id(&self) -> &str;
1649 fn public_key_jwk(&self) -> Result<Jwk>;
1650 fn sign(&self, payload: &[u8]) -> Result<SignatureEnvelope>;
1651 fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<bool> {
1652 if let Ok(jwk) = self.public_key_jwk() {
1653 if let Some(result) = try_verify_with_jwk(&self.algorithm(), &jwk, payload, signature)?
1654 {
1655 return Ok(result);
1656 }
1657 }
1658 self.verify_via_backend(payload, signature)
1659 }
1660
1661 fn verify_via_backend(&self, _payload: &[u8], _signature: &[u8]) -> Result<bool> {
1662 Ok(false)
1663 }
1664
1665 fn sign_json(&self, value: &Value) -> Result<SignatureEnvelope> {
1666 let canonical = serde_json::to_vec(value)?;
1667 self.sign(&canonical)
1668 }
1669}
1670
1671#[derive(Clone, Copy)]
1672enum HmacAlgorithm {
1673 Sha256,
1674 Sha384,
1675 Sha512,
1676}
1677
1678impl HmacAlgorithm {
1679 fn from_option(algorithm: Option<SigningAlgorithm>) -> Result<Self> {
1680 match algorithm {
1681 Some(alg) => match alg.as_str() {
1682 "HS256" => Ok(Self::Sha256),
1683 "HS384" => Ok(Self::Sha384),
1684 "HS512" => Ok(Self::Sha512),
1685 other => Err(anyhow!(
1686 "unsupported HMAC signing algorithm '{}'; expected HS256/HS384/HS512",
1687 other
1688 )),
1689 },
1690 None => Ok(Self::Sha256),
1691 }
1692 }
1693
1694 fn signing_algorithm(&self) -> SigningAlgorithm {
1695 match self {
1696 Self::Sha256 => SigningAlgorithm::new("HS256"),
1697 Self::Sha384 => SigningAlgorithm::new("HS384"),
1698 Self::Sha512 => SigningAlgorithm::new("HS512"),
1699 }
1700 }
1701
1702 fn sign(&self, key: &[u8], payload: &[u8]) -> Result<Vec<u8>> {
1703 match self {
1704 Self::Sha256 => {
1705 let mut signer = HmacSha256::new_from_slice(key)
1706 .context("HMAC (SHA-256) signing key must be valid length")?;
1707 signer.update(payload);
1708 Ok(signer.finalize().into_bytes().to_vec())
1709 }
1710 Self::Sha384 => {
1711 let mut signer = HmacSha384::new_from_slice(key)
1712 .context("HMAC (SHA-384) signing key must be valid length")?;
1713 signer.update(payload);
1714 Ok(signer.finalize().into_bytes().to_vec())
1715 }
1716 Self::Sha512 => {
1717 let mut signer = HmacSha512::new_from_slice(key)
1718 .context("HMAC (SHA-512) signing key must be valid length")?;
1719 signer.update(payload);
1720 Ok(signer.finalize().into_bytes().to_vec())
1721 }
1722 }
1723 }
1724}
1725
1726#[derive(Clone)]
1727pub struct TrustSigner {
1728 key: Vec<u8>,
1729 kid: String,
1730 jwk: Jwk,
1731 hmac_alg: HmacAlgorithm,
1732}
1733
1734impl TrustSigner {
1735 pub fn for_trust() -> Self {
1736 Self::for_trust_with_algorithm(None)
1737 }
1738
1739 pub fn for_trust_with_algorithm(algorithm: Option<SigningAlgorithm>) -> Self {
1740 let key = match load_key_material(
1741 TRUST_SIGNING_KEY_PATH_ENV,
1742 TRUST_SIGNING_KEY_ENV,
1743 "trust",
1744 false,
1745 )
1746 .expect("failed to load trust signing key material")
1747 {
1748 Some((key, _)) => key,
1749 None => default_hmac_key_material(),
1750 };
1751 TrustSigner::from_secret_with_algorithm(key, algorithm)
1752 .expect("failed to initialize trust signer")
1753 }
1754
1755 fn from_secret(key: Vec<u8>) -> Result<Self> {
1756 Self::from_secret_with_algorithm(key, None)
1757 }
1758
1759 fn from_secret_with_algorithm(
1760 key: Vec<u8>,
1761 algorithm: Option<SigningAlgorithm>,
1762 ) -> Result<Self> {
1763 let hmac_alg = HmacAlgorithm::from_option(algorithm)?;
1764 let (jwk, kid) = build_oct_jwk(&key)?;
1765 Ok(Self {
1766 key,
1767 kid,
1768 jwk,
1769 hmac_alg,
1770 })
1771 }
1772
1773 fn sign_raw(&self, payload: &[u8]) -> Result<Vec<u8>> {
1774 self.hmac_alg.sign(&self.key, payload)
1775 }
1776}
1777
1778impl Signer for TrustSigner {
1779 fn algorithm(&self) -> SigningAlgorithm {
1780 self.hmac_alg.signing_algorithm()
1781 }
1782
1783 fn key_id(&self) -> &str {
1784 &self.kid
1785 }
1786
1787 fn public_key_jwk(&self) -> Result<Jwk> {
1788 Ok(self.jwk.clone())
1789 }
1790
1791 fn sign(&self, payload: &[u8]) -> Result<SignatureEnvelope> {
1792 let signature = self.sign_raw(payload)?;
1793 Ok(SignatureEnvelope {
1794 algorithm: self.algorithm(),
1795 signature,
1796 key_id: self.kid.clone(),
1797 public_key: Some(self.jwk.clone()),
1798 })
1799 }
1800
1801 fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<bool> {
1802 let expected = self.sign_raw(payload)?;
1803 Ok(expected == signature)
1804 }
1805}
1806
1807fn load_key_material(
1808 path_env: &str,
1809 key_env: &str,
1810 label: &str,
1811 strict: bool,
1812) -> Result<Option<(Vec<u8>, String)>> {
1813 if let Ok(path) = env::var(path_env) {
1814 match fs::read(&path) {
1815 Ok(bytes) => {
1816 let id = Path::new(&path)
1817 .file_name()
1818 .and_then(|name| name.to_str())
1819 .map(|name| format!("file:{name}"))
1820 .unwrap_or_else(|| format!("file:{label}-key"));
1821 return Ok(Some((bytes, id)));
1822 }
1823 Err(err) if strict => {
1824 return Err(anyhow!(
1825 "failed to read signing key from '{}': {}",
1826 path,
1827 err
1828 ));
1829 }
1830 Err(_) => {
1831 }
1833 }
1834 }
1835
1836 if let Ok(encoded) = env::var(key_env) {
1837 let trimmed = encoded.trim();
1838 if trimmed.is_empty() {
1839 return Ok(None);
1840 }
1841 if let Ok(decoded) = BASE64.decode(trimmed) {
1842 return Ok(Some((decoded, format!("env:{label}"))));
1843 }
1844 return Ok(Some((
1845 trimmed.as_bytes().to_vec(),
1846 format!("env:{label}-raw"),
1847 )));
1848 }
1849
1850 if strict {
1851 Ok(None)
1852 } else {
1853 Ok(None)
1854 }
1855}
1856
1857fn default_hmac_key_material() -> Vec<u8> {
1858 Sha256::digest(b"fleetforge-trust-default-signer").to_vec()
1859}
1860
1861fn load_jwk_from_env(json_env: &str, path_env: &str) -> Result<Option<Jwk>> {
1862 if let Ok(raw) = env::var(json_env) {
1863 let trimmed = raw.trim();
1864 if trimmed.is_empty() {
1865 return Ok(None);
1866 }
1867 let value: Value = serde_json::from_str(trimmed)
1868 .map_err(|err| anyhow!("invalid JWK JSON in {}: {}", json_env, err))?;
1869 return Ok(Some(Jwk::from_value(value)?));
1870 }
1871 if let Ok(path) = env::var(path_env) {
1872 let contents = fs::read_to_string(&path)
1873 .with_context(|| format!("failed to read JWK file '{}'", path))?;
1874 let value: Value = serde_json::from_str(&contents)
1875 .map_err(|err| anyhow!("invalid JWK JSON in {}: {}", path, err))?;
1876 return Ok(Some(Jwk::from_value(value)?));
1877 }
1878 Ok(None)
1879}
1880
1881fn attach_kid(mut jwk: Jwk, kid: &str) -> Jwk {
1882 jwk.fields
1883 .insert("kid".to_string(), Value::String(kid.to_string()));
1884 jwk
1885}
1886
1887#[derive(Clone)]
1888pub struct Ed25519Signer {
1889 kid: String,
1890 signing_key: Ed25519SigningKey,
1891 verifying_key: Ed25519VerifyingKey,
1892 jwk: Jwk,
1893}
1894
1895impl Ed25519Signer {
1896 pub fn from_bytes(secret: &[u8]) -> Result<Self> {
1897 let key_bytes: [u8; 32] = match secret.len() {
1898 32 => {
1899 let mut array = [0u8; 32];
1900 array.copy_from_slice(secret);
1901 array
1902 }
1903 64 => {
1904 let mut array = [0u8; 32];
1905 array.copy_from_slice(&secret[..32]);
1906 array
1907 }
1908 _ => {
1909 return Err(anyhow!(
1910 "Ed25519 key material must be 32 or 64 bytes; got {}",
1911 secret.len()
1912 ));
1913 }
1914 };
1915 let signing_key = Ed25519SigningKey::from_bytes(&key_bytes);
1916 let verifying_key = Ed25519VerifyingKey::from(&signing_key);
1917 let (jwk, kid) = build_ed25519_jwk(&verifying_key.to_bytes())?;
1918 Ok(Self {
1919 kid,
1920 signing_key,
1921 verifying_key,
1922 jwk,
1923 })
1924 }
1925}
1926
1927impl Signer for Ed25519Signer {
1928 fn algorithm(&self) -> SigningAlgorithm {
1929 SigningAlgorithm::new("EdDSA")
1930 }
1931
1932 fn key_id(&self) -> &str {
1933 &self.kid
1934 }
1935
1936 fn public_key_jwk(&self) -> Result<Jwk> {
1937 Ok(self.jwk.clone())
1938 }
1939
1940 fn sign(&self, payload: &[u8]) -> Result<SignatureEnvelope> {
1941 let signature: Ed25519Signature = self.signing_key.sign(payload);
1942 Ok(SignatureEnvelope {
1943 algorithm: self.algorithm(),
1944 signature: signature.to_bytes().to_vec(),
1945 key_id: self.kid.clone(),
1946 public_key: Some(self.jwk.clone()),
1947 })
1948 }
1949
1950 fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<bool> {
1951 if signature.len() != 64 {
1952 return Ok(false);
1953 }
1954 let mut sig_bytes = [0u8; 64];
1955 sig_bytes.copy_from_slice(signature);
1956 let sig = Ed25519Signature::from_bytes(&sig_bytes);
1957 Ok(self.verifying_key.verify(payload, &sig).is_ok())
1958 }
1959}
1960
1961pub fn trust_signer() -> Arc<dyn Signer> {
1963 resolve_signer(&TRUST_SIGNER_SLOT, false).unwrap_or_else(|err| {
1964 eprintln!(
1965 "warning: falling back to local trust signer because configuration failed: {err}"
1966 );
1967 Arc::new(TrustSigner::for_trust())
1968 })
1969}
1970
1971pub fn scitt_signer() -> Result<Arc<dyn Signer>> {
1973 ensure_feature_allowed(LicensedFeature::ScittTransparency)?;
1974 resolve_signer(&SCITT_SIGNER_SLOT, true)
1975}
1976
1977pub fn capability_signer() -> Result<Arc<dyn Signer>> {
1979 resolve_signer(&CAPABILITY_SIGNER_SLOT, false)
1980}
1981
1982pub fn c2pa_signer() -> Result<Arc<dyn Signer>> {
1984 resolve_signer(&C2PA_SIGNER_SLOT, false)
1985}
1986
1987#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1989#[serde(tag = "kind", rename_all = "snake_case")]
1990pub enum Trust {
1991 Trusted,
1992 Untrusted,
1993 Derived {
1994 #[serde(flatten)]
1995 origin: TrustOrigin,
1996 },
1997}
1998
1999impl Trust {
2000 pub fn derived(origin: TrustOrigin) -> Self {
2002 Self::Derived { origin }
2003 }
2004
2005 pub fn origin(&self) -> Option<&TrustOrigin> {
2007 match self {
2008 Trust::Derived { origin } => Some(origin),
2009 _ => None,
2010 }
2011 }
2012}
2013
2014#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2016pub struct TrustOrigin {
2017 pub boundary: TrustBoundary,
2018 #[serde(skip_serializing_if = "Option::is_none")]
2019 pub run_id: Option<Uuid>,
2020 #[serde(skip_serializing_if = "Option::is_none")]
2021 pub step_id: Option<Uuid>,
2022 #[serde(skip_serializing_if = "Option::is_none")]
2023 pub source: Option<TrustSource>,
2024}
2025
2026impl TrustOrigin {
2027 pub fn new(boundary: TrustBoundary) -> Self {
2028 Self {
2029 boundary,
2030 run_id: None,
2031 step_id: None,
2032 source: None,
2033 }
2034 }
2035
2036 pub fn with_run_id(mut self, run_id: Uuid) -> Self {
2037 self.run_id = Some(run_id);
2038 self
2039 }
2040
2041 pub fn with_step_id(mut self, step_id: Uuid) -> Self {
2042 self.step_id = Some(step_id);
2043 self
2044 }
2045
2046 pub fn with_source(mut self, source: TrustSource) -> Self {
2047 self.source = Some(source);
2048 self
2049 }
2050}
2051
2052#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2054#[serde(rename_all = "snake_case")]
2055pub enum TrustBoundary {
2056 IngressPrompt,
2057 IngressTool,
2058 IngressMemory,
2059 IngressDocument,
2060 Scheduler,
2061 Executor,
2062 EgressPrompt,
2063 EgressTool,
2064 EgressMemory,
2065 Artifact,
2066 Gateway,
2067 Unknown,
2068}
2069
2070#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2072#[serde(tag = "type", rename_all = "snake_case")]
2073pub enum TrustSource {
2074 UserInput,
2075 Retrieval {
2076 #[serde(skip_serializing_if = "Option::is_none")]
2077 provider: Option<String>,
2078 },
2079 ToolOutput {
2080 #[serde(skip_serializing_if = "Option::is_none")]
2081 tool: Option<String>,
2082 },
2083 Memory {
2084 namespace: String,
2085 key: String,
2086 },
2087 System,
2088 Other {
2089 #[serde(skip_serializing_if = "Option::is_none")]
2090 detail: Option<String>,
2091 },
2092}
2093
2094#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2096#[serde(transparent)]
2097pub struct Trusted<T>(pub T);
2098
2099impl<T> Trusted<T> {
2100 pub fn into_inner(self) -> T {
2101 self.0
2102 }
2103}
2104
2105impl<T> From<T> for Trusted<T> {
2106 fn from(value: T) -> Self {
2107 Self(value)
2108 }
2109}
2110
2111#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2113#[serde(transparent)]
2114pub struct Untrusted<T>(pub T);
2115
2116impl<T> Untrusted<T> {
2117 pub fn into_inner(self) -> T {
2118 self.0
2119 }
2120
2121 pub fn as_ref(&self) -> &T {
2122 &self.0
2123 }
2124
2125 pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Untrusted<U> {
2126 Untrusted(f(self.0))
2127 }
2128}
2129
2130impl<T> From<T> for Untrusted<T> {
2131 fn from(value: T) -> Self {
2132 Self(value)
2133 }
2134}
2135
2136#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2138pub struct Attestation {
2139 pub id: Uuid,
2140 pub issued_at: DateTime<Utc>,
2142 #[serde(skip_serializing_if = "Option::is_none")]
2144 pub subject: Option<TrustSubject>,
2145 #[serde(skip_serializing_if = "Option::is_none")]
2147 pub artifact_hash: Option<String>,
2148 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2150 pub claims: BTreeMap<String, serde_json::Value>,
2151 #[serde(skip_serializing_if = "Option::is_none")]
2153 pub signature: Option<String>,
2154}
2155
2156impl Attestation {
2157 pub fn new(id: Uuid, issued_at: DateTime<Utc>) -> Self {
2158 Self {
2159 id,
2160 issued_at,
2161 subject: None,
2162 artifact_hash: None,
2163 claims: BTreeMap::new(),
2164 signature: None,
2165 }
2166 }
2167
2168 pub fn with_subject(mut self, subject: TrustSubject) -> Self {
2169 self.subject = Some(subject);
2170 self
2171 }
2172
2173 pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
2174 self.signature = Some(signature.into());
2175 self
2176 }
2177}
2178
2179#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2181#[serde(tag = "type", rename_all = "snake_case")]
2182pub enum TrustSubject {
2183 Run { run_id: Uuid },
2184 Step { run_id: Uuid, step_id: Uuid },
2185 Tool { name: String },
2186 Artifact { uri: String },
2187 CapabilityToken { token_id: Uuid },
2188 Custom { label: String },
2189}
2190
2191#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2193pub struct TrustDecision {
2194 pub subject: TrustSubject,
2195 pub policy_id: String,
2196 pub verdict: TrustVerdict,
2197 #[serde(skip_serializing_if = "Option::is_none")]
2198 pub reason: Option<String>,
2199 #[serde(skip_serializing_if = "Option::is_none")]
2200 pub attestation_id: Option<Uuid>,
2201}
2202
2203impl TrustDecision {
2204 pub fn new(subject: TrustSubject, policy_id: impl Into<String>, verdict: TrustVerdict) -> Self {
2205 Self {
2206 subject,
2207 policy_id: policy_id.into(),
2208 verdict,
2209 reason: None,
2210 attestation_id: None,
2211 }
2212 }
2213
2214 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
2215 self.reason = Some(reason.into());
2216 self
2217 }
2218
2219 pub fn with_attestation(mut self, attestation_id: Uuid) -> Self {
2220 self.attestation_id = Some(attestation_id);
2221 self
2222 }
2223}
2224
2225#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2227#[serde(rename_all = "snake_case")]
2228pub enum TrustVerdict {
2229 Allow,
2230 Deny,
2231 Redact,
2232}
2233
2234pub use c2pa::{
2235 generate_c2pa_manifest, verify_c2pa_manifest, C2paManifestEnvelope, CapabilityEvidence,
2236 CapabilityEvidenceEntry, IdentityEvidence, ManifestInput, ManifestProfile, PolicyEvidence,
2237 VerifiedManifest,
2238};
2239pub use capability::{
2240 mint_capability_token, verify_capability_token, CapabilityBudgetLimits, CapabilityClaims,
2241 CapabilitySchemaRef, CapabilityToken, CapabilityTokenScope, CapabilityTokenSubject,
2242 CapabilityToolScope,
2243};
2244pub use scitt::build_scitt_entry;
2245pub use vault::{digest_bytes, digest_json, AttestationVault, InMemoryAttestationVault};
2246pub use vault_object_store::ObjectStoreAttestationVault;
2247
2248#[cfg(test)]
2249mod tests {
2250 use super::*;
2251 use chrono::Duration;
2252 use std::env;
2253 use std::sync::{Mutex, OnceLock};
2254
2255 fn env_lock() -> &'static Mutex<()> {
2256 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
2257 LOCK.get_or_init(|| Mutex::new(()))
2258 }
2259
2260 #[test]
2261 fn scitt_signer_requires_configuration() {
2262 let _guard = env_lock().lock().unwrap();
2263 let original_key = env::var(super::SCITT_SIGNING_KEY_ENV).ok();
2264 let original_path = env::var(super::SCITT_SIGNING_KEY_PATH_ENV).ok();
2265 let original_tier = env::var("FLEETFORGE_LICENSE_TIER").ok();
2266 env::set_var("FLEETFORGE_LICENSE_TIER", "enterprise");
2267 env::remove_var(super::SCITT_SIGNING_KEY_ENV);
2268 env::remove_var(super::SCITT_SIGNING_KEY_PATH_ENV);
2269
2270 let result = scitt_signer();
2271 assert!(
2272 result.is_err(),
2273 "expected scitt_signer to fail without explicit key"
2274 );
2275
2276 if let Some(value) = original_key {
2277 env::set_var(super::SCITT_SIGNING_KEY_ENV, value);
2278 } else {
2279 env::remove_var(super::SCITT_SIGNING_KEY_ENV);
2280 }
2281
2282 if let Some(value) = original_path {
2283 env::set_var(super::SCITT_SIGNING_KEY_PATH_ENV, value);
2284 } else {
2285 env::remove_var(super::SCITT_SIGNING_KEY_PATH_ENV);
2286 }
2287 if let Some(value) = original_tier {
2288 env::set_var("FLEETFORGE_LICENSE_TIER", value);
2289 } else {
2290 env::remove_var("FLEETFORGE_LICENSE_TIER");
2291 }
2292 }
2293
2294 #[test]
2295 fn capability_token_mints_jws() {
2296 let _guard = env_lock().lock().unwrap();
2297 let signer = capability_signer().expect("capability signer should load");
2298 let subject = CapabilityTokenSubject {
2299 run_id: Uuid::new_v4(),
2300 step_id: Some(Uuid::new_v4()),
2301 attempt: Some(1),
2302 };
2303 let scope = CapabilityTokenScope {
2304 tool: CapabilityToolScope {
2305 name: "test_tool".into(),
2306 id: Some("tool-123".into()),
2307 variant: Some("v1".into()),
2308 },
2309 schema: CapabilitySchemaRef {
2310 hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
2311 version: Some("1.0.0".into()),
2312 },
2313 data_domains: Some(vec!["default".into()]),
2314 budget: Some(CapabilityBudgetLimits {
2315 tokens: Some(100),
2316 cost_usd: Some(1.5),
2317 duration_ms: Some(60000),
2318 }),
2319 };
2320
2321 let token = mint_capability_token(
2322 subject,
2323 scope,
2324 Some(vec!["runtime".into()]),
2325 Duration::minutes(5),
2326 signer.as_ref(),
2327 )
2328 .expect("minting capability token should succeed");
2329
2330 verify_capability_token(&token).expect("capability token must verify with signer");
2331 let parts: Vec<&str> = token.jws.split('.').collect();
2332 assert_eq!(parts.len(), 3, "jws must contain three segments");
2333 assert_eq!(
2334 token.key_id,
2335 signer.key_id(),
2336 "capability token should expose signer key id"
2337 );
2338 assert_ne!(token.claims.token_id, Uuid::nil());
2339 }
2340}