Skip to main content

aven_protocol/claim/membership/
pairing.rs

1use super::super::peer::{PeerAuthority, open, request_parts, seal};
2use super::*;
3
4/// Borrowed installation keys, matched against membership before signing.
5pub struct Device<'a> {
6    pub(super) device: Hash,
7    pub(super) signing: &'a Secret,
8    pub(super) recipient: &'a Secret,
9    bearer: &'a Secret,
10}
11impl<'a> Device<'a> {
12    pub fn seed(seed: &'a SeedAuthority) -> Self {
13        Self {
14            device: seed.genesis().device_id(),
15            signing: seed.protected_signing_seed(),
16            recipient: seed.protected_recipient_key(),
17            bearer: seed.bearer(),
18        }
19    }
20    pub(super) fn active<'m>(&self, membership: &'m Membership) -> Result<&'m Member> {
21        let member = membership.member(&self.device)?;
22        let private = HpkePrivate::from_bytes(self.recipient.expose())
23            .map_err(|_| anyhow::anyhow!("error membership-recipient"))?;
24        check(
25            member.sign
26                == SigningKey::from_bytes(self.signing.expose())
27                    .verifying_key()
28                    .to_bytes()
29                && member.hpke.as_slice()
30                    == <Kem as hpke::Kem>::sk_to_pk(&private).to_bytes().as_slice()
31                && bool::from(member.verifier.ct_eq(&credential_verifier(
32                    membership.genesis.context().vault_id,
33                    self.device,
34                    self.bearer,
35                ))),
36        )?;
37        Ok(member)
38    }
39    pub fn validate(&self, membership: &Membership) -> Result<()> {
40        self.active(membership).map(|_| ())
41    }
42    pub fn prepare_invitation(
43        &self,
44        membership: &Membership,
45        expiry: u64,
46    ) -> Result<(Invitation, Declaration)> {
47        self.invitation_with_psk(membership, expiry, Secret::generate()?)
48    }
49    pub(super) fn invitation_with_psk(
50        &self,
51        membership: &Membership,
52        expiry: u64,
53        psk: Secret,
54    ) -> Result<(Invitation, Declaration)> {
55        let member = self.active(membership)?;
56        check(membership.device_count() < MAX_DEVICES)?;
57        membership.ensure_change_capacity()?;
58        let inv = Invitation {
59            vault: membership.genesis.context().vault_id,
60            inviter: member.hpke,
61            psk,
62        };
63        let mut body = b"AVID\0\x02\x01".to_vec();
64        for field in [
65            inv.vault,
66            membership.genesis.commitment(),
67            membership.head(),
68            self.device,
69            inv.inviter,
70            inv.handle(),
71        ] {
72            body.extend(field);
73        }
74        body.extend(expiry.to_be_bytes());
75        let sig = SigningKey::from_bytes(self.signing.expose())
76            .sign(&cce("aven-e2ee/v1/pairing/declaration", &[&body]));
77        let mut raw = vec![1];
78        bytes(&mut raw, &body);
79        bytes(&mut raw, &sig.to_bytes());
80        Ok((inv, Declaration::from_record(membership, &raw)?))
81    }
82    /// Prepare once and retain the exact returned bytes before any dispatch.
83    /// Repreparation is a different candidate, not an exact retry.
84    pub fn prepare_admission(
85        &self,
86        membership: &Membership,
87        declaration: &Declaration,
88        invitation: &Invitation,
89        request: &[u8],
90        keys: &VerifiedKeys,
91    ) -> Result<Vec<u8>> {
92        self.active(membership)?;
93        let d = Declaration::from_record(membership, declaration.record())?;
94        check(
95            d.inviter == self.device
96                && invitation.vault == membership.genesis.context().vault_id
97                && invitation.inviter == d.hpke
98                && invitation.handle() == d.handle,
99        )?;
100        keys.validate(membership)?;
101        let handle = invitation.handle();
102        let (enc, cipher) = request_parts(request, &handle)?;
103        let plain = open(
104            invitation,
105            self.recipient,
106            &cce(
107                "aven-e2ee/v1/pairing/request",
108                &[&invitation.vault, &handle],
109            ),
110            &cce(
111                "aven-e2ee/v1/pairing/request-aad",
112                &[&invitation.vault, &handle, &invitation.inviter],
113            ),
114            enc,
115            cipher,
116        )?;
117        let recipient = Recipient::parse(&plain)?;
118        recipient.verify(&invitation.vault, &handle, &invitation.inviter)?;
119        membership.unique(&recipient, &handle)?;
120        let (state, core) = admission::state_core(membership, &d, request, &recipient);
121        let plain = admission::grant_plaintext(membership, &d, request, &recipient, keys);
122        let info = cce(
123            "aven-e2ee/v1/pairing/grant",
124            &[&invitation.vault, &handle, &hash(request)],
125        );
126        let (enc, cipher) = seal(invitation, &recipient.hpke, &info, &core, &plain)?;
127        let mut attachments = encoding::packages(1);
128        encoding::package(
129            &mut attachments,
130            1,
131            &recipient.device,
132            &recipient.hpke,
133            &enc,
134            &cipher,
135        );
136        let raw = admission::signed(self.signing, &core, &state, &attachments);
137        membership.append(d.record(), request, &raw)?;
138        Ok(raw)
139    }
140}
141
142#[derive(Clone)]
143pub struct Declaration {
144    record: Vec<u8>,
145    pub(super) handle: Hash,
146    pub(super) inviter: Hash,
147    pub(super) hpke: Hash,
148    expiry: u64,
149}
150impl Declaration {
151    pub fn from_record(membership: &Membership, raw: &[u8]) -> Result<Self> {
152        check(raw.len() == DECLARATION_BYTES)?;
153        let mut r = Reader(raw);
154        check(r.take(1)? == [1])?;
155        let body = r.blob(207)?;
156        let signature = r.blob(64)?;
157        r.end()?;
158        let mut b = Reader(body);
159        check(b.take(7)? == b"AVID\0\x02\x01")?;
160        check(
161            b.array::<32>()? == membership.genesis.context().vault_id
162                && b.array::<32>()? == membership.genesis.commitment(),
163        )?;
164        let anchor = b.array::<32>()?;
165        let anchor_sequence = membership
166            .heads
167            .iter()
168            .position(|head| *head == anchor)
169            .context("error membership-anchor")? as u64
170            + 1;
171        let inviter = b.array()?;
172        let hpke = b.array()?;
173        let member = membership.member(&inviter)?;
174        check(member.admitted_at <= anchor_sequence && member.hpke == hpke)?;
175        let handle = b.array()?;
176        let expiry = u64::from_be_bytes(b.array()?);
177        b.end()?;
178        verify(
179            &member.sign,
180            "aven-e2ee/v1/pairing/declaration",
181            &[body],
182            signature,
183        )?;
184        Ok(Self {
185            record: raw.to_vec(),
186            handle,
187            inviter,
188            hpke,
189            expiry,
190        })
191    }
192    pub fn record(&self) -> &[u8] {
193        &self.record
194    }
195    pub fn commitment(&self) -> Hash {
196        hash(&self.record)
197    }
198    pub fn handle(&self) -> Hash {
199        self.handle
200    }
201    pub fn inviter(&self) -> Hash {
202        self.inviter
203    }
204    pub fn expiry(&self) -> u64 {
205        self.expiry
206    }
207}
208
209/// Device ID and signing, recipient and bearer secrets in protected storage.
210const ATTEMPT_KEY_BYTES: usize = 128;
211
212/// Independently generated installation keys and one immutable PSK request.
213/// Hosts must durably own this material before sending anything.
214pub struct Joiner(pub(super) PeerAuthority);
215impl Joiner {
216    pub fn generate(invitation: Invitation) -> Result<Self> {
217        Ok(Self(PeerAuthority::generate(invitation)?))
218    }
219    pub fn protected_storage_bytes(&self) -> Zeroizing<Vec<u8>> {
220        self.0.protected_storage_bytes()
221    }
222    pub fn from_protected_storage(bytes: &[u8]) -> Result<Self> {
223        Ok(Self(PeerAuthority::from_protected_storage(bytes)?))
224    }
225    pub fn matches_invitation(&self, invitation: &Invitation) -> bool {
226        self.0.matches_invitation(invitation)
227    }
228    /// Another request from these unchanged installation keys, for a
229    /// replacement invitation to the same vault from the same inviter.
230    pub fn retry(&self, invitation: Invitation) -> Result<Self> {
231        self.same_context(&invitation)?;
232        ensure!(
233            !self.matches_invitation(&invitation),
234            "error enrollment-retry-context"
235        );
236        Ok(Self(self.0.retry(invitation)?))
237    }
238    /// The invitation and exact request of this attempt, without the keys.
239    pub fn attempt_bytes(&self) -> Zeroizing<Vec<u8>> {
240        Zeroizing::new(self.protected_storage_bytes()[ATTEMPT_KEY_BYTES..].to_vec())
241    }
242    /// Rebuilds a retained attempt from `attempt_bytes` over these keys, so
243    /// every attempt shares this installation authority by construction.
244    pub fn attempt(&self, bytes: &[u8]) -> Result<Self> {
245        let mut raw = Zeroizing::new(self.protected_storage_bytes()[..ATTEMPT_KEY_BYTES].to_vec());
246        raw.extend_from_slice(bytes);
247        let attempt = Self::from_protected_storage(&raw)?;
248        self.same_context(&attempt.0.invitation)?;
249        Ok(attempt)
250    }
251    fn same_context(&self, invitation: &Invitation) -> Result<()> {
252        ensure!(
253            invitation.vault == self.0.invitation.vault
254                && invitation.inviter == self.0.invitation.inviter,
255            "error enrollment-retry-context"
256        );
257        Ok(())
258    }
259    pub fn vault(&self) -> Hash {
260        self.0.vault()
261    }
262    pub fn handle(&self) -> Hash {
263        self.0.handle()
264    }
265    pub fn device(&self) -> Hash {
266        self.0.device()
267    }
268    pub fn bearer(&self) -> &Secret {
269        self.0.bearer()
270    }
271    pub fn request(&self) -> &[u8] {
272        self.0.request()
273    }
274    pub fn authority(&self) -> Device<'_> {
275        Device {
276            device: self.0.device,
277            signing: &self.0.signing,
278            recipient: &self.0.recipient,
279            bearer: &self.0.bearer,
280        }
281    }
282    /// PSK-authenticated expectations only. No membership or content authority.
283    pub fn open_provisional(&self, declaration: &[u8], record: &[u8]) -> Result<ProvisionalGrant> {
284        check(declaration.len() == DECLARATION_BYTES && record.len() <= MAX_RECORD_BYTES)?;
285        let (core, _, attachments, _) = encoding::components(record)?;
286        let recipient = self.0.recipient()?;
287        // Public framing bounds ciphertext; trust comes only from the PSK opening.
288        let mut r = Reader(attachments);
289        check(r.take(8)? == b"AVGA\0\x05\0\x01" && r.take(2)? == [1, 1])?;
290        check(r.blob(32)? == recipient.device && r.blob(32)? == recipient.hpke)?;
291        let enc = r.blob(32)?;
292        let cipher = encoding::blob(&mut r, MAX_KEY_PLAINTEXT_BYTES + 16)?;
293        r.end()?;
294        let info = cce(
295            "aven-e2ee/v1/pairing/grant",
296            &[&self.0.vault(), &self.0.handle(), &hash(self.request())],
297        );
298        let plain = open(
299            &self.0.invitation,
300            &self.0.recipient,
301            &info,
302            core,
303            enc,
304            cipher,
305        )?;
306        check(plain.len() >= GRANT_PREFIX_BYTES + 2 && plain.len() <= MAX_KEY_PLAINTEXT_BYTES)?;
307        let mut r = Reader(&plain);
308        check(
309            r.take(1)? == [3]
310                && r.array::<32>()? == self.0.vault()
311                && r.array::<32>()? == self.0.handle()
312                && r.array::<32>()? == hash(declaration)
313                && r.array::<32>()? == hash(self.request())
314                && r.array::<32>()? == self.device(),
315        )?;
316        let genesis = r.array()?;
317        let publication = r.array()?;
318        let predecessor = r.array()?;
319        r.take(64)?;
320        let descriptor = r.array()?;
321        Ok(ProvisionalGrant {
322            plaintext: plain,
323            genesis,
324            publication,
325            predecessor,
326            descriptor,
327            outcome: hash(record),
328        })
329    }
330    pub fn verify_enrollment(
331        &self,
332        predecessor: &Membership,
333        declaration: &[u8],
334        record: &[u8],
335    ) -> Result<VerifiedEnrollment> {
336        let grant = self.open_provisional(declaration, record)?;
337        let d = Declaration::from_record(predecessor, declaration)?;
338        let membership = predecessor.admit(&d, self.request(), record)?;
339        check(d.hpke == self.0.invitation.inviter)?;
340        let recipient = self.0.recipient()?;
341        let member = membership.member(&self.device())?;
342        check(
343            member.sign == recipient.sign
344                && member.hpke == recipient.hpke
345                && member.verifier == recipient.verifier
346                && member.admission == d.handle,
347        )?;
348        let mut r = Reader(&grant.plaintext[GRANT_PREFIX_BYTES..]);
349        let keys = VerifiedKeys::read(predecessor, &mut r)?;
350        let expected =
351            admission::grant_plaintext(predecessor, &d, self.request(), &recipient, &keys);
352        check(bool::from(
353            grant.plaintext.as_slice().ct_eq(expected.as_slice()),
354        ))?;
355        Ok(VerifiedEnrollment { membership, keys })
356    }
357}
358
359/// Grant pins cannot prove relay commit, current authorization or freshness.
360pub struct ProvisionalGrant {
361    plaintext: Zeroizing<Vec<u8>>,
362    pub genesis: Hash,
363    pub publication: Hash,
364    pub predecessor: Hash,
365    pub descriptor: Hash,
366    pub outcome: Hash,
367}
368/// Verified historical enrollment and complete key coverage, not install readiness.
369pub struct VerifiedEnrollment {
370    membership: Membership,
371    keys: VerifiedKeys,
372}
373impl VerifiedEnrollment {
374    pub fn genesis(&self) -> &Genesis {
375        self.membership.genesis()
376    }
377    pub fn publication(&self) -> &Publication {
378        self.membership.publication()
379    }
380    pub fn checkpoint(&self) -> Hash {
381        self.membership.head()
382    }
383    pub fn membership(&self) -> &Membership {
384        &self.membership
385    }
386    pub fn keys(&self) -> &VerifiedKeys {
387        &self.keys
388    }
389    pub fn key(&self) -> &LocalSharedStatePackageKey {
390        self.keys
391            .key(self.membership.genesis.context().generation_id)
392            .expect("verified bootstrap coverage")
393    }
394}