Skip to main content

alpine_protocol_sdk/trust/
mod.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::time::{Duration, SystemTime};
4
5use alpine::attestation::{
6    verify_attester_bundle, AttesterBundleError, AttesterRegistry, VerifiedAttesterBundle,
7};
8use base64::{engine::general_purpose, Engine as _};
9use directories::ProjectDirs;
10use reqwest::StatusCode;
11use thiserror::Error;
12use tracing::warn;
13
14use crate::discovery::DeviceTrustState;
15use crate::error::AlpineSdkError;
16
17const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
18
19#[derive(Debug, Clone)]
20pub enum TrustSource {
21    Fetched,
22    Cached,
23    Override,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum TrustPolicy {
28    Strict,
29    WarnOnly,
30    AllowUntrusted,
31}
32
33impl TrustPolicy {
34    /// Alias for strict attestation enforcement (rejects untrusted identities).
35    #[allow(non_upper_case_globals)]
36    pub const RequireAttestation: TrustPolicy = TrustPolicy::Strict;
37}
38
39#[must_use]
40#[derive(Debug, Clone)]
41pub struct TrustView {
42    pub bundle: VerifiedAttesterBundle,
43    pub registry: AttesterRegistry,
44    pub source: TrustSource,
45    pub warnings: Vec<String>,
46}
47
48#[derive(Debug, Clone)]
49pub struct TrustConfig {
50    pub bundle_url: String,
51    pub cache_path: PathBuf,
52    pub root_pubkey: Option<[u8; 32]>,
53    pub override_path: Option<PathBuf>,
54    pub timeout: Duration,
55    pub pinned_bundle_issued_at: Option<u64>,
56    pub pinned_bundle_signer_kid: Option<String>,
57}
58
59#[derive(Debug, Error)]
60pub enum TrustError {
61    #[error("root pubkey not configured")]
62    MissingRootKey,
63    #[error("bundle fetch failed: {0}")]
64    Fetch(String),
65    #[error("bundle cache missing or unreadable: {0}")]
66    CacheRead(String),
67    #[error("bundle cache write failed: {0}")]
68    CacheWrite(String),
69    #[error("bundle verification failed: {0}")]
70    Verify(String),
71    #[error("bundle version pin mismatch: {0}")]
72    PinMismatch(String),
73}
74
75pub fn enforce_trust_policy(
76    trust_state: DeviceTrustState,
77    policy: TrustPolicy,
78) -> Result<(), AlpineSdkError> {
79    match policy {
80        TrustPolicy::AllowUntrusted => Ok(()),
81        TrustPolicy::WarnOnly => {
82            if trust_state != DeviceTrustState::Trusted {
83                warn!(
84                    "[ALPINE][TRUST][WARN] device trust policy warn_only state={}",
85                    trust_state.as_str()
86                );
87            }
88            Ok(())
89        }
90        TrustPolicy::Strict => match trust_state {
91            DeviceTrustState::Trusted => Ok(()),
92            DeviceTrustState::UntrustedNoAttestation => Err(AlpineSdkError::UntrustedDevice(
93                "device identity attestation missing".into(),
94            )),
95            DeviceTrustState::UntrustedNoRegistry => Err(AlpineSdkError::UntrustedDevice(
96                "attester registry not configured".into(),
97            )),
98            DeviceTrustState::UntrustedInvalid(reason) => {
99                Err(AlpineSdkError::UntrustedDevice(reason))
100            }
101        },
102    }
103}
104
105impl TrustConfig {
106    pub fn new(bundle_url: impl Into<String>) -> Self {
107        Self {
108            bundle_url: bundle_url.into(),
109            cache_path: default_cache_path(),
110            root_pubkey: None,
111            override_path: None,
112            timeout: DEFAULT_TIMEOUT,
113            pinned_bundle_issued_at: None,
114            pinned_bundle_signer_kid: None,
115        }
116    }
117
118    pub fn with_root_pubkey(mut self, root_pubkey: [u8; 32]) -> Self {
119        self.root_pubkey = Some(root_pubkey);
120        self
121    }
122
123    pub fn with_cache_path(mut self, cache_path: PathBuf) -> Self {
124        self.cache_path = cache_path;
125        self
126    }
127
128    pub fn with_override_path(mut self, override_path: PathBuf) -> Self {
129        self.override_path = Some(override_path);
130        self
131    }
132
133    pub fn with_timeout(mut self, timeout: Duration) -> Self {
134        self.timeout = timeout;
135        self
136    }
137
138    pub fn with_pinned_bundle_issued_at(mut self, issued_at: u64) -> Self {
139        self.pinned_bundle_issued_at = Some(issued_at);
140        self
141    }
142
143    pub fn with_pinned_bundle_signer_kid(mut self, signer_kid: impl Into<String>) -> Self {
144        self.pinned_bundle_signer_kid = Some(signer_kid.into());
145        self
146    }
147}
148
149pub fn parse_root_pubkey_base64(value: &str) -> Result<[u8; 32], TrustError> {
150    let bytes = general_purpose::STANDARD
151        .decode(value.as_bytes())
152        .map_err(|err| TrustError::Verify(err.to_string()))?;
153    bytes
154        .try_into()
155        .map_err(|_| TrustError::Verify("root pubkey must be 32 bytes".into()))
156}
157
158pub async fn load_or_fetch_trust_view(config: &TrustConfig) -> Result<TrustView, TrustError> {
159    let root_pubkey = config.root_pubkey.ok_or(TrustError::MissingRootKey)?;
160    let now = SystemTime::now();
161    let mut warnings = Vec::new();
162
163    if let Some(override_path) = &config.override_path {
164        return load_override_trust_view(override_path, root_pubkey, warnings, config);
165    }
166
167    let cached = match fs::read(&config.cache_path) {
168        Ok(bytes) => match verify_attester_bundle(&bytes, &root_pubkey, now) {
169            Ok(bundle) => {
170                if enforce_bundle_pinning(&bundle, config).is_ok() {
171                    Some(bundle)
172                } else {
173                    warnings.push("cached bundle rejected: pin mismatch".into());
174                    None
175                }
176            }
177            Err(err) => {
178                warnings.push(format!("cached bundle rejected: {}", err));
179                None
180            }
181        },
182        Err(err) => {
183            warnings.push(format!("cached bundle unavailable: {}", err));
184            None
185        }
186    };
187
188    match fetch_latest_bundle(&config.bundle_url, config.timeout).await {
189        Ok(bytes) => {
190            let bundle = verify_attester_bundle(&bytes, &root_pubkey, now)
191                .map_err(|err| TrustError::Verify(err.to_string()))?;
192            enforce_bundle_pinning(&bundle, config)?;
193            cache_bundle(&config.cache_path, &bytes)?;
194            Ok(TrustView {
195                registry: bundle.registry.clone(),
196                bundle,
197                source: TrustSource::Fetched,
198                warnings,
199            })
200        }
201        Err(fetch_err) => {
202            warnings.push(fetch_err.to_string());
203            if let Some(bundle) = cached {
204                enforce_bundle_pinning(&bundle, config)?;
205                Ok(TrustView {
206                    registry: bundle.registry.clone(),
207                    bundle,
208                    source: TrustSource::Cached,
209                    warnings,
210                })
211            } else {
212                Err(TrustError::Fetch(fetch_err.to_string()))
213            }
214        }
215    }
216}
217
218pub fn load_cached_trust_view(config: &TrustConfig) -> Result<TrustView, TrustError> {
219    let root_pubkey = config.root_pubkey.ok_or(TrustError::MissingRootKey)?;
220    let now = SystemTime::now();
221    if let Some(override_path) = &config.override_path {
222        return load_override_trust_view(override_path, root_pubkey, Vec::new(), config);
223    }
224    let bytes =
225        fs::read(&config.cache_path).map_err(|err| TrustError::CacheRead(err.to_string()))?;
226    let bundle = verify_attester_bundle(&bytes, &root_pubkey, now)
227        .map_err(|err| TrustError::Verify(err.to_string()))?;
228    enforce_bundle_pinning(&bundle, config)?;
229    Ok(TrustView {
230        registry: bundle.registry.clone(),
231        bundle,
232        source: TrustSource::Cached,
233        warnings: Vec::new(),
234    })
235}
236
237async fn fetch_latest_bundle(url: &str, timeout: Duration) -> Result<Vec<u8>, TrustError> {
238    let client = reqwest::Client::builder()
239        .timeout(timeout)
240        .build()
241        .map_err(|err| TrustError::Fetch(err.to_string()))?;
242    let resp = client
243        .get(url)
244        .send()
245        .await
246        .map_err(|err| TrustError::Fetch(err.to_string()))?;
247    if resp.status() != StatusCode::OK {
248        return Err(TrustError::Fetch(format!(
249            "unexpected status {}",
250            resp.status()
251        )));
252    }
253    resp.bytes()
254        .await
255        .map(|b| b.to_vec())
256        .map_err(|err| TrustError::Fetch(err.to_string()))
257}
258
259fn cache_bundle(path: &Path, bytes: &[u8]) -> Result<(), TrustError> {
260    if let Some(parent) = path.parent() {
261        fs::create_dir_all(parent).map_err(|err| TrustError::CacheWrite(err.to_string()))?;
262    }
263    fs::write(path, bytes).map_err(|err| TrustError::CacheWrite(err.to_string()))
264}
265
266fn default_cache_path() -> PathBuf {
267    if let Some(proj) = ProjectDirs::from("io", "alpine", "alpine-protocol-sdk") {
268        proj.cache_dir().join("attesters.bundle.cbor")
269    } else {
270        PathBuf::from(".").join("attesters.bundle.cbor")
271    }
272}
273
274fn load_override_trust_view(
275    override_path: &Path,
276    root_pubkey: [u8; 32],
277    mut warnings: Vec<String>,
278    config: &TrustConfig,
279) -> Result<TrustView, TrustError> {
280    let now = SystemTime::now();
281    let override_bytes =
282        fs::read(override_path).map_err(|err| TrustError::CacheRead(err.to_string()))?;
283    let bundle = verify_attester_bundle(&override_bytes, &root_pubkey, now)
284        .map_err(|err| TrustError::Verify(err.to_string()))?;
285    enforce_bundle_pinning(&bundle, config)?;
286    warnings.push(format!(
287        "attesters override in use: {}",
288        override_path.display()
289    ));
290    Ok(TrustView {
291        registry: bundle.registry.clone(),
292        bundle,
293        source: TrustSource::Override,
294        warnings,
295    })
296}
297
298pub fn verify_cached_bundle(
299    cache_path: &Path,
300    root_pubkey: &[u8; 32],
301    now: SystemTime,
302) -> Result<VerifiedAttesterBundle, AttesterBundleError> {
303    let bytes = fs::read(cache_path).map_err(|err| AttesterBundleError::Decode(err.to_string()))?;
304    verify_attester_bundle(&bytes, root_pubkey, now)
305}
306
307fn enforce_bundle_pinning(
308    bundle: &VerifiedAttesterBundle,
309    config: &TrustConfig,
310) -> Result<(), TrustError> {
311    if let Some(expected) = config.pinned_bundle_issued_at {
312        if bundle.issued_at != expected {
313            return Err(TrustError::PinMismatch(format!(
314                "issued_at expected {} got {}",
315                expected, bundle.issued_at
316            )));
317        }
318    }
319    if let Some(expected) = config.pinned_bundle_signer_kid.as_ref() {
320        if bundle.signer_kid.as_ref() != Some(expected) {
321            return Err(TrustError::PinMismatch(format!(
322                "signer_kid expected {} got {:?}",
323                expected, bundle.signer_kid
324            )));
325        }
326    }
327    Ok(())
328}