Skip to main content

aven_protocol/claim/membership/
rotation.rs

1//! Revoke/freeze and signed generation cutoffs with base-mode recipient packages.
2use super::*;
3
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct Generation {
6    pub id: Hash,
7    pub commitment: Hash,
8    pub starts_after: u64,
9}
10const ROTATION_PLAINTEXT_BYTES: usize = 176;
11const ROTATION_KEY_OFFSET: usize = 6 + 3 * 32 + 2 + 32;
12
13/// Fresh per-candidate material, owned in protected storage before package creation.
14pub struct RotationMaterial {
15    generation: Hash,
16    key: LocalSharedStatePackageKey,
17    entropy: Secret,
18}
19impl RotationMaterial {
20    pub fn generate() -> Result<Self> {
21        Ok(Self {
22            generation: *Secret::generate()?.expose(),
23            key: LocalSharedStatePackageKey::new(*Secret::generate()?.expose()),
24            entropy: Secret::generate()?,
25        })
26    }
27    pub fn protected_storage_bytes(&self) -> Zeroizing<Vec<u8>> {
28        let mut bytes = Zeroizing::new(b"AVRM\0\x01".to_vec());
29        bytes.extend(self.generation);
30        bytes.extend(self.key.protected_storage_bytes());
31        bytes.extend(self.entropy.expose());
32        bytes
33    }
34    pub fn validate_generation(&self, membership: &Membership) -> Result<()> {
35        let generation = membership.current_generation();
36        check(
37            generation.id == self.generation
38                && generation.commitment
39                    == generation_commitment(
40                        LocalSharedStatePackageContext {
41                            vault_id: membership.genesis.context().vault_id,
42                            generation_id: self.generation,
43                        },
44                        self.key.protected_storage_bytes(),
45                    ),
46        )
47    }
48    pub fn from_protected_storage(bytes: &[u8]) -> Result<Self> {
49        check(bytes.len() == 102)?;
50        let mut r = Reader(bytes);
51        check(r.take(6)? == b"AVRM\0\x01")?;
52        Ok(Self {
53            generation: r.array()?,
54            key: LocalSharedStatePackageKey::new(r.array()?),
55            entropy: Secret::new(r.array()?),
56        })
57    }
58}
59fn revoke(m: &Membership, targets: &[Hash]) -> Result<Membership> {
60    check(targets.len() < m.members.len() && targets.windows(2).all(|w| w[0] < w[1]))?;
61    let mut next = m.clone();
62    for target in targets {
63        let member = m.member(target)?;
64        next.retired.push(member.clone());
65    }
66    next.members
67        .retain(|member| !targets.contains(&member.device));
68    next.pending = true;
69    Ok(next)
70}
71fn rotate(m: &Membership, generation: Generation) -> Result<Membership> {
72    check(
73        m.pending
74            && m.generations.len() < MAX_GENERATIONS
75            && !m.generations.iter().any(|g| g.id == generation.id)
76            && generation.starts_after >= m.publication.binding().prefix_count
77            && generation.starts_after >= m.current_generation().starts_after
78            && generation.starts_after <= MAX_CUTOFF,
79    )?;
80    let mut next = m.clone();
81    next.generations.push(generation);
82    next.pending = false;
83    Ok(next)
84}
85pub(super) fn validate(m: &Membership, raw: &[u8]) -> Result<Membership> {
86    let (core, state, attachments, signature) = encoding::components(raw)?;
87    let mut r = Reader(core);
88    check(r.take(1)? == [1] && r.blob(32)? == m.genesis.context().vault_id)?;
89    check(u64::from_be_bytes(r.array()?) == m.sequence() + 1 && r.blob(32)? == m.head())?;
90    check(r.take(1)? == [1])?;
91    let signer: Hash = r.blob(32)?.try_into()?;
92    let member = m.member(&signer)?;
93    let action = r.take(1)?[0];
94    let body = encoding::blob(&mut r, 8 + MAX_DEVICES * 32)?;
95    r.blob(32)?;
96    r.end()?;
97    verify(
98        &member.sign,
99        MEMBERSHIP_SIGN,
100        &[core, attachments],
101        signature,
102    )?;
103    let mut b = Reader(body);
104    let next = match action {
105        4 => {
106            check(b.take(6)? == b"AVRV\0\x01")?;
107            let count = u16::from_be_bytes(b.array()?) as usize;
108            check(count <= MAX_DEVICES)?;
109            let targets = (0..count)
110                .map(|_| b.array())
111                .collect::<Result<Vec<Hash>>>()?;
112            encoding::read_packages(attachments, 0, 0, 0)?;
113            revoke(m, &targets)?
114        }
115        5 => {
116            check(
117                b.take(6)? == b"AVRT\0\x01"
118                    && b.array::<32>()? == m.publication.binding().stream_id,
119            )?;
120            let next = rotate(
121                m,
122                Generation {
123                    id: b.array()?,
124                    commitment: b.array()?,
125                    starts_after: u64::from_be_bytes(b.array()?),
126                },
127            )?;
128            let packages = encoding::read_packages(
129                attachments,
130                0,
131                next.members.len(),
132                ROTATION_PLAINTEXT_BYTES,
133            )?;
134            for (p, member) in packages.iter().zip(&next.members) {
135                check(p.device == member.device && p.public == member.hpke)?;
136            }
137            next
138        }
139        _ => anyhow::bail!("error membership-action"),
140    };
141    b.end()?;
142    check(
143        state == encoding::state(&next) && core == encoding::core(m, signer, action, body, state),
144    )?;
145    Ok(next)
146}
147pub(super) fn info(m: &Membership, member: &Member) -> Vec<u8> {
148    cce(
149        "aven-e2ee/v1/membership/rotation-key",
150        &[
151            &m.genesis.context().vault_id,
152            &[1],
153            &member.device,
154            &member.hpke,
155        ],
156    )
157}
158pub(super) fn plaintext(
159    m: &Membership,
160    member: &Member,
161    g: &Generation,
162    key: &LocalSharedStatePackageKey,
163) -> Zeroizing<Vec<u8>> {
164    let mut out = Zeroizing::new(b"AVRK\0\x01".to_vec());
165    out.extend(m.genesis.context().vault_id);
166    out.extend(m.genesis.commitment());
167    out.extend(member.device);
168    out.extend(1_u16.to_be_bytes());
169    out.extend(g.id);
170    out.extend(key.protected_storage_bytes());
171    out.extend(g.starts_after.to_be_bytes());
172    out
173}
174impl Device<'_> {
175    /// Sorted active targets; an empty list requests rotation without removal.
176    /// Exact bytes must be retained before dispatch. No server commit is implied.
177    pub fn prepare_revoke(&self, m: &Membership, targets: &[Hash]) -> Result<Vec<u8>> {
178        self.active(m)?;
179        let next = revoke(m, targets)?;
180        let mut body = b"AVRV\0\x01".to_vec();
181        body.extend((targets.len() as u16).to_be_bytes());
182        for target in targets {
183            body.extend(target);
184        }
185        let state = encoding::state(&next);
186        let core = encoding::core(m, self.device, 4, &body, &state);
187        let raw = admission::signed(self.signing, &core, &state, &encoding::packages(0));
188        m.append(&[], &[], &raw)?;
189        Ok(raw)
190    }
191    /// A new candidate always generates independent key and generation identities.
192    /// The cutoff is a proposal; server admission must compare its frozen allocator.
193    #[cfg(any(test, feature = "test-support"))]
194    pub fn prepare_rotation(
195        &self,
196        m: &Membership,
197        keys: &VerifiedKeys,
198        cutoff: u64,
199    ) -> Result<Vec<u8>> {
200        self.prepare_rotation_with(m, keys, cutoff, &RotationMaterial::generate()?)
201    }
202    /// Replay one protected candidate. Encapsulation randomness is bound to the
203    /// predecessor, signer, and cutoff, so retries are byte-identical while a
204    /// changed context gets independent HPKE encapsulations. Replacements still
205    /// require fresh material for independent generation keys.
206    pub fn prepare_rotation_with(
207        &self,
208        m: &Membership,
209        keys: &VerifiedKeys,
210        cutoff: u64,
211        material: &RotationMaterial,
212    ) -> Result<Vec<u8>> {
213        let genesis = m.genesis.commitment();
214        let predecessor = m.head();
215        let sequence = m.sequence().to_be_bytes();
216        let stream = m.publication.binding().stream_id;
217        let cutoff_bytes = cutoff.to_be_bytes();
218        let commitment = generation_commitment(
219            LocalSharedStatePackageContext {
220                vault_id: m.genesis.context().vault_id,
221                generation_id: material.generation,
222            },
223            material.key.protected_storage_bytes(),
224        );
225        let context = cce(
226            "aven-e2ee/v1/membership/rotation-rng/context",
227            &[
228                &genesis,
229                &predecessor,
230                &sequence,
231                &stream,
232                &self.device,
233                &cutoff_bytes,
234                &material.generation,
235                &commitment,
236            ],
237        );
238        let kdf = hkdf::Hkdf::<sha2::Sha256>::new(
239            Some(b"aven-e2ee/v1/membership/rotation-rng"),
240            material.entropy.expose(),
241        );
242        let mut seed = Zeroizing::new([0; 32]);
243        kdf.expand(&context, seed.as_mut())
244            .expect("fixed HKDF length");
245        self.rotation_with(
246            m,
247            keys,
248            material.generation,
249            &material.key,
250            cutoff,
251            &mut ChaCha20Rng::from_seed(*seed),
252        )
253    }
254    pub(super) fn rotation_with(
255        &self,
256        m: &Membership,
257        keys: &VerifiedKeys,
258        id: Hash,
259        key: &LocalSharedStatePackageKey,
260        cutoff: u64,
261        rng: &mut impl rand_core::CryptoRng,
262    ) -> Result<Vec<u8>> {
263        self.active(m)?;
264        keys.validate(m)?;
265        let g = Generation {
266            id,
267            commitment: generation_commitment(
268                LocalSharedStatePackageContext {
269                    vault_id: m.genesis.context().vault_id,
270                    generation_id: id,
271                },
272                key.protected_storage_bytes(),
273            ),
274            starts_after: cutoff,
275        };
276        let next = rotate(m, g.clone())?;
277        let mut body = b"AVRT\0\x01".to_vec();
278        body.extend(m.publication.binding().stream_id);
279        body.extend(g.id);
280        body.extend(g.commitment);
281        body.extend(cutoff.to_be_bytes());
282        let state = encoding::state(&next);
283        let core = encoding::core(m, self.device, 5, &body, &state);
284        let mut attachments = encoding::packages(next.members.len());
285        for member in &next.members {
286            let public = <Kem as hpke::Kem>::PublicKey::from_bytes(&member.hpke)
287                .map_err(|_| anyhow::anyhow!("error membership-recipient"))?;
288            let (enc, cipher) = hpke::single_shot_seal_with_rng::<Aead, Kdf, Kem>(
289                &OpModeS::Base,
290                &public,
291                &info(m, member),
292                &plaintext(m, member, &g, key),
293                &core,
294                rng,
295            )
296            .map_err(|_| anyhow::anyhow!("error membership-package-seal"))?;
297            encoding::package(
298                &mut attachments,
299                0,
300                &member.device,
301                &member.hpke,
302                &enc.to_bytes(),
303                &cipher,
304            );
305        }
306        let raw = admission::signed(self.signing, &core, &state, &attachments);
307        self.receive_rotation(m, &raw, keys)?;
308        Ok(raw)
309    }
310    /// Validates the entire transition and own package before yielding full coverage.
311    /// Other recipients' encrypted plaintext cannot be validated by this device.
312    pub fn receive_rotation(
313        &self,
314        before: &Membership,
315        raw: &[u8],
316        keys: &VerifiedKeys,
317    ) -> Result<VerifiedKeys> {
318        self.active(before)?;
319        keys.validate(before)?;
320        let next = before.append(&[], &[], raw)?;
321        check(next.generations.len() == before.generations.len() + 1)?;
322        let member = self.active(&next)?;
323        let (core, _, attachments, _) = encoding::components(raw)?;
324        let packages =
325            encoding::read_packages(attachments, 0, next.members.len(), ROTATION_PLAINTEXT_BYTES)?;
326        let p = packages
327            .iter()
328            .find(|p| p.device == self.device)
329            .context("error membership-package-missing")?;
330        let private = HpkePrivate::from_bytes(self.recipient.expose())
331            .map_err(|_| anyhow::anyhow!("error membership-recipient"))?;
332        let enc =
333            Enc::from_bytes(p.enc).map_err(|_| anyhow::anyhow!("error membership-package-enc"))?;
334        let plain = Zeroizing::new(
335            hpke::single_shot_open::<Aead, Kdf, Kem>(
336                &OpModeR::Base,
337                &private,
338                &enc,
339                &info(before, member),
340                p.cipher,
341                core,
342            )
343            .map_err(|_| anyhow::anyhow!("error membership-package-open"))?,
344        );
345        check(plain.len() == ROTATION_PLAINTEXT_BYTES)?;
346        let key = LocalSharedStatePackageKey::new(
347            plain[ROTATION_KEY_OFFSET..ROTATION_KEY_OFFSET + 32].try_into()?,
348        );
349        check(bool::from(plain.as_slice().ct_eq(&plaintext(
350            before,
351            member,
352            next.current_generation(),
353            &key,
354        ))))?;
355        keys.extended(&next, key)
356    }
357}