fleetforge_trust/
lib.rs

1//! Trust metadata primitives shared across the runtime.
2
3use 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
1525/// Environment flag that gates Trust Mesh alpha capabilities.
1526pub 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                // fall back to alternate sources when not strict
1832            }
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
1961/// Returns the default Trust Mesh signer, falling back to an ephemeral key when none is configured.
1962pub 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
1971/// Loads the SCITT signer configuration, requiring explicit key material.
1972pub fn scitt_signer() -> Result<Arc<dyn Signer>> {
1973    ensure_feature_allowed(LicensedFeature::ScittTransparency)?;
1974    resolve_signer(&SCITT_SIGNER_SLOT, true)
1975}
1976
1977/// Loads the capability signer; falls back to the trust signer when none is configured.
1978pub fn capability_signer() -> Result<Arc<dyn Signer>> {
1979    resolve_signer(&CAPABILITY_SIGNER_SLOT, false)
1980}
1981
1982/// Loads the C2PA signer; falls back to the trust signer when none is configured.
1983pub fn c2pa_signer() -> Result<Arc<dyn Signer>> {
1984    resolve_signer(&C2PA_SIGNER_SLOT, false)
1985}
1986
1987/// High-level trust classification.
1988#[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    /// Convenience constructor for derived trust values.
2001    pub fn derived(origin: TrustOrigin) -> Self {
2002        Self::Derived { origin }
2003    }
2004
2005    /// Returns the underlying origin when present.
2006    pub fn origin(&self) -> Option<&TrustOrigin> {
2007        match self {
2008            Trust::Derived { origin } => Some(origin),
2009            _ => None,
2010        }
2011    }
2012}
2013
2014/// Describes how the runtime obtained an untrusted or derived value.
2015#[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/// Boundary within the runtime where trust is assessed.
2053#[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/// Source system that produced the value.
2071#[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/// Strongly-typed wrapper for trusted values.
2095#[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/// Strongly-typed wrapper for untrusted values.
2112#[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/// Placeholder attestation envelope attached to trust decisions and replay events.
2137#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2138pub struct Attestation {
2139    pub id: Uuid,
2140    /// When the attestation was produced.
2141    pub issued_at: DateTime<Utc>,
2142    /// Subject the attestation refers to (run, step, artifact, etc.).
2143    #[serde(skip_serializing_if = "Option::is_none")]
2144    pub subject: Option<TrustSubject>,
2145    /// Hash or digest of the artifact governed by this attestation.
2146    #[serde(skip_serializing_if = "Option::is_none")]
2147    pub artifact_hash: Option<String>,
2148    /// Arbitrary claims (policy decisions, budget usage, etc.).
2149    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2150    pub claims: BTreeMap<String, serde_json::Value>,
2151    /// Optional signature reference (for external verification services).
2152    #[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/// Identifies what entity an attestation or trust decision covers.
2180#[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/// Outcome of evaluating a policy against a subject.
2192#[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/// Placeholder for future detailed policy verdict information.
2226#[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}