commit 8d857be8772c81413cf8e565e0cc5327d08a3922
parent 73e79be7ed0e9c2680fb68641a82163395872603
Author: triesap <tyson@radroots.org>
Date: Tue, 28 Jul 2026 17:25:28 +0000
event_store: validate generated manifest envelopes
- bind manifest byte lengths
- authenticate declared digests
- parse only verified JSON bytes
- cover each envelope failure
Diffstat:
2 files changed, 127 insertions(+), 51 deletions(-)
diff --git a/contracts/event_store_production_sources.toml b/contracts/event_store_production_sources.toml
@@ -11,7 +11,7 @@ sha256 = "7d588cd181f42b6c8731d84f71e5a778cd6605add07dc9b7d0b8144b51a44a9f"
[[sources]]
path = "crates/event_store/src/migrations.rs"
-sha256 = "4938b568826bbd7c3af87acdfb92eec32034d5bf91703479b6300dbf4f8f9063"
+sha256 = "f698d03ee78de42bfef98c3689c7db3fb3796ad95f2055fe7bf80f290733dfa8"
[[sources]]
path = "crates/event_store/src/model.rs"
diff --git a/crates/event_store/src/migrations.rs b/crates/event_store/src/migrations.rs
@@ -868,6 +868,37 @@ fn parse_generated_manifest(
}
}
+struct GeneratedManifestEnvelope<'a> {
+ name: &'static str,
+ bytes: &'a [u8],
+ expected_byte_length: usize,
+ migration_version: u32,
+ expected_sha256: &'a str,
+ byte_length_reason: &'static str,
+ digest_reason: &'static str,
+}
+
+fn validate_generated_manifest_envelope(
+ envelope: GeneratedManifestEnvelope<'_>,
+) -> Result<serde_json::Value, RadrootsEventStoreError> {
+ if envelope.bytes.len() != envelope.expected_byte_length {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: envelope.byte_length_reason.to_owned(),
+ });
+ }
+ validate_sha256_literal(
+ envelope.migration_version,
+ "hook manifest",
+ envelope.expected_sha256,
+ )?;
+ if sha256_hex(envelope.bytes) != envelope.expected_sha256 {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: envelope.digest_reason.to_owned(),
+ });
+ }
+ parse_generated_manifest(envelope.bytes, envelope.name)
+}
+
fn generated_manifest_u128_to_u64(
value: u128,
reason: &'static str,
@@ -894,22 +925,15 @@ fn generated_manifest_i64_to_u64(
fn validate_generated_nip09_manifest_descriptor() -> Result<(), RadrootsEventStoreError> {
let bytes = nip09_manifest::NIP09_RECONCILIATION_MANIFEST_JSON.as_bytes();
- if bytes.len() != nip09_manifest::NIP09_RECONCILIATION_MANIFEST_BYTE_LENGTH {
- return Err(RadrootsEventStoreError::MigrationRegistryDefect {
- reason: "generated NIP-09 manifest byte length is inconsistent".to_owned(),
- });
- }
- validate_sha256_literal(
- nip09_manifest::NIP09_RECONCILIATION_MIGRATION_VERSION,
- "hook manifest",
- nip09_manifest::NIP09_RECONCILIATION_MANIFEST_SHA256,
- )?;
- if sha256_hex(bytes) != nip09_manifest::NIP09_RECONCILIATION_MANIFEST_SHA256 {
- return Err(RadrootsEventStoreError::MigrationRegistryDefect {
- reason: "generated NIP-09 manifest digest is inconsistent".to_owned(),
- });
- }
- let manifest = parse_generated_manifest(bytes, "NIP-09")?;
+ let manifest = validate_generated_manifest_envelope(GeneratedManifestEnvelope {
+ name: "NIP-09",
+ bytes,
+ expected_byte_length: nip09_manifest::NIP09_RECONCILIATION_MANIFEST_BYTE_LENGTH,
+ migration_version: nip09_manifest::NIP09_RECONCILIATION_MIGRATION_VERSION,
+ expected_sha256: nip09_manifest::NIP09_RECONCILIATION_MANIFEST_SHA256,
+ byte_length_reason: "generated NIP-09 manifest byte length is inconsistent",
+ digest_reason: "generated NIP-09 manifest digest is inconsistent",
+ })?;
let up_byte_length = generated_manifest_u128_to_u64(
nip09_manifest::NIP09_RECONCILIATION_MIGRATION_UP_BYTE_LENGTH as u128,
"generated NIP-09 migration up byte length is out of range",
@@ -1039,24 +1063,15 @@ fn validate_generated_nip09_manifest_descriptor() -> Result<(), RadrootsEventSto
fn validate_generated_food_availability_projection_manifest_descriptor()
-> Result<(), RadrootsEventStoreError> {
let bytes = food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_JSON.as_bytes();
- if bytes.len() != food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_BYTE_LENGTH {
- return Err(RadrootsEventStoreError::MigrationRegistryDefect {
- reason: "generated FoodAvailability projection manifest byte length is inconsistent"
- .to_owned(),
- });
- }
- validate_sha256_literal(
- food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_VERSION,
- "hook manifest",
- food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256,
- )?;
- if sha256_hex(bytes) != food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256 {
- return Err(RadrootsEventStoreError::MigrationRegistryDefect {
- reason: "generated FoodAvailability projection manifest digest is inconsistent"
- .to_owned(),
- });
- }
- let manifest = parse_generated_manifest(bytes, "FoodAvailability projection")?;
+ let manifest = validate_generated_manifest_envelope(GeneratedManifestEnvelope {
+ name: "FoodAvailability projection",
+ bytes,
+ expected_byte_length: food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_BYTE_LENGTH,
+ migration_version: food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_VERSION,
+ expected_sha256: food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256,
+ byte_length_reason: "generated FoodAvailability projection manifest byte length is inconsistent",
+ digest_reason: "generated FoodAvailability projection manifest digest is inconsistent",
+ })?;
let up_byte_length = generated_manifest_u128_to_u64(
food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_BYTE_LENGTH as u128,
"generated FoodAvailability migration up byte length is out of range",
@@ -1247,22 +1262,15 @@ fn validate_generated_source_maintenance_manifest_descriptor() -> Result<(), Rad
use source_maintenance_manifest as source_manifest;
let bytes = source_manifest::SOURCE_MAINTENANCE_MANIFEST_JSON.as_bytes();
- if bytes.len() != source_manifest::SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH {
- return Err(RadrootsEventStoreError::MigrationRegistryDefect {
- reason: "generated source-maintenance manifest byte length is inconsistent".to_owned(),
- });
- }
- validate_sha256_literal(
- source_manifest::SOURCE_MAINTENANCE_MIGRATION_VERSION,
- "hook manifest",
- source_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256,
- )?;
- if sha256_hex(bytes) != source_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256 {
- return Err(RadrootsEventStoreError::MigrationRegistryDefect {
- reason: "generated source-maintenance manifest digest is inconsistent".to_owned(),
- });
- }
- let manifest = parse_generated_manifest(bytes, "source-maintenance")?;
+ let manifest = validate_generated_manifest_envelope(GeneratedManifestEnvelope {
+ name: "source-maintenance",
+ bytes,
+ expected_byte_length: source_manifest::SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH,
+ migration_version: source_manifest::SOURCE_MAINTENANCE_MIGRATION_VERSION,
+ expected_sha256: source_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256,
+ byte_length_reason: "generated source-maintenance manifest byte length is inconsistent",
+ digest_reason: "generated source-maintenance manifest digest is inconsistent",
+ })?;
validate_generated_manifest_metadata(
&manifest,
&[
@@ -1743,6 +1751,74 @@ mod migration_framework {
if reason.starts_with("generated fixture manifest JSON is invalid:")
));
+ let valid_bytes = br#"{"schema_version":1}"#;
+ let valid_sha256 = sha256_hex(valid_bytes);
+ validate_generated_manifest_envelope(GeneratedManifestEnvelope {
+ name: "fixture",
+ bytes: valid_bytes,
+ expected_byte_length: valid_bytes.len(),
+ migration_version: 1,
+ expected_sha256: valid_sha256.as_str(),
+ byte_length_reason: "fixture length",
+ digest_reason: "fixture digest",
+ })
+ .expect("valid generated manifest envelope");
+ assert!(matches!(
+ validate_generated_manifest_envelope(GeneratedManifestEnvelope {
+ name: "fixture",
+ bytes: valid_bytes,
+ expected_byte_length: valid_bytes.len() + 1,
+ migration_version: 1,
+ expected_sha256: valid_sha256.as_str(),
+ byte_length_reason: "fixture length",
+ digest_reason: "fixture digest",
+ }),
+ Err(RadrootsEventStoreError::MigrationRegistryDefect { reason })
+ if reason == "fixture length"
+ ));
+ let other_sha256 = sha256_hex(b"{}");
+ assert!(matches!(
+ validate_generated_manifest_envelope(GeneratedManifestEnvelope {
+ name: "fixture",
+ bytes: valid_bytes,
+ expected_byte_length: valid_bytes.len(),
+ migration_version: 1,
+ expected_sha256: other_sha256.as_str(),
+ byte_length_reason: "fixture length",
+ digest_reason: "fixture digest",
+ }),
+ Err(RadrootsEventStoreError::MigrationRegistryDefect { reason })
+ if reason == "fixture digest"
+ ));
+ assert!(matches!(
+ validate_generated_manifest_envelope(GeneratedManifestEnvelope {
+ name: "fixture",
+ bytes: valid_bytes,
+ expected_byte_length: valid_bytes.len(),
+ migration_version: 1,
+ expected_sha256: "invalid",
+ byte_length_reason: "fixture length",
+ digest_reason: "fixture digest",
+ }),
+ Err(RadrootsEventStoreError::MigrationRegistryDefect { reason })
+ if reason.contains("invalid hook manifest SHA-256 literal")
+ ));
+ let invalid_json = b"{";
+ let invalid_json_sha256 = sha256_hex(invalid_json);
+ assert!(matches!(
+ validate_generated_manifest_envelope(GeneratedManifestEnvelope {
+ name: "fixture",
+ bytes: invalid_json,
+ expected_byte_length: invalid_json.len(),
+ migration_version: 1,
+ expected_sha256: invalid_json_sha256.as_str(),
+ byte_length_reason: "fixture length",
+ digest_reason: "fixture digest",
+ }),
+ Err(RadrootsEventStoreError::MigrationRegistryDefect { reason })
+ if reason.starts_with("generated fixture manifest JSON is invalid:")
+ ));
+
assert_eq!(
generated_manifest_u128_to_u64(u128::from(u64::MAX), "fixture u128 range")
.expect("maximum u64"),