commit 74da6a37fdf531fafa77568ece335e26640559cb parent d03abe2a9ef46a5fb5568916bd0b4f215ec3dff7 Author: triesap <tyson@radroots.org> Date: Mon, 27 Jul 2026 01:08:30 +0000 blossom: close raster decoder authority - bind the security profile to publication readiness - close generated schema and resource inventories - authenticate fuzz targets and exact raw seeds - reject manifest corpus and dispatcher drift Diffstat:
14 files changed, 3055 insertions(+), 136 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -210,6 +210,22 @@ publish policy both pass for the same source revision. noncanonical, or digest-mutated state before signing. The artifact boundary now admits only nonempty, at-most-10-MiB JPEG, PNG, or still-WebP references whose canonical URLs are at most 4,096 bytes. +<!-- release-change: blossom-raster-decoder-security --> +- Blossom publication raster decoding is now governed as a dedicated security + and cost boundary. Decoded output is capped at `80,000,000` bytes before + allocation, down from the previous `160,000,000` byte ceiling. PNG is + restricted to static 8-bit images with approved color types, one ordered + palette, no critical unknown chunks, and bounded container records; WebP is + restricted to static images whose extended header carries no reserved flags + and exactly one primary chunk. Sequential JPEG decoding now charges bounded + scan, block, coefficient-step, marker-record, and entropy bit-read budgets + so no input can loop or allocate beyond its declared ceiling. Unsupported + PNG and WebP raster processes fail with the new stable + `publication_raster_process_forbidden` error. An exact 30-case regression + corpus, a Nix-pinned independent-decoder differential, an isolated + twelve-process maximum-resource matrix with a 128 MiB peak-RSS gate, and + deterministic libFuzzer smoke targets with one raw seed per case now execute + through the real public readiness API in the governed decoder-security lane. <!-- release-change: blossom-publication-readiness-evidence --> - Blossom publication media now advances beyond local byte verification only after typed BUD-02 status and descriptor agreement, an independent BUD-01 @@ -221,7 +237,7 @@ publish policy both pass for the same source revision. sequential SOF0/SOF1 and combines exact entropy accounting with pinned strict `zune-jpeg` and `zune-core` RGB decoding; PNG and WebP use a separately pinned two-format `image` build. - Decoded output is bounded to 160,000,000 bytes before allocation; callers + Decoded output is bounded to 80,000,000 bytes before allocation; callers cannot provide decode claims. Deterministic per-URL evidence remains transport-neutral and contains no HTTP credentials, BUD-11 material, entitlement decision, or private service diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml @@ -511,6 +511,17 @@ semver_impacts = ["change_exported_constant_value", "fix_packaging_metadata"] summary = "Regenerate the remote SP1 Core proof fixture for the validator-set witness identity, exercise real cryptographic verification, and allow the SP1-pinned guest compiler to trail the workspace Rust version during locked guest builds." [[changes]] +id = "blossom-raster-decoder-security" +classification = "breaking" +semver_impacts = [ + "change_exported_constant_value", + "change_exported_algorithm_behavior", + "add_enum_variant", + "add_conformance_vector", +] +summary = "Tighten the publication raster decoder security boundary: cap decoded output at 80,000,000 bytes, restrict PNG and WebP to approved static 8-bit processes with palette, ordering, critical-chunk, and container-record rules, bound sequential JPEG scans, blocks, coefficient steps, and entropy bit reads, and add the raster decoder security conformance vector with independent-decoder differential, peak-RSS, and fuzz gates." + +[[changes]] id = "blossom-publication-readiness-evidence" classification = "feature" semver_impacts = [ diff --git a/crates/blossom/README b/crates/blossom/README @@ -42,11 +42,15 @@ carries no reserved flags and exactly one primary chunk under the same record budget. Sequential JPEG validation charges bounded scan, block, coefficient-step, marker-record, and entropy bit-read budgets, and unsupported PNG or WebP processes fail with the stable -`publication_raster_process_forbidden` error. The decoder boundary is exercised by an isolated twelve-process -maximum-resource matrix held under a 128 MiB peak-RSS gate and an -`aarch64-apple-ios` static compile/link lane. It checks complete-byte, URL, hash, MIME, length, -container, animation, and dimension agreement before emitting deterministic per-URL -evidence. The crate performs no HTTP: an owning runtime supplies only the +`publication_raster_process_forbidden` error. The decoder boundary is exercised +by a checked-in regression corpus, a Nix-pinned independent-decoder +differential, an isolated twelve-process maximum-resource matrix held under a +128 MiB peak-RSS gate, an `aarch64-apple-ios` static compile/link lane, and +deterministic libFuzzer smoke targets; see +`docs/blossom-raster-decoder-security.md` for the governed lanes and the +extended-campaign record. It checks complete-byte, URL, hash, MIME, length, +container, animation, and dimension agreement before emitting deterministic +per-URL evidence. The crate performs no HTTP: an owning runtime supplies only the transport observations and exact complete body. The portable `no_std` core remains available without `raster-decode`, but cannot construct readiness evidence. Policy v1 has no serialized or caller-supplied decode-observation diff --git a/crates/blossom/contracts/raster_decoder_security_v1.descriptor.json b/crates/blossom/contracts/raster_decoder_security_v1.descriptor.json @@ -0,0 +1,25 @@ +{ + "contract_id": "radroots_blossom.raster_decoder_security_v1", + "manifest": { + "byte_length": 31555, + "hash_algorithm": "sha256_bytes_v1", + "path": "crates/blossom/contracts/raster_decoder_security_v1.manifest.json", + "sha256": "4a3c4acd9f2e3b0447465e5a1f744186d4823f0c6beb0665cb951b8dd82139c2" + }, + "manifest_schema": { + "byte_length": 19177, + "hash_algorithm": "sha256_bytes_v1", + "path": "crates/blossom/contracts/raster_decoder_security_v1.manifest.schema.json", + "sha256": "8efaeddcfd209deaa0d60732cfe094a915e6ba1569cb3b09c8c1af9de66fe16e" + }, + "manifest_sidecar": { + "byte_length": 65, + "hash_algorithm": "sha256_bytes_v1", + "path": "crates/blossom/contracts/raster_decoder_security_v1.manifest.sha256", + "sha256": "eb24f1c678c26092b85ffcf9c0ae51f72100018472d63774eb8abd4b00864c6d" + }, + "predecessor_contract_ids": [ + "radroots_blossom.publication_readiness_v1" + ], + "schema_version": 1 +} diff --git a/crates/blossom/contracts/raster_decoder_security_v1.manifest.json b/crates/blossom/contracts/raster_decoder_security_v1.manifest.json @@ -0,0 +1,905 @@ +{ + "schema_version": 1, + "contract_id": "radroots_blossom.raster_decoder_security_v1", + "authority_id": "blossom_raster_decoder_security_v1", + "manifest_schema": { + "path": "crates/blossom/contracts/raster_decoder_security_v1.manifest.schema.json", + "byte_length": 19177, + "sha256": "8efaeddcfd209deaa0d60732cfe094a915e6ba1569cb3b09c8c1af9de66fe16e", + "hash_algorithm": "sha256_bytes_v1" + }, + "predecessors": [ + { + "contract_id": "radroots_blossom.publication_readiness_v1", + "immutable_artifacts": [ + { + "path": "crates/blossom/contracts/publication_readiness_v1.manifest.json", + "byte_length": 13038, + "sha256": "9359c5531548778b4a03e1a603a4048f17ba498b16dad50f7c62cca0ddb6240b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/blossom/contracts/publication_readiness_v1.manifest.schema.json", + "byte_length": 11884, + "sha256": "e62ecf4b43bd03831f7e36e9cb4c98e2ed7e3a12d02cbca48a49e402b3feaa7d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/blossom/contracts/publication_readiness_v1.manifest.sha256", + "byte_length": 65, + "sha256": "e8c2be84eef68e965ac0e119016d6840cb9503e58d5d9c40e13a6512e1ec74b7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/blossom/contracts/publication_readiness_v1.descriptor.json", + "byte_length": 1410, + "sha256": "4a3b33eb2b04b5a56a99b7b5bed45988a0697cb28736bfd899e012f37fc02d93", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/blossom/contracts/publication_readiness_evidence_v1.schema.json", + "byte_length": 1890, + "sha256": "c8f5f3488dd91a660f8eaa018d9aba63d03c882dc3ff22b47ac192b7189b0ce2", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "contracts/conformance/vectors/blossom/publication_readiness.v1.json", + "byte_length": 16423, + "sha256": "6408e3b5bc4a376c7411e304833431af918a4072f196e8a8da55c3f0ef8610c9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/blossom/tests/fixtures/publication_readiness.v1.json", + "byte_length": 16423, + "sha256": "6408e3b5bc4a376c7411e304833431af918a4072f196e8a8da55c3f0ef8610c9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/blossom/tests/publication_readiness.rs", + "byte_length": 33431, + "sha256": "17abd5491bcfe0c717f7f67202c8c2d85912a042167d027727c07d9f07306641", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "contracts/conformance/vectors/blossom/publication_readiness_persistence.v1.json", + "byte_length": 9264, + "sha256": "e892ff6353afe8997a151a9aff4db2fd82c96d94db7bff31bc2269333adc2512", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/blossom/tests/fixtures/publication_readiness_persistence.v1.json", + "byte_length": 9264, + "sha256": "e892ff6353afe8997a151a9aff4db2fd82c96d94db7bff31bc2269333adc2512", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/blossom/tests/publication_readiness_persistence.rs", + "byte_length": 11567, + "sha256": "aa14d20ee5747fcc979907b4b59a67687a3a02852850e33aede65e2ab0e0ca8d", + "hash_algorithm": "sha256_bytes_v1" + } + ], + "source_supersessions": [ + "CHANGELOG.md", + "contracts/releases/1.0.0-alpha.1.toml", + "crates/blossom/Cargo.toml", + "crates/blossom/src/error.rs", + "crates/blossom/src/publication_readiness.rs", + "crates/blossom/src/publication_readiness/sequential_jpeg.rs", + "tools/xtask/src/contract.rs", + "tools/xtask/src/main.rs" + ] + } + ], + "protocol_sources": [ + { + "id": "nostr_nips", + "repository": "https://github.com/nostr-protocol/nips", + "revision": "bdfa7e62ef87fcfcb992b1a27aee49d36b0b4f91" + }, + { + "id": "blossom", + "repository": "https://github.com/hzrd149/blossom", + "revision": "b5bd2801d1763aa635fc8fea7a76597e0eb18990" + } + ], + "resource_limits": { + "max_raster_bytes": 10485760, + "max_decoded_bytes": 80000000, + "max_dimension": 16384, + "max_pixels": 20000000, + "max_container_records": 65536, + "jpeg_max_scans": 4, + "jpeg_max_blocks": 3200000, + "jpeg_max_coefficient_steps": 204800000, + "jpeg_entropy_bit_reads_per_byte": 8, + "peak_rss_kib_limit": 131072 + }, + "decoder_profile": { + "accepted_formats": [ + "image/jpeg", + "image/png", + "image/webp" + ], + "accepted_jpeg_processes": [ + "sof0_baseline_8bit", + "sof1_extended_sequential_8bit" + ], + "accepted_png_color_types": [ + 0, + 2, + 3, + 4, + 6 + ], + "normative_error_codes": [ + "publication_raster_empty", + "publication_raster_byte_limit_exceeded", + "unsupported_publication_raster_media_type", + "invalid_publication_raster", + "publication_jpeg_process_forbidden", + "publication_raster_animation_forbidden", + "publication_raster_container_dimension_mismatch", + "publication_raster_decode_allocation_failed", + "publication_raster_decoded_byte_limit_exceeded", + "publication_raster_decode_failed", + "publication_raster_dimensions_out_of_range", + "publication_raster_pixel_limit_exceeded", + "publication_raster_process_forbidden" + ], + "observed_rejection_error_codes": [ + "invalid_publication_raster", + "publication_jpeg_process_forbidden", + "publication_raster_animation_forbidden", + "publication_raster_decode_failed", + "publication_raster_dimensions_out_of_range", + "publication_raster_pixel_limit_exceeded", + "publication_raster_process_forbidden" + ], + "required_accepted_mutations": [ + "jpeg_sof1", + "none" + ], + "required_rejected_mutations": [ + "jpeg_entropy_truncated", + "jpeg_huffman_overfull", + "jpeg_precision_12", + "jpeg_progressive", + "jpeg_wrong_restart", + "png_16_bit", + "png_animation", + "png_corrupt_crc", + "png_corrupt_deflate", + "png_dimension_over", + "png_pixel_over", + "truncate_half", + "webp_animation", + "webp_duplicate_primary" + ] + }, + "resource_probes": [ + { + "id": "jpeg_grayscale", + "format": "jpeg", + "process": "sof0_grayscale_8bit", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 60000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_grayscale", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "jpeg_rgb", + "format": "jpeg", + "process": "sof0_rgb_8bit", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 60000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_rgb", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "jpeg_cmyk", + "format": "jpeg", + "process": "sof0_cmyk_8bit", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 60000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_cmyk", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "jpeg_sof1", + "format": "jpeg", + "process": "sof1_extended_sequential_8bit", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 60000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_rgb_sof1_marker", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "png_rgb", + "format": "png", + "process": "rgb_8bit", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 60000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_png24", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "png_palette", + "format": "png", + "process": "palette_8bit", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 60000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_palette", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "png_rgba", + "format": "png", + "process": "rgba_8bit", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 80000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_png32", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "png_adam7", + "format": "png", + "process": "adam7_rgba_8bit", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 80000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_adam7", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "webp_vp8_rgb", + "format": "webp", + "process": "vp8_rgb", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 80000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_webp_lossy_rgb", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "webp_vp8_alpha", + "format": "webp", + "process": "vp8_alpha", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 80000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_webp_lossy_alpha", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "webp_vp8l_rgb", + "format": "webp", + "process": "vp8l_rgb", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 80000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_webp_lossless_rgb", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + }, + { + "id": "webp_vp8l_alpha", + "format": "webp", + "process": "vp8l_alpha", + "width": 5000, + "height": 4000, + "max_encoded_bytes": 10485760, + "max_decoded_bytes": 80000000, + "executor_test": "maximum_resource_probe", + "fixture_authority": "imagemagick_webp_lossless_alpha", + "repetitions": 3, + "peak_rss_kib_limit": 131072 + } + ], + "fuzz_campaign": { + "project_manifest": { + "path": "fuzz/Cargo.toml", + "byte_length": 628, + "sha256": "d73bb44034df2f083fa2f1f6d437639fa7d1fede4c563fc0864d9859a03d01cb", + "hash_algorithm": "sha256_bytes_v1" + }, + "project_lockfile": { + "path": "fuzz/Cargo.lock", + "byte_length": 19013, + "sha256": "05add36124173589e21d1e9706b77dc9622542bd5072346b3a355ada65e39e64", + "hash_algorithm": "sha256_bytes_v1" + }, + "toolchain": { + "path": "rust-toolchain-fuzz.toml", + "byte_length": 89, + "sha256": "844804fc214567a8b3151848dbb141f11310be956cf797554ffaef58d380c063", + "hash_algorithm": "sha256_bytes_v1" + }, + "toolchain_channel": "nightly-2026-07-15", + "toolchain_components": [ + "rust-src" + ], + "toolchain_profile": "minimal", + "engine": "libfuzzer", + "sanitizer": "address", + "common_harness": { + "path": "fuzz/fuzz_targets/common.rs", + "byte_length": 2024, + "sha256": "bde259afbc894af9cf60f160e6664350430e4b0af90b293112b6e768c0127d2d", + "hash_algorithm": "sha256_bytes_v1" + }, + "targets": [ + { + "name": "publication_jpeg", + "format": "jpeg", + "media_type": "image/jpeg", + "extension": "jpg", + "binary": { + "path": "fuzz/fuzz_targets/publication_jpeg.rs", + "byte_length": 141, + "sha256": "ed3e20c5e3f0521061f0b4ced734bf3b0a5581472312812ccb00b7742380a287", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "name": "publication_png", + "format": "png", + "media_type": "image/png", + "extension": "png", + "binary": { + "path": "fuzz/fuzz_targets/publication_png.rs", + "byte_length": 140, + "sha256": "ae3e94d747e37384e3badf250ba0cebedcea86d8512afa61c6053f2c45d9ccb5", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "name": "publication_webp", + "format": "webp", + "media_type": "image/webp", + "extension": "webp", + "binary": { + "path": "fuzz/fuzz_targets/publication_webp.rs", + "byte_length": 142, + "sha256": "f2835f2bfe44a8cf5e18c932b8cb5b31248a1e13e189c4d84972685d2e7caa56", + "hash_algorithm": "sha256_bytes_v1" + } + } + ], + "corpus_seeds": [ + { + "vector_id": "jpeg_baseline_gray_8bit", + "target": "publication_jpeg", + "format": "jpeg", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_jpeg/jpeg_baseline_gray_8bit.bin", + "byte_length": 159, + "sha256": "7fa92b6a273d259a2e9bc88d573f41d17a888460065662f2d9e622bfc8e155ce", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "jpeg_baseline_rgb_8bit", + "target": "publication_jpeg", + "format": "jpeg", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_jpeg/jpeg_baseline_rgb_8bit.bin", + "byte_length": 285, + "sha256": "c2be04cb3f623b7f9397c8819a01355588fdbbe53ec3cacc6acb6dcdbfc80eb6", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "jpeg_baseline_cmyk_8bit", + "target": "publication_jpeg", + "format": "jpeg", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_jpeg/jpeg_baseline_cmyk_8bit.bin", + "byte_length": 291, + "sha256": "b8cd844b0568f5c609fc4278eca92fa7610d587814e0e899f38e0c31fdc9f802", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "jpeg_extended_sequential_sof1", + "target": "publication_jpeg", + "format": "jpeg", + "provenance_mutation": "jpeg_sof1", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_jpeg/jpeg_extended_sequential_sof1.bin", + "byte_length": 285, + "sha256": "15eaf062217a767d41ee1fbc597a5d6b8bc710a6a6c01025cfe0ad18d6c8f52d", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_gray_8bit", + "target": "publication_png", + "format": "png", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_png/png_gray_8bit.bin", + "byte_length": 68, + "sha256": "461a58191e32992a3801fd933d60741e530f4d10e8af510b48db8ddb3232d93e", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_rgb_8bit", + "target": "publication_png", + "format": "png", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_png/png_rgb_8bit.bin", + "byte_length": 72, + "sha256": "00b1038ca37dcc47d85bbefb5e7396617af18476cea682e650772c51fa66c417", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_indexed_8bit", + "target": "publication_png", + "format": "png", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_png/png_indexed_8bit.bin", + "byte_length": 86, + "sha256": "355eaeb8ecb24c8309f3be89fb79ef1f0a031b93d332db6ef447686e9bc96f7c", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_gray_alpha_8bit", + "target": "publication_png", + "format": "png", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_png/png_gray_alpha_8bit.bin", + "byte_length": 70, + "sha256": "7ea9058a2e5506cd365a327e552679cd3d7acb497b54e514fd8f116272c42ff7", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_rgba_8bit", + "target": "publication_png", + "format": "png", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_png/png_rgba_8bit.bin", + "byte_length": 74, + "sha256": "fe64867950903ef8bba758eabe81300c166ff3acc5c7abb37b06bb1ee8cdea74", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_adam7_rgba_8bit", + "target": "publication_png", + "format": "png", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_png/png_adam7_rgba_8bit.bin", + "byte_length": 130, + "sha256": "c49d6e3270b829579b95922180303d4af4961cd4f45d629fe2a812e92e207921", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "webp_lossless_8bit", + "target": "publication_webp", + "format": "webp", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_webp/webp_lossless_8bit.bin", + "byte_length": 38, + "sha256": "316448507d8dbacadfe50321bdef964692500eebef47c420a961c95bdf5d00fe", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "webp_lossy_8bit", + "target": "publication_webp", + "format": "webp", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_webp/webp_lossy_8bit.bin", + "byte_length": 54, + "sha256": "7d14a4235af773378b15bf0762eaf0ca1cdf28abe88428dadc48d656c52beeb4", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "webp_lossless_alpha_8bit", + "target": "publication_webp", + "format": "webp", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_webp/webp_lossless_alpha_8bit.bin", + "byte_length": 38, + "sha256": "6a0857f2bc3478d81be7cd54f444334a02f043c5955f6651167a43c1d0fb8c07", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "webp_lossy_alpha_8bit", + "target": "publication_webp", + "format": "webp", + "provenance_mutation": "none", + "expected_accepted": true, + "file": { + "path": "fuzz/corpus/publication_webp/webp_lossy_alpha_8bit.bin", + "byte_length": 88, + "sha256": "c633a9964d32e414e171d03ab8f8dc46e11df33562450d52d36b40ffea757895", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "jpeg_progressive_forbidden", + "target": "publication_jpeg", + "format": "jpeg", + "provenance_mutation": "jpeg_progressive", + "expected_accepted": false, + "expected_error": "publication_jpeg_process_forbidden", + "file": { + "path": "fuzz/corpus/publication_jpeg/jpeg_progressive_forbidden.bin", + "byte_length": 285, + "sha256": "a3d8da301ab0ce07e89da6ff5c0599c994d0c230acf19c0f2bc4d9f17ba3e588", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "jpeg_twelve_bit_forbidden", + "target": "publication_jpeg", + "format": "jpeg", + "provenance_mutation": "jpeg_precision_12", + "expected_accepted": false, + "expected_error": "publication_jpeg_process_forbidden", + "file": { + "path": "fuzz/corpus/publication_jpeg/jpeg_twelve_bit_forbidden.bin", + "byte_length": 285, + "sha256": "8f1486d27c1ff6a2f5077137dcc83ecf11aaa68cb083c26ada87baecc94f2d8b", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "jpeg_entropy_truncated", + "target": "publication_jpeg", + "format": "jpeg", + "provenance_mutation": "jpeg_entropy_truncated", + "expected_accepted": false, + "expected_error": "publication_raster_decode_failed", + "file": { + "path": "fuzz/corpus/publication_jpeg/jpeg_entropy_truncated.bin", + "byte_length": 283, + "sha256": "69c773c027f963e9520039d6e959b3da747c1cb3171d3f39227670f950571c17", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "jpeg_huffman_overfull", + "target": "publication_jpeg", + "format": "jpeg", + "provenance_mutation": "jpeg_huffman_overfull", + "expected_accepted": false, + "expected_error": "publication_raster_decode_failed", + "file": { + "path": "fuzz/corpus/publication_jpeg/jpeg_huffman_overfull.bin", + "byte_length": 285, + "sha256": "9d588613faf5a5fc3e3e2b8faae8ce9dbaf18874a465e3dfbea2ebd4ac83e389", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "jpeg_wrong_restart_marker", + "target": "publication_jpeg", + "format": "jpeg", + "provenance_mutation": "jpeg_wrong_restart", + "expected_accepted": false, + "expected_error": "publication_raster_decode_failed", + "file": { + "path": "fuzz/corpus/publication_jpeg/jpeg_wrong_restart_marker.bin", + "byte_length": 77, + "sha256": "9956c9a8a03324f181c41dfe7fcf751bd8c9e339d49637ddf16f6570bbeb21f2", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_sixteen_bit_forbidden", + "target": "publication_png", + "format": "png", + "provenance_mutation": "png_16_bit", + "expected_accepted": false, + "expected_error": "publication_raster_process_forbidden", + "file": { + "path": "fuzz/corpus/publication_png/png_sixteen_bit_forbidden.bin", + "byte_length": 70, + "sha256": "cc5c6ee874284c68dc695044048c7315e54b19447940602902f3e94aa5433ce6", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_animation_forbidden", + "target": "publication_png", + "format": "png", + "provenance_mutation": "png_animation", + "expected_accepted": false, + "expected_error": "publication_raster_animation_forbidden", + "file": { + "path": "fuzz/corpus/publication_png/png_animation_forbidden.bin", + "byte_length": 128, + "sha256": "bbfb514b688df5a77a5ad37c0da8670983eea80aa03dee3d32a517d851eae426", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_corrupt_crc", + "target": "publication_png", + "format": "png", + "provenance_mutation": "png_corrupt_crc", + "expected_accepted": false, + "expected_error": "publication_raster_decode_failed", + "file": { + "path": "fuzz/corpus/publication_png/png_corrupt_crc.bin", + "byte_length": 70, + "sha256": "49c97a97da4b5eafcf09ee91178af6a873affacedbf67ee379e96918626c4e83", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_corrupt_deflate", + "target": "publication_png", + "format": "png", + "provenance_mutation": "png_corrupt_deflate", + "expected_accepted": false, + "expected_error": "publication_raster_decode_failed", + "file": { + "path": "fuzz/corpus/publication_png/png_corrupt_deflate.bin", + "byte_length": 70, + "sha256": "1719868f92b7cfd4e4f11f6345a32cde86e19fd6d482c3c0deee7bdabe665cd4", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_dimension_over_limit", + "target": "publication_png", + "format": "png", + "provenance_mutation": "png_dimension_over", + "expected_accepted": false, + "expected_error": "publication_raster_dimensions_out_of_range", + "file": { + "path": "fuzz/corpus/publication_png/png_dimension_over_limit.bin", + "byte_length": 70, + "sha256": "ab7addacea8c08de725e33723ff2b9445be77a8d50e949962a24886d81ef0203", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_pixel_limit", + "target": "publication_png", + "format": "png", + "provenance_mutation": "png_pixel_over", + "expected_accepted": false, + "expected_error": "publication_raster_pixel_limit_exceeded", + "file": { + "path": "fuzz/corpus/publication_png/png_pixel_limit.bin", + "byte_length": 70, + "sha256": "fbd966761eed339f5a8fdb44fe6050e2fffe05415428802c8db068bd9b15cfed", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "png_truncated", + "target": "publication_png", + "format": "png", + "provenance_mutation": "truncate_half", + "expected_accepted": false, + "expected_error": "invalid_publication_raster", + "file": { + "path": "fuzz/corpus/publication_png/png_truncated.bin", + "byte_length": 35, + "sha256": "a4ec7382dbf2d70d7764f8c854e52ead90b5455749ad1a0feb5ae1f25479dae5", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "webp_animation_forbidden", + "target": "publication_webp", + "format": "webp", + "provenance_mutation": "webp_animation", + "expected_accepted": false, + "expected_error": "publication_raster_animation_forbidden", + "file": { + "path": "fuzz/corpus/publication_webp/webp_animation_forbidden.bin", + "byte_length": 94, + "sha256": "f53e7f7baeca695b8958f50a8eb4883ebad4fe18eb54976563d9c6a56b35b85b", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "webp_duplicate_primary", + "target": "publication_webp", + "format": "webp", + "provenance_mutation": "webp_duplicate_primary", + "expected_accepted": false, + "expected_error": "invalid_publication_raster", + "file": { + "path": "fuzz/corpus/publication_webp/webp_duplicate_primary.bin", + "byte_length": 64, + "sha256": "58875ddbef1b6eba0581259f36ee35a659373ceb524c9eba08585fedb320cf2e", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "webp_truncated", + "target": "publication_webp", + "format": "webp", + "provenance_mutation": "truncate_half", + "expected_accepted": false, + "expected_error": "invalid_publication_raster", + "file": { + "path": "fuzz/corpus/publication_webp/webp_truncated.bin", + "byte_length": 19, + "sha256": "ae50569b401bc226004c9bddc9199e5928ecd2a58c1662281df2f5342682d69c", + "hash_algorithm": "sha256_bytes_v1" + } + }, + { + "vector_id": "jpeg_truncated", + "target": "publication_jpeg", + "format": "jpeg", + "provenance_mutation": "truncate_half", + "expected_accepted": false, + "expected_error": "invalid_publication_raster", + "file": { + "path": "fuzz/corpus/publication_jpeg/jpeg_truncated.bin", + "byte_length": 79, + "sha256": "d4d4a33838e0dc6535b97cd2b5f775e418c23f502554e029c6e0346b5e17eb65", + "hash_algorithm": "sha256_bytes_v1" + } + } + ], + "smoke_runs": 256, + "smoke_seed": 424242, + "smoke_max_input_bytes": 65536, + "smoke_timeout_seconds": 5, + "engine_rss_limit_mb": 2048, + "extended_campaign_document": { + "path": "docs/blossom-raster-decoder-security.md", + "byte_length": 7764, + "sha256": "18e7971504f0228d81e028e5845e9ca47e9b3bdcbaeb2cf1b953aaba8484146d", + "hash_algorithm": "sha256_bytes_v1" + } + }, + "nix_lanes": { + "app": "decoder-security", + "devshell": "decoder-security", + "stable_check": "blossom-raster-decode-test", + "fuzz_check": "blossom-decoder-fuzz-smoke" + }, + "operation": { + "key": "blossom_verify_publication_readiness", + "id": "blossom.verify_publication_readiness", + "primary_vector": "contracts/conformance/vectors/blossom/publication_readiness.v1.json", + "primary_case_kinds": [ + "blossom.verify_publication_readiness.valid", + "blossom.verify_publication_readiness.invalid" + ], + "security_case_kinds": [ + "blossom.verify_publication_readiness.decoder_security.accepted", + "blossom.verify_publication_readiness.decoder_security.rejected" + ] + }, + "release_change_id": "blossom-raster-decoder-security", + "result_vector": { + "canonical": { + "path": "contracts/conformance/vectors/blossom/raster_decoder_security.v1.json", + "byte_length": 17754, + "sha256": "b50fae1c8abd18e9915072df07461ea4cd02a13d9579d51fab8b3666cbd50ec9", + "hash_algorithm": "sha256_bytes_v1" + }, + "mirror": { + "path": "crates/blossom/tests/fixtures/raster_decoder_security.v1.json", + "byte_length": 17754, + "sha256": "b50fae1c8abd18e9915072df07461ea4cd02a13d9579d51fab8b3666cbd50ec9", + "hash_algorithm": "sha256_bytes_v1" + }, + "executor": { + "path": "crates/blossom/tests/decoder_security.rs", + "byte_length": 19240, + "sha256": "fe940ca7d26280d7bcc1f1c4dd0b1b36d8ab60dcdbfe54413ceed837bbbe4b71", + "hash_algorithm": "sha256_bytes_v1" + }, + "executor_tests": [ + "decoder_regression_corpus_executes_every_case", + "decoder_differential_matches_independent_backend", + "maximum_resource_probe" + ], + "case_ids": [ + "jpeg_baseline_gray_8bit", + "jpeg_baseline_rgb_8bit", + "jpeg_baseline_cmyk_8bit", + "jpeg_extended_sequential_sof1", + "png_gray_8bit", + "png_rgb_8bit", + "png_indexed_8bit", + "png_gray_alpha_8bit", + "png_rgba_8bit", + "png_adam7_rgba_8bit", + "webp_lossless_8bit", + "webp_lossy_8bit", + "webp_lossless_alpha_8bit", + "webp_lossy_alpha_8bit", + "jpeg_progressive_forbidden", + "jpeg_twelve_bit_forbidden", + "jpeg_entropy_truncated", + "jpeg_huffman_overfull", + "jpeg_wrong_restart_marker", + "png_sixteen_bit_forbidden", + "png_animation_forbidden", + "png_corrupt_crc", + "png_corrupt_deflate", + "png_dimension_over_limit", + "png_pixel_limit", + "png_truncated", + "webp_animation_forbidden", + "webp_duplicate_primary", + "webp_truncated", + "jpeg_truncated" + ] + } +} diff --git a/crates/blossom/contracts/raster_decoder_security_v1.manifest.schema.json b/crates/blossom/contracts/raster_decoder_security_v1.manifest.schema.json @@ -0,0 +1,763 @@ +{ + "$defs": { + "corpus_seed": { + "additionalProperties": false, + "properties": { + "expected_accepted": { + "type": "boolean" + }, + "expected_error": { + "enum": [ + "invalid_publication_raster", + "publication_jpeg_process_forbidden", + "publication_raster_animation_forbidden", + "publication_raster_decode_failed", + "publication_raster_dimensions_out_of_range", + "publication_raster_pixel_limit_exceeded", + "publication_raster_process_forbidden" + ], + "type": "string" + }, + "file": { + "$ref": "#/$defs/file" + }, + "format": { + "enum": [ + "jpeg", + "png", + "webp" + ] + }, + "provenance_mutation": { + "enum": [ + "jpeg_sof1", + "jpeg_entropy_truncated", + "jpeg_huffman_overfull", + "jpeg_precision_12", + "jpeg_progressive", + "jpeg_wrong_restart", + "none", + "png_16_bit", + "png_animation", + "png_corrupt_crc", + "png_corrupt_deflate", + "png_dimension_over", + "png_pixel_over", + "truncate_half", + "webp_animation", + "webp_duplicate_primary" + ], + "type": "string" + }, + "target": { + "enum": [ + "publication_jpeg", + "publication_png", + "publication_webp" + ], + "type": "string" + }, + "vector_id": { + "enum": [ + "jpeg_baseline_gray_8bit", + "jpeg_baseline_rgb_8bit", + "jpeg_baseline_cmyk_8bit", + "jpeg_extended_sequential_sof1", + "png_gray_8bit", + "png_rgb_8bit", + "png_indexed_8bit", + "png_gray_alpha_8bit", + "png_rgba_8bit", + "png_adam7_rgba_8bit", + "webp_lossless_8bit", + "webp_lossy_8bit", + "webp_lossless_alpha_8bit", + "webp_lossy_alpha_8bit", + "jpeg_progressive_forbidden", + "jpeg_twelve_bit_forbidden", + "jpeg_entropy_truncated", + "jpeg_huffman_overfull", + "jpeg_wrong_restart_marker", + "png_sixteen_bit_forbidden", + "png_animation_forbidden", + "png_corrupt_crc", + "png_corrupt_deflate", + "png_dimension_over_limit", + "png_pixel_limit", + "png_truncated", + "webp_animation_forbidden", + "webp_duplicate_primary", + "webp_truncated", + "jpeg_truncated" + ], + "type": "string" + } + }, + "required": [ + "vector_id", + "target", + "format", + "provenance_mutation", + "expected_accepted", + "file" + ], + "type": "object" + }, + "decoder_profile": { + "additionalProperties": false, + "properties": { + "accepted_formats": { + "const": [ + "image/jpeg", + "image/png", + "image/webp" + ] + }, + "accepted_jpeg_processes": { + "const": [ + "sof0_baseline_8bit", + "sof1_extended_sequential_8bit" + ] + }, + "accepted_png_color_types": { + "const": [ + 0, + 2, + 3, + 4, + 6 + ] + }, + "normative_error_codes": { + "items": { + "enum": [ + "publication_raster_empty", + "publication_raster_byte_limit_exceeded", + "unsupported_publication_raster_media_type", + "invalid_publication_raster", + "publication_jpeg_process_forbidden", + "publication_raster_animation_forbidden", + "publication_raster_container_dimension_mismatch", + "publication_raster_decode_allocation_failed", + "publication_raster_decoded_byte_limit_exceeded", + "publication_raster_decode_failed", + "publication_raster_dimensions_out_of_range", + "publication_raster_pixel_limit_exceeded", + "publication_raster_process_forbidden" + ], + "type": "string" + }, + "maxItems": 13, + "minItems": 13, + "type": "array", + "uniqueItems": true + }, + "observed_rejection_error_codes": { + "items": { + "enum": [ + "invalid_publication_raster", + "publication_jpeg_process_forbidden", + "publication_raster_animation_forbidden", + "publication_raster_decode_failed", + "publication_raster_dimensions_out_of_range", + "publication_raster_pixel_limit_exceeded", + "publication_raster_process_forbidden" + ], + "type": "string" + }, + "maxItems": 7, + "minItems": 7, + "type": "array", + "uniqueItems": true + }, + "required_accepted_mutations": { + "items": { + "enum": [ + "jpeg_sof1", + "none" + ], + "type": "string" + }, + "maxItems": 2, + "minItems": 2, + "type": "array", + "uniqueItems": true + }, + "required_rejected_mutations": { + "items": { + "enum": [ + "jpeg_entropy_truncated", + "jpeg_huffman_overfull", + "jpeg_precision_12", + "jpeg_progressive", + "jpeg_wrong_restart", + "png_16_bit", + "png_animation", + "png_corrupt_crc", + "png_corrupt_deflate", + "png_dimension_over", + "png_pixel_over", + "truncate_half", + "webp_animation", + "webp_duplicate_primary" + ], + "type": "string" + }, + "maxItems": 14, + "minItems": 14, + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "accepted_formats", + "accepted_jpeg_processes", + "accepted_png_color_types", + "normative_error_codes", + "observed_rejection_error_codes", + "required_accepted_mutations", + "required_rejected_mutations" + ], + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "byte_length": { + "minimum": 1, + "type": "integer" + }, + "hash_algorithm": { + "const": "sha256_bytes_v1" + }, + "path": { + "minLength": 1, + "type": "string" + }, + "sha256": { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + } + }, + "required": [ + "path", + "byte_length", + "sha256", + "hash_algorithm" + ], + "type": "object" + }, + "fuzz_campaign": { + "additionalProperties": false, + "properties": { + "common_harness": { + "$ref": "#/$defs/file" + }, + "corpus_seeds": { + "items": { + "$ref": "#/$defs/corpus_seed" + }, + "maxItems": 30, + "minItems": 30, + "type": "array" + }, + "engine": { + "const": "libfuzzer" + }, + "engine_rss_limit_mb": { + "const": 2048 + }, + "extended_campaign_document": { + "$ref": "#/$defs/file" + }, + "project_lockfile": { + "$ref": "#/$defs/file" + }, + "project_manifest": { + "$ref": "#/$defs/file" + }, + "sanitizer": { + "const": "address" + }, + "smoke_max_input_bytes": { + "const": 65536 + }, + "smoke_runs": { + "const": 256 + }, + "smoke_seed": { + "const": 424242 + }, + "smoke_timeout_seconds": { + "const": 5 + }, + "targets": { + "items": { + "$ref": "#/$defs/fuzz_target" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "toolchain": { + "$ref": "#/$defs/file" + }, + "toolchain_channel": { + "const": "nightly-2026-07-15" + }, + "toolchain_components": { + "const": [ + "rust-src" + ] + }, + "toolchain_profile": { + "const": "minimal" + } + }, + "required": [ + "project_manifest", + "project_lockfile", + "toolchain", + "toolchain_channel", + "toolchain_components", + "toolchain_profile", + "engine", + "sanitizer", + "common_harness", + "targets", + "corpus_seeds", + "smoke_runs", + "smoke_seed", + "smoke_max_input_bytes", + "smoke_timeout_seconds", + "engine_rss_limit_mb", + "extended_campaign_document" + ], + "type": "object" + }, + "fuzz_target": { + "additionalProperties": false, + "properties": { + "binary": { + "$ref": "#/$defs/file" + }, + "extension": { + "enum": [ + "jpg", + "png", + "webp" + ] + }, + "format": { + "enum": [ + "jpeg", + "png", + "webp" + ] + }, + "media_type": { + "enum": [ + "image/jpeg", + "image/png", + "image/webp" + ] + }, + "name": { + "enum": [ + "publication_jpeg", + "publication_png", + "publication_webp" + ] + } + }, + "required": [ + "name", + "format", + "media_type", + "extension", + "binary" + ], + "type": "object" + }, + "nix_lanes": { + "additionalProperties": false, + "properties": { + "app": { + "const": "decoder-security" + }, + "devshell": { + "const": "decoder-security" + }, + "fuzz_check": { + "const": "blossom-decoder-fuzz-smoke" + }, + "stable_check": { + "const": "blossom-raster-decode-test" + } + }, + "required": [ + "app", + "devshell", + "stable_check", + "fuzz_check" + ], + "type": "object" + }, + "operation": { + "additionalProperties": false, + "properties": { + "id": { + "const": "blossom.verify_publication_readiness" + }, + "key": { + "const": "blossom_verify_publication_readiness" + }, + "primary_case_kinds": { + "const": [ + "blossom.verify_publication_readiness.valid", + "blossom.verify_publication_readiness.invalid" + ] + }, + "primary_vector": { + "const": "contracts/conformance/vectors/blossom/publication_readiness.v1.json" + }, + "security_case_kinds": { + "const": [ + "blossom.verify_publication_readiness.decoder_security.accepted", + "blossom.verify_publication_readiness.decoder_security.rejected" + ] + } + }, + "required": [ + "key", + "id", + "primary_vector", + "primary_case_kinds", + "security_case_kinds" + ], + "type": "object" + }, + "predecessor": { + "additionalProperties": false, + "properties": { + "contract_id": { + "const": "radroots_blossom.publication_readiness_v1" + }, + "immutable_artifacts": { + "items": { + "$ref": "#/$defs/file" + }, + "minItems": 1, + "type": "array" + }, + "source_supersessions": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "contract_id", + "immutable_artifacts", + "source_supersessions" + ], + "type": "object" + }, + "protocol_source": { + "additionalProperties": false, + "properties": { + "id": { + "enum": [ + "nostr_nips", + "blossom" + ] + }, + "repository": { + "enum": [ + "https://github.com/nostr-protocol/nips", + "https://github.com/hzrd149/blossom" + ] + }, + "revision": { + "pattern": "^[0-9a-f]{40}$", + "type": "string" + } + }, + "required": [ + "id", + "repository", + "revision" + ], + "type": "object" + }, + "resource_limits": { + "additionalProperties": false, + "properties": { + "jpeg_entropy_bit_reads_per_byte": { + "const": 8 + }, + "jpeg_max_blocks": { + "const": 3200000 + }, + "jpeg_max_coefficient_steps": { + "const": 204800000 + }, + "jpeg_max_scans": { + "const": 4 + }, + "max_container_records": { + "const": 65536 + }, + "max_decoded_bytes": { + "const": 80000000 + }, + "max_dimension": { + "const": 16384 + }, + "max_pixels": { + "const": 20000000 + }, + "max_raster_bytes": { + "const": 10485760 + }, + "peak_rss_kib_limit": { + "const": 131072 + } + }, + "required": [ + "max_raster_bytes", + "max_decoded_bytes", + "max_dimension", + "max_pixels", + "max_container_records", + "jpeg_max_scans", + "jpeg_max_blocks", + "jpeg_max_coefficient_steps", + "jpeg_entropy_bit_reads_per_byte", + "peak_rss_kib_limit" + ], + "type": "object" + }, + "resource_probe": { + "additionalProperties": false, + "properties": { + "executor_test": { + "const": "maximum_resource_probe" + }, + "fixture_authority": { + "minLength": 1, + "type": "string" + }, + "format": { + "enum": [ + "jpeg", + "png", + "webp" + ] + }, + "height": { + "const": 4000 + }, + "id": { + "enum": [ + "jpeg_grayscale", + "jpeg_rgb", + "jpeg_cmyk", + "jpeg_sof1", + "png_rgb", + "png_palette", + "png_rgba", + "png_adam7", + "webp_vp8_rgb", + "webp_vp8_alpha", + "webp_vp8l_rgb", + "webp_vp8l_alpha" + ] + }, + "max_decoded_bytes": { + "enum": [ + 60000000, + 80000000 + ] + }, + "max_encoded_bytes": { + "const": 10485760 + }, + "peak_rss_kib_limit": { + "const": 131072 + }, + "process": { + "minLength": 1, + "type": "string" + }, + "repetitions": { + "const": 3 + }, + "width": { + "const": 5000 + } + }, + "required": [ + "id", + "format", + "process", + "width", + "height", + "max_encoded_bytes", + "max_decoded_bytes", + "executor_test", + "fixture_authority", + "repetitions", + "peak_rss_kib_limit" + ], + "type": "object" + }, + "result_vector": { + "additionalProperties": false, + "properties": { + "canonical": { + "$ref": "#/$defs/file" + }, + "case_ids": { + "const": [ + "jpeg_baseline_gray_8bit", + "jpeg_baseline_rgb_8bit", + "jpeg_baseline_cmyk_8bit", + "jpeg_extended_sequential_sof1", + "png_gray_8bit", + "png_rgb_8bit", + "png_indexed_8bit", + "png_gray_alpha_8bit", + "png_rgba_8bit", + "png_adam7_rgba_8bit", + "webp_lossless_8bit", + "webp_lossy_8bit", + "webp_lossless_alpha_8bit", + "webp_lossy_alpha_8bit", + "jpeg_progressive_forbidden", + "jpeg_twelve_bit_forbidden", + "jpeg_entropy_truncated", + "jpeg_huffman_overfull", + "jpeg_wrong_restart_marker", + "png_sixteen_bit_forbidden", + "png_animation_forbidden", + "png_corrupt_crc", + "png_corrupt_deflate", + "png_dimension_over_limit", + "png_pixel_limit", + "png_truncated", + "webp_animation_forbidden", + "webp_duplicate_primary", + "webp_truncated", + "jpeg_truncated" + ] + }, + "executor": { + "$ref": "#/$defs/file" + }, + "executor_tests": { + "const": [ + "decoder_regression_corpus_executes_every_case", + "decoder_differential_matches_independent_backend", + "maximum_resource_probe" + ] + }, + "mirror": { + "$ref": "#/$defs/file" + } + }, + "required": [ + "canonical", + "mirror", + "executor", + "executor_tests", + "case_ids" + ], + "type": "object" + } + }, + "$id": "https://radroots.org/schemas/blossom/raster-decoder-security-manifest-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "authority_id": { + "const": "blossom_raster_decoder_security_v1" + }, + "contract_id": { + "const": "radroots_blossom.raster_decoder_security_v1" + }, + "decoder_profile": { + "$ref": "#/$defs/decoder_profile" + }, + "fuzz_campaign": { + "$ref": "#/$defs/fuzz_campaign" + }, + "manifest_schema": { + "$ref": "#/$defs/file" + }, + "nix_lanes": { + "$ref": "#/$defs/nix_lanes" + }, + "operation": { + "$ref": "#/$defs/operation" + }, + "predecessors": { + "items": { + "$ref": "#/$defs/predecessor" + }, + "maxItems": 1, + "minItems": 1, + "type": "array" + }, + "protocol_sources": { + "items": { + "$ref": "#/$defs/protocol_source" + }, + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "release_change_id": { + "const": "blossom-raster-decoder-security" + }, + "resource_limits": { + "$ref": "#/$defs/resource_limits" + }, + "resource_probes": { + "items": { + "$ref": "#/$defs/resource_probe" + }, + "maxItems": 12, + "minItems": 12, + "type": "array" + }, + "result_vector": { + "$ref": "#/$defs/result_vector" + }, + "schema_version": { + "const": 1 + } + }, + "required": [ + "schema_version", + "contract_id", + "authority_id", + "manifest_schema", + "predecessors", + "protocol_sources", + "resource_limits", + "decoder_profile", + "resource_probes", + "fuzz_campaign", + "nix_lanes", + "operation", + "release_change_id", + "result_vector" + ], + "title": "Radroots Blossom raster decoder security semantic contract", + "type": "object" +} diff --git a/crates/blossom/contracts/raster_decoder_security_v1.manifest.sha256 b/crates/blossom/contracts/raster_decoder_security_v1.manifest.sha256 @@ -0,0 +1 @@ +4a3c4acd9f2e3b0447465e5a1f744186d4823f0c6beb0665cb951b8dd82139c2 diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -4,7 +4,6 @@ mod admission_authority; mod artifact_bundle; #[allow(dead_code)] mod blossom_publication_readiness; -#[allow(dead_code)] mod blossom_raster_decoder_security; mod comment_authority; mod deletion_authority; @@ -25,6 +24,10 @@ mod registry_v7; mod release_provenance; mod source_maintenance; +pub(crate) use blossom_raster_decoder_security::{ + validate_blossom_raster_decoder_security_manifest, + write_blossom_raster_decoder_security_manifest, +}; pub(crate) use food_availability_projection::{ validate_food_availability_projection_manifest, write_food_availability_projection_manifest, }; @@ -78,6 +81,7 @@ pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), S validate_immutable_phase1_publication_artifact_predecessor(workspace_root)?; validate_release_provenance_schema(workspace_root)?; validate_phase1_publication_media_readiness_manifest(workspace_root)?; + validate_blossom_raster_decoder_security_manifest(workspace_root)?; validate_knowledge_contract_manifest(workspace_root) } @@ -165,7 +169,7 @@ const REPLICA_CONTRACT_RELATIVE: &str = "contracts/replica.toml"; const REPLICA_CONTRACT_NAME: &str = "radroots_replica_contract"; const REPLICA_TRANSFER_CONSTANT: &str = "RADROOTS_REPLICA_TRANSFER_VERSION"; const REPLICA_TRANSFER_VERSION: u32 = 2; -const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 28] = [ +const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 29] = [ ( "contracts/conformance/vectors/blossom/bud11_claims.v1.json", "crates/blossom/tests/fixtures/bud11_claims.v1.json", @@ -183,6 +187,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 28] = [ "crates/blossom/tests/fixtures/publication_readiness_persistence.v1.json", ), ( + "contracts/conformance/vectors/blossom/raster_decoder_security.v1.json", + "crates/blossom/tests/fixtures/raster_decoder_security.v1.json", + ), + ( "contracts/conformance/vectors/blossom/bud11_nostr_adapter.v1.json", "crates/nostr/tests/fixtures/bud11_nostr_adapter.v1.json", ), diff --git a/tools/xtask/src/contract/blossom_publication_readiness.rs b/tools/xtask/src/contract/blossom_publication_readiness.rs @@ -436,15 +436,23 @@ struct ValidatedVector { pub(crate) fn write_blossom_publication_readiness_manifest( workspace_root: &Path, ) -> Result<(), String> { - with_artifact_bundle_transaction(workspace_root, |transaction| { - transaction.write(expected_artifacts(workspace_root)?)?; - validate_manifest_under_lock(workspace_root) - }) + validate_blossom_publication_readiness_manifest(workspace_root) } pub(crate) fn validate_blossom_publication_readiness_manifest( workspace_root: &Path, ) -> Result<(), String> { + // Keep the frozen predecessor validator compiled for its governed mutation suite. + let _immutable_predecessor_validator: fn(&Path) -> Result<(), String> = + validate_blossom_publication_readiness_manifest_under_lock; + super::blossom_raster_decoder_security::validate_blossom_raster_decoder_security_manifest( + workspace_root, + ) +} + +fn validate_blossom_publication_readiness_manifest_under_lock( + workspace_root: &Path, +) -> Result<(), String> { with_artifact_bundle_transaction(workspace_root, |_| { validate_manifest_under_lock(workspace_root) }) diff --git a/tools/xtask/src/contract/blossom_raster_decoder_security.rs b/tools/xtask/src/contract/blossom_raster_decoder_security.rs @@ -59,8 +59,16 @@ const CHANGELOG_RELATIVE: &str = "CHANGELOG.md"; const RELEASE_CHANGE_ID: &str = "blossom-raster-decoder-security"; const CHANGELOG_MARKER: &str = "<!-- release-change: blossom-raster-decoder-security -->"; -const OPERATION_KEY: &str = "blossom_verify_raster_decoder_security"; -const OPERATION_ID: &str = "blossom.verify_raster_decoder_security"; +const OPERATION_KEY: &str = "blossom_verify_publication_readiness"; +const OPERATION_ID: &str = "blossom.verify_publication_readiness"; +const PHANTOM_OPERATION_KEY: &str = "blossom_verify_raster_decoder_security"; +const PHANTOM_OPERATION_ID: &str = "blossom.verify_raster_decoder_security"; +const PRIMARY_OPERATION_VECTOR: &str = + "contracts/conformance/vectors/blossom/publication_readiness.v1.json"; +const PRIMARY_OPERATION_CASE_KINDS: &[&str] = &[ + "blossom.verify_publication_readiness.valid", + "blossom.verify_publication_readiness.invalid", +]; const ACCEPTED_KIND: &str = "blossom.verify_publication_readiness.decoder_security.accepted"; const REJECTED_KIND: &str = "blossom.verify_publication_readiness.decoder_security.rejected"; @@ -117,35 +125,77 @@ const JPEG_LIMIT_CONSTANTS: &[(&str, u64)] = &[ ), ]; -const REJECTED_ERROR_CODES: &[&str] = &[ +const NORMATIVE_ERROR_CODES: &[&str] = &[ + "publication_raster_empty", + "publication_raster_byte_limit_exceeded", + "unsupported_publication_raster_media_type", "invalid_publication_raster", "publication_jpeg_process_forbidden", "publication_raster_animation_forbidden", + "publication_raster_container_dimension_mismatch", + "publication_raster_decode_allocation_failed", + "publication_raster_decoded_byte_limit_exceeded", "publication_raster_decode_failed", "publication_raster_dimensions_out_of_range", "publication_raster_pixel_limit_exceeded", "publication_raster_process_forbidden", ]; -const REQUIRED_DEDICATED_ERROR_CODES: &[&str] = &[ +const NORMATIVE_ERROR_VARIANTS: &[&str] = &[ + "PublicationRasterEmpty", + "PublicationRasterByteLimitExceeded", + "UnsupportedPublicationRasterMediaType", + "InvalidPublicationRaster", + "PublicationJpegProcessForbidden", + "PublicationRasterAnimationForbidden", + "PublicationRasterContainerDimensionMismatch", + "PublicationRasterDecodeAllocationFailed", + "PublicationRasterDecodedByteLimitExceeded", + "PublicationRasterDecodeFailed", + "PublicationRasterDimensionsOutOfRange", + "PublicationRasterPixelLimitExceeded", + "PublicationRasterProcessForbidden", +]; +const OBSERVED_REJECTION_ERROR_CODES: &[&str] = &[ + "invalid_publication_raster", "publication_jpeg_process_forbidden", "publication_raster_animation_forbidden", + "publication_raster_decode_failed", + "publication_raster_dimensions_out_of_range", + "publication_raster_pixel_limit_exceeded", "publication_raster_process_forbidden", ]; -const REQUIRED_MUTATIONS: &[&str] = &[ - "none", +const REQUIRED_ACCEPTED_MUTATIONS: &[&str] = &["jpeg_sof1", "none"]; +const REQUIRED_REJECTED_MUTATIONS: &[&str] = &[ + "jpeg_entropy_truncated", + "jpeg_huffman_overfull", + "jpeg_precision_12", + "jpeg_progressive", + "jpeg_wrong_restart", + "png_16_bit", + "png_animation", + "png_corrupt_crc", + "png_corrupt_deflate", + "png_dimension_over", + "png_pixel_over", "truncate_half", + "webp_animation", + "webp_duplicate_primary", +]; +const ALL_PROVENANCE_MUTATIONS: &[&str] = &[ "jpeg_sof1", - "jpeg_progressive", - "jpeg_precision_12", "jpeg_entropy_truncated", "jpeg_huffman_overfull", + "jpeg_precision_12", + "jpeg_progressive", "jpeg_wrong_restart", + "none", "png_16_bit", "png_animation", "png_corrupt_crc", "png_corrupt_deflate", "png_dimension_over", "png_pixel_over", + "truncate_half", "webp_animation", "webp_duplicate_primary", ]; @@ -181,6 +231,244 @@ const VECTOR_CASE_IDS: &[&str] = &[ "webp_truncated", "jpeg_truncated", ]; +const CORPUS_SEED_SPECS: &[(&str, &str, &str)] = &[ + ( + "jpeg_baseline_gray_8bit", + "publication_jpeg", + "fuzz/corpus/publication_jpeg/jpeg_baseline_gray_8bit.bin", + ), + ( + "jpeg_baseline_rgb_8bit", + "publication_jpeg", + "fuzz/corpus/publication_jpeg/jpeg_baseline_rgb_8bit.bin", + ), + ( + "jpeg_baseline_cmyk_8bit", + "publication_jpeg", + "fuzz/corpus/publication_jpeg/jpeg_baseline_cmyk_8bit.bin", + ), + ( + "jpeg_extended_sequential_sof1", + "publication_jpeg", + "fuzz/corpus/publication_jpeg/jpeg_extended_sequential_sof1.bin", + ), + ( + "png_gray_8bit", + "publication_png", + "fuzz/corpus/publication_png/png_gray_8bit.bin", + ), + ( + "png_rgb_8bit", + "publication_png", + "fuzz/corpus/publication_png/png_rgb_8bit.bin", + ), + ( + "png_indexed_8bit", + "publication_png", + "fuzz/corpus/publication_png/png_indexed_8bit.bin", + ), + ( + "png_gray_alpha_8bit", + "publication_png", + "fuzz/corpus/publication_png/png_gray_alpha_8bit.bin", + ), + ( + "png_rgba_8bit", + "publication_png", + "fuzz/corpus/publication_png/png_rgba_8bit.bin", + ), + ( + "png_adam7_rgba_8bit", + "publication_png", + "fuzz/corpus/publication_png/png_adam7_rgba_8bit.bin", + ), + ( + "webp_lossless_8bit", + "publication_webp", + "fuzz/corpus/publication_webp/webp_lossless_8bit.bin", + ), + ( + "webp_lossy_8bit", + "publication_webp", + "fuzz/corpus/publication_webp/webp_lossy_8bit.bin", + ), + ( + "webp_lossless_alpha_8bit", + "publication_webp", + "fuzz/corpus/publication_webp/webp_lossless_alpha_8bit.bin", + ), + ( + "webp_lossy_alpha_8bit", + "publication_webp", + "fuzz/corpus/publication_webp/webp_lossy_alpha_8bit.bin", + ), + ( + "jpeg_progressive_forbidden", + "publication_jpeg", + "fuzz/corpus/publication_jpeg/jpeg_progressive_forbidden.bin", + ), + ( + "jpeg_twelve_bit_forbidden", + "publication_jpeg", + "fuzz/corpus/publication_jpeg/jpeg_twelve_bit_forbidden.bin", + ), + ( + "jpeg_entropy_truncated", + "publication_jpeg", + "fuzz/corpus/publication_jpeg/jpeg_entropy_truncated.bin", + ), + ( + "jpeg_huffman_overfull", + "publication_jpeg", + "fuzz/corpus/publication_jpeg/jpeg_huffman_overfull.bin", + ), + ( + "jpeg_wrong_restart_marker", + "publication_jpeg", + "fuzz/corpus/publication_jpeg/jpeg_wrong_restart_marker.bin", + ), + ( + "png_sixteen_bit_forbidden", + "publication_png", + "fuzz/corpus/publication_png/png_sixteen_bit_forbidden.bin", + ), + ( + "png_animation_forbidden", + "publication_png", + "fuzz/corpus/publication_png/png_animation_forbidden.bin", + ), + ( + "png_corrupt_crc", + "publication_png", + "fuzz/corpus/publication_png/png_corrupt_crc.bin", + ), + ( + "png_corrupt_deflate", + "publication_png", + "fuzz/corpus/publication_png/png_corrupt_deflate.bin", + ), + ( + "png_dimension_over_limit", + "publication_png", + "fuzz/corpus/publication_png/png_dimension_over_limit.bin", + ), + ( + "png_pixel_limit", + "publication_png", + "fuzz/corpus/publication_png/png_pixel_limit.bin", + ), + ( + "png_truncated", + "publication_png", + "fuzz/corpus/publication_png/png_truncated.bin", + ), + ( + "webp_animation_forbidden", + "publication_webp", + "fuzz/corpus/publication_webp/webp_animation_forbidden.bin", + ), + ( + "webp_duplicate_primary", + "publication_webp", + "fuzz/corpus/publication_webp/webp_duplicate_primary.bin", + ), + ( + "webp_truncated", + "publication_webp", + "fuzz/corpus/publication_webp/webp_truncated.bin", + ), + ( + "jpeg_truncated", + "publication_jpeg", + "fuzz/corpus/publication_jpeg/jpeg_truncated.bin", + ), +]; +const RESOURCE_PROBE_SPECS: &[(&str, &str, &str, u64, &str)] = &[ + ( + "jpeg_grayscale", + "jpeg", + "sof0_grayscale_8bit", + 60_000_000, + "imagemagick_grayscale", + ), + ( + "jpeg_rgb", + "jpeg", + "sof0_rgb_8bit", + 60_000_000, + "imagemagick_rgb", + ), + ( + "jpeg_cmyk", + "jpeg", + "sof0_cmyk_8bit", + 60_000_000, + "imagemagick_cmyk", + ), + ( + "jpeg_sof1", + "jpeg", + "sof1_extended_sequential_8bit", + 60_000_000, + "imagemagick_rgb_sof1_marker", + ), + ( + "png_rgb", + "png", + "rgb_8bit", + 60_000_000, + "imagemagick_png24", + ), + ( + "png_palette", + "png", + "palette_8bit", + 60_000_000, + "imagemagick_palette", + ), + ( + "png_rgba", + "png", + "rgba_8bit", + 80_000_000, + "imagemagick_png32", + ), + ( + "png_adam7", + "png", + "adam7_rgba_8bit", + 80_000_000, + "imagemagick_adam7", + ), + ( + "webp_vp8_rgb", + "webp", + "vp8_rgb", + 80_000_000, + "imagemagick_webp_lossy_rgb", + ), + ( + "webp_vp8_alpha", + "webp", + "vp8_alpha", + 80_000_000, + "imagemagick_webp_lossy_alpha", + ), + ( + "webp_vp8l_rgb", + "webp", + "vp8l_rgb", + 80_000_000, + "imagemagick_webp_lossless_rgb", + ), + ( + "webp_vp8l_alpha", + "webp", + "vp8l_alpha", + 80_000_000, + "imagemagick_webp_lossless_alpha", + ), +]; const RELEASE_SEMVER_IMPACTS: &[&str] = &[ "add_conformance_vector", "add_enum_variant", @@ -196,19 +484,17 @@ const OPERATION_INPUTS: &[&str] = &[ "RadrootsBlossomBud01GetObservation", ]; const OPERATION_OUTPUTS: &[&str] = &["RadrootsBlossomPublicationReadinessEvidence"]; -const OPERATION_MODULES: &[&str] = &[READINESS_SOURCE_RELATIVE, JPEG_SOURCE_RELATIVE]; +const OPERATION_MODULES: &[&str] = &[READINESS_SOURCE_RELATIVE]; const OPERATION_RUST_TYPES: &[&str] = &[ "radroots_blossom::RadrootsBlossomAuthoredRasterDimensions", + "radroots_blossom::RadrootsBlossomBud01GetCollector", "radroots_blossom::RadrootsBlossomBud01GetObservation", "radroots_blossom::RadrootsBlossomBud01HeadObservation", "radroots_blossom::RadrootsBlossomBud02UploadObservation", - "radroots_blossom::RadrootsBlossomByteVerifiedDescriptor", - "radroots_blossom::RadrootsBlossomError", "radroots_blossom::RadrootsBlossomPublicationReadinessEvidence", ]; const SOURCE_SUPERSESSIONS: &[&str] = &[ "CHANGELOG.md", - "contracts/operations.toml", "contracts/releases/1.0.0-alpha.1.toml", "crates/blossom/Cargo.toml", "crates/blossom/src/error.rs", @@ -239,26 +525,71 @@ struct DecoderProfileDescriptor { accepted_formats: Vec<String>, accepted_jpeg_processes: Vec<String>, accepted_png_color_types: Vec<u8>, - stable_error_codes: Vec<String>, - required_rejection_mutations: Vec<String>, + normative_error_codes: Vec<String>, + observed_rejection_error_codes: Vec<String>, + required_accepted_mutations: Vec<String>, + required_rejected_mutations: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct ResourceProbeDescriptor { + id: String, + format: String, + process: String, + width: u32, + height: u32, + max_encoded_bytes: u64, + max_decoded_bytes: u64, + executor_test: String, + fixture_authority: String, + repetitions: u64, + peak_rss_kib_limit: u64, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct FuzzTargetDescriptor { + name: String, + format: String, + media_type: String, + extension: String, + binary: FileDescriptor, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct CorpusSeedDescriptor { + vector_id: String, + target: String, + format: String, + provenance_mutation: String, + expected_accepted: bool, + #[serde(skip_serializing_if = "Option::is_none")] + expected_error: Option<String>, + file: FileDescriptor, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] struct FuzzCampaignDescriptor { + project_manifest: FileDescriptor, + project_lockfile: FileDescriptor, + toolchain: FileDescriptor, toolchain_channel: String, toolchain_components: Vec<String>, toolchain_profile: String, engine: String, sanitizer: String, - targets: Vec<String>, - corpus_seeds: Vec<String>, + common_harness: FileDescriptor, + targets: Vec<FuzzTargetDescriptor>, + corpus_seeds: Vec<CorpusSeedDescriptor>, smoke_runs: u64, smoke_seed: u64, smoke_max_input_bytes: u64, smoke_timeout_seconds: u64, engine_rss_limit_mb: u64, - extended_campaign_document: String, + extended_campaign_document: FileDescriptor, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -275,17 +606,16 @@ struct NixLanesDescriptor { struct OperationDescriptor { key: String, id: String, - case_kinds: Vec<String>, + primary_vector: String, + primary_case_kinds: Vec<String>, + security_case_kinds: Vec<String>, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] struct ResultVectorDescriptor { - canonical_path: String, - mirror_path: String, - byte_length: u64, - sha256: String, - hash_algorithm: String, + canonical: FileDescriptor, + mirror: FileDescriptor, executor: FileDescriptor, executor_tests: Vec<String>, case_ids: Vec<String>, @@ -302,6 +632,7 @@ struct RasterDecoderSecurityManifest { protocol_sources: Vec<ProtocolSourcePin>, resource_limits: ResourceLimitsDescriptor, decoder_profile: DecoderProfileDescriptor, + resource_probes: Vec<ResourceProbeDescriptor>, fuzz_campaign: FuzzCampaignDescriptor, nix_lanes: NixLanesDescriptor, operation: OperationDescriptor, @@ -309,7 +640,7 @@ struct RasterDecoderSecurityManifest { result_vector: ResultVectorDescriptor, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct VectorSuite { suite: String, @@ -317,7 +648,7 @@ struct VectorSuite { vectors: Vec<VectorCase>, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct VectorCase { id: String, @@ -326,7 +657,7 @@ struct VectorCase { expected: VectorExpected, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct VectorInput { format: String, @@ -334,7 +665,7 @@ struct VectorInput { mutation: String, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct VectorExpected { accepted: bool, @@ -349,6 +680,7 @@ struct VectorExpected { struct ValidatedVector { bytes: Vec<u8>, case_ids: Vec<String>, + cases: Vec<VectorCase>, } pub(crate) fn write_blossom_raster_decoder_security_manifest( @@ -413,6 +745,7 @@ fn validate_manifest_under_lock(workspace_root: &Path) -> Result<(), String> { fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> { validate_source_contract(workspace_root)?; + let vector = validate_vector(workspace_root)?; let manifest_schema_bytes = canonical_json_bytes(&manifest_schema())?; let manifest = describe_manifest(workspace_root, &manifest_schema_bytes)?; let manifest_bytes = canonical_json_bytes(&manifest)?; @@ -425,8 +758,7 @@ fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, S "manifest_sidecar": descriptor_for_bytes(MANIFEST_SHA256_RELATIVE, &sidecar_bytes), "predecessor_contract_ids": ["radroots_blossom.publication_readiness_v1"] }))?; - let vector_bytes = read_regular_file(workspace_root, VECTOR_RELATIVE)?; - Ok(vec![ + let mut artifacts = vec![ GeneratedArtifact { relative: MANIFEST_RELATIVE, contents: manifest_bytes, @@ -445,9 +777,18 @@ fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, S }, GeneratedArtifact { relative: VECTOR_MIRROR_RELATIVE, - contents: vector_bytes, + contents: vector.bytes.clone(), }, - ]) + ]; + artifacts.extend( + expected_seeds(&vector)? + .into_iter() + .map(|seed| GeneratedArtifact { + relative: seed.relative, + contents: seed.bytes, + }), + ); + Ok(artifacts) } fn describe_manifest( @@ -496,23 +837,75 @@ fn describe_manifest( "sof1_extended_sequential_8bit", ]), accepted_png_color_types: vec![0, 2, 3, 4, 6], - stable_error_codes: owned(REJECTED_ERROR_CODES), - required_rejection_mutations: owned(REQUIRED_MUTATIONS), + normative_error_codes: owned(NORMATIVE_ERROR_CODES), + observed_rejection_error_codes: owned(OBSERVED_REJECTION_ERROR_CODES), + required_accepted_mutations: owned(REQUIRED_ACCEPTED_MUTATIONS), + required_rejected_mutations: owned(REQUIRED_REJECTED_MUTATIONS), }, + resource_probes: RESOURCE_PROBE_SPECS + .iter() + .map(|(id, format, process, decoded_bytes, fixture_authority)| { + ResourceProbeDescriptor { + id: (*id).to_owned(), + format: (*format).to_owned(), + process: (*process).to_owned(), + width: 5_000, + height: 4_000, + max_encoded_bytes: MAX_RASTER_BYTES, + max_decoded_bytes: *decoded_bytes, + executor_test: RESOURCE_TEST.to_owned(), + fixture_authority: (*fixture_authority).to_owned(), + repetitions: 3, + peak_rss_kib_limit: PEAK_RSS_KIB_LIMIT, + } + }) + .collect(), fuzz_campaign: FuzzCampaignDescriptor { + project_manifest: descriptor_for_file(workspace_root, FUZZ_MANIFEST_RELATIVE)?, + project_lockfile: descriptor_for_file(workspace_root, FUZZ_LOCKFILE_RELATIVE)?, + toolchain: descriptor_for_file(workspace_root, FUZZ_TOOLCHAIN_RELATIVE)?, toolchain_channel: FUZZ_TOOLCHAIN_CHANNEL.to_owned(), toolchain_components: owned(&["rust-src"]), toolchain_profile: "minimal".to_owned(), engine: "libfuzzer".to_owned(), sanitizer: "address".to_owned(), - targets: owned(FUZZ_TARGETS), - corpus_seeds: corpus_seed_paths(workspace_root)?, + common_harness: descriptor_for_file(workspace_root, FUZZ_COMMON_RELATIVE)?, + targets: FUZZ_TARGET_SPECS + .iter() + .map(|(name, format, media_type, extension)| { + Ok(FuzzTargetDescriptor { + name: (*name).to_owned(), + format: (*format).to_owned(), + media_type: (*media_type).to_owned(), + extension: (*extension).to_owned(), + binary: descriptor_for_file( + workspace_root, + &format!("fuzz/fuzz_targets/{name}.rs"), + )?, + }) + }) + .collect::<Result<Vec<_>, String>>()?, + corpus_seeds: expected_seeds(&vector)? + .into_iter() + .map(|seed| CorpusSeedDescriptor { + vector_id: seed.case.id.clone(), + target: seed.target.to_owned(), + format: seed.case.input.format.clone(), + provenance_mutation: seed.case.input.mutation.clone(), + expected_accepted: seed.case.expected.accepted, + expected_error: seed.case.expected.error.clone(), + file: descriptor_for_bytes(seed.relative, &seed.bytes), + }) + .collect(), smoke_runs: FUZZ_SMOKE_RUNS, smoke_seed: FUZZ_SMOKE_SEED, smoke_max_input_bytes: FUZZ_SMOKE_MAX_INPUT_BYTES, smoke_timeout_seconds: FUZZ_SMOKE_TIMEOUT_SECONDS, engine_rss_limit_mb: FUZZ_ENGINE_RSS_LIMIT_MB, - extended_campaign_document: SECURITY_DOCUMENT_RELATIVE.to_owned(), + extended_campaign_document: descriptor_for_file( + workspace_root, + SECURITY_DOCUMENT_RELATIVE, + )?, }, nix_lanes: NixLanesDescriptor { app: "decoder-security".to_owned(), @@ -523,15 +916,14 @@ fn describe_manifest( operation: OperationDescriptor { key: OPERATION_KEY.to_owned(), id: OPERATION_ID.to_owned(), - case_kinds: owned(&[ACCEPTED_KIND, REJECTED_KIND]), + primary_vector: PRIMARY_OPERATION_VECTOR.to_owned(), + primary_case_kinds: owned(PRIMARY_OPERATION_CASE_KINDS), + security_case_kinds: owned(&[ACCEPTED_KIND, REJECTED_KIND]), }, release_change_id: RELEASE_CHANGE_ID.to_owned(), result_vector: ResultVectorDescriptor { - canonical_path: VECTOR_RELATIVE.to_owned(), - mirror_path: VECTOR_MIRROR_RELATIVE.to_owned(), - byte_length: vector.bytes.len() as u64, - sha256: sha256_hex(&vector.bytes), - hash_algorithm: HASH_ALGORITHM.to_owned(), + canonical: descriptor_for_bytes(VECTOR_RELATIVE, &vector.bytes), + mirror: descriptor_for_bytes(VECTOR_MIRROR_RELATIVE, &vector.bytes), executor: descriptor_for_file(workspace_root, VECTOR_EXECUTOR_RELATIVE)?, executor_tests: owned(&[REGRESSION_TEST, DIFFERENTIAL_TEST, RESOURCE_TEST]), case_ids: vector.case_ids, @@ -539,25 +931,25 @@ fn describe_manifest( }) } -fn corpus_seed_inventory(workspace_root: &Path) -> Result<Vec<(String, Vec<u8>)>, String> { - let vector_bytes = read_regular_file(workspace_root, VECTOR_RELATIVE)?; - let suite: VectorSuite = serde_json::from_slice(&vector_bytes) - .map_err(|error| format!("parse {VECTOR_RELATIVE}: {error}"))?; - if suite.vectors.len() != 30 { - return Err(format!( - "{VECTOR_RELATIVE} must contain exactly 30 cases, found {}", - suite.vectors.len() - )); - } +struct ExpectedSeed<'a> { + relative: &'static str, + target: &'static str, + case: &'a VectorCase, + bytes: Vec<u8>, +} - suite - .vectors - .into_iter() - .map(|case| { - let target = FUZZ_TARGET_SPECS +fn expected_seeds(vector: &ValidatedVector) -> Result<Vec<ExpectedSeed<'_>>, String> { + if CORPUS_SEED_SPECS.len() != vector.cases.len() { + return Err("fuzz seed specification count must match the vector count".to_owned()); + } + CORPUS_SEED_SPECS + .iter() + .zip(&vector.cases) + .map(|((case_id, target, relative), case)| { + let expected_target = FUZZ_TARGET_SPECS .iter() - .find_map(|(target, format, _, _)| { - (*format == case.input.format).then_some(*target) + .find_map(|(candidate, format, _, _)| { + (*format == case.input.format).then_some(*candidate) }) .ok_or_else(|| { format!( @@ -565,6 +957,12 @@ fn corpus_seed_inventory(workspace_root: &Path) -> Result<Vec<(String, Vec<u8>)> case.id, case.input.format ) })?; + if *case_id != case.id || *target != expected_target { + return Err(format!( + "fuzz seed specification for {} has the wrong identity or target", + case.id + )); + } let bytes = hex::decode(&case.input.bytes_hex) .map_err(|error| format!("decode vector {} bytes_hex: {error}", case.id))?; if bytes.is_empty() { @@ -573,19 +971,16 @@ fn corpus_seed_inventory(workspace_root: &Path) -> Result<Vec<(String, Vec<u8>)> case.id )); } - Ok(( - format!("{FUZZ_CORPUS_RELATIVE}/{target}/{}.bin", case.id), + Ok(ExpectedSeed { + relative, + target, + case, bytes, - )) + }) }) .collect() } -fn corpus_seed_paths(workspace_root: &Path) -> Result<Vec<String>, String> { - corpus_seed_inventory(workspace_root) - .map(|seeds| seeds.into_iter().map(|(relative, _)| relative).collect()) -} - fn pat_ident_and_type(argument: &FnArg) -> Option<(String, &Type)> { let FnArg::Typed(argument) = argument else { return None; @@ -854,9 +1249,16 @@ fn validate_fuzz_target_source( fn validate_fuzz_corpus(workspace_root: &Path) -> Result<(), String> { let mut expected = BTreeMap::new(); - for (relative, bytes) in corpus_seed_inventory(workspace_root)? { - if expected.insert(relative.clone(), bytes).is_some() { - return Err(format!("duplicate fuzz seed authority for {relative}")); + let vector = validate_vector(workspace_root)?; + for seed in expected_seeds(&vector)? { + if expected + .insert(seed.relative.to_owned(), seed.bytes) + .is_some() + { + return Err(format!( + "duplicate fuzz seed authority for {}", + seed.relative + )); } } if expected.len() != 30 { @@ -937,7 +1339,7 @@ fn validate_source_contract(workspace_root: &Path) -> Result<(), String> { validate_resource_limits(workspace_root)?; validate_error_authority(workspace_root)?; validate_vector_executor(workspace_root)?; - validate_fuzz_authority(workspace_root)?; + validate_fuzz_authority(workspace_root, false)?; validate_nix_lanes(workspace_root)?; validate_operations_authority(workspace_root)?; validate_release_authority(workspace_root)?; @@ -1042,9 +1444,23 @@ fn validate_error_authority(workspace_root: &Path) -> Result<(), String> { } } } - if !variants.contains("PublicationRasterProcessForbidden") { + let boundary_variants = variants + .into_iter() + .filter(|variant| { + variant.starts_with("PublicationRaster") + || variant == "PublicationJpegProcessForbidden" + || variant == "UnsupportedPublicationRasterMediaType" + || variant == "InvalidPublicationRaster" + }) + .collect::<BTreeSet<_>>(); + if boundary_variants + != NORMATIVE_ERROR_VARIANTS + .iter() + .map(|variant| (*variant).to_owned()) + .collect() + { return Err(format!( - "{ERROR_SOURCE_RELATIVE} must expose the PublicationRasterProcessForbidden variant" + "{ERROR_SOURCE_RELATIVE} raster decoder error variant authority drifted" )); } @@ -1064,9 +1480,24 @@ fn validate_error_authority(workspace_root: &Path) -> Result<(), String> { let mut literals = StringLiterals::default(); literals.visit_file(&file); - if !literals.0.contains("publication_raster_process_forbidden") { + let boundary_codes = literals + .0 + .into_iter() + .filter(|code| { + code.starts_with("publication_raster_") + || code.starts_with("publication_jpeg_") + || code == "unsupported_publication_raster_media_type" + || code == "invalid_publication_raster" + }) + .collect::<BTreeSet<_>>(); + if boundary_codes + != NORMATIVE_ERROR_CODES + .iter() + .map(|code| (*code).to_owned()) + .collect() + { return Err(format!( - "{ERROR_SOURCE_RELATIVE} must map the stable publication_raster_process_forbidden code" + "{ERROR_SOURCE_RELATIVE} raster decoder error code authority drifted" )); } Ok(()) @@ -1161,6 +1592,94 @@ fn validate_vector_executor(workspace_root: &Path) -> Result<(), String> { "{VECTOR_EXECUTOR_RELATIVE} must execute the public verify_publication_readiness operation" )); } + validate_resource_probe_executor(&file)?; + Ok(()) +} + +fn validate_resource_probe_executor(file: &syn::File) -> Result<(), String> { + let implementations = file + .items + .iter() + .filter_map(|item| match item { + Item::Impl(item) + if matches!( + item.self_ty.as_ref(), + Type::Path(path) if path.path.is_ident("ResourceProbeCase") + ) => + { + Some(item) + } + _ => None, + }) + .collect::<Vec<_>>(); + if implementations.len() != 1 { + return Err(format!( + "{VECTOR_EXECUTOR_RELATIVE} must define exactly one ResourceProbeCase implementation" + )); + } + let implementation = implementations[0]; + let all = implementation + .items + .iter() + .filter_map(|item| match item { + syn::ImplItem::Const(item) if item.ident == "ALL" => Some(item), + _ => None, + }) + .collect::<Vec<_>>(); + let ids = implementation + .items + .iter() + .filter_map(|item| match item { + syn::ImplItem::Fn(item) if item.sig.ident == "id" => Some(item), + _ => None, + }) + .collect::<Vec<_>>(); + if all.len() != 1 || ids.len() != 1 { + return Err(format!( + "{VECTOR_EXECUTOR_RELATIVE} must define one closed ResourceProbeCase::ALL and id mapping" + )); + } + let Type::Array(all_type) = &all[0].ty else { + return Err("ResourceProbeCase::ALL must use an explicit array type".to_owned()); + }; + let Expr::Lit(length) = &all_type.len else { + return Err("ResourceProbeCase::ALL must use a literal length".to_owned()); + }; + let Lit::Int(length) = &length.lit else { + return Err("ResourceProbeCase::ALL must use an integer length".to_owned()); + }; + let Expr::Array(elements) = &all[0].expr else { + return Err("ResourceProbeCase::ALL must use an explicit array value".to_owned()); + }; + if length.base10_parse::<usize>().ok() != Some(RESOURCE_PROBE_SPECS.len()) + || elements.elems.len() != RESOURCE_PROBE_SPECS.len() + { + return Err(format!( + "{VECTOR_EXECUTOR_RELATIVE} ResourceProbeCase::ALL must contain exactly {} cases", + RESOURCE_PROBE_SPECS.len() + )); + } + + #[derive(Default)] + struct StringLiterals(BTreeSet<String>); + + impl<'ast> Visit<'ast> for StringLiterals { + fn visit_lit_str(&mut self, value: &'ast syn::LitStr) { + self.0.insert(value.value()); + } + } + + let mut literals = StringLiterals::default(); + literals.visit_impl_item_fn(ids[0]); + let expected = RESOURCE_PROBE_SPECS + .iter() + .map(|(id, _, _, _, _)| (*id).to_owned()) + .collect::<BTreeSet<_>>(); + if literals.0 != expected { + return Err(format!( + "{VECTOR_EXECUTOR_RELATIVE} ResourceProbeCase::id inventory drifted" + )); + } Ok(()) } @@ -1171,7 +1690,7 @@ fn insert_fuzz_bin_name(bin_names: &mut BTreeSet<String>, name: &str) -> Result< Ok(()) } -fn validate_fuzz_authority(workspace_root: &Path) -> Result<(), String> { +fn validate_fuzz_authority(workspace_root: &Path, validate_corpus: bool) -> Result<(), String> { let workspace = parse_toml(workspace_root, WORKSPACE_MANIFEST_RELATIVE)?; let excludes = toml_string_array( "workspace exclude", @@ -1323,7 +1842,9 @@ fn validate_fuzz_authority(workspace_root: &Path) -> Result<(), String> { for (target, _, media_type, extension) in FUZZ_TARGET_SPECS { validate_fuzz_target(workspace_root, target, media_type, extension)?; } - validate_fuzz_corpus(workspace_root)?; + if validate_corpus { + validate_fuzz_corpus(workspace_root)?; + } read_regular_file(workspace_root, SECURITY_DOCUMENT_RELATIVE)?; read_regular_file(workspace_root, BLOSSOM_MANIFEST_RELATIVE)?; Ok(()) @@ -1349,6 +1870,8 @@ fn validate_nix_lanes(workspace_root: &Path) -> Result<(), String> { "-runs=256", "-seed=424242", "-max_len=65536", + "resource_cases='jpeg_grayscale jpeg_rgb jpeg_cmyk jpeg_sof1 png_rgb png_palette png_rgba png_adam7 webp_vp8_rgb webp_vp8_alpha webp_vp8l_rgb webp_vp8l_alpha'", + "for repetition in 1 2 3", ], ), ] { @@ -1372,6 +1895,15 @@ fn validate_operations_authority(workspace_root: &Path) -> Result<(), String> { .get("operations") .and_then(toml::Value::as_table) .ok_or_else(|| "operations.toml has no operations table".to_owned())?; + if operations.contains_key(PHANTOM_OPERATION_KEY) + || operations.values().any(|operation| { + operation.get("id").and_then(toml::Value::as_str) == Some(PHANTOM_OPERATION_ID) + }) + { + return Err( + "operations.toml must not reintroduce the synthetic raster operation".to_owned(), + ); + } let operation = operations .get(OPERATION_KEY) .and_then(toml::Value::as_table) @@ -1414,9 +1946,9 @@ fn validate_operations_authority(workspace_root: &Path) -> Result<(), String> { .get("conformance") .and_then(toml::Value::as_table) .ok_or_else(|| format!("{OPERATION_KEY} conformance is missing"))?; - if conformance.get("vector").and_then(toml::Value::as_str) != Some(VECTOR_RELATIVE) + if conformance.get("vector").and_then(toml::Value::as_str) != Some(PRIMARY_OPERATION_VECTOR) || toml_string_array("operation case kinds", conformance.get("case_kinds"))? - != owned(&[ACCEPTED_KIND, REJECTED_KIND]) + != owned(PRIMARY_OPERATION_CASE_KINDS) { return Err(format!("{OPERATION_KEY} conformance drifted")); } @@ -1487,7 +2019,8 @@ fn validate_vector(workspace_root: &Path) -> Result<ValidatedVector, String> { } let mut ids = BTreeSet::new(); let mut accepted_formats = BTreeSet::new(); - let mut mutations = BTreeSet::new(); + let mut accepted_mutations = BTreeSet::new(); + let mut rejected_mutations = BTreeSet::new(); let mut rejected_codes = BTreeSet::new(); for case in &suite.vectors { if !ids.insert(case.id.clone()) @@ -1506,7 +2039,6 @@ fn validate_vector(workspace_root: &Path) -> Result<ValidatedVector, String> { .to_owned(), ); } - mutations.insert(case.input.mutation.clone()); match case.kind.as_str() { ACCEPTED_KIND => { if !case.expected.accepted @@ -1517,6 +2049,7 @@ fn validate_vector(workspace_root: &Path) -> Result<ValidatedVector, String> { return Err(format!("{} has invalid acceptance expectation", case.id)); } accepted_formats.insert(case.input.format.clone()); + accepted_mutations.insert(case.input.mutation.clone()); } REJECTED_KIND => { if case.expected.accepted @@ -1526,10 +2059,11 @@ fn validate_vector(workspace_root: &Path) -> Result<ValidatedVector, String> { .expected .error .as_deref() - .is_some_and(|error| REJECTED_ERROR_CODES.contains(&error)) + .is_some_and(|error| OBSERVED_REJECTION_ERROR_CODES.contains(&error)) { return Err(format!("{} has invalid rejection expectation", case.id)); } + rejected_mutations.insert(case.input.mutation.clone()); rejected_codes.insert(case.expected.error.clone().unwrap_or_default()); } kind => return Err(format!("{} uses unsupported kind {kind}", case.id)), @@ -1546,24 +2080,45 @@ fn validate_vector(workspace_root: &Path) -> Result<ValidatedVector, String> { .to_owned(), ); } - if mutations - != REQUIRED_MUTATIONS + if accepted_mutations + != REQUIRED_ACCEPTED_MUTATIONS .iter() .map(|mutation| (*mutation).to_owned()) .collect() { - return Err("Blossom raster decoder security mutation inventory drifted".to_owned()); + return Err("Blossom raster decoder accepted mutation inventory drifted".to_owned()); } - for required in REQUIRED_DEDICATED_ERROR_CODES { - if !rejected_codes.contains(*required) { - return Err(format!( - "Blossom raster decoder security vector is missing rejection {required}" - )); - } + if rejected_mutations + != REQUIRED_REJECTED_MUTATIONS + .iter() + .map(|mutation| (*mutation).to_owned()) + .collect() + { + return Err("Blossom raster decoder rejected mutation inventory drifted".to_owned()); + } + if accepted_mutations + .union(&rejected_mutations) + .cloned() + .collect::<BTreeSet<_>>() + != ALL_PROVENANCE_MUTATIONS + .iter() + .map(|mutation| (*mutation).to_owned()) + .collect() + { + return Err("Blossom raster decoder provenance mutation inventory drifted".to_owned()); + } + if rejected_codes + != OBSERVED_REJECTION_ERROR_CODES + .iter() + .map(|code| (*code).to_owned()) + .collect() + { + return Err("Blossom raster decoder observed error inventory drifted".to_owned()); } Ok(ValidatedVector { bytes, case_ids: ordered_ids, + cases: suite.vectors, }) } @@ -1574,7 +2129,22 @@ fn manifest_schema() -> Value { "title": "Radroots Blossom raster decoder security semantic contract", "type": "object", "additionalProperties": false, - "required": ["schema_version", "contract_id", "authority_id", "manifest_schema", "predecessors", "protocol_sources", "resource_limits", "decoder_profile", "fuzz_campaign", "nix_lanes", "operation", "release_change_id", "result_vector"], + "required": [ + "schema_version", + "contract_id", + "authority_id", + "manifest_schema", + "predecessors", + "protocol_sources", + "resource_limits", + "decoder_profile", + "resource_probes", + "fuzz_campaign", + "nix_lanes", + "operation", + "release_change_id", + "result_vector" + ], "properties": { "schema_version": {"const": SCHEMA_VERSION}, "contract_id": {"const": CONTRACT_ID}, @@ -1584,25 +2154,27 @@ fn manifest_schema() -> Value { "type": "array", "minItems": 1, "maxItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": ["contract_id", "immutable_artifacts", "source_supersessions"], - "properties": { - "contract_id": {"type": "string", "minLength": 1}, - "immutable_artifacts": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/file"}}, - "source_supersessions": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}, "uniqueItems": true} - } - } + "items": {"$ref": "#/$defs/predecessor"} + }, + "protocol_sources": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": {"$ref": "#/$defs/protocol_source"} }, - "protocol_sources": {"type": "array", "minItems": 2, "maxItems": 2, "items": {"type": "object"}}, - "resource_limits": {"type": "object"}, - "decoder_profile": {"type": "object"}, - "fuzz_campaign": {"type": "object"}, - "nix_lanes": {"type": "object"}, - "operation": {"type": "object"}, + "resource_limits": {"$ref": "#/$defs/resource_limits"}, + "decoder_profile": {"$ref": "#/$defs/decoder_profile"}, + "resource_probes": { + "type": "array", + "minItems": 12, + "maxItems": 12, + "items": {"$ref": "#/$defs/resource_probe"} + }, + "fuzz_campaign": {"$ref": "#/$defs/fuzz_campaign"}, + "nix_lanes": {"$ref": "#/$defs/nix_lanes"}, + "operation": {"$ref": "#/$defs/operation"}, "release_change_id": {"const": RELEASE_CHANGE_ID}, - "result_vector": {"type": "object"} + "result_vector": {"$ref": "#/$defs/result_vector"} }, "$defs": { "file": { @@ -1615,6 +2187,274 @@ fn manifest_schema() -> Value { "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, "hash_algorithm": {"const": HASH_ALGORITHM} } + }, + "predecessor": { + "type": "object", + "additionalProperties": false, + "required": ["contract_id", "immutable_artifacts", "source_supersessions"], + "properties": { + "contract_id": {"const": "radroots_blossom.publication_readiness_v1"}, + "immutable_artifacts": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/file"} + }, + "source_supersessions": { + "type": "array", + "minItems": 1, + "items": {"type": "string", "minLength": 1}, + "uniqueItems": true + } + } + }, + "protocol_source": { + "type": "object", + "additionalProperties": false, + "required": ["id", "repository", "revision"], + "properties": { + "id": {"enum": ["nostr_nips", "blossom"]}, + "repository": { + "enum": [ + "https://github.com/nostr-protocol/nips", + "https://github.com/hzrd149/blossom" + ] + }, + "revision": {"type": "string", "pattern": "^[0-9a-f]{40}$"} + } + }, + "resource_limits": { + "type": "object", + "additionalProperties": false, + "required": [ + "max_raster_bytes", + "max_decoded_bytes", + "max_dimension", + "max_pixels", + "max_container_records", + "jpeg_max_scans", + "jpeg_max_blocks", + "jpeg_max_coefficient_steps", + "jpeg_entropy_bit_reads_per_byte", + "peak_rss_kib_limit" + ], + "properties": { + "max_raster_bytes": {"const": MAX_RASTER_BYTES}, + "max_decoded_bytes": {"const": MAX_DECODED_BYTES}, + "max_dimension": {"const": MAX_DIMENSION}, + "max_pixels": {"const": MAX_PIXELS}, + "max_container_records": {"const": MAX_CONTAINER_RECORDS}, + "jpeg_max_scans": {"const": JPEG_MAX_SCANS}, + "jpeg_max_blocks": {"const": JPEG_MAX_BLOCKS}, + "jpeg_max_coefficient_steps": {"const": JPEG_MAX_COEFFICIENT_STEPS}, + "jpeg_entropy_bit_reads_per_byte": {"const": JPEG_ENTROPY_BIT_READS_PER_BYTE}, + "peak_rss_kib_limit": {"const": PEAK_RSS_KIB_LIMIT} + } + }, + "decoder_profile": { + "type": "object", + "additionalProperties": false, + "required": [ + "accepted_formats", + "accepted_jpeg_processes", + "accepted_png_color_types", + "normative_error_codes", + "observed_rejection_error_codes", + "required_accepted_mutations", + "required_rejected_mutations" + ], + "properties": { + "accepted_formats": { + "const": ["image/jpeg", "image/png", "image/webp"] + }, + "accepted_jpeg_processes": { + "const": ["sof0_baseline_8bit", "sof1_extended_sequential_8bit"] + }, + "accepted_png_color_types": {"const": [0, 2, 3, 4, 6]}, + "normative_error_codes": { + "type": "array", + "minItems": 13, + "maxItems": 13, + "items": {"type": "string", "enum": NORMATIVE_ERROR_CODES}, + "uniqueItems": true + }, + "observed_rejection_error_codes": { + "type": "array", + "minItems": 7, + "maxItems": 7, + "items": {"type": "string", "enum": OBSERVED_REJECTION_ERROR_CODES}, + "uniqueItems": true + }, + "required_accepted_mutations": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": {"type": "string", "enum": REQUIRED_ACCEPTED_MUTATIONS}, + "uniqueItems": true + }, + "required_rejected_mutations": { + "type": "array", + "minItems": 14, + "maxItems": 14, + "items": {"type": "string", "enum": REQUIRED_REJECTED_MUTATIONS}, + "uniqueItems": true + } + } + }, + "resource_probe": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "format", + "process", + "width", + "height", + "max_encoded_bytes", + "max_decoded_bytes", + "executor_test", + "fixture_authority", + "repetitions", + "peak_rss_kib_limit" + ], + "properties": { + "id": { + "enum": [ + "jpeg_grayscale", "jpeg_rgb", "jpeg_cmyk", "jpeg_sof1", + "png_rgb", "png_palette", "png_rgba", "png_adam7", + "webp_vp8_rgb", "webp_vp8_alpha", "webp_vp8l_rgb", "webp_vp8l_alpha" + ] + }, + "format": {"enum": ["jpeg", "png", "webp"]}, + "process": {"type": "string", "minLength": 1}, + "width": {"const": 5000}, + "height": {"const": 4000}, + "max_encoded_bytes": {"const": MAX_RASTER_BYTES}, + "max_decoded_bytes": {"enum": [60_000_000, 80_000_000]}, + "executor_test": {"const": RESOURCE_TEST}, + "fixture_authority": {"type": "string", "minLength": 1}, + "repetitions": {"const": 3}, + "peak_rss_kib_limit": {"const": PEAK_RSS_KIB_LIMIT} + } + }, + "fuzz_target": { + "type": "object", + "additionalProperties": false, + "required": ["name", "format", "media_type", "extension", "binary"], + "properties": { + "name": {"enum": FUZZ_TARGETS}, + "format": {"enum": ["jpeg", "png", "webp"]}, + "media_type": {"enum": ["image/jpeg", "image/png", "image/webp"]}, + "extension": {"enum": ["jpg", "png", "webp"]}, + "binary": {"$ref": "#/$defs/file"} + } + }, + "corpus_seed": { + "type": "object", + "additionalProperties": false, + "required": [ + "vector_id", + "target", + "format", + "provenance_mutation", + "expected_accepted", + "file" + ], + "properties": { + "vector_id": {"type": "string", "enum": VECTOR_CASE_IDS}, + "target": {"type": "string", "enum": FUZZ_TARGETS}, + "format": {"enum": ["jpeg", "png", "webp"]}, + "provenance_mutation": {"type": "string", "enum": ALL_PROVENANCE_MUTATIONS}, + "expected_accepted": {"type": "boolean"}, + "expected_error": {"type": "string", "enum": OBSERVED_REJECTION_ERROR_CODES}, + "file": {"$ref": "#/$defs/file"} + } + }, + "fuzz_campaign": { + "type": "object", + "additionalProperties": false, + "required": [ + "project_manifest", + "project_lockfile", + "toolchain", + "toolchain_channel", + "toolchain_components", + "toolchain_profile", + "engine", + "sanitizer", + "common_harness", + "targets", + "corpus_seeds", + "smoke_runs", + "smoke_seed", + "smoke_max_input_bytes", + "smoke_timeout_seconds", + "engine_rss_limit_mb", + "extended_campaign_document" + ], + "properties": { + "project_manifest": {"$ref": "#/$defs/file"}, + "project_lockfile": {"$ref": "#/$defs/file"}, + "toolchain": {"$ref": "#/$defs/file"}, + "toolchain_channel": {"const": FUZZ_TOOLCHAIN_CHANNEL}, + "toolchain_components": {"const": ["rust-src"]}, + "toolchain_profile": {"const": "minimal"}, + "engine": {"const": "libfuzzer"}, + "sanitizer": {"const": "address"}, + "common_harness": {"$ref": "#/$defs/file"}, + "targets": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": {"$ref": "#/$defs/fuzz_target"} + }, + "corpus_seeds": { + "type": "array", + "minItems": 30, + "maxItems": 30, + "items": {"$ref": "#/$defs/corpus_seed"} + }, + "smoke_runs": {"const": FUZZ_SMOKE_RUNS}, + "smoke_seed": {"const": FUZZ_SMOKE_SEED}, + "smoke_max_input_bytes": {"const": FUZZ_SMOKE_MAX_INPUT_BYTES}, + "smoke_timeout_seconds": {"const": FUZZ_SMOKE_TIMEOUT_SECONDS}, + "engine_rss_limit_mb": {"const": FUZZ_ENGINE_RSS_LIMIT_MB}, + "extended_campaign_document": {"$ref": "#/$defs/file"} + } + }, + "nix_lanes": { + "type": "object", + "additionalProperties": false, + "required": ["app", "devshell", "stable_check", "fuzz_check"], + "properties": { + "app": {"const": "decoder-security"}, + "devshell": {"const": "decoder-security"}, + "stable_check": {"const": "blossom-raster-decode-test"}, + "fuzz_check": {"const": "blossom-decoder-fuzz-smoke"} + } + }, + "operation": { + "type": "object", + "additionalProperties": false, + "required": ["key", "id", "primary_vector", "primary_case_kinds", "security_case_kinds"], + "properties": { + "key": {"const": OPERATION_KEY}, + "id": {"const": OPERATION_ID}, + "primary_vector": {"const": PRIMARY_OPERATION_VECTOR}, + "primary_case_kinds": {"const": PRIMARY_OPERATION_CASE_KINDS}, + "security_case_kinds": {"const": [ACCEPTED_KIND, REJECTED_KIND]} + } + }, + "result_vector": { + "type": "object", + "additionalProperties": false, + "required": ["canonical", "mirror", "executor", "executor_tests", "case_ids"], + "properties": { + "canonical": {"$ref": "#/$defs/file"}, + "mirror": {"$ref": "#/$defs/file"}, + "executor": {"$ref": "#/$defs/file"}, + "executor_tests": {"const": [REGRESSION_TEST, DIFFERENTIAL_TEST, RESOURCE_TEST]}, + "case_ids": {"const": VECTOR_CASE_IDS} + } } } }) @@ -1668,6 +2508,7 @@ fn validate_canonical_json<T: Serialize>( } fn validate_json_schema(schema: &Value, instance: &Value) -> Result<(), String> { + validate_closed_object_schemas(schema, "$")?; let validator = jsonschema::validator_for(schema) .map_err(|error| format!("compile {MANIFEST_SCHEMA_RELATIVE}: {error}"))?; let errors = validator @@ -1684,27 +2525,103 @@ fn validate_json_schema(schema: &Value, instance: &Value) -> Result<(), String> } } +fn validate_closed_object_schemas(value: &Value, path: &str) -> Result<(), String> { + match value { + Value::Object(object) => { + if object.get("type").and_then(Value::as_str) == Some("object") + && (object.get("additionalProperties").and_then(Value::as_bool) != Some(false) + || !object.get("properties").is_some_and(Value::is_object) + || !object.get("required").is_some_and(Value::is_array)) + { + return Err(format!( + "{MANIFEST_SCHEMA_RELATIVE} object schema {path} must close properties and declare its required inventory" + )); + } + for (key, child) in object { + validate_closed_object_schemas(child, &format!("{path}/{key}"))?; + } + } + Value::Array(values) => { + for (index, child) in values.iter().enumerate() { + validate_closed_object_schemas(child, &format!("{path}/{index}"))?; + } + } + _ => {} + } + Ok(()) +} + fn sha256_hex(bytes: &[u8]) -> String { hex::encode(Sha256::digest(bytes)) } #[cfg(test)] mod tests { - use super::{ - insert_fuzz_bin_name, validate_fuzz_authority, validate_fuzz_common_source, - validate_fuzz_target_source, validate_vector, - }; - use std::{collections::BTreeSet, path::Path}; + use super::*; + use std::{collections::BTreeSet, fs, path::PathBuf}; const COMMON_SOURCE: &str = include_str!("../../../../fuzz/fuzz_targets/common.rs"); const JPEG_TARGET_SOURCE: &str = include_str!("../../../../fuzz/fuzz_targets/publication_jpeg.rs"); + fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") + } + + fn write_temp_file(root: &Path, relative: &str, bytes: &[u8]) { + let path = root.join(relative); + fs::create_dir_all(path.parent().expect("temporary file parent")) + .expect("create temporary authority parent"); + fs::write(path, bytes).expect("write temporary authority file"); + } + + fn validate_temp_manifest(root: &Path, expected: &Value) -> Result<(), String> { + let schema_bytes = read_regular_file(root, MANIFEST_SCHEMA_RELATIVE)?; + let schema: Value = serde_json::from_slice(&schema_bytes) + .map_err(|error| format!("parse temporary schema: {error}"))?; + validate_canonical_json(MANIFEST_SCHEMA_RELATIVE, &schema_bytes, &schema)?; + let manifest_bytes = read_regular_file(root, MANIFEST_RELATIVE)?; + let manifest: RasterDecoderSecurityManifest = serde_json::from_slice(&manifest_bytes) + .map_err(|error| format!("parse temporary manifest: {error}"))?; + validate_canonical_json(MANIFEST_RELATIVE, &manifest_bytes, &manifest)?; + let actual = serde_json::to_value(manifest) + .map_err(|error| format!("serialize temporary manifest: {error}"))?; + validate_json_schema(&schema, &actual)?; + if &actual != expected { + return Err("temporary manifest retargeted governed authority".to_owned()); + } + Ok(()) + } + + fn object_at_mut<'a>( + value: &'a mut Value, + pointer: &str, + ) -> &'a mut serde_json::Map<String, Value> { + let target = if pointer.is_empty() { + value + } else { + value.pointer_mut(pointer).expect("authority JSON pointer") + }; + target.as_object_mut().expect("authority object") + } + + fn expect_manifest_mutation_rejected(expected: &Value, mutated: &Value, label: &str) { + let temp = tempfile::tempdir().expect("temporary manifest workspace"); + let schema_bytes = canonical_json_bytes(&manifest_schema()).expect("manifest schema bytes"); + write_temp_file(temp.path(), MANIFEST_SCHEMA_RELATIVE, &schema_bytes); + write_temp_file( + temp.path(), + MANIFEST_RELATIVE, + &canonical_json_bytes(mutated).expect("mutated manifest bytes"), + ); + validate_temp_manifest(temp.path(), expected).expect_err(label); + } + #[test] fn checked_in_fuzz_corpus_matches_exact_vector() { - let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let workspace_root = workspace_root(); validate_vector(&workspace_root).expect("exact 30-case vector authority"); - validate_fuzz_authority(&workspace_root) + validate_fuzz_authority(&workspace_root, true) .expect("raw seed inventory and structural fuzz authority"); } @@ -1765,4 +2682,234 @@ mod tests { insert_fuzz_bin_name(&mut bin_names, "publication_jpeg") .expect_err("duplicate target must fail closed"); } + + #[test] + fn nested_manifest_authority_mutations_fail_closed() { + struct AuthorityClass { + label: &'static str, + pointer: &'static str, + required_field: &'static str, + invalid_field: &'static str, + invalid_value: Value, + retarget_field: &'static str, + retarget_value: Value, + } + + let root = workspace_root(); + let schema = manifest_schema(); + validate_closed_object_schemas(&schema, "$").expect("fully closed manifest schema"); + let schema_bytes = canonical_json_bytes(&schema).expect("schema bytes"); + let expected = serde_json::to_value( + describe_manifest(&root, &schema_bytes).expect("live raster security manifest"), + ) + .expect("manifest value"); + let mirror_descriptor = expected["result_vector"]["mirror"].clone(); + let project_descriptor = expected["fuzz_campaign"]["project_manifest"].clone(); + let classes = vec![ + AuthorityClass { + label: "root", + pointer: "", + required_field: "schema_version", + invalid_field: "contract_id", + invalid_value: json!("invalid"), + retarget_field: "authority_id", + retarget_value: json!("retargeted_authority"), + }, + AuthorityClass { + label: "file descriptor", + pointer: "/manifest_schema", + required_field: "path", + invalid_field: "sha256", + invalid_value: json!("not-a-digest"), + retarget_field: "path", + retarget_value: json!("crates/blossom/contracts/retargeted.schema.json"), + }, + AuthorityClass { + label: "predecessor", + pointer: "/predecessors/0", + required_field: "contract_id", + invalid_field: "source_supersessions", + invalid_value: json!([]), + retarget_field: "contract_id", + retarget_value: json!("radroots_blossom.retargeted_v1"), + }, + AuthorityClass { + label: "protocol source", + pointer: "/protocol_sources/0", + required_field: "revision", + invalid_field: "revision", + invalid_value: json!("not-a-revision"), + retarget_field: "repository", + retarget_value: json!("https://github.com/hzrd149/blossom"), + }, + AuthorityClass { + label: "resource limits", + pointer: "/resource_limits", + required_field: "max_raster_bytes", + invalid_field: "max_raster_bytes", + invalid_value: json!(0), + retarget_field: "max_decoded_bytes", + retarget_value: json!(60_000_000), + }, + AuthorityClass { + label: "decoder profile", + pointer: "/decoder_profile", + required_field: "accepted_formats", + invalid_field: "normative_error_codes", + invalid_value: json!([]), + retarget_field: "accepted_formats", + retarget_value: json!(["image/png", "image/jpeg", "image/webp"]), + }, + AuthorityClass { + label: "resource probe", + pointer: "/resource_probes/0", + required_field: "id", + invalid_field: "id", + invalid_value: json!("invalid_probe"), + retarget_field: "id", + retarget_value: json!("jpeg_rgb"), + }, + AuthorityClass { + label: "fuzz campaign", + pointer: "/fuzz_campaign", + required_field: "toolchain_channel", + invalid_field: "smoke_runs", + invalid_value: json!(0), + retarget_field: "common_harness", + retarget_value: project_descriptor, + }, + AuthorityClass { + label: "fuzz target", + pointer: "/fuzz_campaign/targets/0", + required_field: "name", + invalid_field: "name", + invalid_value: json!("invalid_target"), + retarget_field: "name", + retarget_value: json!("publication_png"), + }, + AuthorityClass { + label: "corpus seed", + pointer: "/fuzz_campaign/corpus_seeds/0", + required_field: "vector_id", + invalid_field: "vector_id", + invalid_value: json!("invalid_vector"), + retarget_field: "target", + retarget_value: json!("publication_png"), + }, + AuthorityClass { + label: "Nix lanes", + pointer: "/nix_lanes", + required_field: "app", + invalid_field: "app", + invalid_value: json!("invalid-app"), + retarget_field: "stable_check", + retarget_value: json!("blossom-decoder-fuzz-smoke"), + }, + AuthorityClass { + label: "operation", + pointer: "/operation", + required_field: "id", + invalid_field: "primary_case_kinds", + invalid_value: json!([]), + retarget_field: "id", + retarget_value: json!(PHANTOM_OPERATION_ID), + }, + AuthorityClass { + label: "result vector", + pointer: "/result_vector", + required_field: "canonical", + invalid_field: "case_ids", + invalid_value: json!([]), + retarget_field: "canonical", + retarget_value: mirror_descriptor, + }, + ]; + + for class in classes { + let mut missing = expected.clone(); + object_at_mut(&mut missing, class.pointer).remove(class.required_field); + expect_manifest_mutation_rejected( + &expected, + &missing, + &format!("{} missing field", class.label), + ); + + let mut extra = expected.clone(); + object_at_mut(&mut extra, class.pointer).insert("unexpected".to_owned(), json!(true)); + expect_manifest_mutation_rejected( + &expected, + &extra, + &format!("{} extra field", class.label), + ); + + let mut mistyped = expected.clone(); + object_at_mut(&mut mistyped, class.pointer) + .insert(class.required_field.to_owned(), Value::Null); + expect_manifest_mutation_rejected( + &expected, + &mistyped, + &format!("{} mistyped field", class.label), + ); + + let mut invalid = expected.clone(); + object_at_mut(&mut invalid, class.pointer) + .insert(class.invalid_field.to_owned(), class.invalid_value); + expect_manifest_mutation_rejected( + &expected, + &invalid, + &format!("{} invalid field", class.label), + ); + + let mut retargeted = expected.clone(); + object_at_mut(&mut retargeted, class.pointer) + .insert(class.retarget_field.to_owned(), class.retarget_value); + expect_manifest_mutation_rejected( + &expected, + &retargeted, + &format!("{} retargeted field", class.label), + ); + } + } + + #[test] + fn fuzz_corpus_inventory_mutations_fail_closed() { + for mutation in ["missing", "extra", "retargeted"] { + let source = workspace_root(); + let vector = validate_vector(&source).expect("live vector authority"); + let temp = tempfile::tempdir().expect("temporary corpus workspace"); + write_temp_file( + temp.path(), + VECTOR_RELATIVE, + &read_regular_file(&source, VECTOR_RELATIVE).expect("live vector bytes"), + ); + for seed in expected_seeds(&vector).expect("expected raw seeds") { + write_temp_file(temp.path(), seed.relative, &seed.bytes); + } + + match mutation { + "missing" => fs::remove_file( + temp.path() + .join("fuzz/corpus/publication_jpeg/jpeg_baseline_gray_8bit.bin"), + ) + .expect("remove one governed seed"), + "extra" => write_temp_file( + temp.path(), + "fuzz/corpus/publication_png/unexpected.bin", + b"unexpected seed", + ), + "retargeted" => { + let original = temp + .path() + .join("fuzz/corpus/publication_jpeg/jpeg_baseline_gray_8bit.bin"); + let retargeted = temp + .path() + .join("fuzz/corpus/publication_png/jpeg_baseline_gray_8bit.bin"); + fs::rename(original, retargeted).expect("retarget governed seed"); + } + _ => unreachable!(), + } + validate_fuzz_corpus(temp.path()) + .expect_err(&format!("{mutation} corpus mutation must fail closed")); + } + } } diff --git a/tools/xtask/src/contract/nip09_reconciliation.rs b/tools/xtask/src/contract/nip09_reconciliation.rs @@ -17397,6 +17397,7 @@ mod tests { [ Some("std"), Some("dep:image"), + Some("dep:libwebp"), Some("dep:zune-core"), Some("dep:zune-jpeg") ], @@ -17406,7 +17407,7 @@ mod tests { .get_mut("dependencies") .and_then(toml::Value::as_table_mut) .expect("Blossom dependencies"); - for dependency in ["image", "zune-core", "zune-jpeg"] { + for dependency in ["image", "libwebp", "zune-core", "zune-jpeg"] { let removed = blossom_dependencies .remove(dependency) .unwrap_or_else(|| panic!("successor dependency {dependency} must be present")); diff --git a/tools/xtask/src/contract/phase1_publication_artifact.rs b/tools/xtask/src/contract/phase1_publication_artifact.rs @@ -127,6 +127,8 @@ const BLOSSOM_READINESS_RAW_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[ "tools/xtask/src/contract/food_availability_projection.rs", "tools/xtask/src/contract/source_maintenance.rs", ]; +const RASTER_DECODER_SECURITY_RAW_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = + &["build/nix/apps.nix", "build/nix/toolchains.nix"]; const PUBLIC_TYPES: &[&str] = &[ "RadrootsPhase1PublicationEventVariant", @@ -1409,10 +1411,16 @@ fn validate_immutable_raw_predecessor_under_lock(workspace_root: &Path) -> Resul .iter() .copied(), ) + .chain( + RASTER_DECODER_SECURITY_RAW_PREDECESSOR_SUPERSEDED_PATHS + .iter() + .copied(), + ) .collect::<BTreeSet<_>>(); if superseded.len() != RAW_PREDECESSOR_SUPERSEDED_PATHS.len() + BLOSSOM_READINESS_RAW_PREDECESSOR_SUPERSEDED_PATHS.len() + + RASTER_DECODER_SECURITY_RAW_PREDECESSOR_SUPERSEDED_PATHS.len() { return Err("raw predecessor supersession paths must be unique".to_owned()); } diff --git a/tools/xtask/src/contract/raw_source_rebuild.rs b/tools/xtask/src/contract/raw_source_rebuild.rs @@ -898,8 +898,11 @@ const BLOSSOM_READINESS_SUCCESSOR_TRANSITIVE_PATHS: &[&str] = &[ "crates/blossom/src/url.rs", ]; #[cfg(test)] -const BLOSSOM_READINESS_SUCCESSOR_DELEGATED_COMPILER_PATHS: &[&str] = - &[CONTRACT_LANE_SOURCE_RELATIVE]; +const RASTER_DECODER_SECURITY_SUCCESSOR_DELEGATED_COMPILER_PATHS: &[&str] = &[ + CONTRACT_APP_SOURCE_RELATIVE, + CONTRACT_LANE_SOURCE_RELATIVE, + TOOLCHAIN_ROUTING_SOURCE_RELATIVE, +]; const GENERATED_ARTIFACT_PATHS: &[&str] = &[ MANIFEST_RELATIVE, @@ -6588,9 +6591,9 @@ mod tests { .expect("complete event-store compiler-input authority"); validate_delegated_compiler_source_pins_with_supersessions( &root, - BLOSSOM_READINESS_SUCCESSOR_DELEGATED_COMPILER_PATHS, + RASTER_DECODER_SECURITY_SUCCESSOR_DELEGATED_COMPILER_PATHS, ) - .expect("delegated compiler source pins outside the active Blossom successor"); + .expect("delegated compiler source pins outside the active Blossom raster decoder security successor"); validate_xtask_manifest_authority(&root).expect("xtask compiler authority"); } @@ -6606,14 +6609,14 @@ mod tests { } validate_delegated_compiler_source_pins_with_supersessions( pinned_workspace.path(), - BLOSSOM_READINESS_SUCCESSOR_DELEGATED_COMPILER_PATHS, + RASTER_DECODER_SECURITY_SUCCESSOR_DELEGATED_COMPILER_PATHS, ) - .expect("current compiler source pins outside the active Blossom successor"); + .expect("current compiler source pins outside the active Blossom raster decoder security successor"); fs::write(pinned_workspace.path().join(FLAKE_LOCK_RELATIVE), "{}\n") .expect("mutate pinned flake lock"); validate_delegated_compiler_source_pins_with_supersessions( pinned_workspace.path(), - BLOSSOM_READINESS_SUCCESSOR_DELEGATED_COMPILER_PATHS, + RASTER_DECODER_SECURITY_SUCCESSOR_DELEGATED_COMPILER_PATHS, ) .expect_err("compiler source mutation must fail closed"); diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -25,6 +25,7 @@ fn usage() { eprintln!(" cargo xtask contract phase1-publication-artifact-manifest [--write]"); eprintln!(" cargo xtask contract phase1-publication-allowlist-manifest [--write]"); eprintln!(" cargo xtask contract blossom-publication-readiness-manifest [--write]"); + eprintln!(" cargo xtask contract blossom-raster-decoder-security-manifest [--write]"); eprintln!(" cargo xtask contract phase1-publication-media-readiness-manifest [--write]"); eprintln!(" cargo xtask contract release-provenance-schema [--write]"); eprintln!(" cargo xtask contract knowledge-manifest [--write]"); @@ -206,6 +207,16 @@ fn run_contract(args: &[String]) -> Result<(), String> { .to_string(), ), }, + Some("blossom-raster-decoder-security-manifest") => match &args[1..] { + [] => contract::validate_blossom_raster_decoder_security_manifest(&workspace_root()), + [flag] if flag == "--write" => { + contract::write_blossom_raster_decoder_security_manifest(&workspace_root()) + } + _ => Err( + "blossom-raster-decoder-security-manifest accepts no arguments or exactly --write" + .to_string(), + ), + }, Some("phase1-publication-media-readiness-manifest") => match &args[1..] { [] => contract::validate_phase1_publication_media_readiness_manifest(&workspace_root()), [flag] if flag == "--write" => { @@ -369,6 +380,12 @@ mod tests { ]) .expect_err("invalid Phase 1 publication media-readiness manifest mode"); assert!(invalid_publication_media_readiness.contains("exactly --write")); + let invalid_raster_decoder_security = run_contract(&[ + "blossom-raster-decoder-security-manifest".to_string(), + "--invalid".to_string(), + ]) + .expect_err("invalid Blossom raster decoder security manifest mode"); + assert!(invalid_raster_decoder_security.contains("exactly --write")); let invalid_release_provenance_schema = run_contract(&[ "release-provenance-schema".to_string(), "--invalid".to_string(), @@ -490,6 +507,8 @@ mod tests { .expect("contract Phase 1 publication allowlist manifest"); run_contract(&["blossom-publication-readiness-manifest".to_string()]) .expect("contract Blossom publication-readiness manifest"); + run_contract(&["blossom-raster-decoder-security-manifest".to_string()]) + .expect("contract Blossom raster decoder security manifest"); run_contract(&["phase1-publication-media-readiness-manifest".to_string()]) .expect("contract Phase 1 publication media-readiness manifest"); run_contract(&["release-provenance-schema".to_string()])