Skip to main content

heddle_thread_api/
authority.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The same client-minted Biscuit rules at device and hosted boundaries. The
3//! application supplies roots already attached to the owner/account and a
4//! resolved spool path. No key or authority is minted by this verifier.
5use std::sync::RwLock;
6
7use api::{heddle::api::common::CallContext, v2::MethodDescriptor};
8use base64::Engine as _;
9use biscuit_verifier::{BiscuitFacts, PublicKey};
10use chrono::{DateTime, Utc};
11
12use crate::transport::Error;
13
14pub struct RootAuthority {
15    roots: RwLock<Vec<PublicKey>>,
16    spool_path: String,
17}
18impl RootAuthority {
19    pub fn new(roots: Vec<PublicKey>, spool_path: String) -> Result<Self, Error> {
20        if roots.is_empty() || spool_path.is_empty() {
21            return Err(Error::Protocol(
22                "root attachment and resolved spool path required",
23            ));
24        }
25        Ok(Self {
26            roots: RwLock::new(roots),
27            spool_path,
28        })
29    }
30    /// Called once per opening; nonce claims remain durable across restarts.
31    pub fn verify(
32        &self,
33        context: &CallContext,
34        method: &'static MethodDescriptor,
35        body: &[u8],
36        right: &str,
37        registry: &repo::thread_replication::ThreadReplica,
38    ) -> Result<VerifiedCall, Error> {
39        let now = Utc::now();
40        let facts = self.check(context, method, right, now)?;
41        let cnf = facts
42            .cnf
43            .as_deref()
44            .ok_or(Error::Protocol("Biscuit must bind a request signing key"))?;
45        let mut key = [0; 32];
46        hex::decode_to_slice(cnf, &mut key)
47            .map_err(|_| Error::Protocol("invalid Biscuit proof key"))?;
48        let proof =
49            crate::request_proof::verify(context, method, body, &key, now.timestamp_millis())?;
50        if !registry
51            .claim_request_nonce(proof.identity(), proof.nonce(), now.timestamp_millis())
52            .map_err(|e| Error::Io(e.to_string()))?
53        {
54            return Err(Error::Protocol("request nonce already consumed"));
55        }
56        Ok(VerifiedCall {
57            context: context.clone(),
58            method,
59            right: right.into(),
60            principal: facts.sub,
61        })
62    }
63    fn check(
64        &self,
65        context: &CallContext,
66        method: &'static MethodDescriptor,
67        right: &str,
68        now: DateTime<Utc>,
69    ) -> Result<BiscuitFacts, Error> {
70        if context
71            .deadline
72            .as_ref()
73            .is_some_and(|deadline| deadline.seconds <= now.timestamp())
74        {
75            return Err(Error::Protocol("request deadline expired"));
76        }
77        // The RPC carries raw Biscuit bytes. Text is confined to the verifier's
78        // existing storage/configuration API; it is never a second wire form.
79        let bearer = base64::engine::general_purpose::URL_SAFE.encode(&context.bearer_capability);
80        let envelope = if context.bearer_grant_envelope.is_empty() {
81            None
82        } else {
83            Some(
84                std::str::from_utf8(&context.bearer_grant_envelope)
85                    .map_err(|_| Error::Protocol("invalid grant envelope encoding"))?,
86            )
87        };
88        let operation = method
89            .path
90            .rsplit('/')
91            .next()
92            .ok_or(Error::Protocol("invalid method path"))?;
93        let facts = biscuit_verifier::verify_any_at_with_resource(
94            &bearer,
95            envelope,
96            &self
97                .roots
98                .read()
99                .map_err(|_| Error::Protocol("root registry unavailable"))?,
100            &[],
101            operation,
102            Some(("spool", &self.spool_path)),
103            now,
104        )
105        .map_err(|_| Error::Protocol("Biscuit does not authorize this operation"))?;
106        if !facts.has_right("spool", &self.spool_path, right) {
107            return Err(Error::Protocol("Biscuit does not grant this spool right"));
108        }
109        Ok(facts)
110    }
111    pub fn recheck(&self, call: &VerifiedCall) -> Result<(), Error> {
112        self.check(&call.context, call.method, &call.right, Utc::now())
113            .map(|_| ())
114    }
115
116    /// Replace the application's current root attachments. An empty set revokes
117    /// all ongoing calls as well as future openings at the next recheck.
118    pub fn replace_roots(&self, roots: Vec<PublicKey>) -> Result<(), Error> {
119        *self
120            .roots
121            .write()
122            .map_err(|_| Error::Protocol("root registry unavailable"))? = roots;
123        Ok(())
124    }
125}
126
127#[derive(Clone)]
128pub struct VerifiedCall {
129    context: CallContext,
130    method: &'static MethodDescriptor,
131    right: String,
132    pub principal: String,
133}