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