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