Skip to main content

actr_hyper/verify/
trust.rs

1//! Trust provider — pluggable verifier for `.actr` package signatures.
2//!
3//! Replaces the old `TrustMode` enum. A `TrustProvider` answers the only
4//! question Hyper cares about at load time: "is this package bytes-authentic
5//! enough for me to execute?". How it answers is up to the provider:
6//!
7//! - [`StaticTrust`] — one pre-configured Ed25519 public key, accepts any
8//!   manufacturer. Offline / air-gapped deployments.
9//! - [`RegistryTrust`] — fetch MFR public keys from an AIS HTTP registry by
10//!   `(manufacturer, signing_key_id)`, cached locally.
11//! - [`ChainTrust`] — try a list of providers in order; first success wins.
12//!
13//! Both built-in Ed25519-based providers delegate to [`actr_pack::verify`],
14//! which performs the full signature + binary-hash + resource-hash chain.
15//! Custom providers (e.g. wasm-side keyless verification, HSM, threshold
16//! signatures) may implement [`TrustProvider`] however they want — the trait
17//! only obliges them to take raw bytes in and return a verified manifest out.
18
19use std::sync::Arc;
20
21use actr_pack::VerifiedPackage;
22use async_trait::async_trait;
23use ed25519_dalek::VerifyingKey;
24
25use crate::error::{HyperError, HyperResult};
26use crate::verify::cert_cache::MfrCertCache;
27
28/// Verifier for `.actr` package signatures.
29///
30/// An implementation fully takes raw package bytes and returns the parsed,
31/// trusted package — or errors. Callers must not use any field of the
32/// returned [`VerifiedPackage`] before calling this.
33#[async_trait]
34pub trait TrustProvider: Send + Sync + std::fmt::Debug {
35    async fn verify_package(&self, bytes: &[u8]) -> HyperResult<VerifiedPackage>;
36}
37
38// ── shared helper for the Ed25519 + pubkey path ──────────────────────────────
39
40/// Verify an `.actr` package against a single Ed25519 public key.
41///
42/// Shared helper used by [`StaticTrust`] and [`RegistryTrust`].
43pub(crate) fn verify_ed25519_manifest(
44    bytes: &[u8],
45    pubkey: &VerifyingKey,
46) -> HyperResult<VerifiedPackage> {
47    let verified = actr_pack::verify(bytes, pubkey).map_err(pack_err_to_hyper)?;
48
49    tracing::info!(
50        actr_type = %verified.manifest.actr_type_str(),
51        ".actr package verified"
52    );
53
54    Ok(verified)
55}
56
57fn pack_err_to_hyper(e: actr_pack::PackError) -> HyperError {
58    match e {
59        actr_pack::PackError::SignatureVerificationFailed(msg) => {
60            HyperError::SignatureVerificationFailed(msg)
61        }
62        actr_pack::PackError::BinaryHashMismatch { .. } => HyperError::BinaryHashMismatch,
63        actr_pack::PackError::SignatureNotFound => {
64            HyperError::SignatureVerificationFailed("signature not found in package".to_string())
65        }
66        actr_pack::PackError::BinaryNotFound(path) => {
67            HyperError::InvalidManifest(format!("binary not found: {path}"))
68        }
69        actr_pack::PackError::ManifestNotFound => HyperError::ManifestNotFound,
70        actr_pack::PackError::ManifestParseError(msg) => HyperError::InvalidManifest(msg),
71        other => HyperError::InvalidManifest(other.to_string()),
72    }
73}
74
75fn parse_pubkey(bytes: &[u8]) -> HyperResult<VerifyingKey> {
76    let arr: [u8; 32] = bytes
77        .try_into()
78        .map_err(|_| HyperError::Config("Ed25519 pubkey must be exactly 32 bytes".to_string()))?;
79    VerifyingKey::from_bytes(&arr)
80        .map_err(|e| HyperError::Config(format!("invalid Ed25519 pubkey: {e}")))
81}
82
83// ── StaticTrust ──────────────────────────────────────────────────────────────
84
85/// Pre-configured single Ed25519 public key. Accepts packages from any
86/// manufacturer as long as they verify against this key.
87///
88/// Intended for dev / air-gapped / self-hosted deployments where the
89/// manufacturer's public key is shipped alongside the package (typically as
90/// `public-key.json`) instead of queried from a registry.
91#[derive(Debug, Clone)]
92pub struct StaticTrust {
93    pubkey: VerifyingKey,
94}
95
96impl StaticTrust {
97    /// Construct from 32 raw Ed25519 public key bytes.
98    pub fn new(pubkey: impl AsRef<[u8]>) -> HyperResult<Self> {
99        Ok(Self {
100            pubkey: parse_pubkey(pubkey.as_ref())?,
101        })
102    }
103
104    /// Development-only trust provider seeded with an all-zero Ed25519 public
105    /// key. Accepts **no real package** (signatures against a zero key always
106    /// fail), but lets test and example code wire a valid `TrustProvider`
107    /// without pulling a real key file.
108    ///
109    /// Never use in production — the only reason this exists is so
110    /// `Node::from_config_file` can distinguish an explicit opt-in to dev
111    /// mode from a missing trust configuration (which is a hard error).
112    /// Emits no warning of its own; callers should log at their discretion
113    /// (`Node::from_config_file` emits a `tracing::warn!` when it selects
114    /// this provider from a `kind = "dev_only"` config entry).
115    pub fn dev_only() -> Self {
116        // `from_bytes` accepts all-zero 32 bytes (it is a valid curve point,
117        // just a broken one for signing) so `.unwrap()` here is sound.
118        Self {
119            pubkey: VerifyingKey::from_bytes(&[0u8; 32]).expect("all-zero pubkey parses"),
120        }
121    }
122}
123
124#[async_trait]
125impl TrustProvider for StaticTrust {
126    async fn verify_package(&self, bytes: &[u8]) -> HyperResult<VerifiedPackage> {
127        verify_ed25519_manifest(bytes, &self.pubkey)
128    }
129}
130
131// ── RegistryTrust ────────────────────────────────────────────────────────────
132
133/// Resolve manufacturer public keys from an AIS HTTP registry and verify
134/// Ed25519 signatures against them. Internal cache with configurable TTL
135/// (default 1h).
136///
137/// The package manifest must carry `signing_key_id`; otherwise the provider
138/// errors out — rebuild with the latest `actr build` to embed one.
139#[derive(Debug, Clone)]
140pub struct RegistryTrust {
141    cache: Arc<MfrCertCache>,
142}
143
144impl RegistryTrust {
145    pub fn new(endpoint: impl Into<String>) -> Self {
146        Self {
147            cache: MfrCertCache::new(endpoint),
148        }
149    }
150}
151
152#[async_trait]
153impl TrustProvider for RegistryTrust {
154    async fn verify_package(&self, bytes: &[u8]) -> HyperResult<VerifiedPackage> {
155        let pack_manifest = actr_pack::read_manifest(bytes).map_err(|e| match e {
156            actr_pack::PackError::ManifestNotFound => HyperError::ManifestNotFound,
157            actr_pack::PackError::ManifestParseError(msg) => HyperError::InvalidManifest(msg),
158            other => HyperError::InvalidManifest(other.to_string()),
159        })?;
160
161        let key_id = pack_manifest.signing_key_id.as_deref().ok_or_else(|| {
162            HyperError::InvalidManifest(
163                "package manifest missing `signing_key_id`; rebuild with the latest `actr build`"
164                    .to_string(),
165            )
166        })?;
167
168        let pubkey = self
169            .cache
170            .get_or_fetch(&pack_manifest.manufacturer, Some(key_id))
171            .await?;
172
173        verify_ed25519_manifest(bytes, &pubkey)
174    }
175}
176
177// ── ChainTrust ───────────────────────────────────────────────────────────────
178
179/// Try a list of providers in order; the first `Ok(_)` wins.
180///
181/// Useful for "local cache first, registry fallback" setups or for rolling
182/// key migrations where an old static key and a new registry-backed provider
183/// coexist.
184#[derive(Debug, Clone)]
185pub struct ChainTrust {
186    providers: Vec<Arc<dyn TrustProvider>>,
187}
188
189impl ChainTrust {
190    pub fn new(providers: Vec<Arc<dyn TrustProvider>>) -> Self {
191        Self { providers }
192    }
193
194    /// Shortcut for a two-provider chain.
195    pub fn of(first: Arc<dyn TrustProvider>, second: Arc<dyn TrustProvider>) -> Self {
196        Self::new(vec![first, second])
197    }
198}
199
200#[async_trait]
201impl TrustProvider for ChainTrust {
202    async fn verify_package(&self, bytes: &[u8]) -> HyperResult<VerifiedPackage> {
203        let mut last_err: Option<HyperError> = None;
204        for p in &self.providers {
205            match p.verify_package(bytes).await {
206                Ok(m) => return Ok(m),
207                Err(e) => last_err = Some(e),
208            }
209        }
210        Err(last_err.unwrap_or_else(|| {
211            HyperError::SignatureVerificationFailed("empty trust chain".to_string())
212        }))
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use ed25519_dalek::{Signer, SigningKey};
220    use rand::rngs::OsRng;
221
222    fn make_minimal_package(signing_key: &SigningKey) -> Vec<u8> {
223        let manifest = actr_pack::PackageManifest {
224            manufacturer: "test-mfr".to_string(),
225            name: "Test".to_string(),
226            version: "1.0.0".to_string(),
227            binary: actr_pack::BinaryEntry {
228                path: "bin/actor.wasm".to_string(),
229                target: "wasm32-wasip1".to_string(),
230                hash: String::new(),
231                size: None,
232                kind: None,
233            },
234            signature_algorithm: "ed25519".to_string(),
235            signing_key_id: Some(actr_pack::compute_key_id(
236                &signing_key.verifying_key().to_bytes(),
237            )),
238            resources: vec![],
239            proto_files: vec![],
240            lock_file: None,
241            metadata: actr_pack::ManifestMetadata::default(),
242        };
243        actr_pack::pack(&actr_pack::PackOptions {
244            manifest,
245            binary_bytes: b"wasm".to_vec(),
246            resources: vec![],
247            proto_files: vec![],
248            lock_file: None,
249            signing_key: signing_key.clone(),
250        })
251        .unwrap()
252    }
253
254    #[tokio::test]
255    async fn static_trust_accepts_valid_package() {
256        let key = SigningKey::generate(&mut OsRng);
257        let vk = key.verifying_key();
258        let pkg = make_minimal_package(&key);
259
260        let trust = StaticTrust::new(vk.to_bytes()).unwrap();
261        let verified = trust.verify_package(&pkg).await.unwrap();
262        assert_eq!(verified.manifest.manufacturer, "test-mfr");
263    }
264
265    #[tokio::test]
266    async fn static_trust_rejects_wrong_key() {
267        let key = SigningKey::generate(&mut OsRng);
268        let wrong = SigningKey::generate(&mut OsRng);
269        let pkg = make_minimal_package(&key);
270
271        let trust = StaticTrust::new(wrong.verifying_key().to_bytes()).unwrap();
272        assert!(matches!(
273            trust.verify_package(&pkg).await,
274            Err(HyperError::SignatureVerificationFailed(_))
275        ));
276    }
277
278    #[tokio::test]
279    async fn chain_first_match_wins() {
280        let key = SigningKey::generate(&mut OsRng);
281        let other = SigningKey::generate(&mut OsRng);
282        let pkg = make_minimal_package(&key);
283
284        let wrong: Arc<dyn TrustProvider> =
285            Arc::new(StaticTrust::new(other.verifying_key().to_bytes()).unwrap());
286        let right: Arc<dyn TrustProvider> =
287            Arc::new(StaticTrust::new(key.verifying_key().to_bytes()).unwrap());
288
289        let chain = ChainTrust::of(wrong, right);
290        let verified = chain.verify_package(&pkg).await.unwrap();
291        assert_eq!(verified.manifest.manufacturer, "test-mfr");
292    }
293
294    #[tokio::test]
295    async fn chain_all_fail_returns_last_error() {
296        let key = SigningKey::generate(&mut OsRng);
297        let wrong1 = SigningKey::generate(&mut OsRng);
298        let wrong2 = SigningKey::generate(&mut OsRng);
299        let pkg = make_minimal_package(&key);
300
301        let chain = ChainTrust::of(
302            Arc::new(StaticTrust::new(wrong1.verifying_key().to_bytes()).unwrap()),
303            Arc::new(StaticTrust::new(wrong2.verifying_key().to_bytes()).unwrap()),
304        );
305        assert!(matches!(
306            chain.verify_package(&pkg).await,
307            Err(HyperError::SignatureVerificationFailed(_))
308        ));
309    }
310
311    // Just so the minimum-bound test doesn't compile away unused Signer import.
312    #[allow(dead_code)]
313    fn _signer_sanity(key: &SigningKey) -> ed25519_dalek::Signature {
314        key.sign(b"x")
315    }
316}