Skip to main content

bitrouter_attestation/
lib.rs

1//! # bitrouter-attestation
2//!
3//! Provider-agnostic, **client-side** confidential-inference verification. The
4//! central abstraction is [`ConfidentialVerifier`]: given a model and a nonce
5//! it proves (L1) the serving endpoint is genuine TEE hardware running the
6//! *legitimate, policy-pinned* model, and given an exact request/response it
7//! proves (L1.5) that exchange ran in that TEE unmodified.
8//!
9//! The design mirrors private-ai-gateway's `UpstreamVerifier` /
10//! `UpstreamVerifiedEvent` normalization, but runs in the caller's own trusted
11//! process (bitrouter-cli's local daemon) instead of inside an attested
12//! re-signing gateway — so it needs **no TEE of its own**. See the refactor
13//! spec (`bitrouter-cloud/docs/bitrouter-attestation-plugin.md`).
14//!
15//! This crate is intentionally pure: no SDK, axum, or server dependency, so it
16//! ships in the daemon, the `bitrouter verify` CLI, the cloud `/v1/aci/verify`
17//! endpoint, and third-party clients alike.
18
19#![forbid(unsafe_code)]
20
21use std::collections::HashMap;
22use std::sync::Arc;
23
24use async_trait::async_trait;
25
26mod cache;
27mod near;
28mod transport;
29mod types;
30
31pub use cache::AttestationCache;
32pub use near::binding::{compose_matches_mr_config, report_data_binds};
33pub use near::dcap::{AciDcapVerifierPolicy, ModelIdentity, PolicyError, model_identity};
34pub use near::eventlog::{event_log_binds_info, replay_rtmr};
35pub use near::nvidia::{
36    NRAS_GPU_URL, NVIDIA_NRAS_JWKS_URL, NrasVerdict, NvidiaEatKey, check_nras_eat, post_nras,
37};
38pub use near::report::{
39    AttestationInfo, AttestationReport, DstackEvent, ModelAttestation, TcbInfo,
40};
41pub use near::signature::{ChatSignature, chat_signing_text, recover_eip191_address, sha256_hex};
42pub use near::tdx::{
43    DcapQuoteVerifier, PHALA_PCCS_URL, QuoteVerifier, TdxMeasurements, parse_tdx_quote,
44    verify_tdx_quote,
45};
46pub use near::{DEFAULT_CACHE_TTL_SECONDS, NearVerifier, TRUST_BOUNDARY};
47pub use transport::{MockTransport, ReportTransport, ReqwestTransport, SIGNING_ALGO};
48pub use types::{
49    AttestationChecks, AttestationVerdict, ExchangeInput, IntegrityProof, VerifiedExchange,
50    VerifyError,
51};
52
53/// Verifies confidential inference for one provider family, client-side.
54///
55/// A *failed* verification is **not** an `Err` — it is an `Ok` verdict with
56/// `verified == false` (fail-closed; spec §1.5 cond. 3). `Err` is reserved for
57/// the verifier being unable to reach a trustworthy verdict at all
58/// (misconfiguration, malformed input).
59#[async_trait]
60pub trait ConfidentialVerifier: Send + Sync {
61    /// Provider type handled — `"near-ai"` (later `"aci-gateway"`, `"tinfoil"`).
62    fn provider(&self) -> &str;
63
64    /// L1 — prove the model endpoint is a genuine, policy-pinned TEE. Yields the
65    /// attested signing identity set that L1.5 binds an exchange signature to.
66    async fn verify_attestation(
67        &self,
68        model: &str,
69        nonce: &str,
70        now_unix: u64,
71    ) -> Result<AttestationVerdict, VerifyError>;
72
73    /// L1.5 — prove a specific exchange ran in that TEE unmodified.
74    async fn verify_exchange(
75        &self,
76        ex: &ExchangeInput<'_>,
77    ) -> Result<VerifiedExchange, VerifyError>;
78
79    /// Hot-path attestation for the plugin/cloud: a verdict for `model` without
80    /// the caller supplying a nonce. The default generates a fresh nonce and
81    /// runs a full [`Self::verify_attestation`]; impls with a cache (e.g.
82    /// `NearVerifier`) override this to serve a TTL'd verdict so NRAS/PCCS isn't
83    /// hit per request.
84    async fn attestation_cached(
85        &self,
86        model: &str,
87        now_unix: u64,
88    ) -> Result<AttestationVerdict, VerifyError> {
89        self.verify_attestation(model, &fresh_nonce_hex(), now_unix)
90            .await
91    }
92}
93
94/// A fresh 32-byte hex nonce for an attestation challenge.
95pub(crate) fn fresh_nonce_hex() -> String {
96    use rand::RngCore;
97    let mut nonce = [0u8; 32];
98    rand::rng().fill_bytes(&mut nonce);
99    hex::encode(nonce)
100}
101
102/// Dispatches by provider so the daemon/cloud can hold one handle and serve
103/// many confidential providers. ← gateway's `RoutingUpstreamVerifier`. NEAR is
104/// the only impl today; the registry exists so Tinfoil/Phala/AciGateway slot in
105/// without touching callers.
106#[derive(Default, Clone)]
107pub struct VerifierRegistry {
108    map: HashMap<String, Arc<dyn ConfidentialVerifier>>,
109}
110
111impl VerifierRegistry {
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    /// Register a verifier under its own `provider()` key. Returns `self` for
117    /// builder-style chaining at boot.
118    pub fn with(mut self, verifier: Arc<dyn ConfidentialVerifier>) -> Self {
119        self.map.insert(verifier.provider().to_string(), verifier);
120        self
121    }
122
123    /// Look up the verifier for a provider, or `UnknownProvider` if none is
124    /// registered — callers fail closed rather than silently skip verification.
125    pub fn get(&self, provider: &str) -> Result<&Arc<dyn ConfidentialVerifier>, VerifyError> {
126        self.map
127            .get(provider)
128            .ok_or_else(|| VerifyError::UnknownProvider(provider.to_string()))
129    }
130
131    /// True if a verifier is registered for `provider`.
132    pub fn handles(&self, provider: &str) -> bool {
133        self.map.contains_key(provider)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    struct StubVerifier;
142
143    #[async_trait]
144    impl ConfidentialVerifier for StubVerifier {
145        fn provider(&self) -> &str {
146            "near-ai"
147        }
148        async fn verify_attestation(
149            &self,
150            model: &str,
151            nonce: &str,
152            now_unix: u64,
153        ) -> Result<AttestationVerdict, VerifyError> {
154            Ok(AttestationVerdict::unverified(model, nonce, now_unix))
155        }
156        async fn verify_exchange(
157            &self,
158            _ex: &ExchangeInput<'_>,
159        ) -> Result<VerifiedExchange, VerifyError> {
160            Err(VerifyError::Malformed {
161                what: "exchange",
162                detail: "not implemented in P1".to_string(),
163            })
164        }
165    }
166
167    #[test]
168    fn registry_dispatches_by_provider_and_fails_closed_on_unknown() {
169        let reg = VerifierRegistry::new().with(Arc::new(StubVerifier));
170        assert!(reg.handles("near-ai"));
171        assert!(reg.get("near-ai").is_ok());
172        assert!(!reg.handles("tinfoil"));
173        match reg.get("tinfoil").err() {
174            Some(VerifyError::UnknownProvider(p)) => assert_eq!(p, "tinfoil"),
175            _ => panic!("expected UnknownProvider for an unregistered provider"),
176        }
177    }
178
179    #[test]
180    fn unverified_verdict_is_fail_closed() {
181        let model = "zai-org/GLM-5.1-FP8";
182        // Nonce derived from `model` (not a hard-coded literal); the value is
183        // immaterial to this fail-closed assertion.
184        let v = AttestationVerdict::unverified(model, format!("test-nonce-{model}"), 42);
185        assert!(!v.verified);
186        assert!(!v.checks.all_pass());
187        assert!(v.attested_addresses.is_empty());
188    }
189}