1use std::{
2 collections::{BTreeMap, BTreeSet},
3 path::Path,
4};
5
6use async_compression::tokio::bufread::ZstdDecoder;
7use iroh_blobs::Hash;
8use tokio::{
9 fs,
10 io::{AsyncRead, AsyncReadExt, BufReader},
11};
12use uuid::Uuid;
13
14use crate::{
15 error::{Result, SyncwebError},
16 folder::CollectionManifest,
17};
18
19const CAR_VERSION: u64 = 1;
20const CID_VERSION: u64 = 1;
21const RAW_CODEC: u64 = 0x55;
22const BLAKE3_MULTIHASH: u64 = 0x1e;
23const HASH_SIZE: u64 = 32;
24const MAX_HEADER_SIZE: u64 = 64 * 1024;
25const MAX_MANIFEST_SIZE: u64 = 16 * 1024 * 1024;
26const COPY_BUFFER_SIZE: usize = 64 * 1024;
27
28#[derive(Clone, Debug, Eq, PartialEq)]
30#[non_exhaustive]
31pub struct DropVerifyResult {
32 pub manifest: Hash,
33 pub collection_id: Uuid,
34 pub version: String,
35 pub entry_count: usize,
36 pub block_count: usize,
37 pub bytes_verified: u64,
38}
39
40pub type DropVerificationResult = DropVerifyResult;
42
43#[derive(Clone, Copy, Debug, Default)]
45#[non_exhaustive]
46pub struct DropVerifier;
47
48impl DropVerifier {
49 #[must_use]
50 pub const fn new() -> Self {
51 Self
52 }
53
54 pub async fn verify(&self, path: impl AsRef<Path>) -> Result<DropVerifyResult> {
61 let file = fs::File::open(path)
62 .await
63 .map_err(|error| SyncwebError::operation("failed to open drop archive", error))?;
64 self.verify_reader(file).await
65 }
66
67 pub async fn verify_reader<R>(&self, reader: R) -> Result<DropVerifyResult>
77 where
78 R: AsyncRead + Unpin,
79 {
80 let decoder = ZstdDecoder::new(BufReader::new(reader));
81 verify_car(decoder).await
82 }
83}
84
85pub async fn verify_archive(path: impl AsRef<Path>) -> Result<DropVerifyResult> {
92 DropVerifier::new().verify(path).await
93}
94
95pub async fn verify_drop_reader<R>(reader: R) -> Result<DropVerifyResult>
102where
103 R: AsyncRead + Unpin,
104{
105 DropVerifier::new().verify_reader(reader).await
106}
107
108async fn verify_car<R>(mut reader: R) -> Result<DropVerifyResult>
109where
110 R: AsyncRead + Unpin,
111{
112 let header_size = read_required_varint(&mut reader, "CAR header length").await?;
113 if header_size > MAX_HEADER_SIZE {
114 return Err(SyncwebError::InvalidConfig(format!(
115 "CAR header exceeds {MAX_HEADER_SIZE} bytes"
116 )));
117 }
118 let header_len = usize::try_from(header_size)
119 .map_err(|error| SyncwebError::operation("CAR header length is not representable", error))?;
120 let mut header = vec![0_u8; header_len];
121 read_exact(&mut reader, &mut header, "failed to read CAR header").await?;
122 let root = parse_car_header(&header)?;
123
124 let first_section = read_section_header(&mut reader)
125 .await?
126 .ok_or_else(|| SyncwebError::InvalidConfig("drop archive does not contain a manifest block".to_owned()))?;
127 if first_section.hash != root {
128 return Err(SyncwebError::InvalidConfig(
129 "drop manifest CID does not match the CAR root".to_owned(),
130 ));
131 }
132 if first_section.payload_size > MAX_MANIFEST_SIZE {
133 return Err(SyncwebError::InvalidConfig(format!(
134 "drop manifest exceeds {MAX_MANIFEST_SIZE} bytes"
135 )));
136 }
137 let (manifest_hash, manifest_data, manifest_size) =
138 read_payload(&mut reader, first_section.payload_size, true).await?;
139 if manifest_hash != root {
140 return Err(SyncwebError::InvalidConfig(
141 "drop manifest hash does not match the CAR root".to_owned(),
142 ));
143 }
144 let manifest_bytes = manifest_data
145 .ok_or_else(|| SyncwebError::InvalidConfig("drop manifest was not buffered for verification".to_owned()))?;
146 let manifest = CollectionManifest::from_bytes(&manifest_bytes)?;
147 manifest.verify_signature()?;
148 if manifest.blob_id()? != root {
149 return Err(SyncwebError::InvalidConfig(
150 "drop manifest hash does not match its serialized content".to_owned(),
151 ));
152 }
153
154 let mut expected = BTreeMap::new();
155 for entry in &manifest.entries {
156 if let Some(previous_size) = expected.insert(entry.content_id, entry.size)
157 && previous_size != entry.size
158 {
159 return Err(SyncwebError::InvalidConfig(format!(
160 "manifest uses content hash {} with inconsistent sizes",
161 entry.content_id
162 )));
163 }
164 }
165
166 let mut seen = BTreeSet::new();
167 let mut block_count = 1_usize;
168 let mut bytes_verified = manifest_size;
169 while let Some(section) = read_section_header(&mut reader).await? {
170 let expected_size = expected.get(§ion.hash).ok_or_else(|| {
171 SyncwebError::InvalidConfig(format!(
172 "drop contains a block not referenced by the manifest: {}",
173 section.hash
174 ))
175 })?;
176 if !seen.insert(section.hash) {
177 return Err(SyncwebError::InvalidConfig(format!(
178 "drop contains duplicate content block: {}",
179 section.hash
180 )));
181 }
182 if section.payload_size != *expected_size {
183 return Err(SyncwebError::InvalidConfig(format!(
184 "drop content size does not match the manifest for {}",
185 section.hash
186 )));
187 }
188 let (actual_hash, _, payload_size) = read_payload(&mut reader, section.payload_size, false).await?;
189 if actual_hash != section.hash {
190 return Err(SyncwebError::InvalidConfig(format!(
191 "drop content hash does not match its CID: expected {}, got {}",
192 section.hash, actual_hash
193 )));
194 }
195 bytes_verified = bytes_verified
196 .checked_add(payload_size)
197 .ok_or_else(|| SyncwebError::operation("verified drop size overflow", "u64 limit exceeded"))?;
198 block_count = block_count
199 .checked_add(1)
200 .ok_or_else(|| SyncwebError::operation("drop block count overflow", "usize limit exceeded"))?;
201 }
202
203 if seen.len() != expected.len() {
204 let missing = expected
205 .keys()
206 .find(|hash| !seen.contains(*hash))
207 .copied()
208 .map_or_else(|| "unknown".to_owned(), |hash| hash.to_string());
209 return Err(SyncwebError::InvalidConfig(format!(
210 "drop is missing content referenced by the manifest: {missing}"
211 )));
212 }
213
214 Ok(DropVerifyResult {
215 manifest: root,
216 collection_id: manifest.collection_id,
217 version: manifest.version,
218 entry_count: manifest.entries.len(),
219 block_count,
220 bytes_verified,
221 })
222}
223
224struct SectionHeader {
225 hash: Hash,
226 payload_size: u64,
227}
228
229async fn read_section_header<R>(reader: &mut R) -> Result<Option<SectionHeader>>
230where
231 R: AsyncRead + Unpin,
232{
233 let Some(section_size) = read_varint(reader, true, "CAR section length").await? else {
234 return Ok(None);
235 };
236 let mut remaining = section_size;
237 let version = read_section_varint(reader, &mut remaining, "CID version").await?;
238 let codec = read_section_varint(reader, &mut remaining, "CID codec").await?;
239 let hash_code = read_section_varint(reader, &mut remaining, "CID multihash code").await?;
240 let hash_size = read_section_varint(reader, &mut remaining, "CID multihash length").await?;
241 if version != CID_VERSION || codec != RAW_CODEC || hash_code != BLAKE3_MULTIHASH || hash_size != HASH_SIZE {
242 return Err(SyncwebError::InvalidConfig(
243 "drop contains an unsupported content identifier".to_owned(),
244 ));
245 }
246 let mut hash_bytes = [0_u8; 32];
247 read_section_bytes(reader, &mut remaining, &mut hash_bytes, "CID digest").await?;
248 Ok(Some(SectionHeader {
249 hash: Hash::from_bytes(hash_bytes),
250 payload_size: remaining,
251 }))
252}
253
254async fn read_payload<R>(reader: &mut R, payload_size: u64, collect: bool) -> Result<(Hash, Option<Vec<u8>>, u64)>
255where
256 R: AsyncRead + Unpin,
257{
258 let mut remaining = payload_size;
259 let mut bytes_read = 0_u64;
260 let mut hasher = blake3::Hasher::new();
261 let mut buffer = vec![0_u8; COPY_BUFFER_SIZE];
262 let mut collected = if collect {
263 let capacity = usize::try_from(payload_size)
264 .map_err(|error| SyncwebError::operation("drop manifest is too large", error))?;
265 Some(Vec::with_capacity(capacity))
266 } else {
267 None
268 };
269 while remaining > 0 {
270 let requested_u64 = remaining.min(
271 u64::try_from(buffer.len())
272 .map_err(|error| SyncwebError::operation("drop read buffer length is not representable", error))?,
273 );
274 let requested = usize::try_from(requested_u64)
275 .map_err(|error| SyncwebError::operation("drop section length is not representable", error))?;
276 let chunk = buffer
277 .get_mut(..requested)
278 .ok_or_else(|| SyncwebError::operation("drop read buffer range is invalid", requested))?;
279 read_exact(reader, chunk, "drop section").await?;
280 hasher.update(chunk);
281 if let Some(bytes) = &mut collected {
282 bytes.extend_from_slice(chunk);
283 }
284 let requested_bytes = u64::try_from(requested)
285 .map_err(|error| SyncwebError::operation("drop section length is not representable", error))?;
286 remaining = remaining
287 .checked_sub(requested_bytes)
288 .ok_or_else(|| SyncwebError::operation("drop section length underflow", "invalid section length"))?;
289 bytes_read = bytes_read
290 .checked_add(requested_bytes)
291 .ok_or_else(|| SyncwebError::operation("verified drop size overflow", "u64 limit exceeded"))?;
292 }
293 Ok((Hash::from_bytes(*hasher.finalize().as_bytes()), collected, bytes_read))
294}
295
296async fn read_section_varint<R>(reader: &mut R, remaining: &mut u64, name: &'static str) -> Result<u64>
297where
298 R: AsyncRead + Unpin,
299{
300 let mut value = 0_u64;
301 let mut shift = 0_u32;
302 for index in 0..10 {
303 let byte = read_section_byte(reader, remaining, name).await?;
304 let bits = u64::from(byte & 0x7f);
305 if shift == 63 && bits > 1 {
306 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
307 }
308 value |= bits << shift;
309 if byte & 0x80 == 0 {
310 return Ok(value);
311 }
312 if index == 9 {
313 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
314 }
315 shift = shift
316 .checked_add(7)
317 .ok_or_else(|| SyncwebError::operation("CAR varint shift overflow", name))?;
318 }
319 Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")))
320}
321
322async fn read_section_byte<R>(reader: &mut R, remaining: &mut u64, name: &'static str) -> Result<u8>
323where
324 R: AsyncRead + Unpin,
325{
326 if *remaining == 0 {
327 return Err(SyncwebError::InvalidConfig(format!("CAR section ends before {name}")));
328 }
329 let mut byte = [0_u8; 1];
330 read_exact(reader, &mut byte, "failed to read drop section").await?;
331 *remaining = remaining
332 .checked_sub(1)
333 .ok_or_else(|| SyncwebError::operation("CAR section length underflow", name))?;
334 byte.first()
335 .copied()
336 .ok_or_else(|| SyncwebError::operation("CAR byte is missing", name))
337}
338
339async fn read_section_bytes<R>(reader: &mut R, remaining: &mut u64, bytes: &mut [u8], name: &'static str) -> Result<()>
340where
341 R: AsyncRead + Unpin,
342{
343 let length = u64::try_from(bytes.len())
344 .map_err(|error| SyncwebError::operation("CAR field length is not representable", error))?;
345 if length > *remaining {
346 return Err(SyncwebError::InvalidConfig(format!("CAR section ends before {name}")));
347 }
348 read_exact(reader, bytes, "failed to read drop section").await?;
349 *remaining = remaining
350 .checked_sub(length)
351 .ok_or_else(|| SyncwebError::operation("CAR section length underflow", name))?;
352 Ok(())
353}
354
355async fn read_required_varint<R>(reader: &mut R, name: &'static str) -> Result<u64>
356where
357 R: AsyncRead + Unpin,
358{
359 read_varint(reader, false, name)
360 .await?
361 .ok_or_else(|| SyncwebError::InvalidConfig(format!("drop archive ended before {name}")))
362}
363
364async fn read_varint<R>(reader: &mut R, allow_eof: bool, name: &'static str) -> Result<Option<u64>>
365where
366 R: AsyncRead + Unpin,
367{
368 let mut byte = [0_u8; 1];
369 let count = reader
370 .read(&mut byte)
371 .await
372 .map_err(|error| SyncwebError::operation("failed to read drop archive", error))?;
373 if count == 0 {
374 if allow_eof {
375 return Ok(None);
376 }
377 return Err(SyncwebError::InvalidConfig(format!("drop archive ended before {name}")));
378 }
379
380 let mut value = 0_u64;
381 let mut shift = 0_u32;
382 for index in 0..10 {
383 let current = if index == 0 {
384 byte.first()
385 .copied()
386 .ok_or_else(|| SyncwebError::operation("drop archive varint byte is missing", name))?
387 } else {
388 read_exact(reader, &mut byte, "failed to read drop archive").await?;
389 byte.first()
390 .copied()
391 .ok_or_else(|| SyncwebError::operation("drop archive varint byte is missing", name))?
392 };
393 let bits = u64::from(current & 0x7f);
394 if shift == 63 && bits > 1 {
395 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
396 }
397 value |= bits << shift;
398 if current & 0x80 == 0 {
399 return Ok(Some(value));
400 }
401 if index == 9 {
402 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
403 }
404 shift = shift
405 .checked_add(7)
406 .ok_or_else(|| SyncwebError::operation("CAR varint shift overflow", name))?;
407 }
408 Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")))
409}
410
411async fn read_exact<R>(reader: &mut R, bytes: &mut [u8], context: &'static str) -> Result<()>
412where
413 R: AsyncRead + Unpin,
414{
415 reader
416 .read_exact(bytes)
417 .await
418 .map(|_| ())
419 .map_err(|error| SyncwebError::operation(context, error))
420}
421
422fn parse_car_header(header: &[u8]) -> Result<Hash> {
423 let mut offset = 0_usize;
424 let root_marker = read_slice_byte(header, &mut offset, "CAR root field")?;
425 if root_marker != 0x0a {
426 return Err(SyncwebError::InvalidConfig(
427 "CAR header does not contain a root CID".to_owned(),
428 ));
429 }
430 let cid_length_raw = read_slice_varint(header, &mut offset, "CAR root CID length")?;
431 let cid_length = usize::try_from(cid_length_raw)
432 .map_err(|error| SyncwebError::operation("CAR root CID length is not representable", error))?;
433 let cid_end = offset
434 .checked_add(cid_length)
435 .ok_or_else(|| SyncwebError::operation("CAR root CID range overflow", "usize limit exceeded"))?;
436 let cid = header
437 .get(offset..cid_end)
438 .ok_or_else(|| SyncwebError::InvalidConfig("CAR root CID is truncated".to_owned()))?;
439 offset = cid_end;
440 let version_marker = read_slice_byte(header, &mut offset, "CAR version field")?;
441 if version_marker != 0x10 {
442 return Err(SyncwebError::InvalidConfig(
443 "CAR header does not contain a version".to_owned(),
444 ));
445 }
446 let version = read_slice_varint(header, &mut offset, "CAR version")?;
447 if version != CAR_VERSION || offset != header.len() {
448 return Err(SyncwebError::InvalidConfig(
449 "unsupported or malformed CAR header".to_owned(),
450 ));
451 }
452 parse_cid(cid)
453}
454
455fn parse_cid(cid: &[u8]) -> Result<Hash> {
456 let mut offset = 0_usize;
457 let version = read_slice_varint(cid, &mut offset, "CID version")?;
458 let codec = read_slice_varint(cid, &mut offset, "CID codec")?;
459 let hash_code = read_slice_varint(cid, &mut offset, "CID multihash code")?;
460 let hash_size = read_slice_varint(cid, &mut offset, "CID multihash length")?;
461 if version != CID_VERSION || codec != RAW_CODEC || hash_code != BLAKE3_MULTIHASH || hash_size != HASH_SIZE {
462 return Err(SyncwebError::InvalidConfig(
463 "drop contains an unsupported root content identifier".to_owned(),
464 ));
465 }
466 let hash_end = offset
467 .checked_add(
468 usize::try_from(HASH_SIZE)
469 .map_err(|error| SyncwebError::operation("CID hash length is not representable", error))?,
470 )
471 .ok_or_else(|| SyncwebError::operation("CID hash range overflow", "usize limit exceeded"))?;
472 let digest = cid
473 .get(offset..hash_end)
474 .ok_or_else(|| SyncwebError::InvalidConfig("CID digest is truncated".to_owned()))?;
475 if hash_end != cid.len() {
476 return Err(SyncwebError::InvalidConfig("CID contains trailing bytes".to_owned()));
477 }
478 let hash: [u8; 32] = digest
479 .try_into()
480 .map_err(|error| SyncwebError::InvalidConfig(format!("CID digest must be 32 bytes: {error}")))?;
481 Ok(Hash::from_bytes(hash))
482}
483
484fn read_slice_byte(bytes: &[u8], offset: &mut usize, name: &'static str) -> Result<u8> {
485 let byte = bytes
486 .get(*offset)
487 .copied()
488 .ok_or_else(|| SyncwebError::InvalidConfig(format!("{name} is truncated")))?;
489 *offset = offset
490 .checked_add(1)
491 .ok_or_else(|| SyncwebError::operation("CAR offset overflow", name))?;
492 Ok(byte)
493}
494
495fn read_slice_varint(bytes: &[u8], offset: &mut usize, name: &'static str) -> Result<u64> {
496 let mut value = 0_u64;
497 let mut shift = 0_u32;
498 for index in 0..10 {
499 let byte = read_slice_byte(bytes, offset, name)?;
500 let bits = u64::from(byte & 0x7f);
501 if shift == 63 && bits > 1 {
502 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
503 }
504 value |= bits << shift;
505 if byte & 0x80 == 0 {
506 return Ok(value);
507 }
508 if index == 9 {
509 return Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")));
510 }
511 shift = shift
512 .checked_add(7)
513 .ok_or_else(|| SyncwebError::operation("CAR varint shift overflow", name))?;
514 }
515 Err(SyncwebError::InvalidConfig(format!("{name} varint is too large")))
516}