alpine_protocol_sdk/trust/
mod.rs1use 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}