diff --git a/docs/configuration.md b/docs/configuration.md index a85aa6c..007347f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -60,6 +60,7 @@ hydrant is configured via environment variables, all prefixed with `HYDRANT_` (e | `BACKFILL_STRATEGY` | `auto` | backfill strategy: `full` keeps the existing `getRepo` path, `sparse-filter` attempts authenticated sparse collection backfill before falling back to full, `auto` probes filtered repos and falls back to full for tiny MST roots | | `REPO_FETCH_TIMEOUT` | `5min` | timeout for fetching a repository | | `VERIFY_SIGNATURES` | `full` | signature verification level: `full`, `backfill-only`, or `none` | +| `VERIFY_CIDS` | `true` | reject firehose and backfill CAR blocks whose content does not hash to their claimed CID. without this, signature verification does not cover record content | | `PLC_URL` | `https://plc.wtf`, `https://plc.directory` (full network) | base URL(s) of the PLC directory, comma-separated | | `IDENTITY_CACHE_SIZE` | `1000000` | number of identity entries to cache in memory | diff --git a/src/config.rs b/src/config.rs index 723d112..45001a2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -197,8 +197,9 @@ pub struct Config { /// set via `HYDRANT_GET_REPO_CONCURRENCY_LIMIT`; defaults to 2. pub get_repo_concurrency_limit: usize, - /// if `true`, cryptographically verifies that all imported block content matches their claimed CIDs. - /// set via `HYDRANT_VERIFY_CIDS=true`. + /// if `true`, rejects CAR blocks whose content does not hash to their claimed CID. + /// CAR readers do not check this, so without it signature verification does not cover + /// record content. set via `HYDRANT_VERIFY_CIDS`. default: true. pub verify_cids: bool, /// if `true`, record blocks are not stored; only the index (records, counts, events) is kept. @@ -364,7 +365,7 @@ impl Default for Config { filter_excludes: None, enable_backlinks: false, get_repo_concurrency_limit: 2, - verify_cids: false, + verify_cids: true, only_index_links: false, new_host_limit: Some(50), offline_host_retry_interval: Some(Duration::from_secs(30 * 60)), @@ -588,6 +589,7 @@ impl fmt::Display for Config { config_line!(f, "plc urls", format_args!("{:?}", self.plc_urls))?; config_line!(f, "full network indexing", self.full_network)?; config_line!(f, "verify signatures", self.verify_signatures)?; + config_line!(f, "verify cids", self.verify_cids)?; config_line!(f, "per-pds concurrency", self.per_pds_concurrency)?; config_line!(f, "backfill enabled", self.enable_backfill)?; config_line!(f, "firehose enabled", self.enable_firehose)?; diff --git a/src/control/hydrant/run.rs b/src/control/hydrant/run.rs index e83313d..111d557 100644 --- a/src/control/hydrant/run.rs +++ b/src/control/hydrant/run.rs @@ -60,11 +60,7 @@ impl Hydrant { sink_seed, matches!(config.verify_signatures, SignatureVerification::Full), config.firehose_workers, - crate::ingest::validation::ValidationOptions { - verify_mst: config.verify_mst, - rev_clock_skew_secs: config.rev_clock_skew_secs, - verify_cids: config.verify_cids, - }, + crate::ingest::validation::ValidationOptions::from(&*config), ); // 5. spawn the backfill worker (not used in relay mode) diff --git a/src/ingest/validation.rs b/src/ingest/validation.rs index 94ed260..ee47094 100644 --- a/src/ingest/validation.rs +++ b/src/ingest/validation.rs @@ -112,20 +112,20 @@ pub struct ValidatedSync { } pub struct ValidationOptions { - /// clock drift window for future-rev rejection (seconds). default: 300 + /// clock drift window for future-rev rejection (seconds) pub rev_clock_skew_secs: i64, - /// run MST inversion validation (expensive). default: false + /// run MST inversion validation (expensive) pub verify_mst: bool, - /// cryptographically verify block CIDs. default: false + /// reject CAR blocks whose content does not hash to their claimed CID pub verify_cids: bool, } -impl Default for ValidationOptions { - fn default() -> Self { +impl From<&crate::config::Config> for ValidationOptions { + fn from(config: &crate::config::Config) -> Self { Self { - rev_clock_skew_secs: 300, - verify_mst: false, - verify_cids: false, + rev_clock_skew_secs: config.rev_clock_skew_secs, + verify_mst: config.verify_mst, + verify_cids: config.verify_cids, } } } @@ -448,6 +448,39 @@ fn verify_mst( #[cfg(test)] mod tests { + use super::*; + use crate::ingest::stream::types::Datetime; + use jacquard_common::types::string::Did; + + #[test] + fn default_config_rejects_forged_car_blocks() { + let opts = ValidationOptions::from(&crate::config::Config::default()); + + let rt = tokio::runtime::Runtime::new().unwrap(); + let _guard = rt.enter(); + let claimed = jacquard_repo::mst::util::compute_cid(b"trusted").unwrap(); + let blocks = rt.block_on(async { + let mut car = Vec::new(); + let header = iroh_car::CarHeader::new_v1(vec![claimed]); + let mut writer = iroh_car::CarWriter::new(header, &mut car); + writer.write(claimed, b"forged".to_vec()).await.unwrap(); + writer.finish().await.unwrap(); + car + }); + let msg = Sync { + blocks: blocks.into(), + did: Did::new_static("did:plc:aaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), + rev: "3l2abcdefgh22".into(), + seq: 1, + time: Datetime(chrono::Utc::now().fixed_offset()), + }; + + let Err(SyncValidationError::MalformedCar(err)) = validate_sync(&msg, None, &opts) else { + panic!("forged block was accepted"); + }; + assert!(err.to_string().contains("CAR block CID mismatch"), "{err}"); + } + #[test] fn record_dag_cbor_projection_accepts_data_and_rejects_invalid_bytes() { let encoded = serde_ipld_dagcbor::to_vec(&serde_json::json!({