1pub(crate) mod catalog;
62pub(crate) mod codec;
63mod domain;
64#[cfg(feature = "test-support")]
65pub(crate) mod fuzz;
66
67pub use codec::MAX_DESCRIPTOR_BYTES;
68
69pub const DOMAIN_VERSION: u32 = 1;
71pub mod download;
72mod projection;
73pub(crate) mod staging;
74
75use super::super::{NeverDispatchedLocalSharedCapture, SharedStateCapture};
76use super::{self as crypto, LocalSharedStatePackageKey};
77use catalog::{Artifact, Declaration, Image, Images};
78use codec::*;
79use zeroize::Zeroizing;
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum Error {
84 Invalid,
85 ResourceLimit,
86 Authentication,
87 Unsupported,
88}
89impl std::fmt::Display for Error {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.write_str(match self {
92 Self::Invalid => "invalid bootstrap representation",
93 Self::ResourceLimit => "bootstrap codec resource limit exceeded",
94 Self::Authentication => "bootstrap authentication failed",
95 Self::Unsupported => "unsupported bootstrap format version",
96 })
97 }
98}
99impl std::error::Error for Error {}
100type Result<T> = std::result::Result<T, Error>;
101
102#[derive(Clone, PartialEq, Eq)]
106pub struct Package {
107 pub descriptor: Vec<u8>,
108 pub catalogs: [Vec<u8>; 3],
109 pub state: Vec<Vec<u8>>,
110 pub manifest: Vec<Vec<u8>>,
111 pub images: Vec<ImageRecords>,
112}
113
114#[derive(Clone, PartialEq, Eq)]
115pub struct ImageRecords {
116 pub object_id: [u8; 32],
117 pub records: Vec<Vec<u8>>,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub struct Completeness {
124 pub prefix_count: u64,
125 pub image_count: u64,
126 pub ciphertext_bytes: u64,
127}
128
129#[derive(Clone, PartialEq, Eq)]
130struct Descriptor {
131 vault: [u8; 32],
132 stream: [u8; 32],
133 generation: [u8; 32],
134 bootstrap: [u8; 32],
135 membership: [u8; 32],
137 prefix: u64,
138 catalogs: [Declaration; 3],
139 manifest: Artifact,
140}
141
142impl Descriptor {
143 fn context(&self) -> crypto::LocalSharedStatePackageContext {
144 crypto::LocalSharedStatePackageContext {
145 vault_id: self.vault,
146 generation_id: self.generation,
147 }
148 }
149 fn binding(&self) -> Vec<u8> {
150 let mut out = b"AVBP\0\x02\x01".to_vec();
151 for id in [
152 self.vault,
153 self.stream,
154 self.generation,
155 self.bootstrap,
156 self.membership,
157 ] {
158 out.extend_from_slice(&id);
159 }
160 u64_bytes(&mut out, self.prefix);
161 for (index, declaration) in self.catalogs.iter().enumerate() {
162 out.push(index as u8 + 1);
163 declaration.write(&mut out);
164 }
165 out
166 }
167 fn encode(&self) -> Result<Vec<u8>> {
168 let mut out = self.binding();
169 self.manifest.write(&mut out)?;
170 Ok(out)
171 }
172 fn decode(bytes: &[u8]) -> Result<Self> {
173 bound(number(bytes.len())?, MAX_DESCRIPTOR_BYTES as u64)?;
174 let mut r = Reader(bytes);
175 valid(r.take(4)? == b"AVBP")?;
176 if r.take(2)? != [0, 2] {
177 return Err(Error::Unsupported);
178 }
179 valid(r.byte()? == 1)?;
180 let vault = r.array()?;
181 let stream = r.array()?;
182 let generation = r.array()?;
183 let bootstrap = r.array()?;
184 let membership = r.array()?;
185 let prefix = r.u64()?;
186 valid(prefix < i64::MAX as u64)?;
187 bound(prefix, RECORD_LIMIT)?;
188 valid(r.byte()? == 1)?;
189 let data = Declaration::read(&mut r)?;
190 valid(r.byte()? == 2)?;
191 let ids = Declaration::read(&mut r)?;
192 valid(r.byte()? == 3)?;
193 let images = Declaration::read(&mut r)?;
194 let manifest = Artifact::read(&mut r, CHUNK, false)?;
195 r.end()?;
196 Ok(Self {
197 vault,
198 stream,
199 generation,
200 bootstrap,
201 membership,
202 prefix,
203 catalogs: [data, ids, images],
204 manifest,
205 })
206 }
207}
208
209fn state_catalog(state: &Artifact) -> Result<Vec<u8>> {
210 let mut row = vec![1];
211 state.write(&mut row)?;
212 stream(1, &[row])
213}
214
215fn decode_state_catalog(bytes: &[u8]) -> Result<Artifact> {
216 let records = read_stream(bytes, 1)?;
217 valid(records.len() == 1)?;
218 let mut r = Reader(records[0]);
219 valid(r.byte()? == 1)?;
220 let state = Artifact::read(&mut r, STATE_LIMIT, false)?;
221 r.end()?;
222 Ok(state)
223}
224
225fn manifest_plaintext(d: &Descriptor, stats: &domain::Stats) -> Vec<u8> {
226 let mut out = b"AVBM\0\x01".to_vec();
227 out.extend_from_slice(&d.binding());
228 out.extend_from_slice(&(domain::SECTIONS as u16).to_be_bytes());
229 for (index, (count, length)) in stats.iter().enumerate() {
230 out.extend_from_slice(&(index as u16 + 1).to_be_bytes());
231 u64_bytes(&mut out, *count);
232 u64_bytes(&mut out, *length);
233 }
234 out
235}
236
237struct StateOut {
240 writer: crypto::ArtifactWriter,
241 digest: sha2::Sha256,
242 len: usize,
243 failed: bool,
244}
245
246impl domain::Out for StateOut {
247 fn begin(&mut self, total: usize) {
248 self.failed |= self.writer.begin(total).is_err();
249 }
250 fn len(&self) -> usize {
251 self.len
252 }
253 fn put(&mut self, bytes: &[u8]) {
254 self.len += bytes.len();
255 sha2::Digest::update(&mut self.digest, bytes);
256 self.failed = self.failed || self.writer.write(bytes).is_err();
257 }
258}
259
260pub(super) struct ImageSummary {
262 pub(super) sha256: String,
263 pub(super) object_id: [u8; 32],
264 pub(super) artifact: Artifact,
265}
266
267impl ImageSummary {
268 pub(super) fn authenticated(
269 image: &crypto::EncryptedImage,
270 context: crypto::LocalSharedStatePackageContext,
271 stream: [u8; 32],
272 key: &LocalSharedStatePackageKey,
273 ) -> Result<Self> {
274 let image_key = crypto::derive_image_key(key, context, image.object_id)
275 .map_err(|_| Error::Authentication)?;
276 let reader = crypto::ArtifactReader::new(
277 &image.artifact,
278 context,
279 stream,
280 image.object_id,
281 1,
282 0,
283 &image_key,
284 size(IMAGE_LIMIT)?,
285 )
286 .map_err(|_| Error::Authentication)?;
287 valid(hex::encode(hash_plaintext(reader)?) == image.sha256)?;
288 Ok(Self {
289 sha256: image.sha256.clone(),
290 object_id: image.object_id,
291 artifact: Artifact::from_encrypted(&image.artifact)?,
292 })
293 }
294}
295
296pub(super) fn build_metadata(
299 capture: &NeverDispatchedLocalSharedCapture,
300 context: crypto::LocalSharedStatePackageContext,
301 images: &[ImageSummary],
302 key: &LocalSharedStatePackageKey,
303 membership_predecessor: [u8; 32],
304) -> Result<(Package, AttachmentIndex)> {
305 let stream_id =
306 crypto::decode_context_id(capture.stream_id(), "stream").map_err(|_| Error::Invalid)?;
307 let bootstrap = crypto::decode_context_id(capture.candidate_id(), "candidate")
308 .map_err(|_| Error::Invalid)?;
309 let objects = images
310 .iter()
311 .map(|image| (image.sha256.as_str(), image.object_id))
312 .collect::<std::collections::HashMap<_, _>>();
313 let mappings = capture
314 .images
315 .iter()
316 .map(|r| domain::Mapping {
317 sha256: r.sha256.clone(),
318 classification: r.classification.clone(),
319 object: objects.get(r.sha256.as_str()).copied(),
320 })
321 .collect::<Vec<_>>();
322 let state_key = crypto::derive_bootstrap_class_key(key, context, stream_id, bootstrap, 1)
323 .map_err(|_| Error::Authentication)?;
324 let mut out = StateOut {
325 writer: crypto::ArtifactWriter::new(context, stream_id, bootstrap, 2, 1, &state_key)
326 .map_err(|_| Error::Authentication)?,
327 digest: sha2::Digest::new(),
328 len: 0,
329 failed: false,
330 };
331 let stats = domain::encode_into(&mut out, &capture.capture.snapshot.tables, &mappings)?;
332 valid(!out.failed)?;
333 let encoded = <[u8; 32]>::from(sha2::Digest::finalize(out.digest));
334 let state = out.writer.finish().map_err(|_| Error::Authentication)?;
335 let objects = images
336 .iter()
337 .map(|image| {
338 let mapping = mappings
339 .iter()
340 .find(|m| m.object == Some(image.object_id))
341 .ok_or(Error::Invalid)?;
342 Ok(Image {
343 id: image.object_id,
344 selection: if mapping.classification == "current_selected" {
345 1
346 } else {
347 2
348 },
349 artifact: image.artifact.clone(),
350 })
351 })
352 .collect::<Result<Vec<_>>>()?;
353 let image_catalog = projection::images(
354 &capture.capture.snapshot.tables,
355 &mappings,
356 Images {
357 objects,
358 parents: Vec::new(),
359 references: Vec::new(),
360 },
361 )?;
362 let prefix = projection::prefix(&capture.capture.snapshot.tables)?;
363 let catalogs = [
364 state_catalog(&Artifact::from_encrypted(&state)?)?,
365 catalog::prefix_encode(&prefix)?,
366 image_catalog.encode()?,
367 ];
368 let mut d = Descriptor {
369 vault: context.vault_id,
370 stream: stream_id,
371 generation: context.generation_id,
372 bootstrap,
373 membership: membership_predecessor,
374 prefix: number(prefix.len())?,
375 catalogs: [
376 Declaration::new(&catalogs[0], 1)?,
377 Declaration::new(&catalogs[1], 2)?,
378 Declaration::new(&catalogs[2], 3)?,
379 ],
380 manifest: Artifact {
381 total: 0,
382 aggregate: [0; 32],
383 chunks: Vec::new(),
384 },
385 };
386 let manifest_key = crypto::derive_bootstrap_class_key(key, context, stream_id, bootstrap, 2)
387 .map_err(|_| Error::Authentication)?;
388 let manifest = crypto::encrypt_artifact(
389 &manifest_plaintext(&d, &stats),
390 context,
391 stream_id,
392 bootstrap,
393 2,
394 2,
395 &manifest_key,
396 )
397 .map_err(|_| Error::Authentication)?;
398 d.manifest = Artifact::from_encrypted(&manifest)?;
399 let package = Package {
400 descriptor: d.encode()?,
401 catalogs,
402 state: state.into_records(),
403 manifest: manifest.into_records(),
404 images: Vec::new(),
405 };
406 let index = check_capture(
407 &package,
408 capture,
409 key,
410 membership_predecessor,
411 StateCheck::Encoded {
412 digest: encoded,
413 mappings: &mappings,
414 stats: &stats,
415 },
416 false,
417 )?;
418 Ok((package, index))
419}
420
421#[cfg(test)]
423pub(super) fn build(
424 capture: &NeverDispatchedLocalSharedCapture,
425 context: crypto::LocalSharedStatePackageContext,
426 images: Vec<crypto::EncryptedImage>,
427 key: &LocalSharedStatePackageKey,
428 membership_predecessor: [u8; 32],
429) -> Result<(Package, AttachmentIndex)> {
430 let stream =
431 crypto::decode_context_id(capture.stream_id(), "stream").map_err(|_| Error::Invalid)?;
432 let summaries = images
433 .iter()
434 .map(|image| ImageSummary::authenticated(image, context, stream, key))
435 .collect::<Result<Vec<_>>>()?;
436 let (mut package, _) =
437 build_metadata(capture, context, &summaries, key, membership_predecessor)?;
438 package.images = images
439 .into_iter()
440 .map(|image| ImageRecords {
441 object_id: image.object_id,
442 records: image.artifact.into_records(),
443 })
444 .collect();
445 package.images.sort_by_key(|image| image.object_id);
446 let index = authenticate_capture(&package, capture, key, membership_predecessor)?;
447 Ok((package, index))
448}
449
450pub fn authenticate(
453 package: &Package,
454 key: &LocalSharedStatePackageKey,
455 context: crypto::LocalSharedStatePackageContext,
456 stream_id: [u8; 32],
457 bootstrap_id: [u8; 32],
458 membership_predecessor: [u8; 32],
459) -> Result<()> {
460 let d = Descriptor::decode(&package.descriptor)?;
461 valid(
462 d.context() == context
463 && d.stream == stream_id
464 && d.bootstrap == bootstrap_id
465 && d.membership == membership_predecessor,
466 )?;
467 decrypt_domain(package, key)?;
468 Ok(())
469}
470
471pub(super) fn context_and_membership(
472 descriptor: &[u8],
473) -> Result<(crypto::LocalSharedStatePackageContext, [u8; 32])> {
474 let d = Descriptor::decode(descriptor)?;
475 Ok((d.context(), d.membership))
476}
477
478pub fn validate_keyless(package: &Package) -> Result<Completeness> {
482 keyless(package).map(|(completeness, _, _)| completeness)
483}
484
485fn keyless(package: &Package) -> Result<(Completeness, Descriptor, Images)> {
488 let metadata = download::MetadataView::from(package);
489 let (d, images) = validate_metadata(&metadata)?;
490 valid(images.objects.len() == package.images.len())?;
491 for (object, records) in images.objects.iter().zip(&package.images) {
492 valid(object.id == records.object_id)?;
493 object
494 .artifact
495 .verify(&records.records, d.context(), d.stream, object.id, 1, 0)?;
496 }
497 let ciphertext_bytes = std::iter::once(&package.state)
498 .chain(std::iter::once(&package.manifest))
499 .chain(package.images.iter().map(|i| &i.records))
500 .flatten()
501 .try_fold(0, |total, record| add(total, number(record.len())?))?;
502 let completeness = Completeness {
503 prefix_count: d.prefix,
504 image_count: number(images.objects.len())?,
505 ciphertext_bytes,
506 };
507 Ok((completeness, d, images))
508}
509
510fn validate_metadata(metadata: &download::MetadataView<'_>) -> Result<(Descriptor, Images)> {
511 let d = Descriptor::decode(metadata.descriptor)?;
512 for (i, bytes) in metadata.catalogs.iter().enumerate() {
513 d.catalogs[i].verify(bytes, i as u8 + 1)?;
514 }
515 let state = decode_state_catalog(&metadata.catalogs[0])?;
516 catalog::prefix_decode(&metadata.catalogs[1], d.prefix)?;
517 let images = Images::decode(&metadata.catalogs[2])?;
518 state.verify(metadata.state, d.context(), d.stream, d.bootstrap, 2, 1)?;
519 d.manifest
520 .verify(metadata.manifest, d.context(), d.stream, d.bootstrap, 2, 2)?;
521 Ok((d, images))
522}
523
524fn decrypt_domain(
525 package: &Package,
526 key: &LocalSharedStatePackageKey,
527) -> Result<(SharedStateCapture, Vec<domain::Mapping>)> {
528 decrypt_domain_images(package, key, |_, _| {})
529}
530
531fn decrypt_domain_images(
532 package: &Package,
533 key: &LocalSharedStatePackageKey,
534 mut accept_image: impl FnMut(&str, Zeroizing<Vec<u8>>),
535) -> Result<(SharedStateCapture, Vec<domain::Mapping>)> {
536 validate_keyless(package)?;
537 let metadata = download::MetadataView::from(package);
538 let (capture, mappings) = decrypt_metadata(&metadata, key)?;
539 let (d, images) = validate_metadata(&metadata)?;
540 for (object, records) in images.objects.iter().zip(&package.images) {
541 let mapping = mappings
542 .iter()
543 .find(|m| m.object == Some(object.id))
544 .ok_or(Error::Invalid)?;
545 let image_key = crypto::derive_image_key(key, d.context(), object.id)
546 .map_err(|_| Error::Authentication)?;
547 let bytes = Zeroizing::new(
548 crypto::decrypt_artifact(
549 &object.artifact.encrypted(&records.records),
550 d.context(),
551 d.stream,
552 object.id,
553 1,
554 0,
555 &image_key,
556 size(IMAGE_LIMIT)?,
557 )
558 .map_err(|_| Error::Authentication)?,
559 );
560 valid(hex::encode(crate::sync::codec::hash(&bytes)) == mapping.sha256)?;
561 accept_image(&mapping.sha256, bytes);
562 }
563 Ok((capture, mappings))
564}
565
566fn decrypt_metadata(
567 metadata: &download::MetadataView<'_>,
568 key: &LocalSharedStatePackageKey,
569) -> Result<(SharedStateCapture, Vec<domain::Mapping>)> {
570 validate_metadata(metadata)?;
571 #[cfg(test)]
572 crate::sync::shared_state::counters::keyed_pass();
573 let d = Descriptor::decode(metadata.descriptor)?;
574 let state = decode_state_catalog(&metadata.catalogs[0])?;
575 let state_key = crypto::derive_bootstrap_class_key(key, d.context(), d.stream, d.bootstrap, 1)
576 .map_err(|_| Error::Authentication)?;
577 let plaintext = Zeroizing::new(
578 crypto::decrypt_artifact(
579 &state.encrypted(metadata.state),
580 d.context(),
581 d.stream,
582 d.bootstrap,
583 2,
584 1,
585 &state_key,
586 size(STATE_LIMIT)?,
587 )
588 .map_err(|_| Error::Authentication)?,
589 );
590 let (tables, mappings, stats) = domain::decode(&plaintext)?;
591 let manifest_key =
592 crypto::derive_bootstrap_class_key(key, d.context(), d.stream, d.bootstrap, 2)
593 .map_err(|_| Error::Authentication)?;
594 let manifest = Zeroizing::new(
595 crypto::decrypt_artifact(
596 &d.manifest.encrypted(metadata.manifest),
597 d.context(),
598 d.stream,
599 d.bootstrap,
600 2,
601 2,
602 &manifest_key,
603 size(CHUNK)?,
604 )
605 .map_err(|_| Error::Authentication)?,
606 );
607 valid(*manifest == manifest_plaintext(&d, &stats))?;
608 let capture = capture(tables);
609 capture.validate().map_err(|_| Error::Invalid)?;
610 valid(
611 projection::prefix(&capture.snapshot.tables)?
612 == catalog::prefix_decode(&metadata.catalogs[1], d.prefix)?,
613 )?;
614 let images = Images::decode(&metadata.catalogs[2])?;
615 let expected = projection::images(
616 &capture.snapshot.tables,
617 &mappings,
618 Images {
619 objects: images.objects.clone(),
620 parents: Vec::new(),
621 references: Vec::new(),
622 },
623 )?;
624 valid(images == expected)?;
625 Ok((capture, mappings))
626}
627
628fn capture(tables: crate::data_safety::export_types::ExportTables) -> SharedStateCapture {
630 let version = if tables.shared_history_provenance.is_empty() {
632 3
633 } else {
634 4
635 };
636 SharedStateCapture {
637 snapshot: crate::data_safety::export_types::AvenExport {
638 format: "aven-export".into(),
639 version,
640 exported_at: String::new(),
641 schema_version: 0,
642 blobs_included: false,
643 tables,
644 },
645 }
646}
647
648pub(super) fn decrypt_capture(
650 package: &Package,
651 key: &LocalSharedStatePackageKey,
652) -> Result<SharedStateCapture> {
653 Ok(decrypt_domain(package, key)?.0)
654}
655
656#[cfg(test)]
658pub(super) fn decrypt_images(
659 package: &Package,
660 key: &LocalSharedStatePackageKey,
661) -> Result<Vec<(String, Zeroizing<Vec<u8>>)>> {
662 let mut images = Vec::new();
663 decrypt_domain_images(package, key, |sha256, bytes| {
664 images.push((sha256.to_string(), bytes));
665 })?;
666 Ok(images)
667}
668
669pub fn validate_against_capture(
673 package: &Package,
674 capture: &NeverDispatchedLocalSharedCapture,
675 key: &LocalSharedStatePackageKey,
676 membership_predecessor: [u8; 32],
677) -> Result<()> {
678 authenticate_capture(package, capture, key, membership_predecessor).map(drop)
679}
680
681pub(crate) fn authenticate_capture(
686 package: &Package,
687 capture: &NeverDispatchedLocalSharedCapture,
688 key: &LocalSharedStatePackageKey,
689 membership_predecessor: [u8; 32],
690) -> Result<AttachmentIndex> {
691 check_capture(
692 package,
693 capture,
694 key,
695 membership_predecessor,
696 StateCheck::Capture,
697 true,
698 )
699}
700
701enum StateCheck<'a> {
704 Capture,
707 Encoded {
711 digest: [u8; 32],
712 mappings: &'a [domain::Mapping],
713 stats: &'a domain::Stats,
714 },
715}
716
717struct Plain<'r, 'a>(&'r mut crypto::ArtifactReader<'a>);
719
720impl domain::In for Plain<'_, '_> {
721 fn remaining(&self) -> u64 {
722 self.0.remaining()
723 }
724 fn read(&mut self, into: &mut [u8]) -> Result<()> {
725 let mut filled = 0;
726 while filled < into.len() {
727 let piece = self
728 .0
729 .take(into.len() - filled)
730 .map_err(|_| Error::Authentication)?;
731 valid(!piece.is_empty())?;
732 into[filled..filled + piece.len()].copy_from_slice(piece);
733 filled += piece.len();
734 }
735 Ok(())
736 }
737 fn skip(&mut self, n: u64) -> Result<()> {
738 let mut left = size(n)?;
739 while left > 0 {
740 let piece = self.0.take(left).map_err(|_| Error::Authentication)?;
741 valid(!piece.is_empty())?;
742 left -= piece.len();
743 }
744 Ok(())
745 }
746}
747
748struct PlainMatcher<'r, 'a> {
751 reader: &'r mut crypto::ArtifactReader<'a>,
752 written: usize,
753 equal: bool,
754 failed: bool,
755}
756
757impl domain::Out for PlainMatcher<'_, '_> {
758 fn begin(&mut self, total: usize) {
759 self.equal &= total as u64 == self.reader.remaining();
760 }
761 fn len(&self) -> usize {
762 self.written
763 }
764 fn put(&mut self, mut bytes: &[u8]) {
765 self.written += bytes.len();
766 while self.equal && !bytes.is_empty() {
767 match self.reader.take(bytes.len()) {
768 Ok(piece) if !piece.is_empty() => {
769 self.equal = piece == &bytes[..piece.len()];
770 bytes = &bytes[piece.len()..];
771 }
772 Ok(_) => self.equal = false,
773 Err(_) => {
774 self.failed = true;
775 self.equal = false;
776 }
777 }
778 }
779 }
780}
781
782fn match_state(
785 mut reader: crypto::ArtifactReader<'_>,
786 tables: &crate::data_safety::export_types::ExportTables,
787 mappings: &[domain::Mapping],
788) -> Result<domain::Stats> {
789 let mut matcher = PlainMatcher {
790 reader: &mut reader,
791 written: 0,
792 equal: true,
793 failed: false,
794 };
795 let stats = domain::encode_into(&mut matcher, tables, mappings)?;
796 if matcher.failed {
797 return Err(Error::Authentication);
798 }
799 valid(matcher.equal && reader.remaining() == 0)?;
800 reader.finish().map_err(|_| Error::Invalid)?;
801 Ok(stats)
802}
803
804fn hash_plaintext(mut reader: crypto::ArtifactReader<'_>) -> Result<[u8; 32]> {
806 let mut hash = <sha2::Sha256 as sha2::Digest>::new();
807 loop {
808 let piece = reader.take(usize::MAX).map_err(|_| Error::Authentication)?;
809 if piece.is_empty() {
810 break;
811 }
812 sha2::Digest::update(&mut hash, piece);
813 }
814 reader.finish().map_err(|_| Error::Invalid)?;
815 Ok(sha2::Digest::finalize(hash).into())
816}
817
818fn state_reader<'a>(
819 d: &Descriptor,
820 state: &'a crypto::EncryptedArtifact<'a>,
821 key: &LocalSharedStatePackageKey,
822) -> Result<crypto::ArtifactReader<'a>> {
823 let state_key = crypto::derive_bootstrap_class_key(key, d.context(), d.stream, d.bootstrap, 1)
824 .map_err(|_| Error::Authentication)?;
825 crypto::ArtifactReader::new(
826 state,
827 d.context(),
828 d.stream,
829 d.bootstrap,
830 2,
831 1,
832 &state_key,
833 size(STATE_LIMIT)?,
834 )
835 .map_err(|_| Error::Authentication)
836}
837
838fn check_capture(
842 package: &Package,
843 capture: &NeverDispatchedLocalSharedCapture,
844 key: &LocalSharedStatePackageKey,
845 membership_predecessor: [u8; 32],
846 check: StateCheck<'_>,
847 authenticate_images: bool,
848) -> Result<AttachmentIndex> {
849 #[cfg(test)]
850 crate::sync::shared_state::counters::keyed_pass();
851 let metadata = download::MetadataView::from(package);
852 let (d, images) = validate_metadata(&metadata)?;
853 valid(
854 d.membership == membership_predecessor
855 && capture.stream_id() == hex::encode(d.stream)
856 && capture.candidate_id() == hex::encode(d.bootstrap),
857 )?;
858 let state = decode_state_catalog(&metadata.catalogs[0])?;
859 let state = state.encrypted(metadata.state);
860 let tables = &capture.capture.snapshot.tables;
861 let decoded;
862 let computed;
863 let (mappings, stats) = match check {
864 StateCheck::Capture => {
865 decoded = domain::decode_mappings(&mut Plain(&mut state_reader(&d, &state, key)?))?;
866 computed = match_state(state_reader(&d, &state, key)?, tables, &decoded)?;
867 (&decoded[..], &computed)
868 }
869 StateCheck::Encoded {
870 digest,
871 mappings,
872 stats,
873 } => {
874 valid(hash_plaintext(state_reader(&d, &state, key)?)? == digest)?;
875 (mappings, stats)
876 }
877 };
878 let manifest_key =
879 crypto::derive_bootstrap_class_key(key, d.context(), d.stream, d.bootstrap, 2)
880 .map_err(|_| Error::Authentication)?;
881 let manifest = Zeroizing::new(
882 crypto::decrypt_artifact(
883 &d.manifest.encrypted(metadata.manifest),
884 d.context(),
885 d.stream,
886 d.bootstrap,
887 2,
888 2,
889 &manifest_key,
890 size(CHUNK)?,
891 )
892 .map_err(|_| Error::Authentication)?,
893 );
894 valid(*manifest == manifest_plaintext(&d, stats))?;
895 valid(projection::prefix(tables)? == catalog::prefix_decode(&metadata.catalogs[1], d.prefix)?)?;
896 let expected_images = projection::images(
897 tables,
898 mappings,
899 Images {
900 objects: images.objects.clone(),
901 parents: Vec::new(),
902 references: Vec::new(),
903 },
904 )?;
905 valid(images == expected_images)?;
906 valid(mappings.len() == capture.images.len())?;
907 let captured_images = capture
908 .images
909 .iter()
910 .map(|r| (r.sha256.as_str(), r.classification.as_str()))
911 .collect::<std::collections::HashMap<_, _>>();
912 for mapping in mappings {
913 valid(
914 captured_images.get(mapping.sha256.as_str()).copied()
915 == Some(mapping.classification.as_str()),
916 )?;
917 }
918 if authenticate_images {
919 valid(images.objects.len() == package.images.len())?;
920 for (object, records) in images.objects.iter().zip(&package.images) {
921 valid(object.id == records.object_id)?;
922 let mapping = mappings
923 .iter()
924 .find(|m| m.object == Some(object.id))
925 .ok_or(Error::Invalid)?;
926 authenticate_image(
927 d.context(),
928 d.stream,
929 object,
930 &mapping.sha256,
931 &records.records,
932 key,
933 )?;
934 }
935 }
936 index_from_mappings(&metadata, mappings).map_err(|_| Error::Invalid)
937}
938
939pub(super) fn authenticate_metadata_capture(
942 package: &Package,
943 capture: &NeverDispatchedLocalSharedCapture,
944 key: &LocalSharedStatePackageKey,
945 membership_predecessor: [u8; 32],
946) -> Result<AttachmentIndex> {
947 check_capture(
948 package,
949 capture,
950 key,
951 membership_predecessor,
952 StateCheck::Capture,
953 false,
954 )
955}
956
957fn authenticate_image(
958 context: crypto::LocalSharedStatePackageContext,
959 stream: [u8; 32],
960 object: &Image,
961 sha256: &str,
962 records: &[Vec<u8>],
963 key: &LocalSharedStatePackageKey,
964) -> Result<()> {
965 let image_key =
966 crypto::derive_image_key(key, context, object.id).map_err(|_| Error::Authentication)?;
967 let artifact = object.artifact.encrypted(records);
968 let reader = crypto::ArtifactReader::new(
969 &artifact,
970 context,
971 stream,
972 object.id,
973 1,
974 0,
975 &image_key,
976 size(IMAGE_LIMIT)?,
977 )
978 .map_err(|_| Error::Authentication)?;
979 valid(hex::encode(hash_plaintext(reader)?) == sha256)
980}
981
982pub(super) fn authenticate_indexed_image(
983 index: &AttachmentIndex,
984 object_id: [u8; 32],
985 sha256: &str,
986 records: &[Vec<u8>],
987 key: &LocalSharedStatePackageKey,
988) -> Result<()> {
989 let (object, expected) = index
990 .objects
991 .iter()
992 .find(|(object, _)| object.id == object_id)
993 .ok_or(Error::Invalid)?;
994 valid(expected == sha256)?;
995 authenticate_image(index.context, index.stream, object, sha256, records, key)
996}
997
998#[cfg(test)]
1001pub(crate) fn validate_against_capture_decoding(
1002 package: &Package,
1003 capture: &NeverDispatchedLocalSharedCapture,
1004 key: &LocalSharedStatePackageKey,
1005 membership_predecessor: [u8; 32],
1006) -> Result<()> {
1007 let d = Descriptor::decode(&package.descriptor)?;
1008 valid(
1009 d.membership == membership_predecessor
1010 && capture.stream_id() == hex::encode(d.stream)
1011 && capture.candidate_id() == hex::encode(d.bootstrap),
1012 )?;
1013 let (decoded, mappings) = decrypt_domain(package, key)?;
1014 valid(
1015 domain::encode(&decoded.snapshot.tables, &mappings)?.0
1016 == domain::encode(&capture.capture.snapshot.tables, &mappings)?.0,
1017 )?;
1018 valid(mappings.len() == capture.images.len())?;
1019 let captured_images = capture
1020 .images
1021 .iter()
1022 .map(|r| (r.sha256.as_str(), r.classification.as_str()))
1023 .collect::<std::collections::HashMap<_, _>>();
1024 for mapping in &mappings {
1025 valid(
1026 captured_images.get(mapping.sha256.as_str()).copied()
1027 == Some(mapping.classification.as_str()),
1028 )?;
1029 }
1030 Ok(())
1031}
1032
1033#[cfg(test)]
1034mod tests;
1035
1036#[cfg(test)]
1039pub(crate) fn recommit_catalog(package: &mut Package, index: usize) {
1040 let mut d = Descriptor::decode(&package.descriptor).unwrap();
1041 d.catalogs[index] = Declaration::new(&package.catalogs[index], index as u8 + 1).unwrap();
1042 package.descriptor = d.encode().unwrap();
1043}
1044
1045#[cfg(test)]
1049pub(crate) fn replace_prefix_catalog(package: &mut Package, count: u64) {
1050 let rows = (1..=count)
1051 .map(|rank| (rank, format!("{rank:026}")))
1052 .collect::<Vec<_>>();
1053 package.catalogs[1] = catalog::prefix_encode(&rows).unwrap();
1054 let mut d = Descriptor::decode(&package.descriptor).unwrap();
1055 d.prefix = count;
1056 package.descriptor = d.encode().unwrap();
1057 recommit_catalog(package, 1);
1058}
1059
1060#[derive(Clone)]
1061pub(crate) struct AttachmentIndex {
1062 pub context: crypto::LocalSharedStatePackageContext,
1063 pub stream: [u8; 32],
1064 pub objects: Vec<(catalog::Image, String)>,
1065 pub references: Vec<catalog::Reference>,
1066}
1067
1068#[cfg(any(test, feature = "test-support"))]
1070pub(crate) fn attachment_index(
1071 package: &Package,
1072 key: &LocalSharedStatePackageKey,
1073) -> anyhow::Result<AttachmentIndex> {
1074 let (_, mappings) = decrypt_domain_images(package, key, |_, _| {})?;
1075 index_from_mappings(&download::MetadataView::from(package), &mappings)
1076}
1077
1078pub(crate) fn index_from_objects(
1082 descriptor: &[u8],
1083 catalogs: &[Vec<u8>; 3],
1084 recorded: &[([u8; 32], String)],
1085) -> anyhow::Result<AttachmentIndex> {
1086 use anyhow::Context as _;
1087 let declaration = staging::DeclarationView::decode(descriptor)?;
1088 for (class, bytes) in catalogs.iter().enumerate() {
1089 declaration.catalog(class, bytes)?;
1090 }
1091 let descriptor = Descriptor::decode(descriptor)?;
1092 let images = Images::decode(&catalogs[2])?;
1093 anyhow::ensure!(
1094 recorded.len() == images.objects.len(),
1095 "error seed-capture-changed"
1096 );
1097 let objects = images
1098 .objects
1099 .into_iter()
1100 .map(|image| {
1101 let sha256 = recorded
1102 .iter()
1103 .find(|(id, _)| *id == image.id)
1104 .map(|(_, sha256)| sha256.clone())
1105 .context("error seed-capture-changed")?;
1106 Ok((image, sha256))
1107 })
1108 .collect::<anyhow::Result<Vec<_>>>()?;
1109 Ok(AttachmentIndex {
1110 context: descriptor.context(),
1111 stream: descriptor.stream,
1112 objects,
1113 references: images.references,
1114 })
1115}
1116
1117fn index_from_mappings(
1118 metadata: &download::MetadataView<'_>,
1119 mappings: &[domain::Mapping],
1120) -> anyhow::Result<AttachmentIndex> {
1121 let descriptor = Descriptor::decode(metadata.descriptor)?;
1122 let images = Images::decode(&metadata.catalogs[2])?;
1123 let objects = images
1124 .objects
1125 .into_iter()
1126 .map(|image| {
1127 let mapping = mappings
1128 .iter()
1129 .find(|m| m.object == Some(image.id))
1130 .ok_or(Error::Invalid)?;
1131 Ok((image, mapping.sha256.clone()))
1132 })
1133 .collect::<Result<Vec<_>>>()?;
1134 Ok(AttachmentIndex {
1135 context: descriptor.context(),
1136 stream: descriptor.stream,
1137 objects,
1138 references: images.references,
1139 })
1140}