Skip to main content

aven_protocol/claim/
membership.rs

1//! Pure signed membership transitions and recipient-verified generation coverage.
2//!
3//! Verified signatures establish client intent, not server acceptance, current
4//! authorization, protected persistence or dispatch readiness. Unsupported state
5//! transitions and enrollment versions fail closed.
6mod admission;
7mod encoding;
8mod evidence;
9mod pairing;
10pub use evidence::{Evidence, EvidenceRecord, MAX_EVIDENCE_JSON_BYTES, Mailbox};
11mod keys;
12mod rotation;
13pub use keys::{MAX_COVERAGE_BYTES, VerifiedKeys};
14pub use rotation::{Generation, RotationMaterial};
15#[cfg(any(test, feature = "test-support"))]
16pub mod test_support;
17#[cfg(test)]
18pub(crate) mod tests;
19
20pub use super::peer::Invitation;
21use super::peer::{Recipient, verify};
22use super::*;
23use anyhow::Result;
24pub use pairing::{Declaration, Device, Joiner, ProvisionalGrant, VerifiedEnrollment};
25
26pub const MAX_DEVICES: usize = 32;
27pub use crate::wire::enrollment::MAX_RECORD_BYTES;
28/// Lifetime signed transitions after genesis; every admission, removal and
29/// rotation consumes one.
30pub const MAX_TRANSITIONS: usize = 256;
31/// Lifetime key generations including the initial one; every rotation consumes one.
32pub const MAX_GENERATIONS: usize = 64;
33/// Largest generation cutoff that leaves a representable successor tail rank.
34pub const MAX_CUTOFF: u64 = (i64::MAX - 1) as u64;
35/// Grant plaintext carrying every generation key.
36pub const MAX_KEY_PLAINTEXT_BYTES: usize =
37    GRANT_PREFIX_BYTES + 2 + GENERATION_BYTES * MAX_GENERATIONS;
38pub const DECLARATION_BYTES: usize = 280;
39pub const REQUEST_BYTES: usize = 314;
40const CORE_BYTES: usize = 461;
41const GRANT_PREFIX_BYTES: usize = 1 + 12 * 32 + 8;
42/// Encoded member and generation entries of the full state carried by every record.
43const MEMBER_BYTES: usize = 164;
44const GENERATION_BYTES: usize = 72;
45/// Largest admission: its state and grant both list every generation.
46const MAX_ADMISSION_BYTES: usize =
47    1257 + MEMBER_BYTES * MAX_DEVICES + 2 * GENERATION_BYTES * MAX_GENERATIONS;
48/// Largest rotation: full state plus one key package per member.
49const MAX_ROTATION_BYTES: usize =
50    540 + (MEMBER_BYTES + 306) * MAX_DEVICES + GENERATION_BYTES * MAX_GENERATIONS;
51/// Largest removal, with no targets and every member retained.
52const MAX_REVOKE_BYTES: usize =
53    438 + MEMBER_BYTES * MAX_DEVICES + GENERATION_BYTES * MAX_GENERATIONS;
54/// Worst-case chain: every rotation at its maximum and every other transition
55/// a maximal admission with its declaration and request.
56pub const MAX_CHAIN_BYTES: usize = GENESIS_BYTES
57    + PUBLICATION_BYTES
58    + crate::artifact::codec::MAX_DESCRIPTOR_BYTES
59    + (MAX_GENERATIONS - 1) * MAX_ROTATION_BYTES
60    + (MAX_TRANSITIONS + 1 - MAX_GENERATIONS)
61        * (DECLARATION_BYTES + REQUEST_BYTES + MAX_ADMISSION_BYTES);
62const _: () = assert!(
63    MAX_ADMISSION_BYTES <= MAX_RECORD_BYTES
64        && MAX_ROTATION_BYTES <= MAX_RECORD_BYTES
65        && MAX_REVOKE_BYTES <= DECLARATION_BYTES + REQUEST_BYTES + MAX_ADMISSION_BYTES
66);
67const DOMAIN_VERSION: u32 = 1;
68
69type Hash = [u8; 32];
70fn check(condition: bool) -> Result<()> {
71    ensure!(condition, "error membership-invalid");
72    Ok(())
73}
74
75#[derive(Clone, PartialEq, Eq)]
76struct Member {
77    device: Hash,
78    sign: Hash,
79    hpke: Hash,
80    verifier: Hash,
81    admission: Hash,
82    admitted_at: u64,
83}
84impl Member {
85    fn write(&self, out: &mut Vec<u8>) {
86        for field in [self.device, self.sign, self.hpke, self.verifier] {
87            out.extend(field);
88        }
89        out.extend(1_u32.to_be_bytes());
90        out.extend(self.admission);
91    }
92}
93
94/// A chain derived exclusively from verified signed predecessors. No server
95/// observation or resulting-state DTO can construct this value directly.
96#[derive(Clone)]
97pub struct Membership {
98    genesis: Genesis,
99    publication: Publication,
100    members: Vec<Member>,
101    heads: Vec<Hash>,
102    handles: Vec<Hash>,
103    retired: Vec<Member>,
104    generations: Vec<Generation>,
105    pending: bool,
106}
107impl Membership {
108    pub fn from_publication(
109        genesis: &Genesis,
110        descriptor: &[u8],
111        publication: &[u8],
112    ) -> Result<Self> {
113        let publication = Publication::from_record(genesis, descriptor, publication)?;
114        Ok(Self {
115            genesis: genesis.clone(),
116            heads: vec![publication.commitment()],
117            publication,
118            members: vec![Member {
119                device: genesis.device_id(),
120                sign: genesis.signing_public(),
121                hpke: genesis.hpke_public(),
122                verifier: genesis.verifier(),
123                admission: genesis.claim_id(),
124                admitted_at: 0,
125            }],
126            handles: vec![],
127            retired: vec![],
128            generations: vec![Generation {
129                id: genesis.context().generation_id,
130                commitment: genesis.generation_commitment(),
131                starts_after: 0,
132            }],
133            pending: false,
134        })
135    }
136    pub fn head(&self) -> Hash {
137        self.heads[self.heads.len() - 1]
138    }
139    pub fn sequence(&self) -> u64 {
140        self.heads.len() as u64
141    }
142    pub fn has_device(&self, device: Hash) -> bool {
143        self.members.iter().any(|member| member.device == device)
144    }
145    pub fn devices(&self) -> impl Iterator<Item = Hash> + '_ {
146        self.members.iter().map(|member| member.device)
147    }
148    /// Active devices with the membership sequence that admitted each; the
149    /// device that set up sync has sequence 0.
150    pub fn admissions(&self) -> impl Iterator<Item = (Hash, u64)> + '_ {
151        self.members
152            .iter()
153            .map(|member| (member.device, member.admitted_at))
154    }
155    /// Whether an active member owns this admitted invitation.
156    pub fn has_admission(&self, handle: Hash) -> bool {
157        self.members.iter().any(|member| member.admission == handle)
158    }
159    /// Signer and action framing, not signature or successor verification.
160    pub fn record_action(record: &[u8]) -> Result<(Hash, u8)> {
161        let (core, _, _, _) = encoding::components(record)?;
162        encoding::signer_action(core)
163    }
164    pub fn device_count(&self) -> usize {
165        self.members.len()
166    }
167    pub fn genesis(&self) -> &Genesis {
168        &self.genesis
169    }
170    pub fn publication(&self) -> &Publication {
171        &self.publication
172    }
173    /// Ancestry, not a claim that either checkpoint is the latest server head.
174    pub fn extends(&self, previous: &Self) -> bool {
175        self.genesis == previous.genesis && self.heads.starts_with(&previous.heads)
176    }
177    /// Authenticate the actual credential before examining its requested head.
178    pub fn authenticate(
179        &self,
180        auth: &super::peer::Authentication<'_>,
181        ancestor: bool,
182    ) -> Result<()> {
183        ensure!(
184            auth.vault == self.genesis.context().vault_id
185                && auth.genesis == self.genesis.commitment(),
186            Unauthorized
187        );
188        let member = self.member(&auth.device).map_err(|_| Unauthorized)?;
189        ensure!(
190            bool::from(member.verifier.ct_eq(&credential_verifier(
191                auth.vault,
192                auth.device,
193                auth.bearer
194            ))),
195            Unauthorized
196        );
197        ensure!(
198            self.heads.contains(&auth.head),
199            "error membership-context-unknown"
200        );
201        if !ancestor && auth.head != self.head() {
202            anyhow::bail!(StaleContext);
203        }
204        Ok(())
205    }
206    pub fn head_at(&self, sequence: u64) -> Option<Hash> {
207        sequence
208            .checked_sub(1)
209            .and_then(|n| self.heads.get(n as usize))
210            .copied()
211    }
212    pub fn validate_key(&self, key: &LocalSharedStatePackageKey) -> Result<()> {
213        check(self.generations.len() == 1)?;
214        check(
215            generation_commitment(self.genesis.context(), key.protected_storage_bytes())
216                == self.genesis.generation_commitment(),
217        )
218    }
219    pub fn contains_head(&self, head: &Hash) -> bool {
220        self.heads.contains(head)
221    }
222    fn member(&self, device: &Hash) -> Result<&Member> {
223        self.members
224            .iter()
225            .find(|m| &m.device == device)
226            .context("error membership-signer")
227    }
228    fn unique(&self, recipient: &Recipient, handle: &Hash) -> Result<()> {
229        check(self.members.len() < MAX_DEVICES && !self.handles.contains(handle))?;
230        check(self.members.iter().chain(&self.retired).all(|m| {
231            m.device != recipient.device && m.sign != recipient.sign && m.hpke != recipient.hpke
232        }))
233    }
234    /// Validate one exact successor without mutating the trusted predecessor.
235    /// Expiry, current bearer authorization and atomic server CAS are separate.
236    pub fn append(&self, declaration: &[u8], request: &[u8], record: &[u8]) -> Result<Self> {
237        let (core, _, _, _) = encoding::components(record)?;
238        let (_, action) = encoding::signer_action(core)?;
239        match action {
240            3 => self.admit(
241                &Declaration::from_record(self, declaration)?,
242                request,
243                record,
244            ),
245            4 | 5 => {
246                check(declaration.is_empty() && request.is_empty())?;
247                self.finish(rotation::validate(self, record)?, record)
248            }
249            _ => anyhow::bail!("error membership-action"),
250        }
251    }
252    /// `append` for an admission whose declaration was already parsed against
253    /// this membership.
254    pub(super) fn admit(
255        &self,
256        declaration: &Declaration,
257        request: &[u8],
258        record: &[u8],
259    ) -> Result<Self> {
260        let recipient = admission::validate(self, declaration, request, record)?;
261        let mut next = self.clone();
262        next.members.push(Member {
263            device: recipient.device,
264            sign: recipient.sign,
265            hpke: recipient.hpke,
266            verifier: recipient.verifier,
267            admission: declaration.handle,
268            admitted_at: self.sequence() + 1,
269        });
270        next.members.sort_by_key(|m| m.device);
271        next.handles.push(declaration.handle);
272        self.finish(next, record)
273    }
274    fn finish(&self, mut next: Self, record: &[u8]) -> Result<Self> {
275        next.heads.push(hash(record));
276        next.capacity()?;
277        Ok(next)
278    }
279    /// Counts alone bound the chain: `MAX_CHAIN_BYTES` is the worst case of
280    /// every record at its canonical maximum within these counts.
281    fn capacity(&self) -> Result<()> {
282        let reserve = usize::from(self.pending);
283        ensure!(
284            self.heads.len() - 1 + reserve <= MAX_TRANSITIONS
285                && self.generations.len() + reserve <= MAX_GENERATIONS,
286            "error membership-change-limit"
287        );
288        Ok(())
289    }
290    /// Room for the costlier outcome of an invitation: withdrawing it with a
291    /// freeze and key rotation, which also covers admitting its device.
292    pub fn ensure_change_capacity(&self) -> Result<()> {
293        ensure!(
294            self.heads.len() - 1 + 2 <= MAX_TRANSITIONS && self.generations.len() < MAX_GENERATIONS,
295            "error membership-change-limit"
296        );
297        Ok(())
298    }
299    pub fn rotation_pending(&self) -> bool {
300        self.pending
301    }
302    pub fn generations(&self) -> &[Generation] {
303        &self.generations
304    }
305    pub fn current_generation(&self) -> &Generation {
306        self.generations.last().expect("genesis generation")
307    }
308    /// Signed interval eligibility, not proof of server acceptance or completeness.
309    pub fn generation_allows(&self, generation: Hash, sequence: u64) -> bool {
310        self.generations
311            .iter()
312            .position(|g| g.id == generation)
313            .is_some_and(|i| {
314                sequence > self.generations[i].starts_after
315                    && self
316                        .generations
317                        .get(i + 1)
318                        .is_none_or(|g| sequence <= g.starts_after)
319            })
320    }
321}
322
323/// A verified ancestor context may be refreshed once; it never authorizes content.
324#[derive(Debug)]
325pub struct StaleContext;
326impl fmt::Display for StaleContext {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        f.write_str("error enrollment-context-stale")
329    }
330}
331impl std::error::Error for StaleContext {}
332
333/// The credential or device may not perform this membership operation.
334#[derive(Debug)]
335pub struct Unauthorized;
336impl fmt::Display for Unauthorized {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        f.write_str("error enrollment-unauthorized")
339    }
340}
341impl std::error::Error for Unauthorized {}
342
343#[derive(serde::Serialize, serde::Deserialize)]
344#[serde(deny_unknown_fields)]
345pub struct ManagementPreparation {
346    pub evidence: Evidence,
347    pub high_water: u64,
348}