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