alpine_protocol_sdk/trust/
mod.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::time::{Duration, SystemTime};
4
5use alpine::attestation::{verify_attester_bundle, AttesterBundleError, AttesterRegistry, VerifiedAttesterBundle};
6use base64::{engine::general_purpose, Engine as _};
7use directories::ProjectDirs;
8use reqwest::StatusCode;
9use thiserror::Error;
10
11const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
12
13#[derive(Debug, Clone)]
14pub enum TrustSource {
15    Fetched,
16    Cached,
17    Override,
18}
19
20#[derive(Debug, Clone)]
21pub struct TrustView {
22    pub bundle: VerifiedAttesterBundle,
23    pub registry: AttesterRegistry,
24    pub source: TrustSource,
25    pub warnings: Vec<String>,
26}
27
28#[derive(Debug, Clone)]
29pub struct TrustConfig {
30    pub bundle_url: String,
31    pub cache_path: PathBuf,
32    pub root_pubkey: Option<[u8; 32]>,
33    pub override_path: Option<PathBuf>,
34    pub timeout: Duration,
35}
36
37#[derive(Debug, Error)]
38pub enum TrustError {
39    #[error("root pubkey not configured")]
40    MissingRootKey,
41    #[error("bundle fetch failed: {0}")]
42    Fetch(String),
43    #[error("bundle cache missing or unreadable: {0}")]
44    CacheRead(String),
45    #[error("bundle cache write failed: {0}")]
46    CacheWrite(String),
47    #[error("bundle verification failed: {0}")]
48    Verify(String),
49}
50
51impl TrustConfig {
52    pub fn new(bundle_url: impl Into<String>) -> Self {
53        Self {
54            bundle_url: bundle_url.into(),
55            cache_path: default_cache_path(),
56            root_pubkey: None,
57            override_path: None,
58            timeout: DEFAULT_TIMEOUT,
59        }
60    }
61
62    pub fn with_root_pubkey(mut self, root_pubkey: [u8; 32]) -> Self {
63        self.root_pubkey = Some(root_pubkey);
64        self
65    }
66
67    pub fn with_cache_path(mut self, cache_path: PathBuf) -> Self {
68        self.cache_path = cache_path;
69        self
70    }
71
72    pub fn with_override_path(mut self, override_path: PathBuf) -> Self {
73        self.override_path = Some(override_path);
74        self
75    }
76
77    pub fn with_timeout(mut self, timeout: Duration) -> Self {
78        self.timeout = timeout;
79        self
80    }
81}
82
83pub fn parse_root_pubkey_base64(value: &str) -> Result<[u8; 32], TrustError> {
84    let bytes = general_purpose::STANDARD
85        .decode(value.as_bytes())
86        .map_err(|err| TrustError::Verify(err.to_string()))?;
87    bytes
88        .try_into()
89        .map_err(|_| TrustError::Verify("root pubkey must be 32 bytes".into()))
90}
91
92pub async fn load_or_fetch_trust_view(config: &TrustConfig) -> Result<TrustView, TrustError> {
93    let root_pubkey = config.root_pubkey.ok_or(TrustError::MissingRootKey)?;
94    let now = SystemTime::now();
95    let mut warnings = Vec::new();
96
97    if let Some(override_path) = &config.override_path {
98        return load_override_trust_view(override_path, root_pubkey, warnings);
99    }
100
101    let cached = match fs::read(&config.cache_path) {
102        Ok(bytes) => match verify_attester_bundle(&bytes, &root_pubkey, now) {
103            Ok(bundle) => Some(bundle),
104            Err(err) => {
105                warnings.push(format!("cached bundle rejected: {}", err));
106                None
107            }
108        },
109        Err(err) => {
110            warnings.push(format!("cached bundle unavailable: {}", err));
111            None
112        }
113    };
114
115    match fetch_latest_bundle(&config.bundle_url, config.timeout).await {
116        Ok(bytes) => {
117            let bundle = verify_attester_bundle(&bytes, &root_pubkey, now)
118                .map_err(|err| TrustError::Verify(err.to_string()))?;
119            cache_bundle(&config.cache_path, &bytes)?;
120            Ok(TrustView {
121                registry: bundle.registry.clone(),
122                bundle,
123                source: TrustSource::Fetched,
124                warnings,
125            })
126        }
127        Err(fetch_err) => {
128            warnings.push(fetch_err.to_string());
129            if let Some(bundle) = cached {
130                Ok(TrustView {
131                    registry: bundle.registry.clone(),
132                    bundle,
133                    source: TrustSource::Cached,
134                    warnings,
135                })
136            } else {
137                Err(TrustError::Fetch(fetch_err.to_string()))
138            }
139        }
140    }
141}
142
143pub fn load_cached_trust_view(config: &TrustConfig) -> Result<TrustView, TrustError> {
144    let root_pubkey = config.root_pubkey.ok_or(TrustError::MissingRootKey)?;
145    let now = SystemTime::now();
146    if let Some(override_path) = &config.override_path {
147        return load_override_trust_view(override_path, root_pubkey, Vec::new());
148    }
149    let bytes = fs::read(&config.cache_path)
150        .map_err(|err| TrustError::CacheRead(err.to_string()))?;
151    let bundle = verify_attester_bundle(&bytes, &root_pubkey, now)
152        .map_err(|err| TrustError::Verify(err.to_string()))?;
153    Ok(TrustView {
154        registry: bundle.registry.clone(),
155        bundle,
156        source: TrustSource::Cached,
157        warnings: Vec::new(),
158    })
159}
160
161async fn fetch_latest_bundle(url: &str, timeout: Duration) -> Result<Vec<u8>, TrustError> {
162    let client = reqwest::Client::builder()
163        .timeout(timeout)
164        .build()
165        .map_err(|err| TrustError::Fetch(err.to_string()))?;
166    let resp = client
167        .get(url)
168        .send()
169        .await
170        .map_err(|err| TrustError::Fetch(err.to_string()))?;
171    if resp.status() != StatusCode::OK {
172        return Err(TrustError::Fetch(format!(
173            "unexpected status {}",
174            resp.status()
175        )));
176    }
177    resp.bytes()
178        .await
179        .map(|b| b.to_vec())
180        .map_err(|err| TrustError::Fetch(err.to_string()))
181}
182
183fn cache_bundle(path: &Path, bytes: &[u8]) -> Result<(), TrustError> {
184    if let Some(parent) = path.parent() {
185        fs::create_dir_all(parent).map_err(|err| TrustError::CacheWrite(err.to_string()))?;
186    }
187    fs::write(path, bytes).map_err(|err| TrustError::CacheWrite(err.to_string()))
188}
189
190fn default_cache_path() -> PathBuf {
191    if let Some(proj) = ProjectDirs::from("io", "alpine", "alpine-protocol-sdk") {
192        proj.cache_dir().join("attesters.bundle.cbor")
193    } else {
194        PathBuf::from(".").join("attesters.bundle.cbor")
195    }
196}
197
198fn load_override_trust_view(
199    override_path: &Path,
200    root_pubkey: [u8; 32],
201    mut warnings: Vec<String>,
202) -> Result<TrustView, TrustError> {
203    let now = SystemTime::now();
204    let override_bytes =
205        fs::read(override_path).map_err(|err| TrustError::CacheRead(err.to_string()))?;
206    let bundle = verify_attester_bundle(&override_bytes, &root_pubkey, now)
207        .map_err(|err| TrustError::Verify(err.to_string()))?;
208    warnings.push(format!(
209        "attesters override in use: {}",
210        override_path.display()
211    ));
212    Ok(TrustView {
213        registry: bundle.registry.clone(),
214        bundle,
215        source: TrustSource::Override,
216        warnings,
217    })
218}
219
220pub fn verify_cached_bundle(
221    cache_path: &Path,
222    root_pubkey: &[u8; 32],
223    now: SystemTime,
224) -> Result<VerifiedAttesterBundle, AttesterBundleError> {
225    let bytes = fs::read(cache_path).map_err(|err| AttesterBundleError::Decode(err.to_string()))?;
226    verify_attester_bundle(&bytes, root_pubkey, now)
227}