1use std::fmt;
12
13use anyhow::{Context, Result, ensure};
14use chacha20::ChaCha20Rng;
15use ed25519_dalek::{Signer, SigningKey};
16use hpke::{Deserializable, OpModeR, OpModeS, Serializable};
17use rand_core::SeedableRng;
18use subtle::ConstantTimeEq;
19use zeroize::Zeroizing;
20
21use crate::context::{LocalSharedStatePackageContext, LocalSharedStatePackageKey};
22pub mod codec;
23
24use codec::{MEMBERSHIP_SIGN, Reader, bytes, cce, hash, membership_core, valid};
25
26type Kem = hpke::kem::X25519HkdfSha256;
27type Aead = hpke::aead::ChaCha20Poly1305;
28type Kdf = hpke::kdf::HkdfSha256;
29type HpkePrivate = <Kem as hpke::Kem>::PrivateKey;
30type Enc = <Kem as hpke::Kem>::EncappedKey;
31
32pub const GENESIS_BYTES: usize = 902;
33pub const CLAIM_BYTES: usize = 912;
34pub const SEED_STORAGE_BYTES: usize = 96 + GENESIS_BYTES;
36
37pub struct Secret(Zeroizing<[u8; 32]>);
39
40impl Secret {
41 pub fn new(bytes: [u8; 32]) -> Self {
42 Self(Zeroizing::new(bytes))
43 }
44
45 pub fn generate() -> Result<Self> {
46 let mut bytes = Zeroizing::new([0; 32]);
47 getrandom::fill(bytes.as_mut()).context("error seed-claim-entropy-unavailable")?;
48 Ok(Self(bytes))
49 }
50
51 pub fn expose(&self) -> &[u8; 32] {
53 &self.0
54 }
55}
56
57impl fmt::Debug for Secret {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 f.write_str("Secret([REDACTED])")
60 }
61}
62
63#[derive(Clone, PartialEq, Eq)]
65pub struct Genesis {
66 record: [u8; GENESIS_BYTES],
67 context: LocalSharedStatePackageContext,
68 setup: [u8; 32],
69 claim: [u8; 32],
70 device: [u8; 32],
71 signing_public: [u8; 32],
72 hpke_public: [u8; 32],
73 verifier: [u8; 32],
74 generation_commitment: [u8; 32],
75}
76
77impl fmt::Debug for Genesis {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 f.debug_struct("Genesis")
80 .field("commitment", &self.commitment())
81 .finish()
82 }
83}
84
85impl Genesis {
86 pub fn from_claim(bytes: &[u8]) -> Result<Self> {
87 Self::from_record(codec::claim_record(bytes)?)
88 }
89
90 pub fn from_record(record: &[u8]) -> Result<Self> {
91 codec::parse(record)
92 }
93
94 pub fn record(&self) -> &[u8; GENESIS_BYTES] {
95 &self.record
96 }
97
98 pub fn commitment(&self) -> [u8; 32] {
99 hash(&self.record)
100 }
101
102 pub fn authorizes_bearer(&self, token: &Secret) -> bool {
103 bool::from(
104 credential_verifier(self.context.vault_id, self.device, token).ct_eq(&self.verifier),
105 )
106 }
107
108 pub fn context(&self) -> LocalSharedStatePackageContext {
109 self.context
110 }
111
112 pub fn setup_id(&self) -> [u8; 32] {
113 self.setup
114 }
115
116 pub fn claim_id(&self) -> [u8; 32] {
117 self.claim
118 }
119
120 pub fn device_id(&self) -> [u8; 32] {
121 self.device
122 }
123
124 pub fn signing_public(&self) -> [u8; 32] {
125 self.signing_public
126 }
127
128 pub fn hpke_public(&self) -> [u8; 32] {
129 self.hpke_public
130 }
131
132 pub fn verifier(&self) -> [u8; 32] {
133 self.verifier
134 }
135
136 pub fn generation_commitment(&self) -> [u8; 32] {
137 self.generation_commitment
138 }
139
140 pub fn claim_bytes(&self) -> Vec<u8> {
141 let mut out = b"AVCL\0\x01".to_vec();
142 bytes(&mut out, &self.record);
143 out
144 }
145
146 fn info(&self) -> Vec<u8> {
147 cce(
148 "aven-e2ee/v1/genesis/self",
149 &[
150 &1_u16.to_be_bytes(),
151 &[1],
152 &self.context.vault_id,
153 &self.setup,
154 &self.claim,
155 &self.device,
156 &self.hpke_public,
157 &self.context.generation_id,
158 ],
159 )
160 }
161
162 fn state(&self) -> Vec<u8> {
163 let mut out = b"AVGS\0\x01\x01".to_vec();
164 out.extend(self.context.vault_id);
165 out.extend([0, 0, 0, 1]);
166 out.extend(self.device);
167 out.extend(self.signing_public);
168 out.extend(self.hpke_public);
169 out.extend(self.verifier);
170 out.extend(1_u32.to_be_bytes());
171 out.extend(self.claim);
172 out.push(1);
173 out.extend(self.context.generation_id);
174 out.extend(self.generation_commitment);
175 out.extend(0_u64.to_be_bytes());
176 out
177 }
178
179 fn core(&self, state: &[u8]) -> Vec<u8> {
180 let mut body = b"AVGC\0\x01".to_vec();
181 body.extend(self.setup);
182 body.extend(self.claim);
183 membership_core(
184 &self.context.vault_id,
185 0,
186 &[0; 32],
187 &self.device,
188 1,
189 &body,
190 state,
191 )
192 }
193}
194
195pub struct SeedAuthority {
198 signing: Secret,
199 recipient: Secret,
200 bearer: Secret,
201 genesis: Genesis,
202}
203
204impl fmt::Debug for SeedAuthority {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 f.write_str("SeedAuthority([REDACTED])")
207 }
208}
209
210impl SeedAuthority {
211 pub fn generate(
212 context: LocalSharedStatePackageContext,
213 key: &LocalSharedStatePackageKey,
214 setup: [u8; 32],
215 ) -> Result<Self> {
216 let signing = Secret::generate()?;
217 let ikm = Secret::generate()?;
218 let (recipient, _) = <Kem as hpke::Kem>::derive_keypair(ikm.expose());
219 let bearer = Secret::generate()?;
220 let claim = *Secret::generate()?.expose();
221 let device = *Secret::generate()?.expose();
222 let entropy = Secret::generate()?;
223 let mut rng = ChaCha20Rng::from_seed(*entropy.expose());
224 Self::build(
225 context, key, setup, claim, device, signing, recipient, bearer, &mut rng,
226 )
227 }
228
229 #[allow(clippy::too_many_arguments)]
230 fn build(
231 context: LocalSharedStatePackageContext,
232 key: &LocalSharedStatePackageKey,
233 setup: [u8; 32],
234 claim: [u8; 32],
235 device: [u8; 32],
236 signing: Secret,
237 recipient: HpkePrivate,
238 bearer: Secret,
239 rng: &mut impl rand_core::CryptoRng,
240 ) -> Result<Self> {
241 let signer = SigningKey::from_bytes(signing.expose());
242 let hpke_public = <Kem as hpke::Kem>::sk_to_pk(&recipient);
243 let mut genesis = Genesis {
244 record: [0; GENESIS_BYTES],
245 context,
246 setup,
247 claim,
248 device,
249 signing_public: signer.verifying_key().to_bytes(),
250 hpke_public: hpke_public.to_bytes().into(),
251 verifier: credential_verifier(context.vault_id, device, &bearer),
252 generation_commitment: generation_commitment(context, key.protected_storage_bytes()),
253 };
254 let state = genesis.state();
255 let core = genesis.core(&state);
256 let plaintext = self_plaintext(&genesis, key.protected_storage_bytes());
257 let (enc, ciphertext) = hpke::single_shot_seal_with_rng::<Aead, Kdf, Kem>(
258 &OpModeS::Base,
259 &hpke_public,
260 &genesis.info(),
261 &plaintext,
262 &core,
263 rng,
264 )
265 .map_err(|_| anyhow::anyhow!("error seed-claim-self-seal"))?;
266 let mut attachments = b"AVGA\0\x01\x01\x01\x01".to_vec();
267 bytes(&mut attachments, &device);
268 bytes(&mut attachments, &genesis.hpke_public);
269 bytes(&mut attachments, &enc.to_bytes());
270 bytes(&mut attachments, &ciphertext);
271 let signature = signer.sign(&cce(MEMBERSHIP_SIGN, &[&core, &attachments]));
272 let mut record = vec![1];
273 for component in [&core[..], &state, &attachments, &signature.to_bytes()] {
274 bytes(&mut record, component);
275 }
276 genesis = Genesis::from_record(&record)?;
277 let authority = Self {
278 signing,
279 recipient: Secret::new(recipient.to_bytes().into()),
280 bearer,
281 genesis,
282 };
283 authority.validate(context, key)?;
284 Ok(authority)
285 }
286
287 pub fn genesis(&self) -> &Genesis {
288 &self.genesis
289 }
290
291 pub fn protected_signing_seed(&self) -> &Secret {
294 &self.signing
295 }
296
297 pub fn protected_recipient_key(&self) -> &Secret {
300 &self.recipient
301 }
302
303 pub fn bearer(&self) -> &Secret {
304 &self.bearer
305 }
306
307 pub fn protected_storage_bytes(&self) -> Zeroizing<Vec<u8>> {
309 let mut out = Zeroizing::new(Vec::with_capacity(SEED_STORAGE_BYTES));
310 out.extend_from_slice(self.signing.expose());
311 out.extend_from_slice(self.recipient.expose());
312 out.extend_from_slice(self.bearer.expose());
313 out.extend_from_slice(self.genesis.record());
314 out
315 }
316
317 pub fn from_protected_storage(
318 raw: &[u8],
319 context: LocalSharedStatePackageContext,
320 key: &LocalSharedStatePackageKey,
321 ) -> Result<Self> {
322 valid(raw.len() == SEED_STORAGE_BYTES)?;
323 let mut reader = Reader(raw);
324 let signing = Secret::new(reader.array()?);
325 let recipient = Secret::new(reader.array()?);
326 let bearer = Secret::new(reader.array()?);
327 let genesis = Genesis::from_record(reader.take(GENESIS_BYTES)?)?;
328 reader.end()?;
329 let authority = Self {
330 signing,
331 recipient,
332 bearer,
333 genesis,
334 };
335 authority.validate(context, key)?;
336 Ok(authority)
337 }
338
339 pub fn validate(
340 &self,
341 context: LocalSharedStatePackageContext,
342 key: &LocalSharedStatePackageKey,
343 ) -> Result<()> {
344 let g = &self.genesis;
345 valid(g.context == context)?;
346 valid(
347 SigningKey::from_bytes(self.signing.expose())
348 .verifying_key()
349 .to_bytes()
350 == g.signing_public,
351 )?;
352 valid(bool::from(
353 credential_verifier(context.vault_id, g.device, &self.bearer).ct_eq(&g.verifier),
354 ))?;
355 valid(
356 generation_commitment(context, key.protected_storage_bytes())
357 == g.generation_commitment,
358 )?;
359 let private = HpkePrivate::from_bytes(self.recipient.expose())
360 .map_err(|_| anyhow::anyhow!("error seed-claim-recipient"))?;
361 valid(<Kem as hpke::Kem>::sk_to_pk(&private).to_bytes().as_slice() == g.hpke_public)?;
362 let (core, _, attachments, _) = codec::components(&g.record)?;
363 let mut reader = Reader(attachments);
364 reader.take(9)?;
365 reader.blob(32)?;
366 reader.blob(32)?;
367 let enc = Enc::from_bytes(reader.blob(32)?)
368 .map_err(|_| anyhow::anyhow!("error seed-claim-encapsulation"))?;
369 let ciphertext = reader.blob(191)?;
370 reader.end()?;
371 let plaintext = Zeroizing::new(
372 hpke::single_shot_open::<Aead, Kdf, Kem>(
373 &OpModeR::Base,
374 &private,
375 &enc,
376 &g.info(),
377 ciphertext,
378 core,
379 )
380 .map_err(|_| anyhow::anyhow!("error seed-claim-self-open"))?,
381 );
382 valid(bool::from(
383 plaintext
384 .as_slice()
385 .ct_eq(&self_plaintext(g, key.protected_storage_bytes())),
386 ))
387 }
388}
389
390fn self_plaintext(g: &Genesis, secret: &[u8; 32]) -> Zeroizing<Vec<u8>> {
391 let mut out = Zeroizing::new(b"AVGK\0\x01\x01".to_vec());
392 for value in [
393 &g.context.vault_id,
394 &g.claim,
395 &g.device,
396 &g.context.generation_id,
397 secret,
398 ] {
399 out.extend_from_slice(value);
400 }
401 out.extend_from_slice(&0_u64.to_be_bytes());
402 out
403}
404
405pub fn generation_commitment(
406 context: LocalSharedStatePackageContext,
407 secret: &[u8; 32],
408) -> [u8; 32] {
409 let message = Zeroizing::new(cce(
410 "aven-e2ee/v1/generation/commit",
411 &[&context.vault_id, &context.generation_id, secret],
412 ));
413 hash(&message)
414}
415
416pub fn credential_verifier(vault: [u8; 32], device: [u8; 32], token: &Secret) -> [u8; 32] {
417 let message = Zeroizing::new(cce(
418 "aven-e2ee/v1/credential/verifier",
419 &[&vault, &device, &1_u32.to_be_bytes(), token.expose()],
420 ));
421 hash(&message)
422}
423
424#[derive(Clone, Debug)]
426pub struct SetupAuthority {
427 id: [u8; 32],
428 verifier: [u8; 32],
429}
430
431impl SetupAuthority {
432 pub fn from_verifier(id: [u8; 32], verifier: [u8; 32]) -> Self {
433 Self { id, verifier }
434 }
435
436 pub fn id(&self) -> [u8; 32] {
437 self.id
438 }
439
440 pub fn verifier(id: [u8; 32], secret: &Secret) -> [u8; 32] {
441 let message = Zeroizing::new(cce("aven-e2ee/v1/setup/verifier", &[&id, secret.expose()]));
442 hash(&message)
443 }
444
445 pub fn authorizes(&self, id: [u8; 32], secret: &Secret) -> bool {
446 self.id == id && bool::from(Self::verifier(id, secret).ct_eq(&self.verifier))
447 }
448}
449
450#[derive(Debug)]
451pub enum ClaimAuthentication<'a> {
452 SetupSecret(&'a Secret),
453 SeedBearer(&'a Secret),
454}
455
456#[derive(Clone, Copy, Debug, PartialEq, Eq)]
459pub enum ClaimRefusal {
460 Unauthorized { claimed: bool },
463 Expired,
466 Conflict,
468 Retired,
470}
471
472impl fmt::Display for ClaimRefusal {
473 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474 f.write_str(match self {
475 Self::Unauthorized { .. } => "error seed-claim-unauthorized",
476 Self::Expired => "error seed-claim-setup-expired",
477 Self::Conflict => "error seed-claim-conflict",
478 Self::Retired => "error seed-claim-retired",
479 })
480 }
481}
482
483impl std::error::Error for ClaimRefusal {}
484
485#[derive(Debug)]
487pub struct StorageNotEmpty;
488
489impl fmt::Display for StorageNotEmpty {
490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 f.write_str("error e2ee-server-storage-not-empty")
492 }
493}
494
495impl std::error::Error for StorageNotEmpty {}
496
497#[derive(Clone, Debug, PartialEq, Eq)]
499pub struct ClaimResult {
500 pub vault_id: [u8; 32],
501 pub claim_id: [u8; 32],
502 pub genesis_commitment: [u8; 32],
503}
504
505impl ClaimResult {
506 pub fn from_genesis(g: &Genesis) -> Self {
507 Self {
508 vault_id: g.context.vault_id,
509 claim_id: g.claim,
510 genesis_commitment: g.commitment(),
511 }
512 }
513
514 pub fn validate_pinned(&self, pinned: &Genesis) -> Result<()> {
515 ensure!(
516 *self == Self::from_genesis(pinned),
517 "error seed-claim-result-mismatch"
518 );
519 Ok(())
520 }
521}
522
523#[cfg(test)]
524mod tests;
525
526pub mod membership;
527pub mod peer;
528pub mod publication;
529pub use publication::{PUBLICATION_BYTES, Publication, PublicationBinding, PublicationOutcome};