From 80531172b671fc5c7a36d0b8a77ecc1bf07ba120 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 11:53:55 -0700 Subject: [PATCH 001/170] test(core): define release manifest artifact admission contract --- .../tests/release_manifest.rs | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 crates/originweave-core/tests/release_manifest.rs diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs new file mode 100644 index 000000000..edb16481c --- /dev/null +++ b/crates/originweave-core/tests/release_manifest.rs @@ -0,0 +1,209 @@ +use std::error::Error; + +use originweave_core::release_manifest::{ + MAX_RELEASE_ARTIFACTS, MAX_RELEASE_ARTIFACT_NAME_BYTES, MAX_RELEASE_REVISION_BYTES, + ReleaseArtifact, ReleaseArtifactError, ReleaseChannel, ReleaseManifest, ReleaseManifestError, +}; + +const SOURCE_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; +const CHROMIUM_REVISION: &str = "150.0.7871.129"; + +fn sha256_digest(hex_digit: char) -> String { + format!("sha256:{}", hex_digit.to_string().repeat(64)) +} + +#[test] +fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result<(), Box> { + let runtime = ReleaseArtifact::new( + "originweave-linux-x86_64.tar.zst", + &sha256_digest('a'), + )?; + let sbom = ReleaseArtifact::new("originweave.spdx.json", &sha256_digest('b'))?; + + let manifest = ReleaseManifest::new( + SOURCE_COMMIT, + CHROMIUM_REVISION, + ReleaseChannel::Stable, + [sbom, runtime], + )?; + + assert_eq!(manifest.source_commit(), SOURCE_COMMIT); + assert_eq!(manifest.chromium_revision(), CHROMIUM_REVISION); + assert_eq!(manifest.channel(), ReleaseChannel::Stable); + assert_eq!(manifest.artifacts().len(), 2); + assert_eq!( + manifest.artifacts()[0].name(), + "originweave-linux-x86_64.tar.zst" + ); + assert_eq!( + manifest.artifacts()[0].sha256_digest(), + sha256_digest('a') + ); + assert_eq!(manifest.artifacts()[1].name(), "originweave.spdx.json"); + assert_eq!( + manifest.artifacts()[1].sha256_digest(), + sha256_digest('b') + ); + Ok(()) +} + +#[test] +fn release_artifact_rejects_ambiguous_names_and_noncanonical_digests() { + let valid_digest = sha256_digest('c'); + let overlong_name = "a".repeat(MAX_RELEASE_ARTIFACT_NAME_BYTES + 1); + + for invalid_name in [ + "", + ".hidden", + "artifact/child.bin", + "artifact\\child.bin", + "artifact..bin", + "artifact-.bin-", + overlong_name.as_str(), + ] { + assert_eq!( + ReleaseArtifact::new(invalid_name, &valid_digest), + Err(ReleaseArtifactError::InvalidName) + ); + } + + for invalid_digest in [ + "", + "sha256:abc", + "SHA256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "sha256:gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg", + ] { + assert_eq!( + ReleaseArtifact::new("originweave.bin", invalid_digest), + Err(ReleaseArtifactError::InvalidDigest) + ); + } +} + +#[test] +fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evidence( +) -> Result<(), Box> { + let artifact = ReleaseArtifact::new("originweave.bin", &sha256_digest('d'))?; + + for invalid_commit in [ + "", + "0123456789abcdef0123456789abcdef0123456", + "0123456789ABCDEF0123456789ABCDEF01234567", + "g123456789abcdef0123456789abcdef01234567", + ] { + assert_eq!( + ReleaseManifest::new( + invalid_commit, + CHROMIUM_REVISION, + ReleaseChannel::Development, + [artifact.clone()], + ), + Err(ReleaseManifestError::InvalidSourceCommit) + ); + } + + let overlong_revision = "a".repeat(MAX_RELEASE_REVISION_BYTES + 1); + for invalid_revision in [ + "", + " chromium-150", + "chromium 150", + "chromium/150", + "chromium-150-", + overlong_revision.as_str(), + ] { + assert_eq!( + ReleaseManifest::new( + SOURCE_COMMIT, + invalid_revision, + ReleaseChannel::Beta, + [artifact.clone()], + ), + Err(ReleaseManifestError::InvalidChromiumRevision) + ); + } + + assert_eq!( + ReleaseManifest::new( + SOURCE_COMMIT, + CHROMIUM_REVISION, + ReleaseChannel::Stable, + std::iter::empty(), + ), + Err(ReleaseManifestError::MissingArtifacts) + ); + + let duplicate = ReleaseArtifact::new("originweave.bin", &sha256_digest('e'))?; + assert_eq!( + ReleaseManifest::new( + SOURCE_COMMIT, + CHROMIUM_REVISION, + ReleaseChannel::Stable, + [artifact.clone(), duplicate], + ), + Err(ReleaseManifestError::DuplicateArtifactName) + ); + + let mut too_many = Vec::new(); + for index in 0..=MAX_RELEASE_ARTIFACTS { + too_many.push(ReleaseArtifact::new( + &format!("artifact-{index}.bin"), + &sha256_digest('f'), + )?); + } + assert_eq!( + ReleaseManifest::new( + SOURCE_COMMIT, + CHROMIUM_REVISION, + ReleaseChannel::Stable, + too_many, + ), + Err(ReleaseManifestError::TooManyArtifacts) + ); + Ok(()) +} + +#[test] +fn release_manifest_errors_are_standard_source_free_rust_errors() { + let artifact_errors = [ + ( + ReleaseArtifactError::InvalidName, + "release artifact name is not a canonical bounded leaf name", + ), + ( + ReleaseArtifactError::InvalidDigest, + "release artifact digest must be sha256: followed by 64 lowercase hexadecimal digits", + ), + ]; + for (error, expected) in artifact_errors { + assert_eq!(error.to_string(), expected); + assert!(Error::source(&error).is_none()); + } + + let manifest_errors = [ + ( + ReleaseManifestError::InvalidSourceCommit, + "release source commit must be exactly 40 lowercase hexadecimal digits", + ), + ( + ReleaseManifestError::InvalidChromiumRevision, + "Chromium revision must be a canonical bounded release token", + ), + ( + ReleaseManifestError::MissingArtifacts, + "release manifest must contain at least one artifact", + ), + ( + ReleaseManifestError::TooManyArtifacts, + "release manifest exceeds the artifact-count limit", + ), + ( + ReleaseManifestError::DuplicateArtifactName, + "release manifest contains a duplicate artifact name", + ), + ]; + for (error, expected) in manifest_errors { + assert_eq!(error.to_string(), expected); + assert!(Error::source(&error).is_none()); + } +} From 663087f6a3bbc475d7dba492bb90cdf652fb51a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 11:55:29 -0700 Subject: [PATCH 002/170] test(core): format release manifest contract before semantic red --- .../tests/release_manifest.rs | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs index edb16481c..cd63693db 100644 --- a/crates/originweave-core/tests/release_manifest.rs +++ b/crates/originweave-core/tests/release_manifest.rs @@ -1,7 +1,7 @@ use std::error::Error; use originweave_core::release_manifest::{ - MAX_RELEASE_ARTIFACTS, MAX_RELEASE_ARTIFACT_NAME_BYTES, MAX_RELEASE_REVISION_BYTES, + MAX_RELEASE_ARTIFACT_NAME_BYTES, MAX_RELEASE_ARTIFACTS, MAX_RELEASE_REVISION_BYTES, ReleaseArtifact, ReleaseArtifactError, ReleaseChannel, ReleaseManifest, ReleaseManifestError, }; @@ -13,11 +13,9 @@ fn sha256_digest(hex_digit: char) -> String { } #[test] -fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result<(), Box> { - let runtime = ReleaseArtifact::new( - "originweave-linux-x86_64.tar.zst", - &sha256_digest('a'), - )?; +fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result<(), Box> +{ + let runtime = ReleaseArtifact::new("originweave-linux-x86_64.tar.zst", &sha256_digest('a'))?; let sbom = ReleaseArtifact::new("originweave.spdx.json", &sha256_digest('b'))?; let manifest = ReleaseManifest::new( @@ -35,15 +33,9 @@ fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result< manifest.artifacts()[0].name(), "originweave-linux-x86_64.tar.zst" ); - assert_eq!( - manifest.artifacts()[0].sha256_digest(), - sha256_digest('a') - ); + assert_eq!(manifest.artifacts()[0].sha256_digest(), sha256_digest('a')); assert_eq!(manifest.artifacts()[1].name(), "originweave.spdx.json"); - assert_eq!( - manifest.artifacts()[1].sha256_digest(), - sha256_digest('b') - ); + assert_eq!(manifest.artifacts()[1].sha256_digest(), sha256_digest('b')); Ok(()) } @@ -82,8 +74,8 @@ fn release_artifact_rejects_ambiguous_names_and_noncanonical_digests() { } #[test] -fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evidence( -) -> Result<(), Box> { +fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evidence() +-> Result<(), Box> { let artifact = ReleaseArtifact::new("originweave.bin", &sha256_digest('d'))?; for invalid_commit in [ From 69a1c4f8507e6082af947543d6d4e0373122fbf1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 11:57:29 -0700 Subject: [PATCH 003/170] feat(core): add fail-closed release manifest identity primitive --- .../originweave-core/src/release_manifest.rs | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 crates/originweave-core/src/release_manifest.rs diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs new file mode 100644 index 000000000..d95de87ae --- /dev/null +++ b/crates/originweave-core/src/release_manifest.rs @@ -0,0 +1,255 @@ +//! Fail-closed identity binding for release artifacts. +//! +//! The types in this module are deliberately inert metadata contracts. They bind an exact +//! source commit, Chromium revision, release channel, and artifact digests without granting +//! signing, publication, installation, update, rollback, or release authority. + +use std::collections::BTreeSet; +use std::error::Error; +use std::fmt; + +/// Maximum number of artifacts admitted by one release manifest. +pub const MAX_RELEASE_ARTIFACTS: usize = 64; +/// Maximum UTF-8 byte length admitted for one canonical artifact leaf name. +pub const MAX_RELEASE_ARTIFACT_NAME_BYTES: usize = 128; +/// Maximum UTF-8 byte length admitted for one Chromium revision token. +pub const MAX_RELEASE_REVISION_BYTES: usize = 128; + +/// Buyer-visible release channel bound by a release manifest. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReleaseChannel { + /// Stable release channel. + Stable, + /// Beta release channel. + Beta, + /// Development release channel. + Development, +} + +/// One canonical release artifact identity. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReleaseArtifact { + name: String, + sha256_digest: String, +} + +impl ReleaseArtifact { + /// Construct one artifact from a canonical leaf name and lowercase SHA-256 digest. + /// + /// The digest must use the exact `sha256:` prefix followed by 64 lowercase hexadecimal + /// digits. Artifact names are ASCII leaf names and cannot contain path separators, + /// traversal-like double dots, leading punctuation, or trailing punctuation. + pub fn new(name: &str, sha256_digest: &str) -> Result { + if !valid_artifact_name(name) { + return Err(ReleaseArtifactError::InvalidName); + } + if !valid_sha256_digest(sha256_digest) { + return Err(ReleaseArtifactError::InvalidDigest); + } + Ok(Self { + name: name.to_owned(), + sha256_digest: sha256_digest.to_owned(), + }) + } + + /// Return the canonical artifact leaf name. + #[must_use] + pub fn name(&self) -> &str { + &self.name + } + + /// Return the canonical lowercase `sha256:` artifact digest. + #[must_use] + pub fn sha256_digest(&self) -> &str { + &self.sha256_digest + } +} + +/// Validation error for one release artifact. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReleaseArtifactError { + /// The artifact name is not a canonical bounded leaf name. + InvalidName, + /// The artifact digest is not a canonical lowercase SHA-256 digest. + InvalidDigest, +} + +impl fmt::Display for ReleaseArtifactError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidName => { + formatter.write_str("release artifact name is not a canonical bounded leaf name") + } + Self::InvalidDigest => formatter.write_str( + "release artifact digest must be sha256: followed by 64 lowercase hexadecimal digits", + ), + } + } +} + +impl Error for ReleaseArtifactError {} + +/// Deterministic, bounded identity manifest for one OriginWeave release candidate. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReleaseManifest { + source_commit: String, + chromium_revision: String, + channel: ReleaseChannel, + artifacts: Vec, +} + +impl ReleaseManifest { + /// Construct an inert release manifest from exact identity evidence. + /// + /// Source identity is a full 40-digit lowercase Git commit SHA. Chromium revision is a + /// bounded canonical ASCII token. Artifact names must be unique and are sorted + /// deterministically before storage. Constructing this value does not authenticate any + /// artifact and does not authorize release or installation. + pub fn new( + source_commit: &str, + chromium_revision: &str, + channel: ReleaseChannel, + artifacts: I, + ) -> Result + where + I: IntoIterator, + { + if !valid_source_commit(source_commit) { + return Err(ReleaseManifestError::InvalidSourceCommit); + } + if !valid_revision(chromium_revision) { + return Err(ReleaseManifestError::InvalidChromiumRevision); + } + + let mut admitted = Vec::new(); + let mut artifact_names = BTreeSet::new(); + for artifact in artifacts { + if admitted.len() >= MAX_RELEASE_ARTIFACTS { + return Err(ReleaseManifestError::TooManyArtifacts); + } + if !artifact_names.insert(artifact.name.clone()) { + return Err(ReleaseManifestError::DuplicateArtifactName); + } + admitted.push(artifact); + } + if admitted.is_empty() { + return Err(ReleaseManifestError::MissingArtifacts); + } + admitted.sort_by(|left, right| left.name.cmp(&right.name)); + + Ok(Self { + source_commit: source_commit.to_owned(), + chromium_revision: chromium_revision.to_owned(), + channel, + artifacts: admitted, + }) + } + + /// Return the exact lowercase source commit bound by this manifest. + #[must_use] + pub fn source_commit(&self) -> &str { + &self.source_commit + } + + /// Return the canonical Chromium revision token bound by this manifest. + #[must_use] + pub fn chromium_revision(&self) -> &str { + &self.chromium_revision + } + + /// Return the release channel bound by this manifest. + #[must_use] + pub const fn channel(&self) -> ReleaseChannel { + self.channel + } + + /// Return artifacts sorted deterministically by canonical name. + #[must_use] + pub fn artifacts(&self) -> &[ReleaseArtifact] { + &self.artifacts + } +} + +/// Validation error for release-manifest identity evidence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReleaseManifestError { + /// Source commit is not a full lowercase Git SHA-1 identity. + InvalidSourceCommit, + /// Chromium revision is not a canonical bounded release token. + InvalidChromiumRevision, + /// No release artifacts were supplied. + MissingArtifacts, + /// Artifact inventory exceeds the bounded release-manifest limit. + TooManyArtifacts, + /// Artifact inventory repeats a canonical artifact name. + DuplicateArtifactName, +} + +impl fmt::Display for ReleaseManifestError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidSourceCommit => formatter.write_str( + "release source commit must be exactly 40 lowercase hexadecimal digits", + ), + Self::InvalidChromiumRevision => { + formatter.write_str("Chromium revision must be a canonical bounded release token") + } + Self::MissingArtifacts => { + formatter.write_str("release manifest must contain at least one artifact") + } + Self::TooManyArtifacts => { + formatter.write_str("release manifest exceeds the artifact-count limit") + } + Self::DuplicateArtifactName => { + formatter.write_str("release manifest contains a duplicate artifact name") + } + } + } +} + +impl Error for ReleaseManifestError {} + +fn valid_artifact_name(name: &str) -> bool { + if name.is_empty() + || name.len() > MAX_RELEASE_ARTIFACT_NAME_BYTES + || !name.is_ascii() + || name.contains("..") + { + return false; + } + let bytes = name.as_bytes(); + bytes[0].is_ascii_alphanumeric() + && bytes[bytes.len() - 1].is_ascii_alphanumeric() + && bytes.iter().all(|byte| { + byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b'-') + }) +} + +fn valid_sha256_digest(digest: &str) -> bool { + let Some(hex) = digest.strip_prefix("sha256:") else { + return false; + }; + hex.len() == 64 + && hex + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) +} + +fn valid_source_commit(source_commit: &str) -> bool { + source_commit.len() == 40 + && source_commit + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) +} + +fn valid_revision(revision: &str) -> bool { + if revision.is_empty() || revision.len() > MAX_RELEASE_REVISION_BYTES || !revision.is_ascii() { + return false; + } + let bytes = revision.as_bytes(); + bytes[0].is_ascii_alphanumeric() + && bytes[bytes.len() - 1].is_ascii_alphanumeric() + && bytes.iter().all(|byte| { + byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b'-' | b'+' | b':' | b'@') + }) +} From dac79ef8f29b072cfc7821af1ad13729cfa2d19e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 12:01:08 -0700 Subject: [PATCH 004/170] feat(core): expose release manifest identity contract --- crates/originweave-core/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index b6ed55ff2..62f9a4ca4 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -7,6 +7,9 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] +/// Fail-closed identity binding for release artifacts. +pub mod release_manifest; + use std::collections::BTreeSet; use std::fmt; use std::net::{Ipv4Addr, Ipv6Addr}; From db4990c62b211b69cdf862da4b79c68547404eb7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 12:02:55 -0700 Subject: [PATCH 005/170] style(core): apply canonical release manifest formatting --- crates/originweave-core/src/release_manifest.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index d95de87ae..2ae774f25 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -188,9 +188,8 @@ pub enum ReleaseManifestError { impl fmt::Display for ReleaseManifestError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::InvalidSourceCommit => formatter.write_str( - "release source commit must be exactly 40 lowercase hexadecimal digits", - ), + Self::InvalidSourceCommit => formatter + .write_str("release source commit must be exactly 40 lowercase hexadecimal digits"), Self::InvalidChromiumRevision => { formatter.write_str("Chromium revision must be a canonical bounded release token") } @@ -220,9 +219,9 @@ fn valid_artifact_name(name: &str) -> bool { let bytes = name.as_bytes(); bytes[0].is_ascii_alphanumeric() && bytes[bytes.len() - 1].is_ascii_alphanumeric() - && bytes.iter().all(|byte| { - byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b'-') - }) + && bytes + .iter() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b'-')) } fn valid_sha256_digest(digest: &str) -> bool { From d611f20dd352bce738d3ebdd39ca4b128f7d2bf7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 12:07:16 -0700 Subject: [PATCH 006/170] test(core): close exact release manifest coverage branches --- .../tests/release_manifest.rs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs index cd63693db..3e52ea205 100644 --- a/crates/originweave-core/tests/release_manifest.rs +++ b/crates/originweave-core/tests/release_manifest.rs @@ -22,7 +22,7 @@ fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result< SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, - [sbom, runtime], + vec![sbom, runtime], )?; assert_eq!(manifest.source_commit(), SOURCE_COMMIT); @@ -36,6 +36,18 @@ fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result< assert_eq!(manifest.artifacts()[0].sha256_digest(), sha256_digest('a')); assert_eq!(manifest.artifacts()[1].name(), "originweave.spdx.json"); assert_eq!(manifest.artifacts()[1].sha256_digest(), sha256_digest('b')); + + let punctuation = ReleaseArtifact::new("originweave_cli-x86_64.bin", &sha256_digest('7'))?; + let punctuation_manifest = ReleaseManifest::new( + SOURCE_COMMIT, + "chromium_150-0+build:1@stable", + ReleaseChannel::Development, + vec![punctuation], + )?; + assert_eq!( + punctuation_manifest.chromium_revision(), + "chromium_150-0+build:1@stable" + ); Ok(()) } @@ -51,6 +63,7 @@ fn release_artifact_rejects_ambiguous_names_and_noncanonical_digests() { "artifact\\child.bin", "artifact..bin", "artifact-.bin-", + "artifact-µ.bin", overlong_name.as_str(), ] { assert_eq!( @@ -89,7 +102,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi invalid_commit, CHROMIUM_REVISION, ReleaseChannel::Development, - [artifact.clone()], + vec![artifact.clone()], ), Err(ReleaseManifestError::InvalidSourceCommit) ); @@ -102,6 +115,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi "chromium 150", "chromium/150", "chromium-150-", + "chromium-µ", overlong_revision.as_str(), ] { assert_eq!( @@ -109,7 +123,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, invalid_revision, ReleaseChannel::Beta, - [artifact.clone()], + vec![artifact.clone()], ), Err(ReleaseManifestError::InvalidChromiumRevision) ); @@ -120,7 +134,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, - std::iter::empty(), + Vec::new(), ), Err(ReleaseManifestError::MissingArtifacts) ); @@ -131,7 +145,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, - [artifact.clone(), duplicate], + vec![artifact.clone(), duplicate], ), Err(ReleaseManifestError::DuplicateArtifactName) ); From 489d33267dc114f7dd69650ad672ed5b6d62773e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 12:12:48 -0700 Subject: [PATCH 007/170] docs(changelog): record release manifest identity primitive --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d17419927..993909683 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, and deterministic bounded artifact leaf names with lowercase SHA-256 digests without granting signing, publication, installation, update, rollback, or release authority. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. From b5d5365e364497b6c68cd0c35f2101675510f372 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 13:22:48 -0700 Subject: [PATCH 008/170] test(core): reject case-colliding release artifacts --- crates/originweave-core/tests/release_manifest.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs index 3e52ea205..5085dfc93 100644 --- a/crates/originweave-core/tests/release_manifest.rs +++ b/crates/originweave-core/tests/release_manifest.rs @@ -150,6 +150,17 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi Err(ReleaseManifestError::DuplicateArtifactName) ); + let case_collision = ReleaseArtifact::new("ORIGINWEAVE.BIN", &sha256_digest('6'))?; + assert_eq!( + ReleaseManifest::new( + SOURCE_COMMIT, + CHROMIUM_REVISION, + ReleaseChannel::Stable, + vec![artifact.clone(), case_collision], + ), + Err(ReleaseManifestError::DuplicateArtifactName) + ); + let mut too_many = Vec::new(); for index in 0..=MAX_RELEASE_ARTIFACTS { too_many.push(ReleaseArtifact::new( From d640a41f70ea54def917be40adc24fa95050e592 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 13:25:27 -0700 Subject: [PATCH 009/170] fix(core): reject case-colliding release artifacts --- crates/originweave-core/src/release_manifest.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 2ae774f25..06e570744 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -102,9 +102,11 @@ impl ReleaseManifest { /// Construct an inert release manifest from exact identity evidence. /// /// Source identity is a full 40-digit lowercase Git commit SHA. Chromium revision is a - /// bounded canonical ASCII token. Artifact names must be unique and are sorted - /// deterministically before storage. Constructing this value does not authenticate any - /// artifact and does not authorize release or installation. + /// bounded canonical ASCII token. Artifact names must be unique under ASCII case folding + /// so one manifest cannot bind two names that collide on a case-insensitive target + /// filesystem; original spelling is preserved and artifacts are sorted deterministically + /// before storage. Constructing this value does not authenticate any artifact and does not + /// authorize release or installation. pub fn new( source_commit: &str, chromium_revision: &str, @@ -127,7 +129,7 @@ impl ReleaseManifest { if admitted.len() >= MAX_RELEASE_ARTIFACTS { return Err(ReleaseManifestError::TooManyArtifacts); } - if !artifact_names.insert(artifact.name.clone()) { + if !artifact_names.insert(artifact.name.to_ascii_lowercase()) { return Err(ReleaseManifestError::DuplicateArtifactName); } admitted.push(artifact); @@ -181,7 +183,7 @@ pub enum ReleaseManifestError { MissingArtifacts, /// Artifact inventory exceeds the bounded release-manifest limit. TooManyArtifacts, - /// Artifact inventory repeats a canonical artifact name. + /// Artifact inventory repeats an ASCII-case-folded artifact name. DuplicateArtifactName, } From 4627924ee55c855a4a3c7478f08dadf4543f05dc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 13:26:08 -0700 Subject: [PATCH 010/170] docs(changelog): record cross-platform artifact collision guard --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 993909683..11b268093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, and deterministic bounded artifact leaf names with lowercase SHA-256 digests without granting signing, publication, installation, update, rollback, or release authority. +- Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. From 2d00a17c5db6a78cf6a730ab9c4a7ba1b3df3041 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 13:32:31 -0700 Subject: [PATCH 011/170] test(core): reject Windows device artifact names --- .../tests/release_manifest.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs index 5085dfc93..fbcbeec2e 100644 --- a/crates/originweave-core/tests/release_manifest.rs +++ b/crates/originweave-core/tests/release_manifest.rs @@ -64,11 +64,28 @@ fn release_artifact_rejects_ambiguous_names_and_noncanonical_digests() { "artifact..bin", "artifact-.bin-", "artifact-µ.bin", + "CON", + "con.zip", + "PRN.tar.zst", + "aux.bin", + "NUL.spdx.json", + "COM1", + "com9.zip", + "LPT1", + "lpt9.tar.zst", overlong_name.as_str(), ] { assert_eq!( ReleaseArtifact::new(invalid_name, &valid_digest), - Err(ReleaseArtifactError::InvalidName) + Err(ReleaseArtifactError::InvalidName), + "reserved or ambiguous artifact name must fail closed: {invalid_name}" + ); + } + + for valid_name in ["console.bin", "com10.bin", "lpt10.tar.zst"] { + assert!( + ReleaseArtifact::new(valid_name, &valid_digest).is_ok(), + "non-device artifact name must remain admissible: {valid_name}" ); } From 091b5a7d55d715d8d24a2d474356f486c089bb07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 13:35:46 -0700 Subject: [PATCH 012/170] fix(core): reject Windows device artifact names --- .../originweave-core/src/release_manifest.rs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 06e570744..d7c90af2e 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -38,7 +38,8 @@ impl ReleaseArtifact { /// /// The digest must use the exact `sha256:` prefix followed by 64 lowercase hexadecimal /// digits. Artifact names are ASCII leaf names and cannot contain path separators, - /// traversal-like double dots, leading punctuation, or trailing punctuation. + /// traversal-like double dots, leading or trailing punctuation, or Windows reserved device + /// basenames (including those basenames followed by extensions). pub fn new(name: &str, sha256_digest: &str) -> Result { if !valid_artifact_name(name) { return Err(ReleaseArtifactError::InvalidName); @@ -215,6 +216,7 @@ fn valid_artifact_name(name: &str) -> bool { || name.len() > MAX_RELEASE_ARTIFACT_NAME_BYTES || !name.is_ascii() || name.contains("..") + || windows_reserved_device_basename(name) { return false; } @@ -226,6 +228,27 @@ fn valid_artifact_name(name: &str) -> bool { .all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b'-')) } +fn windows_reserved_device_basename(name: &str) -> bool { + let basename = match name.find('.') { + Some(dot_index) => &name[..dot_index], + None => name, + }; + + if basename.eq_ignore_ascii_case("CON") + || basename.eq_ignore_ascii_case("PRN") + || basename.eq_ignore_ascii_case("AUX") + || basename.eq_ignore_ascii_case("NUL") + { + return true; + } + + let bytes = basename.as_bytes(); + bytes.len() == 4 + && (basename[..3].eq_ignore_ascii_case("COM") + || basename[..3].eq_ignore_ascii_case("LPT")) + && matches!(bytes[3], b'1'..=b'9') +} + fn valid_sha256_digest(digest: &str) -> bool { let Some(hex) = digest.strip_prefix("sha256:") else { return false; From 10eeaedb8765d8ccf831a95db4ca27b151df61cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 13:37:19 -0700 Subject: [PATCH 013/170] style(core): apply canonical release-manifest formatting --- crates/originweave-core/src/release_manifest.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index d7c90af2e..972ff4fe7 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -244,8 +244,7 @@ fn windows_reserved_device_basename(name: &str) -> bool { let bytes = basename.as_bytes(); bytes.len() == 4 - && (basename[..3].eq_ignore_ascii_case("COM") - || basename[..3].eq_ignore_ascii_case("LPT")) + && (basename[..3].eq_ignore_ascii_case("COM") || basename[..3].eq_ignore_ascii_case("LPT")) && matches!(bytes[3], b'1'..=b'9') } From bcabafdcc59894f180733616735aabb26be13031 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 13:41:54 -0700 Subject: [PATCH 014/170] test(core): cover ordinary four-byte artifact basenames --- crates/originweave-core/tests/release_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs index fbcbeec2e..8155af484 100644 --- a/crates/originweave-core/tests/release_manifest.rs +++ b/crates/originweave-core/tests/release_manifest.rs @@ -82,7 +82,7 @@ fn release_artifact_rejects_ambiguous_names_and_noncanonical_digests() { ); } - for valid_name in ["console.bin", "com10.bin", "lpt10.tar.zst"] { + for valid_name in ["console.bin", "com10.bin", "lpt10.tar.zst", "data.bin"] { assert!( ReleaseArtifact::new(valid_name, &valid_digest).is_ok(), "non-device artifact name must remain admissible: {valid_name}" From 7f30d4e11ecc1f389e4d823b28de01eba86cd00a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 13:44:39 -0700 Subject: [PATCH 015/170] docs(adr): record release manifest identity boundary --- docs/adr/0015-release-manifest-identity.md | 101 +++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/adr/0015-release-manifest-identity.md diff --git a/docs/adr/0015-release-manifest-identity.md b/docs/adr/0015-release-manifest-identity.md new file mode 100644 index 000000000..093e372df --- /dev/null +++ b/docs/adr/0015-release-manifest-identity.md @@ -0,0 +1,101 @@ +# ADR 0015: Release manifest identity boundary + +- Status: Proposed +- Date: 2026-08-23 + +## Context + +Issue #201 requires commercial OriginWeave releases to bind an exact OriginWeave source identity, Chromium revision, release channel, and the artifacts buyers install or verify. That larger release lifecycle will later add signing, SBOM/SLSA provenance, updater trust, rollback, platform support, and operational acceptance. Those authorities do not yet exist in the current source tree. + +A smaller durable boundary is nevertheless required before packaging work can safely compose: one deterministic, bounded manifest identity for a release candidate. Without an explicit contract, equivalent release inventories can be represented differently, case-insensitive target filesystems can collapse distinct names, and host-specific device namespaces can reinterpret an apparent artifact leaf name. + +## Decision drivers + +- One release-candidate identity must not depend on caller ordering. +- Artifact references must remain leaf identities rather than filesystem paths. +- The same manifest must be unambiguous on supported case-sensitive and case-insensitive platforms. +- Manifest construction must remain inert metadata admission and must not grant signing, publication, installation, update, rollback, or release authority. +- Inputs must be bounded and fail closed before later packaging, signing, or updater layers consume them. + +## Assumptions and authority boundaries + +- Source identity is a full 40-character lowercase Git commit SHA. +- Chromium identity is a bounded canonical ASCII release token; this ADR does not claim that the token alone authenticates Chromium bytes. +- A release channel is explicit (`Stable`, `Beta`, or `Development`) metadata, not authorization to publish or promote a release. +- Every admitted artifact carries a bounded ASCII leaf name and an exact lowercase `sha256:` digest. +- The manifest contains no private signing material, credentials, secrets, installer authority, network authority, or update authority. +- Later release systems must independently authenticate the build, signer, provenance, platform package, updater metadata, and operational acceptance evidence. + +## Options considered + +### Caller-order manifest with host-local filenames + +Rejected. Caller ordering makes identity representation unstable, and host-local filename rules can produce collisions or device-name reinterpretation on another supported platform. + +### Canonical manifest that normalizes stored spelling + +Rejected for this slice. Destructively rewriting admitted artifact spelling would make the manifest differ from the exact release artifact identity that packaging and verification need to preserve. + +### Canonical admission with preserved spelling and collision guards + +Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, and separately reject names whose ASCII-case-folded identities collide or whose basenames are reserved Win32 device names. + +## Decision + +OriginWeave release-manifest admission is a deterministic, bounded, fail-closed identity contract: + +1. `source_commit` must be exactly 40 lowercase hexadecimal digits. +2. `chromium_revision` must be a non-empty bounded canonical ASCII token. +3. `channel` must be an explicit `ReleaseChannel` variant. +4. The artifact inventory must be non-empty and contain at most 64 entries. +5. Each artifact name must be a bounded ASCII leaf name containing only alphanumerics, `.`, `_`, and `-`; it cannot contain path separators, traversal-like `..`, leading punctuation, or trailing punctuation. +6. Artifact basenames `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9` are rejected case-insensitively, including when followed by an extension. The artifact grammar already rejects the non-ASCII superscript-digit Win32 aliases. +7. Artifact names must be unique under ASCII case folding while their original admitted spelling is retained. +8. Each artifact digest must be exactly `sha256:` followed by 64 lowercase hexadecimal digits. +9. Admitted artifacts are stored in deterministic name order. +10. Validation errors remain typed, deterministic standard Rust errors. + +Constructing or possessing a valid manifest does **not** authenticate an artifact, prove reproducibility, prove provenance, verify a signature, establish a signing identity, authorize a release channel, publish software, install software, update software, roll software back, or satisfy release acceptance. + +## Consequences + +The release candidate receives one bounded artifact-identity representation that is stable across caller order and avoids known case-insensitive and Win32 device-name collisions. Packaging, signing, provenance, and update layers can compose on top of this contract without inheriting ambient authority from it. + +The contract is intentionally narrower than issue #201's final release manifest. Additional fields such as toolchain identity, adapter versions, build environment, dependency locks, signing identity, timestamp, SBOM/provenance references, and platform package identity remain future reviewed work rather than being inferred from this primitive. + +## Failure and degraded behavior + +Malformed, ambiguous, duplicate, unbounded, or empty identity evidence fails closed before a manifest is produced. There is no fallback that silently rewrites an invalid name, accepts an alternate digest representation, drops an artifact, or substitutes another release channel. + +A caller that cannot provide canonical evidence does not receive a release manifest. That failure is not converted into permission to sign, publish, install, or update through another path. + +## Security / privacy / governance impact + +The boundary reduces path-confusion and cross-platform filename ambiguity without introducing credentials or protected values. It does not alter GitHub governance, reviewer authority, release signing authority, or protected-main policy. Scheduled development agents remain unable to merge, tag, publish, sign, or change release authority. + +## Tests and acceptance evidence + +The owning `originweave-core` tests must cover valid deterministic ordering, exact digests, identifier bounds, malformed names, path/traversal-like names, case-only collisions, Win32 reserved device basenames with and without extensions, neighboring admissible names, exact inventory bounds, duplicate names, channel access, and deterministic standard error contracts. + +Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. CI/security/scanner evidence is exact-head evidence only; predecessor or model-only evidence cannot satisfy acceptance. + +## Migration and rollback + +This is a new admission primitive with no protected-main persisted manifest migration. If the contract proves incompatible before acceptance, the Proposed ADR and owning feature branch can be revised or withdrawn without granting legacy inputs grandfathered authority. + +After acceptance and external release artifacts depend on this schema, any incompatible identity change requires an explicit versioning/migration decision rather than silent parser broadening. + +## Open follow-ups + +- Complete issue #201's signed cross-platform distribution and updater trust architecture. +- Bind toolchain, adapter, build-environment, dependency-lock, SBOM/SLSA provenance, signing, timestamp, and platform-package evidence through separately reviewed contracts. +- Define manifest serialization/versioning before any durable external release-manifest format is promised. +- Add signing-key rotation, compromise response, update rollback/freeze protection, and platform release acceptance without transferring authority from this metadata type. + +## Supersession / reversal conditions + +Supersede this ADR when a versioned external release-manifest specification replaces the in-process identity primitive, or when supported-platform packaging requires a stronger canonical filename identity. Reversal must preserve fail-closed artifact identity and cannot make possession of metadata equivalent to release authority. + +## References + +Microsoft. (n.d.). *Naming files, paths, and namespaces*. Microsoft Learn. Retrieved August 23, 2026, from https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file From cdd4bdd6fd4f6f901d17713e8352a6125d7a6a53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 13:55:36 -0700 Subject: [PATCH 016/170] docs: index proposed release manifest ADR --- docs/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/README.md b/docs/README.md index 03b573c54..ee7db1077 100644 --- a/docs/README.md +++ b/docs/README.md @@ -86,4 +86,10 @@ Proposed ADRs are reviewable architecture memory, not shipped behavior and not a The second group exists only on this documentation branch until the branch integrates. After integration, the heading remains useful historical provenance; it does not promote either ADR from Proposed to Accepted and it does not claim that the described runtime capability is implemented. +### Active feature proposed decisions + +- [ADR 0015: Release manifest identity boundary](adr/0015-release-manifest-identity.md) + +ADR 0015 is active-PR architecture evidence for the release-manifest identity slice. Its presence in this index keeps the documentation graph complete without promoting the Proposed decision or its implementation to protected-main truth. + See the [ADR index](adr/README.md) for status rules, required decision structure, supersession rules, and active feature ADRs. The index and each ADR's own status metadata must agree; a PR body, chat transcript, automation prompt, or stale issue reference cannot change ADR status. From 441fab74eea47450db65e6e0e7a4f08ad40af42f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 13:56:27 -0700 Subject: [PATCH 017/170] docs: index active release manifest ADR --- docs/adr/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/adr/README.md b/docs/adr/README.md index 416231b1c..de0644e81 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -57,6 +57,14 @@ Proposed ADR files are reviewable target architecture without becoming Accepted ADR 0013 and ADR 0014 exist only on this documentation branch until it integrates. After integration, this subsection remains historical provenance rather than an active-PR claim; both decisions remain Proposed until a later policy-compliant change explicitly changes their lifecycle. +### Active feature proposed decisions + +| ADR | Decision | Status | Governs | +|---|---|---|---| +| [0015](0015-release-manifest-identity.md) | Release manifest identity boundary | Proposed | bounded release-candidate metadata identity, deterministic artifact ordering, cross-platform filename collision guards, and explicit exclusion of signing/publication/install/update authority | + +ADR 0015 is active-PR architecture evidence for the release-manifest identity slice. Indexing it satisfies canonical discovery without promoting the Proposed decision or its implementation to protected-main truth. + Other active feature PRs may contain additional Proposed ADRs. Those files are not part of this canonical documentation line until integrated or deliberately reconciled here. Historical PR checks, stale branch state, or chat decisions never transfer ADR acceptance across a changed head. ## Index completeness rule From 459631a219a23e03f6872246f3b9ec521174c610 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 14:00:10 -0700 Subject: [PATCH 018/170] docs: trace release filename portability evidence --- docs/doctoring.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index f0133bb5d..483adc407 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -74,6 +74,10 @@ Credential-free TLS evidence records the canonical origin, TCP peers, reference The test-only rcgen 0.14.8 dependency creates a local CA and deterministic certificate-policy scenarios. It is not part of production arithmetic or trust. Tests cover trusted DNS identity, Common Name non-fallback, wrong name, untrusted root, expired and not-yet-valid validity, exact IPv4 and IPv6 SAN identity, TLS 1.2 and TLS 1.3, required and optional ALPN, and equality between TLS origin and TCP authority. +### Release artifact filename portability + +Microsoft's Win32 filename guidance requires applications not to assume case sensitivity and reserves device basenames including `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9`, including those names followed by extensions. The same documentation also identifies non-ASCII superscript-digit device aliases. OriginWeave's release-manifest identity therefore rejects ASCII-case-folded duplicate artifact names and those reserved ASCII device basenames; its ASCII-only artifact grammar separately excludes the superscript aliases. These checks make one admitted artifact inventory portable across case-sensitive and default case-insensitive target filesystems without rewriting the stored artifact spelling. They remain identity hygiene only and do not grant signing, publication, installation, update, rollback, or release authority. + ### Crawling policy RFC 9309 standardizes robots parsing, matching, error handling, and caching. It also states that robots rules are not access authorization. OriginWeave therefore requires robots evidence for public crawler mode while maintaining authentication, terms, rate, privacy, and retention policy as separate controls. @@ -142,6 +146,8 @@ Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *OAuth 2.0 securi Microsoft. (2025, July 25). *Azure IP address 168.63.129.16 overview*. Microsoft Learn. https://learn.microsoft.com/azure/virtual-network/what-is-ip-address-168-63-129-16 +Microsoft. (n.d.). *Naming files, paths, and namespaces*. Microsoft Learn. Retrieved August 23, 2026, from https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file + Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025). *Learning to orchestrate agents in natural language with the Conductor* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04388 Rescorla, E. (2026). *The Transport Layer Security (TLS) protocol version 1.3* (RFC 9846). Internet Engineering Task Force. https://doi.org/10.17487/RFC9846 @@ -170,4 +176,4 @@ World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 -Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 +Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 \ No newline at end of file From d14148353110dbac3bf3a66c39c56fe801075209 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 14:26:04 -0700 Subject: [PATCH 019/170] test(core): require release build identity evidence --- .../tests/release_manifest_build_identity.rs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 crates/originweave-core/tests/release_manifest_build_identity.rs diff --git a/crates/originweave-core/tests/release_manifest_build_identity.rs b/crates/originweave-core/tests/release_manifest_build_identity.rs new file mode 100644 index 000000000..02fbbcef5 --- /dev/null +++ b/crates/originweave-core/tests/release_manifest_build_identity.rs @@ -0,0 +1,71 @@ +use std::error::Error; + +use originweave_core::release_manifest::{ + MAX_RELEASE_TOOLCHAIN_BYTES, ReleaseBuildIdentity, ReleaseBuildIdentityError, +}; + +fn sha256_digest(hex_digit: char) -> String { + format!("sha256:{}", hex_digit.to_string().repeat(64)) +} + +#[test] +fn release_build_identity_binds_exact_toolchain_and_dependency_lock() -> Result<(), Box> { + let identity = ReleaseBuildIdentity::new("1.97.1", &sha256_digest('a'))?; + + assert_eq!(identity.rust_toolchain(), "1.97.1"); + assert_eq!(identity.dependency_lock_sha256(), sha256_digest('a')); + Ok(()) +} + +#[test] +fn release_build_identity_rejects_ambiguous_or_unbounded_evidence() { + let digest = sha256_digest('b'); + let overlong_toolchain = "a".repeat(MAX_RELEASE_TOOLCHAIN_BYTES + 1); + + for invalid_toolchain in [ + "", + " 1.97.1", + "1.97.1 ", + "rust 1.97.1", + "1.97.1/nightly", + "1.97.1-µ", + overlong_toolchain.as_str(), + ] { + assert_eq!( + ReleaseBuildIdentity::new(invalid_toolchain, &digest), + Err(ReleaseBuildIdentityError::InvalidRustToolchain), + ); + } + + for invalid_digest in [ + "", + "sha256:abc", + "SHA256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "sha256:gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg", + ] { + assert_eq!( + ReleaseBuildIdentity::new("1.97.1", invalid_digest), + Err(ReleaseBuildIdentityError::InvalidDependencyLockDigest), + ); + } +} + +#[test] +fn release_build_identity_errors_are_standard_source_free_rust_errors() { + let errors = [ + ( + ReleaseBuildIdentityError::InvalidRustToolchain, + "release Rust toolchain is not a canonical bounded token", + ), + ( + ReleaseBuildIdentityError::InvalidDependencyLockDigest, + "release dependency lock digest must be sha256: followed by 64 lowercase hexadecimal digits", + ), + ]; + + for (error, expected) in errors { + assert_eq!(error.to_string(), expected); + assert!(Error::source(&error).is_none()); + } +} From 78f23598bd8ea8cf469081650c03b331234d4525 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 14:28:12 -0700 Subject: [PATCH 020/170] test(core): canonicalize release build identity regression --- .../originweave-core/tests/release_manifest_build_identity.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/release_manifest_build_identity.rs b/crates/originweave-core/tests/release_manifest_build_identity.rs index 02fbbcef5..86d485427 100644 --- a/crates/originweave-core/tests/release_manifest_build_identity.rs +++ b/crates/originweave-core/tests/release_manifest_build_identity.rs @@ -9,7 +9,8 @@ fn sha256_digest(hex_digit: char) -> String { } #[test] -fn release_build_identity_binds_exact_toolchain_and_dependency_lock() -> Result<(), Box> { +fn release_build_identity_binds_exact_toolchain_and_dependency_lock() -> Result<(), Box> +{ let identity = ReleaseBuildIdentity::new("1.97.1", &sha256_digest('a'))?; assert_eq!(identity.rust_toolchain(), "1.97.1"); From 000ff2dd54883bea797b682ac34e5efb5eed6ccd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 14:32:05 -0700 Subject: [PATCH 021/170] feat(core): bind release build identity evidence --- .../originweave-core/src/release_manifest.rs | 104 +++++++++++++++++- .../tests/release_manifest.rs | 21 +++- 2 files changed, 118 insertions(+), 7 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 972ff4fe7..879739f41 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -1,8 +1,8 @@ //! Fail-closed identity binding for release artifacts. //! //! The types in this module are deliberately inert metadata contracts. They bind an exact -//! source commit, Chromium revision, release channel, and artifact digests without granting -//! signing, publication, installation, update, rollback, or release authority. +//! source commit, Chromium revision, release channel, build identity, and artifact digests +//! without granting signing, publication, installation, update, rollback, or release authority. use std::collections::BTreeSet; use std::error::Error; @@ -14,6 +14,8 @@ pub const MAX_RELEASE_ARTIFACTS: usize = 64; pub const MAX_RELEASE_ARTIFACT_NAME_BYTES: usize = 128; /// Maximum UTF-8 byte length admitted for one Chromium revision token. pub const MAX_RELEASE_REVISION_BYTES: usize = 128; +/// Maximum UTF-8 byte length admitted for one canonical Rust toolchain token. +pub const MAX_RELEASE_TOOLCHAIN_BYTES: usize = 64; /// Buyer-visible release channel bound by a release manifest. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -26,6 +28,73 @@ pub enum ReleaseChannel { Development, } +/// Exact build identity retained by one release manifest. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReleaseBuildIdentity { + rust_toolchain: String, + dependency_lock_sha256: String, +} + +impl ReleaseBuildIdentity { + /// Construct bounded build identity from a canonical Rust toolchain token and lock digest. + /// + /// The dependency-lock digest must use the exact `sha256:` prefix followed by 64 lowercase + /// hexadecimal digits. Constructing this value does not prove reproducibility or authenticate + /// the build environment; it only prevents those two identity fields from being omitted or + /// represented ambiguously in a release manifest. + pub fn new( + rust_toolchain: &str, + dependency_lock_sha256: &str, + ) -> Result { + if !valid_toolchain(rust_toolchain) { + return Err(ReleaseBuildIdentityError::InvalidRustToolchain); + } + if !valid_sha256_digest(dependency_lock_sha256) { + return Err(ReleaseBuildIdentityError::InvalidDependencyLockDigest); + } + Ok(Self { + rust_toolchain: rust_toolchain.to_owned(), + dependency_lock_sha256: dependency_lock_sha256.to_owned(), + }) + } + + /// Return the exact canonical Rust toolchain token. + #[must_use] + pub fn rust_toolchain(&self) -> &str { + &self.rust_toolchain + } + + /// Return the exact lowercase `sha256:` dependency-lock digest. + #[must_use] + pub fn dependency_lock_sha256(&self) -> &str { + &self.dependency_lock_sha256 + } +} + +/// Validation error for release build-identity evidence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReleaseBuildIdentityError { + /// Rust toolchain is not a canonical bounded token. + InvalidRustToolchain, + /// Dependency-lock digest is not a canonical lowercase SHA-256 digest. + InvalidDependencyLockDigest, +} + +impl fmt::Display for ReleaseBuildIdentityError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidRustToolchain => { + formatter.write_str("release Rust toolchain is not a canonical bounded token") + } + Self::InvalidDependencyLockDigest => formatter.write_str( + "release dependency lock digest must be sha256: followed by 64 lowercase hexadecimal digits", + ), + } + } +} + +impl Error for ReleaseBuildIdentityError {} + /// One canonical release artifact identity. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReleaseArtifact { @@ -96,22 +165,25 @@ pub struct ReleaseManifest { source_commit: String, chromium_revision: String, channel: ReleaseChannel, + build_identity: ReleaseBuildIdentity, artifacts: Vec, } impl ReleaseManifest { /// Construct an inert release manifest from exact identity evidence. /// - /// Source identity is a full 40-digit lowercase Git commit SHA. Chromium revision is a - /// bounded canonical ASCII token. Artifact names must be unique under ASCII case folding + /// Source identity is a full 40-digit lowercase Git commit SHA. Chromium revision and Rust + /// toolchain are bounded canonical ASCII tokens, while dependency-lock identity is a + /// canonical lowercase SHA-256 digest. Artifact names must be unique under ASCII case folding /// so one manifest cannot bind two names that collide on a case-insensitive target /// filesystem; original spelling is preserved and artifacts are sorted deterministically - /// before storage. Constructing this value does not authenticate any artifact and does not - /// authorize release or installation. + /// before storage. Constructing this value does not authenticate any artifact, prove + /// reproducibility, or authorize release or installation. pub fn new( source_commit: &str, chromium_revision: &str, channel: ReleaseChannel, + build_identity: ReleaseBuildIdentity, artifacts: I, ) -> Result where @@ -144,6 +216,7 @@ impl ReleaseManifest { source_commit: source_commit.to_owned(), chromium_revision: chromium_revision.to_owned(), channel, + build_identity, artifacts: admitted, }) } @@ -166,6 +239,12 @@ impl ReleaseManifest { self.channel } + /// Return the exact build identity bound by this manifest. + #[must_use] + pub const fn build_identity(&self) -> &ReleaseBuildIdentity { + &self.build_identity + } + /// Return artifacts sorted deterministically by canonical name. #[must_use] pub fn artifacts(&self) -> &[ReleaseArtifact] { @@ -276,3 +355,16 @@ fn valid_revision(revision: &str) -> bool { byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b'-' | b'+' | b':' | b'@') }) } + +fn valid_toolchain(toolchain: &str) -> bool { + if toolchain.is_empty() || toolchain.len() > MAX_RELEASE_TOOLCHAIN_BYTES || !toolchain.is_ascii() + { + return false; + } + let bytes = toolchain.as_bytes(); + bytes[0].is_ascii_alphanumeric() + && bytes[bytes.len() - 1].is_ascii_alphanumeric() + && bytes.iter().all(|byte| { + byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b'-' | b'+') + }) +} diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs index 8155af484..c6fe23076 100644 --- a/crates/originweave-core/tests/release_manifest.rs +++ b/crates/originweave-core/tests/release_manifest.rs @@ -2,7 +2,8 @@ use std::error::Error; use originweave_core::release_manifest::{ MAX_RELEASE_ARTIFACT_NAME_BYTES, MAX_RELEASE_ARTIFACTS, MAX_RELEASE_REVISION_BYTES, - ReleaseArtifact, ReleaseArtifactError, ReleaseChannel, ReleaseManifest, ReleaseManifestError, + ReleaseArtifact, ReleaseArtifactError, ReleaseBuildIdentity, ReleaseChannel, ReleaseManifest, + ReleaseManifestError, }; const SOURCE_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; @@ -12,6 +13,11 @@ fn sha256_digest(hex_digit: char) -> String { format!("sha256:{}", hex_digit.to_string().repeat(64)) } +fn build_identity() -> ReleaseBuildIdentity { + ReleaseBuildIdentity::new("1.97.1", &sha256_digest('9')) + .expect("release build identity fixture must be canonical") +} + #[test] fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result<(), Box> { @@ -22,12 +28,18 @@ fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result< SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, + build_identity(), vec![sbom, runtime], )?; assert_eq!(manifest.source_commit(), SOURCE_COMMIT); assert_eq!(manifest.chromium_revision(), CHROMIUM_REVISION); assert_eq!(manifest.channel(), ReleaseChannel::Stable); + assert_eq!(manifest.build_identity().rust_toolchain(), "1.97.1"); + assert_eq!( + manifest.build_identity().dependency_lock_sha256(), + sha256_digest('9') + ); assert_eq!(manifest.artifacts().len(), 2); assert_eq!( manifest.artifacts()[0].name(), @@ -42,6 +54,7 @@ fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result< SOURCE_COMMIT, "chromium_150-0+build:1@stable", ReleaseChannel::Development, + build_identity(), vec![punctuation], )?; assert_eq!( @@ -119,6 +132,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi invalid_commit, CHROMIUM_REVISION, ReleaseChannel::Development, + build_identity(), vec![artifact.clone()], ), Err(ReleaseManifestError::InvalidSourceCommit) @@ -140,6 +154,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, invalid_revision, ReleaseChannel::Beta, + build_identity(), vec![artifact.clone()], ), Err(ReleaseManifestError::InvalidChromiumRevision) @@ -151,6 +166,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, + build_identity(), Vec::new(), ), Err(ReleaseManifestError::MissingArtifacts) @@ -162,6 +178,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, + build_identity(), vec![artifact.clone(), duplicate], ), Err(ReleaseManifestError::DuplicateArtifactName) @@ -173,6 +190,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, + build_identity(), vec![artifact.clone(), case_collision], ), Err(ReleaseManifestError::DuplicateArtifactName) @@ -190,6 +208,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, + build_identity(), too_many, ), Err(ReleaseManifestError::TooManyArtifacts) From ec2323b167bf30c1fe56565af354c16a7bf6e56b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 14:34:21 -0700 Subject: [PATCH 022/170] style(core): canonicalize release build identity formatting --- crates/originweave-core/src/release_manifest.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 879739f41..5db6e15a5 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -357,14 +357,16 @@ fn valid_revision(revision: &str) -> bool { } fn valid_toolchain(toolchain: &str) -> bool { - if toolchain.is_empty() || toolchain.len() > MAX_RELEASE_TOOLCHAIN_BYTES || !toolchain.is_ascii() + if toolchain.is_empty() + || toolchain.len() > MAX_RELEASE_TOOLCHAIN_BYTES + || !toolchain.is_ascii() { return false; } let bytes = toolchain.as_bytes(); bytes[0].is_ascii_alphanumeric() && bytes[bytes.len() - 1].is_ascii_alphanumeric() - && bytes.iter().all(|byte| { - byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b'-' | b'+') - }) + && bytes + .iter() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b'-' | b'+')) } From a0d23b637c478f4b084f88c5690f0da44bad36dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 14:38:24 -0700 Subject: [PATCH 023/170] docs(adr): bind release build identity evidence --- docs/adr/0015-release-manifest-identity.md | 43 ++++++++++++---------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/docs/adr/0015-release-manifest-identity.md b/docs/adr/0015-release-manifest-identity.md index 093e372df..49440dd92 100644 --- a/docs/adr/0015-release-manifest-identity.md +++ b/docs/adr/0015-release-manifest-identity.md @@ -5,13 +5,14 @@ ## Context -Issue #201 requires commercial OriginWeave releases to bind an exact OriginWeave source identity, Chromium revision, release channel, and the artifacts buyers install or verify. That larger release lifecycle will later add signing, SBOM/SLSA provenance, updater trust, rollback, platform support, and operational acceptance. Those authorities do not yet exist in the current source tree. +Issue #201 requires commercial OriginWeave releases to bind an exact OriginWeave source identity, Chromium revision, release channel, build identity, and the artifacts buyers install or verify. That larger release lifecycle will later add signing, SBOM/SLSA provenance, updater trust, rollback, platform support, and operational acceptance. Those authorities do not yet exist in the current source tree. -A smaller durable boundary is nevertheless required before packaging work can safely compose: one deterministic, bounded manifest identity for a release candidate. Without an explicit contract, equivalent release inventories can be represented differently, case-insensitive target filesystems can collapse distinct names, and host-specific device namespaces can reinterpret an apparent artifact leaf name. +A smaller durable boundary is nevertheless required before packaging work can safely compose: one deterministic, bounded manifest identity for a release candidate. Without an explicit contract, equivalent release inventories can be represented differently, exact build inputs can be omitted or represented ambiguously, case-insensitive target filesystems can collapse distinct names, and host-specific device namespaces can reinterpret an apparent artifact leaf name. ## Decision drivers - One release-candidate identity must not depend on caller ordering. +- The manifest must not omit or ambiguously represent the exact Rust toolchain and dependency-lock identity used for the candidate. - Artifact references must remain leaf identities rather than filesystem paths. - The same manifest must be unambiguous on supported case-sensitive and case-insensitive platforms. - Manifest construction must remain inert metadata admission and must not grant signing, publication, installation, update, rollback, or release authority. @@ -22,6 +23,8 @@ A smaller durable boundary is nevertheless required before packaging work can sa - Source identity is a full 40-character lowercase Git commit SHA. - Chromium identity is a bounded canonical ASCII release token; this ADR does not claim that the token alone authenticates Chromium bytes. - A release channel is explicit (`Stable`, `Beta`, or `Development`) metadata, not authorization to publish or promote a release. +- Rust toolchain identity is a non-empty bounded canonical ASCII token; binding that token does not prove that a build actually used it. +- Dependency-lock identity is an exact lowercase `sha256:` digest; binding that digest does not authenticate the dependency source, build environment, or resulting artifact. - Every admitted artifact carries a bounded ASCII leaf name and an exact lowercase `sha256:` digest. - The manifest contains no private signing material, credentials, secrets, installer authority, network authority, or update authority. - Later release systems must independently authenticate the build, signer, provenance, platform package, updater metadata, and operational acceptance evidence. @@ -38,7 +41,7 @@ Rejected for this slice. Destructively rewriting admitted artifact spelling woul ### Canonical admission with preserved spelling and collision guards -Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, and separately reject names whose ASCII-case-folded identities collide or whose basenames are reserved Win32 device names. +Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, bind exact build-identity fields separately, and reject names whose ASCII-case-folded identities collide or whose basenames are reserved Win32 device names. ## Decision @@ -47,35 +50,37 @@ OriginWeave release-manifest admission is a deterministic, bounded, fail-closed 1. `source_commit` must be exactly 40 lowercase hexadecimal digits. 2. `chromium_revision` must be a non-empty bounded canonical ASCII token. 3. `channel` must be an explicit `ReleaseChannel` variant. -4. The artifact inventory must be non-empty and contain at most 64 entries. -5. Each artifact name must be a bounded ASCII leaf name containing only alphanumerics, `.`, `_`, and `-`; it cannot contain path separators, traversal-like `..`, leading punctuation, or trailing punctuation. -6. Artifact basenames `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9` are rejected case-insensitively, including when followed by an extension. The artifact grammar already rejects the non-ASCII superscript-digit Win32 aliases. -7. Artifact names must be unique under ASCII case folding while their original admitted spelling is retained. -8. Each artifact digest must be exactly `sha256:` followed by 64 lowercase hexadecimal digits. -9. Admitted artifacts are stored in deterministic name order. -10. Validation errors remain typed, deterministic standard Rust errors. - -Constructing or possessing a valid manifest does **not** authenticate an artifact, prove reproducibility, prove provenance, verify a signature, establish a signing identity, authorize a release channel, publish software, install software, update software, roll software back, or satisfy release acceptance. +4. `rust_toolchain` must be a non-empty bounded canonical ASCII token beginning and ending with an alphanumeric byte; internal bytes are limited to alphanumerics, `.`, `_`, `-`, and `+`. +5. `dependency_lock_sha256` must be exactly `sha256:` followed by 64 lowercase hexadecimal digits. +6. The artifact inventory must be non-empty and contain at most 64 entries. +7. Each artifact name must be a bounded ASCII leaf name containing only alphanumerics, `.`, `_`, and `-`; it cannot contain path separators, traversal-like `..`, leading punctuation, or trailing punctuation. +8. Artifact basenames `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9` are rejected case-insensitively, including when followed by an extension. The artifact grammar already rejects the non-ASCII superscript-digit Win32 aliases. +9. Artifact names must be unique under ASCII case folding while their original admitted spelling is retained. +10. Each artifact digest must be exactly `sha256:` followed by 64 lowercase hexadecimal digits. +11. Admitted artifacts are stored in deterministic name order. +12. Validation errors remain typed, deterministic standard Rust errors. + +Constructing or possessing a valid manifest does **not** authenticate an artifact, prove that the claimed toolchain or dependency lock was used, prove reproducibility, prove provenance, verify a signature, establish a signing identity, authorize a release channel, publish software, install software, update software, roll software back, or satisfy release acceptance. ## Consequences -The release candidate receives one bounded artifact-identity representation that is stable across caller order and avoids known case-insensitive and Win32 device-name collisions. Packaging, signing, provenance, and update layers can compose on top of this contract without inheriting ambient authority from it. +The release candidate receives one bounded build-and-artifact identity representation that is stable across caller order, records exact Rust toolchain and dependency-lock evidence, and avoids known case-insensitive and Win32 device-name collisions. Packaging, signing, provenance, and update layers can compose on top of this contract without inheriting ambient authority from it. -The contract is intentionally narrower than issue #201's final release manifest. Additional fields such as toolchain identity, adapter versions, build environment, dependency locks, signing identity, timestamp, SBOM/provenance references, and platform package identity remain future reviewed work rather than being inferred from this primitive. +The contract is intentionally narrower than issue #201's final release manifest. Additional fields such as adapter versions, build environment, signing identity, timestamp, SBOM/provenance references, and platform package identity remain future reviewed work rather than being inferred from this primitive. ## Failure and degraded behavior -Malformed, ambiguous, duplicate, unbounded, or empty identity evidence fails closed before a manifest is produced. There is no fallback that silently rewrites an invalid name, accepts an alternate digest representation, drops an artifact, or substitutes another release channel. +Malformed, ambiguous, duplicate, unbounded, or empty identity evidence fails closed before a manifest is produced. Invalid Rust toolchain or dependency-lock identity is rejected before a manifest can carry that build evidence. There is no fallback that silently rewrites an invalid name, substitutes another toolchain or dependency-lock digest, accepts an alternate digest representation, drops an artifact, or substitutes another release channel. A caller that cannot provide canonical evidence does not receive a release manifest. That failure is not converted into permission to sign, publish, install, or update through another path. ## Security / privacy / governance impact -The boundary reduces path-confusion and cross-platform filename ambiguity without introducing credentials or protected values. It does not alter GitHub governance, reviewer authority, release signing authority, or protected-main policy. Scheduled development agents remain unable to merge, tag, publish, sign, or change release authority. +The boundary reduces omitted-build-identity, path-confusion, and cross-platform filename ambiguity without introducing credentials or protected values. It does not alter GitHub governance, reviewer authority, release signing authority, or protected-main policy. Scheduled development agents remain unable to merge, tag, publish, sign, or change release authority. ## Tests and acceptance evidence -The owning `originweave-core` tests must cover valid deterministic ordering, exact digests, identifier bounds, malformed names, path/traversal-like names, case-only collisions, Win32 reserved device basenames with and without extensions, neighboring admissible names, exact inventory bounds, duplicate names, channel access, and deterministic standard error contracts. +The owning `originweave-core` tests must cover valid deterministic ordering, exact artifact and dependency-lock digests, valid and malformed Rust toolchain tokens, identifier bounds, malformed names, path/traversal-like names, case-only collisions, Win32 reserved device basenames with and without extensions, neighboring admissible names, exact inventory bounds, duplicate names, channel and build-identity access, and deterministic standard error contracts. Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. CI/security/scanner evidence is exact-head evidence only; predecessor or model-only evidence cannot satisfy acceptance. @@ -88,13 +93,13 @@ After acceptance and external release artifacts depend on this schema, any incom ## Open follow-ups - Complete issue #201's signed cross-platform distribution and updater trust architecture. -- Bind toolchain, adapter, build-environment, dependency-lock, SBOM/SLSA provenance, signing, timestamp, and platform-package evidence through separately reviewed contracts. +- Bind adapter, build-environment, SBOM/SLSA provenance, signing, timestamp, and platform-package evidence through separately reviewed contracts. - Define manifest serialization/versioning before any durable external release-manifest format is promised. - Add signing-key rotation, compromise response, update rollback/freeze protection, and platform release acceptance without transferring authority from this metadata type. ## Supersession / reversal conditions -Supersede this ADR when a versioned external release-manifest specification replaces the in-process identity primitive, or when supported-platform packaging requires a stronger canonical filename identity. Reversal must preserve fail-closed artifact identity and cannot make possession of metadata equivalent to release authority. +Supersede this ADR when a versioned external release-manifest specification replaces the in-process identity primitive, or when supported-platform packaging requires a stronger canonical filename identity. Reversal must preserve fail-closed build and artifact identity and cannot make possession of metadata equivalent to release authority. ## References From 7eddd3edd05436d1e6ba2d9d0ae0581ff2e086d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 14:39:22 -0700 Subject: [PATCH 024/170] docs(changelog): record release build identity binding --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b268093..e601713ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. +- Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, bounded canonical Rust toolchain, exact lowercase dependency-lock SHA-256 evidence, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; build identity remains metadata rather than reproducibility proof, and artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. From 965d995c19bde057a37fcc15b7076119d87096da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 14:42:27 -0700 Subject: [PATCH 025/170] test(core): satisfy strict release manifest Clippy contract --- .../tests/release_manifest.rs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs index c6fe23076..b2ab4144e 100644 --- a/crates/originweave-core/tests/release_manifest.rs +++ b/crates/originweave-core/tests/release_manifest.rs @@ -2,8 +2,8 @@ use std::error::Error; use originweave_core::release_manifest::{ MAX_RELEASE_ARTIFACT_NAME_BYTES, MAX_RELEASE_ARTIFACTS, MAX_RELEASE_REVISION_BYTES, - ReleaseArtifact, ReleaseArtifactError, ReleaseBuildIdentity, ReleaseChannel, ReleaseManifest, - ReleaseManifestError, + ReleaseArtifact, ReleaseArtifactError, ReleaseBuildIdentity, ReleaseBuildIdentityError, + ReleaseChannel, ReleaseManifest, ReleaseManifestError, }; const SOURCE_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; @@ -13,9 +13,8 @@ fn sha256_digest(hex_digit: char) -> String { format!("sha256:{}", hex_digit.to_string().repeat(64)) } -fn build_identity() -> ReleaseBuildIdentity { +fn build_identity() -> Result { ReleaseBuildIdentity::new("1.97.1", &sha256_digest('9')) - .expect("release build identity fixture must be canonical") } #[test] @@ -28,7 +27,7 @@ fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result< SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, - build_identity(), + build_identity()?, vec![sbom, runtime], )?; @@ -54,7 +53,7 @@ fn release_manifest_binds_and_canonicalizes_exact_artifact_evidence() -> Result< SOURCE_COMMIT, "chromium_150-0+build:1@stable", ReleaseChannel::Development, - build_identity(), + build_identity()?, vec![punctuation], )?; assert_eq!( @@ -132,7 +131,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi invalid_commit, CHROMIUM_REVISION, ReleaseChannel::Development, - build_identity(), + build_identity()?, vec![artifact.clone()], ), Err(ReleaseManifestError::InvalidSourceCommit) @@ -154,7 +153,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, invalid_revision, ReleaseChannel::Beta, - build_identity(), + build_identity()?, vec![artifact.clone()], ), Err(ReleaseManifestError::InvalidChromiumRevision) @@ -166,7 +165,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, - build_identity(), + build_identity()?, Vec::new(), ), Err(ReleaseManifestError::MissingArtifacts) @@ -178,7 +177,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, - build_identity(), + build_identity()?, vec![artifact.clone(), duplicate], ), Err(ReleaseManifestError::DuplicateArtifactName) @@ -190,7 +189,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, - build_identity(), + build_identity()?, vec![artifact.clone(), case_collision], ), Err(ReleaseManifestError::DuplicateArtifactName) @@ -208,7 +207,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi SOURCE_COMMIT, CHROMIUM_REVISION, ReleaseChannel::Stable, - build_identity(), + build_identity()?, too_many, ), Err(ReleaseManifestError::TooManyArtifacts) From 42ca2fd98750e7d0b09f98325ae939af8afdb27d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:29:20 -0700 Subject: [PATCH 026/170] test(release): reject unpinned Rust toolchains --- .../originweave-core/tests/release_manifest_build_identity.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/originweave-core/tests/release_manifest_build_identity.rs b/crates/originweave-core/tests/release_manifest_build_identity.rs index 86d485427..655de400d 100644 --- a/crates/originweave-core/tests/release_manifest_build_identity.rs +++ b/crates/originweave-core/tests/release_manifest_build_identity.rs @@ -25,6 +25,10 @@ fn release_build_identity_rejects_ambiguous_or_unbounded_evidence() { for invalid_toolchain in [ "", + "stable", + "beta", + "nightly", + "1.98.0", " 1.97.1", "1.97.1 ", "rust 1.97.1", From 923fe8ba7c0acf16e351df4c4f28ffc89af0cd62 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:32:53 -0700 Subject: [PATCH 027/170] fix(release): bind exact pinned Rust toolchain --- crates/originweave-core/src/release_manifest.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 5db6e15a5..cc571912a 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -357,16 +357,5 @@ fn valid_revision(revision: &str) -> bool { } fn valid_toolchain(toolchain: &str) -> bool { - if toolchain.is_empty() - || toolchain.len() > MAX_RELEASE_TOOLCHAIN_BYTES - || !toolchain.is_ascii() - { - return false; - } - let bytes = toolchain.as_bytes(); - bytes[0].is_ascii_alphanumeric() - && bytes[bytes.len() - 1].is_ascii_alphanumeric() - && bytes - .iter() - .all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b'-' | b'+')) + toolchain == "1.97.1" } From eff600d5b57fa6c5633fb90b2b4955bfd41eb01a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:33:31 -0700 Subject: [PATCH 028/170] docs(adr): bind release identity to pinned Rust baseline --- docs/adr/0015-release-manifest-identity.md | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/adr/0015-release-manifest-identity.md b/docs/adr/0015-release-manifest-identity.md index 49440dd92..d69c95e22 100644 --- a/docs/adr/0015-release-manifest-identity.md +++ b/docs/adr/0015-release-manifest-identity.md @@ -7,12 +7,13 @@ Issue #201 requires commercial OriginWeave releases to bind an exact OriginWeave source identity, Chromium revision, release channel, build identity, and the artifacts buyers install or verify. That larger release lifecycle will later add signing, SBOM/SLSA provenance, updater trust, rollback, platform support, and operational acceptance. Those authorities do not yet exist in the current source tree. -A smaller durable boundary is nevertheless required before packaging work can safely compose: one deterministic, bounded manifest identity for a release candidate. Without an explicit contract, equivalent release inventories can be represented differently, exact build inputs can be omitted or represented ambiguously, case-insensitive target filesystems can collapse distinct names, and host-specific device namespaces can reinterpret an apparent artifact leaf name. +A smaller durable boundary is nevertheless required before packaging work can safely compose: one deterministic, bounded manifest identity for a release candidate. Without an explicit contract, equivalent release inventories can be represented differently, exact build inputs can be omitted or represented ambiguously, moving toolchain aliases can resolve to different compiler identities over time, case-insensitive target filesystems can collapse distinct names, and host-specific device namespaces can reinterpret an apparent artifact leaf name. ## Decision drivers - One release-candidate identity must not depend on caller ordering. - The manifest must not omit or ambiguously represent the exact Rust toolchain and dependency-lock identity used for the candidate. +- The Rust toolchain field must not accept moving aliases or an alternate compiler version as though it were the repository-pinned build identity. - Artifact references must remain leaf identities rather than filesystem paths. - The same manifest must be unambiguous on supported case-sensitive and case-insensitive platforms. - Manifest construction must remain inert metadata admission and must not grant signing, publication, installation, update, rollback, or release authority. @@ -23,7 +24,7 @@ A smaller durable boundary is nevertheless required before packaging work can sa - Source identity is a full 40-character lowercase Git commit SHA. - Chromium identity is a bounded canonical ASCII release token; this ADR does not claim that the token alone authenticates Chromium bytes. - A release channel is explicit (`Stable`, `Beta`, or `Development`) metadata, not authorization to publish or promote a release. -- Rust toolchain identity is a non-empty bounded canonical ASCII token; binding that token does not prove that a build actually used it. +- Rust toolchain identity is the exact protected-repository baseline `1.97.1`; moving aliases such as `stable`, `beta`, and `nightly`, and alternate versions, are not admissible release build identities. Binding the exact token does not prove that a build actually used it. - Dependency-lock identity is an exact lowercase `sha256:` digest; binding that digest does not authenticate the dependency source, build environment, or resulting artifact. - Every admitted artifact carries a bounded ASCII leaf name and an exact lowercase `sha256:` digest. - The manifest contains no private signing material, credentials, secrets, installer authority, network authority, or update authority. @@ -41,7 +42,7 @@ Rejected for this slice. Destructively rewriting admitted artifact spelling woul ### Canonical admission with preserved spelling and collision guards -Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, bind exact build-identity fields separately, and reject names whose ASCII-case-folded identities collide or whose basenames are reserved Win32 device names. +Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, bind exact build-identity fields separately, require the repository-pinned Rust compiler identity, and reject names whose ASCII-case-folded identities collide or whose basenames are reserved Win32 device names. ## Decision @@ -50,7 +51,7 @@ OriginWeave release-manifest admission is a deterministic, bounded, fail-closed 1. `source_commit` must be exactly 40 lowercase hexadecimal digits. 2. `chromium_revision` must be a non-empty bounded canonical ASCII token. 3. `channel` must be an explicit `ReleaseChannel` variant. -4. `rust_toolchain` must be a non-empty bounded canonical ASCII token beginning and ending with an alphanumeric byte; internal bytes are limited to alphanumerics, `.`, `_`, `-`, and `+`. +4. `rust_toolchain` must equal the protected repository's exact pinned Rust toolchain `1.97.1`. Moving aliases (`stable`, `beta`, `nightly`) and alternate versions fail closed until the protected baseline and this binding decision are deliberately changed together. 5. `dependency_lock_sha256` must be exactly `sha256:` followed by 64 lowercase hexadecimal digits. 6. The artifact inventory must be non-empty and contain at most 64 entries. 7. Each artifact name must be a bounded ASCII leaf name containing only alphanumerics, `.`, `_`, and `-`; it cannot contain path separators, traversal-like `..`, leading punctuation, or trailing punctuation. @@ -64,23 +65,25 @@ Constructing or possessing a valid manifest does **not** authenticate an artifac ## Consequences -The release candidate receives one bounded build-and-artifact identity representation that is stable across caller order, records exact Rust toolchain and dependency-lock evidence, and avoids known case-insensitive and Win32 device-name collisions. Packaging, signing, provenance, and update layers can compose on top of this contract without inheriting ambient authority from it. +The release candidate receives one bounded build-and-artifact identity representation that is stable across caller order, records the exact repository-pinned Rust toolchain and dependency-lock evidence, and avoids known case-insensitive and Win32 device-name collisions. Packaging, signing, provenance, and update layers can compose on top of this contract without inheriting ambient authority from it. + +Changing the protected Rust baseline now requires deliberate convergence of the repository toolchain pin, release-manifest admission contract, tests, and this ADR. A moving channel alias cannot silently change the compiler identity represented by a release manifest. The contract is intentionally narrower than issue #201's final release manifest. Additional fields such as adapter versions, build environment, signing identity, timestamp, SBOM/provenance references, and platform package identity remain future reviewed work rather than being inferred from this primitive. ## Failure and degraded behavior -Malformed, ambiguous, duplicate, unbounded, or empty identity evidence fails closed before a manifest is produced. Invalid Rust toolchain or dependency-lock identity is rejected before a manifest can carry that build evidence. There is no fallback that silently rewrites an invalid name, substitutes another toolchain or dependency-lock digest, accepts an alternate digest representation, drops an artifact, or substitutes another release channel. +Malformed, ambiguous, duplicate, unbounded, empty, moving-alias, or alternate-version identity evidence fails closed before a manifest is produced. A Rust toolchain other than the exact protected baseline, or an invalid dependency-lock identity, is rejected before a manifest can carry that build evidence. There is no fallback that silently rewrites an invalid name, substitutes another toolchain or dependency-lock digest, accepts an alternate digest representation, drops an artifact, or substitutes another release channel. A caller that cannot provide canonical evidence does not receive a release manifest. That failure is not converted into permission to sign, publish, install, or update through another path. ## Security / privacy / governance impact -The boundary reduces omitted-build-identity, path-confusion, and cross-platform filename ambiguity without introducing credentials or protected values. It does not alter GitHub governance, reviewer authority, release signing authority, or protected-main policy. Scheduled development agents remain unable to merge, tag, publish, sign, or change release authority. +The boundary reduces omitted-build-identity, moving-toolchain ambiguity, path-confusion, and cross-platform filename ambiguity without introducing credentials or protected values. It does not alter GitHub governance, reviewer authority, release signing authority, or protected-main policy. Scheduled development agents remain unable to merge, tag, publish, sign, or change release authority. ## Tests and acceptance evidence -The owning `originweave-core` tests must cover valid deterministic ordering, exact artifact and dependency-lock digests, valid and malformed Rust toolchain tokens, identifier bounds, malformed names, path/traversal-like names, case-only collisions, Win32 reserved device basenames with and without extensions, neighboring admissible names, exact inventory bounds, duplicate names, channel and build-identity access, and deterministic standard error contracts. +The owning `originweave-core` tests must cover valid deterministic ordering, exact artifact and dependency-lock digests, the exact pinned Rust toolchain, moving aliases and alternate toolchain versions, malformed toolchain inputs, identifier bounds, malformed names, path/traversal-like names, case-only collisions, Win32 reserved device basenames with and without extensions, neighboring admissible names, exact inventory bounds, duplicate names, channel and build-identity access, and deterministic standard error contracts. Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. CI/security/scanner evidence is exact-head evidence only; predecessor or model-only evidence cannot satisfy acceptance. @@ -88,7 +91,7 @@ Owned production function, line, region, and branch coverage remains exactly 100 This is a new admission primitive with no protected-main persisted manifest migration. If the contract proves incompatible before acceptance, the Proposed ADR and owning feature branch can be revised or withdrawn without granting legacy inputs grandfathered authority. -After acceptance and external release artifacts depend on this schema, any incompatible identity change requires an explicit versioning/migration decision rather than silent parser broadening. +After acceptance and external release artifacts depend on this schema, any incompatible identity change requires an explicit versioning/migration decision rather than silent parser broadening. A future Rust upgrade must change the repository baseline and release-manifest contract together rather than relying on a moving alias. ## Open follow-ups @@ -99,7 +102,7 @@ After acceptance and external release artifacts depend on this schema, any incom ## Supersession / reversal conditions -Supersede this ADR when a versioned external release-manifest specification replaces the in-process identity primitive, or when supported-platform packaging requires a stronger canonical filename identity. Reversal must preserve fail-closed build and artifact identity and cannot make possession of metadata equivalent to release authority. +Supersede this ADR when a versioned external release-manifest specification replaces the in-process identity primitive, when the protected Rust baseline changes under a reviewed migration, or when supported-platform packaging requires a stronger canonical filename identity. Reversal must preserve fail-closed build and artifact identity and cannot make possession of metadata equivalent to release authority. ## References From 547b53ac9aa22d25c209c4b3245b0699dcc46a6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:34:46 -0700 Subject: [PATCH 029/170] docs(changelog): record exact release toolchain binding --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e601713ef..6dcf02b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, bounded canonical Rust toolchain, exact lowercase dependency-lock SHA-256 evidence, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; build identity remains metadata rather than reproducibility proof, and artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. +- Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, the exact repository-pinned Rust 1.97.1 toolchain, exact lowercase dependency-lock SHA-256 evidence, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; build identity remains metadata rather than reproducibility proof, moving toolchain aliases and alternate versions fail closed, and artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. From fc8e51d6b0b182601c8501b86ffac528db193d23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:35:04 -0700 Subject: [PATCH 030/170] test(release): require exact toolchain error contract --- .../originweave-core/tests/release_manifest_build_identity.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/release_manifest_build_identity.rs b/crates/originweave-core/tests/release_manifest_build_identity.rs index 655de400d..ac890bdb9 100644 --- a/crates/originweave-core/tests/release_manifest_build_identity.rs +++ b/crates/originweave-core/tests/release_manifest_build_identity.rs @@ -61,7 +61,7 @@ fn release_build_identity_errors_are_standard_source_free_rust_errors() { let errors = [ ( ReleaseBuildIdentityError::InvalidRustToolchain, - "release Rust toolchain is not a canonical bounded token", + "release Rust toolchain must match the exact repository-pinned baseline", ), ( ReleaseBuildIdentityError::InvalidDependencyLockDigest, From 7e5ccf1b56cdb9e4b090ef329b0679974203eb59 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:36:39 -0700 Subject: [PATCH 031/170] test(release): decouple exact pin from obsolete token bound --- .../tests/release_manifest_build_identity.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/tests/release_manifest_build_identity.rs b/crates/originweave-core/tests/release_manifest_build_identity.rs index ac890bdb9..fd9a2852e 100644 --- a/crates/originweave-core/tests/release_manifest_build_identity.rs +++ b/crates/originweave-core/tests/release_manifest_build_identity.rs @@ -1,8 +1,6 @@ use std::error::Error; -use originweave_core::release_manifest::{ - MAX_RELEASE_TOOLCHAIN_BYTES, ReleaseBuildIdentity, ReleaseBuildIdentityError, -}; +use originweave_core::release_manifest::{ReleaseBuildIdentity, ReleaseBuildIdentityError}; fn sha256_digest(hex_digit: char) -> String { format!("sha256:{}", hex_digit.to_string().repeat(64)) @@ -21,7 +19,7 @@ fn release_build_identity_binds_exact_toolchain_and_dependency_lock() -> Result< #[test] fn release_build_identity_rejects_ambiguous_or_unbounded_evidence() { let digest = sha256_digest('b'); - let overlong_toolchain = "a".repeat(MAX_RELEASE_TOOLCHAIN_BYTES + 1); + let overlong_toolchain = "a".repeat(65); for invalid_toolchain in [ "", From a957ff610967e9b3bb01b7c54fa6d84fae353982 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:37:07 -0700 Subject: [PATCH 032/170] fix(release): make pinned toolchain contract explicit --- .../originweave-core/src/release_manifest.rs | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index cc571912a..43bacfcf4 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -14,8 +14,6 @@ pub const MAX_RELEASE_ARTIFACTS: usize = 64; pub const MAX_RELEASE_ARTIFACT_NAME_BYTES: usize = 128; /// Maximum UTF-8 byte length admitted for one Chromium revision token. pub const MAX_RELEASE_REVISION_BYTES: usize = 128; -/// Maximum UTF-8 byte length admitted for one canonical Rust toolchain token. -pub const MAX_RELEASE_TOOLCHAIN_BYTES: usize = 64; /// Buyer-visible release channel bound by a release manifest. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -36,12 +34,13 @@ pub struct ReleaseBuildIdentity { } impl ReleaseBuildIdentity { - /// Construct bounded build identity from a canonical Rust toolchain token and lock digest. + /// Construct build identity from the exact repository-pinned Rust toolchain and lock digest. /// - /// The dependency-lock digest must use the exact `sha256:` prefix followed by 64 lowercase - /// hexadecimal digits. Constructing this value does not prove reproducibility or authenticate - /// the build environment; it only prevents those two identity fields from being omitted or - /// represented ambiguously in a release manifest. + /// The Rust toolchain must match the protected repository baseline exactly; moving aliases + /// and alternate versions fail closed. The dependency-lock digest must use the exact + /// `sha256:` prefix followed by 64 lowercase hexadecimal digits. Constructing this value does + /// not prove reproducibility or authenticate the build environment; it only prevents those + /// two identity fields from being omitted or represented ambiguously in a release manifest. pub fn new( rust_toolchain: &str, dependency_lock_sha256: &str, @@ -58,7 +57,7 @@ impl ReleaseBuildIdentity { }) } - /// Return the exact canonical Rust toolchain token. + /// Return the exact repository-pinned Rust toolchain token. #[must_use] pub fn rust_toolchain(&self) -> &str { &self.rust_toolchain @@ -74,7 +73,7 @@ impl ReleaseBuildIdentity { /// Validation error for release build-identity evidence. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ReleaseBuildIdentityError { - /// Rust toolchain is not a canonical bounded token. + /// Rust toolchain does not match the exact repository-pinned baseline. InvalidRustToolchain, /// Dependency-lock digest is not a canonical lowercase SHA-256 digest. InvalidDependencyLockDigest, @@ -83,9 +82,8 @@ pub enum ReleaseBuildIdentityError { impl fmt::Display for ReleaseBuildIdentityError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::InvalidRustToolchain => { - formatter.write_str("release Rust toolchain is not a canonical bounded token") - } + Self::InvalidRustToolchain => formatter + .write_str("release Rust toolchain must match the exact repository-pinned baseline"), Self::InvalidDependencyLockDigest => formatter.write_str( "release dependency lock digest must be sha256: followed by 64 lowercase hexadecimal digits", ), @@ -172,13 +170,13 @@ pub struct ReleaseManifest { impl ReleaseManifest { /// Construct an inert release manifest from exact identity evidence. /// - /// Source identity is a full 40-digit lowercase Git commit SHA. Chromium revision and Rust - /// toolchain are bounded canonical ASCII tokens, while dependency-lock identity is a - /// canonical lowercase SHA-256 digest. Artifact names must be unique under ASCII case folding - /// so one manifest cannot bind two names that collide on a case-insensitive target - /// filesystem; original spelling is preserved and artifacts are sorted deterministically - /// before storage. Constructing this value does not authenticate any artifact, prove - /// reproducibility, or authorize release or installation. + /// Source identity is a full 40-digit lowercase Git commit SHA. Chromium revision is a + /// bounded canonical ASCII token, Rust toolchain identity is the exact repository pin, and + /// dependency-lock identity is a canonical lowercase SHA-256 digest. Artifact names must be + /// unique under ASCII case folding so one manifest cannot bind two names that collide on a + /// case-insensitive target filesystem; original spelling is preserved and artifacts are + /// sorted deterministically before storage. Constructing this value does not authenticate any + /// artifact, prove reproducibility, or authorize release or installation. pub fn new( source_commit: &str, chromium_revision: &str, From c875358316ac78fa1075f2c2da947f3d21e68faf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:59:25 -0700 Subject: [PATCH 033/170] test(release): reject null Git source identity --- crates/originweave-core/tests/release_manifest.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs index b2ab4144e..a57e7c5fa 100644 --- a/crates/originweave-core/tests/release_manifest.rs +++ b/crates/originweave-core/tests/release_manifest.rs @@ -122,6 +122,7 @@ fn release_manifest_rejects_invalid_identity_missing_duplicate_and_unbounded_evi for invalid_commit in [ "", + "0000000000000000000000000000000000000000", "0123456789abcdef0123456789abcdef0123456", "0123456789ABCDEF0123456789ABCDEF01234567", "g123456789abcdef0123456789abcdef01234567", From cf17966ea48f63d87b23373f0f65846ffc92957c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 16:02:52 -0700 Subject: [PATCH 034/170] fix(release): reject null Git source identity --- crates/originweave-core/src/release_manifest.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 43bacfcf4..a393486be 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -340,6 +340,7 @@ fn valid_source_commit(source_commit: &str) -> bool { && source_commit .bytes() .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + && source_commit.bytes().any(|byte| byte != b'0') } fn valid_revision(revision: &str) -> bool { From 7d7dd947bd98fbf8402fde83b26a7f99b235c23f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 16:05:14 -0700 Subject: [PATCH 035/170] docs(release): reject Git zero-id as source identity --- docs/adr/0015-release-manifest-identity.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/adr/0015-release-manifest-identity.md b/docs/adr/0015-release-manifest-identity.md index d69c95e22..6a3541d0a 100644 --- a/docs/adr/0015-release-manifest-identity.md +++ b/docs/adr/0015-release-manifest-identity.md @@ -9,11 +9,14 @@ Issue #201 requires commercial OriginWeave releases to bind an exact OriginWeave A smaller durable boundary is nevertheless required before packaging work can safely compose: one deterministic, bounded manifest identity for a release candidate. Without an explicit contract, equivalent release inventories can be represented differently, exact build inputs can be omitted or represented ambiguously, moving toolchain aliases can resolve to different compiler identities over time, case-insensitive target filesystems can collapse distinct names, and host-specific device namespaces can reinterpret an apparent artifact leaf name. +Git protocol grammar distinguishes a 40-zero `zero-id` from ordinary object identifiers, and push protocol uses that zero-id as the sentinel for absent/create/delete reference state. A release manifest that admitted the all-zero value as a concrete source commit could therefore claim an exact source identity that does not identify a real Git object. OriginWeave rejects that sentinel at admission rather than relying on later publication or provenance layers to reinterpret it. + ## Decision drivers - One release-candidate identity must not depend on caller ordering. - The manifest must not omit or ambiguously represent the exact Rust toolchain and dependency-lock identity used for the candidate. - The Rust toolchain field must not accept moving aliases or an alternate compiler version as though it were the repository-pinned build identity. +- Source commit identity must distinguish a concrete Git object identifier from Git's all-zero protocol sentinel. - Artifact references must remain leaf identities rather than filesystem paths. - The same manifest must be unambiguous on supported case-sensitive and case-insensitive platforms. - Manifest construction must remain inert metadata admission and must not grant signing, publication, installation, update, rollback, or release authority. @@ -21,7 +24,7 @@ A smaller durable boundary is nevertheless required before packaging work can sa ## Assumptions and authority boundaries -- Source identity is a full 40-character lowercase Git commit SHA. +- Source identity is a full 40-character lowercase Git commit SHA and must not be Git's all-zero `zero-id` sentinel. - Chromium identity is a bounded canonical ASCII release token; this ADR does not claim that the token alone authenticates Chromium bytes. - A release channel is explicit (`Stable`, `Beta`, or `Development`) metadata, not authorization to publish or promote a release. - Rust toolchain identity is the exact protected-repository baseline `1.97.1`; moving aliases such as `stable`, `beta`, and `nightly`, and alternate versions, are not admissible release build identities. Binding the exact token does not prove that a build actually used it. @@ -42,13 +45,13 @@ Rejected for this slice. Destructively rewriting admitted artifact spelling woul ### Canonical admission with preserved spelling and collision guards -Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, bind exact build-identity fields separately, require the repository-pinned Rust compiler identity, and reject names whose ASCII-case-folded identities collide or whose basenames are reserved Win32 device names. +Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, bind exact build-identity fields separately, require the repository-pinned Rust compiler identity, reject Git's all-zero source-identity sentinel, and reject names whose ASCII-case-folded identities collide or whose basenames are reserved Win32 device names. ## Decision OriginWeave release-manifest admission is a deterministic, bounded, fail-closed identity contract: -1. `source_commit` must be exactly 40 lowercase hexadecimal digits. +1. `source_commit` must be exactly 40 lowercase hexadecimal digits and must not be the all-zero Git `zero-id` sentinel. 2. `chromium_revision` must be a non-empty bounded canonical ASCII token. 3. `channel` must be an explicit `ReleaseChannel` variant. 4. `rust_toolchain` must equal the protected repository's exact pinned Rust toolchain `1.97.1`. Moving aliases (`stable`, `beta`, `nightly`) and alternate versions fail closed until the protected baseline and this binding decision are deliberately changed together. @@ -65,7 +68,7 @@ Constructing or possessing a valid manifest does **not** authenticate an artifac ## Consequences -The release candidate receives one bounded build-and-artifact identity representation that is stable across caller order, records the exact repository-pinned Rust toolchain and dependency-lock evidence, and avoids known case-insensitive and Win32 device-name collisions. Packaging, signing, provenance, and update layers can compose on top of this contract without inheriting ambient authority from it. +The release candidate receives one bounded build-and-artifact identity representation that is stable across caller order, records a concrete non-null Git source identity plus the exact repository-pinned Rust toolchain and dependency-lock evidence, and avoids known case-insensitive and Win32 device-name collisions. Packaging, signing, provenance, and update layers can compose on top of this contract without inheriting ambient authority from it. Changing the protected Rust baseline now requires deliberate convergence of the repository toolchain pin, release-manifest admission contract, tests, and this ADR. A moving channel alias cannot silently change the compiler identity represented by a release manifest. @@ -73,17 +76,17 @@ The contract is intentionally narrower than issue #201's final release manifest. ## Failure and degraded behavior -Malformed, ambiguous, duplicate, unbounded, empty, moving-alias, or alternate-version identity evidence fails closed before a manifest is produced. A Rust toolchain other than the exact protected baseline, or an invalid dependency-lock identity, is rejected before a manifest can carry that build evidence. There is no fallback that silently rewrites an invalid name, substitutes another toolchain or dependency-lock digest, accepts an alternate digest representation, drops an artifact, or substitutes another release channel. +Malformed, ambiguous, duplicate, unbounded, empty, null-sentinel, moving-alias, or alternate-version identity evidence fails closed before a manifest is produced. A source identity equal to Git's 40-zero `zero-id`, a Rust toolchain other than the exact protected baseline, or an invalid dependency-lock identity is rejected before a manifest can carry that evidence. There is no fallback that silently rewrites an invalid name, substitutes another source commit, toolchain, or dependency-lock digest, accepts an alternate digest representation, drops an artifact, or substitutes another release channel. A caller that cannot provide canonical evidence does not receive a release manifest. That failure is not converted into permission to sign, publish, install, or update through another path. ## Security / privacy / governance impact -The boundary reduces omitted-build-identity, moving-toolchain ambiguity, path-confusion, and cross-platform filename ambiguity without introducing credentials or protected values. It does not alter GitHub governance, reviewer authority, release signing authority, or protected-main policy. Scheduled development agents remain unable to merge, tag, publish, sign, or change release authority. +The boundary reduces null-source-identity, omitted-build-identity, moving-toolchain ambiguity, path-confusion, and cross-platform filename ambiguity without introducing credentials or protected values. It does not alter GitHub governance, reviewer authority, release signing authority, or protected-main policy. Scheduled development agents remain unable to merge, tag, publish, sign, or change release authority. ## Tests and acceptance evidence -The owning `originweave-core` tests must cover valid deterministic ordering, exact artifact and dependency-lock digests, the exact pinned Rust toolchain, moving aliases and alternate toolchain versions, malformed toolchain inputs, identifier bounds, malformed names, path/traversal-like names, case-only collisions, Win32 reserved device basenames with and without extensions, neighboring admissible names, exact inventory bounds, duplicate names, channel and build-identity access, and deterministic standard error contracts. +The owning `originweave-core` tests must cover valid deterministic ordering, exact artifact and dependency-lock digests, one concrete non-null 40-character source commit, rejection of Git's all-zero source sentinel, the exact pinned Rust toolchain, moving aliases and alternate toolchain versions, malformed toolchain inputs, identifier bounds, malformed names, path/traversal-like names, case-only collisions, Win32 reserved device basenames with and without extensions, neighboring admissible names, exact inventory bounds, duplicate names, channel and build-identity access, and deterministic standard error contracts. Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. CI/security/scanner evidence is exact-head evidence only; predecessor or model-only evidence cannot satisfy acceptance. @@ -106,4 +109,8 @@ Supersede this ADR when a versioned external release-manifest specification repl ## References +Git. (2025). *gitprotocol-common documentation*. https://git-scm.com/docs/gitprotocol-common + +Git. (2026). *gitprotocol-pack documentation*. https://git-scm.com/docs/gitprotocol-pack + Microsoft. (n.d.). *Naming files, paths, and namespaces*. Microsoft Learn. Retrieved August 23, 2026, from https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file From bf29cb4de5983319661ab72eef78821d2397628b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 16:10:54 -0700 Subject: [PATCH 036/170] docs(release): record non-null Git identity rationale --- docs/doctoring.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/doctoring.md b/docs/doctoring.md index 483adc407..88e353103 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -78,6 +78,10 @@ The test-only rcgen 0.14.8 dependency creates a local CA and deterministic certi Microsoft's Win32 filename guidance requires applications not to assume case sensitivity and reserves device basenames including `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9`, including those names followed by extensions. The same documentation also identifies non-ASCII superscript-digit device aliases. OriginWeave's release-manifest identity therefore rejects ASCII-case-folded duplicate artifact names and those reserved ASCII device basenames; its ASCII-only artifact grammar separately excludes the superscript aliases. These checks make one admitted artifact inventory portable across case-sensitive and default case-insensitive target filesystems without rewriting the stored artifact spelling. They remain identity hygiene only and do not grant signing, publication, installation, update, rollback, or release authority. +### Git source identity admission + +Git's protocol grammar distinguishes an ordinary 40-hex object identifier from `zero-id = 40*"0"`. The pack protocol uses that all-zero sentinel to represent absent refs and create/delete reference state rather than a concrete source object. OriginWeave therefore requires release-manifest `source_commit` and WARC PROV `software_commit_sha` values to be canonical lowercase 40-hex identifiers with at least one nonzero digit. This is an identity-admission rule only: it prevents a null Git sentinel from being represented as a concrete software revision, but does not prove repository reachability, GitHub authenticity, commit trust, provenance, or release authority. + ### Crawling policy RFC 9309 standardizes robots parsing, matching, error handling, and caching. It also states that robots rules are not access authorization. OriginWeave therefore requires robots evidence for public crawler mode while maintaining authentication, terms, rate, privacy, and retention policy as separate controls. @@ -130,6 +134,10 @@ Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). Fugu Team, Sakana AI. (2026). *Sakana Fugu technical report* [Technical report]. arXiv. https://doi.org/10.48550/arXiv.2606.21228 +Git. (2025). *gitprotocol-common documentation*. https://git-scm.com/docs/gitprotocol-common + +Git. (2026). *gitprotocol-pack documentation*. https://git-scm.com/docs/gitprotocol-pack + Huston, G., & Buraglio, N. (2024). *Expanding the IPv6 documentation space* (RFC 9637). Internet Engineering Task Force. https://doi.org/10.17487/RFC9637 Internet Assigned Numbers Authority. (2025, October 9). *IPv4 special-purpose address space*. https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml From ac9f4ff80fb8615b0002c770750194397936adb0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 18:05:33 -0700 Subject: [PATCH 037/170] test(core): pin non-null release source error contract --- crates/originweave-core/tests/release_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs index a57e7c5fa..942fae1fe 100644 --- a/crates/originweave-core/tests/release_manifest.rs +++ b/crates/originweave-core/tests/release_manifest.rs @@ -236,7 +236,7 @@ fn release_manifest_errors_are_standard_source_free_rust_errors() { let manifest_errors = [ ( ReleaseManifestError::InvalidSourceCommit, - "release source commit must be exactly 40 lowercase hexadecimal digits", + "release source commit must be a non-null 40-digit lowercase Git object identity", ), ( ReleaseManifestError::InvalidChromiumRevision, From a7f995e5729f3c5c69755cb083070fe99f7e45b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 18:08:11 -0700 Subject: [PATCH 038/170] fix(core): align release source diagnostic with non-null identity --- crates/originweave-core/src/release_manifest.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index a393486be..211632ac7 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -253,7 +253,7 @@ impl ReleaseManifest { /// Validation error for release-manifest identity evidence. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ReleaseManifestError { - /// Source commit is not a full lowercase Git SHA-1 identity. + /// Source commit is not a non-null full lowercase 40-hex Git object identity. InvalidSourceCommit, /// Chromium revision is not a canonical bounded release token. InvalidChromiumRevision, @@ -268,8 +268,9 @@ pub enum ReleaseManifestError { impl fmt::Display for ReleaseManifestError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::InvalidSourceCommit => formatter - .write_str("release source commit must be exactly 40 lowercase hexadecimal digits"), + Self::InvalidSourceCommit => formatter.write_str( + "release source commit must be a non-null 40-digit lowercase Git object identity", + ), Self::InvalidChromiumRevision => { formatter.write_str("Chromium revision must be a canonical bounded release token") } From 070798d4d5ca2da254f0faf282de895462a2eaea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 19:43:22 -0700 Subject: [PATCH 039/170] docs(release): pin Git protocol references --- docs/adr/0015-release-manifest-identity.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/0015-release-manifest-identity.md b/docs/adr/0015-release-manifest-identity.md index 6a3541d0a..15d1d611a 100644 --- a/docs/adr/0015-release-manifest-identity.md +++ b/docs/adr/0015-release-manifest-identity.md @@ -109,8 +109,8 @@ Supersede this ADR when a versioned external release-manifest specification repl ## References -Git. (2025). *gitprotocol-common documentation*. https://git-scm.com/docs/gitprotocol-common +Git. (2026). *gitprotocol-common documentation (Git 2.55.0)*. https://git-scm.com/docs/gitprotocol-common/2.55.0 -Git. (2026). *gitprotocol-pack documentation*. https://git-scm.com/docs/gitprotocol-pack +Git. (2026). *gitprotocol-pack documentation (Git 2.55.0)*. https://git-scm.com/docs/gitprotocol-pack/2.55.0 Microsoft. (n.d.). *Naming files, paths, and namespaces*. Microsoft Learn. Retrieved August 23, 2026, from https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file From f7c3145d751a742d9115aa80e069e03dd7ebcb91 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 19:44:38 -0700 Subject: [PATCH 040/170] docs(release): pin doctoring Git references --- docs/doctoring.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 88e353103..76f1d43e9 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -134,9 +134,9 @@ Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). Fugu Team, Sakana AI. (2026). *Sakana Fugu technical report* [Technical report]. arXiv. https://doi.org/10.48550/arXiv.2606.21228 -Git. (2025). *gitprotocol-common documentation*. https://git-scm.com/docs/gitprotocol-common +Git. (2026). *gitprotocol-common documentation (Git 2.55.0)*. https://git-scm.com/docs/gitprotocol-common/2.55.0 -Git. (2026). *gitprotocol-pack documentation*. https://git-scm.com/docs/gitprotocol-pack +Git. (2026). *gitprotocol-pack documentation (Git 2.55.0)*. https://git-scm.com/docs/gitprotocol-pack/2.55.0 Huston, G., & Buraglio, N. (2024). *Expanding the IPv6 documentation space* (RFC 9637). Internet Engineering Task Force. https://doi.org/10.17487/RFC9637 From 37d846f6788524a8298b22962bd073565052f61e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 20:35:23 -0700 Subject: [PATCH 041/170] test(release): reject Win32 COM0 and LPT0 aliases --- crates/originweave-core/tests/release_manifest.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/originweave-core/tests/release_manifest.rs b/crates/originweave-core/tests/release_manifest.rs index 942fae1fe..531b13685 100644 --- a/crates/originweave-core/tests/release_manifest.rs +++ b/crates/originweave-core/tests/release_manifest.rs @@ -81,8 +81,12 @@ fn release_artifact_rejects_ambiguous_names_and_noncanonical_digests() { "PRN.tar.zst", "aux.bin", "NUL.spdx.json", + "COM0", + "com0.zip", "COM1", "com9.zip", + "LPT0", + "lpt0.tar.zst", "LPT1", "lpt9.tar.zst", overlong_name.as_str(), From 9ced1c22ff3a3c5799038221c338355ed650f614 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 20:37:01 -0700 Subject: [PATCH 042/170] fix(release): reject Win32 COM0 and LPT0 device aliases --- crates/originweave-core/src/release_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 211632ac7..14dd8023b 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -323,7 +323,7 @@ fn windows_reserved_device_basename(name: &str) -> bool { let bytes = basename.as_bytes(); bytes.len() == 4 && (basename[..3].eq_ignore_ascii_case("COM") || basename[..3].eq_ignore_ascii_case("LPT")) - && matches!(bytes[3], b'1'..=b'9') + && matches!(bytes[3], b'0'..=b'9') } fn valid_sha256_digest(digest: &str) -> bool { From 73d3453304135d033528c27956bf2d0bf8cf55e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 20:38:06 -0700 Subject: [PATCH 043/170] docs(adr): record portable COM0 and LPT0 exclusion --- docs/adr/0015-release-manifest-identity.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/adr/0015-release-manifest-identity.md b/docs/adr/0015-release-manifest-identity.md index 15d1d611a..92679d625 100644 --- a/docs/adr/0015-release-manifest-identity.md +++ b/docs/adr/0015-release-manifest-identity.md @@ -18,7 +18,7 @@ Git protocol grammar distinguishes a 40-zero `zero-id` from ordinary object iden - The Rust toolchain field must not accept moving aliases or an alternate compiler version as though it were the repository-pinned build identity. - Source commit identity must distinguish a concrete Git object identifier from Git's all-zero protocol sentinel. - Artifact references must remain leaf identities rather than filesystem paths. -- The same manifest must be unambiguous on supported case-sensitive and case-insensitive platforms. +- The same manifest must be unambiguous on supported case-sensitive and case-insensitive platforms and common buyer synchronization paths. - Manifest construction must remain inert metadata admission and must not grant signing, publication, installation, update, rollback, or release authority. - Inputs must be bounded and fail closed before later packaging, signing, or updater layers consume them. @@ -45,7 +45,7 @@ Rejected for this slice. Destructively rewriting admitted artifact spelling woul ### Canonical admission with preserved spelling and collision guards -Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, bind exact build-identity fields separately, require the repository-pinned Rust compiler identity, reject Git's all-zero source-identity sentinel, and reject names whose ASCII-case-folded identities collide or whose basenames are reserved Win32 device names. +Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, bind exact build-identity fields separately, require the repository-pinned Rust compiler identity, reject Git's all-zero source-identity sentinel, and reject names whose ASCII-case-folded identities collide or whose basenames are reserved or operationally device-like across supported Windows and Microsoft synchronization paths. ## Decision @@ -58,7 +58,7 @@ OriginWeave release-manifest admission is a deterministic, bounded, fail-closed 5. `dependency_lock_sha256` must be exactly `sha256:` followed by 64 lowercase hexadecimal digits. 6. The artifact inventory must be non-empty and contain at most 64 entries. 7. Each artifact name must be a bounded ASCII leaf name containing only alphanumerics, `.`, `_`, and `-`; it cannot contain path separators, traversal-like `..`, leading punctuation, or trailing punctuation. -8. Artifact basenames `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9` are rejected case-insensitively, including when followed by an extension. The artifact grammar already rejects the non-ASCII superscript-digit Win32 aliases. +8. Artifact basenames `CON`, `PRN`, `AUX`, `NUL`, `COM0` through `COM9`, and `LPT0` through `LPT9` are rejected case-insensitively, including when followed by an extension. Microsoft Win32 filename guidance explicitly reserves `COM1`-`COM9` and `LPT1`-`LPT9` and documents `COM0` as a possible Win32 namespace symlink; Microsoft OneDrive and SharePoint additionally reject `COM0`-`COM9` and `LPT0`-`LPT9`. OriginWeave deliberately adopts that stricter portable artifact-name set rather than accept a release leaf name that can become a device or synchronization conflict in a buyer environment. The artifact grammar already rejects the non-ASCII superscript-digit Win32 aliases. 9. Artifact names must be unique under ASCII case folding while their original admitted spelling is retained. 10. Each artifact digest must be exactly `sha256:` followed by 64 lowercase hexadecimal digits. 11. Admitted artifacts are stored in deterministic name order. @@ -68,7 +68,7 @@ Constructing or possessing a valid manifest does **not** authenticate an artifac ## Consequences -The release candidate receives one bounded build-and-artifact identity representation that is stable across caller order, records a concrete non-null Git source identity plus the exact repository-pinned Rust toolchain and dependency-lock evidence, and avoids known case-insensitive and Win32 device-name collisions. Packaging, signing, provenance, and update layers can compose on top of this contract without inheriting ambient authority from it. +The release candidate receives one bounded build-and-artifact identity representation that is stable across caller order, records a concrete non-null Git source identity plus the exact repository-pinned Rust toolchain and dependency-lock evidence, and avoids known case-insensitive, Win32 device-name, and Microsoft synchronization-name collisions. Packaging, signing, provenance, and update layers can compose on top of this contract without inheriting ambient authority from it. Changing the protected Rust baseline now requires deliberate convergence of the repository toolchain pin, release-manifest admission contract, tests, and this ADR. A moving channel alias cannot silently change the compiler identity represented by a release manifest. @@ -86,7 +86,7 @@ The boundary reduces null-source-identity, omitted-build-identity, moving-toolch ## Tests and acceptance evidence -The owning `originweave-core` tests must cover valid deterministic ordering, exact artifact and dependency-lock digests, one concrete non-null 40-character source commit, rejection of Git's all-zero source sentinel, the exact pinned Rust toolchain, moving aliases and alternate toolchain versions, malformed toolchain inputs, identifier bounds, malformed names, path/traversal-like names, case-only collisions, Win32 reserved device basenames with and without extensions, neighboring admissible names, exact inventory bounds, duplicate names, channel and build-identity access, and deterministic standard error contracts. +The owning `originweave-core` tests must cover valid deterministic ordering, exact artifact and dependency-lock digests, one concrete non-null 40-character source commit, rejection of Git's all-zero source sentinel, the exact pinned Rust toolchain, moving aliases and alternate toolchain versions, malformed toolchain inputs, identifier bounds, malformed names, path/traversal-like names, case-only collisions, `COM0`-`COM9` and `LPT0`-`LPT9` device/synchronization-conflict basenames with and without extensions, neighboring admissible names such as `COM10` and `LPT10`, exact inventory bounds, duplicate names, channel and build-identity access, and deterministic standard error contracts. Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. CI/security/scanner evidence is exact-head evidence only; predecessor or model-only evidence cannot satisfy acceptance. @@ -114,3 +114,5 @@ Git. (2026). *gitprotocol-common documentation (Git 2.55.0)*. https://git-scm.co Git. (2026). *gitprotocol-pack documentation (Git 2.55.0)*. https://git-scm.com/docs/gitprotocol-pack/2.55.0 Microsoft. (n.d.). *Naming files, paths, and namespaces*. Microsoft Learn. Retrieved August 23, 2026, from https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file + +Microsoft. (n.d.). *Restrictions and limitations in OneDrive and SharePoint*. Microsoft Support. Retrieved August 23, 2026, from https://support.microsoft.com/en-US/onedrive/restrictions-and-limitations-in-onedrive-and-sharepoint From bfec85900dcdcba25812efef7ab22d71f828b7fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 20:41:50 -0700 Subject: [PATCH 044/170] fix(release): satisfy strict ASCII digit lint --- crates/originweave-core/src/release_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 14dd8023b..2611163d8 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -323,7 +323,7 @@ fn windows_reserved_device_basename(name: &str) -> bool { let bytes = basename.as_bytes(); bytes.len() == 4 && (basename[..3].eq_ignore_ascii_case("COM") || basename[..3].eq_ignore_ascii_case("LPT")) - && matches!(bytes[3], b'0'..=b'9') + && bytes[3].is_ascii_digit() } fn valid_sha256_digest(digest: &str) -> bool { From 45f024dcda1707192faf506429a2dddbecc4b71d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 20:46:35 -0700 Subject: [PATCH 045/170] docs(doctoring): record portable COM0 and LPT0 exclusions --- docs/doctoring.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 76f1d43e9..12b7c4754 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -76,7 +76,7 @@ The test-only rcgen 0.14.8 dependency creates a local CA and deterministic certi ### Release artifact filename portability -Microsoft's Win32 filename guidance requires applications not to assume case sensitivity and reserves device basenames including `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9`, including those names followed by extensions. The same documentation also identifies non-ASCII superscript-digit device aliases. OriginWeave's release-manifest identity therefore rejects ASCII-case-folded duplicate artifact names and those reserved ASCII device basenames; its ASCII-only artifact grammar separately excludes the superscript aliases. These checks make one admitted artifact inventory portable across case-sensitive and default case-insensitive target filesystems without rewriting the stored artifact spelling. They remain identity hygiene only and do not grant signing, publication, installation, update, rollback, or release authority. +Microsoft's Win32 filename guidance requires applications not to assume case sensitivity and reserves device basenames including `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9`, including those names followed by extensions. The same documentation identifies non-ASCII superscript-digit device aliases and also documents `COM0` as a possible `Global??` Win32 namespace symlink. Separately, Microsoft's current OneDrive and SharePoint restrictions reject `COM0` through `COM9` and `LPT0` through `LPT9`. OriginWeave therefore adopts the stricter portable release-artifact deny set: ASCII-case-folded duplicate names and `CON`, `PRN`, `AUX`, `NUL`, `COM0` through `COM9`, and `LPT0` through `LPT9`, including extensions, are rejected so an admitted artifact does not become a device or synchronization conflict in a buyer environment. The ASCII-only grammar separately excludes the superscript aliases. These checks preserve the original artifact spelling and remain identity hygiene only; they do not grant signing, publication, installation, update, rollback, or release authority. ### Git source identity admission @@ -156,6 +156,8 @@ Microsoft. (2025, July 25). *Azure IP address 168.63.129.16 overview*. Microsoft Microsoft. (n.d.). *Naming files, paths, and namespaces*. Microsoft Learn. Retrieved August 23, 2026, from https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file +Microsoft. (n.d.). *Restrictions and limitations in OneDrive and SharePoint*. Microsoft Support. Retrieved August 23, 2026, from https://support.microsoft.com/en-US/onedrive/restrictions-and-limitations-in-onedrive-and-sharepoint + Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025). *Learning to orchestrate agents in natural language with the Conductor* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04388 Rescorla, E. (2026). *The Transport Layer Security (TLS) protocol version 1.3* (RFC 9846). Internet Engineering Task Force. https://doi.org/10.17487/RFC9846 @@ -184,4 +186,4 @@ World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 -Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 \ No newline at end of file +Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 From 8145d40f1b028a8f4dc7e7da47ac89bb9e5bb2c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 20:47:10 -0700 Subject: [PATCH 046/170] docs(changelog): record portable release device aliases --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dcf02b8c..be0724651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security +- Release artifact admission rejects `COM0` through `COM9` and `LPT0` through `LPT9` case-insensitively, including extensions, so package names cannot become Windows device or Microsoft synchronization conflicts in buyer environments. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. - Crawler mode is read-only, must pair with the public-crawl purpose, and fails closed without an applicable robots-policy decision. @@ -76,4 +77,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file From 723f80b2e5b369fb5604ba8857050cbe374f84b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 23:33:16 -0700 Subject: [PATCH 047/170] test(core): require manifest-bound SPDX SBOM identity --- .../tests/release_sbom_binding.rs | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 crates/originweave-core/tests/release_sbom_binding.rs diff --git a/crates/originweave-core/tests/release_sbom_binding.rs b/crates/originweave-core/tests/release_sbom_binding.rs new file mode 100644 index 000000000..f51816b0c --- /dev/null +++ b/crates/originweave-core/tests/release_sbom_binding.rs @@ -0,0 +1,139 @@ +use std::error::Error; + +use originweave_core::release_manifest::{ + ReleaseArtifact, ReleaseBuildIdentity, ReleaseChannel, ReleaseManifest, ReleaseSbomBinding, + ReleaseSbomBindingError, ReleaseSbomFormat, +}; + +const SOURCE_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; +const CHROMIUM_REVISION: &str = "150.0.7871.129"; + +fn sha256_digest(hex_digit: char) -> String { + format!("sha256:{}", hex_digit.to_string().repeat(64)) +} + +fn release_manifest() -> Result> { + ReleaseManifest::new( + SOURCE_COMMIT, + CHROMIUM_REVISION, + ReleaseChannel::Stable, + ReleaseBuildIdentity::new("1.97.1", &sha256_digest('9'))?, + vec![ + ReleaseArtifact::new("originweave-linux-x86_64.tar.zst", &sha256_digest('a'))?, + ReleaseArtifact::new("originweave-native-host.bin", &sha256_digest('b'))?, + ReleaseArtifact::new("originweave.spdx.jsonld", &sha256_digest('c'))?, + ], + ) + .map_err(Into::into) +} + +#[test] +fn spdx_sbom_binding_requires_manifest_backed_sbom_and_described_artifacts() +-> Result<(), Box> { + let manifest = release_manifest()?; + let binding = ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + [ + "originweave-native-host.bin", + "originweave-linux-x86_64.tar.zst", + ], + )?; + + assert_eq!(binding.format(), ReleaseSbomFormat::Spdx30JsonLd); + assert_eq!(binding.spdx_specification_version(), "3.0.1"); + assert_eq!(binding.sbom_artifact().name(), "originweave.spdx.jsonld"); + assert_eq!(binding.sbom_artifact().sha256_digest(), sha256_digest('c')); + assert_eq!( + binding.described_artifact_names(), + [ + "originweave-linux-x86_64.tar.zst", + "originweave-native-host.bin", + ] + ); + Ok(()) +} + +#[test] +fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() +-> Result<(), Box> { + let manifest = release_manifest()?; + + assert_eq!( + ReleaseSbomBinding::new( + &manifest, + "missing.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + ["originweave-linux-x86_64.tar.zst"], + ), + Err(ReleaseSbomBindingError::UnknownSbomArtifact) + ); + assert_eq!( + ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + std::iter::empty::<&str>(), + ), + Err(ReleaseSbomBindingError::MissingDescribedArtifacts) + ); + assert_eq!( + ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + ["not-in-release.bin"], + ), + Err(ReleaseSbomBindingError::UnknownDescribedArtifact) + ); + assert_eq!( + ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + [ + "originweave-linux-x86_64.tar.zst", + "originweave-linux-x86_64.tar.zst", + ], + ), + Err(ReleaseSbomBindingError::DuplicateDescribedArtifact) + ); + assert_eq!( + ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + ["ORIGINWEAVE-LINUX-X86_64.TAR.ZST"], + ), + Err(ReleaseSbomBindingError::UnknownDescribedArtifact) + ); + Ok(()) +} + +#[test] +fn spdx_sbom_binding_errors_are_standard_source_free_rust_errors() { + let errors = [ + ( + ReleaseSbomBindingError::UnknownSbomArtifact, + "release SBOM artifact is not present in the bound release manifest", + ), + ( + ReleaseSbomBindingError::MissingDescribedArtifacts, + "release SBOM must describe at least one release artifact", + ), + ( + ReleaseSbomBindingError::UnknownDescribedArtifact, + "release SBOM describes an artifact that is not present in the bound release manifest", + ), + ( + ReleaseSbomBindingError::DuplicateDescribedArtifact, + "release SBOM repeats a described release artifact", + ), + ]; + + for (error, expected) in errors { + assert_eq!(error.to_string(), expected); + assert!(Error::source(&error).is_none()); + } +} From 1e09c8c7474adcadaca6d85f22bb5c195a23942b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 23:36:44 -0700 Subject: [PATCH 048/170] feat(core): bind SPDX SBOM identity to release manifest --- .../originweave-core/src/release_manifest.rs | 144 +++++++++++++++++- 1 file changed, 142 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 2611163d8..cc815eba6 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -1,8 +1,9 @@ //! Fail-closed identity binding for release artifacts. //! //! The types in this module are deliberately inert metadata contracts. They bind an exact -//! source commit, Chromium revision, release channel, build identity, and artifact digests -//! without granting signing, publication, installation, update, rollback, or release authority. +//! source commit, Chromium revision, release channel, build identity, artifact digests, and +//! release-SBOM identity without granting signing, publication, installation, update, rollback, +//! or release authority. use std::collections::BTreeSet; use std::error::Error; @@ -157,6 +158,145 @@ impl fmt::Display for ReleaseArtifactError { impl Error for ReleaseArtifactError {} +/// SPDX serialization identity admitted for release-SBOM evidence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReleaseSbomFormat { + /// SPDX 3.0.1 serialized as JSON-LD. + Spdx30JsonLd, +} + +impl ReleaseSbomFormat { + /// Return the exact SPDX specification version represented by this format. + #[must_use] + pub const fn spdx_specification_version(self) -> &'static str { + match self { + Self::Spdx30JsonLd => "3.0.1", + } + } +} + +/// Inert binding between one release manifest and its declared SPDX SBOM artifact identity. +/// +/// This value proves only that the named SBOM and described artifacts are already admitted by +/// the same immutable [`ReleaseManifest`]. It does not parse or validate SPDX content, prove that +/// the SBOM actually describes those artifacts, authenticate any digest, or grant release +/// authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReleaseSbomBinding { + format: ReleaseSbomFormat, + sbom_artifact: ReleaseArtifact, + described_artifact_names: Vec, +} + +impl ReleaseSbomBinding { + /// Bind a declared SPDX SBOM artifact to exact artifacts in one release manifest. + /// + /// The SBOM artifact name and every described artifact name must match the manifest exactly. + /// At least one described artifact is required, duplicates fail closed, and stored described + /// names are sorted deterministically. The release manifest's own admission bound therefore + /// also bounds this inventory. + pub fn new<'a, I>( + manifest: &ReleaseManifest, + sbom_artifact_name: &str, + format: ReleaseSbomFormat, + described_artifact_names: I, + ) -> Result + where + I: IntoIterator, + { + let Some(sbom_artifact) = manifest + .artifacts() + .iter() + .find(|artifact| artifact.name() == sbom_artifact_name) + else { + return Err(ReleaseSbomBindingError::UnknownSbomArtifact); + }; + + let mut described = Vec::new(); + let mut seen = BTreeSet::new(); + for artifact_name in described_artifact_names { + if !manifest + .artifacts() + .iter() + .any(|artifact| artifact.name() == artifact_name) + { + return Err(ReleaseSbomBindingError::UnknownDescribedArtifact); + } + if !seen.insert(artifact_name.to_owned()) { + return Err(ReleaseSbomBindingError::DuplicateDescribedArtifact); + } + described.push(artifact_name.to_owned()); + } + if described.is_empty() { + return Err(ReleaseSbomBindingError::MissingDescribedArtifacts); + } + described.sort(); + + Ok(Self { + format, + sbom_artifact: sbom_artifact.clone(), + described_artifact_names: described, + }) + } + + /// Return the declared SPDX serialization format. + #[must_use] + pub const fn format(&self) -> ReleaseSbomFormat { + self.format + } + + /// Return the exact SPDX specification version represented by the declared format. + #[must_use] + pub const fn spdx_specification_version(&self) -> &'static str { + self.format.spdx_specification_version() + } + + /// Return the exact manifest-backed SBOM artifact identity. + #[must_use] + pub const fn sbom_artifact(&self) -> &ReleaseArtifact { + &self.sbom_artifact + } + + /// Return described manifest artifact names in deterministic lexical order. + #[must_use] + pub fn described_artifact_names(&self) -> &[String] { + &self.described_artifact_names + } +} + +/// Validation error for release-SBOM identity binding. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReleaseSbomBindingError { + /// The named SBOM artifact is absent from the release manifest. + UnknownSbomArtifact, + /// No release artifacts were declared as described by the SBOM. + MissingDescribedArtifacts, + /// A described artifact name is absent from the release manifest. + UnknownDescribedArtifact, + /// A described artifact name was repeated. + DuplicateDescribedArtifact, +} + +impl fmt::Display for ReleaseSbomBindingError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownSbomArtifact => formatter + .write_str("release SBOM artifact is not present in the bound release manifest"), + Self::MissingDescribedArtifacts => { + formatter.write_str("release SBOM must describe at least one release artifact") + } + Self::UnknownDescribedArtifact => formatter.write_str( + "release SBOM describes an artifact that is not present in the bound release manifest", + ), + Self::DuplicateDescribedArtifact => { + formatter.write_str("release SBOM repeats a described release artifact") + } + } + } +} + +impl Error for ReleaseSbomBindingError {} + /// Deterministic, bounded identity manifest for one OriginWeave release candidate. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReleaseManifest { From 6a2433953f6cf1a06bb117f60d75d1c75783f420 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 23:39:31 -0700 Subject: [PATCH 049/170] docs(changelog): record release SBOM identity boundary --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be0724651..92d7b7dfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Added an inert SPDX 3.0.1 JSON-LD SBOM identity binding that requires the SBOM artifact and every declared described artifact to be exact members of the same release manifest, rejects empty, foreign, duplicate, and case-drifted artifact identities, and does not claim SPDX content conformance, artifact authentication, provenance, signing, publication, installation, update, rollback, or release authority. - Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, the exact repository-pinned Rust 1.97.1 toolchain, exact lowercase dependency-lock SHA-256 evidence, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; build identity remains metadata rather than reproducibility proof, moving toolchain aliases and alternate versions fail closed, and artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. @@ -56,7 +57,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Shortened, integer, hexadecimal, and legacy octal-looking IPv4 host spellings are rejected so the policy origin cannot diverge from Chromium host interpretation. - IPv4-mapped IPv6 is canonicalized before destination classification and pin comparison so mapped private or loopback addresses cannot bypass IPv4 policy. - The default destination policy permits only public addresses and denies unspecified, loopback, private, shared, link-local, metadata, documentation, benchmarking, multicast, broadcast, transition, and protocol-reserved destinations. -- Azure platform IP `168.63.129.16` and Amazon EKS Pod Identity endpoints `169.254.170.23` and `fd00:ec2::23` are classified as metadata or platform services before broader public, link-local, or unique-local rules. +- Azure platform IP `168.63.129.16` and Amazon EKS Pod Identity endpoints `169.254.170.23` and `fd00:ec2::23` are classified as metadata or platform services before broader address-range rules. - Resolver answers are rejected when empty or larger than 256 addresses, preventing an unbounded resolver response from entering policy state. - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. From f4bd3b4a4a8d6c162d4691201696df02e4fb8f17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 23:40:56 -0700 Subject: [PATCH 050/170] docs(doctoring): record SPDX 3.0.1 SBOM boundary --- docs/doctoring.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 12b7c4754..9fc5ba94e 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -78,6 +78,10 @@ The test-only rcgen 0.14.8 dependency creates a local CA and deterministic certi Microsoft's Win32 filename guidance requires applications not to assume case sensitivity and reserves device basenames including `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9`, including those names followed by extensions. The same documentation identifies non-ASCII superscript-digit device aliases and also documents `COM0` as a possible `Global??` Win32 namespace symlink. Separately, Microsoft's current OneDrive and SharePoint restrictions reject `COM0` through `COM9` and `LPT0` through `LPT9`. OriginWeave therefore adopts the stricter portable release-artifact deny set: ASCII-case-folded duplicate names and `CON`, `PRN`, `AUX`, `NUL`, `COM0` through `COM9`, and `LPT0` through `LPT9`, including extensions, are rejected so an admitted artifact does not become a device or synchronization conflict in a buyer environment. The ASCII-only grammar separately excludes the superscript aliases. These checks preserve the original artifact spelling and remain identity hygiene only; they do not grant signing, publication, installation, update, rollback, or release authority. +### Release SBOM identity + +SPDX 3.0.1 defines a software bill of materials as a collection of SPDX elements describing a single package and defines JSON-LD as an RDF serialization for SPDX data. Conformant SPDX JSON-LD additionally requires structural validation against the versioned SPDX JSON Schema and semantic validation against the SPDX ontology. OriginWeave's first release-SBOM slice deliberately does not claim that content-level conformance: it binds only a declared SPDX 3.0.1 JSON-LD SBOM artifact identity and the exact release-artifact names it is intended to describe to one immutable `ReleaseManifest`. The SBOM artifact and every described artifact must already be admitted by that manifest; empty inventories, foreign identities, duplicates, and case-drifted names fail closed. Generation, content validation, package/component completeness, signatures, provenance, publication, installation, update, rollback, and release authority remain separate reviewed boundaries. + ### Git source identity admission Git's protocol grammar distinguishes an ordinary 40-hex object identifier from `zero-id = 40*"0"`. The pack protocol uses that all-zero sentinel to represent absent refs and create/delete reference state rather than a concrete source object. OriginWeave therefore requires release-manifest `source_commit` and WARC PROV `software_commit_sha` values to be canonical lowercase 40-hex identifiers with at least one nonzero digit. This is an identity-admission rule only: it prevents a null Git sentinel from being represented as a concrete software revision, but does not prove repository reachability, GitHub authenticity, commit trust, provenance, or release authority. @@ -172,6 +176,8 @@ Sakana AI. (2026, June 22). *Sakana Fugu: One model to command them all*. https: Sheffer, Y., Saint-Andre, P., & Fossati, T. (2022). *Recommendations for secure use of Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS)* (RFC 9325). Internet Engineering Task Force. https://doi.org/10.17487/RFC9325 +SPDX Workgroup. (2026). *SPDX specification 3.0.1*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/ + The Rust Project Developers. (2026). *Ipv4Addr in std::net* (Rust 1.97.1) [Software documentation]. https://doc.rust-lang.org/stable/std/net/struct.Ipv4Addr.html The Rust Project Developers. (2026). *Ipv6Addr in std::net* (Rust 1.97.1) [Software documentation]. https://doc.rust-lang.org/stable/std/net/struct.Ipv6Addr.html @@ -186,4 +192,4 @@ World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 -Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 +Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 \ No newline at end of file From 69b53442026042cd32e70636d155b780d5c387f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 23:41:52 -0700 Subject: [PATCH 051/170] docs(adr): define release SBOM identity boundary --- docs/adr/0016-release-sbom-identity.md | 105 +++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 docs/adr/0016-release-sbom-identity.md diff --git a/docs/adr/0016-release-sbom-identity.md b/docs/adr/0016-release-sbom-identity.md new file mode 100644 index 000000000..547351139 --- /dev/null +++ b/docs/adr/0016-release-sbom-identity.md @@ -0,0 +1,105 @@ +# ADR 0016: Release SBOM identity binding + +- Status: Proposed +- Date: 2026-08-23 + +## Context + +Issue #201 requires acquisition-grade release evidence, including a software bill of materials that is tied to the same immutable release identity buyers verify. ADR 0015 deliberately stops at a bounded release-manifest identity and does not claim SBOM content conformance, provenance, signing, publication, installation, update, rollback, or release authority. + +SPDX 3.0.1 defines a Software `Sbom` as a collection of SPDX elements describing a single package. SPDX also defines JSON-LD serialization and separate structural and semantic validation requirements. A release system therefore needs to distinguish two different claims: that a release manifest names a particular SBOM artifact and exact release artifacts the SBOM is intended to describe, versus the stronger claim that the SBOM bytes themselves conform to SPDX and accurately describe those artifacts. + +This slice establishes only the first claim so later SPDX generation and verification can compose without inventing artifact identities outside the release manifest. + +## Decision drivers + +- SBOM identity must not drift away from the exact release manifest it supplements. +- The SBOM artifact itself must already be an admitted release artifact with a manifest-bound SHA-256 identity. +- Every release artifact declared as described by the SBOM must already be admitted by that same manifest. +- Empty, duplicate, foreign, and case-drifted artifact identities must fail closed. +- The binding must be deterministic and bounded by the existing release-manifest inventory limit. +- Metadata binding must not be confused with SPDX content validation, provenance, signing, publication, installation, update, rollback, or release authority. + +## Assumptions and authority boundaries + +- ADR 0015 remains the owning decision for release-manifest identity admission. +- `ReleaseSbomFormat::Spdx30JsonLd` identifies SPDX 3.0.1 serialized as JSON-LD only; it is not a parser or conformance certificate. +- `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. +- The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase SHA-256 identity. +- The binding contains no package/component graph, license conclusions, vulnerability state, provenance attestations, signing keys, credentials, network authority, installer authority, or update authority. +- Later SPDX generation and verification must independently validate the actual serialized bytes against the reviewed SPDX schema/ontology and product completeness requirements. + +## Options considered + +### Store an arbitrary SBOM path or URL next to the manifest + +Rejected. An ambient path or URL can drift from the bounded release inventory and does not guarantee that the SBOM itself is one of the artifacts buyers can hash against the manifest. + +### Accept described artifact names that are not in the manifest + +Rejected. That would allow the SBOM layer to invent a second release inventory and weaken ADR 0015's canonical artifact identity boundary. + +### Treat successful construction as SPDX conformance + +Rejected. Binding names and digests cannot establish that the SBOM bytes satisfy SPDX structural/semantic requirements or accurately describe package composition. + +### Exact manifest-backed identity binding + +Selected. The SBOM artifact and all described artifact names must resolve exactly within the same release manifest, duplicates and empty sets fail closed, and described identities are stored in deterministic lexical order. + +## Decision + +OriginWeave adds an inert release-SBOM identity boundary: + +1. The supported initial format is `ReleaseSbomFormat::Spdx30JsonLd`, which reports SPDX specification version `3.0.1`. +2. The named SBOM artifact must exactly match an artifact admitted by the same `ReleaseManifest`. +3. At least one described release artifact is required. +4. Every described artifact name must exactly match an artifact admitted by the same manifest. +5. Duplicate described artifact names fail closed. +6. Case-drifted names are not normalized and fail as unknown unless they exactly match the preserved manifest spelling. +7. Described artifact names are stored in deterministic lexical order. +8. The manifest's existing bounded artifact inventory transitively bounds the binding. +9. Validation failures remain typed, deterministic standard Rust errors without hidden fallback. +10. Constructing the binding does not claim SPDX content conformance, generation completeness, artifact authentication, provenance, signatures, publication, installation, update, rollback, or release authority. + +## Consequences + +Later SPDX tooling receives one explicit join point between SBOM identity and the exact release inventory. It cannot silently describe an artifact that the release manifest did not admit, and buyers can retain the manifest's existing digest identity for the SBOM artifact itself. + +This boundary is intentionally narrower than a commercial SBOM generator or verifier. Those later components must consume the actual SBOM bytes, establish SPDX 3.0.1 structural and semantic conformance, verify required software/package content, and preserve exact artifact/provenance linkage without broadening authority. + +## Failure and degraded behavior + +A missing SBOM artifact, empty described-artifact set, foreign described artifact, duplicate described identity, or case-drifted identity fails closed before a binding is produced. No fallback rewrites a name, drops an artifact, substitutes another manifest entry, or converts an invalid binding into success. + +Failure to construct this metadata binding does not authorize release through another path. Conversely, successful construction cannot be used as evidence that SPDX content is valid or complete. + +## Security / privacy / governance impact + +The boundary reduces supply-chain identity ambiguity by preventing the SBOM layer from creating a second ambient artifact namespace. It introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. Scheduled development agents remain unable to merge, sign, tag, publish, install, or change repository governance. + +## Tests and acceptance evidence + +The owning `originweave-core` tests must prove exact manifest-backed SBOM admission, deterministic described-artifact ordering, and fail-closed behavior for missing SBOM identity, empty described sets, foreign artifacts, duplicates, and case drift. Public error text and `std::error::Error` behavior are part of the deterministic contract. + +Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. Rust formatting, workspace/all-target checks, full tests, strict Clippy, and rustdoc must pass on that same head. Predecessor checks or model-only evidence do not transfer. + +## Migration and rollback + +This is a new in-process metadata primitive with no protected-main persisted SBOM schema migration. Before acceptance it can be revised or withdrawn with its owning feature branch. If a durable external release-manifest/SBOM format later depends on this contract, incompatible identity changes require explicit versioning and migration rather than parser broadening. + +## Open follow-ups + +- Generate SPDX 3.0.1 SBOM content for supported release packages through a separately reviewed deterministic pipeline. +- Validate JSON-LD structure and SPDX ontology semantics against the reviewed 3.0.1 resources. +- Define package/component completeness and root-element rules for OriginWeave distribution artifacts. +- Bind SBOM content, SLSA provenance, signing identity, timestamps, platform packages, reproducibility evidence, and updater trust without transferring authority from this metadata type. +- Complete issue #201's integrated release acceptance and rollback/freeze-protection architecture. + +## Supersession / reversal conditions + +Supersede this ADR when a versioned external release/SBOM specification replaces the in-process binding, when OriginWeave adopts a different SBOM standard under a reviewed migration, or when a stronger integrated provenance model subsumes this identity join. Reversal must preserve fail-closed manifest-backed artifact identity and cannot make metadata possession equivalent to release authority. + +## References + +SPDX Workgroup. (2026). *SPDX specification 3.0.1*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/ From 5e27afab68a5fc9a5cfa70d52dec60ef3024e741 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 23:42:24 -0700 Subject: [PATCH 052/170] docs(adr): index release SBOM identity decision --- docs/adr/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/adr/README.md b/docs/adr/README.md index de0644e81..1ee28228a 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -62,8 +62,9 @@ ADR 0013 and ADR 0014 exist only on this documentation branch until it integrate | ADR | Decision | Status | Governs | |---|---|---|---| | [0015](0015-release-manifest-identity.md) | Release manifest identity boundary | Proposed | bounded release-candidate metadata identity, deterministic artifact ordering, cross-platform filename collision guards, and explicit exclusion of signing/publication/install/update authority | +| [0016](0016-release-sbom-identity.md) | Release SBOM identity binding | Proposed | exact manifest-backed SPDX 3.0.1 SBOM artifact identity and described-artifact membership without content-conformance or release authority claims | -ADR 0015 is active-PR architecture evidence for the release-manifest identity slice. Indexing it satisfies canonical discovery without promoting the Proposed decision or its implementation to protected-main truth. +ADR 0015 is active-PR architecture evidence for the release-manifest identity slice. ADR 0016 is stacked active-PR architecture evidence for the release-SBOM identity slice. Indexing them satisfies canonical discovery without promoting either Proposed decision or its implementation to protected-main truth. Other active feature PRs may contain additional Proposed ADRs. Those files are not part of this canonical documentation line until integrated or deliberately reconciled here. Historical PR checks, stale branch state, or chat decisions never transfer ADR acceptance across a changed head. From f938d336ddfeb26ae638b40b07dba422509f9127 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 23:44:44 -0700 Subject: [PATCH 053/170] docs(index): include release SBOM ADR --- docs/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index ee7db1077..ec5cb108d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -89,7 +89,8 @@ The second group exists only on this documentation branch until the branch integ ### Active feature proposed decisions - [ADR 0015: Release manifest identity boundary](adr/0015-release-manifest-identity.md) +- [ADR 0016: Release SBOM identity binding](adr/0016-release-sbom-identity.md) -ADR 0015 is active-PR architecture evidence for the release-manifest identity slice. Its presence in this index keeps the documentation graph complete without promoting the Proposed decision or its implementation to protected-main truth. +ADR 0015 is active-PR architecture evidence for the release-manifest identity slice. ADR 0016 is stacked active-PR architecture evidence for the release-SBOM identity slice. Their presence in this index keeps the documentation graph complete without promoting either Proposed decision or its implementation to protected-main truth. See the [ADR index](adr/README.md) for status rules, required decision structure, supersession rules, and active feature ADRs. The index and each ADR's own status metadata must agree; a PR body, chat transcript, automation prompt, or stale issue reference cannot change ADR status. From 41ba34dd9d483754fd2f9350f62bb80e1d0cf5da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 23:48:27 -0700 Subject: [PATCH 054/170] test(core): cover SBOM binding in one concrete iterator contract --- .../originweave-core/tests/release_sbom_binding.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/tests/release_sbom_binding.rs b/crates/originweave-core/tests/release_sbom_binding.rs index f51816b0c..e75c56aa9 100644 --- a/crates/originweave-core/tests/release_sbom_binding.rs +++ b/crates/originweave-core/tests/release_sbom_binding.rs @@ -35,7 +35,7 @@ fn spdx_sbom_binding_requires_manifest_backed_sbom_and_described_artifacts() &manifest, "originweave.spdx.jsonld", ReleaseSbomFormat::Spdx30JsonLd, - [ + vec![ "originweave-native-host.bin", "originweave-linux-x86_64.tar.zst", ], @@ -65,7 +65,7 @@ fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() &manifest, "missing.spdx.jsonld", ReleaseSbomFormat::Spdx30JsonLd, - ["originweave-linux-x86_64.tar.zst"], + vec!["originweave-linux-x86_64.tar.zst"], ), Err(ReleaseSbomBindingError::UnknownSbomArtifact) ); @@ -74,7 +74,7 @@ fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() &manifest, "originweave.spdx.jsonld", ReleaseSbomFormat::Spdx30JsonLd, - std::iter::empty::<&str>(), + Vec::<&str>::new(), ), Err(ReleaseSbomBindingError::MissingDescribedArtifacts) ); @@ -83,7 +83,7 @@ fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() &manifest, "originweave.spdx.jsonld", ReleaseSbomFormat::Spdx30JsonLd, - ["not-in-release.bin"], + vec!["not-in-release.bin"], ), Err(ReleaseSbomBindingError::UnknownDescribedArtifact) ); @@ -92,7 +92,7 @@ fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() &manifest, "originweave.spdx.jsonld", ReleaseSbomFormat::Spdx30JsonLd, - [ + vec![ "originweave-linux-x86_64.tar.zst", "originweave-linux-x86_64.tar.zst", ], @@ -104,7 +104,7 @@ fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() &manifest, "originweave.spdx.jsonld", ReleaseSbomFormat::Spdx30JsonLd, - ["ORIGINWEAVE-LINUX-X86_64.TAR.ZST"], + vec!["ORIGINWEAVE-LINUX-X86_64.TAR.ZST"], ), Err(ReleaseSbomBindingError::UnknownDescribedArtifact) ); From 8b377ab4e6370d044767b3b9755b9d855f7c196c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 23:59:45 -0700 Subject: [PATCH 055/170] test(core): prove SBOM binding retains exact artifact identity --- .../tests/release_sbom_binding.rs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/originweave-core/tests/release_sbom_binding.rs b/crates/originweave-core/tests/release_sbom_binding.rs index e75c56aa9..f042c9c1c 100644 --- a/crates/originweave-core/tests/release_sbom_binding.rs +++ b/crates/originweave-core/tests/release_sbom_binding.rs @@ -55,6 +55,42 @@ fn spdx_sbom_binding_requires_manifest_backed_sbom_and_described_artifacts() Ok(()) } +#[test] +fn spdx_sbom_binding_retains_exact_described_artifact_identity() -> Result<(), Box> { + let original_manifest = release_manifest()?; + let changed_manifest = ReleaseManifest::new( + SOURCE_COMMIT, + CHROMIUM_REVISION, + ReleaseChannel::Stable, + ReleaseBuildIdentity::new("1.97.1", &sha256_digest('9'))?, + vec![ + ReleaseArtifact::new("originweave-linux-x86_64.tar.zst", &sha256_digest('d'))?, + ReleaseArtifact::new("originweave-native-host.bin", &sha256_digest('b'))?, + ReleaseArtifact::new("originweave.spdx.jsonld", &sha256_digest('c'))?, + ], + )?; + let described_names = vec![ + "originweave-native-host.bin", + "originweave-linux-x86_64.tar.zst", + ]; + + let original_binding = ReleaseSbomBinding::new( + &original_manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + described_names.clone(), + )?; + let changed_binding = ReleaseSbomBinding::new( + &changed_manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + described_names, + )?; + + assert_ne!(original_binding, changed_binding); + Ok(()) +} + #[test] fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() -> Result<(), Box> { From 5ec9bbb0f9613016a6ee9461a127b728e2ffe181 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 00:07:12 -0700 Subject: [PATCH 056/170] fix(core): retain exact artifact identity in SBOM binding --- .../originweave-core/src/release_manifest.rs | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index cc815eba6..a58f9aa04 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -177,24 +177,26 @@ impl ReleaseSbomFormat { /// Inert binding between one release manifest and its declared SPDX SBOM artifact identity. /// -/// This value proves only that the named SBOM and described artifacts are already admitted by -/// the same immutable [`ReleaseManifest`]. It does not parse or validate SPDX content, prove that -/// the SBOM actually describes those artifacts, authenticate any digest, or grant release -/// authority. +/// This value proves only that the named SBOM and exact described artifact identities are already +/// admitted by the same immutable [`ReleaseManifest`]. It retains each described artifact digest +/// so a same-name artifact change produces a different binding. It does not parse or validate SPDX +/// content, prove that the SBOM actually describes those artifacts, authenticate any digest, or +/// grant release authority. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReleaseSbomBinding { format: ReleaseSbomFormat, sbom_artifact: ReleaseArtifact, described_artifact_names: Vec, + described_artifacts: Vec, } impl ReleaseSbomBinding { /// Bind a declared SPDX SBOM artifact to exact artifacts in one release manifest. /// /// The SBOM artifact name and every described artifact name must match the manifest exactly. - /// At least one described artifact is required, duplicates fail closed, and stored described - /// names are sorted deterministically. The release manifest's own admission bound therefore - /// also bounds this inventory. + /// At least one described artifact is required, duplicates fail closed, and exact described + /// artifact identities plus their public names are sorted deterministically. The release + /// manifest's own admission bound therefore also bounds this inventory. pub fn new<'a, I>( manifest: &ReleaseManifest, sbom_artifact_name: &str, @@ -212,30 +214,35 @@ impl ReleaseSbomBinding { return Err(ReleaseSbomBindingError::UnknownSbomArtifact); }; - let mut described = Vec::new(); + let mut described_artifacts = Vec::new(); let mut seen = BTreeSet::new(); for artifact_name in described_artifact_names { - if !manifest + let Some(artifact) = manifest .artifacts() .iter() - .any(|artifact| artifact.name() == artifact_name) - { + .find(|artifact| artifact.name() == artifact_name) + else { return Err(ReleaseSbomBindingError::UnknownDescribedArtifact); - } + }; if !seen.insert(artifact_name.to_owned()) { return Err(ReleaseSbomBindingError::DuplicateDescribedArtifact); } - described.push(artifact_name.to_owned()); + described_artifacts.push(artifact.clone()); } - if described.is_empty() { + if described_artifacts.is_empty() { return Err(ReleaseSbomBindingError::MissingDescribedArtifacts); } - described.sort(); + described_artifacts.sort_by(|left, right| left.name.cmp(&right.name)); + let described_artifact_names = described_artifacts + .iter() + .map(|artifact| artifact.name().to_owned()) + .collect(); Ok(Self { format, sbom_artifact: sbom_artifact.clone(), - described_artifact_names: described, + described_artifact_names, + described_artifacts, }) } From 6d86b5f87d62ec536d70bed43eae71b0bd1640be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 01:03:43 -0700 Subject: [PATCH 057/170] test(core): bind SBOM identity to exact release manifest --- .../tests/release_sbom_binding.rs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/originweave-core/tests/release_sbom_binding.rs b/crates/originweave-core/tests/release_sbom_binding.rs index f042c9c1c..a599a8802 100644 --- a/crates/originweave-core/tests/release_sbom_binding.rs +++ b/crates/originweave-core/tests/release_sbom_binding.rs @@ -91,6 +91,52 @@ fn spdx_sbom_binding_retains_exact_described_artifact_identity() -> Result<(), B Ok(()) } +#[test] +fn spdx_sbom_binding_retains_exact_release_manifest_identity() -> Result<(), Box> { + let original_manifest = release_manifest()?; + let changed_source_manifest = ReleaseManifest::new( + "89abcdef0123456789abcdef0123456789abcdef", + CHROMIUM_REVISION, + ReleaseChannel::Stable, + ReleaseBuildIdentity::new("1.97.1", &sha256_digest('9'))?, + original_manifest.artifacts().iter().cloned(), + )?; + let changed_channel_manifest = ReleaseManifest::new( + SOURCE_COMMIT, + CHROMIUM_REVISION, + ReleaseChannel::Beta, + ReleaseBuildIdentity::new("1.97.1", &sha256_digest('9'))?, + original_manifest.artifacts().iter().cloned(), + )?; + let described_names = vec![ + "originweave-native-host.bin", + "originweave-linux-x86_64.tar.zst", + ]; + + let original_binding = ReleaseSbomBinding::new( + &original_manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + described_names.clone(), + )?; + let changed_source_binding = ReleaseSbomBinding::new( + &changed_source_manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + described_names.clone(), + )?; + let changed_channel_binding = ReleaseSbomBinding::new( + &changed_channel_manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + described_names, + )?; + + assert_ne!(original_binding, changed_source_binding); + assert_ne!(original_binding, changed_channel_binding); + Ok(()) +} + #[test] fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() -> Result<(), Box> { From a89112d23e35b204799d6ff5a8eb798dfaf8e94f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 01:07:04 -0700 Subject: [PATCH 058/170] fix(core): retain exact release manifest in SBOM binding --- .../originweave-core/src/release_manifest.rs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index a58f9aa04..6d82cb3d4 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -178,12 +178,14 @@ impl ReleaseSbomFormat { /// Inert binding between one release manifest and its declared SPDX SBOM artifact identity. /// /// This value proves only that the named SBOM and exact described artifact identities are already -/// admitted by the same immutable [`ReleaseManifest`]. It retains each described artifact digest -/// so a same-name artifact change produces a different binding. It does not parse or validate SPDX -/// content, prove that the SBOM actually describes those artifacts, authenticate any digest, or -/// grant release authority. +/// admitted by the same immutable [`ReleaseManifest`]. It retains the complete bounded release +/// manifest identity and each described artifact digest, so any source, Chromium, channel, build, +/// artifact-inventory, or described-artifact change produces a different binding. It does not +/// parse or validate SPDX content, prove that the SBOM actually describes those artifacts, +/// authenticate any digest, or grant release authority. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReleaseSbomBinding { + release_manifest: ReleaseManifest, format: ReleaseSbomFormat, sbom_artifact: ReleaseArtifact, described_artifact_names: Vec, @@ -193,10 +195,11 @@ pub struct ReleaseSbomBinding { impl ReleaseSbomBinding { /// Bind a declared SPDX SBOM artifact to exact artifacts in one release manifest. /// - /// The SBOM artifact name and every described artifact name must match the manifest exactly. - /// At least one described artifact is required, duplicates fail closed, and exact described - /// artifact identities plus their public names are sorted deterministically. The release - /// manifest's own admission bound therefore also bounds this inventory. + /// The complete bounded release-manifest identity is retained in the binding. The SBOM + /// artifact name and every described artifact name must match that manifest exactly. At least + /// one described artifact is required, duplicates fail closed, and exact described artifact + /// identities plus their public names are sorted deterministically. The release manifest's own + /// admission bound therefore also bounds this inventory. pub fn new<'a, I>( manifest: &ReleaseManifest, sbom_artifact_name: &str, @@ -239,6 +242,7 @@ impl ReleaseSbomBinding { .collect(); Ok(Self { + release_manifest: manifest.clone(), format, sbom_artifact: sbom_artifact.clone(), described_artifact_names, @@ -505,4 +509,4 @@ fn valid_revision(revision: &str) -> bool { fn valid_toolchain(toolchain: &str) -> bool { toolchain == "1.97.1" -} +} \ No newline at end of file From 29d960d31fa09148c97ad757a7685a52d1ba7478 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 01:10:19 -0700 Subject: [PATCH 059/170] style(core): apply canonical Rust formatting --- crates/originweave-core/src/release_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 6d82cb3d4..38a04a1fc 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -509,4 +509,4 @@ fn valid_revision(revision: &str) -> bool { fn valid_toolchain(toolchain: &str) -> bool { toolchain == "1.97.1" -} \ No newline at end of file +} From e72b59b79016ffab4b74bc77c3b4148ab2b5f0bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 02:05:09 -0700 Subject: [PATCH 060/170] test(core): reject self-describing release SBOM --- .../tests/release_sbom_binding.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/originweave-core/tests/release_sbom_binding.rs b/crates/originweave-core/tests/release_sbom_binding.rs index a599a8802..81eba0061 100644 --- a/crates/originweave-core/tests/release_sbom_binding.rs +++ b/crates/originweave-core/tests/release_sbom_binding.rs @@ -193,6 +193,23 @@ fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() Ok(()) } +#[test] +fn spdx_sbom_binding_rejects_self_description() -> Result<(), Box> { + let manifest = release_manifest()?; + let binding = ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + vec!["originweave.spdx.jsonld"], + ); + + assert!( + binding.is_err(), + "release SBOM identity must fail closed when the SBOM artifact describes itself" + ); + Ok(()) +} + #[test] fn spdx_sbom_binding_errors_are_standard_source_free_rust_errors() { let errors = [ From 25cd8518636af73967871340e51367572ad76e8f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 02:10:02 -0700 Subject: [PATCH 061/170] fix(core): reject self-describing release SBOM --- crates/originweave-core/src/release_manifest.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 38a04a1fc..6e0b456ee 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -196,8 +196,9 @@ impl ReleaseSbomBinding { /// Bind a declared SPDX SBOM artifact to exact artifacts in one release manifest. /// /// The complete bounded release-manifest identity is retained in the binding. The SBOM - /// artifact name and every described artifact name must match that manifest exactly. At least - /// one described artifact is required, duplicates fail closed, and exact described artifact + /// artifact name and every described artifact name must match that manifest exactly. The SBOM + /// artifact itself cannot be admitted as one of its described artifacts. At least one other + /// described artifact is required, duplicates fail closed, and exact described artifact /// identities plus their public names are sorted deterministically. The release manifest's own /// admission bound therefore also bounds this inventory. pub fn new<'a, I>( @@ -220,6 +221,9 @@ impl ReleaseSbomBinding { let mut described_artifacts = Vec::new(); let mut seen = BTreeSet::new(); for artifact_name in described_artifact_names { + if artifact_name == sbom_artifact.name() { + return Err(ReleaseSbomBindingError::SelfDescribedSbomArtifact); + } let Some(artifact) = manifest .artifacts() .iter() @@ -282,6 +286,8 @@ pub enum ReleaseSbomBindingError { UnknownSbomArtifact, /// No release artifacts were declared as described by the SBOM. MissingDescribedArtifacts, + /// The SBOM artifact was also declared as an artifact that the SBOM describes. + SelfDescribedSbomArtifact, /// A described artifact name is absent from the release manifest. UnknownDescribedArtifact, /// A described artifact name was repeated. @@ -296,6 +302,9 @@ impl fmt::Display for ReleaseSbomBindingError { Self::MissingDescribedArtifacts => { formatter.write_str("release SBOM must describe at least one release artifact") } + Self::SelfDescribedSbomArtifact => { + formatter.write_str("release SBOM must not describe its own artifact") + } Self::UnknownDescribedArtifact => formatter.write_str( "release SBOM describes an artifact that is not present in the bound release manifest", ), From ed3dfe61371892bcbfee3891578eabb8be4cce0d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 02:10:45 -0700 Subject: [PATCH 062/170] test(core): require typed SBOM self-description denial --- .../tests/release_sbom_binding.rs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/originweave-core/tests/release_sbom_binding.rs b/crates/originweave-core/tests/release_sbom_binding.rs index 81eba0061..d99b23979 100644 --- a/crates/originweave-core/tests/release_sbom_binding.rs +++ b/crates/originweave-core/tests/release_sbom_binding.rs @@ -196,16 +196,15 @@ fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() #[test] fn spdx_sbom_binding_rejects_self_description() -> Result<(), Box> { let manifest = release_manifest()?; - let binding = ReleaseSbomBinding::new( - &manifest, - "originweave.spdx.jsonld", - ReleaseSbomFormat::Spdx30JsonLd, - vec!["originweave.spdx.jsonld"], - ); - assert!( - binding.is_err(), - "release SBOM identity must fail closed when the SBOM artifact describes itself" + assert_eq!( + ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + vec!["originweave.spdx.jsonld"], + ), + Err(ReleaseSbomBindingError::SelfDescribedSbomArtifact) ); Ok(()) } @@ -221,6 +220,10 @@ fn spdx_sbom_binding_errors_are_standard_source_free_rust_errors() { ReleaseSbomBindingError::MissingDescribedArtifacts, "release SBOM must describe at least one release artifact", ), + ( + ReleaseSbomBindingError::SelfDescribedSbomArtifact, + "release SBOM must not describe its own artifact", + ), ( ReleaseSbomBindingError::UnknownDescribedArtifact, "release SBOM describes an artifact that is not present in the bound release manifest", From 8a72af122730a485eeec22785c3f5b9ba015e7a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 02:12:44 -0700 Subject: [PATCH 063/170] docs(release): require non-circular SBOM identity --- docs/adr/0016-release-sbom-identity.md | 35 +++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/adr/0016-release-sbom-identity.md b/docs/adr/0016-release-sbom-identity.md index 547351139..f3f772cfc 100644 --- a/docs/adr/0016-release-sbom-identity.md +++ b/docs/adr/0016-release-sbom-identity.md @@ -16,7 +16,8 @@ This slice establishes only the first claim so later SPDX generation and verific - SBOM identity must not drift away from the exact release manifest it supplements. - The SBOM artifact itself must already be an admitted release artifact with a manifest-bound SHA-256 identity. - Every release artifact declared as described by the SBOM must already be admitted by that same manifest. -- Empty, duplicate, foreign, and case-drifted artifact identities must fail closed. +- The SBOM artifact must not be one of its own described artifact identities; OriginWeave requires a non-circular artifact join that later content verification can check independently. +- Empty, duplicate, foreign, self-described, and case-drifted artifact identities must fail closed. - The binding must be deterministic and bounded by the existing release-manifest inventory limit. - Metadata binding must not be confused with SPDX content validation, provenance, signing, publication, installation, update, rollback, or release authority. @@ -25,7 +26,8 @@ This slice establishes only the first claim so later SPDX generation and verific - ADR 0015 remains the owning decision for release-manifest identity admission. - `ReleaseSbomFormat::Spdx30JsonLd` identifies SPDX 3.0.1 serialized as JSON-LD only; it is not a parser or conformance certificate. - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. -- The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase SHA-256 identity. +- The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase SHA-256 identity, and cannot also appear in the binding's described-artifact set. +- The self-description prohibition is an OriginWeave release-integrity rule for a non-circular identity join; it is not presented as a general SPDX conformance requirement. - The binding contains no package/component graph, license conclusions, vulnerability state, provenance attestations, signing keys, credentials, network authority, installer authority, or update authority. - Later SPDX generation and verification must independently validate the actual serialized bytes against the reviewed SPDX schema/ontology and product completeness requirements. @@ -39,13 +41,17 @@ Rejected. An ambient path or URL can drift from the bounded release inventory an Rejected. That would allow the SBOM layer to invent a second release inventory and weaken ADR 0015's canonical artifact identity boundary. +### Allow the SBOM artifact to describe itself + +Rejected for OriginWeave's release identity contract. A self-referential described-artifact set makes the artifact-to-SBOM join circular at the exact-digest boundary that later verification must evaluate. This product boundary therefore requires at least one distinct release artifact and rejects the SBOM artifact itself as a described identity without claiming that SPDX generally forbids such a relationship. + ### Treat successful construction as SPDX conformance Rejected. Binding names and digests cannot establish that the SBOM bytes satisfy SPDX structural/semantic requirements or accurately describe package composition. ### Exact manifest-backed identity binding -Selected. The SBOM artifact and all described artifact names must resolve exactly within the same release manifest, duplicates and empty sets fail closed, and described identities are stored in deterministic lexical order. +Selected. The SBOM artifact and all described artifact names must resolve exactly within the same release manifest, the SBOM artifact cannot describe itself, duplicates and empty sets fail closed, and described identities are stored in deterministic lexical order. ## Decision @@ -54,33 +60,34 @@ OriginWeave adds an inert release-SBOM identity boundary: 1. The supported initial format is `ReleaseSbomFormat::Spdx30JsonLd`, which reports SPDX specification version `3.0.1`. 2. The named SBOM artifact must exactly match an artifact admitted by the same `ReleaseManifest`. 3. At least one described release artifact is required. -4. Every described artifact name must exactly match an artifact admitted by the same manifest. -5. Duplicate described artifact names fail closed. -6. Case-drifted names are not normalized and fail as unknown unless they exactly match the preserved manifest spelling. -7. Described artifact names are stored in deterministic lexical order. -8. The manifest's existing bounded artifact inventory transitively bounds the binding. -9. Validation failures remain typed, deterministic standard Rust errors without hidden fallback. -10. Constructing the binding does not claim SPDX content conformance, generation completeness, artifact authentication, provenance, signatures, publication, installation, update, rollback, or release authority. +4. The SBOM artifact itself cannot be one of its described artifact identities; self-description fails closed with a typed validation error. +5. Every described artifact name must exactly match an artifact admitted by the same manifest. +6. Duplicate described artifact names fail closed. +7. Case-drifted names are not normalized and fail as unknown unless they exactly match the preserved manifest spelling. +8. Described artifact names are stored in deterministic lexical order. +9. The manifest's existing bounded artifact inventory transitively bounds the binding. +10. Validation failures remain typed, deterministic standard Rust errors without hidden fallback. +11. Constructing the binding does not claim SPDX content conformance, generation completeness, artifact authentication, provenance, signatures, publication, installation, update, rollback, or release authority. ## Consequences -Later SPDX tooling receives one explicit join point between SBOM identity and the exact release inventory. It cannot silently describe an artifact that the release manifest did not admit, and buyers can retain the manifest's existing digest identity for the SBOM artifact itself. +Later SPDX tooling receives one explicit non-circular join point between SBOM identity and the exact release inventory. It cannot silently describe an artifact that the release manifest did not admit, cannot treat the SBOM artifact as its own exact described release artifact, and buyers can retain the manifest's existing digest identity for the SBOM artifact itself. This boundary is intentionally narrower than a commercial SBOM generator or verifier. Those later components must consume the actual SBOM bytes, establish SPDX 3.0.1 structural and semantic conformance, verify required software/package content, and preserve exact artifact/provenance linkage without broadening authority. ## Failure and degraded behavior -A missing SBOM artifact, empty described-artifact set, foreign described artifact, duplicate described identity, or case-drifted identity fails closed before a binding is produced. No fallback rewrites a name, drops an artifact, substitutes another manifest entry, or converts an invalid binding into success. +A missing SBOM artifact, empty described-artifact set, self-described SBOM artifact, foreign described artifact, duplicate described identity, or case-drifted identity fails closed before a binding is produced. No fallback rewrites a name, drops an artifact, substitutes another manifest entry, accepts a circular identity, or converts an invalid binding into success. Failure to construct this metadata binding does not authorize release through another path. Conversely, successful construction cannot be used as evidence that SPDX content is valid or complete. ## Security / privacy / governance impact -The boundary reduces supply-chain identity ambiguity by preventing the SBOM layer from creating a second ambient artifact namespace. It introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. Scheduled development agents remain unable to merge, sign, tag, publish, install, or change repository governance. +The boundary reduces supply-chain identity ambiguity by preventing the SBOM layer from creating a second ambient artifact namespace or a circular exact-digest join. It introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. Scheduled development agents remain unable to merge, sign, tag, publish, install, or change repository governance. ## Tests and acceptance evidence -The owning `originweave-core` tests must prove exact manifest-backed SBOM admission, deterministic described-artifact ordering, and fail-closed behavior for missing SBOM identity, empty described sets, foreign artifacts, duplicates, and case drift. Public error text and `std::error::Error` behavior are part of the deterministic contract. +The owning `originweave-core` tests must prove exact manifest-backed SBOM admission, deterministic described-artifact ordering, and typed fail-closed behavior for missing SBOM identity, empty described sets, self-description, foreign artifacts, duplicates, and case drift. Public error text and `std::error::Error` behavior are part of the deterministic contract. Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. Rust formatting, workspace/all-target checks, full tests, strict Clippy, and rustdoc must pass on that same head. Predecessor checks or model-only evidence do not transfer. From 3a39bab39087e3c85a120446ac7432889c2d3151 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 02:13:36 -0700 Subject: [PATCH 064/170] docs(changelog): record SBOM self-description rejection --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92d7b7dfc..9846c3b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- Added an inert SPDX 3.0.1 JSON-LD SBOM identity binding that requires the SBOM artifact and every declared described artifact to be exact members of the same release manifest, rejects empty, foreign, duplicate, and case-drifted artifact identities, and does not claim SPDX content conformance, artifact authentication, provenance, signing, publication, installation, update, rollback, or release authority. +- Added an inert SPDX 3.0.1 JSON-LD SBOM identity binding that requires the SBOM artifact and every declared described artifact to be exact members of the same release manifest, rejects empty, foreign, duplicate, self-described, and case-drifted artifact identities, preserves a non-circular exact-digest join, and does not claim SPDX content conformance, artifact authentication, provenance, signing, publication, installation, update, rollback, or release authority. - Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, the exact repository-pinned Rust 1.97.1 toolchain, exact lowercase dependency-lock SHA-256 evidence, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; build identity remains metadata rather than reproducibility proof, moving toolchain aliases and alternate versions fail closed, and artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. @@ -78,4 +78,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD From 91e7850892f0a513a49bc4fc23eb8237f4f574e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 03:25:08 -0700 Subject: [PATCH 065/170] test(core): require inspectable SBOM identity binding --- .../tests/release_sbom_binding.rs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/originweave-core/tests/release_sbom_binding.rs b/crates/originweave-core/tests/release_sbom_binding.rs index d99b23979..8cd83a306 100644 --- a/crates/originweave-core/tests/release_sbom_binding.rs +++ b/crates/originweave-core/tests/release_sbom_binding.rs @@ -137,6 +137,40 @@ fn spdx_sbom_binding_retains_exact_release_manifest_identity() -> Result<(), Box Ok(()) } +#[test] +fn spdx_sbom_binding_exposes_exact_bound_identities() -> Result<(), Box> { + let manifest = release_manifest()?; + let binding = ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + vec![ + "originweave-native-host.bin", + "originweave-linux-x86_64.tar.zst", + ], + )?; + + assert_eq!(binding.release_manifest(), &manifest); + assert_eq!(binding.described_artifacts().len(), 2); + assert_eq!( + binding.described_artifacts()[0].name(), + "originweave-linux-x86_64.tar.zst" + ); + assert_eq!( + binding.described_artifacts()[0].sha256_digest(), + sha256_digest('a') + ); + assert_eq!( + binding.described_artifacts()[1].name(), + "originweave-native-host.bin" + ); + assert_eq!( + binding.described_artifacts()[1].sha256_digest(), + sha256_digest('b') + ); + Ok(()) +} + #[test] fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() -> Result<(), Box> { From eeff1401e9175c9d37170b8dd2322b4a6939e561 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 03:28:35 -0700 Subject: [PATCH 066/170] fix(core): expose exact SBOM binding identities --- .../originweave-core/src/release_manifest.rs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 6e0b456ee..958ceb8d4 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -254,6 +254,15 @@ impl ReleaseSbomBinding { }) } + /// Return the complete exact release manifest retained by this SBOM binding. + /// + /// This is inert identity evidence for downstream verification and provenance composition. It + /// does not authenticate the manifest, validate SBOM content, or grant release authority. + #[must_use] + pub const fn release_manifest(&self) -> &ReleaseManifest { + &self.release_manifest + } + /// Return the declared SPDX serialization format. #[must_use] pub const fn format(&self) -> ReleaseSbomFormat { @@ -262,7 +271,7 @@ impl ReleaseSbomBinding { /// Return the exact SPDX specification version represented by the declared format. #[must_use] - pub const fn spdx_specification_version(&self) -> &'static str { + pub const fn spdx_specification_version(self) -> &'static str { self.format.spdx_specification_version() } @@ -277,6 +286,16 @@ impl ReleaseSbomBinding { pub fn described_artifact_names(&self) -> &[String] { &self.described_artifact_names } + + /// Return exact described artifact identities in deterministic lexical order. + /// + /// These name-and-digest identities are retained from the same admitted release manifest and + /// are suitable for later verification joins. Their presence alone does not authenticate + /// artifact bytes or prove that the SBOM content describes them. + #[must_use] + pub fn described_artifacts(&self) -> &[ReleaseArtifact] { + &self.described_artifacts + } } /// Validation error for release-SBOM identity binding. From 5de775273646e4c2c804dbb548e74256f5f8c855 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 03:29:29 -0700 Subject: [PATCH 067/170] fix(core): preserve borrowed SBOM version accessor --- crates/originweave-core/src/release_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 958ceb8d4..182cbf362 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -271,7 +271,7 @@ impl ReleaseSbomBinding { /// Return the exact SPDX specification version represented by the declared format. #[must_use] - pub const fn spdx_specification_version(self) -> &'static str { + pub const fn spdx_specification_version(&self) -> &'static str { self.format.spdx_specification_version() } From 4a6825a24f475cce2dd23f1867fb8f960b683e6e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 03:30:20 -0700 Subject: [PATCH 068/170] docs: record inspectable SBOM identity contract --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9846c3b77..c00f79c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- Added an inert SPDX 3.0.1 JSON-LD SBOM identity binding that requires the SBOM artifact and every declared described artifact to be exact members of the same release manifest, rejects empty, foreign, duplicate, self-described, and case-drifted artifact identities, preserves a non-circular exact-digest join, and does not claim SPDX content conformance, artifact authentication, provenance, signing, publication, installation, update, rollback, or release authority. +- Added an inert SPDX 3.0.1 JSON-LD SBOM identity binding that requires the SBOM artifact and every declared described artifact to be exact members of the same release manifest, rejects empty, foreign, duplicate, self-described, and case-drifted artifact identities, preserves a non-circular exact-digest join, exposes the complete retained manifest and exact described artifact identities for downstream verification/provenance composition, and does not claim SPDX content conformance, artifact authentication, provenance, signing, publication, installation, update, rollback, or release authority. - Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, the exact repository-pinned Rust 1.97.1 toolchain, exact lowercase dependency-lock SHA-256 evidence, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; build identity remains metadata rather than reproducibility proof, moving toolchain aliases and alternate versions fail closed, and artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. From e18d00207f89b3f3a63b08fc5297dcd38fe8e8e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 03:37:28 -0700 Subject: [PATCH 069/170] docs: remove unrelated SBOM stack drift --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c00f79c1a..074d1e5c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Added an inert SPDX 3.0.1 JSON-LD SBOM identity binding that requires the SBOM artifact and every declared described artifact to be exact members of the same release manifest, rejects empty, foreign, duplicate, self-described, and case-drifted artifact identities, preserves a non-circular exact-digest join, exposes the complete retained manifest and exact described artifact identities for downstream verification/provenance composition, and does not claim SPDX content conformance, artifact authentication, provenance, signing, publication, installation, update, rollback, or release authority. - Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, the exact repository-pinned Rust 1.97.1 toolchain, exact lowercase dependency-lock SHA-256 evidence, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; build identity remains metadata rather than reproducibility proof, moving toolchain aliases and alternate versions fail closed, and artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. -- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. +- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, browsing context, and canonical origin, so a same-session navigation or port change cannot reuse the grant. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. @@ -57,7 +57,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Shortened, integer, hexadecimal, and legacy octal-looking IPv4 host spellings are rejected so the policy origin cannot diverge from Chromium host interpretation. - IPv4-mapped IPv6 is canonicalized before destination classification and pin comparison so mapped private or loopback addresses cannot bypass IPv4 policy. - The default destination policy permits only public addresses and denies unspecified, loopback, private, shared, link-local, metadata, documentation, benchmarking, multicast, broadcast, transition, and protocol-reserved destinations. -- Azure platform IP `168.63.129.16` and Amazon EKS Pod Identity endpoints `169.254.170.23` and `fd00:ec2::23` are classified as metadata or platform services before broader address-range rules. +- Azure platform IP `168.63.129.16` and Amazon EKS Pod Identity endpoints `169.254.170.23` and `fd00:ec2::23` are classified as metadata or platform services before broader public, link-local, or unique-local rules. - Resolver answers are rejected when empty or larger than 256 addresses, preventing an unbounded resolver response from entering policy state. - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. From c73e07c9fc862b8f70c8e6805f03be113eaa2bac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 03:38:07 -0700 Subject: [PATCH 070/170] docs: restore inherited extension wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074d1e5c3..db89a8cff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Added an inert SPDX 3.0.1 JSON-LD SBOM identity binding that requires the SBOM artifact and every declared described artifact to be exact members of the same release manifest, rejects empty, foreign, duplicate, self-described, and case-drifted artifact identities, preserves a non-circular exact-digest join, exposes the complete retained manifest and exact described artifact identities for downstream verification/provenance composition, and does not claim SPDX content conformance, artifact authentication, provenance, signing, publication, installation, update, rollback, or release authority. - Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, the exact repository-pinned Rust 1.97.1 toolchain, exact lowercase dependency-lock SHA-256 evidence, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; build identity remains metadata rather than reproducibility proof, moving toolchain aliases and alternate versions fail closed, and artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. -- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, browsing context, and canonical origin, so a same-session navigation or port change cannot reuse the grant. +- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. From a9e936818584cb4d2fb62d3fae34ff9bc6d3cba0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 10:03:43 -0700 Subject: [PATCH 071/170] test(core): require complete release SBOM inventory --- .../tests/release_sbom_completeness.rs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 crates/originweave-core/tests/release_sbom_completeness.rs diff --git a/crates/originweave-core/tests/release_sbom_completeness.rs b/crates/originweave-core/tests/release_sbom_completeness.rs new file mode 100644 index 000000000..b7f97f25e --- /dev/null +++ b/crates/originweave-core/tests/release_sbom_completeness.rs @@ -0,0 +1,42 @@ +use std::error::Error; + +use originweave_core::release_manifest::{ + ReleaseArtifact, ReleaseBuildIdentity, ReleaseChannel, ReleaseManifest, ReleaseSbomBinding, + ReleaseSbomFormat, +}; + +const SOURCE_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; +const CHROMIUM_REVISION: &str = "150.0.7871.129"; + +fn sha256_digest(hex_digit: char) -> String { + format!("sha256:{}", hex_digit.to_string().repeat(64)) +} + +#[test] +fn release_sbom_binding_rejects_incomplete_manifest_inventory() -> Result<(), Box> { + let manifest = ReleaseManifest::new( + SOURCE_COMMIT, + CHROMIUM_REVISION, + ReleaseChannel::Stable, + ReleaseBuildIdentity::new("1.97.1", &sha256_digest('9'))?, + vec![ + ReleaseArtifact::new("originweave-linux-x86_64.tar.zst", &sha256_digest('a'))?, + ReleaseArtifact::new("originweave-native-host.bin", &sha256_digest('b'))?, + ReleaseArtifact::new("originweave.spdx.jsonld", &sha256_digest('c'))?, + ], + )?; + + let error = ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + vec!["originweave-linux-x86_64.tar.zst"], + ) + .expect_err("incomplete release SBOM inventory must fail closed"); + + assert_eq!( + error.to_string(), + "release SBOM must describe every non-SBOM release artifact" + ); + Ok(()) +} From 97d9c71d548eebeb251ff7058c245b8a01d7d0a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 10:06:58 -0700 Subject: [PATCH 072/170] fix(core): require complete release SBOM inventory --- .../originweave-core/src/release_manifest.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 182cbf362..9c1b8e049 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -193,14 +193,15 @@ pub struct ReleaseSbomBinding { } impl ReleaseSbomBinding { - /// Bind a declared SPDX SBOM artifact to exact artifacts in one release manifest. + /// Bind a declared SPDX SBOM artifact to every other artifact in one release manifest. /// /// The complete bounded release-manifest identity is retained in the binding. The SBOM /// artifact name and every described artifact name must match that manifest exactly. The SBOM - /// artifact itself cannot be admitted as one of its described artifacts. At least one other - /// described artifact is required, duplicates fail closed, and exact described artifact - /// identities plus their public names are sorted deterministically. The release manifest's own - /// admission bound therefore also bounds this inventory. + /// artifact itself cannot be admitted as one of its described artifacts. Every other manifest + /// artifact must be described exactly once; missing, foreign, self-described, and duplicate + /// entries fail closed. Exact described artifact identities plus their public names are sorted + /// deterministically. The release manifest's own admission bound therefore also bounds this + /// inventory. pub fn new<'a, I>( manifest: &ReleaseManifest, sbom_artifact_name: &str, @@ -239,6 +240,9 @@ impl ReleaseSbomBinding { if described_artifacts.is_empty() { return Err(ReleaseSbomBindingError::MissingDescribedArtifacts); } + if described_artifacts.len() + 1 != manifest.artifacts().len() { + return Err(ReleaseSbomBindingError::IncompleteDescribedArtifacts); + } described_artifacts.sort_by(|left, right| left.name.cmp(&right.name)); let described_artifact_names = described_artifacts .iter() @@ -305,6 +309,8 @@ pub enum ReleaseSbomBindingError { UnknownSbomArtifact, /// No release artifacts were declared as described by the SBOM. MissingDescribedArtifacts, + /// The SBOM omitted at least one non-SBOM artifact from the same release manifest. + IncompleteDescribedArtifacts, /// The SBOM artifact was also declared as an artifact that the SBOM describes. SelfDescribedSbomArtifact, /// A described artifact name is absent from the release manifest. @@ -321,6 +327,9 @@ impl fmt::Display for ReleaseSbomBindingError { Self::MissingDescribedArtifacts => { formatter.write_str("release SBOM must describe at least one release artifact") } + Self::IncompleteDescribedArtifacts => { + formatter.write_str("release SBOM must describe every non-SBOM release artifact") + } Self::SelfDescribedSbomArtifact => { formatter.write_str("release SBOM must not describe its own artifact") } From 70fe74a09bcd14d6d62f6be8356700034825cecc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 10:08:30 -0700 Subject: [PATCH 073/170] docs(adr): require release SBOM inventory completeness --- docs/adr/0016-release-sbom-identity.md | 43 +++++++++++++++----------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/docs/adr/0016-release-sbom-identity.md b/docs/adr/0016-release-sbom-identity.md index f3f772cfc..0028db312 100644 --- a/docs/adr/0016-release-sbom-identity.md +++ b/docs/adr/0016-release-sbom-identity.md @@ -15,9 +15,10 @@ This slice establishes only the first claim so later SPDX generation and verific - SBOM identity must not drift away from the exact release manifest it supplements. - The SBOM artifact itself must already be an admitted release artifact with a manifest-bound SHA-256 identity. +- Every non-SBOM release artifact in the bound manifest must be represented exactly once in the described-artifact inventory; an admitted release artifact cannot silently fall outside the SBOM identity join. - Every release artifact declared as described by the SBOM must already be admitted by that same manifest. - The SBOM artifact must not be one of its own described artifact identities; OriginWeave requires a non-circular artifact join that later content verification can check independently. -- Empty, duplicate, foreign, self-described, and case-drifted artifact identities must fail closed. +- Empty, incomplete, duplicate, foreign, self-described, and case-drifted artifact identities must fail closed. - The binding must be deterministic and bounded by the existing release-manifest inventory limit. - Metadata binding must not be confused with SPDX content validation, provenance, signing, publication, installation, update, rollback, or release authority. @@ -27,6 +28,7 @@ This slice establishes only the first claim so later SPDX generation and verific - `ReleaseSbomFormat::Spdx30JsonLd` identifies SPDX 3.0.1 serialized as JSON-LD only; it is not a parser or conformance certificate. - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. - The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase SHA-256 identity, and cannot also appear in the binding's described-artifact set. +- Every other artifact admitted by that same release manifest must be retained in the described-artifact set exactly once; this is an OriginWeave release-inventory completeness rule, not a general assertion about how SPDX package relationships must be modeled internally. - The self-description prohibition is an OriginWeave release-integrity rule for a non-circular identity join; it is not presented as a general SPDX conformance requirement. - The binding contains no package/component graph, license conclusions, vulnerability state, provenance attestations, signing keys, credentials, network authority, installer authority, or update authority. - Later SPDX generation and verification must independently validate the actual serialized bytes against the reviewed SPDX schema/ontology and product completeness requirements. @@ -41,6 +43,10 @@ Rejected. An ambient path or URL can drift from the bounded release inventory an Rejected. That would allow the SBOM layer to invent a second release inventory and weaken ADR 0015's canonical artifact identity boundary. +### Accept only a selected subset of manifest artifacts + +Rejected. A non-empty manifest-backed subset can look well-formed while silently omitting another release artifact. That would make the identity binding unsuitable as release-level SBOM inventory evidence even though the omitted artifact remains part of the canonical release manifest. OriginWeave therefore requires every non-SBOM manifest artifact to appear exactly once in the described-artifact inventory. + ### Allow the SBOM artifact to describe itself Rejected for OriginWeave's release identity contract. A self-referential described-artifact set makes the artifact-to-SBOM join circular at the exact-digest boundary that later verification must evaluate. This product boundary therefore requires at least one distinct release artifact and rejects the SBOM artifact itself as a described identity without claiming that SPDX generally forbids such a relationship. @@ -51,7 +57,7 @@ Rejected. Binding names and digests cannot establish that the SBOM bytes satisfy ### Exact manifest-backed identity binding -Selected. The SBOM artifact and all described artifact names must resolve exactly within the same release manifest, the SBOM artifact cannot describe itself, duplicates and empty sets fail closed, and described identities are stored in deterministic lexical order. +Selected. The SBOM artifact and every non-SBOM release artifact resolve exactly within the same release manifest, the SBOM artifact cannot describe itself, incomplete, duplicate, and empty described inventories fail closed, and described identities are stored in deterministic lexical order. ## Decision @@ -59,35 +65,36 @@ OriginWeave adds an inert release-SBOM identity boundary: 1. The supported initial format is `ReleaseSbomFormat::Spdx30JsonLd`, which reports SPDX specification version `3.0.1`. 2. The named SBOM artifact must exactly match an artifact admitted by the same `ReleaseManifest`. -3. At least one described release artifact is required. -4. The SBOM artifact itself cannot be one of its described artifact identities; self-description fails closed with a typed validation error. -5. Every described artifact name must exactly match an artifact admitted by the same manifest. -6. Duplicate described artifact names fail closed. -7. Case-drifted names are not normalized and fail as unknown unless they exactly match the preserved manifest spelling. -8. Described artifact names are stored in deterministic lexical order. -9. The manifest's existing bounded artifact inventory transitively bounds the binding. -10. Validation failures remain typed, deterministic standard Rust errors without hidden fallback. -11. Constructing the binding does not claim SPDX content conformance, generation completeness, artifact authentication, provenance, signatures, publication, installation, update, rollback, or release authority. +3. At least one distinct described release artifact is required. +4. Every non-SBOM artifact admitted by the same release manifest must appear exactly once in the described-artifact inventory; omission fails closed with a typed validation error. +5. The SBOM artifact itself cannot be one of its described artifact identities; self-description fails closed with a typed validation error. +6. Every described artifact name must exactly match an artifact admitted by the same manifest. +7. Duplicate described artifact names fail closed. +8. Case-drifted names are not normalized and fail as unknown unless they exactly match the preserved manifest spelling. +9. Described artifact names are stored in deterministic lexical order. +10. The manifest's existing bounded artifact inventory transitively bounds the binding. +11. Validation failures remain typed, deterministic standard Rust errors without hidden fallback. +12. Constructing the binding does not claim SPDX content conformance, package/component completeness inside the serialized SBOM, artifact authentication, provenance, signatures, publication, installation, update, rollback, or release authority. ## Consequences -Later SPDX tooling receives one explicit non-circular join point between SBOM identity and the exact release inventory. It cannot silently describe an artifact that the release manifest did not admit, cannot treat the SBOM artifact as its own exact described release artifact, and buyers can retain the manifest's existing digest identity for the SBOM artifact itself. +Later SPDX tooling receives one explicit non-circular and release-inventory-complete join point between SBOM identity and the exact release inventory. It cannot silently omit a release artifact admitted by the manifest, cannot describe an artifact that the release manifest did not admit, cannot treat the SBOM artifact as its own exact described release artifact, and buyers can retain the manifest's existing digest identity for the SBOM artifact itself. -This boundary is intentionally narrower than a commercial SBOM generator or verifier. Those later components must consume the actual SBOM bytes, establish SPDX 3.0.1 structural and semantic conformance, verify required software/package content, and preserve exact artifact/provenance linkage without broadening authority. +This boundary is intentionally narrower than a commercial SBOM generator or verifier. Those later components must consume the actual SBOM bytes, establish SPDX 3.0.1 structural and semantic conformance, verify required software/package content, and preserve exact artifact/provenance linkage without broadening authority. Release-level artifact-inventory completeness here does not prove package/component completeness inside the SPDX document. ## Failure and degraded behavior -A missing SBOM artifact, empty described-artifact set, self-described SBOM artifact, foreign described artifact, duplicate described identity, or case-drifted identity fails closed before a binding is produced. No fallback rewrites a name, drops an artifact, substitutes another manifest entry, accepts a circular identity, or converts an invalid binding into success. +A missing SBOM artifact, empty or incomplete described-artifact set, self-described SBOM artifact, foreign described artifact, duplicate described identity, or case-drifted identity fails closed before a binding is produced. No fallback rewrites a name, drops an artifact, substitutes another manifest entry, accepts a partial or circular identity inventory, or converts an invalid binding into success. -Failure to construct this metadata binding does not authorize release through another path. Conversely, successful construction cannot be used as evidence that SPDX content is valid or complete. +Failure to construct this metadata binding does not authorize release through another path. Conversely, successful construction cannot be used as evidence that SPDX content is structurally valid, semantically valid, or package/component-complete. ## Security / privacy / governance impact -The boundary reduces supply-chain identity ambiguity by preventing the SBOM layer from creating a second ambient artifact namespace or a circular exact-digest join. It introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. Scheduled development agents remain unable to merge, sign, tag, publish, install, or change repository governance. +The boundary reduces supply-chain identity ambiguity by preventing the SBOM layer from creating a second ambient artifact namespace, silently omitting a release artifact, or creating a circular exact-digest join. It introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. Scheduled development agents remain unable to merge, sign, tag, publish, install, or change repository governance. ## Tests and acceptance evidence -The owning `originweave-core` tests must prove exact manifest-backed SBOM admission, deterministic described-artifact ordering, and typed fail-closed behavior for missing SBOM identity, empty described sets, self-description, foreign artifacts, duplicates, and case drift. Public error text and `std::error::Error` behavior are part of the deterministic contract. +The owning `originweave-core` tests must prove exact manifest-backed SBOM admission, complete coverage of every non-SBOM manifest artifact, deterministic described-artifact ordering, and typed fail-closed behavior for missing SBOM identity, empty or incomplete described sets, self-description, foreign artifacts, duplicates, and case drift. Public error text and `std::error::Error` behavior are part of the deterministic contract. Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. Rust formatting, workspace/all-target checks, full tests, strict Clippy, and rustdoc must pass on that same head. Predecessor checks or model-only evidence do not transfer. @@ -99,7 +106,7 @@ This is a new in-process metadata primitive with no protected-main persisted SBO - Generate SPDX 3.0.1 SBOM content for supported release packages through a separately reviewed deterministic pipeline. - Validate JSON-LD structure and SPDX ontology semantics against the reviewed 3.0.1 resources. -- Define package/component completeness and root-element rules for OriginWeave distribution artifacts. +- Define package/component completeness and root-element rules inside the SPDX document for OriginWeave distribution artifacts. - Bind SBOM content, SLSA provenance, signing identity, timestamps, platform packages, reproducibility evidence, and updater trust without transferring authority from this metadata type. - Complete issue #201's integrated release acceptance and rollback/freeze-protection architecture. From 4bed6b61182390fca32fc3c45b944750961b8f89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 10:09:31 -0700 Subject: [PATCH 074/170] docs(changelog): record complete release SBOM inventory --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db89a8cff..8d20bd1cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- Added an inert SPDX 3.0.1 JSON-LD SBOM identity binding that requires the SBOM artifact and every declared described artifact to be exact members of the same release manifest, rejects empty, foreign, duplicate, self-described, and case-drifted artifact identities, preserves a non-circular exact-digest join, exposes the complete retained manifest and exact described artifact identities for downstream verification/provenance composition, and does not claim SPDX content conformance, artifact authentication, provenance, signing, publication, installation, update, rollback, or release authority. +- Added an inert SPDX 3.0.1 JSON-LD SBOM identity binding that requires the SBOM artifact and every non-SBOM release artifact to be exact members of the same release manifest, rejects empty, incomplete, foreign, duplicate, self-described, and case-drifted artifact identities, preserves a non-circular release-inventory-complete exact-digest join, exposes the complete retained manifest and exact described artifact identities for downstream verification/provenance composition, and does not claim SPDX content conformance, package/component completeness inside the SBOM, artifact authentication, provenance, signing, publication, installation, update, rollback, or release authority. - Added a fail-closed release-manifest identity primitive that binds an exact lowercase source commit, bounded canonical Chromium revision, explicit release channel, the exact repository-pinned Rust 1.97.1 toolchain, exact lowercase dependency-lock SHA-256 evidence, and deterministic bounded artifact leaf names with lowercase SHA-256 digests; build identity remains metadata rather than reproducibility proof, moving toolchain aliases and alternate versions fail closed, and artifact identity is unique under ASCII case folding so case-only names cannot collide on case-insensitive target filesystems, without granting signing, publication, installation, update, rollback, or release authority. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. @@ -78,4 +78,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file From 6879348cf030239408ba58519eb4618b2384b359 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 10:14:40 -0700 Subject: [PATCH 075/170] test(core): satisfy SBOM completeness quality gates --- .../tests/release_sbom_completeness.rs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/originweave-core/tests/release_sbom_completeness.rs b/crates/originweave-core/tests/release_sbom_completeness.rs index b7f97f25e..47aff1534 100644 --- a/crates/originweave-core/tests/release_sbom_completeness.rs +++ b/crates/originweave-core/tests/release_sbom_completeness.rs @@ -2,7 +2,7 @@ use std::error::Error; use originweave_core::release_manifest::{ ReleaseArtifact, ReleaseBuildIdentity, ReleaseChannel, ReleaseManifest, ReleaseSbomBinding, - ReleaseSbomFormat, + ReleaseSbomBindingError, ReleaseSbomFormat, }; const SOURCE_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567"; @@ -26,17 +26,25 @@ fn release_sbom_binding_rejects_incomplete_manifest_inventory() -> Result<(), Bo ], )?; - let error = ReleaseSbomBinding::new( + let complete_binding = ReleaseSbomBinding::new( &manifest, "originweave.spdx.jsonld", ReleaseSbomFormat::Spdx30JsonLd, - vec!["originweave-linux-x86_64.tar.zst"], - ) - .expect_err("incomplete release SBOM inventory must fail closed"); + vec![ + "originweave-linux-x86_64.tar.zst", + "originweave-native-host.bin", + ], + )?; + assert_eq!(complete_binding.described_artifacts().len(), 2); assert_eq!( - error.to_string(), - "release SBOM must describe every non-SBOM release artifact" + ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + vec!["originweave-linux-x86_64.tar.zst"], + ), + Err(ReleaseSbomBindingError::IncompleteDescribedArtifacts) ); Ok(()) } From e0a8d2dcc8f8d602a0848cf77505c134440ac5a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 10:15:21 -0700 Subject: [PATCH 076/170] test(core): cover SBOM completeness error contract --- crates/originweave-core/tests/release_sbom_completeness.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/originweave-core/tests/release_sbom_completeness.rs b/crates/originweave-core/tests/release_sbom_completeness.rs index 47aff1534..5a11ac475 100644 --- a/crates/originweave-core/tests/release_sbom_completeness.rs +++ b/crates/originweave-core/tests/release_sbom_completeness.rs @@ -46,5 +46,9 @@ fn release_sbom_binding_rejects_incomplete_manifest_inventory() -> Result<(), Bo ), Err(ReleaseSbomBindingError::IncompleteDescribedArtifacts) ); + assert_eq!( + ReleaseSbomBindingError::IncompleteDescribedArtifacts.to_string(), + "release SBOM must describe every non-SBOM release artifact" + ); Ok(()) } From 26fa8e80731c0bc30882eff51331dcf9b91faed2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 10:19:36 -0700 Subject: [PATCH 077/170] test(core): cover SBOM completeness in canonical binding suite --- .../originweave-core/tests/release_sbom_binding.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/originweave-core/tests/release_sbom_binding.rs b/crates/originweave-core/tests/release_sbom_binding.rs index 8cd83a306..61cc7a073 100644 --- a/crates/originweave-core/tests/release_sbom_binding.rs +++ b/crates/originweave-core/tests/release_sbom_binding.rs @@ -194,6 +194,15 @@ fn spdx_sbom_binding_fails_closed_on_missing_or_ambiguous_manifest_identity() ), Err(ReleaseSbomBindingError::MissingDescribedArtifacts) ); + assert_eq!( + ReleaseSbomBinding::new( + &manifest, + "originweave.spdx.jsonld", + ReleaseSbomFormat::Spdx30JsonLd, + vec!["originweave-linux-x86_64.tar.zst"], + ), + Err(ReleaseSbomBindingError::IncompleteDescribedArtifacts) + ); assert_eq!( ReleaseSbomBinding::new( &manifest, @@ -254,6 +263,10 @@ fn spdx_sbom_binding_errors_are_standard_source_free_rust_errors() { ReleaseSbomBindingError::MissingDescribedArtifacts, "release SBOM must describe at least one release artifact", ), + ( + ReleaseSbomBindingError::IncompleteDescribedArtifacts, + "release SBOM must describe every non-SBOM release artifact", + ), ( ReleaseSbomBindingError::SelfDescribedSbomArtifact, "release SBOM must not describe its own artifact", From b9881166b2bcd6652d885cd9f4b233ffa25882cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 11:03:34 -0700 Subject: [PATCH 078/170] test(core): require exact SPDX JSON-LD context identity --- crates/originweave-core/tests/release_sbom_format.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 crates/originweave-core/tests/release_sbom_format.rs diff --git a/crates/originweave-core/tests/release_sbom_format.rs b/crates/originweave-core/tests/release_sbom_format.rs new file mode 100644 index 000000000..28e1879a0 --- /dev/null +++ b/crates/originweave-core/tests/release_sbom_format.rs @@ -0,0 +1,12 @@ +use originweave_core::release_manifest::ReleaseSbomFormat; + +#[test] +fn spdx_30_json_ld_exposes_required_global_context_identity() { + let format = ReleaseSbomFormat::Spdx30JsonLd; + + assert_eq!(format.spdx_specification_version(), "3.0.1"); + assert_eq!( + format.json_ld_context_uri(), + "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" + ); +} From 30d77b73e2b7f38a5b03ebda0c41b2facabc263f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 11:07:28 -0700 Subject: [PATCH 079/170] feat(core): bind SPDX JSON-LD context identity --- crates/originweave-core/src/release_manifest.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/originweave-core/src/release_manifest.rs b/crates/originweave-core/src/release_manifest.rs index 9c1b8e049..011225625 100644 --- a/crates/originweave-core/src/release_manifest.rs +++ b/crates/originweave-core/src/release_manifest.rs @@ -173,6 +173,18 @@ impl ReleaseSbomFormat { Self::Spdx30JsonLd => "3.0.1", } } + + /// Return the exact global JSON-LD context URI required by this SPDX format. + /// + /// This versioned identifier is inert serialization metadata for downstream validation and + /// serialization. It does not validate SBOM bytes, authenticate artifacts, or grant release + /// authority. + #[must_use] + pub const fn json_ld_context_uri(self) -> &'static str { + match self { + Self::Spdx30JsonLd => "https://spdx.org/rdf/3.0.1/spdx-context.jsonld", + } + } } /// Inert binding between one release manifest and its declared SPDX SBOM artifact identity. From aec7f6fcee907b3f00a591a22353e5fcc99330b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 11:11:32 -0700 Subject: [PATCH 080/170] docs(adr): bind SPDX 3.0.1 JSON-LD context --- docs/adr/0016-release-sbom-identity.md | 27 ++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/adr/0016-release-sbom-identity.md b/docs/adr/0016-release-sbom-identity.md index 0028db312..5e9fbec8d 100644 --- a/docs/adr/0016-release-sbom-identity.md +++ b/docs/adr/0016-release-sbom-identity.md @@ -7,13 +7,14 @@ Issue #201 requires acquisition-grade release evidence, including a software bill of materials that is tied to the same immutable release identity buyers verify. ADR 0015 deliberately stops at a bounded release-manifest identity and does not claim SBOM content conformance, provenance, signing, publication, installation, update, rollback, or release authority. -SPDX 3.0.1 defines a Software `Sbom` as a collection of SPDX elements describing a single package. SPDX also defines JSON-LD serialization and separate structural and semantic validation requirements. A release system therefore needs to distinguish two different claims: that a release manifest names a particular SBOM artifact and exact release artifacts the SBOM is intended to describe, versus the stronger claim that the SBOM bytes themselves conform to SPDX and accurately describe those artifacts. +SPDX 3.0.1 defines a Software `Sbom` as a collection of SPDX elements describing a single package. SPDX also defines JSON-LD serialization and separate structural and semantic validation requirements. For SPDX 3.0.1 JSON-LD, every document must reference the versioned global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld` at the top level. A release system therefore needs to distinguish serialization identity, the claim that a release manifest names a particular SBOM artifact and exact release artifacts the SBOM is intended to describe, and the stronger claim that the SBOM bytes themselves conform to SPDX and accurately describe those artifacts. -This slice establishes only the first claim so later SPDX generation and verification can compose without inventing artifact identities outside the release manifest. +This slice establishes the first two identity claims only so later SPDX generation and verification can compose without inventing serialization or artifact identities outside the release manifest. ## Decision drivers - SBOM identity must not drift away from the exact release manifest it supplements. +- The supported SPDX JSON-LD format must expose its exact specification version and required versioned global context identity rather than rely on an ambient or moving context. - The SBOM artifact itself must already be an admitted release artifact with a manifest-bound SHA-256 identity. - Every non-SBOM release artifact in the bound manifest must be represented exactly once in the described-artifact inventory; an admitted release artifact cannot silently fall outside the SBOM identity join. - Every release artifact declared as described by the SBOM must already be admitted by that same manifest. @@ -25,13 +26,13 @@ This slice establishes only the first claim so later SPDX generation and verific ## Assumptions and authority boundaries - ADR 0015 remains the owning decision for release-manifest identity admission. -- `ReleaseSbomFormat::Spdx30JsonLd` identifies SPDX 3.0.1 serialized as JSON-LD only; it is not a parser or conformance certificate. +- `ReleaseSbomFormat::Spdx30JsonLd` identifies SPDX 3.0.1 serialized as JSON-LD and exposes the exact required global context URI `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`; these are inert serialization identities, not a parser or conformance certificate. - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. - The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase SHA-256 identity, and cannot also appear in the binding's described-artifact set. - Every other artifact admitted by that same release manifest must be retained in the described-artifact set exactly once; this is an OriginWeave release-inventory completeness rule, not a general assertion about how SPDX package relationships must be modeled internally. - The self-description prohibition is an OriginWeave release-integrity rule for a non-circular identity join; it is not presented as a general SPDX conformance requirement. - The binding contains no package/component graph, license conclusions, vulnerability state, provenance attestations, signing keys, credentials, network authority, installer authority, or update authority. -- Later SPDX generation and verification must independently validate the actual serialized bytes against the reviewed SPDX schema/ontology and product completeness requirements. +- Later SPDX generation and verification must independently emit the required top-level JSON-LD context reference and validate the actual serialized bytes against the reviewed SPDX schema/ontology and product completeness requirements. ## Options considered @@ -57,13 +58,13 @@ Rejected. Binding names and digests cannot establish that the SBOM bytes satisfy ### Exact manifest-backed identity binding -Selected. The SBOM artifact and every non-SBOM release artifact resolve exactly within the same release manifest, the SBOM artifact cannot describe itself, incomplete, duplicate, and empty described inventories fail closed, and described identities are stored in deterministic lexical order. +Selected. The supported format exposes exact SPDX 3.0.1 serialization identity, while the SBOM artifact and every non-SBOM release artifact resolve exactly within the same release manifest. The SBOM artifact cannot describe itself, incomplete, duplicate, and empty described inventories fail closed, and described identities are stored in deterministic lexical order. ## Decision OriginWeave adds an inert release-SBOM identity boundary: -1. The supported initial format is `ReleaseSbomFormat::Spdx30JsonLd`, which reports SPDX specification version `3.0.1`. +1. The supported initial format is `ReleaseSbomFormat::Spdx30JsonLd`, which reports SPDX specification version `3.0.1` and exact global JSON-LD context URI `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`. 2. The named SBOM artifact must exactly match an artifact admitted by the same `ReleaseManifest`. 3. At least one distinct described release artifact is required. 4. Every non-SBOM artifact admitted by the same release manifest must appear exactly once in the described-artifact inventory; omission fails closed with a typed validation error. @@ -78,23 +79,23 @@ OriginWeave adds an inert release-SBOM identity boundary: ## Consequences -Later SPDX tooling receives one explicit non-circular and release-inventory-complete join point between SBOM identity and the exact release inventory. It cannot silently omit a release artifact admitted by the manifest, cannot describe an artifact that the release manifest did not admit, cannot treat the SBOM artifact as its own exact described release artifact, and buyers can retain the manifest's existing digest identity for the SBOM artifact itself. +Later SPDX tooling receives one explicit versioned serialization identity plus one non-circular and release-inventory-complete join point between SBOM identity and the exact release inventory. It does not need to guess which JSON-LD context belongs to `Spdx30JsonLd`, cannot silently omit a release artifact admitted by the manifest, cannot describe an artifact that the release manifest did not admit, cannot treat the SBOM artifact as its own exact described release artifact, and buyers can retain the manifest's existing digest identity for the SBOM artifact itself. -This boundary is intentionally narrower than a commercial SBOM generator or verifier. Those later components must consume the actual SBOM bytes, establish SPDX 3.0.1 structural and semantic conformance, verify required software/package content, and preserve exact artifact/provenance linkage without broadening authority. Release-level artifact-inventory completeness here does not prove package/component completeness inside the SPDX document. +This boundary is intentionally narrower than a commercial SBOM generator or verifier. Those later components must consume the actual SBOM bytes, emit and verify the required top-level JSON-LD context reference, establish SPDX 3.0.1 structural and semantic conformance, verify required software/package content, and preserve exact artifact/provenance linkage without broadening authority. Release-level artifact-inventory completeness or exposure of the required context URI here does not prove package/component completeness or content conformance inside the SPDX document. ## Failure and degraded behavior A missing SBOM artifact, empty or incomplete described-artifact set, self-described SBOM artifact, foreign described artifact, duplicate described identity, or case-drifted identity fails closed before a binding is produced. No fallback rewrites a name, drops an artifact, substitutes another manifest entry, accepts a partial or circular identity inventory, or converts an invalid binding into success. -Failure to construct this metadata binding does not authorize release through another path. Conversely, successful construction cannot be used as evidence that SPDX content is structurally valid, semantically valid, or package/component-complete. +Failure to construct this metadata binding does not authorize release through another path. Conversely, successful construction or access to the versioned JSON-LD context identity cannot be used as evidence that SPDX content is structurally valid, semantically valid, or package/component-complete. ## Security / privacy / governance impact -The boundary reduces supply-chain identity ambiguity by preventing the SBOM layer from creating a second ambient artifact namespace, silently omitting a release artifact, or creating a circular exact-digest join. It introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. Scheduled development agents remain unable to merge, sign, tag, publish, install, or change repository governance. +The boundary reduces supply-chain identity ambiguity by preventing both an ambient JSON-LD context choice and the SBOM layer from creating a second ambient artifact namespace, silently omitting a release artifact, or creating a circular exact-digest join. It introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. Scheduled development agents remain unable to merge, sign, tag, publish, install, or change repository governance. ## Tests and acceptance evidence -The owning `originweave-core` tests must prove exact manifest-backed SBOM admission, complete coverage of every non-SBOM manifest artifact, deterministic described-artifact ordering, and typed fail-closed behavior for missing SBOM identity, empty or incomplete described sets, self-description, foreign artifacts, duplicates, and case drift. Public error text and `std::error::Error` behavior are part of the deterministic contract. +The owning `originweave-core` tests must prove exact SPDX specification/context identity, exact manifest-backed SBOM admission, complete coverage of every non-SBOM manifest artifact, deterministic described-artifact ordering, and typed fail-closed behavior for missing SBOM identity, empty or incomplete described sets, self-description, foreign artifacts, duplicates, and case drift. Public error text and `std::error::Error` behavior are part of the deterministic contract. Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. Rust formatting, workspace/all-target checks, full tests, strict Clippy, and rustdoc must pass on that same head. Predecessor checks or model-only evidence do not transfer. @@ -105,7 +106,7 @@ This is a new in-process metadata primitive with no protected-main persisted SBO ## Open follow-ups - Generate SPDX 3.0.1 SBOM content for supported release packages through a separately reviewed deterministic pipeline. -- Validate JSON-LD structure and SPDX ontology semantics against the reviewed 3.0.1 resources. +- Emit and validate the required top-level SPDX 3.0.1 global JSON-LD context reference, then validate JSON-LD structure and SPDX ontology semantics against the reviewed 3.0.1 resources. - Define package/component completeness and root-element rules inside the SPDX document for OriginWeave distribution artifacts. - Bind SBOM content, SLSA provenance, signing identity, timestamps, platform packages, reproducibility evidence, and updater trust without transferring authority from this metadata type. - Complete issue #201's integrated release acceptance and rollback/freeze-protection architecture. @@ -117,3 +118,5 @@ Supersede this ADR when a versioned external release/SBOM specification replaces ## References SPDX Workgroup. (2026). *SPDX specification 3.0.1*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/ + +SPDX Workgroup. (2026). *SPDX specification 3.0.1: Model and serializations*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/serializations/ From 19ac78fbdb4ffd88366ff68e49f3d7158762678e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 12:13:06 -0700 Subject: [PATCH 081/170] test(release): require bounded SPDX JSON-LD envelope verification --- ...t_release_spdx_jsonld_envelope_contract.py | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 tests/test_release_spdx_jsonld_envelope_contract.py diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py new file mode 100644 index 000000000..341a6fe34 --- /dev/null +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -0,0 +1,133 @@ +"""Fail-closed contract tests for bounded SPDX 3.0.1 JSON-LD envelope verification.""" + +from __future__ import annotations + +import json +import pathlib +import runpy +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +VALIDATOR = ROOT / "scripts" / "release" / "validate_spdx_jsonld.py" +CONTEXT = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" + + +class ReleaseSpdxJsonLdEnvelopeContractTests(unittest.TestCase): + """Verify exact serialization identity without promoting partial checks to SPDX conformance.""" + + @classmethod + def setUpClass(cls) -> None: + cls.namespace = runpy.run_path(str(VALIDATOR), run_name="release_spdx_envelope_contract") + cls.validate = cls.namespace["validate_spdx_3_0_1_jsonld_bytes"] + cls.error_type = cls.namespace["SpdxJsonLdEnvelopeError"] + cls.max_bytes = cls.namespace["MAX_SPDX_JSONLD_BYTES"] + cls.max_graph_objects = cls.namespace["MAX_SPDX_GRAPH_OBJECTS"] + + @staticmethod + def _payload(graph: list[object], *, context: object = CONTEXT) -> bytes: + return json.dumps( + {"@context": context, "@graph": graph}, + separators=(",", ":"), + ensure_ascii=True, + ).encode("utf-8") + + def _assert_error_code(self, payload: bytes, expected_code: str) -> Exception: + with self.assertRaises(self.error_type) as captured: + self.validate(payload) + self.assertEqual(captured.exception.code, expected_code) + return captured.exception + + def test_exact_context_and_single_document_are_admitted(self) -> None: + summary = self.validate( + self._payload( + [ + {"type": "CreationInfo", "@id": "_:creation", "specVersion": "3.0.1"}, + { + "type": "SpdxDocument", + "spdxId": "https://example.invalid/spdx/document", + "creationInfo": "_:creation", + }, + ] + ) + ) + + self.assertEqual(summary["context"], CONTEXT) + self.assertEqual(summary["graph_object_count"], 2) + self.assertEqual(summary["spdx_document_count"], 1) + + def test_context_must_match_exact_spdx_3_0_1_identity(self) -> None: + self._assert_error_code( + self._payload( + [{"type": "SpdxDocument"}], + context="https://spdx.org/rdf/3.1/spdx-context.jsonld", + ), + "invalid_context", + ) + + def test_top_level_object_must_contain_only_context_and_graph(self) -> None: + payload = json.dumps( + {"@context": CONTEXT, "@graph": [{"type": "SpdxDocument"}], "extra": True}, + separators=(",", ":"), + ).encode("utf-8") + self._assert_error_code(payload, "invalid_top_level") + + def test_duplicate_json_keys_fail_closed(self) -> None: + payload = ( + '{"@context":"' + + CONTEXT + + '","@context":"' + + CONTEXT + + '","@graph":[{"type":"SpdxDocument"}]}' + ).encode("utf-8") + self._assert_error_code(payload, "duplicate_key") + + def test_graph_must_be_bounded_object_array(self) -> None: + self._assert_error_code( + self._payload([{"type": "SpdxDocument"}, "not-an-object"]), + "invalid_graph_object", + ) + oversized_graph = [{"type": "Package"}] * (self.max_graph_objects + 1) + self._assert_error_code(self._payload(oversized_graph), "too_many_graph_objects") + + def test_exactly_one_spdx_document_is_required(self) -> None: + self._assert_error_code(self._payload([{"type": "Package"}]), "invalid_document_count") + self._assert_error_code( + self._payload( + [ + {"type": "SpdxDocument"}, + {"type": "SpdxDocument"}, + ] + ), + "invalid_document_count", + ) + + def test_invalid_utf8_and_nonfinite_json_fail_closed(self) -> None: + self._assert_error_code(b"\xff", "invalid_utf8") + payload = ( + '{"@context":"' + + CONTEXT + + '","@graph":[{"type":"SpdxDocument","score":NaN}]}' + ).encode("utf-8") + self._assert_error_code(payload, "invalid_json") + + def test_empty_and_oversized_payloads_fail_before_semantic_use(self) -> None: + self._assert_error_code(b"", "invalid_size") + self._assert_error_code(b" " * (self.max_bytes + 1), "invalid_size") + + def test_external_bytes_are_never_reflected_in_errors(self) -> None: + marker = "buyer-secret-marker-must-not-reach-release-diagnostics" + payload = self._payload( + [ + { + "type": "SpdxDocument", + "unexpected": marker, + } + ], + context=marker, + ) + error = self._assert_error_code(payload, "invalid_context") + self.assertNotIn(marker, str(error)) + + +if __name__ == "__main__": + unittest.main() From 9d01ded6a61d9930b4e08427be55a128a6132c0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 12:14:33 -0700 Subject: [PATCH 082/170] feat(release): validate bounded SPDX JSON-LD envelope --- scripts/release/validate_spdx_jsonld.py | 138 ++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 scripts/release/validate_spdx_jsonld.py diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py new file mode 100644 index 000000000..eae5f3853 --- /dev/null +++ b/scripts/release/validate_spdx_jsonld.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Bounded, fail-closed SPDX 3.0.1 JSON-LD envelope verification. + +This verifier deliberately checks only the serialization envelope needed before deeper +schema/ontology validation: exact versioned global context identity, a bounded top-level +JSON object, a bounded object-only ``@graph``, and exactly one ``SpdxDocument`` element. +It does not claim full SPDX structural or semantic conformance, artifact authenticity, +SBOM completeness, provenance, signing, publication, installation, update, or rollback +authority. +""" + +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +from collections.abc import Iterable +from typing import Any + +SPDX_3_0_1_CONTEXT = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" +MAX_SPDX_JSONLD_BYTES = 16 * 1024 * 1024 +MAX_SPDX_GRAPH_OBJECTS = 65_536 + + +class SpdxJsonLdEnvelopeError(ValueError): + """Typed, value-redacted failure for an SPDX JSON-LD envelope check.""" + + def __init__(self, code: str) -> None: + self.code = code + super().__init__(f"SPDX JSON-LD envelope validation failed: {code}") + + +class _DuplicateJsonKey(ValueError): + """Internal sentinel used to distinguish duplicate JSON object members.""" + + +def _object_without_duplicates(pairs: Iterable[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise _DuplicateJsonKey + result[key] = value + return result + + +def _reject_nonfinite_constant(_value: str) -> None: + raise ValueError("non-finite JSON number") + + +def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: + """Validate the bounded SPDX 3.0.1 JSON-LD serialization envelope. + + External document bytes are never included in raised errors. Successful validation + proves only the narrow envelope contract documented by this module; callers must still + perform the official SPDX JSON Schema and OWL/SHACL validation before claiming SPDX + conformance. + """ + + if not isinstance(payload, bytes) or not payload or len(payload) > MAX_SPDX_JSONLD_BYTES: + raise SpdxJsonLdEnvelopeError("invalid_size") + + try: + text = payload.decode("utf-8", errors="strict") + except UnicodeDecodeError as error: + raise SpdxJsonLdEnvelopeError("invalid_utf8") from error + + try: + decoded = json.loads( + text, + object_pairs_hook=_object_without_duplicates, + parse_constant=_reject_nonfinite_constant, + ) + except _DuplicateJsonKey as error: + raise SpdxJsonLdEnvelopeError("duplicate_key") from error + except (json.JSONDecodeError, ValueError) as error: + raise SpdxJsonLdEnvelopeError("invalid_json") from error + + if not isinstance(decoded, dict) or set(decoded) != {"@context", "@graph"}: + raise SpdxJsonLdEnvelopeError("invalid_top_level") + if decoded["@context"] != SPDX_3_0_1_CONTEXT: + raise SpdxJsonLdEnvelopeError("invalid_context") + + graph = decoded["@graph"] + if not isinstance(graph, list): + raise SpdxJsonLdEnvelopeError("invalid_graph") + if len(graph) > MAX_SPDX_GRAPH_OBJECTS: + raise SpdxJsonLdEnvelopeError("too_many_graph_objects") + + document_count = 0 + for element in graph: + if not isinstance(element, dict) or not isinstance(element.get("type"), str): + raise SpdxJsonLdEnvelopeError("invalid_graph_object") + if element["type"] == "SpdxDocument": + document_count += 1 + + if document_count != 1: + raise SpdxJsonLdEnvelopeError("invalid_document_count") + + return { + "context": SPDX_3_0_1_CONTEXT, + "graph_object_count": len(graph), + "spdx_document_count": document_count, + } + + +def _read_bounded(path: pathlib.Path) -> bytes: + try: + with path.open("rb") as source: + payload = source.read(MAX_SPDX_JSONLD_BYTES + 1) + except OSError as error: + raise SpdxJsonLdEnvelopeError("read_failed") from error + if not payload or len(payload) > MAX_SPDX_JSONLD_BYTES: + raise SpdxJsonLdEnvelopeError("invalid_size") + return payload + + +def main(argv: list[str] | None = None) -> int: + """Validate one local SPDX JSON-LD envelope without echoing untrusted document bytes.""" + + parser = argparse.ArgumentParser( + description="Validate a bounded SPDX 3.0.1 JSON-LD serialization envelope." + ) + parser.add_argument("document", type=pathlib.Path) + arguments = parser.parse_args(argv) + + try: + summary = validate_spdx_3_0_1_jsonld_bytes(_read_bounded(arguments.document)) + except SpdxJsonLdEnvelopeError as error: + print(error, file=sys.stderr) + return 2 + + print(json.dumps(summary, sort_keys=True, separators=(",", ":"))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From b6bdb931da851a7d590af845f3c0b403e1351ac8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 12:16:01 -0700 Subject: [PATCH 083/170] test(release): keep validator callable unbound --- tests/test_release_spdx_jsonld_envelope_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 341a6fe34..51af9a78e 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -18,7 +18,7 @@ class ReleaseSpdxJsonLdEnvelopeContractTests(unittest.TestCase): @classmethod def setUpClass(cls) -> None: cls.namespace = runpy.run_path(str(VALIDATOR), run_name="release_spdx_envelope_contract") - cls.validate = cls.namespace["validate_spdx_3_0_1_jsonld_bytes"] + cls.validate = staticmethod(cls.namespace["validate_spdx_3_0_1_jsonld_bytes"]) cls.error_type = cls.namespace["SpdxJsonLdEnvelopeError"] cls.max_bytes = cls.namespace["MAX_SPDX_JSONLD_BYTES"] cls.max_graph_objects = cls.namespace["MAX_SPDX_GRAPH_OBJECTS"] From 4507701ae97a6b060a860391d8be6551f8c16c57 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 12:18:38 -0700 Subject: [PATCH 084/170] docs(adr): bind SPDX envelope verification limits --- docs/adr/0016-release-sbom-identity.md | 118 ++++++++++++++----------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/docs/adr/0016-release-sbom-identity.md b/docs/adr/0016-release-sbom-identity.md index 5e9fbec8d..b62d287b3 100644 --- a/docs/adr/0016-release-sbom-identity.md +++ b/docs/adr/0016-release-sbom-identity.md @@ -1,119 +1,137 @@ -# ADR 0016: Release SBOM identity binding +# ADR 0016: Release SBOM identity binding and bounded JSON-LD envelope verification - Status: Proposed - Date: 2026-08-23 ## Context -Issue #201 requires acquisition-grade release evidence, including a software bill of materials that is tied to the same immutable release identity buyers verify. ADR 0015 deliberately stops at a bounded release-manifest identity and does not claim SBOM content conformance, provenance, signing, publication, installation, update, rollback, or release authority. +Issue #201 requires acquisition-grade release evidence, including a software bill of materials tied to the same immutable release identity buyers verify. ADR 0015 deliberately stops at a bounded release-manifest identity and does not claim SBOM content conformance, provenance, signing, publication, installation, update, rollback, or release authority. -SPDX 3.0.1 defines a Software `Sbom` as a collection of SPDX elements describing a single package. SPDX also defines JSON-LD serialization and separate structural and semantic validation requirements. For SPDX 3.0.1 JSON-LD, every document must reference the versioned global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld` at the top level. A release system therefore needs to distinguish serialization identity, the claim that a release manifest names a particular SBOM artifact and exact release artifacts the SBOM is intended to describe, and the stronger claim that the SBOM bytes themselves conform to SPDX and accurately describe those artifacts. +SPDX 3.0.1 defines JSON-LD serialization and separate structural and semantic validation requirements. Every SPDX 3.0.1 JSON-LD document must reference the versioned global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld` at the top level. A serialization must not define more than one `SpdxDocument`. Full SPDX JSON-LD conformance additionally requires structural validation against the versioned SPDX JSON Schema and semantic validation against the SPDX OWL ontology/SHACL constraints. -This slice establishes the first two identity claims only so later SPDX generation and verification can compose without inventing serialization or artifact identities outside the release manifest. +OriginWeave therefore separates four claims that must not be collapsed: + +1. the declared SPDX serialization identity; +2. the immutable release-manifest/SBOM artifact identity join; +3. a narrow, bounded check of the actual JSON-LD serialization envelope; and +4. full SPDX content conformance and product-specific package/component completeness. + +This slice implements claims 1 through 3 only. Claim 4 remains a later independently reviewed release gate. ## Decision drivers - SBOM identity must not drift away from the exact release manifest it supplements. - The supported SPDX JSON-LD format must expose its exact specification version and required versioned global context identity rather than rely on an ambient or moving context. +- Actual serialized bytes must not be accepted merely because metadata says they are SPDX 3.0.1 JSON-LD. +- Envelope parsing must be bounded before deeper semantic use and reject malformed UTF-8, malformed JSON, duplicate JSON object members, non-finite JSON constants, unexpected top-level members, non-object graph entries, excessive graph cardinality, and zero or multiple `SpdxDocument` objects. +- Error diagnostics must not reflect attacker- or supplier-controlled document bytes. - The SBOM artifact itself must already be an admitted release artifact with a manifest-bound SHA-256 identity. -- Every non-SBOM release artifact in the bound manifest must be represented exactly once in the described-artifact inventory; an admitted release artifact cannot silently fall outside the SBOM identity join. -- Every release artifact declared as described by the SBOM must already be admitted by that same manifest. -- The SBOM artifact must not be one of its own described artifact identities; OriginWeave requires a non-circular artifact join that later content verification can check independently. +- Every non-SBOM release artifact in the bound manifest must be represented exactly once in the described-artifact inventory. - Empty, incomplete, duplicate, foreign, self-described, and case-drifted artifact identities must fail closed. -- The binding must be deterministic and bounded by the existing release-manifest inventory limit. -- Metadata binding must not be confused with SPDX content validation, provenance, signing, publication, installation, update, rollback, or release authority. +- Partial envelope validation must not be represented as SPDX JSON Schema/OWL/SHACL conformance, package completeness, provenance, signing, publication, installation, update, rollback, or release authority. ## Assumptions and authority boundaries - ADR 0015 remains the owning decision for release-manifest identity admission. -- `ReleaseSbomFormat::Spdx30JsonLd` identifies SPDX 3.0.1 serialized as JSON-LD and exposes the exact required global context URI `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`; these are inert serialization identities, not a parser or conformance certificate. +- `ReleaseSbomFormat::Spdx30JsonLd` identifies SPDX 3.0.1 serialized as JSON-LD and exposes the exact required global context URI. - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. - The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase SHA-256 identity, and cannot also appear in the binding's described-artifact set. -- Every other artifact admitted by that same release manifest must be retained in the described-artifact set exactly once; this is an OriginWeave release-inventory completeness rule, not a general assertion about how SPDX package relationships must be modeled internally. -- The self-description prohibition is an OriginWeave release-integrity rule for a non-circular identity join; it is not presented as a general SPDX conformance requirement. -- The binding contains no package/component graph, license conclusions, vulnerability state, provenance attestations, signing keys, credentials, network authority, installer authority, or update authority. -- Later SPDX generation and verification must independently emit the required top-level JSON-LD context reference and validate the actual serialized bytes against the reviewed SPDX schema/ontology and product completeness requirements. +- Every other artifact admitted by that release manifest must be retained in the described-artifact set exactly once. This is an OriginWeave release-inventory completeness rule, not a general SPDX modeling assertion. +- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes only for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, requires exactly the top-level `@context` and `@graph` members, requires the exact SPDX 3.0.1 global context string, bounds the graph to 65,536 objects, requires each graph entry to be an object with a string `type`, and requires exactly one object whose type is `SpdxDocument`. +- The envelope verifier deliberately does not fetch remote contexts or schemas, resolve external identifiers, authenticate artifacts, validate the SPDX JSON Schema, evaluate the SPDX ontology/SHACL constraints, infer package completeness, or grant any signing/release/update authority. +- Full conformance still requires validation of the same serialized bytes against reviewed SPDX 3.0.1 structural and semantic resources plus OriginWeave product-completeness rules. ## Options considered -### Store an arbitrary SBOM path or URL next to the manifest +### Trust the declared SBOM format without reading bytes -Rejected. An ambient path or URL can drift from the bounded release inventory and does not guarantee that the SBOM itself is one of the artifacts buyers can hash against the manifest. +Rejected. A manifest can correctly identify an artifact while the artifact bytes contain the wrong context, malformed JSON, an ambiguous duplicate-key object, or multiple `SpdxDocument` definitions. -### Accept described artifact names that are not in the manifest +### Treat a general-purpose JSON parse as SPDX conformance -Rejected. That would allow the SBOM layer to invent a second release inventory and weaken ADR 0015's canonical artifact identity boundary. +Rejected. JSON syntax alone does not establish the required SPDX context, document cardinality, JSON Schema validity, ontology/SHACL validity, or SBOM completeness. -### Accept only a selected subset of manifest artifacts +### Fetch the JSON-LD context and schema during the first verifier -Rejected. A non-empty manifest-backed subset can look well-formed while silently omitting another release artifact. That would make the identity binding unsuitable as release-level SBOM inventory evidence even though the omitted artifact remains part of the canonical release manifest. OriginWeave therefore requires every non-SBOM manifest artifact to appear exactly once in the described-artifact inventory. +Rejected. Ambient network retrieval would introduce moving external authority and availability into a release gate before pinned schema/ontology identities and offline verification are designed. The first verifier uses fixed local constants and no network access. -### Allow the SBOM artifact to describe itself +### Store an arbitrary SBOM path or URL next to the manifest -Rejected for OriginWeave's release identity contract. A self-referential described-artifact set makes the artifact-to-SBOM join circular at the exact-digest boundary that later verification must evaluate. This product boundary therefore requires at least one distinct release artifact and rejects the SBOM artifact itself as a described identity without claiming that SPDX generally forbids such a relationship. +Rejected. An ambient path or URL can drift from the bounded release inventory and does not guarantee that the SBOM itself is one of the artifacts buyers can hash against the manifest. + +### Accept only a selected subset of manifest artifacts -### Treat successful construction as SPDX conformance +Rejected. A non-empty manifest-backed subset can look well formed while silently omitting another release artifact. OriginWeave therefore requires every non-SBOM manifest artifact to appear exactly once in the described-artifact inventory. -Rejected. Binding names and digests cannot establish that the SBOM bytes satisfy SPDX structural/semantic requirements or accurately describe package composition. +### Allow the SBOM artifact to describe itself -### Exact manifest-backed identity binding +Rejected for OriginWeave's release identity contract. A self-referential described-artifact set makes the artifact-to-SBOM join circular at the exact-digest boundary later verification must evaluate. This is an OriginWeave integrity rule, not a claim that SPDX generally forbids such a relationship. -Selected. The supported format exposes exact SPDX 3.0.1 serialization identity, while the SBOM artifact and every non-SBOM release artifact resolve exactly within the same release manifest. The SBOM artifact cannot describe itself, incomplete, duplicate, and empty described inventories fail closed, and described identities are stored in deterministic lexical order. +### Layered manifest binding plus bounded envelope verification + +Selected. Metadata establishes exact release/SBOM identity; the bounded verifier checks the first non-ambient properties of the actual serialized bytes; later gates remain responsible for full SPDX and product conformance. ## Decision -OriginWeave adds an inert release-SBOM identity boundary: +OriginWeave adopts the following release-SBOM boundary: -1. The supported initial format is `ReleaseSbomFormat::Spdx30JsonLd`, which reports SPDX specification version `3.0.1` and exact global JSON-LD context URI `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`. +1. The initial format is `ReleaseSbomFormat::Spdx30JsonLd`, reporting SPDX specification version `3.0.1` and global context URI `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`. 2. The named SBOM artifact must exactly match an artifact admitted by the same `ReleaseManifest`. 3. At least one distinct described release artifact is required. -4. Every non-SBOM artifact admitted by the same release manifest must appear exactly once in the described-artifact inventory; omission fails closed with a typed validation error. -5. The SBOM artifact itself cannot be one of its described artifact identities; self-description fails closed with a typed validation error. -6. Every described artifact name must exactly match an artifact admitted by the same manifest. -7. Duplicate described artifact names fail closed. -8. Case-drifted names are not normalized and fail as unknown unless they exactly match the preserved manifest spelling. -9. Described artifact names are stored in deterministic lexical order. -10. The manifest's existing bounded artifact inventory transitively bounds the binding. -11. Validation failures remain typed, deterministic standard Rust errors without hidden fallback. -12. Constructing the binding does not claim SPDX content conformance, package/component completeness inside the serialized SBOM, artifact authentication, provenance, signatures, publication, installation, update, rollback, or release authority. +4. Every non-SBOM artifact admitted by the same release manifest must appear exactly once in the described-artifact inventory. +5. The SBOM artifact itself cannot be one of its described artifact identities. +6. Foreign, duplicate, incomplete, and case-drifted described artifact identities fail closed. +7. Described artifact identities are stored deterministically and the manifest's existing bound transitively limits the inventory. +8. Before deeper SPDX processing, actual candidate JSON-LD bytes may pass through `validate_spdx_3_0_1_jsonld_bytes`. +9. That verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB, rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, requires the exact versioned context, allows at most 65,536 graph objects, requires object entries with string `type`, and requires exactly one `SpdxDocument`. +10. Validation errors expose stable error codes and generic diagnostics without reflecting document bytes. +11. Envelope success is not a certificate of SPDX JSON Schema validity, semantic ontology/SHACL validity, package/component completeness, artifact authenticity, provenance, signatures, publication, installation, update, rollback, or release authority. +12. No verifier fallback may substitute another SPDX version, relax the context, discard duplicate keys, silently truncate a graph, fetch an ambient remote resource, or convert malformed content into success. ## Consequences -Later SPDX tooling receives one explicit versioned serialization identity plus one non-circular and release-inventory-complete join point between SBOM identity and the exact release inventory. It does not need to guess which JSON-LD context belongs to `Spdx30JsonLd`, cannot silently omit a release artifact admitted by the manifest, cannot describe an artifact that the release manifest did not admit, cannot treat the SBOM artifact as its own exact described release artifact, and buyers can retain the manifest's existing digest identity for the SBOM artifact itself. +Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over actual bytes. A candidate cannot pass this layer with the wrong SPDX context, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, or zero/multiple `SpdxDocument` objects. -This boundary is intentionally narrower than a commercial SBOM generator or verifier. Those later components must consume the actual SBOM bytes, emit and verify the required top-level JSON-LD context reference, establish SPDX 3.0.1 structural and semantic conformance, verify required software/package content, and preserve exact artifact/provenance linkage without broadening authority. Release-level artifact-inventory completeness or exposure of the required context URI here does not prove package/component completeness or content conformance inside the SPDX document. +The boundary remains intentionally narrower than a commercial SBOM generator or complete verifier. The official SPDX 3.0.1 specification requires structural validation against the JSON Schema and semantic validation against the OWL ontology/SHACL constraints; those checks, package/component completeness, root-element rules, artifact digest verification, provenance, signing, and integrated release acceptance remain separate work. Envelope success must never be presented as full SPDX conformance. ## Failure and degraded behavior -A missing SBOM artifact, empty or incomplete described-artifact set, self-described SBOM artifact, foreign described artifact, duplicate described identity, or case-drifted identity fails closed before a binding is produced. No fallback rewrites a name, drops an artifact, substitutes another manifest entry, accepts a partial or circular identity inventory, or converts an invalid binding into success. +Manifest-binding failures occur before a binding is produced. Envelope failures return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input is rejected before JSON parsing. Invalid UTF-8, invalid JSON, duplicate keys, incorrect context, invalid top-level shape, non-object graph entries, excessive graph cardinality, and invalid `SpdxDocument` cardinality all fail closed. -Failure to construct this metadata binding does not authorize release through another path. Conversely, successful construction or access to the versioned JSON-LD context identity cannot be used as evidence that SPDX content is structurally valid, semantically valid, or package/component-complete. +Failure at either layer does not authorize release through another path. There is no alternate version fallback, network context fallback, permissive parse mode, or silent default success. ## Security / privacy / governance impact -The boundary reduces supply-chain identity ambiguity by preventing both an ambient JSON-LD context choice and the SBOM layer from creating a second ambient artifact namespace, silently omitting a release artifact, or creating a circular exact-digest join. It introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. Scheduled development agents remain unable to merge, sign, tag, publish, install, or change repository governance. +The boundary reduces supply-chain ambiguity without introducing ambient network authority. Bounded parsing constrains memory exposure before deeper validation, duplicate-key rejection prevents parser interpretation drift, exact context matching prevents version drift, and redacted diagnostics prevent supplier-controlled SBOM bytes from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. ## Tests and acceptance evidence -The owning `originweave-core` tests must prove exact SPDX specification/context identity, exact manifest-backed SBOM admission, complete coverage of every non-SBOM manifest artifact, deterministic described-artifact ordering, and typed fail-closed behavior for missing SBOM identity, empty or incomplete described sets, self-description, foreign artifacts, duplicates, and case drift. Public error text and `std::error::Error` behavior are part of the deterministic contract. +The Rust identity tests must continue to prove exact SPDX specification/context identity, manifest-backed SBOM admission, complete coverage of every non-SBOM manifest artifact, deterministic ordering, and typed fail-closed binding behavior. + +The Python release-verifier contract tests must prove: + +- exact SPDX 3.0.1 context and one `SpdxDocument` are admitted; +- wrong context and unexpected top-level members fail closed; +- duplicate JSON keys, malformed UTF-8, non-finite constants, invalid graph entries, excessive graph size, and zero/multiple documents fail closed; and +- hostile external bytes never appear in diagnostics. -Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. Rust formatting, workspace/all-target checks, full tests, strict Clippy, and rustdoc must pass on that same head. Predecessor checks or model-only evidence do not transfer. +Repository Python contracts, Rust 1.97.1 formatting/check/tests/Clippy/rustdoc, exact Rust production function/line/region/branch coverage, and any applicable browser/security gates must pass on the unchanged exact head. Predecessor evidence does not transfer. ## Migration and rollback -This is a new in-process metadata primitive with no protected-main persisted SBOM schema migration. Before acceptance it can be revised or withdrawn with its owning feature branch. If a durable external release-manifest/SBOM format later depends on this contract, incompatible identity changes require explicit versioning and migration rather than parser broadening. +This remains a pre-release branch with no protected-main persisted SBOM schema migration. The verifier has no network or durable state. Before acceptance it can be revised or withdrawn with its owning branch. If a durable external release/SBOM format depends on this boundary, incompatible context, size, graph, or error-contract changes require explicit versioning and migration rather than silent parser broadening. ## Open follow-ups -- Generate SPDX 3.0.1 SBOM content for supported release packages through a separately reviewed deterministic pipeline. -- Emit and validate the required top-level SPDX 3.0.1 global JSON-LD context reference, then validate JSON-LD structure and SPDX ontology semantics against the reviewed 3.0.1 resources. -- Define package/component completeness and root-element rules inside the SPDX document for OriginWeave distribution artifacts. -- Bind SBOM content, SLSA provenance, signing identity, timestamps, platform packages, reproducibility evidence, and updater trust without transferring authority from this metadata type. +- Generate deterministic SPDX 3.0.1 SBOM content for supported release packages. +- Validate the same candidate bytes against the reviewed SPDX 3.0.1 JSON Schema and OWL/SHACL resources using immutable, offline-verifiable identities. +- Define package/component completeness and root-element rules for OriginWeave distribution artifacts. +- Verify the bound SBOM artifact digest before content admission and compose the result with SLSA provenance, signing identity, timestamps, platform packages, reproducibility evidence, and updater trust. - Complete issue #201's integrated release acceptance and rollback/freeze-protection architecture. ## Supersession / reversal conditions -Supersede this ADR when a versioned external release/SBOM specification replaces the in-process binding, when OriginWeave adopts a different SBOM standard under a reviewed migration, or when a stronger integrated provenance model subsumes this identity join. Reversal must preserve fail-closed manifest-backed artifact identity and cannot make metadata possession equivalent to release authority. +Supersede this ADR when a versioned external release/SBOM specification replaces the in-process binding and envelope contract, when OriginWeave adopts a different SBOM standard under a reviewed migration, or when a stronger integrated provenance model subsumes these boundaries. Reversal must preserve fail-closed manifest-backed identity and must not make metadata or partial parsing equivalent to release authority. ## References From 8215a79317b760f82be7ccf400f9e06f9946ba76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 12:26:19 -0700 Subject: [PATCH 085/170] test(release): admit SPDX namespace context mappings --- tests/test_release_spdx_jsonld_envelope_contract.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 51af9a78e..fd2de69df 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -55,6 +55,17 @@ def test_exact_context_and_single_document_are_admitted(self) -> None: self.assertEqual(summary["graph_object_count"], 2) self.assertEqual(summary["spdx_document_count"], 1) + def test_required_global_context_allows_additional_namespace_mapping(self) -> None: + summary = self.validate( + self._payload( + [{"type": "SpdxDocument"}], + context=[CONTEXT, {"buyer": "https://example.invalid/spdx/buyer#"}], + ) + ) + + self.assertEqual(summary["context"], CONTEXT) + self.assertEqual(summary["spdx_document_count"], 1) + def test_context_must_match_exact_spdx_3_0_1_identity(self) -> None: self._assert_error_code( self._payload( From 4c34ae820d5cfd4f1b004b5fd467285a985374ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 12:27:50 -0700 Subject: [PATCH 086/170] fix(release): accept SPDX namespace context mappings --- scripts/release/validate_spdx_jsonld.py | 31 ++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index eae5f3853..e7836e5e3 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -2,11 +2,11 @@ """Bounded, fail-closed SPDX 3.0.1 JSON-LD envelope verification. This verifier deliberately checks only the serialization envelope needed before deeper -schema/ontology validation: exact versioned global context identity, a bounded top-level -JSON object, a bounded object-only ``@graph``, and exactly one ``SpdxDocument`` element. -It does not claim full SPDX structural or semantic conformance, artifact authenticity, -SBOM completeness, provenance, signing, publication, installation, update, or rollback -authority. +schema/ontology validation: required versioned global context identity with optional local +namespace mappings, a bounded top-level JSON object, a bounded object-only ``@graph``, and +exactly one ``SpdxDocument`` element. It does not claim full SPDX structural or semantic +conformance, artifact authenticity, SBOM completeness, provenance, signing, publication, +installation, update, or rollback authority. """ from __future__ import annotations @@ -48,6 +48,25 @@ def _reject_nonfinite_constant(_value: str) -> None: raise ValueError("non-finite JSON number") +def _has_required_spdx_context(context: Any) -> bool: + """Accept the required SPDX context plus only inline namespace-mapping objects. + + SPDX 3.0.1 permits additional namespace mappings in a separate JSON-LD context + object. Requiring the versioned SPDX URI first keeps the governing vocabulary + explicit, while rejecting later string/``null`` contexts prevents this envelope + gate from admitting additional ambient remote-context authority. + """ + + if context == SPDX_3_0_1_CONTEXT: + return True + return ( + isinstance(context, list) + and len(context) >= 2 + and context[0] == SPDX_3_0_1_CONTEXT + and all(isinstance(extension, dict) for extension in context[1:]) + ) + + def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: """Validate the bounded SPDX 3.0.1 JSON-LD serialization envelope. @@ -78,7 +97,7 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: if not isinstance(decoded, dict) or set(decoded) != {"@context", "@graph"}: raise SpdxJsonLdEnvelopeError("invalid_top_level") - if decoded["@context"] != SPDX_3_0_1_CONTEXT: + if not _has_required_spdx_context(decoded["@context"]): raise SpdxJsonLdEnvelopeError("invalid_context") graph = decoded["@graph"] From 545a107a30e0902bdf99b21d7f336775838f35a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 12:28:36 -0700 Subject: [PATCH 087/170] docs(adr): permit pinned SPDX context extensions --- docs/adr/0016-release-sbom-identity.md | 29 +++++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/adr/0016-release-sbom-identity.md b/docs/adr/0016-release-sbom-identity.md index b62d287b3..a561812be 100644 --- a/docs/adr/0016-release-sbom-identity.md +++ b/docs/adr/0016-release-sbom-identity.md @@ -7,7 +7,7 @@ Issue #201 requires acquisition-grade release evidence, including a software bill of materials tied to the same immutable release identity buyers verify. ADR 0015 deliberately stops at a bounded release-manifest identity and does not claim SBOM content conformance, provenance, signing, publication, installation, update, rollback, or release authority. -SPDX 3.0.1 defines JSON-LD serialization and separate structural and semantic validation requirements. Every SPDX 3.0.1 JSON-LD document must reference the versioned global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld` at the top level. A serialization must not define more than one `SpdxDocument`. Full SPDX JSON-LD conformance additionally requires structural validation against the versioned SPDX JSON Schema and semantic validation against the SPDX OWL ontology/SHACL constraints. +SPDX 3.0.1 defines JSON-LD serialization and separate structural and semantic validation requirements. Every SPDX 3.0.1 JSON-LD document must reference the versioned global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld` at the top level, while the specification explicitly permits additional namespace mappings in a separate object within the context. JSON-LD 1.1 permits a context to be an array of context entries, so a document can retain the required SPDX context while adding local namespace mappings. A serialization must not define more than one `SpdxDocument`. Full SPDX JSON-LD conformance additionally requires structural validation against the versioned SPDX JSON Schema and semantic validation against the SPDX OWL ontology/SHACL constraints. OriginWeave therefore separates four claims that must not be collapsed: @@ -22,6 +22,8 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - SBOM identity must not drift away from the exact release manifest it supplements. - The supported SPDX JSON-LD format must expose its exact specification version and required versioned global context identity rather than rely on an ambient or moving context. +- Standards-permitted inline namespace mappings must not be rejected merely because `@context` is represented as a JSON-LD context array. +- Additional remote context strings must not become ambient authority through the narrow envelope gate. - Actual serialized bytes must not be accepted merely because metadata says they are SPDX 3.0.1 JSON-LD. - Envelope parsing must be bounded before deeper semantic use and reject malformed UTF-8, malformed JSON, duplicate JSON object members, non-finite JSON constants, unexpected top-level members, non-object graph entries, excessive graph cardinality, and zero or multiple `SpdxDocument` objects. - Error diagnostics must not reflect attacker- or supplier-controlled document bytes. @@ -37,7 +39,7 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. - The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase SHA-256 identity, and cannot also appear in the binding's described-artifact set. - Every other artifact admitted by that release manifest must be retained in the described-artifact set exactly once. This is an OriginWeave release-inventory completeness rule, not a general SPDX modeling assertion. -- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes only for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, requires exactly the top-level `@context` and `@graph` members, requires the exact SPDX 3.0.1 global context string, bounds the graph to 65,536 objects, requires each graph entry to be an object with a string `type`, and requires exactly one object whose type is `SpdxDocument`. +- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes only for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, requires exactly the top-level `@context` and `@graph` members, and requires either the exact SPDX 3.0.1 global context string or a JSON-LD context array whose first entry is that exact URI and whose remaining entries are inline mapping objects. Later remote-context strings and `null` entries are rejected. The verifier bounds the graph to 65,536 objects, requires each graph entry to be an object with a string `type`, and requires exactly one object whose type is `SpdxDocument`. - The envelope verifier deliberately does not fetch remote contexts or schemas, resolve external identifiers, authenticate artifacts, validate the SPDX JSON Schema, evaluate the SPDX ontology/SHACL constraints, infer package completeness, or grant any signing/release/update authority. - Full conformance still requires validation of the same serialized bytes against reviewed SPDX 3.0.1 structural and semantic resources plus OriginWeave product-completeness rules. @@ -47,6 +49,14 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe Rejected. A manifest can correctly identify an artifact while the artifact bytes contain the wrong context, malformed JSON, an ambiguous duplicate-key object, or multiple `SpdxDocument` definitions. +### Require `@context` to be only the SPDX global-context string + +Rejected. SPDX 3.0.1 explicitly permits additional namespace mappings in a separate object within the context, and JSON-LD 1.1 permits context arrays. Rejecting that representation would turn a fail-closed release gate into a standards-incompatible false negative. + +### Permit arbitrary additional JSON-LD context entries + +Rejected for this bounded gate. Later remote context strings or `null` entries would broaden authority beyond the pinned SPDX vocabulary and make downstream interpretation dependent on ambient external resources. This slice admits only inline context objects after the exact versioned SPDX URI; deeper semantic validity remains the responsibility of the full SPDX verifier. + ### Treat a general-purpose JSON parse as SPDX conformance Rejected. JSON syntax alone does not establish the required SPDX context, document cardinality, JSON Schema validity, ontology/SHACL validity, or SBOM completeness. @@ -83,26 +93,26 @@ OriginWeave adopts the following release-SBOM boundary: 6. Foreign, duplicate, incomplete, and case-drifted described artifact identities fail closed. 7. Described artifact identities are stored deterministically and the manifest's existing bound transitively limits the inventory. 8. Before deeper SPDX processing, actual candidate JSON-LD bytes may pass through `validate_spdx_3_0_1_jsonld_bytes`. -9. That verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB, rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, requires the exact versioned context, allows at most 65,536 graph objects, requires object entries with string `type`, and requires exactly one `SpdxDocument`. +9. That verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB, rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, and requires the versioned SPDX context either as the exact context string or as the first entry of a JSON-LD context array followed only by inline mapping objects. It rejects additional remote context strings or `null`, allows at most 65,536 graph objects, requires object entries with string `type`, and requires exactly one `SpdxDocument`. 10. Validation errors expose stable error codes and generic diagnostics without reflecting document bytes. 11. Envelope success is not a certificate of SPDX JSON Schema validity, semantic ontology/SHACL validity, package/component completeness, artifact authenticity, provenance, signatures, publication, installation, update, rollback, or release authority. -12. No verifier fallback may substitute another SPDX version, relax the context, discard duplicate keys, silently truncate a graph, fetch an ambient remote resource, or convert malformed content into success. +12. No verifier fallback may substitute another SPDX version, discard the required pinned context, discard duplicate keys, silently truncate a graph, fetch an ambient remote resource, or convert malformed content into success. ## Consequences -Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over actual bytes. A candidate cannot pass this layer with the wrong SPDX context, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, or zero/multiple `SpdxDocument` objects. +Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over actual bytes. A standards-valid document can use the required SPDX context together with inline namespace mappings without being rejected solely for using JSON-LD's context-array form. A candidate cannot pass this layer with the wrong SPDX context, an additional remote context string, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, or zero/multiple `SpdxDocument` objects. The boundary remains intentionally narrower than a commercial SBOM generator or complete verifier. The official SPDX 3.0.1 specification requires structural validation against the JSON Schema and semantic validation against the OWL ontology/SHACL constraints; those checks, package/component completeness, root-element rules, artifact digest verification, provenance, signing, and integrated release acceptance remain separate work. Envelope success must never be presented as full SPDX conformance. ## Failure and degraded behavior -Manifest-binding failures occur before a binding is produced. Envelope failures return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input is rejected before JSON parsing. Invalid UTF-8, invalid JSON, duplicate keys, incorrect context, invalid top-level shape, non-object graph entries, excessive graph cardinality, and invalid `SpdxDocument` cardinality all fail closed. +Manifest-binding failures occur before a binding is produced. Envelope failures return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input is rejected before JSON parsing. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, disallowed ambient context entries, invalid top-level shape, non-object graph entries, excessive graph cardinality, and invalid `SpdxDocument` cardinality all fail closed. Failure at either layer does not authorize release through another path. There is no alternate version fallback, network context fallback, permissive parse mode, or silent default success. ## Security / privacy / governance impact -The boundary reduces supply-chain ambiguity without introducing ambient network authority. Bounded parsing constrains memory exposure before deeper validation, duplicate-key rejection prevents parser interpretation drift, exact context matching prevents version drift, and redacted diagnostics prevent supplier-controlled SBOM bytes from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. +The boundary reduces supply-chain ambiguity without introducing ambient network authority. Bounded parsing constrains memory exposure before deeper validation, duplicate-key rejection prevents parser interpretation drift, the pinned versioned SPDX context prevents version drift, inline mapping objects preserve standards interoperability, disallowing later remote context strings prevents an envelope pass from authorizing an additional moving context, and redacted diagnostics prevent supplier-controlled SBOM bytes from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. ## Tests and acceptance evidence @@ -111,7 +121,8 @@ The Rust identity tests must continue to prove exact SPDX specification/context The Python release-verifier contract tests must prove: - exact SPDX 3.0.1 context and one `SpdxDocument` are admitted; -- wrong context and unexpected top-level members fail closed; +- the required SPDX context plus an inline namespace-mapping object is admitted; +- wrong context, an additional remote context string, and unexpected top-level members fail closed; - duplicate JSON keys, malformed UTF-8, non-finite constants, invalid graph entries, excessive graph size, and zero/multiple documents fail closed; and - hostile external bytes never appear in diagnostics. @@ -138,3 +149,5 @@ Supersede this ADR when a versioned external release/SBOM specification replaces SPDX Workgroup. (2026). *SPDX specification 3.0.1*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/ SPDX Workgroup. (2026). *SPDX specification 3.0.1: Model and serializations*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/serializations/ + +World Wide Web Consortium. (2020, July 16). *JSON-LD 1.1: A JSON-based serialization for linked data*. https://www.w3.org/TR/json-ld11/ From c5210399c254f3f815c0d785bc797d2a34d67e31 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 12:29:06 -0700 Subject: [PATCH 088/170] test(release): keep SPDX context extensions local --- tests/test_release_spdx_jsonld_envelope_contract.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index fd2de69df..814248a72 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -66,6 +66,19 @@ def test_required_global_context_allows_additional_namespace_mapping(self) -> No self.assertEqual(summary["context"], CONTEXT) self.assertEqual(summary["spdx_document_count"], 1) + def test_context_extensions_cannot_add_ambient_remote_authority(self) -> None: + self._assert_error_code( + self._payload( + [{"type": "SpdxDocument"}], + context=[CONTEXT, "https://example.invalid/moving-context.jsonld"], + ), + "invalid_context", + ) + self._assert_error_code( + self._payload([{"type": "SpdxDocument"}], context=[CONTEXT, None]), + "invalid_context", + ) + def test_context_must_match_exact_spdx_3_0_1_identity(self) -> None: self._assert_error_code( self._payload( From f242237d47ebf040ab0382eb39f6774454e196df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 13:15:53 -0700 Subject: [PATCH 089/170] test(release): reject semantic JSON-LD context overrides --- ...test_release_spdx_jsonld_envelope_contract.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 814248a72..5fc4bf1c1 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -66,6 +66,22 @@ def test_required_global_context_allows_additional_namespace_mapping(self) -> No self.assertEqual(summary["context"], CONTEXT) self.assertEqual(summary["spdx_document_count"], 1) + def test_inline_context_cannot_import_or_rebind_spdx_semantics(self) -> None: + hostile_contexts = [ + [CONTEXT, {"@import": "https://example.invalid/moving-context.jsonld"}], + [CONTEXT, {"@base": "https://example.invalid/base/"}], + [CONTEXT, {"@vocab": "https://example.invalid/vocab#"}], + [CONTEXT, {"type": "https://example.invalid/NotSpdxDocument"}], + [CONTEXT, {"spdxId": "https://example.invalid/not-id"}], + ] + + for context in hostile_contexts: + with self.subTest(context=context): + self._assert_error_code( + self._payload([{"type": "SpdxDocument"}], context=context), + "invalid_context", + ) + def test_context_extensions_cannot_add_ambient_remote_authority(self) -> None: self._assert_error_code( self._payload( From 483b97e7b5edced581c99203fb81b8c4c71e2949 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 13:18:07 -0700 Subject: [PATCH 090/170] fix(release): pin SPDX context semantics fail closed --- scripts/release/validate_spdx_jsonld.py | 30 ++++++++++--------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index e7836e5e3..6222244d2 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -2,11 +2,11 @@ """Bounded, fail-closed SPDX 3.0.1 JSON-LD envelope verification. This verifier deliberately checks only the serialization envelope needed before deeper -schema/ontology validation: required versioned global context identity with optional local -namespace mappings, a bounded top-level JSON object, a bounded object-only ``@graph``, and -exactly one ``SpdxDocument`` element. It does not claim full SPDX structural or semantic -conformance, artifact authenticity, SBOM completeness, provenance, signing, publication, -installation, update, or rollback authority. +schema/ontology validation: the exact versioned global context identity required by the +official SPDX 3.0.1 JSON Schema, a bounded top-level JSON object, a bounded object-only +``@graph``, and exactly one ``SpdxDocument`` element. It does not claim full SPDX structural +or semantic conformance, artifact authenticity, SBOM completeness, provenance, signing, +publication, installation, update, or rollback authority. """ from __future__ import annotations @@ -49,22 +49,16 @@ def _reject_nonfinite_constant(_value: str) -> None: def _has_required_spdx_context(context: Any) -> bool: - """Accept the required SPDX context plus only inline namespace-mapping objects. + """Require the exact SPDX 3.0.1 context identity used by the official JSON Schema. - SPDX 3.0.1 permits additional namespace mappings in a separate JSON-LD context - object. Requiring the versioned SPDX URI first keeps the governing vocabulary - explicit, while rejecting later string/``null`` contexts prevents this envelope - gate from admitting additional ambient remote-context authority. + The SPDX 3.0.1 prose permits additional namespace mappings, but the same version's + normative structural-validation resource constrains ``@context`` to this exact string. + This preliminary gate therefore fails closed on context arrays rather than interpreting + inline JSON-LD controls or term redefinitions before a reviewed schema-aware validator + can establish their semantics. """ - if context == SPDX_3_0_1_CONTEXT: - return True - return ( - isinstance(context, list) - and len(context) >= 2 - and context[0] == SPDX_3_0_1_CONTEXT - and all(isinstance(extension, dict) for extension in context[1:]) - ) + return context == SPDX_3_0_1_CONTEXT def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: From eed3094c3c69990095c2fb509d87360d7cf7c7ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 13:18:52 -0700 Subject: [PATCH 091/170] test(release): lock SPDX envelope to schema context --- tests/test_release_spdx_jsonld_envelope_contract.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 5fc4bf1c1..1e3954bd0 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -55,17 +55,15 @@ def test_exact_context_and_single_document_are_admitted(self) -> None: self.assertEqual(summary["graph_object_count"], 2) self.assertEqual(summary["spdx_document_count"], 1) - def test_required_global_context_allows_additional_namespace_mapping(self) -> None: - summary = self.validate( + def test_context_array_is_rejected_until_schema_aware_validation(self) -> None: + self._assert_error_code( self._payload( [{"type": "SpdxDocument"}], context=[CONTEXT, {"buyer": "https://example.invalid/spdx/buyer#"}], - ) + ), + "invalid_context", ) - self.assertEqual(summary["context"], CONTEXT) - self.assertEqual(summary["spdx_document_count"], 1) - def test_inline_context_cannot_import_or_rebind_spdx_semantics(self) -> None: hostile_contexts = [ [CONTEXT, {"@import": "https://example.invalid/moving-context.jsonld"}], From 0db65f2f3296db513c1babf004f85681a6b83098 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 13:19:56 -0700 Subject: [PATCH 092/170] docs(adr): fail closed on SPDX context composition --- docs/adr/0016-release-sbom-identity.md | 57 ++++++++++++++------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/docs/adr/0016-release-sbom-identity.md b/docs/adr/0016-release-sbom-identity.md index a561812be..84461ca6b 100644 --- a/docs/adr/0016-release-sbom-identity.md +++ b/docs/adr/0016-release-sbom-identity.md @@ -7,9 +7,11 @@ Issue #201 requires acquisition-grade release evidence, including a software bill of materials tied to the same immutable release identity buyers verify. ADR 0015 deliberately stops at a bounded release-manifest identity and does not claim SBOM content conformance, provenance, signing, publication, installation, update, rollback, or release authority. -SPDX 3.0.1 defines JSON-LD serialization and separate structural and semantic validation requirements. Every SPDX 3.0.1 JSON-LD document must reference the versioned global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld` at the top level, while the specification explicitly permits additional namespace mappings in a separate object within the context. JSON-LD 1.1 permits a context to be an array of context entries, so a document can retain the required SPDX context while adding local namespace mappings. A serialization must not define more than one `SpdxDocument`. Full SPDX JSON-LD conformance additionally requires structural validation against the versioned SPDX JSON Schema and semantic validation against the SPDX OWL ontology/SHACL constraints. +SPDX 3.0.1 defines JSON-LD serialization and separate structural and semantic validation requirements. The serialization prose requires the versioned global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`, defines `spdxId` as an alias for `@id` and `type` as an alias for `@type`, and says additional namespace mappings may be defined in a separate context object. The same SPDX 3.0.1 specification requires structural validation against its official JSON Schema. That schema constrains top-level `@context` with a `const` equal to the exact versioned context string. -OriginWeave therefore separates four claims that must not be collapsed: +Those two official 3.0.1 resources therefore expose an interoperability tension: the prose describes extension mappings that JSON-LD can represent with context composition, while the published structural schema accepts only the exact context string. More importantly for this preliminary verifier, an arbitrary inline JSON-LD context object is not merely a namespace map: JSON-LD control terms such as `@import`, `@base`, and `@vocab`, or redefinitions of SPDX aliases such as `type` and `spdxId`, can change downstream interpretation. Treating every inline object as a benign namespace map would therefore create semantic and remote-authority ambiguity before schema/ontology validation. + +OriginWeave separates four claims that must not be collapsed: 1. the declared SPDX serialization identity; 2. the immutable release-manifest/SBOM artifact identity join; @@ -22,8 +24,8 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - SBOM identity must not drift away from the exact release manifest it supplements. - The supported SPDX JSON-LD format must expose its exact specification version and required versioned global context identity rather than rely on an ambient or moving context. -- Standards-permitted inline namespace mappings must not be rejected merely because `@context` is represented as a JSON-LD context array. -- Additional remote context strings must not become ambient authority through the narrow envelope gate. +- The preliminary envelope gate must not interpret arbitrary JSON-LD context controls or term redefinitions as harmless namespace declarations. +- The gate should remain structurally compatible with the official SPDX 3.0.1 JSON Schema until a reviewed schema-aware extension policy exists. - Actual serialized bytes must not be accepted merely because metadata says they are SPDX 3.0.1 JSON-LD. - Envelope parsing must be bounded before deeper semantic use and reject malformed UTF-8, malformed JSON, duplicate JSON object members, non-finite JSON constants, unexpected top-level members, non-object graph entries, excessive graph cardinality, and zero or multiple `SpdxDocument` objects. - Error diagnostics must not reflect attacker- or supplier-controlled document bytes. @@ -39,8 +41,9 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. - The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase SHA-256 identity, and cannot also appear in the binding's described-artifact set. - Every other artifact admitted by that release manifest must be retained in the described-artifact set exactly once. This is an OriginWeave release-inventory completeness rule, not a general SPDX modeling assertion. -- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes only for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, requires exactly the top-level `@context` and `@graph` members, and requires either the exact SPDX 3.0.1 global context string or a JSON-LD context array whose first entry is that exact URI and whose remaining entries are inline mapping objects. Later remote-context strings and `null` entries are rejected. The verifier bounds the graph to 65,536 objects, requires each graph entry to be an object with a string `type`, and requires exactly one object whose type is `SpdxDocument`. -- The envelope verifier deliberately does not fetch remote contexts or schemas, resolve external identifiers, authenticate artifacts, validate the SPDX JSON Schema, evaluate the SPDX ontology/SHACL constraints, infer package completeness, or grant any signing/release/update authority. +- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes only for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, requires exactly the top-level `@context` and `@graph` members, and requires `@context` to equal the exact SPDX 3.0.1 global-context string. Context arrays and inline/remote context extensions fail closed at this stage. +- The exact-string choice is deliberately narrower than the SPDX serialization prose because the official 3.0.1 JSON Schema itself uses that exact-string constraint and because this preliminary verifier does not implement complete JSON-LD context semantics or collision detection against the pinned SPDX context. +- The envelope verifier does not fetch remote contexts or schemas, resolve external identifiers, authenticate artifacts, validate the full SPDX JSON Schema, evaluate the SPDX ontology/SHACL constraints, infer package completeness, or grant any signing/release/update authority. - Full conformance still requires validation of the same serialized bytes against reviewed SPDX 3.0.1 structural and semantic resources plus OriginWeave product-completeness rules. ## Options considered @@ -49,22 +52,22 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe Rejected. A manifest can correctly identify an artifact while the artifact bytes contain the wrong context, malformed JSON, an ambiguous duplicate-key object, or multiple `SpdxDocument` definitions. +### Admit the exact SPDX context plus arbitrary inline context objects + +Rejected. An inline JSON-LD object can contain `@import`, `@base`, `@vocab`, or term/alias redefinitions rather than only a new namespace prefix. Without a complete pinned-context collision model, accepting those objects would allow the preliminary gate to count raw `type` values under semantics that later JSON-LD processing may reinterpret. + ### Require `@context` to be only the SPDX global-context string -Rejected. SPDX 3.0.1 explicitly permits additional namespace mappings in a separate object within the context, and JSON-LD 1.1 permits context arrays. Rejecting that representation would turn a fail-closed release gate into a standards-incompatible false negative. +Selected for this preliminary gate. The official SPDX 3.0.1 JSON Schema constrains `@context` to that exact string, this representation is used by the specification's normative example, and the rule prevents context-array semantic or remote-authority ambiguity. SPDX prose support for additional namespace mappings is retained as an explicit interoperability follow-up rather than silently approximated with an unsafe object-type check. -### Permit arbitrary additional JSON-LD context entries +### Fetch the JSON-LD context or schema during the first verifier -Rejected for this bounded gate. Later remote context strings or `null` entries would broaden authority beyond the pinned SPDX vocabulary and make downstream interpretation dependent on ambient external resources. This slice admits only inline context objects after the exact versioned SPDX URI; deeper semantic validity remains the responsibility of the full SPDX verifier. +Rejected. Ambient network retrieval would introduce moving external authority and availability into a release gate before pinned schema/ontology identities and offline verification are designed. The first verifier uses fixed local constants and no network access. ### Treat a general-purpose JSON parse as SPDX conformance Rejected. JSON syntax alone does not establish the required SPDX context, document cardinality, JSON Schema validity, ontology/SHACL validity, or SBOM completeness. -### Fetch the JSON-LD context and schema during the first verifier - -Rejected. Ambient network retrieval would introduce moving external authority and availability into a release gate before pinned schema/ontology identities and offline verification are designed. The first verifier uses fixed local constants and no network access. - ### Store an arbitrary SBOM path or URL next to the manifest Rejected. An ambient path or URL can drift from the bounded release inventory and does not guarantee that the SBOM itself is one of the artifacts buyers can hash against the manifest. @@ -77,10 +80,6 @@ Rejected. A non-empty manifest-backed subset can look well formed while silently Rejected for OriginWeave's release identity contract. A self-referential described-artifact set makes the artifact-to-SBOM join circular at the exact-digest boundary later verification must evaluate. This is an OriginWeave integrity rule, not a claim that SPDX generally forbids such a relationship. -### Layered manifest binding plus bounded envelope verification - -Selected. Metadata establishes exact release/SBOM identity; the bounded verifier checks the first non-ambient properties of the actual serialized bytes; later gates remain responsible for full SPDX and product conformance. - ## Decision OriginWeave adopts the following release-SBOM boundary: @@ -93,26 +92,28 @@ OriginWeave adopts the following release-SBOM boundary: 6. Foreign, duplicate, incomplete, and case-drifted described artifact identities fail closed. 7. Described artifact identities are stored deterministically and the manifest's existing bound transitively limits the inventory. 8. Before deeper SPDX processing, actual candidate JSON-LD bytes may pass through `validate_spdx_3_0_1_jsonld_bytes`. -9. That verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB, rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, and requires the versioned SPDX context either as the exact context string or as the first entry of a JSON-LD context array followed only by inline mapping objects. It rejects additional remote context strings or `null`, allows at most 65,536 graph objects, requires object entries with string `type`, and requires exactly one `SpdxDocument`. +9. That verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB, rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, and requires `@context` to equal the exact versioned SPDX 3.0.1 context string. Context arrays and all additional context entries fail closed. It allows at most 65,536 graph objects, requires object entries with string `type`, and requires exactly one raw `SpdxDocument` under the pinned alias semantics. 10. Validation errors expose stable error codes and generic diagnostics without reflecting document bytes. -11. Envelope success is not a certificate of SPDX JSON Schema validity, semantic ontology/SHACL validity, package/component completeness, artifact authenticity, provenance, signatures, publication, installation, update, rollback, or release authority. -12. No verifier fallback may substitute another SPDX version, discard the required pinned context, discard duplicate keys, silently truncate a graph, fetch an ambient remote resource, or convert malformed content into success. +11. Envelope success is not a certificate of complete SPDX JSON Schema validity, semantic ontology/SHACL validity, package/component completeness, artifact authenticity, provenance, signatures, publication, installation, update, rollback, or release authority. +12. No verifier fallback may substitute another SPDX version, discard the required pinned context, reinterpret an unvalidated context extension, discard duplicate keys, silently truncate a graph, fetch an ambient remote resource, or convert malformed content into success. ## Consequences -Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over actual bytes. A standards-valid document can use the required SPDX context together with inline namespace mappings without being rejected solely for using JSON-LD's context-array form. A candidate cannot pass this layer with the wrong SPDX context, an additional remote context string, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, or zero/multiple `SpdxDocument` objects. +Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over actual bytes. A candidate cannot pass this layer with the wrong SPDX context, a context array that can alter JSON-LD interpretation, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, or zero/multiple `SpdxDocument` objects. + +The exact-context rule can reject a serialization that the SPDX 3.0.1 prose intends to allow through additional namespace mappings. That false negative is accepted at this preliminary gate because the same version's official structural schema currently requires the exact string and because OriginWeave does not yet possess a reviewed, schema-aware rule capable of proving that an extension object is namespace-only and non-colliding. Support for such mappings must be added only together with pinned full structural/semantic validation or another equally strong proof of interpretation. The boundary remains intentionally narrower than a commercial SBOM generator or complete verifier. The official SPDX 3.0.1 specification requires structural validation against the JSON Schema and semantic validation against the OWL ontology/SHACL constraints; those checks, package/component completeness, root-element rules, artifact digest verification, provenance, signing, and integrated release acceptance remain separate work. Envelope success must never be presented as full SPDX conformance. ## Failure and degraded behavior -Manifest-binding failures occur before a binding is produced. Envelope failures return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input is rejected before JSON parsing. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, disallowed ambient context entries, invalid top-level shape, non-object graph entries, excessive graph cardinality, and invalid `SpdxDocument` cardinality all fail closed. +Manifest-binding failures occur before a binding is produced. Envelope failures return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input is rejected before JSON parsing. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, context arrays/extensions, invalid top-level shape, non-object graph entries, excessive graph cardinality, and invalid `SpdxDocument` cardinality all fail closed. Failure at either layer does not authorize release through another path. There is no alternate version fallback, network context fallback, permissive parse mode, or silent default success. ## Security / privacy / governance impact -The boundary reduces supply-chain ambiguity without introducing ambient network authority. Bounded parsing constrains memory exposure before deeper validation, duplicate-key rejection prevents parser interpretation drift, the pinned versioned SPDX context prevents version drift, inline mapping objects preserve standards interoperability, disallowing later remote context strings prevents an envelope pass from authorizing an additional moving context, and redacted diagnostics prevent supplier-controlled SBOM bytes from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. +The boundary reduces supply-chain ambiguity without introducing ambient network authority. Bounded parsing constrains memory exposure before deeper validation, duplicate-key rejection prevents parser interpretation drift, and the pinned exact SPDX context prevents version drift, remote `@import`, `@base`/`@vocab` mutation, and term/alias rebinding from being treated as already-admitted envelope semantics. Redacted diagnostics prevent supplier-controlled SBOM bytes from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. ## Tests and acceptance evidence @@ -121,8 +122,9 @@ The Rust identity tests must continue to prove exact SPDX specification/context The Python release-verifier contract tests must prove: - exact SPDX 3.0.1 context and one `SpdxDocument` are admitted; -- the required SPDX context plus an inline namespace-mapping object is admitted; -- wrong context, an additional remote context string, and unexpected top-level members fail closed; +- context arrays, including apparently simple namespace mappings, fail closed until schema-aware extension validation exists; +- `@import`, `@base`, `@vocab`, `type`, and `spdxId` context overrides fail closed; +- wrong context, additional remote context entries, and unexpected top-level members fail closed; - duplicate JSON keys, malformed UTF-8, non-finite constants, invalid graph entries, excessive graph size, and zero/multiple documents fail closed; and - hostile external bytes never appear in diagnostics. @@ -135,7 +137,8 @@ This remains a pre-release branch with no protected-main persisted SBOM schema m ## Open follow-ups - Generate deterministic SPDX 3.0.1 SBOM content for supported release packages. -- Validate the same candidate bytes against the reviewed SPDX 3.0.1 JSON Schema and OWL/SHACL resources using immutable, offline-verifiable identities. +- Validate the same candidate bytes against reviewed, immutable SPDX 3.0.1 JSON Schema and OWL/SHACL resources using offline-verifiable identities. +- Reconcile the SPDX 3.0.1 prose allowance for additional namespace mappings with the official 3.0.1 JSON Schema's exact-string `@context` constraint before admitting context composition. - Define package/component completeness and root-element rules for OriginWeave distribution artifacts. - Verify the bound SBOM artifact digest before content admission and compose the result with SLSA provenance, signing identity, timestamps, platform packages, reproducibility evidence, and updater trust. - Complete issue #201's integrated release acceptance and rollback/freeze-protection architecture. @@ -150,4 +153,6 @@ SPDX Workgroup. (2026). *SPDX specification 3.0.1*. The Linux Foundation. https: SPDX Workgroup. (2026). *SPDX specification 3.0.1: Model and serializations*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/serializations/ +SPDX Workgroup. (2026). *SPDX 3.0.1 JSON Schema*. The Linux Foundation. https://spdx.org/schema/3.0.1/spdx-json-schema.json + World Wide Web Consortium. (2020, July 16). *JSON-LD 1.1: A JSON-based serialization for linked data*. https://www.w3.org/TR/json-ld11/ From 69a00cdc434d3f92ca5a2bdefd496208e8795146 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 13:22:24 -0700 Subject: [PATCH 093/170] docs(research): record SPDX context authority boundary --- docs/doctoring.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 9fc5ba94e..f5bb304dc 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -80,7 +80,7 @@ Microsoft's Win32 filename guidance requires applications not to assume case sen ### Release SBOM identity -SPDX 3.0.1 defines a software bill of materials as a collection of SPDX elements describing a single package and defines JSON-LD as an RDF serialization for SPDX data. Conformant SPDX JSON-LD additionally requires structural validation against the versioned SPDX JSON Schema and semantic validation against the SPDX ontology. OriginWeave's first release-SBOM slice deliberately does not claim that content-level conformance: it binds only a declared SPDX 3.0.1 JSON-LD SBOM artifact identity and the exact release-artifact names it is intended to describe to one immutable `ReleaseManifest`. The SBOM artifact and every described artifact must already be admitted by that manifest; empty inventories, foreign identities, duplicates, and case-drifted names fail closed. Generation, content validation, package/component completeness, signatures, provenance, publication, installation, update, rollback, and release authority remain separate reviewed boundaries. +SPDX 3.0.1 defines JSON-LD as an RDF serialization for SPDX data and requires conformant JSON-LD to pass both the versioned JSON Schema and the SPDX ontology/SHACL constraints. Its serialization prose requires the global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`, defines `spdxId` and `type` as aliases for JSON-LD `@id` and `@type`, and says additional namespace mappings may be defined in a separate context object. The official SPDX 3.0.1 JSON Schema simultaneously constrains top-level `@context` to the exact global-context string. Because an arbitrary inline JSON-LD context object can also contain semantic controls such as `@import`, `@base`, or `@vocab`, or redefine SPDX aliases, OriginWeave does not approximate “namespace mapping” by accepting every object. The preliminary envelope gate follows the official schema's exact-string identity and rejects context composition until a reviewed schema-aware validator can reconcile the prose/schema tension without granting ambient or redefinition authority. This gate deliberately does not claim full content-level conformance: it binds a declared SPDX 3.0.1 JSON-LD SBOM artifact identity and exact release-artifact names to one immutable `ReleaseManifest`, while generation, full structural/semantic validation, package/component completeness, signatures, provenance, publication, installation, update, rollback, and release authority remain separate reviewed boundaries. ### Git source identity admission @@ -178,6 +178,10 @@ Sheffer, Y., Saint-Andre, P., & Fossati, T. (2022). *Recommendations for secure SPDX Workgroup. (2026). *SPDX specification 3.0.1*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/ +SPDX Workgroup. (2026). *SPDX specification 3.0.1: Model and serializations*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/serializations/ + +SPDX Workgroup. (2026). *SPDX 3.0.1 JSON Schema*. The Linux Foundation. https://spdx.org/schema/3.0.1/spdx-json-schema.json + The Rust Project Developers. (2026). *Ipv4Addr in std::net* (Rust 1.97.1) [Software documentation]. https://doc.rust-lang.org/stable/std/net/struct.Ipv4Addr.html The Rust Project Developers. (2026). *Ipv6Addr in std::net* (Rust 1.97.1) [Software documentation]. https://doc.rust-lang.org/stable/std/net/struct.Ipv6Addr.html @@ -192,4 +196,4 @@ World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 -Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 \ No newline at end of file +Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 From dc04358fbeded3082a08867d26cfae935cd04e12 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 13:23:29 -0700 Subject: [PATCH 094/170] docs(changelog): record SPDX context fail-closed repair --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d20bd1cc..ab3aec984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security +- The SPDX 3.0.1 release-envelope gate now requires the exact versioned global-context string and rejects context arrays, `@import`, `@base`, `@vocab`, and alias/term-rebinding attempts until reviewed schema-aware validation can prove extension semantics without ambient or reinterpretation authority. - Release artifact admission rejects `COM0` through `COM9` and `LPT0` through `LPT9` case-insensitively, including extensions, so package names cannot become Windows device or Microsoft synchronization conflicts in buyer environments. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. From b6cc7958e81c4990f51b262edbbd2ba3d6bd4872 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 14:05:31 -0700 Subject: [PATCH 095/170] test(release): reproduce deeply nested SPDX parser failure --- ...st_release_spdx_jsonld_envelope_contract.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 1e3954bd0..552285f3f 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -148,6 +148,24 @@ def test_invalid_utf8_and_nonfinite_json_fail_closed(self) -> None: ).encode("utf-8") self._assert_error_code(payload, "invalid_json") + def test_excessive_json_nesting_fails_with_typed_redacted_error(self) -> None: + marker = "buyer-secret-marker-must-not-reach-release-diagnostics" + depth = 10_000 + payload = ( + '{"@context":"' + + CONTEXT + + '","@graph":[{"type":"SpdxDocument","nested":' + + "[" * depth + + '"' + + marker + + '"' + + "]" * depth + + "}]}" + ).encode("utf-8") + + error = self._assert_error_code(payload, "invalid_json") + self.assertNotIn(marker, str(error)) + def test_empty_and_oversized_payloads_fail_before_semantic_use(self) -> None: self._assert_error_code(b"", "invalid_size") self._assert_error_code(b" " * (self.max_bytes + 1), "invalid_size") From 03bb9a5491e22a53bc3d32b945f032a52084ac2a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 14:07:50 -0700 Subject: [PATCH 096/170] fix(release): fail closed on excessive SPDX JSON nesting --- scripts/release/validate_spdx_jsonld.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 6222244d2..26cdc4ca5 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -67,7 +67,8 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: External document bytes are never included in raised errors. Successful validation proves only the narrow envelope contract documented by this module; callers must still perform the official SPDX JSON Schema and OWL/SHACL validation before claiming SPDX - conformance. + conformance. Excessive JSON nesting is treated as invalid external input instead of + escaping the typed, value-redacted validation boundary. """ if not isinstance(payload, bytes) or not payload or len(payload) > MAX_SPDX_JSONLD_BYTES: @@ -86,7 +87,7 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: ) except _DuplicateJsonKey as error: raise SpdxJsonLdEnvelopeError("duplicate_key") from error - except (json.JSONDecodeError, ValueError) as error: + except (json.JSONDecodeError, RecursionError, ValueError) as error: raise SpdxJsonLdEnvelopeError("invalid_json") from error if not isinstance(decoded, dict) or set(decoded) != {"@context", "@graph"}: From a03815fa531c455acd3112b0878124df251b575f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:05:32 -0700 Subject: [PATCH 097/170] test(release): reject streaming SPDX input paths --- ..._release_spdx_jsonld_file_type_contract.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_release_spdx_jsonld_file_type_contract.py diff --git a/tests/test_release_spdx_jsonld_file_type_contract.py b/tests/test_release_spdx_jsonld_file_type_contract.py new file mode 100644 index 000000000..52172c764 --- /dev/null +++ b/tests/test_release_spdx_jsonld_file_type_contract.py @@ -0,0 +1,53 @@ +"""Regression contract for bounded SPDX JSON-LD release input file types.""" + +from __future__ import annotations + +import os +import pathlib +import runpy +import tempfile +import threading +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +VALIDATOR = ROOT / "scripts" / "release" / "validate_spdx_jsonld.py" + + +class ReleaseSpdxJsonLdFileTypeContractTests(unittest.TestCase): + """Prevent streaming/special files from bypassing the bounded release-input contract.""" + + def test_fifo_candidate_is_rejected_before_document_bytes_are_accepted(self) -> None: + """A named pipe is not an immutable bounded release artifact input.""" + + if not hasattr(os, "mkfifo"): + self.fail("the release verifier file-type regression requires POSIX mkfifo support") + + namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_file_type_contract") + read_bounded = namespace["_read_bounded"] + envelope_error = namespace["SpdxJsonLdEnvelopeError"] + + with tempfile.TemporaryDirectory(prefix="originweave-spdx-fifo-") as directory: + candidate = pathlib.Path(directory) / "candidate.spdx.jsonld" + os.mkfifo(candidate) + + writer_started = threading.Event() + + def write_candidate() -> None: + writer_started.set() + with candidate.open("wb") as sink: + sink.write(b"{}") + + writer = threading.Thread(target=write_candidate, daemon=True) + writer.start() + self.assertTrue(writer_started.wait(timeout=1.0)) + + with self.assertRaises(envelope_error) as captured: + read_bounded(candidate) + + writer.join(timeout=1.0) + self.assertFalse(writer.is_alive(), "FIFO writer remained blocked after rejection") + self.assertEqual(captured.exception.code, "invalid_file_type") + + +if __name__ == "__main__": + unittest.main() From 41befbe6e8f0494453f753523e09bd82ceb5b1ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:09:06 -0700 Subject: [PATCH 098/170] test(release): make FIFO regression cleanup deterministic --- tests/test_release_spdx_jsonld_file_type_contract.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_release_spdx_jsonld_file_type_contract.py b/tests/test_release_spdx_jsonld_file_type_contract.py index 52172c764..c0c53a220 100644 --- a/tests/test_release_spdx_jsonld_file_type_contract.py +++ b/tests/test_release_spdx_jsonld_file_type_contract.py @@ -41,10 +41,15 @@ def write_candidate() -> None: writer.start() self.assertTrue(writer_started.wait(timeout=1.0)) - with self.assertRaises(envelope_error) as captured: - read_bounded(candidate) + try: + with self.assertRaises(envelope_error) as captured: + read_bounded(candidate) + finally: + if writer.is_alive(): + with candidate.open("rb", buffering=0) as release_reader: + release_reader.read(2) + writer.join(timeout=1.0) - writer.join(timeout=1.0) self.assertFalse(writer.is_alive(), "FIFO writer remained blocked after rejection") self.assertEqual(captured.exception.code, "invalid_file_type") From b729cde739a92b5218886a9b2a5f58e443307a27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:09:41 -0700 Subject: [PATCH 099/170] fix(release): reject non-regular SPDX inputs --- scripts/release/validate_spdx_jsonld.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 26cdc4ca5..f5b238925 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -13,7 +13,9 @@ import argparse import json +import os import pathlib +import stat import sys from collections.abc import Iterable from typing import Any @@ -118,9 +120,19 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: } +def _nonblocking_read_opener(path: str, flags: int) -> int: + """Open one local candidate without allowing a FIFO/device open to wait indefinitely.""" + + return os.open(path, flags | getattr(os, "O_NONBLOCK", 0)) + + def _read_bounded(path: pathlib.Path) -> bytes: + """Read one bounded regular-file candidate without accepting streaming file types.""" + try: - with path.open("rb") as source: + with open(path, "rb", opener=_nonblocking_read_opener) as source: + if not stat.S_ISREG(os.fstat(source.fileno()).st_mode): + raise SpdxJsonLdEnvelopeError("invalid_file_type") payload = source.read(MAX_SPDX_JSONLD_BYTES + 1) except OSError as error: raise SpdxJsonLdEnvelopeError("read_failed") from error From 724b0f28f2159d014c76747d53a9db60b1473791 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:38:37 -0700 Subject: [PATCH 100/170] test(release): bind SPDX bytes to declared artifact digest --- ...st_release_spdx_digest_binding_contract.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/test_release_spdx_digest_binding_contract.py diff --git a/tests/test_release_spdx_digest_binding_contract.py b/tests/test_release_spdx_digest_binding_contract.py new file mode 100644 index 000000000..da0723932 --- /dev/null +++ b/tests/test_release_spdx_digest_binding_contract.py @@ -0,0 +1,77 @@ +"""Regression contract binding validated SPDX bytes to the declared release artifact digest.""" + +from __future__ import annotations + +import hashlib +import json +import pathlib +import runpy +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +VALIDATOR = ROOT / "scripts" / "release" / "validate_spdx_jsonld.py" +CONTEXT = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" + + +class ReleaseSpdxDigestBindingContractTests(unittest.TestCase): + """Prevent a valid but substituted SPDX document from satisfying release evidence.""" + + @classmethod + def setUpClass(cls) -> None: + namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_digest_binding_contract") + cls.validate_bound = staticmethod(namespace["validate_release_spdx_3_0_1_jsonld_bytes"]) + cls.error_type = namespace["SpdxJsonLdEnvelopeError"] + + @staticmethod + def _payload(marker: str = "release-a") -> bytes: + return json.dumps( + { + "@context": CONTEXT, + "@graph": [{"type": "SpdxDocument", "spdxId": marker}], + }, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + @staticmethod + def _digest(payload: bytes) -> str: + return "sha256:" + hashlib.sha256(payload).hexdigest() + + def test_exact_candidate_bytes_match_the_manifest_backed_sbom_digest(self) -> None: + payload = self._payload() + expected_digest = self._digest(payload) + + summary = self.validate_bound(payload, expected_digest) + + self.assertEqual(summary["artifact_sha256"], expected_digest) + self.assertEqual(summary["spdx_document_count"], 1) + + def test_valid_but_substituted_document_fails_closed_on_digest_mismatch(self) -> None: + declared = self._payload("declared-release") + substituted = self._payload("substituted-release-secret-marker") + + with self.assertRaises(self.error_type) as captured: + self.validate_bound(substituted, self._digest(declared)) + + self.assertEqual(captured.exception.code, "digest_mismatch") + self.assertNotIn("substituted-release-secret-marker", str(captured.exception)) + + def test_expected_digest_must_use_canonical_lowercase_sha256_identity(self) -> None: + payload = self._payload() + valid = self._digest(payload) + invalid_digests = ( + valid.removeprefix("sha256:"), + "SHA256:" + valid.removeprefix("sha256:"), + "sha256:" + valid.removeprefix("sha256:").upper(), + "sha256:" + "0" * 63, + ) + + for digest in invalid_digests: + with self.subTest(digest=digest): + with self.assertRaises(self.error_type) as captured: + self.validate_bound(payload, digest) + self.assertEqual(captured.exception.code, "invalid_expected_digest") + + +if __name__ == "__main__": + unittest.main() From c40b046a1c0a89a6a438cb5a2e99d89f0abaa75b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:39:52 -0700 Subject: [PATCH 101/170] fix(release): verify SPDX bytes against artifact digest --- scripts/release/validate_spdx_jsonld.py | 46 +++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index f5b238925..fff638f35 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -4,14 +4,18 @@ This verifier deliberately checks only the serialization envelope needed before deeper schema/ontology validation: the exact versioned global context identity required by the official SPDX 3.0.1 JSON Schema, a bounded top-level JSON object, a bounded object-only -``@graph``, and exactly one ``SpdxDocument`` element. It does not claim full SPDX structural -or semantic conformance, artifact authenticity, SBOM completeness, provenance, signing, -publication, installation, update, or rollback authority. +``@graph``, and exactly one ``SpdxDocument`` element. A release-facing helper additionally +binds candidate bytes to the canonical SHA-256 identity already declared by the release +manifest. It does not claim full SPDX structural or semantic conformance, artifact +authenticity, SBOM completeness, provenance, signing, publication, installation, update, or +rollback authority. """ from __future__ import annotations import argparse +import hashlib +import hmac import json import os import pathlib @@ -120,6 +124,42 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: } +def _valid_expected_sha256_digest(value: object) -> bool: + """Return whether a release-artifact digest uses the canonical manifest spelling.""" + + if not isinstance(value, str) or not value.startswith("sha256:") or len(value) != 71: + return False + return all(character in "0123456789abcdef" for character in value[7:]) + + +def validate_release_spdx_3_0_1_jsonld_bytes( + payload: bytes, + expected_sha256_digest: str, +) -> dict[str, int | str]: + """Validate exact release SBOM bytes against their manifest-backed digest and envelope. + + The expected digest is inert identity evidence supplied by the already-admitted release + manifest. Candidate bytes must remain within the same bounded input contract, and a valid + but substituted SPDX document fails before its contents can be promoted as release evidence. + Diagnostics never reflect document-controlled values or the expected digest. Digest equality + does not authenticate the manifest or grant signing, publication, installation, or update + authority. + """ + + if not isinstance(payload, bytes) or not payload or len(payload) > MAX_SPDX_JSONLD_BYTES: + raise SpdxJsonLdEnvelopeError("invalid_size") + if not _valid_expected_sha256_digest(expected_sha256_digest): + raise SpdxJsonLdEnvelopeError("invalid_expected_digest") + + actual_sha256_digest = "sha256:" + hashlib.sha256(payload).hexdigest() + if not hmac.compare_digest(actual_sha256_digest, expected_sha256_digest): + raise SpdxJsonLdEnvelopeError("digest_mismatch") + + summary = validate_spdx_3_0_1_jsonld_bytes(payload) + summary["artifact_sha256"] = actual_sha256_digest + return summary + + def _nonblocking_read_opener(path: str, flags: int) -> int: """Open one local candidate without allowing a FIFO/device open to wait indefinitely.""" From 04b983e1f3f05d9d3b4966bd17f24fcc075c91cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:42:41 -0700 Subject: [PATCH 102/170] docs(release): record SPDX artifact digest binding --- docs/adr/0016-release-sbom-identity.md | 59 +++++++++++++++----------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/docs/adr/0016-release-sbom-identity.md b/docs/adr/0016-release-sbom-identity.md index 84461ca6b..9701ede97 100644 --- a/docs/adr/0016-release-sbom-identity.md +++ b/docs/adr/0016-release-sbom-identity.md @@ -14,8 +14,8 @@ Those two official 3.0.1 resources therefore expose an interoperability tension: OriginWeave separates four claims that must not be collapsed: 1. the declared SPDX serialization identity; -2. the immutable release-manifest/SBOM artifact identity join; -3. a narrow, bounded check of the actual JSON-LD serialization envelope; and +2. the immutable release-manifest/SBOM artifact identity join, including the exact manifest-backed SBOM SHA-256 digest; +3. a narrow, bounded check of the actual JSON-LD serialization envelope over those exact candidate bytes; and 4. full SPDX content conformance and product-specific package/component completeness. This slice implements claims 1 through 3 only. Claim 4 remains a later independently reviewed release gate. @@ -27,30 +27,37 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - The preliminary envelope gate must not interpret arbitrary JSON-LD context controls or term redefinitions as harmless namespace declarations. - The gate should remain structurally compatible with the official SPDX 3.0.1 JSON Schema until a reviewed schema-aware extension policy exists. - Actual serialized bytes must not be accepted merely because metadata says they are SPDX 3.0.1 JSON-LD. +- A syntactically valid but substituted SPDX document must not satisfy release evidence when its bytes do not match the exact SBOM artifact digest already admitted by the release manifest. - Envelope parsing must be bounded before deeper semantic use and reject malformed UTF-8, malformed JSON, duplicate JSON object members, non-finite JSON constants, unexpected top-level members, non-object graph entries, excessive graph cardinality, and zero or multiple `SpdxDocument` objects. -- Error diagnostics must not reflect attacker- or supplier-controlled document bytes. +- Error diagnostics must not reflect attacker- or supplier-controlled document bytes or release digest values. - The SBOM artifact itself must already be an admitted release artifact with a manifest-bound SHA-256 identity. - Every non-SBOM release artifact in the bound manifest must be represented exactly once in the described-artifact inventory. - Empty, incomplete, duplicate, foreign, self-described, and case-drifted artifact identities must fail closed. -- Partial envelope validation must not be represented as SPDX JSON Schema/OWL/SHACL conformance, package completeness, provenance, signing, publication, installation, update, rollback, or release authority. +- Partial digest/envelope validation must not be represented as SPDX JSON Schema/OWL/SHACL conformance, cryptographic provenance, signing, publication, installation, update, rollback, or release authority. ## Assumptions and authority boundaries - ADR 0015 remains the owning decision for release-manifest identity admission. - `ReleaseSbomFormat::Spdx30JsonLd` identifies SPDX 3.0.1 serialized as JSON-LD and exposes the exact required global context URI. - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. -- The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase SHA-256 identity, and cannot also appear in the binding's described-artifact set. +- The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase `sha256:` identity, and cannot also appear in the binding's described-artifact set. - Every other artifact admitted by that release manifest must be retained in the described-artifact set exactly once. This is an OriginWeave release-inventory completeness rule, not a general SPDX modeling assertion. -- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes only for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, requires exactly the top-level `@context` and `@graph` members, and requires `@context` to equal the exact SPDX 3.0.1 global-context string. Context arrays and inline/remote context extensions fail closed at this stage. -- The exact-string choice is deliberately narrower than the SPDX serialization prose because the official 3.0.1 JSON Schema itself uses that exact-string constraint and because this preliminary verifier does not implement complete JSON-LD context semantics or collision detection against the pinned SPDX context. -- The envelope verifier does not fetch remote contexts or schemas, resolve external identifiers, authenticate artifacts, validate the full SPDX JSON Schema, evaluate the SPDX ontology/SHACL constraints, infer package completeness, or grant any signing/release/update authority. -- Full conformance still requires validation of the same serialized bytes against reviewed SPDX 3.0.1 structural and semantic resources plus OriginWeave product-completeness rules. +- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, requires exactly the top-level `@context` and `@graph` members, and requires `@context` to equal the exact SPDX 3.0.1 global-context string. Context arrays and inline/remote context extensions fail closed at this stage. +- Release admission that composes those bytes with `ReleaseSbomBinding` uses `validate_release_spdx_3_0_1_jsonld_bytes` and supplies the exact canonical lowercase `sha256:` identity from `ReleaseSbomBinding::sbom_artifact()`. The helper recomputes SHA-256 from the bounded candidate bytes and rejects malformed expected identities or mismatches before promoting envelope evidence. +- Digest equality proves correspondence to the declared manifest artifact identity only. It does not authenticate who produced the manifest or bytes and does not replace provenance, signature, transparency, reproducibility, or release-approval checks. +- The exact-string context choice is deliberately narrower than the SPDX serialization prose because the official 3.0.1 JSON Schema itself uses that exact-string constraint and because this preliminary verifier does not implement complete JSON-LD context semantics or collision detection against the pinned SPDX context. +- The verifier does not fetch remote contexts or schemas, resolve external identifiers, validate the full SPDX JSON Schema, evaluate the SPDX ontology/SHACL constraints, infer package completeness, or grant any signing/release/update authority. +- Full conformance still requires validation of the same digest-bound serialized bytes against reviewed SPDX 3.0.1 structural and semantic resources plus OriginWeave product-completeness rules. ## Options considered ### Trust the declared SBOM format without reading bytes -Rejected. A manifest can correctly identify an artifact while the artifact bytes contain the wrong context, malformed JSON, an ambiguous duplicate-key object, or multiple `SpdxDocument` definitions. +Rejected. A manifest can correctly identify an artifact while a different candidate file is supplied, or while the candidate bytes contain the wrong context, malformed JSON, an ambiguous duplicate-key object, or multiple `SpdxDocument` definitions. + +### Validate an SPDX envelope without binding the bytes to the declared artifact digest + +Rejected for release admission. A completely valid SPDX document for another release could pass the envelope gate and be associated with the wrong `ReleaseSbomBinding`. The release-facing helper therefore hashes the exact bounded candidate bytes and compares the canonical `sha256:` identity to the manifest-backed SBOM artifact before returning release evidence. ### Admit the exact SPDX context plus arbitrary inline context objects @@ -78,7 +85,7 @@ Rejected. A non-empty manifest-backed subset can look well formed while silently ### Allow the SBOM artifact to describe itself -Rejected for OriginWeave's release identity contract. A self-referential described-artifact set makes the artifact-to-SBOM join circular at the exact-digest boundary later verification must evaluate. This is an OriginWeave integrity rule, not a claim that SPDX generally forbids such a relationship. +Rejected for OriginWeave's release identity contract. A self-referential described-artifact set makes the artifact-to-SBOM join circular at the exact-digest boundary verification must evaluate. This is an OriginWeave integrity rule, not a claim that SPDX generally forbids such a relationship. ## Decision @@ -91,29 +98,30 @@ OriginWeave adopts the following release-SBOM boundary: 5. The SBOM artifact itself cannot be one of its described artifact identities. 6. Foreign, duplicate, incomplete, and case-drifted described artifact identities fail closed. 7. Described artifact identities are stored deterministically and the manifest's existing bound transitively limits the inventory. -8. Before deeper SPDX processing, actual candidate JSON-LD bytes may pass through `validate_spdx_3_0_1_jsonld_bytes`. -9. That verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB, rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, and requires `@context` to equal the exact versioned SPDX 3.0.1 context string. Context arrays and all additional context entries fail closed. It allows at most 65,536 graph objects, requires object entries with string `type`, and requires exactly one raw `SpdxDocument` under the pinned alias semantics. -10. Validation errors expose stable error codes and generic diagnostics without reflecting document bytes. -11. Envelope success is not a certificate of complete SPDX JSON Schema validity, semantic ontology/SHACL validity, package/component completeness, artifact authenticity, provenance, signatures, publication, installation, update, rollback, or release authority. -12. No verifier fallback may substitute another SPDX version, discard the required pinned context, reinterpret an unvalidated context extension, discard duplicate keys, silently truncate a graph, fetch an ambient remote resource, or convert malformed content into success. +8. Release-facing validation of actual candidate bytes uses `validate_release_spdx_3_0_1_jsonld_bytes` with the exact `sha256:` identity from the manifest-backed SBOM artifact. The expected digest must be `sha256:` followed by 64 lowercase hexadecimal digits; the verifier recomputes SHA-256 over the same bounded bytes and a mismatch fails closed with a value-redacted typed error. +9. Only after exact digest correspondence is established do those candidate bytes pass through the narrow `validate_spdx_3_0_1_jsonld_bytes` envelope contract. The lower-level function remains available for composition and testing but is not by itself release-artifact correspondence evidence. +10. The envelope verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB, rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, and requires `@context` to equal the exact versioned SPDX 3.0.1 context string. Context arrays and all additional context entries fail closed. It allows at most 65,536 graph objects, requires object entries with string `type`, and requires exactly one raw `SpdxDocument` under the pinned alias semantics. +11. Validation errors expose stable error codes and generic diagnostics without reflecting document bytes or expected digest values. +12. Digest/envelope success is not a certificate of complete SPDX JSON Schema validity, semantic ontology/SHACL validity, package/component completeness, producer authenticity, provenance, signatures, publication, installation, update, rollback, or release authority. +13. No verifier fallback may substitute another SPDX version, discard the required pinned context or expected artifact digest, reinterpret an unvalidated context extension, discard duplicate keys, silently truncate a graph, fetch an ambient remote resource, or convert malformed or mismatched content into success. ## Consequences -Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over actual bytes. A candidate cannot pass this layer with the wrong SPDX context, a context array that can alter JSON-LD interpretation, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, or zero/multiple `SpdxDocument` objects. +Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over the exact bytes named by that join. A syntactically valid SPDX document for a different artifact cannot satisfy the release-facing helper merely because its envelope is valid. A candidate also cannot pass this layer with the wrong SPDX context, a context array that can alter JSON-LD interpretation, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, or zero/multiple `SpdxDocument` objects. The exact-context rule can reject a serialization that the SPDX 3.0.1 prose intends to allow through additional namespace mappings. That false negative is accepted at this preliminary gate because the same version's official structural schema currently requires the exact string and because OriginWeave does not yet possess a reviewed, schema-aware rule capable of proving that an extension object is namespace-only and non-colliding. Support for such mappings must be added only together with pinned full structural/semantic validation or another equally strong proof of interpretation. -The boundary remains intentionally narrower than a commercial SBOM generator or complete verifier. The official SPDX 3.0.1 specification requires structural validation against the JSON Schema and semantic validation against the OWL ontology/SHACL constraints; those checks, package/component completeness, root-element rules, artifact digest verification, provenance, signing, and integrated release acceptance remain separate work. Envelope success must never be presented as full SPDX conformance. +The boundary remains intentionally narrower than a commercial SBOM generator or complete verifier. The official SPDX 3.0.1 specification requires structural validation against the JSON Schema and semantic validation against the OWL ontology/SHACL constraints; those checks, package/component completeness, root-element rules, producer/provenance authentication, signing, and integrated release acceptance remain separate work. Digest/envelope success must never be presented as full SPDX conformance or signed supply-chain provenance. ## Failure and degraded behavior -Manifest-binding failures occur before a binding is produced. Envelope failures return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input is rejected before JSON parsing. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, context arrays/extensions, invalid top-level shape, non-object graph entries, excessive graph cardinality, and invalid `SpdxDocument` cardinality all fail closed. +Manifest-binding failures occur before a binding is produced. Release-facing byte verification rejects a malformed expected digest or candidate-byte digest mismatch with deterministic value-redacted error codes. Envelope failures likewise return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input is rejected before hashing or JSON parsing. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, context arrays/extensions, invalid top-level shape, non-object graph entries, excessive graph cardinality, and invalid `SpdxDocument` cardinality all fail closed. -Failure at either layer does not authorize release through another path. There is no alternate version fallback, network context fallback, permissive parse mode, or silent default success. +Failure at either layer does not authorize release through another path. There is no alternate digest, alternate version fallback, network context fallback, permissive parse mode, or silent default success. ## Security / privacy / governance impact -The boundary reduces supply-chain ambiguity without introducing ambient network authority. Bounded parsing constrains memory exposure before deeper validation, duplicate-key rejection prevents parser interpretation drift, and the pinned exact SPDX context prevents version drift, remote `@import`, `@base`/`@vocab` mutation, and term/alias rebinding from being treated as already-admitted envelope semantics. Redacted diagnostics prevent supplier-controlled SBOM bytes from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. +The boundary reduces supply-chain ambiguity without introducing ambient network authority. Exact SHA-256 correspondence prevents a different but otherwise valid SPDX document from being promoted under the manifest-backed SBOM identity. Bounded parsing constrains memory exposure before deeper validation, duplicate-key rejection prevents parser interpretation drift, and the pinned exact SPDX context prevents version drift, remote `@import`, `@base`/`@vocab` mutation, and term/alias rebinding from being treated as already-admitted envelope semantics. Redacted diagnostics prevent supplier-controlled SBOM bytes or expected digest values from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. ## Tests and acceptance evidence @@ -121,6 +129,9 @@ The Rust identity tests must continue to prove exact SPDX specification/context The Python release-verifier contract tests must prove: +- exact candidate bytes match the canonical lowercase manifest-backed `sha256:` identity; +- a valid but substituted SPDX document fails closed on digest mismatch without reflecting document-controlled bytes; +- malformed, uppercase, truncated, or prefixless expected digests fail closed; - exact SPDX 3.0.1 context and one `SpdxDocument` are admitted; - context arrays, including apparently simple namespace mappings, fail closed until schema-aware extension validation exists; - `@import`, `@base`, `@vocab`, `type`, and `spdxId` context overrides fail closed; @@ -132,15 +143,15 @@ Repository Python contracts, Rust 1.97.1 formatting/check/tests/Clippy/rustdoc, ## Migration and rollback -This remains a pre-release branch with no protected-main persisted SBOM schema migration. The verifier has no network or durable state. Before acceptance it can be revised or withdrawn with its owning branch. If a durable external release/SBOM format depends on this boundary, incompatible context, size, graph, or error-contract changes require explicit versioning and migration rather than silent parser broadening. +This remains a pre-release branch with no protected-main persisted SBOM schema migration. The verifier has no network or durable state. Before acceptance it can be revised or withdrawn with its owning branch. If a durable external release/SBOM format depends on this boundary, incompatible digest, context, size, graph, or error-contract changes require explicit versioning and migration rather than silent parser broadening. ## Open follow-ups - Generate deterministic SPDX 3.0.1 SBOM content for supported release packages. -- Validate the same candidate bytes against reviewed, immutable SPDX 3.0.1 JSON Schema and OWL/SHACL resources using offline-verifiable identities. +- Validate the same digest-bound candidate bytes against reviewed, immutable SPDX 3.0.1 JSON Schema and OWL/SHACL resources using offline-verifiable identities. - Reconcile the SPDX 3.0.1 prose allowance for additional namespace mappings with the official 3.0.1 JSON Schema's exact-string `@context` constraint before admitting context composition. - Define package/component completeness and root-element rules for OriginWeave distribution artifacts. -- Verify the bound SBOM artifact digest before content admission and compose the result with SLSA provenance, signing identity, timestamps, platform packages, reproducibility evidence, and updater trust. +- Compose the digest-bound SBOM result with SLSA provenance, signing identity, timestamps, platform packages, reproducibility evidence, and updater trust. - Complete issue #201's integrated release acceptance and rollback/freeze-protection architecture. ## Supersession / reversal conditions From d14221cc21708528405c08a15960446260b37b2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:09:17 -0700 Subject: [PATCH 103/170] test(release): reject symlinked SPDX candidates --- ..._release_spdx_jsonld_file_type_contract.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_release_spdx_jsonld_file_type_contract.py b/tests/test_release_spdx_jsonld_file_type_contract.py index c0c53a220..4d2942693 100644 --- a/tests/test_release_spdx_jsonld_file_type_contract.py +++ b/tests/test_release_spdx_jsonld_file_type_contract.py @@ -14,7 +14,7 @@ class ReleaseSpdxJsonLdFileTypeContractTests(unittest.TestCase): - """Prevent streaming/special files from bypassing the bounded release-input contract.""" + """Prevent streaming/indirect files from bypassing the bounded release-input contract.""" def test_fifo_candidate_is_rejected_before_document_bytes_are_accepted(self) -> None: """A named pipe is not an immutable bounded release artifact input.""" @@ -53,6 +53,25 @@ def write_candidate() -> None: self.assertFalse(writer.is_alive(), "FIFO writer remained blocked after rejection") self.assertEqual(captured.exception.code, "invalid_file_type") + def test_symlink_candidate_is_rejected_instead_of_following_indirect_release_input(self) -> None: + """A symlink is not the regular local artifact path whose bytes are being admitted.""" + + namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_symlink_contract") + read_bounded = namespace["_read_bounded"] + envelope_error = namespace["SpdxJsonLdEnvelopeError"] + + with tempfile.TemporaryDirectory(prefix="originweave-spdx-symlink-") as directory: + root = pathlib.Path(directory) + target = root / "actual.spdx.jsonld" + target.write_bytes(b"{}") + candidate = root / "candidate.spdx.jsonld" + candidate.symlink_to(target.name) + + with self.assertRaises(envelope_error) as captured: + read_bounded(candidate) + + self.assertEqual(captured.exception.code, "invalid_file_type") + if __name__ == "__main__": unittest.main() From 6f339df1e5b3ddb265f4ddd7b262d4de1e0b5e1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:12:00 -0700 Subject: [PATCH 104/170] fix(release): reject indirect SPDX input paths --- scripts/release/validate_spdx_jsonld.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index fff638f35..a9acf1aaa 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -14,6 +14,7 @@ from __future__ import annotations import argparse +import errno import hashlib import hmac import json @@ -161,20 +162,27 @@ def validate_release_spdx_3_0_1_jsonld_bytes( def _nonblocking_read_opener(path: str, flags: int) -> int: - """Open one local candidate without allowing a FIFO/device open to wait indefinitely.""" + """Open one local candidate without following symlinks or waiting on special files.""" - return os.open(path, flags | getattr(os, "O_NONBLOCK", 0)) + return os.open( + path, + flags | getattr(os, "O_NONBLOCK", 0) | getattr(os, "O_NOFOLLOW", 0), + ) def _read_bounded(path: pathlib.Path) -> bytes: - """Read one bounded regular-file candidate without accepting streaming file types.""" + """Read one bounded direct regular-file candidate without accepting indirect/streaming paths.""" try: + if not stat.S_ISREG(path.lstat().st_mode): + raise SpdxJsonLdEnvelopeError("invalid_file_type") with open(path, "rb", opener=_nonblocking_read_opener) as source: if not stat.S_ISREG(os.fstat(source.fileno()).st_mode): raise SpdxJsonLdEnvelopeError("invalid_file_type") payload = source.read(MAX_SPDX_JSONLD_BYTES + 1) except OSError as error: + if error.errno == errno.ELOOP: + raise SpdxJsonLdEnvelopeError("invalid_file_type") from error raise SpdxJsonLdEnvelopeError("read_failed") from error if not payload or len(payload) > MAX_SPDX_JSONLD_BYTES: raise SpdxJsonLdEnvelopeError("invalid_size") From 46c8cbf772097cb0c5fcc0f24e07028bbb854be1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:14:35 -0700 Subject: [PATCH 105/170] test(release): reject symlinked SBOM parent paths --- ...ase_spdx_jsonld_parent_symlink_contract.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/test_release_spdx_jsonld_parent_symlink_contract.py diff --git a/tests/test_release_spdx_jsonld_parent_symlink_contract.py b/tests/test_release_spdx_jsonld_parent_symlink_contract.py new file mode 100644 index 000000000..28b0b5c15 --- /dev/null +++ b/tests/test_release_spdx_jsonld_parent_symlink_contract.py @@ -0,0 +1,39 @@ +"""Regression contract for indirect SPDX JSON-LD release parent paths.""" + +from __future__ import annotations + +import pathlib +import runpy +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +VALIDATOR = ROOT / "scripts" / "release" / "validate_spdx_jsonld.py" + + +class ReleaseSpdxJsonLdParentSymlinkContractTests(unittest.TestCase): + """Require every admitted release-artifact path component to be direct.""" + + def test_symlinked_parent_directory_is_rejected_as_indirect_release_input(self) -> None: + """An ancestor symlink may not redirect release-artifact path authority.""" + + namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_parent_symlink_contract") + read_bounded = namespace["_read_bounded"] + envelope_error = namespace["SpdxJsonLdEnvelopeError"] + + with tempfile.TemporaryDirectory(prefix="originweave-spdx-parent-symlink-") as directory: + root = pathlib.Path(directory) + actual_directory = root / "actual" + actual_directory.mkdir() + (actual_directory / "candidate.spdx.jsonld").write_bytes(b"{}") + alias_directory = root / "alias" + alias_directory.symlink_to(actual_directory.name, target_is_directory=True) + + with self.assertRaises(envelope_error) as captured: + read_bounded(alias_directory / "candidate.spdx.jsonld") + + self.assertEqual(captured.exception.code, "invalid_file_type") + + +if __name__ == "__main__": + unittest.main() From 24616ea069a5ffcc7a5b74a2d62e300f4e6628fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:17:35 -0700 Subject: [PATCH 106/170] fix(release): reject indirect SBOM ancestor paths --- scripts/release/validate_spdx_jsonld.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index a9acf1aaa..be9b79f6d 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -170,15 +170,34 @@ def _nonblocking_read_opener(path: str, flags: int) -> int: ) +def _require_direct_parent_chain(path: pathlib.Path) -> None: + """Reject any existing ancestor that is not a direct directory entry.""" + + parent = path.parent + while parent != parent.parent: + if not stat.S_ISDIR(parent.lstat().st_mode): + raise SpdxJsonLdEnvelopeError("invalid_file_type") + parent = parent.parent + + def _read_bounded(path: pathlib.Path) -> bytes: """Read one bounded direct regular-file candidate without accepting indirect/streaming paths.""" try: - if not stat.S_ISREG(path.lstat().st_mode): + _require_direct_parent_chain(path) + candidate_stat = path.lstat() + if not stat.S_ISREG(candidate_stat.st_mode): raise SpdxJsonLdEnvelopeError("invalid_file_type") with open(path, "rb", opener=_nonblocking_read_opener) as source: - if not stat.S_ISREG(os.fstat(source.fileno()).st_mode): + opened_stat = os.fstat(source.fileno()) + if not stat.S_ISREG(opened_stat.st_mode): + raise SpdxJsonLdEnvelopeError("invalid_file_type") + if (candidate_stat.st_dev, candidate_stat.st_ino) != ( + opened_stat.st_dev, + opened_stat.st_ino, + ): raise SpdxJsonLdEnvelopeError("invalid_file_type") + _require_direct_parent_chain(path) payload = source.read(MAX_SPDX_JSONLD_BYTES + 1) except OSError as error: if error.errno == errno.ELOOP: From 2e44d2d3d8d97cde2a1faee6d225a81fc187e720 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:01:54 -0700 Subject: [PATCH 107/170] test(release): reject transient parent symlink swaps --- ...elease_spdx_jsonld_parent_swap_contract.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_release_spdx_jsonld_parent_swap_contract.py diff --git a/tests/test_release_spdx_jsonld_parent_swap_contract.py b/tests/test_release_spdx_jsonld_parent_swap_contract.py new file mode 100644 index 000000000..3f35451c7 --- /dev/null +++ b/tests/test_release_spdx_jsonld_parent_swap_contract.py @@ -0,0 +1,60 @@ +"""Regression contract for ancestor-swap races in SPDX release input paths.""" + +from __future__ import annotations + +import os +import pathlib +import runpy +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +VALIDATOR = ROOT / "scripts" / "release" / "validate_spdx_jsonld.py" + + +class ReleaseSpdxJsonLdParentSwapContractTests(unittest.TestCase): + """Require path traversal to remain direct for the actual open operation.""" + + @unittest.skipUnless(hasattr(os, "symlink") and hasattr(os, "link"), "requires links") + def test_parent_swap_to_symlink_during_open_fails_closed(self) -> None: + """Pre/post pathname checks must not miss a transient ancestor symlink.""" + + namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_parent_swap_contract") + read_bounded = namespace["_read_bounded"] + envelope_error = namespace["SpdxJsonLdEnvelopeError"] + original_opener = namespace["_nonblocking_read_opener"] + + with tempfile.TemporaryDirectory(prefix="originweave-spdx-parent-swap-") as directory: + root = pathlib.Path(directory) + direct_directory = root / "direct" + direct_directory.mkdir() + actual_directory = root / "actual" + actual_directory.mkdir() + + actual_candidate = actual_directory / "candidate.spdx.jsonld" + actual_candidate.write_bytes(b"{\"indirect\":true}") + direct_candidate = direct_directory / "candidate.spdx.jsonld" + os.link(actual_candidate, direct_candidate) + parked_directory = root / "direct-parked" + + def swapping_opener(path: str, flags: int) -> int: + direct_directory.rename(parked_directory) + direct_directory.symlink_to(actual_directory.name, target_is_directory=True) + try: + return original_opener(path, flags) + finally: + direct_directory.unlink() + parked_directory.rename(direct_directory) + + read_bounded.__globals__["_nonblocking_read_opener"] = swapping_opener + try: + with self.assertRaises(envelope_error) as captured: + read_bounded(direct_candidate) + finally: + read_bounded.__globals__["_nonblocking_read_opener"] = original_opener + + self.assertEqual(captured.exception.code, "invalid_file_type") + + +if __name__ == "__main__": + unittest.main() From a7e9f501e4c99bc0cae86f9c2dd53138555bb2d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:04:31 -0700 Subject: [PATCH 108/170] fix(release): pin SPDX path traversal to directory descriptors --- scripts/release/validate_spdx_jsonld.py | 44 +++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index be9b79f6d..ad6e96df0 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -162,12 +162,44 @@ def validate_release_spdx_3_0_1_jsonld_bytes( def _nonblocking_read_opener(path: str, flags: int) -> int: - """Open one local candidate without following symlinks or waiting on special files.""" + """Open one candidate through a no-follow descriptor-relative component walk. - return os.open( - path, - flags | getattr(os, "O_NONBLOCK", 0) | getattr(os, "O_NOFOLLOW", 0), - ) + Opening every ancestor relative to an already-open directory descriptor prevents a + pathname swap from redirecting a later component through a transient symlink. Platforms + that cannot provide both ``O_NOFOLLOW`` and descriptor-relative ``os.open`` fail closed + instead of silently weakening this direct-path release boundary. + """ + + nofollow = getattr(os, "O_NOFOLLOW", 0) + directory = getattr(os, "O_DIRECTORY", 0) + cloexec = getattr(os, "O_CLOEXEC", 0) + if not nofollow or not directory or os.open not in os.supports_dir_fd: + raise OSError(errno.ENOTSUP, "secure descriptor-relative open is unavailable") + + candidate = pathlib.Path(path) + components = list(candidate.parts) + if candidate.is_absolute(): + anchor = candidate.anchor + components = components[1:] + else: + anchor = "." + if not components or any(component in {"", ".", ".."} for component in components): + raise OSError(errno.ELOOP, "indirect path component is not permitted") + + directory_flags = os.O_RDONLY | directory | nofollow | cloexec + parent_fd = os.open(anchor, directory_flags) + try: + for component in components[:-1]: + next_fd = os.open(component, directory_flags, dir_fd=parent_fd) + os.close(parent_fd) + parent_fd = next_fd + return os.open( + components[-1], + flags | getattr(os, "O_NONBLOCK", 0) | nofollow | cloexec, + dir_fd=parent_fd, + ) + finally: + os.close(parent_fd) def _require_direct_parent_chain(path: pathlib.Path) -> None: @@ -200,7 +232,7 @@ def _read_bounded(path: pathlib.Path) -> bytes: _require_direct_parent_chain(path) payload = source.read(MAX_SPDX_JSONLD_BYTES + 1) except OSError as error: - if error.errno == errno.ELOOP: + if error.errno in {errno.ELOOP, errno.ENOTDIR}: raise SpdxJsonLdEnvelopeError("invalid_file_type") from error raise SpdxJsonLdEnvelopeError("read_failed") from error if not payload or len(payload) > MAX_SPDX_JSONLD_BYTES: From 2532493f00208befc600ac62250fe1539c7f6c99 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 22:29:38 -0700 Subject: [PATCH 109/170] test(release): reject numeric overflow in SPDX JSON --- tests/test_release_spdx_jsonld_envelope_contract.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 552285f3f..65688633d 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -148,6 +148,13 @@ def test_invalid_utf8_and_nonfinite_json_fail_closed(self) -> None: ).encode("utf-8") self._assert_error_code(payload, "invalid_json") + overflow_payload = ( + '{"@context":"' + + CONTEXT + + '","@graph":[{"type":"SpdxDocument","score":1e400}]}' + ).encode("utf-8") + self._assert_error_code(overflow_payload, "invalid_json") + def test_excessive_json_nesting_fails_with_typed_redacted_error(self) -> None: marker = "buyer-secret-marker-must-not-reach-release-diagnostics" depth = 10_000 From 3d065c284fd12967c563f09499eedede9eaf275e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 22:31:57 -0700 Subject: [PATCH 110/170] fix(release): reject non-finite numeric overflow --- scripts/release/validate_spdx_jsonld.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index ad6e96df0..b88d99517 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -18,6 +18,7 @@ import hashlib import hmac import json +import math import os import pathlib import stat @@ -55,6 +56,15 @@ def _reject_nonfinite_constant(_value: str) -> None: raise ValueError("non-finite JSON number") +def _finite_json_float(value: str) -> float: + """Parse one JSON float only when its binary representation remains finite.""" + + parsed = float(value) + if not math.isfinite(parsed): + raise ValueError("non-finite JSON number") + return parsed + + def _has_required_spdx_context(context: Any) -> bool: """Require the exact SPDX 3.0.1 context identity used by the official JSON Schema. @@ -91,6 +101,7 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: text, object_pairs_hook=_object_without_duplicates, parse_constant=_reject_nonfinite_constant, + parse_float=_finite_json_float, ) except _DuplicateJsonKey as error: raise SpdxJsonLdEnvelopeError("duplicate_key") from error From 5c47f9d63cbcc4526825de3be12c81a043d473ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 00:07:36 -0700 Subject: [PATCH 111/170] test(release): pin manifest documentation truth --- ...release_manifest_documentation_contract.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/test_release_manifest_documentation_contract.py diff --git a/tests/test_release_manifest_documentation_contract.py b/tests/test_release_manifest_documentation_contract.py new file mode 100644 index 000000000..5de8c445e --- /dev/null +++ b/tests/test_release_manifest_documentation_contract.py @@ -0,0 +1,44 @@ +"""Regression contracts for release-manifest documentation truth and source citations.""" + +from __future__ import annotations + +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +ADR_PATH = ROOT / "docs/adr/0015-release-manifest-identity.md" +DOCTORING_PATH = ROOT / "docs/doctoring.md" + + +class ReleaseManifestDocumentationContractTests(unittest.TestCase): + """Keep release-manifest portability and Git-source claims narrower than the code proves.""" + + def test_sync_compatibility_claim_matches_the_actual_filename_validator(self) -> None: + """Reserved-device guards must not be presented as full OneDrive/SharePoint compatibility.""" + adr = ADR_PATH.read_text(encoding="utf-8") + doctoring = DOCTORING_PATH.read_text(encoding="utf-8") + + for text in (adr, doctoring): + with self.subTest(document=text[:40]): + self.assertIn("desktop.ini", text) + self.assertIn( + "not a complete OneDrive or SharePoint synchronization-compatibility guarantee", + text, + ) + + self.assertNotIn("common buyer synchronization paths", adr) + self.assertNotIn("does not become a device or synchronization conflict", adr) + + def test_git_protocol_references_pin_the_verified_manual_revisions(self) -> None: + """Protocol evidence must resolve to the manual revisions that contain the cited rules.""" + for path in (ADR_PATH, DOCTORING_PATH): + text = path.read_text(encoding="utf-8") + with self.subTest(path=path): + self.assertIn("gitprotocol-common/2.50.0", text) + self.assertIn("gitprotocol-pack/2.54.0", text) + self.assertNotIn("gitprotocol-common/2.55.0", text) + self.assertNotIn("gitprotocol-pack/2.55.0", text) + + +if __name__ == "__main__": + unittest.main() From 1ca7becfdfcafb2bfe2a59cc636dc63923e07ce8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 00:09:08 -0700 Subject: [PATCH 112/170] test(release): align Git citation revision contracts --- tests/test_release_manifest_documentation_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_release_manifest_documentation_contract.py b/tests/test_release_manifest_documentation_contract.py index 5de8c445e..c666b96d8 100644 --- a/tests/test_release_manifest_documentation_contract.py +++ b/tests/test_release_manifest_documentation_contract.py @@ -31,9 +31,15 @@ def test_sync_compatibility_claim_matches_the_actual_filename_validator(self) -> def test_git_protocol_references_pin_the_verified_manual_revisions(self) -> None: """Protocol evidence must resolve to the manual revisions that contain the cited rules.""" + expected_citations = ( + "Git. (2025). *gitprotocol-common documentation (Git 2.50.0)*.", + "Git. (2026). *gitprotocol-pack documentation (Git 2.54.0)*.", + ) for path in (ADR_PATH, DOCTORING_PATH): text = path.read_text(encoding="utf-8") with self.subTest(path=path): + for citation in expected_citations: + self.assertIn(citation, text) self.assertIn("gitprotocol-common/2.50.0", text) self.assertIn("gitprotocol-pack/2.54.0", text) self.assertNotIn("gitprotocol-common/2.55.0", text) From 15648b35b0408a501634afbce682503d0eaecd63 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 00:11:55 -0700 Subject: [PATCH 113/170] docs(release): narrow manifest portability claims --- docs/adr/0015-release-manifest-identity.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/adr/0015-release-manifest-identity.md b/docs/adr/0015-release-manifest-identity.md index 92679d625..367668513 100644 --- a/docs/adr/0015-release-manifest-identity.md +++ b/docs/adr/0015-release-manifest-identity.md @@ -18,7 +18,7 @@ Git protocol grammar distinguishes a 40-zero `zero-id` from ordinary object iden - The Rust toolchain field must not accept moving aliases or an alternate compiler version as though it were the repository-pinned build identity. - Source commit identity must distinguish a concrete Git object identifier from Git's all-zero protocol sentinel. - Artifact references must remain leaf identities rather than filesystem paths. -- The same manifest must be unambiguous on supported case-sensitive and case-insensitive platforms and common buyer synchronization paths. +- The same manifest must avoid the explicitly modeled ASCII case-fold and Windows reserved-device basename collisions; that bounded rule is not a complete OneDrive or SharePoint synchronization-compatibility guarantee. - Manifest construction must remain inert metadata admission and must not grant signing, publication, installation, update, rollback, or release authority. - Inputs must be bounded and fail closed before later packaging, signing, or updater layers consume them. @@ -45,7 +45,7 @@ Rejected for this slice. Destructively rewriting admitted artifact spelling woul ### Canonical admission with preserved spelling and collision guards -Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, bind exact build-identity fields separately, require the repository-pinned Rust compiler identity, reject Git's all-zero source-identity sentinel, and reject names whose ASCII-case-folded identities collide or whose basenames are reserved or operationally device-like across supported Windows and Microsoft synchronization paths. +Selected. Preserve the admitted artifact spelling, sort artifacts deterministically by that spelling, bind exact build-identity fields separately, require the repository-pinned Rust compiler identity, reject Git's all-zero source-identity sentinel, and reject names whose ASCII-case-folded identities collide or whose basenames fall within the explicitly modeled Windows reserved-device deny set. ## Decision @@ -58,7 +58,7 @@ OriginWeave release-manifest admission is a deterministic, bounded, fail-closed 5. `dependency_lock_sha256` must be exactly `sha256:` followed by 64 lowercase hexadecimal digits. 6. The artifact inventory must be non-empty and contain at most 64 entries. 7. Each artifact name must be a bounded ASCII leaf name containing only alphanumerics, `.`, `_`, and `-`; it cannot contain path separators, traversal-like `..`, leading punctuation, or trailing punctuation. -8. Artifact basenames `CON`, `PRN`, `AUX`, `NUL`, `COM0` through `COM9`, and `LPT0` through `LPT9` are rejected case-insensitively, including when followed by an extension. Microsoft Win32 filename guidance explicitly reserves `COM1`-`COM9` and `LPT1`-`LPT9` and documents `COM0` as a possible Win32 namespace symlink; Microsoft OneDrive and SharePoint additionally reject `COM0`-`COM9` and `LPT0`-`LPT9`. OriginWeave deliberately adopts that stricter portable artifact-name set rather than accept a release leaf name that can become a device or synchronization conflict in a buyer environment. The artifact grammar already rejects the non-ASCII superscript-digit Win32 aliases. +8. Artifact basenames `CON`, `PRN`, `AUX`, `NUL`, `COM0` through `COM9`, and `LPT0` through `LPT9` are rejected case-insensitively, including when followed by an extension. Microsoft Win32 filename guidance explicitly reserves `COM1`-`COM9` and `LPT1`-`LPT9` and documents `COM0` as a possible Win32 namespace symlink; Microsoft OneDrive and SharePoint also restrict additional names, including `desktop.ini`, that this validator does not model. OriginWeave deliberately enforces the listed reserved-device deny set as bounded filename identity hygiene. This is not a complete OneDrive or SharePoint synchronization-compatibility guarantee; packaging or synchronization validation for a target service must separately enforce that service's full current restrictions. The artifact grammar already rejects the non-ASCII superscript-digit Win32 aliases. 9. Artifact names must be unique under ASCII case folding while their original admitted spelling is retained. 10. Each artifact digest must be exactly `sha256:` followed by 64 lowercase hexadecimal digits. 11. Admitted artifacts are stored in deterministic name order. @@ -68,7 +68,7 @@ Constructing or possessing a valid manifest does **not** authenticate an artifac ## Consequences -The release candidate receives one bounded build-and-artifact identity representation that is stable across caller order, records a concrete non-null Git source identity plus the exact repository-pinned Rust toolchain and dependency-lock evidence, and avoids known case-insensitive, Win32 device-name, and Microsoft synchronization-name collisions. Packaging, signing, provenance, and update layers can compose on top of this contract without inheriting ambient authority from it. +The release candidate receives one bounded build-and-artifact identity representation that is stable across caller order, records a concrete non-null Git source identity plus the exact repository-pinned Rust toolchain and dependency-lock evidence, and avoids the explicitly modeled ASCII case-fold and Windows reserved-device basename ambiguities. Packaging, signing, provenance, synchronization validation, and update layers can compose on top of this contract without inheriting ambient authority from it. Changing the protected Rust baseline now requires deliberate convergence of the repository toolchain pin, release-manifest admission contract, tests, and this ADR. A moving channel alias cannot silently change the compiler identity represented by a release manifest. @@ -86,7 +86,7 @@ The boundary reduces null-source-identity, omitted-build-identity, moving-toolch ## Tests and acceptance evidence -The owning `originweave-core` tests must cover valid deterministic ordering, exact artifact and dependency-lock digests, one concrete non-null 40-character source commit, rejection of Git's all-zero source sentinel, the exact pinned Rust toolchain, moving aliases and alternate toolchain versions, malformed toolchain inputs, identifier bounds, malformed names, path/traversal-like names, case-only collisions, `COM0`-`COM9` and `LPT0`-`LPT9` device/synchronization-conflict basenames with and without extensions, neighboring admissible names such as `COM10` and `LPT10`, exact inventory bounds, duplicate names, channel and build-identity access, and deterministic standard error contracts. +The owning `originweave-core` tests must cover valid deterministic ordering, exact artifact and dependency-lock digests, one concrete non-null 40-character source commit, rejection of Git's all-zero source sentinel, the exact pinned Rust toolchain, moving aliases and alternate toolchain versions, malformed toolchain inputs, identifier bounds, malformed names, path/traversal-like names, case-only collisions, `COM0`-`COM9` and `LPT0`-`LPT9` reserved-device basenames with and without extensions, neighboring admissible names such as `COM10` and `LPT10`, exact inventory bounds, duplicate names, channel and build-identity access, and deterministic standard error contracts. Documentation regression tests also keep this bounded filename claim distinct from broader OneDrive/SharePoint restrictions such as `desktop.ini`. Owned production function, line, region, and branch coverage remains exactly 100% on the unchanged reviewed head. CI/security/scanner evidence is exact-head evidence only; predecessor or model-only evidence cannot satisfy acceptance. @@ -109,9 +109,9 @@ Supersede this ADR when a versioned external release-manifest specification repl ## References -Git. (2026). *gitprotocol-common documentation (Git 2.55.0)*. https://git-scm.com/docs/gitprotocol-common/2.55.0 +Git. (2025). *gitprotocol-common documentation (Git 2.50.0)*. https://git-scm.com/docs/gitprotocol-common/2.50.0 -Git. (2026). *gitprotocol-pack documentation (Git 2.55.0)*. https://git-scm.com/docs/gitprotocol-pack/2.55.0 +Git. (2026). *gitprotocol-pack documentation (Git 2.54.0)*. https://git-scm.com/docs/gitprotocol-pack/2.54.0 Microsoft. (n.d.). *Naming files, paths, and namespaces*. Microsoft Learn. Retrieved August 23, 2026, from https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file From ff9be05c61ab500c24b403fc128d17cf4be299aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 00:14:45 -0700 Subject: [PATCH 114/170] docs(release): align portability and Git evidence --- docs/doctoring.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 12b7c4754..52d386654 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -76,7 +76,7 @@ The test-only rcgen 0.14.8 dependency creates a local CA and deterministic certi ### Release artifact filename portability -Microsoft's Win32 filename guidance requires applications not to assume case sensitivity and reserves device basenames including `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9`, including those names followed by extensions. The same documentation identifies non-ASCII superscript-digit device aliases and also documents `COM0` as a possible `Global??` Win32 namespace symlink. Separately, Microsoft's current OneDrive and SharePoint restrictions reject `COM0` through `COM9` and `LPT0` through `LPT9`. OriginWeave therefore adopts the stricter portable release-artifact deny set: ASCII-case-folded duplicate names and `CON`, `PRN`, `AUX`, `NUL`, `COM0` through `COM9`, and `LPT0` through `LPT9`, including extensions, are rejected so an admitted artifact does not become a device or synchronization conflict in a buyer environment. The ASCII-only grammar separately excludes the superscript aliases. These checks preserve the original artifact spelling and remain identity hygiene only; they do not grant signing, publication, installation, update, rollback, or release authority. +Microsoft's Win32 filename guidance requires applications not to assume case sensitivity and reserves device basenames including `CON`, `PRN`, `AUX`, `NUL`, `COM1` through `COM9`, and `LPT1` through `LPT9`, including those names followed by extensions. The same documentation identifies non-ASCII superscript-digit device aliases and also documents `COM0` as a possible `Global??` Win32 namespace symlink. Separately, Microsoft's current OneDrive and SharePoint restrictions reject `COM0` through `COM9` and `LPT0` through `LPT9` and additional names including `desktop.ini`. OriginWeave therefore adopts a bounded release-artifact identity deny set: ASCII-case-folded duplicate names and `CON`, `PRN`, `AUX`, `NUL`, `COM0` through `COM9`, and `LPT0` through `LPT9`, including extensions, are rejected. The ASCII-only grammar separately excludes the superscript aliases. This bounded filename rule is not a complete OneDrive or SharePoint synchronization-compatibility guarantee; packaging or synchronization validation for a target service must separately enforce that service's full current restrictions. These checks preserve the original artifact spelling and remain identity hygiene only; they do not grant signing, publication, installation, update, rollback, or release authority. ### Git source identity admission @@ -134,9 +134,9 @@ Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). Fugu Team, Sakana AI. (2026). *Sakana Fugu technical report* [Technical report]. arXiv. https://doi.org/10.48550/arXiv.2606.21228 -Git. (2026). *gitprotocol-common documentation (Git 2.55.0)*. https://git-scm.com/docs/gitprotocol-common/2.55.0 +Git. (2025). *gitprotocol-common documentation (Git 2.50.0)*. https://git-scm.com/docs/gitprotocol-common/2.50.0 -Git. (2026). *gitprotocol-pack documentation (Git 2.55.0)*. https://git-scm.com/docs/gitprotocol-pack/2.55.0 +Git. (2026). *gitprotocol-pack documentation (Git 2.54.0)*. https://git-scm.com/docs/gitprotocol-pack/2.54.0 Huston, G., & Buraglio, N. (2024). *Expanding the IPv6 documentation space* (RFC 9637). Internet Engineering Task Force. https://doi.org/10.17487/RFC9637 @@ -186,4 +186,4 @@ World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 -Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 +Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 \ No newline at end of file From 0aed213ec98c649f7671aac921418a248fc4f29d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 00:16:19 -0700 Subject: [PATCH 115/170] test(release): cover changelog portability truth --- tests/test_release_manifest_documentation_contract.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_release_manifest_documentation_contract.py b/tests/test_release_manifest_documentation_contract.py index c666b96d8..3c0a4c18e 100644 --- a/tests/test_release_manifest_documentation_contract.py +++ b/tests/test_release_manifest_documentation_contract.py @@ -7,6 +7,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] ADR_PATH = ROOT / "docs/adr/0015-release-manifest-identity.md" +CHANGELOG_PATH = ROOT / "CHANGELOG.md" DOCTORING_PATH = ROOT / "docs/doctoring.md" @@ -16,9 +17,10 @@ class ReleaseManifestDocumentationContractTests(unittest.TestCase): def test_sync_compatibility_claim_matches_the_actual_filename_validator(self) -> None: """Reserved-device guards must not be presented as full OneDrive/SharePoint compatibility.""" adr = ADR_PATH.read_text(encoding="utf-8") + changelog = CHANGELOG_PATH.read_text(encoding="utf-8") doctoring = DOCTORING_PATH.read_text(encoding="utf-8") - for text in (adr, doctoring): + for text in (adr, changelog, doctoring): with self.subTest(document=text[:40]): self.assertIn("desktop.ini", text) self.assertIn( @@ -28,6 +30,10 @@ def test_sync_compatibility_claim_matches_the_actual_filename_validator(self) -> self.assertNotIn("common buyer synchronization paths", adr) self.assertNotIn("does not become a device or synchronization conflict", adr) + self.assertNotIn( + "cannot become Windows device or Microsoft synchronization conflicts", + changelog, + ) def test_git_protocol_references_pin_the_verified_manual_revisions(self) -> None: """Protocol evidence must resolve to the manual revisions that contain the cited rules.""" From c2e581fc5b354f4ff0cf2e5efc83a5b1801c3044 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 00:17:47 -0700 Subject: [PATCH 116/170] docs(release): bound changelog portability claim --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 592ffccf5..1a9605b9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security -- Release artifact admission rejects `COM0` through `COM9` and `LPT0` through `LPT9` case-insensitively, including extensions, so package names cannot become Windows device or Microsoft synchronization conflicts in buyer environments. +- Release artifact admission rejects `COM0` through `COM9` and `LPT0` through `LPT9` case-insensitively, including extensions, as bounded filename identity hygiene. OneDrive and SharePoint impose additional restrictions, including `desktop.ini`, that this validator does not model; this is not a complete OneDrive or SharePoint synchronization-compatibility guarantee. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. - Crawler mode is read-only, must pair with the public-crawl purpose, and fails closed without an applicable robots-policy decision. From f9c98ad81eb5b05d545c4fba7f4454b3fb8043bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:57:43 -0700 Subject: [PATCH 117/170] test(release): reject final-path swap after SPDX open --- ..._release_spdx_jsonld_file_type_contract.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_release_spdx_jsonld_file_type_contract.py b/tests/test_release_spdx_jsonld_file_type_contract.py index 4d2942693..14db9f2da 100644 --- a/tests/test_release_spdx_jsonld_file_type_contract.py +++ b/tests/test_release_spdx_jsonld_file_type_contract.py @@ -72,6 +72,35 @@ def test_symlink_candidate_is_rejected_instead_of_following_indirect_release_inp self.assertEqual(captured.exception.code, "invalid_file_type") + def test_candidate_path_replaced_after_open_is_rejected(self) -> None: + """The admitted descriptor must still own the candidate path after the bounded read.""" + + namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_leaf_swap_contract") + read_bounded = namespace["_read_bounded"] + envelope_error = namespace["SpdxJsonLdEnvelopeError"] + original_opener = namespace["_nonblocking_read_opener"] + + with tempfile.TemporaryDirectory(prefix="originweave-spdx-leaf-swap-") as directory: + root = pathlib.Path(directory) + candidate = root / "candidate.spdx.jsonld" + displaced = root / "displaced.spdx.jsonld" + candidate.write_bytes(b'{"@context":"https://spdx.org/rdf/3.0.1/spdx-context.jsonld","@graph":[{"type":"SpdxDocument"}]}') + + def swap_after_open(path: str, flags: int) -> int: + descriptor = original_opener(path, flags) + candidate.rename(displaced) + candidate.write_bytes(b"{}") + return descriptor + + read_bounded.__globals__["_nonblocking_read_opener"] = swap_after_open + + with self.assertRaises(envelope_error) as captured: + read_bounded(candidate) + + self.assertEqual(captured.exception.code, "invalid_file_type") + self.assertEqual(candidate.read_bytes(), b"{}") + self.assertTrue(displaced.exists()) + if __name__ == "__main__": unittest.main() From 12f20d715041b7fbc027edd4ad61801ebf847d2a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 12:01:12 -0700 Subject: [PATCH 118/170] fix(release): revalidate SPDX leaf identity after read --- scripts/release/validate_spdx_jsonld.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index b88d99517..57c981522 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -242,6 +242,13 @@ def _read_bounded(path: pathlib.Path) -> bytes: raise SpdxJsonLdEnvelopeError("invalid_file_type") _require_direct_parent_chain(path) payload = source.read(MAX_SPDX_JSONLD_BYTES + 1) + _require_direct_parent_chain(path) + final_stat = path.lstat() + if not stat.S_ISREG(final_stat.st_mode) or ( + final_stat.st_dev, + final_stat.st_ino, + ) != (opened_stat.st_dev, opened_stat.st_ino): + raise SpdxJsonLdEnvelopeError("invalid_file_type") except OSError as error: if error.errno in {errno.ELOOP, errno.ENOTDIR}: raise SpdxJsonLdEnvelopeError("invalid_file_type") from error From b375dade11ada146814cf0314148bb6a32c068f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 12:07:24 -0700 Subject: [PATCH 119/170] docs(release): record direct SPDX file identity hardening --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69ec69969..2b3642a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security - The SPDX 3.0.1 release-envelope gate requires the exact versioned global-context string and rejects context arrays, `@import`, `@base`, `@vocab`, and alias/term-rebinding attempts until reviewed schema-aware validation can prove extension semantics without ambient or reinterpretation authority. +- SPDX JSON-LD release-file admission now requires a direct regular-file leaf reached through a no-follow descriptor-relative path walk and revalidates the final leaf's device/inode identity after the bounded read, so symlinks, FIFOs, swapped ancestors, and post-open pathname replacement fail closed instead of silently changing the artifact path identity. - Release artifact admission rejects `COM0` through `COM9` and `LPT0` through `LPT9` case-insensitively, including extensions, as bounded filename identity hygiene. OneDrive and SharePoint impose additional restrictions, including `desktop.ini`, that this validator does not model; this is not a complete OneDrive or SharePoint synchronization-compatibility guarantee. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. From 1d1ab65b09c9a2475594eaa9d5011bf29493a3a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 12:15:24 -0700 Subject: [PATCH 120/170] docs: mark merged MCP routing as protected-main truth --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b3642a8c..3400ccd96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. -- Active PR #168 adds deterministic MCP `2026-07-28` stateless tool-routing foundations with bounded names, a single reviewed tool-to-action registry shared by routing and discovery metadata, and fail-closed policy binding that grants no ambient authority. This is active-PR evidence only; the complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned until separately integrated on protected `main`. +- Protected `main` includes deterministic MCP `2026-07-28` stateless tool-routing foundations with bounded names, a single reviewed tool-to-action registry shared by routing and discovery metadata, and fail-closed policy binding that grants no ambient authority. The complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned until separately integrated. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance. From bc898a31744faa528a4615b3eaf975a5dd446717 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 14:06:22 -0700 Subject: [PATCH 121/170] test(release): reject parent identity replacement --- ..._release_spdx_jsonld_file_type_contract.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_release_spdx_jsonld_file_type_contract.py b/tests/test_release_spdx_jsonld_file_type_contract.py index 14db9f2da..bf0db5d75 100644 --- a/tests/test_release_spdx_jsonld_file_type_contract.py +++ b/tests/test_release_spdx_jsonld_file_type_contract.py @@ -101,6 +101,37 @@ def swap_after_open(path: str, flags: int) -> int: self.assertEqual(candidate.read_bytes(), b"{}") self.assertTrue(displaced.exists()) + def test_parent_directory_replaced_with_same_leaf_inode_is_rejected(self) -> None: + """A release path must retain its admitted parent-directory identity through opening.""" + + namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_parent_identity_contract") + read_bounded = namespace["_read_bounded"] + envelope_error = namespace["SpdxJsonLdEnvelopeError"] + original_opener = namespace["_nonblocking_read_opener"] + + with tempfile.TemporaryDirectory(prefix="originweave-spdx-parent-identity-") as directory: + root = pathlib.Path(directory) + parent = root / "candidate-parent" + displaced_parent = root / "displaced-parent" + parent.mkdir() + candidate = parent / "candidate.spdx.jsonld" + candidate.write_bytes(b'{"@context":"https://spdx.org/rdf/3.0.1/spdx-context.jsonld","@graph":[{"type":"SpdxDocument"}]}') + original_inode = candidate.stat().st_ino + + def replace_parent_before_open(path: str, flags: int) -> int: + parent.rename(displaced_parent) + parent.mkdir() + os.link(displaced_parent / candidate.name, parent / candidate.name) + self.assertEqual((parent / candidate.name).stat().st_ino, original_inode) + return original_opener(path, flags) + + read_bounded.__globals__["_nonblocking_read_opener"] = replace_parent_before_open + + with self.assertRaises(envelope_error) as captured: + read_bounded(candidate) + + self.assertEqual(captured.exception.code, "invalid_file_type") + if __name__ == "__main__": unittest.main() From e0e85391d0f4a8831f68a8559ff3e38d1b5e087e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 14:10:08 -0700 Subject: [PATCH 122/170] fix(release): bind SPDX parent directory identity --- scripts/release/validate_spdx_jsonld.py | 131 ++++++++++++++++-------- 1 file changed, 88 insertions(+), 43 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 57c981522..f64d7e0fb 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -14,6 +14,7 @@ from __future__ import annotations import argparse +import contextvars import errno import hashlib import hmac @@ -29,6 +30,9 @@ SPDX_3_0_1_CONTEXT = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" MAX_SPDX_JSONLD_BYTES = 16 * 1024 * 1024 MAX_SPDX_GRAPH_OBJECTS = 65_536 +_EXPECTED_PARENT_IDENTITIES: contextvars.ContextVar[tuple[tuple[int, int], ...]] = ( + contextvars.ContextVar("spdx_expected_parent_identities", default=()) +) class SpdxJsonLdEnvelopeError(ValueError): @@ -172,13 +176,60 @@ def validate_release_spdx_3_0_1_jsonld_bytes( return summary +def _path_components(path: pathlib.Path) -> tuple[str, list[str]]: + """Return the direct lexical anchor and components used by the secure open walk.""" + + components = list(path.parts) + if path.is_absolute(): + anchor = path.anchor + components = components[1:] + else: + anchor = "." + if not components or any(component in {"", ".", ".."} for component in components): + raise OSError(errno.ELOOP, "indirect path component is not permitted") + return anchor, components + + +def _direct_parent_identities(path: pathlib.Path) -> tuple[tuple[int, int], ...]: + """Capture the exact directory identities that own one direct candidate path.""" + + anchor, components = _path_components(path) + current = pathlib.Path(anchor) + identities: list[tuple[int, int]] = [] + for component in [None, *components[:-1]]: + if component is not None: + current = current / component + parent_stat = current.lstat() + if not stat.S_ISDIR(parent_stat.st_mode): + raise SpdxJsonLdEnvelopeError("invalid_file_type") + identities.append((parent_stat.st_dev, parent_stat.st_ino)) + return tuple(identities) + + +def _require_expected_directory_identity( + descriptor: int, + expected_identity: tuple[int, int], +) -> None: + """Fail the descriptor walk when an admitted directory identity has changed.""" + + opened_stat = os.fstat(descriptor) + if not stat.S_ISDIR(opened_stat.st_mode) or ( + opened_stat.st_dev, + opened_stat.st_ino, + ) != expected_identity: + raise OSError(errno.ELOOP, "direct path parent identity changed") + + def _nonblocking_read_opener(path: str, flags: int) -> int: - """Open one candidate through a no-follow descriptor-relative component walk. + """Open one candidate through an identity-bound descriptor-relative component walk. Opening every ancestor relative to an already-open directory descriptor prevents a - pathname swap from redirecting a later component through a transient symlink. Platforms - that cannot provide both ``O_NOFOLLOW`` and descriptor-relative ``os.open`` fail closed - instead of silently weakening this direct-path release boundary. + pathname swap from redirecting a later component through a transient symlink. Every + opened directory must also retain the identity captured by the caller before leaf + admission, so replacing a parent with a different real directory cannot preserve trust + merely by hard-linking the same leaf inode into the replacement. Platforms that cannot + provide both ``O_NOFOLLOW`` and descriptor-relative ``os.open`` fail closed instead of + silently weakening this direct-path release boundary. """ nofollow = getattr(os, "O_NOFOLLOW", 0) @@ -188,22 +239,20 @@ def _nonblocking_read_opener(path: str, flags: int) -> int: raise OSError(errno.ENOTSUP, "secure descriptor-relative open is unavailable") candidate = pathlib.Path(path) - components = list(candidate.parts) - if candidate.is_absolute(): - anchor = candidate.anchor - components = components[1:] - else: - anchor = "." - if not components or any(component in {"", ".", ".."} for component in components): - raise OSError(errno.ELOOP, "indirect path component is not permitted") + anchor, components = _path_components(candidate) + expected_parent_identities = _EXPECTED_PARENT_IDENTITIES.get() + if len(expected_parent_identities) != len(components): + raise OSError(errno.ELOOP, "direct path parent identity is unavailable") directory_flags = os.O_RDONLY | directory | nofollow | cloexec parent_fd = os.open(anchor, directory_flags) try: - for component in components[:-1]: + _require_expected_directory_identity(parent_fd, expected_parent_identities[0]) + for index, component in enumerate(components[:-1], start=1): next_fd = os.open(component, directory_flags, dir_fd=parent_fd) os.close(parent_fd) parent_fd = next_fd + _require_expected_directory_identity(parent_fd, expected_parent_identities[index]) return os.open( components[-1], flags | getattr(os, "O_NONBLOCK", 0) | nofollow | cloexec, @@ -213,42 +262,38 @@ def _nonblocking_read_opener(path: str, flags: int) -> int: os.close(parent_fd) -def _require_direct_parent_chain(path: pathlib.Path) -> None: - """Reject any existing ancestor that is not a direct directory entry.""" - - parent = path.parent - while parent != parent.parent: - if not stat.S_ISDIR(parent.lstat().st_mode): - raise SpdxJsonLdEnvelopeError("invalid_file_type") - parent = parent.parent - - def _read_bounded(path: pathlib.Path) -> bytes: - """Read one bounded direct regular-file candidate without accepting indirect/streaming paths.""" + """Read one bounded direct regular-file candidate with stable path-owner identities.""" try: - _require_direct_parent_chain(path) + expected_parent_identities = _direct_parent_identities(path) candidate_stat = path.lstat() if not stat.S_ISREG(candidate_stat.st_mode): raise SpdxJsonLdEnvelopeError("invalid_file_type") - with open(path, "rb", opener=_nonblocking_read_opener) as source: - opened_stat = os.fstat(source.fileno()) - if not stat.S_ISREG(opened_stat.st_mode): - raise SpdxJsonLdEnvelopeError("invalid_file_type") - if (candidate_stat.st_dev, candidate_stat.st_ino) != ( - opened_stat.st_dev, - opened_stat.st_ino, - ): - raise SpdxJsonLdEnvelopeError("invalid_file_type") - _require_direct_parent_chain(path) - payload = source.read(MAX_SPDX_JSONLD_BYTES + 1) - _require_direct_parent_chain(path) - final_stat = path.lstat() - if not stat.S_ISREG(final_stat.st_mode) or ( - final_stat.st_dev, - final_stat.st_ino, - ) != (opened_stat.st_dev, opened_stat.st_ino): - raise SpdxJsonLdEnvelopeError("invalid_file_type") + identity_token = _EXPECTED_PARENT_IDENTITIES.set(expected_parent_identities) + try: + with open(path, "rb", opener=_nonblocking_read_opener) as source: + opened_stat = os.fstat(source.fileno()) + if not stat.S_ISREG(opened_stat.st_mode): + raise SpdxJsonLdEnvelopeError("invalid_file_type") + if (candidate_stat.st_dev, candidate_stat.st_ino) != ( + opened_stat.st_dev, + opened_stat.st_ino, + ): + raise SpdxJsonLdEnvelopeError("invalid_file_type") + if _direct_parent_identities(path) != expected_parent_identities: + raise SpdxJsonLdEnvelopeError("invalid_file_type") + payload = source.read(MAX_SPDX_JSONLD_BYTES + 1) + if _direct_parent_identities(path) != expected_parent_identities: + raise SpdxJsonLdEnvelopeError("invalid_file_type") + final_stat = path.lstat() + if not stat.S_ISREG(final_stat.st_mode) or ( + final_stat.st_dev, + final_stat.st_ino, + ) != (opened_stat.st_dev, opened_stat.st_ino): + raise SpdxJsonLdEnvelopeError("invalid_file_type") + finally: + _EXPECTED_PARENT_IDENTITIES.reset(identity_token) except OSError as error: if error.errno in {errno.ELOOP, errno.ENOTDIR}: raise SpdxJsonLdEnvelopeError("invalid_file_type") from error From b247baa164eeef51223d47ea6870315f8a7bb1c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 15:03:16 -0700 Subject: [PATCH 123/170] test(release): reject nested duplicate SPDX documents --- ...t_release_spdx_jsonld_envelope_contract.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 65688633d..439da9ec7 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -139,6 +139,28 @@ def test_exactly_one_spdx_document_is_required(self) -> None: "invalid_document_count", ) + def test_nested_second_spdx_document_fails_closed(self) -> None: + self._assert_error_code( + self._payload( + [ + {"type": "CreationInfo", "@id": "_:creation", "specVersion": "3.0.1"}, + { + "type": "SpdxDocument", + "spdxId": "https://example.invalid/spdx/document", + "creationInfo": "_:creation", + "element": [ + { + "type": "SpdxDocument", + "spdxId": "https://example.invalid/spdx/nested-document", + "creationInfo": "_:creation", + } + ], + }, + ] + ), + "invalid_document_count", + ) + def test_invalid_utf8_and_nonfinite_json_fail_closed(self) -> None: self._assert_error_code(b"\xff", "invalid_utf8") payload = ( From 8caf156bcb69a0425613a40adff335d5c95e47b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 15:07:48 -0700 Subject: [PATCH 124/170] fix(release): reject nested duplicate SPDX documents --- scripts/release/validate_spdx_jsonld.py | 29 ++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index f64d7e0fb..0e72ebc40 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -82,6 +82,31 @@ def _has_required_spdx_context(context: Any) -> bool: return context == SPDX_3_0_1_CONTEXT +def _count_spdx_documents(value: Any) -> int: + """Count SPDX document elements at any depth without recursive interpreter use. + + SPDX element collections may inline other elements below the top-level graph. The + serialization contract admits at most one ``SpdxDocument`` element, so a nested second + document must not evade the preliminary envelope gate merely because it is not a direct + ``@graph`` member. The walk is iterative and remains bounded by the already size-bounded + parsed input. + """ + + count = 0 + pending = [value] + while pending: + current = pending.pop() + if isinstance(current, dict): + if current.get("type") == "SpdxDocument": + count += 1 + if count > 1: + return count + pending.extend(current.values()) + elif isinstance(current, list): + pending.extend(current) + return count + + def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: """Validate the bounded SPDX 3.0.1 JSON-LD serialization envelope. @@ -123,13 +148,11 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: if len(graph) > MAX_SPDX_GRAPH_OBJECTS: raise SpdxJsonLdEnvelopeError("too_many_graph_objects") - document_count = 0 for element in graph: if not isinstance(element, dict) or not isinstance(element.get("type"), str): raise SpdxJsonLdEnvelopeError("invalid_graph_object") - if element["type"] == "SpdxDocument": - document_count += 1 + document_count = _count_spdx_documents(graph) if document_count != 1: raise SpdxJsonLdEnvelopeError("invalid_document_count") From 62120e5802448ac65dd8aeeda65ddbc2457f86e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 15:31:15 -0700 Subject: [PATCH 125/170] test(release): prevent SPDX payload retention in parse errors --- ...t_release_spdx_error_redaction_contract.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/test_release_spdx_error_redaction_contract.py diff --git a/tests/test_release_spdx_error_redaction_contract.py b/tests/test_release_spdx_error_redaction_contract.py new file mode 100644 index 000000000..ec9894e08 --- /dev/null +++ b/tests/test_release_spdx_error_redaction_contract.py @@ -0,0 +1,48 @@ +"""Regression tests for payload-safe SPDX parser failure diagnostics.""" + +from __future__ import annotations + +import pathlib +import runpy +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +VALIDATOR = ROOT / "scripts" / "release" / "validate_spdx_jsonld.py" +CONTEXT = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" + + +class ReleaseSpdxErrorRedactionContractTests(unittest.TestCase): + """Ensure raised validation errors retain no external document payload object.""" + + @classmethod + def setUpClass(cls) -> None: + cls.namespace = runpy.run_path(str(VALIDATOR), run_name="release_spdx_error_redaction") + cls.validate = staticmethod(cls.namespace["validate_spdx_3_0_1_jsonld_bytes"]) + cls.error_type = cls.namespace["SpdxJsonLdEnvelopeError"] + + def _assert_payload_free_error(self, payload: bytes, expected_code: str) -> None: + with self.assertRaises(self.error_type) as captured: + self.validate(payload) + error = captured.exception + self.assertEqual(error.code, expected_code) + self.assertIsNone(error.__cause__) + self.assertIsNone(error.__context__) + + def test_invalid_utf8_does_not_survive_in_exception_chain(self) -> None: + marker = b"buyer-secret-marker-must-not-survive-validation" + self._assert_payload_free_error(marker + b"\xff", "invalid_utf8") + + def test_malformed_json_does_not_survive_in_exception_chain(self) -> None: + marker = "buyer-secret-marker-must-not-survive-validation" + payload = ( + '{"@context":"' + + CONTEXT + + '","@graph":[{"type":"SpdxDocument","private":"' + + marker + + '",}]}' + ).encode("utf-8") + self._assert_payload_free_error(payload, "invalid_json") + + +if __name__ == "__main__": + unittest.main() From f8abe9e5427fc4e691f494893b8e002068899b31 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 15:32:34 -0700 Subject: [PATCH 126/170] fix(release): detach SPDX payloads from parser errors --- scripts/release/validate_spdx_jsonld.py | 40 +++++++++++++++++-------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 0e72ebc40..b5cb7ae6f 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -110,21 +110,30 @@ def _count_spdx_documents(value: Any) -> int: def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: """Validate the bounded SPDX 3.0.1 JSON-LD serialization envelope. - External document bytes are never included in raised errors. Successful validation - proves only the narrow envelope contract documented by this module; callers must still - perform the official SPDX JSON Schema and OWL/SHACL validation before claiming SPDX - conformance. Excessive JSON nesting is treated as invalid external input instead of - escaping the typed, value-redacted validation boundary. + External document bytes are never included in raised errors or retained by their + exception chain. Successful validation proves only the narrow envelope contract + documented by this module; callers must still perform the official SPDX JSON Schema and + OWL/SHACL validation before claiming SPDX conformance. Excessive JSON nesting is treated + as invalid external input instead of escaping the typed, value-redacted validation + boundary. """ if not isinstance(payload, bytes) or not payload or len(payload) > MAX_SPDX_JSONLD_BYTES: raise SpdxJsonLdEnvelopeError("invalid_size") + invalid_utf8 = False try: text = payload.decode("utf-8", errors="strict") - except UnicodeDecodeError as error: - raise SpdxJsonLdEnvelopeError("invalid_utf8") from error - + except UnicodeDecodeError: + invalid_utf8 = True + text = "" + if invalid_utf8: + # UnicodeDecodeError retains the complete input bytes in its ``object`` attribute. + # Raise only after leaving the handler so the public error keeps no payload-bearing + # ``__cause__`` or ``__context__`` reference. + raise SpdxJsonLdEnvelopeError("invalid_utf8") + + parse_error_code: str | None = None try: decoded = json.loads( text, @@ -132,10 +141,17 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: parse_constant=_reject_nonfinite_constant, parse_float=_finite_json_float, ) - except _DuplicateJsonKey as error: - raise SpdxJsonLdEnvelopeError("duplicate_key") from error - except (json.JSONDecodeError, RecursionError, ValueError) as error: - raise SpdxJsonLdEnvelopeError("invalid_json") from error + except _DuplicateJsonKey: + parse_error_code = "duplicate_key" + decoded = None + except (json.JSONDecodeError, RecursionError, ValueError): + parse_error_code = "invalid_json" + decoded = None + if parse_error_code is not None: + # JSONDecodeError retains the complete decoded document in its ``doc`` attribute. + # Do not chain parser exceptions that could therefore turn private SBOM bytes into + # logging, tracing, or support-bundle data downstream. + raise SpdxJsonLdEnvelopeError(parse_error_code) if not isinstance(decoded, dict) or set(decoded) != {"@context", "@graph"}: raise SpdxJsonLdEnvelopeError("invalid_top_level") From 90026e890c7cd5431028fde83c39066846652414 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 16:39:52 -0700 Subject: [PATCH 127/170] test(release): redact failed SPDX path evidence --- ...st_release_spdx_error_redaction_contract.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_release_spdx_error_redaction_contract.py b/tests/test_release_spdx_error_redaction_contract.py index ec9894e08..a1cacb152 100644 --- a/tests/test_release_spdx_error_redaction_contract.py +++ b/tests/test_release_spdx_error_redaction_contract.py @@ -2,8 +2,10 @@ from __future__ import annotations +import errno import pathlib import runpy +import tempfile import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] @@ -18,6 +20,7 @@ class ReleaseSpdxErrorRedactionContractTests(unittest.TestCase): def setUpClass(cls) -> None: cls.namespace = runpy.run_path(str(VALIDATOR), run_name="release_spdx_error_redaction") cls.validate = staticmethod(cls.namespace["validate_spdx_3_0_1_jsonld_bytes"]) + cls.read_bounded = staticmethod(cls.namespace["_read_bounded"]) cls.error_type = cls.namespace["SpdxJsonLdEnvelopeError"] def _assert_payload_free_error(self, payload: bytes, expected_code: str) -> None: @@ -43,6 +46,21 @@ def test_malformed_json_does_not_survive_in_exception_chain(self) -> None: ).encode("utf-8") self._assert_payload_free_error(payload, "invalid_json") + def test_failed_file_path_does_not_survive_in_exception_source(self) -> None: + marker = "buyer-secret-path-marker-must-not-survive-validation" + with tempfile.TemporaryDirectory() as temporary_directory: + missing_path = pathlib.Path(temporary_directory) / marker + with self.assertRaises(self.error_type) as captured: + self.read_bounded(missing_path) + + error = captured.exception + self.assertEqual(error.code, "read_failed") + self.assertIsInstance(error.__cause__, OSError) + self.assertEqual(error.__cause__.errno, errno.ENOENT) + self.assertIsNone(error.__cause__.filename) + self.assertNotIn(marker, str(error.__cause__)) + self.assertNotIn(marker, repr(error.__cause__)) + if __name__ == "__main__": unittest.main() From 45071dae31abeec9a68161f931c841ec258880bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 16:42:55 -0700 Subject: [PATCH 128/170] fix(release): redact SPDX file read causes --- scripts/release/validate_spdx_jsonld.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index b5cb7ae6f..ff580eb21 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -334,9 +334,10 @@ def _read_bounded(path: pathlib.Path) -> bytes: finally: _EXPECTED_PARENT_IDENTITIES.reset(identity_token) except OSError as error: + redacted_error = OSError(error.errno, "release SBOM file operation failed") if error.errno in {errno.ELOOP, errno.ENOTDIR}: - raise SpdxJsonLdEnvelopeError("invalid_file_type") from error - raise SpdxJsonLdEnvelopeError("read_failed") from error + raise SpdxJsonLdEnvelopeError("invalid_file_type") from redacted_error + raise SpdxJsonLdEnvelopeError("read_failed") from redacted_error if not payload or len(payload) > MAX_SPDX_JSONLD_BYTES: raise SpdxJsonLdEnvelopeError("invalid_size") return payload From 8c1cde46968010f379b8fcc511056c378a71c94f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 18:06:41 -0700 Subject: [PATCH 129/170] test(release): reject path retention in exception context --- tests/test_release_spdx_error_redaction_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_release_spdx_error_redaction_contract.py b/tests/test_release_spdx_error_redaction_contract.py index a1cacb152..7bf5fc081 100644 --- a/tests/test_release_spdx_error_redaction_contract.py +++ b/tests/test_release_spdx_error_redaction_contract.py @@ -60,6 +60,8 @@ def test_failed_file_path_does_not_survive_in_exception_source(self) -> None: self.assertIsNone(error.__cause__.filename) self.assertNotIn(marker, str(error.__cause__)) self.assertNotIn(marker, repr(error.__cause__)) + self.assertIsNone(error.__cause__.__context__) + self.assertIsNone(error.__context__) if __name__ == "__main__": From 9b38a5934f204944ab38eba0d7f02e97c63b4505 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 18:11:21 -0700 Subject: [PATCH 130/170] fix(release): redact implicit file error context --- scripts/release/validate_spdx_jsonld.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index ff580eb21..15cfcec4a 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -304,6 +304,8 @@ def _nonblocking_read_opener(path: str, flags: int) -> int: def _read_bounded(path: pathlib.Path) -> bytes: """Read one bounded direct regular-file candidate with stable path-owner identities.""" + read_error_code: str | None = None + redacted_error: OSError | None = None try: expected_parent_identities = _direct_parent_identities(path) candidate_stat = path.lstat() @@ -335,9 +337,16 @@ def _read_bounded(path: pathlib.Path) -> bytes: _EXPECTED_PARENT_IDENTITIES.reset(identity_token) except OSError as error: redacted_error = OSError(error.errno, "release SBOM file operation failed") - if error.errno in {errno.ELOOP, errno.ENOTDIR}: - raise SpdxJsonLdEnvelopeError("invalid_file_type") from redacted_error - raise SpdxJsonLdEnvelopeError("read_failed") from redacted_error + read_error_code = ( + "invalid_file_type" + if error.errno in {errno.ELOOP, errno.ENOTDIR} + else "read_failed" + ) + if read_error_code is not None: + # Raise only after leaving the original handler. Otherwise Python retains the + # path-bearing source error as implicit ``__context__`` even when an explicit + # redacted ``__cause__`` is supplied. + raise SpdxJsonLdEnvelopeError(read_error_code) from redacted_error if not payload or len(payload) > MAX_SPDX_JSONLD_BYTES: raise SpdxJsonLdEnvelopeError("invalid_size") return payload From 02829ceaa97b99340546147c212fe95462678a10 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 19:04:48 -0700 Subject: [PATCH 131/170] test(release): catch post-read parent authority swap --- ...elease_spdx_jsonld_parent_swap_contract.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_release_spdx_jsonld_parent_swap_contract.py b/tests/test_release_spdx_jsonld_parent_swap_contract.py index 3f35451c7..000f711f8 100644 --- a/tests/test_release_spdx_jsonld_parent_swap_contract.py +++ b/tests/test_release_spdx_jsonld_parent_swap_contract.py @@ -55,6 +55,59 @@ def swapping_opener(path: str, flags: int) -> int: self.assertEqual(captured.exception.code, "invalid_file_type") + @unittest.skipUnless(hasattr(os, "link"), "requires hard links") + def test_parent_swap_after_post_read_identity_probe_fails_closed(self) -> None: + """A final leaf check must not outlive its admitted parent identity.""" + + namespace = runpy.run_path( + str(VALIDATOR), run_name="spdx_parent_post_read_swap_contract" + ) + read_bounded = namespace["_read_bounded"] + envelope_error = namespace["SpdxJsonLdEnvelopeError"] + original_parent_identities = namespace["_direct_parent_identities"] + + with tempfile.TemporaryDirectory( + prefix="originweave-spdx-parent-post-read-swap-" + ) as directory: + root = pathlib.Path(directory) + direct_directory = root / "direct" + direct_directory.mkdir() + replacement_directory = root / "replacement" + replacement_directory.mkdir() + parked_directory = root / "direct-parked" + + direct_candidate = direct_directory / "candidate.spdx.jsonld" + direct_candidate.write_bytes(b"stable-release-sbom-bytes") + replacement_candidate = replacement_directory / direct_candidate.name + os.link(direct_candidate, replacement_candidate) + + probe_count = 0 + swapped = False + + def swap_after_post_read_probe(path: pathlib.Path) -> tuple[tuple[int, int], ...]: + nonlocal probe_count, swapped + identities = original_parent_identities(path) + probe_count += 1 + if probe_count == 3: + direct_directory.rename(parked_directory) + replacement_directory.rename(direct_directory) + swapped = True + return identities + + read_bounded.__globals__["_direct_parent_identities"] = swap_after_post_read_probe + try: + with self.assertRaises(envelope_error) as captured: + read_bounded(direct_candidate) + finally: + read_bounded.__globals__["_direct_parent_identities"] = ( + original_parent_identities + ) + if swapped: + direct_directory.rename(replacement_directory) + parked_directory.rename(direct_directory) + + self.assertEqual(captured.exception.code, "invalid_file_type") + if __name__ == "__main__": unittest.main() From 879c671bb379d8f79ab3a992abb13896681ed220 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 19:07:19 -0700 Subject: [PATCH 132/170] fix(release): revalidate parent identity after leaf check --- scripts/release/validate_spdx_jsonld.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 15cfcec4a..443674d8e 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -333,6 +333,8 @@ def _read_bounded(path: pathlib.Path) -> bytes: final_stat.st_ino, ) != (opened_stat.st_dev, opened_stat.st_ino): raise SpdxJsonLdEnvelopeError("invalid_file_type") + if _direct_parent_identities(path) != expected_parent_identities: + raise SpdxJsonLdEnvelopeError("invalid_file_type") finally: _EXPECTED_PARENT_IDENTITIES.reset(identity_token) except OSError as error: From fa6eab3d9b39b603c16bfcbfea3e2c4712ed1533 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 13:38:05 -0700 Subject: [PATCH 133/170] test(release): reject oversized SPDX numeric tokens --- ...est_release_spdx_jsonld_envelope_contract.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 439da9ec7..37d5922be 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -88,8 +88,7 @@ def test_context_extensions_cannot_add_ambient_remote_authority(self) -> None: ), "invalid_context", ) - self._assert_error_code( - self._payload([{"type": "SpdxDocument"}], context=[CONTEXT, None]), + self._assert_error_code(self._payload([{"type": "SpdxDocument"}], context=[CONTEXT, None]), "invalid_context", ) @@ -177,6 +176,20 @@ def test_invalid_utf8_and_nonfinite_json_fail_closed(self) -> None: ).encode("utf-8") self._assert_error_code(overflow_payload, "invalid_json") + def test_oversized_numeric_tokens_fail_closed_before_conversion(self) -> None: + oversized_integer = "9" * 4097 + oversized_float = "1." + ("0" * 4095) + "1" + for numeric_token in (oversized_integer, oversized_float): + with self.subTest(token_kind="float" if "." in numeric_token else "integer"): + payload = ( + '{"@context":"' + + CONTEXT + + '","@graph":[{"type":"SpdxDocument","score":' + + numeric_token + + "}]}" + ).encode("utf-8") + self._assert_error_code(payload, "invalid_json") + def test_excessive_json_nesting_fails_with_typed_redacted_error(self) -> None: marker = "buyer-secret-marker-must-not-reach-release-diagnostics" depth = 10_000 From b4965d3e0c8b7c4b34780902b2ffc03dadd754b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 13:38:47 -0700 Subject: [PATCH 134/170] style(release): preserve canonical test formatting --- tests/test_release_spdx_jsonld_envelope_contract.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 37d5922be..61a67a4f7 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -88,7 +88,8 @@ def test_context_extensions_cannot_add_ambient_remote_authority(self) -> None: ), "invalid_context", ) - self._assert_error_code(self._payload([{"type": "SpdxDocument"}], context=[CONTEXT, None]), + self._assert_error_code( + self._payload([{"type": "SpdxDocument"}], context=[CONTEXT, None]), "invalid_context", ) From 105108859565c54e99157e4e421eee22000ce32f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 13:40:42 -0700 Subject: [PATCH 135/170] fix(release): bound SPDX JSON numeric tokens --- scripts/release/validate_spdx_jsonld.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 443674d8e..96bac073b 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -30,6 +30,7 @@ SPDX_3_0_1_CONTEXT = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" MAX_SPDX_JSONLD_BYTES = 16 * 1024 * 1024 MAX_SPDX_GRAPH_OBJECTS = 65_536 +MAX_SPDX_JSON_NUMBER_TOKEN_BYTES = 4096 _EXPECTED_PARENT_IDENTITIES: contextvars.ContextVar[tuple[tuple[int, int], ...]] = ( contextvars.ContextVar("spdx_expected_parent_identities", default=()) ) @@ -60,9 +61,19 @@ def _reject_nonfinite_constant(_value: str) -> None: raise ValueError("non-finite JSON number") +def _bounded_json_integer(value: str) -> int: + """Parse one JSON integer only within the fixed numeric-token resource budget.""" + + if len(value) > MAX_SPDX_JSON_NUMBER_TOKEN_BYTES: + raise ValueError("JSON number token exceeds resource budget") + return int(value) + + def _finite_json_float(value: str) -> float: - """Parse one JSON float only when its binary representation remains finite.""" + """Parse one bounded JSON float only when its binary representation remains finite.""" + if len(value) > MAX_SPDX_JSON_NUMBER_TOKEN_BYTES: + raise ValueError("JSON number token exceeds resource budget") parsed = float(value) if not math.isfinite(parsed): raise ValueError("non-finite JSON number") @@ -139,6 +150,7 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: text, object_pairs_hook=_object_without_duplicates, parse_constant=_reject_nonfinite_constant, + parse_int=_bounded_json_integer, parse_float=_finite_json_float, ) except _DuplicateJsonKey: From 927ee3c094794242c59404a8e50f67160b3c5033 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 17:46:06 -0700 Subject: [PATCH 136/170] test(release): require CLI SBOM digest binding --- .../test_release_spdx_cli_digest_contract.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_release_spdx_cli_digest_contract.py diff --git a/tests/test_release_spdx_cli_digest_contract.py b/tests/test_release_spdx_cli_digest_contract.py new file mode 100644 index 000000000..bd006fe6b --- /dev/null +++ b/tests/test_release_spdx_cli_digest_contract.py @@ -0,0 +1,79 @@ +"""Regression contract for manifest-backed SPDX validation through the release CLI.""" + +from __future__ import annotations + +import contextlib +import hashlib +import io +import json +import pathlib +import runpy +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +VALIDATOR = ROOT / "scripts" / "release" / "validate_spdx_jsonld.py" +CONTEXT = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" + + +class ReleaseSpdxCliDigestContractTests(unittest.TestCase): + """Require release CLI callers to opt into exact manifest-backed digest evidence.""" + + @classmethod + def setUpClass(cls) -> None: + namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_cli_digest_contract") + cls.main = staticmethod(namespace["main"]) + + @staticmethod + def _payload() -> bytes: + return json.dumps( + { + "@context": CONTEXT, + "@graph": [{"type": "SpdxDocument", "spdxId": "release-cli"}], + }, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + @staticmethod + def _digest(payload: bytes) -> str: + return "sha256:" + hashlib.sha256(payload).hexdigest() + + def test_cli_can_bind_direct_file_to_manifest_backed_digest(self) -> None: + payload = self._payload() + expected_digest = self._digest(payload) + + with tempfile.TemporaryDirectory(prefix="originweave-spdx-cli-") as directory: + document = pathlib.Path(directory) / "release.spdx.jsonld" + document.write_bytes(payload) + output = io.StringIO() + with contextlib.redirect_stdout(output): + status = self.main( + [str(document), "--expected-sha256", expected_digest] + ) + + self.assertEqual(status, 0) + summary = json.loads(output.getvalue()) + self.assertEqual(summary["artifact_sha256"], expected_digest) + self.assertEqual(summary["spdx_document_count"], 1) + + def test_cli_digest_mismatch_fails_closed_without_reflecting_identity_or_path(self) -> None: + payload = self._payload() + wrong_digest = "sha256:" + "0" * 64 + + with tempfile.TemporaryDirectory(prefix="originweave-spdx-cli-secret-") as directory: + document = pathlib.Path(directory) / "customer-secret-release.spdx.jsonld" + document.write_bytes(payload) + error_output = io.StringIO() + with contextlib.redirect_stderr(error_output): + status = self.main([str(document), "--expected-sha256", wrong_digest]) + + diagnostic = error_output.getvalue() + self.assertEqual(status, 2) + self.assertIn("digest_mismatch", diagnostic) + self.assertNotIn(wrong_digest, diagnostic) + self.assertNotIn("customer-secret-release.spdx.jsonld", diagnostic) + + +if __name__ == "__main__": + unittest.main() From 3a2db6485ca142f7de622726463bc3b94bce4176 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 17:50:07 -0700 Subject: [PATCH 137/170] feat(release): bind SPDX CLI to manifest digest --- scripts/release/validate_spdx_jsonld.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 96bac073b..64ae49455 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -367,16 +367,33 @@ def _read_bounded(path: pathlib.Path) -> bytes: def main(argv: list[str] | None = None) -> int: - """Validate one local SPDX JSON-LD envelope without echoing untrusted document bytes.""" + """Validate one local SPDX JSON-LD artifact without echoing untrusted values.""" parser = argparse.ArgumentParser( - description="Validate a bounded SPDX 3.0.1 JSON-LD serialization envelope." + description=( + "Validate a bounded SPDX 3.0.1 JSON-LD serialization envelope, optionally " + "binding it to an exact manifest-backed SHA-256 identity." + ) ) parser.add_argument("document", type=pathlib.Path) + parser.add_argument( + "--expected-sha256", + help=( + "require the exact manifest-backed sha256: artifact identity before promoting " + "release evidence" + ), + ) arguments = parser.parse_args(argv) try: - summary = validate_spdx_3_0_1_jsonld_bytes(_read_bounded(arguments.document)) + payload = _read_bounded(arguments.document) + if arguments.expected_sha256 is None: + summary = validate_spdx_3_0_1_jsonld_bytes(payload) + else: + summary = validate_release_spdx_3_0_1_jsonld_bytes( + payload, + arguments.expected_sha256, + ) except SpdxJsonLdEnvelopeError as error: print(error, file=sys.stderr) return 2 @@ -386,4 +403,4 @@ def main(argv: list[str] | None = None) -> int: if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file From a9b1cc9a3685f3468d577b37ae0f28c8c09be8c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 16:25:08 -0700 Subject: [PATCH 138/170] test(release): reject malformed SBOM paths safely --- ...est_release_spdx_error_redaction_contract.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_release_spdx_error_redaction_contract.py b/tests/test_release_spdx_error_redaction_contract.py index 7bf5fc081..5ca92f31d 100644 --- a/tests/test_release_spdx_error_redaction_contract.py +++ b/tests/test_release_spdx_error_redaction_contract.py @@ -63,6 +63,23 @@ def test_failed_file_path_does_not_survive_in_exception_source(self) -> None: self.assertIsNone(error.__cause__.__context__) self.assertIsNone(error.__context__) + def test_embedded_nul_path_fails_through_redacted_typed_boundary(self) -> None: + marker = "buyer-secret-nul-path-marker-must-not-survive-validation" + malformed_path = pathlib.Path("\0" + marker) + + with self.assertRaises(self.error_type) as captured: + self.read_bounded(malformed_path) + + error = captured.exception + self.assertEqual(error.code, "read_failed") + self.assertIsInstance(error.__cause__, OSError) + self.assertEqual(error.__cause__.errno, errno.EINVAL) + self.assertIsNone(error.__cause__.filename) + self.assertNotIn(marker, str(error.__cause__)) + self.assertNotIn(marker, repr(error.__cause__)) + self.assertIsNone(error.__cause__.__context__) + self.assertIsNone(error.__context__) + if __name__ == "__main__": unittest.main() From 44146e5872310e10309a6423ef16ad1389bda698 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 16:50:50 -0700 Subject: [PATCH 139/170] fix(release): redact malformed SBOM path errors --- scripts/release/validate_spdx_jsonld.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 64ae49455..7cf9dc37d 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -349,6 +349,8 @@ def _read_bounded(path: pathlib.Path) -> bytes: raise SpdxJsonLdEnvelopeError("invalid_file_type") finally: _EXPECTED_PARENT_IDENTITIES.reset(identity_token) + except SpdxJsonLdEnvelopeError: + raise except OSError as error: redacted_error = OSError(error.errno, "release SBOM file operation failed") read_error_code = ( @@ -356,6 +358,9 @@ def _read_bounded(path: pathlib.Path) -> bytes: if error.errno in {errno.ELOOP, errno.ENOTDIR} else "read_failed" ) + except ValueError: + redacted_error = OSError(errno.EINVAL, "release SBOM file operation failed") + read_error_code = "read_failed" if read_error_code is not None: # Raise only after leaving the original handler. Otherwise Python retains the # path-bearing source error as implicit ``__context__`` even when an explicit @@ -403,4 +408,4 @@ def main(argv: list[str] | None = None) -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) From 6ab525257912e3679a5b69f671466ca6fa45369b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:00:31 +0900 Subject: [PATCH 140/170] test(release): reject nested-only SPDX document --- ...st_release_spdx_jsonld_envelope_contract.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 61a67a4f7..a509d65ab 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -139,6 +139,24 @@ def test_exactly_one_spdx_document_is_required(self) -> None: "invalid_document_count", ) + def test_spdx_document_must_be_a_top_level_graph_element(self) -> None: + self._assert_error_code( + self._payload( + [ + { + "type": "Bundle", + "element": [ + { + "type": "SpdxDocument", + "spdxId": "https://example.invalid/spdx/nested-document", + } + ], + } + ] + ), + "invalid_document_count", + ) + def test_nested_second_spdx_document_fails_closed(self) -> None: self._assert_error_code( self._payload( From e42e8d10cc30b988724e76913598a702e037126f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:03:15 +0900 Subject: [PATCH 141/170] fix(release): require top-level SPDX document --- scripts/release/validate_spdx_jsonld.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 7cf9dc37d..51099edd9 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -4,9 +4,9 @@ This verifier deliberately checks only the serialization envelope needed before deeper schema/ontology validation: the exact versioned global context identity required by the official SPDX 3.0.1 JSON Schema, a bounded top-level JSON object, a bounded object-only -``@graph``, and exactly one ``SpdxDocument`` element. A release-facing helper additionally -binds candidate bytes to the canonical SHA-256 identity already declared by the release -manifest. It does not claim full SPDX structural or semantic conformance, artifact +``@graph``, and exactly one top-level ``SpdxDocument`` element. A release-facing helper +additionally binds candidate bytes to the canonical SHA-256 identity already declared by the +release manifest. It does not claim full SPDX structural or semantic conformance, artifact authenticity, SBOM completeness, provenance, signing, publication, installation, update, or rollback authority. """ @@ -94,13 +94,12 @@ def _has_required_spdx_context(context: Any) -> bool: def _count_spdx_documents(value: Any) -> int: - """Count SPDX document elements at any depth without recursive interpreter use. + """Count SPDX document objects at any depth without recursive interpreter use. - SPDX element collections may inline other elements below the top-level graph. The - serialization contract admits at most one ``SpdxDocument`` element, so a nested second - document must not evade the preliminary envelope gate merely because it is not a direct - ``@graph`` member. The walk is iterative and remains bounded by the already size-bounded - parsed input. + SPDX 3.0.1 requires Element nodes to live at the top-level ``@graph``. The recursive + count is therefore a defense against a nested ``SpdxDocument`` being accepted in addition + to the one required top-level document. The walk is iterative and remains bounded by the + already size-bounded parsed input. """ count = 0 @@ -180,8 +179,11 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: if not isinstance(element, dict) or not isinstance(element.get("type"), str): raise SpdxJsonLdEnvelopeError("invalid_graph_object") + top_level_document_count = sum( + 1 for element in graph if element.get("type") == "SpdxDocument" + ) document_count = _count_spdx_documents(graph) - if document_count != 1: + if top_level_document_count != 1 or document_count != 1: raise SpdxJsonLdEnvelopeError("invalid_document_count") return { From c914438d4846796ec360b165c5c4581845e1535c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:05:32 +0900 Subject: [PATCH 142/170] docs(release): align SPDX document placement contract --- docs/adr/0018-release-sbom-identity.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/adr/0018-release-sbom-identity.md b/docs/adr/0018-release-sbom-identity.md index 63a609a51..f967031ec 100644 --- a/docs/adr/0018-release-sbom-identity.md +++ b/docs/adr/0018-release-sbom-identity.md @@ -7,7 +7,7 @@ Issue #201 requires acquisition-grade release evidence, including a software bill of materials tied to the same immutable release identity buyers verify. ADR 0015 deliberately stops at a bounded release-manifest identity and does not claim SBOM content conformance, provenance, signing, publication, installation, update, rollback, or release authority. -SPDX 3.0.1 defines JSON-LD serialization and separate structural and semantic validation requirements. The serialization prose requires the versioned global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`, defines `spdxId` as an alias for `@id` and `type` as an alias for `@type`, and says additional namespace mappings may be defined in a separate context object. The same SPDX 3.0.1 specification requires structural validation against its official JSON Schema. That schema constrains top-level `@context` with a `const` equal to the exact versioned context string. +SPDX 3.0.1 defines JSON-LD serialization and separate structural and semantic validation requirements. The serialization prose requires the versioned global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`, defines `spdxId` as an alias for `@id` and `type` as an alias for `@type`, requires Element nodes to be represented as top-level objects in `@graph`, and says additional namespace mappings may be defined in a separate context object. The same SPDX 3.0.1 specification requires structural validation against its official JSON Schema. That schema constrains top-level `@context` with a `const` equal to the exact versioned context string. Those two official 3.0.1 resources therefore expose an interoperability tension: the prose describes extension mappings that JSON-LD can represent with context composition, while the published structural schema accepts only the exact context string. More importantly for this preliminary verifier, an arbitrary inline JSON-LD context object is not merely a namespace map: JSON-LD control terms such as `@import`, `@base`, and `@vocab`, or redefinitions of SPDX aliases such as `type` and `spdxId`, can change downstream interpretation. Treating every inline object as a benign namespace map would therefore create semantic and remote-authority ambiguity before schema/ontology validation. @@ -28,7 +28,7 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - The gate should remain structurally compatible with the official SPDX 3.0.1 JSON Schema until a reviewed schema-aware extension policy exists. - Actual serialized bytes must not be accepted merely because metadata says they are SPDX 3.0.1 JSON-LD. - A syntactically valid but substituted SPDX document must not satisfy release evidence when its bytes do not match the exact SBOM artifact digest already admitted by the release manifest. -- Envelope parsing must be bounded before deeper semantic use and reject malformed UTF-8, malformed JSON, duplicate JSON object members, non-finite JSON constants, unexpected top-level members, non-object graph entries, excessive graph cardinality, and zero or multiple `SpdxDocument` objects. +- Envelope parsing must be bounded before deeper semantic use and reject malformed UTF-8, malformed JSON, duplicate JSON object members, non-finite JSON constants, unexpected top-level members, non-object graph entries, excessive graph cardinality, a nested-only `SpdxDocument`, and zero or multiple `SpdxDocument` objects. - Error diagnostics must not reflect attacker- or supplier-controlled document bytes or release digest values. - The SBOM artifact itself must already be an admitted release artifact with a manifest-bound SHA-256 identity. - Every non-SBOM release artifact in the bound manifest must be represented exactly once in the described-artifact inventory. @@ -42,7 +42,7 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. - The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase `sha256:` identity, and cannot also appear in the binding's described-artifact set. - Every other artifact admitted by that release manifest must be retained in the described-artifact set exactly once. This is an OriginWeave release-inventory completeness rule, not a general SPDX modeling assertion. -- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, requires exactly the top-level `@context` and `@graph` members, and requires `@context` to equal the exact SPDX 3.0.1 global-context string. Context arrays and inline/remote context extensions fail closed at this stage. +- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, requires exactly the top-level `@context` and `@graph` members, requires `@context` to equal the exact SPDX 3.0.1 global-context string, requires exactly one top-level `SpdxDocument`, and rejects any additional nested `SpdxDocument`. Context arrays and inline/remote context extensions fail closed at this stage. - Release admission that composes those bytes with `ReleaseSbomBinding` uses `validate_release_spdx_3_0_1_jsonld_bytes` and supplies the exact canonical lowercase `sha256:` identity from `ReleaseSbomBinding::sbom_artifact()`. The helper recomputes SHA-256 from the bounded candidate bytes and rejects malformed expected identities or mismatches before promoting envelope evidence. - Digest equality proves correspondence to the declared manifest artifact identity only. It does not authenticate who produced the manifest or bytes and does not replace provenance, signature, transparency, reproducibility, or release-approval checks. - The exact-string context choice is deliberately narrower than the SPDX serialization prose because the official 3.0.1 JSON Schema itself uses that exact-string constraint and because this preliminary verifier does not implement complete JSON-LD context semantics or collision detection against the pinned SPDX context. @@ -59,6 +59,10 @@ Rejected. A manifest can correctly identify an artifact while a different candid Rejected for release admission. A completely valid SPDX document for another release could pass the envelope gate and be associated with the wrong `ReleaseSbomBinding`. The release-facing helper therefore hashes the exact bounded candidate bytes and compares the canonical `sha256:` identity to the manifest-backed SBOM artifact before returning release evidence. +### Admit a nested-only `SpdxDocument` + +Rejected. SPDX 3.0.1 places Element nodes, including `SpdxDocument`, at the top-level `@graph`. Recursively finding one document below another graph object proves only that a matching string exists somewhere in the JSON tree; it does not satisfy the serialization structure. The envelope gate therefore requires exactly one top-level `SpdxDocument` and separately rejects any additional nested document. + ### Admit the exact SPDX context plus arbitrary inline context objects Rejected. An inline JSON-LD object can contain `@import`, `@base`, `@vocab`, or term/alias redefinitions rather than only a new namespace prefix. Without a complete pinned-context collision model, accepting those objects would allow the preliminary gate to count raw `type` values under semantics that later JSON-LD processing may reinterpret. @@ -73,7 +77,7 @@ Rejected. Ambient network retrieval would introduce moving external authority an ### Treat a general-purpose JSON parse as SPDX conformance -Rejected. JSON syntax alone does not establish the required SPDX context, document cardinality, JSON Schema validity, ontology/SHACL validity, or SBOM completeness. +Rejected. JSON syntax alone does not establish the required SPDX context, document cardinality or placement, JSON Schema validity, ontology/SHACL validity, or SBOM completeness. ### Store an arbitrary SBOM path or URL next to the manifest @@ -100,14 +104,14 @@ OriginWeave adopts the following release-SBOM boundary: 7. Described artifact identities are stored deterministically and the manifest's existing bound transitively limits the inventory. 8. Release-facing validation of actual candidate bytes uses `validate_release_spdx_3_0_1_jsonld_bytes` with the exact `sha256:` identity from the manifest-backed SBOM artifact. The expected digest must be `sha256:` followed by 64 lowercase hexadecimal digits; the verifier recomputes SHA-256 over the same bounded bytes and a mismatch fails closed with a value-redacted typed error. 9. Only after exact digest correspondence is established do those candidate bytes pass through the narrow `validate_spdx_3_0_1_jsonld_bytes` envelope contract. The lower-level function remains available for composition and testing but is not by itself release-artifact correspondence evidence. -10. The envelope verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB, rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, and requires `@context` to equal the exact versioned SPDX 3.0.1 context string. Context arrays and all additional context entries fail closed. It allows at most 65,536 graph objects, requires object entries with string `type`, and requires exactly one raw `SpdxDocument` under the pinned alias semantics. +10. The envelope verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB, rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, and requires `@context` to equal the exact versioned SPDX 3.0.1 context string. Context arrays and all additional context entries fail closed. It allows at most 65,536 graph objects, requires object entries with string `type`, requires exactly one top-level raw `SpdxDocument` under the pinned alias semantics, and rejects any additional nested `SpdxDocument`. 11. Validation errors expose stable error codes and generic diagnostics without reflecting document bytes or expected digest values. 12. Digest/envelope success is not a certificate of complete SPDX JSON Schema validity, semantic ontology/SHACL validity, package/component completeness, producer authenticity, provenance, signatures, publication, installation, update, rollback, or release authority. 13. No verifier fallback may substitute another SPDX version, discard the required pinned context or expected artifact digest, reinterpret an unvalidated context extension, discard duplicate keys, silently truncate a graph, fetch an ambient remote resource, or convert malformed or mismatched content into success. ## Consequences -Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over the exact bytes named by that join. A syntactically valid SPDX document for a different artifact cannot satisfy the release-facing helper merely because its envelope is valid. A candidate also cannot pass this layer with the wrong SPDX context, a context array that can alter JSON-LD interpretation, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, or zero/multiple `SpdxDocument` objects. +Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over the exact bytes named by that join. A syntactically valid SPDX document for a different artifact cannot satisfy the release-facing helper merely because its envelope is valid. A candidate also cannot pass this layer with the wrong SPDX context, a context array that can alter JSON-LD interpretation, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, a nested-only `SpdxDocument`, or zero/multiple `SpdxDocument` objects. The exact-context rule can reject a serialization that the SPDX 3.0.1 prose intends to allow through additional namespace mappings. That false negative is accepted at this preliminary gate because the same version's official structural schema currently requires the exact string and because OriginWeave does not yet possess a reviewed, schema-aware rule capable of proving that an extension object is namespace-only and non-colliding. Support for such mappings must be added only together with pinned full structural/semantic validation or another equally strong proof of interpretation. @@ -115,13 +119,13 @@ The boundary remains intentionally narrower than a commercial SBOM generator or ## Failure and degraded behavior -Manifest-binding failures occur before a binding is produced. Release-facing byte verification rejects a malformed expected digest or candidate-byte digest mismatch with deterministic value-redacted error codes. Envelope failures likewise return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input is rejected before hashing or JSON parsing. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, context arrays/extensions, invalid top-level shape, non-object graph entries, excessive graph cardinality, and invalid `SpdxDocument` cardinality all fail closed. +Manifest-binding failures occur before a binding is produced. Release-facing byte verification rejects a malformed expected digest or candidate-byte digest mismatch with deterministic value-redacted error codes. Envelope failures likewise return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input is rejected before hashing or JSON parsing. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, context arrays/extensions, invalid top-level shape, non-object graph entries, excessive graph cardinality, nested-only documents, and invalid `SpdxDocument` cardinality all fail closed. Failure at either layer does not authorize release through another path. There is no alternate digest, alternate version fallback, network context fallback, permissive parse mode, or silent default success. ## Security / privacy / governance impact -The boundary reduces supply-chain ambiguity without introducing ambient network authority. Exact SHA-256 correspondence prevents a different but otherwise valid SPDX document from being promoted under the manifest-backed SBOM identity. Bounded parsing constrains memory exposure before deeper validation, duplicate-key rejection prevents parser interpretation drift, and the pinned exact SPDX context prevents version drift, remote `@import`, `@base`/`@vocab` mutation, and term/alias rebinding from being treated as already-admitted envelope semantics. Redacted diagnostics prevent supplier-controlled SBOM bytes or expected digest values from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. +The boundary reduces supply-chain ambiguity without introducing ambient network authority. Exact SHA-256 correspondence prevents a different but otherwise valid SPDX document from being promoted under the manifest-backed SBOM identity. Bounded parsing constrains memory exposure before deeper validation, duplicate-key rejection prevents parser interpretation drift, top-level `SpdxDocument` placement prevents a nested object from masquerading as the serialization's document element, and the pinned exact SPDX context prevents version drift, remote `@import`, `@base`/`@vocab` mutation, and term/alias rebinding from being treated as already-admitted envelope semantics. Redacted diagnostics prevent supplier-controlled SBOM bytes or expected digest values from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. ## Tests and acceptance evidence @@ -132,7 +136,8 @@ The Python release-verifier contract tests must prove: - exact candidate bytes match the canonical lowercase manifest-backed `sha256:` identity; - a valid but substituted SPDX document fails closed on digest mismatch without reflecting document-controlled bytes; - malformed, uppercase, truncated, or prefixless expected digests fail closed; -- exact SPDX 3.0.1 context and one `SpdxDocument` are admitted; +- exact SPDX 3.0.1 context and one top-level `SpdxDocument` are admitted; +- a nested-only `SpdxDocument` and a nested second `SpdxDocument` fail closed; - context arrays, including apparently simple namespace mappings, fail closed until schema-aware extension validation exists; - `@import`, `@base`, `@vocab`, `type`, and `spdxId` context overrides fail closed; - wrong context, additional remote context entries, and unexpected top-level members fail closed; From 5d245c8a0e8681d295aa30cd9664a428f655f41a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:07:15 +0900 Subject: [PATCH 143/170] docs(doctoring): bind SPDX document placement --- docs/doctoring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index a9816dd68..568983eee 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -88,7 +88,7 @@ Microsoft's Win32 filename guidance requires applications not to assume case sen ### Release SBOM identity -SPDX 3.0.1 defines JSON-LD as an RDF serialization for SPDX data and requires conformant JSON-LD to pass both the versioned JSON Schema and the SPDX ontology/SHACL constraints. Its serialization prose requires the global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`, defines `spdxId` and `type` as aliases for JSON-LD `@id` and `@type`, and says additional namespace mappings may be defined in a separate context object. The official SPDX 3.0.1 JSON Schema simultaneously constrains top-level `@context` to the exact global-context string. Because an arbitrary inline JSON-LD context object can also contain semantic controls such as `@import`, `@base`, or `@vocab`, or redefine SPDX aliases, OriginWeave does not approximate “namespace mapping” by accepting every object. The preliminary envelope gate follows the official schema's exact-string identity and rejects context composition until a reviewed schema-aware validator can reconcile the prose/schema tension without granting ambient or redefinition authority. This gate deliberately does not claim full content-level conformance: it binds a declared SPDX 3.0.1 JSON-LD SBOM artifact identity and exact release-artifact names to one immutable `ReleaseManifest`, while generation, full structural/semantic validation, package/component completeness, signatures, provenance, publication, installation, update, rollback, and release authority remain separate reviewed boundaries. +SPDX 3.0.1 defines JSON-LD as an RDF serialization for SPDX data and requires conformant JSON-LD to pass both the versioned JSON Schema and the SPDX ontology/SHACL constraints. Its serialization prose requires the global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`, defines `spdxId` and `type` as aliases for JSON-LD `@id` and `@type`, and requires Element nodes—including `SpdxDocument`—to be represented as top-level objects under `@graph`. It also says additional namespace mappings may be defined in a separate context object. The official SPDX 3.0.1 JSON Schema simultaneously constrains top-level `@context` to the exact global-context string. Because an arbitrary inline JSON-LD context object can contain semantic controls such as `@import`, `@base`, or `@vocab`, or redefine SPDX aliases, OriginWeave does not approximate “namespace mapping” by accepting every object. The preliminary envelope gate therefore requires exactly one top-level `SpdxDocument`, rejects a nested-only document as well as any additional nested document, follows the official schema's exact-string context identity, and rejects context composition until a reviewed schema-aware validator can reconcile the prose/schema tension without granting ambient or redefinition authority. This gate deliberately does not claim full content-level conformance: it binds a declared SPDX 3.0.1 JSON-LD SBOM artifact identity and exact release-artifact names to one immutable `ReleaseManifest`, while generation, full structural/semantic validation, package/component completeness, signatures, provenance, publication, installation, update, rollback, and release authority remain separate reviewed boundaries. ### Git source identity admission From f341ab946beb509f53a9cc8cc52fc20d650faa87 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:08:23 +0900 Subject: [PATCH 144/170] docs(changelog): record SPDX placement hardening --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8843b0787..a8ecf1d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security +- The SPDX 3.0.1 release-envelope gate now requires exactly one top-level `SpdxDocument` under `@graph` and rejects both a nested-only document and any additional nested `SpdxDocument`, preventing recursive string/type discovery from substituting for the required serialization placement. - The SPDX 3.0.1 release-envelope gate requires the exact versioned global-context string and rejects context arrays, `@import`, `@base`, `@vocab`, and alias/term-rebinding attempts until reviewed schema-aware validation can prove extension semantics without ambient or reinterpretation authority. - SPDX JSON-LD release-file admission now requires a direct regular-file leaf reached through a no-follow descriptor-relative path walk and revalidates the final leaf's device/inode identity after the bounded read, so symlinks, FIFOs, swapped ancestors, and post-open pathname replacement fail closed instead of silently changing the artifact path identity. - Explicit proxy server identifiers require ASCII decimal port tokens before numeric range parsing, preventing Rust-specific leading-plus spellings from widening proxy authority. From f3a519e986dc6bda46fb03ac3768ea2bef7a131d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 08:05:19 +0900 Subject: [PATCH 145/170] test(release): pin SBOM canonical-owner migration --- ...t_release_sbom_owner_migration_contract.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/test_release_sbom_owner_migration_contract.py diff --git a/tests/test_release_sbom_owner_migration_contract.py b/tests/test_release_sbom_owner_migration_contract.py new file mode 100644 index 000000000..579f2bc94 --- /dev/null +++ b/tests/test_release_sbom_owner_migration_contract.py @@ -0,0 +1,34 @@ +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +MIGRATION = ROOT / "docs/traceability/release-sbom-owner-migration.md" + + +class ReleaseSbomOwnerMigrationContractTests(unittest.TestCase): + """Keep the pre-GA release/SBOM owner repair explicit and non-duplicative.""" + + def test_release_sbom_migration_keeps_one_canonical_owner(self): + """The repair plan must converge SBOM policy and verification on one Release owner.""" + self.assertTrue(MIGRATION.is_file()) + text = MIGRATION.read_text(encoding="utf-8") + required = ( + "`originweave-release`", + "`ReleaseManifest`", + "`originweave-core`", + "`scripts/release/validate_spdx_jsonld.py`", + "thin conformance/fixture harness", + "Rust-owned", + "no compatibility shim", + "no duplicated release API", + "#221", + "#240", + ) + for token in required: + self.assertIn(token, text) + self.assertIn("Proposed", text) + self.assertIn("not shipped truth", text) + + +if __name__ == "__main__": + unittest.main() From 21a9a40a481a32257cc4b923ec204ded97674421 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 08:07:02 +0900 Subject: [PATCH 146/170] docs(release): define SBOM canonical-owner migration --- .../release-sbom-owner-migration.md | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 docs/traceability/release-sbom-owner-migration.md diff --git a/docs/traceability/release-sbom-owner-migration.md b/docs/traceability/release-sbom-owner-migration.md new file mode 100644 index 000000000..6fffe50d1 --- /dev/null +++ b/docs/traceability/release-sbom-owner-migration.md @@ -0,0 +1,73 @@ +# Release SBOM canonical-owner migration + +- Status: Proposed +- Owning gap: #201 +- Repair PR: #221 +- Release-context owner candidate: #240 + +This record is migration evidence for active pre-GA work. Active PR metadata is **not shipped truth**; protected `main` remains implementation truth until the dependency-ordered reconstruction, exact-head verification, and normal governance path complete. + +## Problem + +PR #221 currently carries release-specific SBOM identity and completeness semantics in `originweave-core` and implements release-facing digest, hostile JSON-LD envelope, and local file admission canonically in `scripts/release/validate_spdx_jsonld.py`. Two independently valid constraints now make that placement provisional: + +1. OriginWeave's first-party security/runtime boundary is Rust-first. Digest correspondence, bounded hostile JSON parsing, and descriptor-relative/no-follow file admission have practical Rust implementations, so Python cannot remain the canonical security boundary without an explicit no-practical-Rust rationale and removal condition. +2. Active PR #240 establishes `originweave-release` as the dedicated Release bounded context and moves release policy/evidence ownership out of `originweave-core`. Shipping #221 unchanged would therefore create competing canonical ownership. + +## Target ownership + +`ReleaseManifest` remains a stable cross-context identity contract in `originweave-core`. The Release context depends inward on that contract; core must not depend outward on Release. + +After the #240 owner lineage is available for integration, `originweave-release` is the proposed canonical owner for: + +- SPDX serialization identity and SBOM binding; +- manifest-to-SBOM described-artifact completeness policy; +- release-facing SHA-256 correspondence; +- bounded hostile JSON-LD envelope admission; +- identity-stable local SBOM file admission; and +- later full SPDX structural/semantic conformance and its release evidence. + +There is **no compatibility shim** that makes core depend on Release, and there is **no duplicated release API** split across `originweave-core` and `originweave-release`. The active #240 branch is evidence of the intended owner boundary, not shipped truth and not authority to copy its tree wholesale. + +`scripts/release/validate_spdx_jsonld.py` may remain only as a **thin conformance/fixture harness** after the Rust-owned admission boundary exists. Python must not remain the canonical source of release/security authority. + +## Invariants that reconstruction must preserve + +The Rust-owned replacement must preserve or strengthen the currently reviewed fail-closed behavior: + +- maximum 16 MiB candidate input before unbounded parsing/allocation; +- strict UTF-8 and JSON admission; +- duplicate object-key and non-finite numeric rejection; +- exact SPDX 3.0.1 global context identity for this preliminary gate; +- top-level `@graph` object shape and bounded graph cardinality; +- exactly one top-level `SpdxDocument` and rejection of any additional nested `SpdxDocument`; +- canonical lowercase `sha256:` manifest/SBOM digest correspondence; +- value-redacted diagnostics that do not echo hostile document bytes, expected digest values, or candidate paths; +- direct descriptor-relative, regular-file, no-follow, nonblocking, identity-stable file admission before/during/after the bounded read; and +- no ambient schema/context retrieval, producer-authentication claim, signing authority, publication authority, update authority, or rollback authority. + +Digest/envelope success remains correspondence evidence only. It is not full SPDX JSON Schema/OWL/SHACL conformance, SLSA provenance, reproducibility, signature verification, or release approval. + +## Dependency-ordered repair + +1. Preserve #221's validated manifest-join, digest, envelope, hostile-input, file-admission tests and exact historical evidence as reconstruction input. Do not close the PR or discard its unique delta. +2. Resolve the dedicated Release owner lineage through #240 without treating its active branch as protected-main truth. Read intervening changes and reconstruct non-destructively rather than force-rebasing or copying the whole branch. +3. On the resulting `originweave-release` lineage, add the smallest realistic failing Rust test for each migrated canonical invariant and observe the failure before production implementation. +4. Implement the smallest Rust-owned admission boundary that depends inward on the core `ReleaseManifest` identity contract. Do not introduce a core-to-release dependency, source copy, cross-context SQL, generic JSON authority, or permissive fallback. +5. Reduce `scripts/release/validate_spdx_jsonld.py` to a thin conformance/fixture harness only after Rust owns the release decision boundary and parity regressions prove the behavior. +6. Update ADR 0018, `ARCHITECTURE.md`, `CHANGELOG.md`, doctoring/TRACEABILITY, and the product/technical gap baseline to name the final owner and distinguish protected-main truth from active-PR evidence. +7. Require unchanged exact-head Rust 1.97.1 formatting/check/tests/strict Clippy/rustdoc, 100% owned-production function/line/region/branch coverage, applicable security/review gates, and current live-base verification before any readiness transition. + +## Current exact evidence + +The migration contract was introduced test-first on #221 after reviews `5095297316` (Rust-first boundary) and `5095598850` (DDD owner conflict). The predecessor #221 head was `f341ab946beb509f53a9cc8cc52fc20d650faa87`; the test-only owner-migration contract is commit `f3a519e986dc6bda46fb03ac3768ea2bef7a131d`. Active #240 was last independently read at `47ab81b721aca246948607754be1807b9d4c8dda` and was not exact-head GREEN. + +This document is the causal documentation fix for the missing durable owner-migration contract. It does not claim that the Rust migration itself has occurred, that #240 has shipped, or that #221 is exact-head GREEN. + +## Standards traceability + +SPDX Workgroup. (2026). *SPDX specification 3.0.1*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/ + +SPDX Workgroup. (2026). *SPDX specification 3.0.1: Model and serializations*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/serializations/ + +SPDX Workgroup. (2026). *SpdxDocument*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/SpdxDocument/ From fae0aeee028cc8aae55896057abc3aacb0fe265c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 08:32:55 +0900 Subject: [PATCH 147/170] docs(release): require cross-platform secure SBOM admission --- .../release-sbom-owner-migration.md | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/traceability/release-sbom-owner-migration.md b/docs/traceability/release-sbom-owner-migration.md index 6fffe50d1..9d1f08426 100644 --- a/docs/traceability/release-sbom-owner-migration.md +++ b/docs/traceability/release-sbom-owner-migration.md @@ -9,10 +9,11 @@ This record is migration evidence for active pre-GA work. Active PR metadata is ## Problem -PR #221 currently carries release-specific SBOM identity and completeness semantics in `originweave-core` and implements release-facing digest, hostile JSON-LD envelope, and local file admission canonically in `scripts/release/validate_spdx_jsonld.py`. Two independently valid constraints now make that placement provisional: +PR #221 currently carries release-specific SBOM identity and completeness semantics in `originweave-core` and implements release-facing digest, hostile JSON-LD envelope, and local file admission canonically in `scripts/release/validate_spdx_jsonld.py`. Three independently valid constraints now make that placement provisional: 1. OriginWeave's first-party security/runtime boundary is Rust-first. Digest correspondence, bounded hostile JSON parsing, and descriptor-relative/no-follow file admission have practical Rust implementations, so Python cannot remain the canonical security boundary without an explicit no-practical-Rust rationale and removal condition. 2. Active PR #240 establishes `originweave-release` as the dedicated Release bounded context and moves release policy/evidence ownership out of `originweave-core`. Shipping #221 unchanged would therefore create competing canonical ownership. +3. The current Python secure-open path deliberately fails closed when POSIX-style `O_NOFOLLOW` plus descriptor-relative `os.open(..., dir_fd=...)` are unavailable. That is safe for the provisional verifier, but #201 is a cross-platform distribution/release track. The final Rust owner must preserve the same anti-symlink/reparse and identity-stability guarantees on each supported release platform instead of making a Unix-only primitive the commercial release contract. ## Target ownership @@ -43,8 +44,10 @@ The Rust-owned replacement must preserve or strengthen the currently reviewed fa - exactly one top-level `SpdxDocument` and rejection of any additional nested `SpdxDocument`; - canonical lowercase `sha256:` manifest/SBOM digest correspondence; - value-redacted diagnostics that do not echo hostile document bytes, expected digest values, or candidate paths; -- direct descriptor-relative, regular-file, no-follow, nonblocking, identity-stable file admission before/during/after the bounded read; and -- no ambient schema/context retrieval, producer-authentication claim, signing authority, publication authority, update authority, or rollback authority. +- regular-file admission through an already-opened, identity-stable handle rather than path-only trust; +- on POSIX-capable targets, descriptor-relative `openat`-class traversal with no-follow semantics and identity revalidation before/during/after the bounded read; +- on Windows targets, reviewed handle-based no-reparse traversal/admission semantics that prevent ordinary reparse-point following and revalidate file/directory identity around the bounded read; and +- fail-closed behavior on a platform where equivalent anti-redirection and identity-stability guarantees cannot be established. Portability must not silently degrade to lexical/path-only checks. Digest/envelope success remains correspondence evidence only. It is not full SPDX JSON Schema/OWL/SHACL conformance, SLSA provenance, reproducibility, signature verification, or release approval. @@ -52,17 +55,17 @@ Digest/envelope success remains correspondence evidence only. It is not full SPD 1. Preserve #221's validated manifest-join, digest, envelope, hostile-input, file-admission tests and exact historical evidence as reconstruction input. Do not close the PR or discard its unique delta. 2. Resolve the dedicated Release owner lineage through #240 without treating its active branch as protected-main truth. Read intervening changes and reconstruct non-destructively rather than force-rebasing or copying the whole branch. -3. On the resulting `originweave-release` lineage, add the smallest realistic failing Rust test for each migrated canonical invariant and observe the failure before production implementation. -4. Implement the smallest Rust-owned admission boundary that depends inward on the core `ReleaseManifest` identity contract. Do not introduce a core-to-release dependency, source copy, cross-context SQL, generic JSON authority, or permissive fallback. +3. On the resulting `originweave-release` lineage, add the smallest realistic failing Rust test for each migrated canonical invariant and observe the failure before production implementation. Include Linux/macOS symlink-swap and parent-replacement cases plus Windows reparse-point/junction and handle-identity cases for every platform claimed by #201. +4. Implement the smallest Rust-owned admission boundary that depends inward on the core `ReleaseManifest` identity contract. Keep platform-specific secure-open mechanics behind Release-owned adapters; do not introduce a core-to-release dependency, source copy, cross-context SQL, generic JSON authority, path-only portability fallback, or permissive fallback. 5. Reduce `scripts/release/validate_spdx_jsonld.py` to a thin conformance/fixture harness only after Rust owns the release decision boundary and parity regressions prove the behavior. 6. Update ADR 0018, `ARCHITECTURE.md`, `CHANGELOG.md`, doctoring/TRACEABILITY, and the product/technical gap baseline to name the final owner and distinguish protected-main truth from active-PR evidence. -7. Require unchanged exact-head Rust 1.97.1 formatting/check/tests/strict Clippy/rustdoc, 100% owned-production function/line/region/branch coverage, applicable security/review gates, and current live-base verification before any readiness transition. +7. Require unchanged exact-head Rust 1.97.1 formatting/check/tests/strict Clippy/rustdoc, 100% owned-production function/line/region/branch coverage, applicable security/review gates, current live-base verification, and supported-platform parity before any readiness transition. ## Current exact evidence -The migration contract was introduced test-first on #221 after reviews `5095297316` (Rust-first boundary) and `5095598850` (DDD owner conflict). The predecessor #221 head was `f341ab946beb509f53a9cc8cc52fc20d650faa87`; the test-only owner-migration contract is commit `f3a519e986dc6bda46fb03ac3768ea2bef7a131d`. Active #240 was last independently read at `47ab81b721aca246948607754be1807b9d4c8dda` and was not exact-head GREEN. +The migration contract was introduced test-first on #221 after reviews `5095297316` (Rust-first boundary) and `5095598850` (DDD owner conflict). The predecessor #221 head was `f341ab946beb509f53a9cc8cc52fc20d650faa87`; the test-only owner-migration contract is commit `f3a519e986dc6bda46fb03ac3768ea2bef7a131d`. Review `5096271206` adds the cross-platform secure-open finding after confirming that the provisional Python implementation explicitly rejects platforms without POSIX-style no-follow and descriptor-relative open support. Active #240 was last independently read at `47ab81b721aca246948607754be1807b9d4c8dda` and was not exact-head GREEN. -This document is the causal documentation fix for the missing durable owner-migration contract. It does not claim that the Rust migration itself has occurred, that #240 has shipped, or that #221 is exact-head GREEN. +This document is the causal documentation fix for the missing durable owner-migration and portability contract. It does not claim that the Rust migration itself has occurred, that Windows/macOS parity has been implemented, that #240 has shipped, or that #221 is exact-head GREEN. ## Standards traceability @@ -71,3 +74,7 @@ SPDX Workgroup. (2026). *SPDX specification 3.0.1*. The Linux Foundation. https: SPDX Workgroup. (2026). *SPDX specification 3.0.1: Model and serializations*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/serializations/ SPDX Workgroup. (2026). *SpdxDocument*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/SpdxDocument/ + +The Open Group. (2024). *open, openat — open file* (POSIX.1-2024). https://pubs.opengroup.org/onlinepubs/9799919799/functions/open.html + +Microsoft. (n.d.). *CreateFileW function (fileapi.h)*. Microsoft Learn. Retrieved September 3, 2026, from https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew From 6662d0af8ef2e37f60323edf8f4a98a06491f431 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 09:06:25 +0900 Subject: [PATCH 148/170] test(release): pin Windows SBOM handle-identity contract --- tests/test_release_sbom_owner_migration_contract.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_release_sbom_owner_migration_contract.py b/tests/test_release_sbom_owner_migration_contract.py index 579f2bc94..989add737 100644 --- a/tests/test_release_sbom_owner_migration_contract.py +++ b/tests/test_release_sbom_owner_migration_contract.py @@ -29,6 +29,19 @@ def test_release_sbom_migration_keeps_one_canonical_owner(self): self.assertIn("Proposed", text) self.assertIn("not shipped truth", text) + def test_windows_secure_open_contract_names_non_reparse_and_identity_primitives(self): + """Windows parity must name the concrete non-reparse and handle-identity primitives.""" + text = MIGRATION.read_text(encoding="utf-8") + required = ( + "FILE_FLAG_OPEN_REPARSE_POINT", + "GetFileInformationByHandleEx", + "FILE_ID_INFO", + "VolumeSerialNumber", + "FileId", + ) + for token in required: + self.assertIn(token, text) + if __name__ == "__main__": unittest.main() From 9a821776a3e496f4ab50c30a44eb50b10e30c019 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 09:08:01 +0900 Subject: [PATCH 149/170] docs(release): bind Windows SBOM admission to handle identity --- docs/traceability/release-sbom-owner-migration.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/traceability/release-sbom-owner-migration.md b/docs/traceability/release-sbom-owner-migration.md index 9d1f08426..7ffaf3a34 100644 --- a/docs/traceability/release-sbom-owner-migration.md +++ b/docs/traceability/release-sbom-owner-migration.md @@ -46,7 +46,7 @@ The Rust-owned replacement must preserve or strengthen the currently reviewed fa - value-redacted diagnostics that do not echo hostile document bytes, expected digest values, or candidate paths; - regular-file admission through an already-opened, identity-stable handle rather than path-only trust; - on POSIX-capable targets, descriptor-relative `openat`-class traversal with no-follow semantics and identity revalidation before/during/after the bounded read; -- on Windows targets, reviewed handle-based no-reparse traversal/admission semantics that prevent ordinary reparse-point following and revalidate file/directory identity around the bounded read; and +- on Windows targets, use reviewed handle-based traversal/admission that prevents ordinary reparse-point following. The concrete leaf/reparse admission primitive must include `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT`; identity checks must use `GetFileInformationByHandleEx(FileIdInfo)` / `FILE_ID_INFO`, comparing the `VolumeSerialNumber` plus `FileId` for the opened handle before and after the bounded read. `CreateFileW` alone is not treated as proof that parent-directory swaps are impossible; the final implementation must use an equivalent handle-relative parent traversal or another reviewed mechanism and prove it with junction/reparse and parent-swap regressions; and - fail-closed behavior on a platform where equivalent anti-redirection and identity-stability guarantees cannot be established. Portability must not silently degrade to lexical/path-only checks. Digest/envelope success remains correspondence evidence only. It is not full SPDX JSON Schema/OWL/SHACL conformance, SLSA provenance, reproducibility, signature verification, or release approval. @@ -78,3 +78,7 @@ SPDX Workgroup. (2026). *SpdxDocument*. The Linux Foundation. https://spdx.githu The Open Group. (2024). *open, openat — open file* (POSIX.1-2024). https://pubs.opengroup.org/onlinepubs/9799919799/functions/open.html Microsoft. (n.d.). *CreateFileW function (fileapi.h)*. Microsoft Learn. Retrieved September 3, 2026, from https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew + +Microsoft. (2024). *GetFileInformationByHandleEx function (winbase.h)*. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getfileinformationbyhandleex + +Microsoft. (2024). *FILE_ID_INFO structure (winbase.h)*. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_id_info From e9c43d9940c03df4ead17f1e2b0b80efa5fcfd0f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:05:02 +0900 Subject: [PATCH 150/170] test(release): bound nested SPDX JSON containers --- tests/test_release_spdx_jsonld_envelope_contract.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index a509d65ab..1bbde8a6e 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -127,6 +127,19 @@ def test_graph_must_be_bounded_object_array(self) -> None: oversized_graph = [{"type": "Package"}] * (self.max_graph_objects + 1) self._assert_error_code(self._payload(oversized_graph), "too_many_graph_objects") + def test_nested_container_explosion_fails_before_json_materialization(self) -> None: + nested_container_count = 524_289 + payload = ( + '{"@context":"' + + CONTEXT + + '","@graph":[{"type":"SpdxDocument","nested":[' + + "[]," * nested_container_count + + "[]]}]}" + ).encode("utf-8") + + self.assertLess(len(payload), self.max_bytes) + self._assert_error_code(payload, "too_many_json_containers") + def test_exactly_one_spdx_document_is_required(self) -> None: self._assert_error_code(self._payload([{"type": "Package"}]), "invalid_document_count") self._assert_error_code( From 3e2b4a39b0bb22309f70cf88aaa88a536ec25d1d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:06:01 +0900 Subject: [PATCH 151/170] fix(release): preflight SPDX JSON container fan-out --- scripts/release/validate_spdx_jsonld.py | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 51099edd9..73ffd4907 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -30,6 +30,7 @@ SPDX_3_0_1_CONTEXT = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" MAX_SPDX_JSONLD_BYTES = 16 * 1024 * 1024 MAX_SPDX_GRAPH_OBJECTS = 65_536 +MAX_SPDX_JSON_CONTAINERS = 524_288 MAX_SPDX_JSON_NUMBER_TOKEN_BYTES = 4096 _EXPECTED_PARENT_IDENTITIES: contextvars.ContextVar[tuple[tuple[int, int], ...]] = ( contextvars.ContextVar("spdx_expected_parent_identities", default=()) @@ -80,6 +81,37 @@ def _finite_json_float(value: str) -> float: return parsed +def _reject_excessive_json_container_count(text: str) -> None: + """Reject container fan-out before the standard JSON parser materializes the tree. + + The byte-size limit alone does not bound heap amplification: a compact JSON string can + encode hundreds of thousands of empty arrays or objects. This lexical preflight counts + only structural ``[`` and ``{`` tokens outside JSON strings, so container-shaped text + inside values does not consume the budget. Grammar validation still belongs to + ``json.loads`` immediately after this resource check. + """ + + container_count = 0 + in_string = False + escaped = False + for character in text: + if in_string: + if escaped: + escaped = False + elif character == "\\": + escaped = True + elif character == '"': + in_string = False + continue + + if character == '"': + in_string = True + elif character in "[{": + container_count += 1 + if container_count > MAX_SPDX_JSON_CONTAINERS: + raise SpdxJsonLdEnvelopeError("too_many_json_containers") + + def _has_required_spdx_context(context: Any) -> bool: """Require the exact SPDX 3.0.1 context identity used by the official JSON Schema. @@ -143,6 +175,8 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: # ``__cause__`` or ``__context__`` reference. raise SpdxJsonLdEnvelopeError("invalid_utf8") + _reject_excessive_json_container_count(text) + parse_error_code: str | None = None try: decoded = json.loads( From 0317263f6e412c2040b4bdfe578781c6fa24a11c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:07:01 +0900 Subject: [PATCH 152/170] test(release): cover SPDX container preflight escapes --- tests/test_release_spdx_jsonld_envelope_contract.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 1bbde8a6e..e07726d2e 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -140,6 +140,13 @@ def test_nested_container_explosion_fails_before_json_materialization(self) -> N self.assertLess(len(payload), self.max_bytes) self._assert_error_code(payload, "too_many_json_containers") + def test_container_preflight_ignores_structural_tokens_inside_strings(self) -> None: + marker = '[{"nested":[]}]\\buyer"quote' + summary = self.validate(self._payload([{"type": "SpdxDocument", "note": marker}])) + + self.assertEqual(summary["graph_object_count"], 1) + self.assertEqual(summary["spdx_document_count"], 1) + def test_exactly_one_spdx_document_is_required(self) -> None: self._assert_error_code(self._payload([{"type": "Package"}]), "invalid_document_count") self._assert_error_code( From 34368493fad746cf0d11540dcaa86937742381a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:08:26 +0900 Subject: [PATCH 153/170] docs(release): trace SPDX parser resource bound --- docs/traceability/release-sbom-owner-migration.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/traceability/release-sbom-owner-migration.md b/docs/traceability/release-sbom-owner-migration.md index 7ffaf3a34..5d2a7cfee 100644 --- a/docs/traceability/release-sbom-owner-migration.md +++ b/docs/traceability/release-sbom-owner-migration.md @@ -9,11 +9,12 @@ This record is migration evidence for active pre-GA work. Active PR metadata is ## Problem -PR #221 currently carries release-specific SBOM identity and completeness semantics in `originweave-core` and implements release-facing digest, hostile JSON-LD envelope, and local file admission canonically in `scripts/release/validate_spdx_jsonld.py`. Three independently valid constraints now make that placement provisional: +PR #221 currently carries release-specific SBOM identity and completeness semantics in `originweave-core` and implements release-facing digest, hostile JSON-LD envelope, and local file admission canonically in `scripts/release/validate_spdx_jsonld.py`. Four independently valid constraints now make that placement provisional: 1. OriginWeave's first-party security/runtime boundary is Rust-first. Digest correspondence, bounded hostile JSON parsing, and descriptor-relative/no-follow file admission have practical Rust implementations, so Python cannot remain the canonical security boundary without an explicit no-practical-Rust rationale and removal condition. 2. Active PR #240 establishes `originweave-release` as the dedicated Release bounded context and moves release policy/evidence ownership out of `originweave-core`. Shipping #221 unchanged would therefore create competing canonical ownership. 3. The current Python secure-open path deliberately fails closed when POSIX-style `O_NOFOLLOW` plus descriptor-relative `os.open(..., dir_fd=...)` are unavailable. That is safe for the provisional verifier, but #201 is a cross-platform distribution/release track. The final Rust owner must preserve the same anti-symlink/reparse and identity-stability guarantees on each supported release platform instead of making a Unix-only primitive the commercial release contract. +4. A 16 MiB input-byte ceiling and 65,536 top-level `@graph` objects did not bound parser heap amplification. A roughly 1.5 MiB valid JSON document with one top-level `SpdxDocument` and more than 524,288 nested empty arrays was accepted by the preceding verifier and forced the standard JSON parser to materialize every nested container before any graph-cardinality check. The provisional boundary now performs a string-aware lexical container-count preflight before `json.loads`; the Rust migration must preserve an equivalent pre-materialization resource invariant rather than deserialize hostile input into an unbounded generic tree first. ## Target ownership @@ -37,6 +38,7 @@ There is **no compatibility shim** that makes core depend on Release, and there The Rust-owned replacement must preserve or strengthen the currently reviewed fail-closed behavior: - maximum 16 MiB candidate input before unbounded parsing/allocation; +- a pre-materialization JSON-container budget equivalent to or stricter than the provisional 524,288-container ceiling, with container-shaped text inside strings excluded from the structural count; - strict UTF-8 and JSON admission; - duplicate object-key and non-finite numeric rejection; - exact SPDX 3.0.1 global context identity for this preliminary gate; @@ -55,8 +57,8 @@ Digest/envelope success remains correspondence evidence only. It is not full SPD 1. Preserve #221's validated manifest-join, digest, envelope, hostile-input, file-admission tests and exact historical evidence as reconstruction input. Do not close the PR or discard its unique delta. 2. Resolve the dedicated Release owner lineage through #240 without treating its active branch as protected-main truth. Read intervening changes and reconstruct non-destructively rather than force-rebasing or copying the whole branch. -3. On the resulting `originweave-release` lineage, add the smallest realistic failing Rust test for each migrated canonical invariant and observe the failure before production implementation. Include Linux/macOS symlink-swap and parent-replacement cases plus Windows reparse-point/junction and handle-identity cases for every platform claimed by #201. -4. Implement the smallest Rust-owned admission boundary that depends inward on the core `ReleaseManifest` identity contract. Keep platform-specific secure-open mechanics behind Release-owned adapters; do not introduce a core-to-release dependency, source copy, cross-context SQL, generic JSON authority, path-only portability fallback, or permissive fallback. +3. On the resulting `originweave-release` lineage, add the smallest realistic failing Rust test for each migrated canonical invariant and observe the failure before production implementation. Include a compact JSON container-amplification case that remains well below the byte ceiling but exceeds the admitted container budget, Linux/macOS symlink-swap and parent-replacement cases, plus Windows reparse-point/junction and handle-identity cases for every platform claimed by #201. +4. Implement the smallest Rust-owned admission boundary that depends inward on the core `ReleaseManifest` identity contract. Parse hostile JSON through a bounded or streaming path that enforces byte, container, graph, numeric, and diagnostic limits before an unbounded generic tree can be materialized. Keep platform-specific secure-open mechanics behind Release-owned adapters; do not introduce a core-to-release dependency, source copy, cross-context SQL, generic JSON authority, path-only portability fallback, or permissive fallback. 5. Reduce `scripts/release/validate_spdx_jsonld.py` to a thin conformance/fixture harness only after Rust owns the release decision boundary and parity regressions prove the behavior. 6. Update ADR 0018, `ARCHITECTURE.md`, `CHANGELOG.md`, doctoring/TRACEABILITY, and the product/technical gap baseline to name the final owner and distinguish protected-main truth from active-PR evidence. 7. Require unchanged exact-head Rust 1.97.1 formatting/check/tests/strict Clippy/rustdoc, 100% owned-production function/line/region/branch coverage, applicable security/review gates, current live-base verification, and supported-platform parity before any readiness transition. @@ -65,10 +67,14 @@ Digest/envelope success remains correspondence evidence only. It is not full SPD The migration contract was introduced test-first on #221 after reviews `5095297316` (Rust-first boundary) and `5095598850` (DDD owner conflict). The predecessor #221 head was `f341ab946beb509f53a9cc8cc52fc20d650faa87`; the test-only owner-migration contract is commit `f3a519e986dc6bda46fb03ac3768ea2bef7a131d`. Review `5096271206` adds the cross-platform secure-open finding after confirming that the provisional Python implementation explicitly rejects platforms without POSIX-style no-follow and descriptor-relative open support. Active #240 was last independently read at `47ab81b721aca246948607754be1807b9d4c8dda` and was not exact-head GREEN. -This document is the causal documentation fix for the missing durable owner-migration and portability contract. It does not claim that the Rust migration itself has occurred, that Windows/macOS parity has been implemented, that #240 has shipped, or that #221 is exact-head GREEN. +The parser-amplification repair was also performed test-first on #221. Test-only commit `e9c43d9940c03df4ead17f1e2b0b80efa5fcfd0f` adds a roughly 1.5 MiB payload containing more than 524,288 nested empty arrays under one `SpdxDocument`; the preceding verifier accepted the same shape in the focused reproduction instead of failing closed. Production commit `3e2b4a39b0bb22309f70cf88aaa88a536ec25d1d` adds a string-aware lexical preflight that returns `too_many_json_containers` before `json.loads` materialization, and `0317263f6e412c2040b4bdfe578781c6fa24a11c` adds escaped-string coverage so `[` and `{` inside JSON string values do not consume the structural budget. This is provisional Python hardening while the Rust owner migration remains blocked on dependency order; it is not evidence that the canonical Rust release boundary exists. + +This document is the causal documentation fix for the missing durable owner-migration, parser-resource, and portability contracts. It does not claim that the Rust migration itself has occurred, that Windows/macOS parity has been implemented, that #240 has shipped, or that #221 is exact-head GREEN. ## Standards traceability +Bray, T. (2017). *The JavaScript Object Notation (JSON) Data Interchange Format* (RFC 8259; STD 90). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc8259 + SPDX Workgroup. (2026). *SPDX specification 3.0.1*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/ SPDX Workgroup. (2026). *SPDX specification 3.0.1: Model and serializations*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/serializations/ From 9c03561cc79210dc8f729b523b20ad808ba3037d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:10:46 +0900 Subject: [PATCH 154/170] docs(changelog): record SPDX parser fan-out hardening --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8ecf1d4f..0e52f9588 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security +- The provisional SPDX 3.0.1 release-envelope parser now rejects more than 524,288 JSON containers in a string-aware lexical preflight before `json.loads`, preventing a compact nested-container payload from amplifying into an unbounded in-memory object graph while preserving `[` and `{` that occur inside JSON string values. The eventual Rust-owned Release boundary must retain an equivalent pre-materialization resource invariant. - The SPDX 3.0.1 release-envelope gate now requires exactly one top-level `SpdxDocument` under `@graph` and rejects both a nested-only document and any additional nested `SpdxDocument`, preventing recursive string/type discovery from substituting for the required serialization placement. - The SPDX 3.0.1 release-envelope gate requires the exact versioned global-context string and rejects context arrays, `@import`, `@base`, `@vocab`, and alias/term-rebinding attempts until reviewed schema-aware validation can prove extension semantics without ambient or reinterpretation authority. - SPDX JSON-LD release-file admission now requires a direct regular-file leaf reached through a no-follow descriptor-relative path walk and revalidates the final leaf's device/inode identity after the bounded read, so symlinks, FIFOs, swapped ancestors, and post-open pathname replacement fail closed instead of silently changing the artifact path identity. @@ -87,7 +88,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Shortened, integer, hexadecimal, and legacy octal-looking IPv4 host spellings are rejected so the policy origin cannot diverge from Chromium host interpretation. - IPv4-mapped IPv6 is canonicalized before destination classification and pin comparison so mapped private or loopback addresses cannot bypass IPv4 policy. - The default destination policy permits only public addresses and denies unspecified, loopback, private, shared, link-local, metadata, documentation, benchmarking, multicast, broadcast, transition, unallocated, and protocol-reserved destinations. -- Azure platform IP `168.63.129.16` and Amazon EKS Pod Identity endpoints `169.254.170.23` and `fd00:ec2::23` are classified as metadata or platform services before broader public, link-local, or unique-local rules. +- Azure platform IP `168.63.129.16` and Amazon EKS Pod Identity endpoints `169.254.170.23` and `fd00:ec2::23` are classified as metadata or platform services before broader address-range rules. - Resolver answers are rejected when empty or larger than 256 addresses, preventing an unbounded resolver response from entering policy state. - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. @@ -110,4 +111,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file From 3e2ad552ceb2c50b56587d5b7463d23d9600dee1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:15:39 +0900 Subject: [PATCH 155/170] docs(adr): bind SPDX parser resource admission --- docs/adr/0018-release-sbom-identity.md | 41 ++++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/adr/0018-release-sbom-identity.md b/docs/adr/0018-release-sbom-identity.md index f967031ec..fe9013991 100644 --- a/docs/adr/0018-release-sbom-identity.md +++ b/docs/adr/0018-release-sbom-identity.md @@ -11,6 +11,8 @@ SPDX 3.0.1 defines JSON-LD serialization and separate structural and semantic va Those two official 3.0.1 resources therefore expose an interoperability tension: the prose describes extension mappings that JSON-LD can represent with context composition, while the published structural schema accepts only the exact context string. More importantly for this preliminary verifier, an arbitrary inline JSON-LD context object is not merely a namespace map: JSON-LD control terms such as `@import`, `@base`, and `@vocab`, or redefinitions of SPDX aliases such as `type` and `spdxId`, can change downstream interpretation. Treating every inline object as a benign namespace map would therefore create semantic and remote-authority ambiguity before schema/ontology validation. +RFC 8259 permits implementations to set limits on accepted JSON size and nesting depth. That latitude matters here because a byte ceiling alone is not a heap-allocation ceiling. The preceding verifier admitted a roughly 1.5 MiB valid JSON document containing one top-level `SpdxDocument` and more than 524,288 nested empty arrays. `json.loads` had to materialize those containers before the later top-level `@graph` cardinality check could run. A release-facing hostile-input boundary therefore needs a pre-materialization resource invariant in addition to its byte, graph, and numeric-token bounds. + OriginWeave separates four claims that must not be collapsed: 1. the declared SPDX serialization identity; @@ -28,7 +30,8 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - The gate should remain structurally compatible with the official SPDX 3.0.1 JSON Schema until a reviewed schema-aware extension policy exists. - Actual serialized bytes must not be accepted merely because metadata says they are SPDX 3.0.1 JSON-LD. - A syntactically valid but substituted SPDX document must not satisfy release evidence when its bytes do not match the exact SBOM artifact digest already admitted by the release manifest. -- Envelope parsing must be bounded before deeper semantic use and reject malformed UTF-8, malformed JSON, duplicate JSON object members, non-finite JSON constants, unexpected top-level members, non-object graph entries, excessive graph cardinality, a nested-only `SpdxDocument`, and zero or multiple `SpdxDocument` objects. +- Envelope parsing must be bounded before deeper semantic use and before unbounded generic-tree materialization. A compact payload must not bypass resource policy merely because it remains below the byte ceiling or keeps the top-level graph small. +- Envelope parsing must reject malformed UTF-8, malformed JSON, duplicate JSON object members, non-finite JSON constants, unexpected top-level members, non-object graph entries, excessive graph cardinality, a nested-only `SpdxDocument`, and zero or multiple `SpdxDocument` objects. - Error diagnostics must not reflect attacker- or supplier-controlled document bytes or release digest values. - The SBOM artifact itself must already be an admitted release artifact with a manifest-bound SHA-256 identity. - Every non-SBOM release artifact in the bound manifest must be represented exactly once in the described-artifact inventory. @@ -42,12 +45,14 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. - The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase `sha256:` identity, and cannot also appear in the binding's described-artifact set. - Every other artifact admitted by that release manifest must be retained in the described-artifact set exactly once. This is an OriginWeave release-inventory completeness rule, not a general SPDX modeling assertion. -- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, requires exactly the top-level `@context` and `@graph` members, requires `@context` to equal the exact SPDX 3.0.1 global-context string, requires exactly one top-level `SpdxDocument`, and rejects any additional nested `SpdxDocument`. Context arrays and inline/remote context extensions fail closed at this stage. +- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, performs a string-aware lexical preflight that admits at most 524,288 structural JSON containers before `json.loads`, requires exactly the top-level `@context` and `@graph` members, requires `@context` to equal the exact SPDX 3.0.1 global-context string, requires exactly one top-level `SpdxDocument`, and rejects any additional nested `SpdxDocument`. Context arrays and inline/remote context extensions fail closed at this stage. +- The lexical preflight counts `[` and `{` only outside JSON strings. It is a resource-admission check, not a substitute JSON grammar; `json.loads` remains responsible for syntax after the preflight. The 524,288 threshold is an OriginWeave provisional resource-policy ceiling rather than an SPDX semantic limit. - Release admission that composes those bytes with `ReleaseSbomBinding` uses `validate_release_spdx_3_0_1_jsonld_bytes` and supplies the exact canonical lowercase `sha256:` identity from `ReleaseSbomBinding::sbom_artifact()`. The helper recomputes SHA-256 from the bounded candidate bytes and rejects malformed expected identities or mismatches before promoting envelope evidence. - Digest equality proves correspondence to the declared manifest artifact identity only. It does not authenticate who produced the manifest or bytes and does not replace provenance, signature, transparency, reproducibility, or release-approval checks. - The exact-string context choice is deliberately narrower than the SPDX serialization prose because the official 3.0.1 JSON Schema itself uses that exact-string constraint and because this preliminary verifier does not implement complete JSON-LD context semantics or collision detection against the pinned SPDX context. - The verifier does not fetch remote contexts or schemas, resolve external identifiers, validate the full SPDX JSON Schema, evaluate the SPDX ontology/SHACL constraints, infer package completeness, or grant any signing/release/update authority. - Full conformance still requires validation of the same digest-bound serialized bytes against reviewed SPDX 3.0.1 structural and semantic resources plus OriginWeave product-completeness rules. +- The current Python implementation is provisional migration evidence. Release-facing parser/resource and secure-file admission remain scheduled for reconstruction under the Rust-owned `originweave-release` bounded context; Python must not become the permanent canonical security authority. ## Options considered @@ -59,6 +64,10 @@ Rejected. A manifest can correctly identify an artifact while a different candid Rejected for release admission. A completely valid SPDX document for another release could pass the envelope gate and be associated with the wrong `ReleaseSbomBinding`. The release-facing helper therefore hashes the exact bounded candidate bytes and compares the canonical `sha256:` identity to the manifest-backed SBOM artifact before returning release evidence. +### Bound only raw bytes and top-level graph cardinality + +Rejected. A compact document can keep both measures within their limits while encoding hundreds of thousands of nested empty arrays or objects. A generic JSON parser then materializes that tree before the top-level graph check runs. The preliminary boundary therefore needs a structural-container budget before materialization, and the final Rust owner must enforce an equivalent or stricter invariant through a bounded or streaming parse path. + ### Admit a nested-only `SpdxDocument` Rejected. SPDX 3.0.1 places Element nodes, including `SpdxDocument`, at the top-level `@graph`. Recursively finding one document below another graph object proves only that a matching string exists somewhere in the JSON tree; it does not satisfy the serialization structure. The envelope gate therefore requires exactly one top-level `SpdxDocument` and separately rejects any additional nested document. @@ -104,39 +113,44 @@ OriginWeave adopts the following release-SBOM boundary: 7. Described artifact identities are stored deterministically and the manifest's existing bound transitively limits the inventory. 8. Release-facing validation of actual candidate bytes uses `validate_release_spdx_3_0_1_jsonld_bytes` with the exact `sha256:` identity from the manifest-backed SBOM artifact. The expected digest must be `sha256:` followed by 64 lowercase hexadecimal digits; the verifier recomputes SHA-256 over the same bounded bytes and a mismatch fails closed with a value-redacted typed error. 9. Only after exact digest correspondence is established do those candidate bytes pass through the narrow `validate_spdx_3_0_1_jsonld_bytes` envelope contract. The lower-level function remains available for composition and testing but is not by itself release-artifact correspondence evidence. -10. The envelope verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB, rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, and requires `@context` to equal the exact versioned SPDX 3.0.1 context string. Context arrays and all additional context entries fail closed. It allows at most 65,536 graph objects, requires object entries with string `type`, requires exactly one top-level raw `SpdxDocument` under the pinned alias semantics, and rejects any additional nested `SpdxDocument`. +10. The envelope verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB. Before `json.loads`, it applies a string-aware structural preflight that admits at most 524,288 JSON containers and ignores bracket/brace characters inside JSON strings. It then rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, requires `@context` to equal the exact versioned SPDX 3.0.1 context string, allows at most 65,536 graph objects, requires object entries with string `type`, requires exactly one top-level raw `SpdxDocument` under the pinned alias semantics, and rejects any additional nested `SpdxDocument`. 11. Validation errors expose stable error codes and generic diagnostics without reflecting document bytes or expected digest values. 12. Digest/envelope success is not a certificate of complete SPDX JSON Schema validity, semantic ontology/SHACL validity, package/component completeness, producer authenticity, provenance, signatures, publication, installation, update, rollback, or release authority. -13. No verifier fallback may substitute another SPDX version, discard the required pinned context or expected artifact digest, reinterpret an unvalidated context extension, discard duplicate keys, silently truncate a graph, fetch an ambient remote resource, or convert malformed or mismatched content into success. +13. No verifier fallback may substitute another SPDX version, discard the required pinned context or expected artifact digest, reinterpret an unvalidated context extension, discard duplicate keys, silently truncate a graph, bypass the structural resource budget, fetch an ambient remote resource, or convert malformed or mismatched content into success. +14. The eventual Rust-owned `originweave-release` implementation must preserve or strengthen the byte/container/graph/numeric and diagnostic limits before unbounded generic-tree materialization. The provisional Python scanner is migration evidence, not the permanent canonical parser/security boundary. ## Consequences -Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over the exact bytes named by that join. A syntactically valid SPDX document for a different artifact cannot satisfy the release-facing helper merely because its envelope is valid. A candidate also cannot pass this layer with the wrong SPDX context, a context array that can alter JSON-LD interpretation, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, a nested-only `SpdxDocument`, or zero/multiple `SpdxDocument` objects. +Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over the exact bytes named by that join. A syntactically valid SPDX document for a different artifact cannot satisfy the release-facing helper merely because its envelope is valid. A candidate also cannot pass this layer with the wrong SPDX context, a context array that can alter JSON-LD interpretation, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, excessive structural-container fan-out, a nested-only `SpdxDocument`, or zero/multiple `SpdxDocument` objects. The exact-context rule can reject a serialization that the SPDX 3.0.1 prose intends to allow through additional namespace mappings. That false negative is accepted at this preliminary gate because the same version's official structural schema currently requires the exact string and because OriginWeave does not yet possess a reviewed, schema-aware rule capable of proving that an extension object is namespace-only and non-colliding. Support for such mappings must be added only together with pinned full structural/semantic validation or another equally strong proof of interpretation. +The structural-container threshold can reject a JSON document that is syntactically valid and within the byte ceiling. That false negative is also accepted at this release-security boundary: RFC 8259 permits implementation resource limits, and OriginWeave prefers a bounded failure over generic-tree heap amplification. The exact ceiling is provisional and may change only with measured realistic SBOM corpus and resource evidence; it is not an SPDX modeling requirement. + The boundary remains intentionally narrower than a commercial SBOM generator or complete verifier. The official SPDX 3.0.1 specification requires structural validation against the JSON Schema and semantic validation against the OWL ontology/SHACL constraints; those checks, package/component completeness, root-element rules, producer/provenance authentication, signing, and integrated release acceptance remain separate work. Digest/envelope success must never be presented as full SPDX conformance or signed supply-chain provenance. ## Failure and degraded behavior -Manifest-binding failures occur before a binding is produced. Release-facing byte verification rejects a malformed expected digest or candidate-byte digest mismatch with deterministic value-redacted error codes. Envelope failures likewise return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input is rejected before hashing or JSON parsing. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, context arrays/extensions, invalid top-level shape, non-object graph entries, excessive graph cardinality, nested-only documents, and invalid `SpdxDocument` cardinality all fail closed. +Manifest-binding failures occur before a binding is produced. Release-facing byte verification rejects a malformed expected digest or candidate-byte digest mismatch with deterministic value-redacted error codes. Envelope failures likewise return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input and excessive structural-container fan-out are rejected before JSON tree materialization. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, context arrays/extensions, invalid top-level shape, non-object graph entries, excessive graph cardinality, nested-only documents, and invalid `SpdxDocument` cardinality all fail closed. -Failure at either layer does not authorize release through another path. There is no alternate digest, alternate version fallback, network context fallback, permissive parse mode, or silent default success. +Failure at either layer does not authorize release through another path. There is no alternate digest, alternate version fallback, network context fallback, permissive parse mode, resource-limit bypass, or silent default success. ## Security / privacy / governance impact -The boundary reduces supply-chain ambiguity without introducing ambient network authority. Exact SHA-256 correspondence prevents a different but otherwise valid SPDX document from being promoted under the manifest-backed SBOM identity. Bounded parsing constrains memory exposure before deeper validation, duplicate-key rejection prevents parser interpretation drift, top-level `SpdxDocument` placement prevents a nested object from masquerading as the serialization's document element, and the pinned exact SPDX context prevents version drift, remote `@import`, `@base`/`@vocab` mutation, and term/alias rebinding from being treated as already-admitted envelope semantics. Redacted diagnostics prevent supplier-controlled SBOM bytes or expected digest values from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. +The boundary reduces supply-chain ambiguity without introducing ambient network authority. Exact SHA-256 correspondence prevents a different but otherwise valid SPDX document from being promoted under the manifest-backed SBOM identity. Pre-materialization container admission plus byte, graph, and numeric bounds constrain memory exposure before deeper validation; duplicate-key rejection prevents parser interpretation drift; top-level `SpdxDocument` placement prevents a nested object from masquerading as the serialization's document element; and the pinned exact SPDX context prevents version drift, remote `@import`, `@base`/`@vocab` mutation, and term/alias rebinding from being treated as already-admitted envelope semantics. Redacted diagnostics prevent supplier-controlled SBOM bytes or expected digest values from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. ## Tests and acceptance evidence The Rust identity tests must continue to prove exact SPDX specification/context identity, manifest-backed SBOM admission, complete coverage of every non-SBOM manifest artifact, deterministic ordering, and typed fail-closed binding behavior. -The Python release-verifier contract tests must prove: +The provisional Python release-verifier contract tests must prove: - exact candidate bytes match the canonical lowercase manifest-backed `sha256:` identity; - a valid but substituted SPDX document fails closed on digest mismatch without reflecting document-controlled bytes; - malformed, uppercase, truncated, or prefixless expected digests fail closed; - exact SPDX 3.0.1 context and one top-level `SpdxDocument` are admitted; +- a compact payload below the 16 MiB byte ceiling but above the structural-container budget fails before JSON tree materialization; +- bracket/brace characters and escaped quote/backslash content inside JSON strings do not consume the structural-container budget; - a nested-only `SpdxDocument` and a nested second `SpdxDocument` fail closed; - context arrays, including apparently simple namespace mappings, fail closed until schema-aware extension validation exists; - `@import`, `@base`, `@vocab`, `type`, and `spdxId` context overrides fail closed; @@ -144,17 +158,18 @@ The Python release-verifier contract tests must prove: - duplicate JSON keys, malformed UTF-8, non-finite constants, invalid graph entries, excessive graph size, and zero/multiple documents fail closed; and - hostile external bytes never appear in diagnostics. -Repository Python contracts, Rust 1.97.1 formatting/check/tests/Clippy/rustdoc, exact Rust production function/line/region/branch coverage, and any applicable browser/security gates must pass on the unchanged exact head. Predecessor evidence does not transfer. +The final Rust Release migration must reproduce the same parser-resource RED before implementing its bounded or streaming parser. Repository Python contracts, Rust 1.97.1 formatting/check/tests/Clippy/rustdoc, exact Rust production function/line/region/branch coverage, and any applicable browser/security gates must pass on the unchanged exact head. Predecessor evidence does not transfer. ## Migration and rollback -This remains a pre-release branch with no protected-main persisted SBOM schema migration. The verifier has no network or durable state. Before acceptance it can be revised or withdrawn with its owning branch. If a durable external release/SBOM format depends on this boundary, incompatible digest, context, size, graph, or error-contract changes require explicit versioning and migration rather than silent parser broadening. +This remains a pre-release branch with no protected-main persisted SBOM schema migration. The verifier has no network or durable state. Before acceptance it can be revised or withdrawn with its owning branch. If a durable external release/SBOM format depends on this boundary, incompatible digest, context, size, structural-container, graph, or error-contract changes require explicit versioning and migration rather than silent parser broadening. ## Open follow-ups - Generate deterministic SPDX 3.0.1 SBOM content for supported release packages. - Validate the same digest-bound candidate bytes against reviewed, immutable SPDX 3.0.1 JSON Schema and OWL/SHACL resources using offline-verifiable identities. - Reconcile the SPDX 3.0.1 prose allowance for additional namespace mappings with the official 3.0.1 JSON Schema's exact-string `@context` constraint before admitting context composition. +- Migrate the release-facing hostile JSON/resource boundary into `originweave-release` Rust with a bounded or streaming parser and preserve the pre-materialization resource contract. - Define package/component completeness and root-element rules for OriginWeave distribution artifacts. - Compose the digest-bound SBOM result with SLSA provenance, signing identity, timestamps, platform packages, reproducibility evidence, and updater trust. - Complete issue #201's integrated release acceptance and rollback/freeze-protection architecture. @@ -165,10 +180,12 @@ Supersede this ADR when a versioned external release/SBOM specification replaces ## References +Bray, T. (2017). *The JavaScript Object Notation (JSON) Data Interchange Format* (RFC 8259; STD 90). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc8259 + SPDX Workgroup. (2026). *SPDX specification 3.0.1*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/ SPDX Workgroup. (2026). *SPDX specification 3.0.1: Model and serializations*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/serializations/ SPDX Workgroup. (2026). *SPDX 3.0.1 JSON Schema*. The Linux Foundation. https://spdx.org/schema/3.0.1/spdx-json-schema.json -World Wide Web Consortium. (2020, July 16). *JSON-LD 1.1: A JSON-based serialization for linked data*. https://www.w3.org/TR/json-ld11/ +World Wide Web Consortium. (2020, July 16). *JSON-LD 1.1: A JSON-based serialization for linked data*. https://www.w3.org/TR/json-ld11/ \ No newline at end of file From cbd0d9629d4d5ce42d217c47c54e5f6d33e83255 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:17:00 +0900 Subject: [PATCH 156/170] test(release): expose scalar JSON fan-out amplification --- tests/test_release_spdx_jsonld_envelope_contract.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index e07726d2e..f004ce0cd 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -140,6 +140,19 @@ def test_nested_container_explosion_fails_before_json_materialization(self) -> N self.assertLess(len(payload), self.max_bytes) self._assert_error_code(payload, "too_many_json_containers") + def test_scalar_fan_out_fails_before_json_materialization(self) -> None: + scalar_count = 1_048_577 + payload = ( + '{"@context":"' + + CONTEXT + + '","@graph":[{"type":"SpdxDocument","nested":[' + + "0," * scalar_count + + "0]}]}" + ).encode("utf-8") + + self.assertLess(len(payload), self.max_bytes) + self._assert_error_code(payload, "too_many_json_structure_tokens") + def test_container_preflight_ignores_structural_tokens_inside_strings(self) -> None: marker = '[{"nested":[]}]\\buyer"quote' summary = self.validate(self._payload([{"type": "SpdxDocument", "note": marker}])) From d301f8f79e34f3f09744b0c38c71a54e58ad6283 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:17:44 +0900 Subject: [PATCH 157/170] fix(release): bound scalar JSON fan-out before parse --- scripts/release/validate_spdx_jsonld.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 73ffd4907..a9372c9c4 100644 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -31,6 +31,7 @@ MAX_SPDX_JSONLD_BYTES = 16 * 1024 * 1024 MAX_SPDX_GRAPH_OBJECTS = 65_536 MAX_SPDX_JSON_CONTAINERS = 524_288 +MAX_SPDX_JSON_STRUCTURE_TOKENS = 1_048_576 MAX_SPDX_JSON_NUMBER_TOKEN_BYTES = 4096 _EXPECTED_PARENT_IDENTITIES: contextvars.ContextVar[tuple[tuple[int, int], ...]] = ( contextvars.ContextVar("spdx_expected_parent_identities", default=()) @@ -81,17 +82,19 @@ def _finite_json_float(value: str) -> float: return parsed -def _reject_excessive_json_container_count(text: str) -> None: - """Reject container fan-out before the standard JSON parser materializes the tree. +def _reject_excessive_json_structure(text: str) -> None: + """Reject hostile JSON fan-out before the standard parser materializes the tree. The byte-size limit alone does not bound heap amplification: a compact JSON string can - encode hundreds of thousands of empty arrays or objects. This lexical preflight counts - only structural ``[`` and ``{`` tokens outside JSON strings, so container-shaped text - inside values does not consume the budget. Grammar validation still belongs to - ``json.loads`` immediately after this resource check. + encode hundreds of thousands of containers or more than a million scalar/object members. + This lexical preflight counts structure only outside JSON strings. Opening containers have + a tighter dedicated budget, while all brackets, braces, commas, and colons share a broader + structure-token budget that also constrains scalar fan-out. Grammar validation still + belongs to ``json.loads`` immediately after this resource check. """ container_count = 0 + structure_token_count = 0 in_string = False escaped = False for character in text: @@ -106,10 +109,15 @@ def _reject_excessive_json_container_count(text: str) -> None: if character == '"': in_string = True - elif character in "[{": + continue + if character in "[{": container_count += 1 if container_count > MAX_SPDX_JSON_CONTAINERS: raise SpdxJsonLdEnvelopeError("too_many_json_containers") + if character in "[]{}:,": + structure_token_count += 1 + if structure_token_count > MAX_SPDX_JSON_STRUCTURE_TOKENS: + raise SpdxJsonLdEnvelopeError("too_many_json_structure_tokens") def _has_required_spdx_context(context: Any) -> bool: @@ -175,7 +183,7 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: # ``__cause__`` or ``__context__`` reference. raise SpdxJsonLdEnvelopeError("invalid_utf8") - _reject_excessive_json_container_count(text) + _reject_excessive_json_structure(text) parse_error_code: str | None = None try: From 96e8dbd2ad0414481b0ff6ff0e841751147fb780 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:18:51 +0900 Subject: [PATCH 158/170] test(release): separate container and scalar fan-out bounds --- ...test_release_spdx_jsonld_envelope_contract.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index f004ce0cd..5a68e6510 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -22,6 +22,8 @@ def setUpClass(cls) -> None: cls.error_type = cls.namespace["SpdxJsonLdEnvelopeError"] cls.max_bytes = cls.namespace["MAX_SPDX_JSONLD_BYTES"] cls.max_graph_objects = cls.namespace["MAX_SPDX_GRAPH_OBJECTS"] + cls.max_containers = cls.namespace["MAX_SPDX_JSON_CONTAINERS"] + cls.max_structure_tokens = cls.namespace["MAX_SPDX_JSON_STRUCTURE_TOKENS"] @staticmethod def _payload(graph: list[object], *, context: object = CONTEXT) -> bytes: @@ -128,20 +130,22 @@ def test_graph_must_be_bounded_object_array(self) -> None: self._assert_error_code(self._payload(oversized_graph), "too_many_graph_objects") def test_nested_container_explosion_fails_before_json_materialization(self) -> None: - nested_container_count = 524_289 + nested_container_count = self.max_containers + 1 payload = ( '{"@context":"' + CONTEXT - + '","@graph":[{"type":"SpdxDocument","nested":[' - + "[]," * nested_container_count - + "[]]}]}" + + '","@graph":[{"type":"SpdxDocument","nested":' + + "[" * nested_container_count + + "0" + + "]" * nested_container_count + + "}]}" ).encode("utf-8") self.assertLess(len(payload), self.max_bytes) self._assert_error_code(payload, "too_many_json_containers") def test_scalar_fan_out_fails_before_json_materialization(self) -> None: - scalar_count = 1_048_577 + scalar_count = self.max_structure_tokens + 1 payload = ( '{"@context":"' + CONTEXT @@ -153,7 +157,7 @@ def test_scalar_fan_out_fails_before_json_materialization(self) -> None: self.assertLess(len(payload), self.max_bytes) self._assert_error_code(payload, "too_many_json_structure_tokens") - def test_container_preflight_ignores_structural_tokens_inside_strings(self) -> None: + def test_structure_preflight_ignores_structural_tokens_inside_strings(self) -> None: marker = '[{"nested":[]}]\\buyer"quote' summary = self.validate(self._payload([{"type": "SpdxDocument", "note": marker}])) From 6be9e8fd9191006691d06815aa804f0b9360ff4e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:19:37 +0900 Subject: [PATCH 159/170] docs(release): trace scalar JSON fan-out bound --- docs/traceability/release-sbom-owner-migration.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/traceability/release-sbom-owner-migration.md b/docs/traceability/release-sbom-owner-migration.md index 5d2a7cfee..0d5028a73 100644 --- a/docs/traceability/release-sbom-owner-migration.md +++ b/docs/traceability/release-sbom-owner-migration.md @@ -14,7 +14,7 @@ PR #221 currently carries release-specific SBOM identity and completeness semant 1. OriginWeave's first-party security/runtime boundary is Rust-first. Digest correspondence, bounded hostile JSON parsing, and descriptor-relative/no-follow file admission have practical Rust implementations, so Python cannot remain the canonical security boundary without an explicit no-practical-Rust rationale and removal condition. 2. Active PR #240 establishes `originweave-release` as the dedicated Release bounded context and moves release policy/evidence ownership out of `originweave-core`. Shipping #221 unchanged would therefore create competing canonical ownership. 3. The current Python secure-open path deliberately fails closed when POSIX-style `O_NOFOLLOW` plus descriptor-relative `os.open(..., dir_fd=...)` are unavailable. That is safe for the provisional verifier, but #201 is a cross-platform distribution/release track. The final Rust owner must preserve the same anti-symlink/reparse and identity-stability guarantees on each supported release platform instead of making a Unix-only primitive the commercial release contract. -4. A 16 MiB input-byte ceiling and 65,536 top-level `@graph` objects did not bound parser heap amplification. A roughly 1.5 MiB valid JSON document with one top-level `SpdxDocument` and more than 524,288 nested empty arrays was accepted by the preceding verifier and forced the standard JSON parser to materialize every nested container before any graph-cardinality check. The provisional boundary now performs a string-aware lexical container-count preflight before `json.loads`; the Rust migration must preserve an equivalent pre-materialization resource invariant rather than deserialize hostile input into an unbounded generic tree first. +4. A 16 MiB input-byte ceiling and 65,536 top-level `@graph` objects did not bound parser heap amplification. The first reproduction used a compact nested-container payload; a second reproduction showed that a roughly 2 MiB document with more than 1,048,576 scalar array members also passed the container-only preflight and was materialized by `json.loads`. The provisional boundary therefore now applies both a 524,288 opening-container ceiling and a 1,048,576 JSON structure-token ceiling before generic-tree materialization. The Rust migration must preserve an equivalent or stricter pre-materialization resource invariant rather than deserialize hostile input into an unbounded generic tree first. ## Target ownership @@ -38,7 +38,9 @@ There is **no compatibility shim** that makes core depend on Release, and there The Rust-owned replacement must preserve or strengthen the currently reviewed fail-closed behavior: - maximum 16 MiB candidate input before unbounded parsing/allocation; -- a pre-materialization JSON-container budget equivalent to or stricter than the provisional 524,288-container ceiling, with container-shaped text inside strings excluded from the structural count; +- a pre-materialization opening-container budget equivalent to or stricter than the provisional 524,288-container ceiling; +- a broader pre-materialization JSON structure-token budget equivalent to or stricter than the provisional 1,048,576-token ceiling so scalar/member fan-out is bounded as well as container count; +- bracket/brace/comma/colon text inside JSON strings must not consume structural budgets, while malformed JSON still fails in the grammar parser; - strict UTF-8 and JSON admission; - duplicate object-key and non-finite numeric rejection; - exact SPDX 3.0.1 global context identity for this preliminary gate; @@ -57,8 +59,8 @@ Digest/envelope success remains correspondence evidence only. It is not full SPD 1. Preserve #221's validated manifest-join, digest, envelope, hostile-input, file-admission tests and exact historical evidence as reconstruction input. Do not close the PR or discard its unique delta. 2. Resolve the dedicated Release owner lineage through #240 without treating its active branch as protected-main truth. Read intervening changes and reconstruct non-destructively rather than force-rebasing or copying the whole branch. -3. On the resulting `originweave-release` lineage, add the smallest realistic failing Rust test for each migrated canonical invariant and observe the failure before production implementation. Include a compact JSON container-amplification case that remains well below the byte ceiling but exceeds the admitted container budget, Linux/macOS symlink-swap and parent-replacement cases, plus Windows reparse-point/junction and handle-identity cases for every platform claimed by #201. -4. Implement the smallest Rust-owned admission boundary that depends inward on the core `ReleaseManifest` identity contract. Parse hostile JSON through a bounded or streaming path that enforces byte, container, graph, numeric, and diagnostic limits before an unbounded generic tree can be materialized. Keep platform-specific secure-open mechanics behind Release-owned adapters; do not introduce a core-to-release dependency, source copy, cross-context SQL, generic JSON authority, path-only portability fallback, or permissive fallback. +3. On the resulting `originweave-release` lineage, add the smallest realistic failing Rust test for each migrated canonical invariant and observe the failure before production implementation. Include both compact nested-container and scalar/member fan-out cases that remain well below the byte ceiling, Linux/macOS symlink-swap and parent-replacement cases, plus Windows reparse-point/junction and handle-identity cases for every platform claimed by #201. +4. Implement the smallest Rust-owned admission boundary that depends inward on the core `ReleaseManifest` identity contract. Parse hostile JSON through a bounded or streaming path that enforces byte, container, structure-token/member, graph, numeric, and diagnostic limits before an unbounded generic tree can be materialized. Keep platform-specific secure-open mechanics behind Release-owned adapters; do not introduce a core-to-release dependency, source copy, cross-context SQL, generic JSON authority, path-only portability fallback, or permissive fallback. 5. Reduce `scripts/release/validate_spdx_jsonld.py` to a thin conformance/fixture harness only after Rust owns the release decision boundary and parity regressions prove the behavior. 6. Update ADR 0018, `ARCHITECTURE.md`, `CHANGELOG.md`, doctoring/TRACEABILITY, and the product/technical gap baseline to name the final owner and distinguish protected-main truth from active-PR evidence. 7. Require unchanged exact-head Rust 1.97.1 formatting/check/tests/strict Clippy/rustdoc, 100% owned-production function/line/region/branch coverage, applicable security/review gates, current live-base verification, and supported-platform parity before any readiness transition. @@ -67,7 +69,7 @@ Digest/envelope success remains correspondence evidence only. It is not full SPD The migration contract was introduced test-first on #221 after reviews `5095297316` (Rust-first boundary) and `5095598850` (DDD owner conflict). The predecessor #221 head was `f341ab946beb509f53a9cc8cc52fc20d650faa87`; the test-only owner-migration contract is commit `f3a519e986dc6bda46fb03ac3768ea2bef7a131d`. Review `5096271206` adds the cross-platform secure-open finding after confirming that the provisional Python implementation explicitly rejects platforms without POSIX-style no-follow and descriptor-relative open support. Active #240 was last independently read at `47ab81b721aca246948607754be1807b9d4c8dda` and was not exact-head GREEN. -The parser-amplification repair was also performed test-first on #221. Test-only commit `e9c43d9940c03df4ead17f1e2b0b80efa5fcfd0f` adds a roughly 1.5 MiB payload containing more than 524,288 nested empty arrays under one `SpdxDocument`; the preceding verifier accepted the same shape in the focused reproduction instead of failing closed. Production commit `3e2b4a39b0bb22309f70cf88aaa88a536ec25d1d` adds a string-aware lexical preflight that returns `too_many_json_containers` before `json.loads` materialization, and `0317263f6e412c2040b4bdfe578781c6fa24a11c` adds escaped-string coverage so `[` and `{` inside JSON string values do not consume the structural budget. This is provisional Python hardening while the Rust owner migration remains blocked on dependency order; it is not evidence that the canonical Rust release boundary exists. +The parser-amplification repair was performed test-first in two passes. Test-only `e9c43d9940c03df4ead17f1e2b0b80efa5fcfd0f` exposed nested-container amplification below the byte ceiling; `3e2b4a39b0bb22309f70cf88aaa88a536ec25d1d` added the first string-aware opening-container preflight and `0317263f6e412c2040b4bdfe578781c6fa24a11c` covered escaped-string handling. Follow-up review found that a flat nested scalar array could still create more than one million parsed values with only a handful of containers. Test-only `cbd0d9629d4d5ce42d217c47c54e5f6d33e83255` exposes that scalar fan-out; production `d301f8f79e34f3f09744b0c38c71a54e58ad6283` adds the broader JSON structure-token budget; and `96e8dbd2ad0414481b0ff6ff0e841751147fb780` separates the container-specific and scalar-specific edge cases using the production-exported limits. These are provisional Python hardening commits while the Rust owner migration remains dependency-ordered; they are not evidence that the canonical Rust release boundary exists. This document is the causal documentation fix for the missing durable owner-migration, parser-resource, and portability contracts. It does not claim that the Rust migration itself has occurred, that Windows/macOS parity has been implemented, that #240 has shipped, or that #221 is exact-head GREEN. From 9f2a51591d11e8c8f2205475798c6e2a2a0855f1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:20:29 +0900 Subject: [PATCH 160/170] docs(adr): include scalar JSON fan-out admission --- docs/adr/0018-release-sbom-identity.md | 35 +++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/adr/0018-release-sbom-identity.md b/docs/adr/0018-release-sbom-identity.md index fe9013991..7d434f8a0 100644 --- a/docs/adr/0018-release-sbom-identity.md +++ b/docs/adr/0018-release-sbom-identity.md @@ -11,7 +11,7 @@ SPDX 3.0.1 defines JSON-LD serialization and separate structural and semantic va Those two official 3.0.1 resources therefore expose an interoperability tension: the prose describes extension mappings that JSON-LD can represent with context composition, while the published structural schema accepts only the exact context string. More importantly for this preliminary verifier, an arbitrary inline JSON-LD context object is not merely a namespace map: JSON-LD control terms such as `@import`, `@base`, and `@vocab`, or redefinitions of SPDX aliases such as `type` and `spdxId`, can change downstream interpretation. Treating every inline object as a benign namespace map would therefore create semantic and remote-authority ambiguity before schema/ontology validation. -RFC 8259 permits implementations to set limits on accepted JSON size and nesting depth. That latitude matters here because a byte ceiling alone is not a heap-allocation ceiling. The preceding verifier admitted a roughly 1.5 MiB valid JSON document containing one top-level `SpdxDocument` and more than 524,288 nested empty arrays. `json.loads` had to materialize those containers before the later top-level `@graph` cardinality check could run. A release-facing hostile-input boundary therefore needs a pre-materialization resource invariant in addition to its byte, graph, and numeric-token bounds. +RFC 8259 permits implementations to set limits on accepted JSON size and nesting depth. That latitude matters here because a byte ceiling alone is not a heap-allocation ceiling. The preceding verifier admitted compact valid documents that stayed well below 16 MiB while forcing large generic trees: first through more than 524,288 nested containers, then through more than 1,048,576 scalar array members with only a few containers. A release-facing hostile-input boundary therefore needs pre-materialization structural fan-out invariants in addition to byte, graph, and numeric-token bounds. OriginWeave separates four claims that must not be collapsed: @@ -30,7 +30,7 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - The gate should remain structurally compatible with the official SPDX 3.0.1 JSON Schema until a reviewed schema-aware extension policy exists. - Actual serialized bytes must not be accepted merely because metadata says they are SPDX 3.0.1 JSON-LD. - A syntactically valid but substituted SPDX document must not satisfy release evidence when its bytes do not match the exact SBOM artifact digest already admitted by the release manifest. -- Envelope parsing must be bounded before deeper semantic use and before unbounded generic-tree materialization. A compact payload must not bypass resource policy merely because it remains below the byte ceiling or keeps the top-level graph small. +- Envelope parsing must be bounded before deeper semantic use and before unbounded generic-tree materialization. Compact container or scalar/member fan-out must not bypass resource policy merely because the byte ceiling or top-level graph bound is still satisfied. - Envelope parsing must reject malformed UTF-8, malformed JSON, duplicate JSON object members, non-finite JSON constants, unexpected top-level members, non-object graph entries, excessive graph cardinality, a nested-only `SpdxDocument`, and zero or multiple `SpdxDocument` objects. - Error diagnostics must not reflect attacker- or supplier-controlled document bytes or release digest values. - The SBOM artifact itself must already be an admitted release artifact with a manifest-bound SHA-256 identity. @@ -45,8 +45,8 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. - The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase `sha256:` identity, and cannot also appear in the binding's described-artifact set. - Every other artifact admitted by that release manifest must be retained in the described-artifact set exactly once. This is an OriginWeave release-inventory completeness rule, not a general SPDX modeling assertion. -- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, performs a string-aware lexical preflight that admits at most 524,288 structural JSON containers before `json.loads`, requires exactly the top-level `@context` and `@graph` members, requires `@context` to equal the exact SPDX 3.0.1 global-context string, requires exactly one top-level `SpdxDocument`, and rejects any additional nested `SpdxDocument`. Context arrays and inline/remote context extensions fail closed at this stage. -- The lexical preflight counts `[` and `{` only outside JSON strings. It is a resource-admission check, not a substitute JSON grammar; `json.loads` remains responsible for syntax after the preflight. The 524,288 threshold is an OriginWeave provisional resource-policy ceiling rather than an SPDX semantic limit. +- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, performs a string-aware lexical preflight before `json.loads`, caps opening JSON containers at 524,288 and total structural `[]{}:,` tokens at 1,048,576, requires exactly the top-level `@context` and `@graph` members, requires `@context` to equal the exact SPDX 3.0.1 global-context string, requires exactly one top-level `SpdxDocument`, and rejects any additional nested `SpdxDocument`. Context arrays and inline/remote context extensions fail closed at this stage. +- The lexical preflight counts structural punctuation only outside JSON strings. It is a resource-admission check, not a substitute JSON grammar; `json.loads` remains responsible for syntax after the preflight. The 524,288-container and 1,048,576-structure-token thresholds are provisional OriginWeave resource-policy ceilings rather than SPDX semantic limits. - Release admission that composes those bytes with `ReleaseSbomBinding` uses `validate_release_spdx_3_0_1_jsonld_bytes` and supplies the exact canonical lowercase `sha256:` identity from `ReleaseSbomBinding::sbom_artifact()`. The helper recomputes SHA-256 from the bounded candidate bytes and rejects malformed expected identities or mismatches before promoting envelope evidence. - Digest equality proves correspondence to the declared manifest artifact identity only. It does not authenticate who produced the manifest or bytes and does not replace provenance, signature, transparency, reproducibility, or release-approval checks. - The exact-string context choice is deliberately narrower than the SPDX serialization prose because the official 3.0.1 JSON Schema itself uses that exact-string constraint and because this preliminary verifier does not implement complete JSON-LD context semantics or collision detection against the pinned SPDX context. @@ -66,7 +66,7 @@ Rejected for release admission. A completely valid SPDX document for another rel ### Bound only raw bytes and top-level graph cardinality -Rejected. A compact document can keep both measures within their limits while encoding hundreds of thousands of nested empty arrays or objects. A generic JSON parser then materializes that tree before the top-level graph check runs. The preliminary boundary therefore needs a structural-container budget before materialization, and the final Rust owner must enforce an equivalent or stricter invariant through a bounded or streaming parse path. +Rejected. A compact document can keep both measures within their limits while encoding hundreds of thousands of nested containers or more than a million scalar members. A generic JSON parser then materializes that tree before the top-level graph check runs. The preliminary boundary therefore needs both opening-container and broader structure-token budgets before materialization, and the final Rust owner must enforce an equivalent or stricter invariant through a bounded or streaming parse path. ### Admit a nested-only `SpdxDocument` @@ -113,31 +113,31 @@ OriginWeave adopts the following release-SBOM boundary: 7. Described artifact identities are stored deterministically and the manifest's existing bound transitively limits the inventory. 8. Release-facing validation of actual candidate bytes uses `validate_release_spdx_3_0_1_jsonld_bytes` with the exact `sha256:` identity from the manifest-backed SBOM artifact. The expected digest must be `sha256:` followed by 64 lowercase hexadecimal digits; the verifier recomputes SHA-256 over the same bounded bytes and a mismatch fails closed with a value-redacted typed error. 9. Only after exact digest correspondence is established do those candidate bytes pass through the narrow `validate_spdx_3_0_1_jsonld_bytes` envelope contract. The lower-level function remains available for composition and testing but is not by itself release-artifact correspondence evidence. -10. The envelope verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB. Before `json.loads`, it applies a string-aware structural preflight that admits at most 524,288 JSON containers and ignores bracket/brace characters inside JSON strings. It then rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, requires `@context` to equal the exact versioned SPDX 3.0.1 context string, allows at most 65,536 graph objects, requires object entries with string `type`, requires exactly one top-level raw `SpdxDocument` under the pinned alias semantics, and rejects any additional nested `SpdxDocument`. +10. The envelope verifier admits only non-empty strict-UTF-8 payloads no larger than 16 MiB. Before `json.loads`, it applies a string-aware structural preflight that admits at most 524,288 opening JSON containers and 1,048,576 total `[]{}:,` structural tokens, while ignoring that punctuation inside JSON strings. It then rejects duplicate JSON keys and non-finite constants, requires exactly the top-level `@context` and `@graph` members, requires `@context` to equal the exact versioned SPDX 3.0.1 context string, allows at most 65,536 graph objects, requires object entries with string `type`, requires exactly one top-level raw `SpdxDocument` under the pinned alias semantics, and rejects any additional nested `SpdxDocument`. 11. Validation errors expose stable error codes and generic diagnostics without reflecting document bytes or expected digest values. 12. Digest/envelope success is not a certificate of complete SPDX JSON Schema validity, semantic ontology/SHACL validity, package/component completeness, producer authenticity, provenance, signatures, publication, installation, update, rollback, or release authority. -13. No verifier fallback may substitute another SPDX version, discard the required pinned context or expected artifact digest, reinterpret an unvalidated context extension, discard duplicate keys, silently truncate a graph, bypass the structural resource budget, fetch an ambient remote resource, or convert malformed or mismatched content into success. -14. The eventual Rust-owned `originweave-release` implementation must preserve or strengthen the byte/container/graph/numeric and diagnostic limits before unbounded generic-tree materialization. The provisional Python scanner is migration evidence, not the permanent canonical parser/security boundary. +13. No verifier fallback may substitute another SPDX version, discard the required pinned context or expected artifact digest, reinterpret an unvalidated context extension, discard duplicate keys, silently truncate a graph, bypass either structural resource budget, fetch an ambient remote resource, or convert malformed or mismatched content into success. +14. The eventual Rust-owned `originweave-release` implementation must preserve or strengthen the byte/container/structure-token/graph/numeric and diagnostic limits before unbounded generic-tree materialization. The provisional Python scanner is migration evidence, not the permanent canonical parser/security boundary. ## Consequences -Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over the exact bytes named by that join. A syntactically valid SPDX document for a different artifact cannot satisfy the release-facing helper merely because its envelope is valid. A candidate also cannot pass this layer with the wrong SPDX context, a context array that can alter JSON-LD interpretation, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, excessive structural-container fan-out, a nested-only `SpdxDocument`, or zero/multiple `SpdxDocument` objects. +Downstream release tooling receives an exact versioned serialization identity, a non-circular and release-inventory-complete identity join, and a bounded first check over the exact bytes named by that join. A syntactically valid SPDX document for a different artifact cannot satisfy the release-facing helper merely because its envelope is valid. A candidate also cannot pass this layer with the wrong SPDX context, a context array that can alter JSON-LD interpretation, an extra top-level field, malformed JSON, duplicate members, a non-object graph entry, an unbounded graph, excessive container or scalar/member fan-out, a nested-only `SpdxDocument`, or zero/multiple `SpdxDocument` objects. The exact-context rule can reject a serialization that the SPDX 3.0.1 prose intends to allow through additional namespace mappings. That false negative is accepted at this preliminary gate because the same version's official structural schema currently requires the exact string and because OriginWeave does not yet possess a reviewed, schema-aware rule capable of proving that an extension object is namespace-only and non-colliding. Support for such mappings must be added only together with pinned full structural/semantic validation or another equally strong proof of interpretation. -The structural-container threshold can reject a JSON document that is syntactically valid and within the byte ceiling. That false negative is also accepted at this release-security boundary: RFC 8259 permits implementation resource limits, and OriginWeave prefers a bounded failure over generic-tree heap amplification. The exact ceiling is provisional and may change only with measured realistic SBOM corpus and resource evidence; it is not an SPDX modeling requirement. +The structural limits can reject a JSON document that is syntactically valid and within the byte ceiling. That false negative is also accepted at this release-security boundary: RFC 8259 permits implementation resource limits, and OriginWeave prefers a bounded failure over generic-tree heap amplification. The exact ceilings are provisional and may change only with measured realistic SBOM corpus and resource evidence; they are not SPDX modeling requirements. The boundary remains intentionally narrower than a commercial SBOM generator or complete verifier. The official SPDX 3.0.1 specification requires structural validation against the JSON Schema and semantic validation against the OWL ontology/SHACL constraints; those checks, package/component completeness, root-element rules, producer/provenance authentication, signing, and integrated release acceptance remain separate work. Digest/envelope success must never be presented as full SPDX conformance or signed supply-chain provenance. ## Failure and degraded behavior -Manifest-binding failures occur before a binding is produced. Release-facing byte verification rejects a malformed expected digest or candidate-byte digest mismatch with deterministic value-redacted error codes. Envelope failures likewise return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input and excessive structural-container fan-out are rejected before JSON tree materialization. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, context arrays/extensions, invalid top-level shape, non-object graph entries, excessive graph cardinality, nested-only documents, and invalid `SpdxDocument` cardinality all fail closed. +Manifest-binding failures occur before a binding is produced. Release-facing byte verification rejects a malformed expected digest or candidate-byte digest mismatch with deterministic value-redacted error codes. Envelope failures likewise return deterministic redacted error codes and no document-controlled value is echoed into the diagnostic. Oversized input, excessive opening-container count, and excessive broader structural fan-out are rejected before JSON tree materialization. Invalid UTF-8, invalid JSON, duplicate keys, missing or incorrect required context, context arrays/extensions, invalid top-level shape, non-object graph entries, excessive graph cardinality, nested-only documents, and invalid `SpdxDocument` cardinality all fail closed. Failure at either layer does not authorize release through another path. There is no alternate digest, alternate version fallback, network context fallback, permissive parse mode, resource-limit bypass, or silent default success. ## Security / privacy / governance impact -The boundary reduces supply-chain ambiguity without introducing ambient network authority. Exact SHA-256 correspondence prevents a different but otherwise valid SPDX document from being promoted under the manifest-backed SBOM identity. Pre-materialization container admission plus byte, graph, and numeric bounds constrain memory exposure before deeper validation; duplicate-key rejection prevents parser interpretation drift; top-level `SpdxDocument` placement prevents a nested object from masquerading as the serialization's document element; and the pinned exact SPDX context prevents version drift, remote `@import`, `@base`/`@vocab` mutation, and term/alias rebinding from being treated as already-admitted envelope semantics. Redacted diagnostics prevent supplier-controlled SBOM bytes or expected digest values from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. +The boundary reduces supply-chain ambiguity without introducing ambient network authority. Exact SHA-256 correspondence prevents a different but otherwise valid SPDX document from being promoted under the manifest-backed SBOM identity. Pre-materialization container and structure-token admission plus byte, graph, and numeric bounds constrain memory exposure before deeper validation; duplicate-key rejection prevents parser interpretation drift; top-level `SpdxDocument` placement prevents a nested object from masquerading as the serialization's document element; and the pinned exact SPDX context prevents version drift, remote `@import`, `@base`/`@vocab` mutation, and term/alias rebinding from being treated as already-admitted envelope semantics. Redacted diagnostics prevent supplier-controlled SBOM bytes or expected digest values from being reflected into CI/release logs. The verifier introduces no secrets, personal data, signing material, privileged network access, reviewer authority, or release authority. ## Tests and acceptance evidence @@ -149,8 +149,9 @@ The provisional Python release-verifier contract tests must prove: - a valid but substituted SPDX document fails closed on digest mismatch without reflecting document-controlled bytes; - malformed, uppercase, truncated, or prefixless expected digests fail closed; - exact SPDX 3.0.1 context and one top-level `SpdxDocument` are admitted; -- a compact payload below the 16 MiB byte ceiling but above the structural-container budget fails before JSON tree materialization; -- bracket/brace characters and escaped quote/backslash content inside JSON strings do not consume the structural-container budget; +- a compact payload below the 16 MiB byte ceiling but above the opening-container budget fails before JSON tree materialization; +- a compact scalar-array payload below the byte ceiling but above the broader structure-token budget also fails before JSON tree materialization; +- bracket/brace/comma/colon characters and escaped quote/backslash content inside JSON strings do not consume the structural budgets; - a nested-only `SpdxDocument` and a nested second `SpdxDocument` fail closed; - context arrays, including apparently simple namespace mappings, fail closed until schema-aware extension validation exists; - `@import`, `@base`, `@vocab`, `type`, and `spdxId` context overrides fail closed; @@ -158,18 +159,18 @@ The provisional Python release-verifier contract tests must prove: - duplicate JSON keys, malformed UTF-8, non-finite constants, invalid graph entries, excessive graph size, and zero/multiple documents fail closed; and - hostile external bytes never appear in diagnostics. -The final Rust Release migration must reproduce the same parser-resource RED before implementing its bounded or streaming parser. Repository Python contracts, Rust 1.97.1 formatting/check/tests/Clippy/rustdoc, exact Rust production function/line/region/branch coverage, and any applicable browser/security gates must pass on the unchanged exact head. Predecessor evidence does not transfer. +The final Rust Release migration must reproduce both parser-amplification RED classes before implementing its bounded or streaming parser. Repository Python contracts, Rust 1.97.1 formatting/check/tests/Clippy/rustdoc, exact Rust production function/line/region/branch coverage, and any applicable browser/security gates must pass on the unchanged exact head. Predecessor evidence does not transfer. ## Migration and rollback -This remains a pre-release branch with no protected-main persisted SBOM schema migration. The verifier has no network or durable state. Before acceptance it can be revised or withdrawn with its owning branch. If a durable external release/SBOM format depends on this boundary, incompatible digest, context, size, structural-container, graph, or error-contract changes require explicit versioning and migration rather than silent parser broadening. +This remains a pre-release branch with no protected-main persisted SBOM schema migration. The verifier has no network or durable state. Before acceptance it can be revised or withdrawn with its owning branch. If a durable external release/SBOM format depends on this boundary, incompatible digest, context, size, container, structure-token, graph, or error-contract changes require explicit versioning and migration rather than silent parser broadening. ## Open follow-ups - Generate deterministic SPDX 3.0.1 SBOM content for supported release packages. - Validate the same digest-bound candidate bytes against reviewed, immutable SPDX 3.0.1 JSON Schema and OWL/SHACL resources using offline-verifiable identities. - Reconcile the SPDX 3.0.1 prose allowance for additional namespace mappings with the official 3.0.1 JSON Schema's exact-string `@context` constraint before admitting context composition. -- Migrate the release-facing hostile JSON/resource boundary into `originweave-release` Rust with a bounded or streaming parser and preserve the pre-materialization resource contract. +- Migrate the release-facing hostile JSON/resource boundary into `originweave-release` Rust with a bounded or streaming parser and preserve both pre-materialization resource contracts. - Define package/component completeness and root-element rules for OriginWeave distribution artifacts. - Compose the digest-bound SBOM result with SLSA provenance, signing identity, timestamps, platform packages, reproducibility evidence, and updater trust. - Complete issue #201's integrated release acceptance and rollback/freeze-protection architecture. From c06bc96d322b1bf03544c3f17bf73cefe612ac34 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:22:05 +0900 Subject: [PATCH 161/170] docs(doctoring): trace bounded SPDX JSON parsing --- docs/doctoring.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 568983eee..bc5ec81c3 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -90,6 +90,8 @@ Microsoft's Win32 filename guidance requires applications not to assume case sen SPDX 3.0.1 defines JSON-LD as an RDF serialization for SPDX data and requires conformant JSON-LD to pass both the versioned JSON Schema and the SPDX ontology/SHACL constraints. Its serialization prose requires the global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`, defines `spdxId` and `type` as aliases for JSON-LD `@id` and `@type`, and requires Element nodes—including `SpdxDocument`—to be represented as top-level objects under `@graph`. It also says additional namespace mappings may be defined in a separate context object. The official SPDX 3.0.1 JSON Schema simultaneously constrains top-level `@context` to the exact global-context string. Because an arbitrary inline JSON-LD context object can contain semantic controls such as `@import`, `@base`, or `@vocab`, or redefine SPDX aliases, OriginWeave does not approximate “namespace mapping” by accepting every object. The preliminary envelope gate therefore requires exactly one top-level `SpdxDocument`, rejects a nested-only document as well as any additional nested document, follows the official schema's exact-string context identity, and rejects context composition until a reviewed schema-aware validator can reconcile the prose/schema tension without granting ambient or redefinition authority. This gate deliberately does not claim full content-level conformance: it binds a declared SPDX 3.0.1 JSON-LD SBOM artifact identity and exact release-artifact names to one immutable `ReleaseManifest`, while generation, full structural/semantic validation, package/component completeness, signatures, provenance, publication, installation, update, rollback, and release authority remain separate reviewed boundaries. +RFC 8259 §9 permits implementations to set limits on accepted JSON size and nesting depth. OriginWeave applies that implementation latitude narrowly to hostile release evidence. A 16 MiB byte ceiling and 65,536 top-level graph-object ceiling did not prevent compact valid documents from forcing a large generic JSON tree: one reproduction exceeded 524,288 nested containers, and another exceeded 1,048,576 scalar array members while using only a few containers. The provisional Python envelope gate now scans structural punctuation outside JSON strings before `json.loads`, caps opening containers at 524,288 and total `[]{}:,` structure tokens at 1,048,576, and leaves grammar validation to the JSON parser. These are OriginWeave resource-policy ceilings rather than SPDX semantic limits. The final Rust-owned Release boundary must reproduce both amplification cases and enforce equivalent or stricter pre-materialization byte/container/member/graph/numeric bounds through a bounded or streaming parse path; Python remains migration evidence rather than canonical release/security authority. + ### Git source identity admission Git's protocol grammar distinguishes an ordinary 40-hex object identifier from `zero-id = 40*"0"`. The pack protocol uses that all-zero sentinel to represent absent refs and create/delete reference state rather than a concrete source object. OriginWeave therefore requires release-manifest `source_commit` and WARC PROV `software_commit_sha` values to be canonical lowercase 40-hex identifiers with at least one nonzero digit. This is an identity-admission rule only: it prevents a null Git sentinel from being represented as a concrete software revision, but does not prove repository reachability, GitHub authenticity, commit trust, provenance, or release authority. @@ -134,6 +136,8 @@ Berners-Lee, T., Fielding, R., & Masinter, L. (2005). *Uniform resource identifi Bonica, R., Cotton, M., Haberman, B., & Vegoda, L. (2017). *Updates to the special-purpose IP address registries* (RFC 8190). Internet Engineering Task Force. https://doi.org/10.17487/RFC8190 +Bray, T. (2017). *The JavaScript Object Notation (JSON) Data Interchange Format* (RFC 8259; STD 90). Internet Engineering Task Force. https://doi.org/10.17487/RFC8259 + Chromium Authors. (n.d.). *Proxy support in Chrome* [Source documentation]. Chromium. https://chromium.googlesource.com/chromium/src/+/a3e71ebfa307d8760eb68b777e2998a869940092/net/docs/proxy.md Chromium Authors. (2026). *URL canonicalizer unit tests* [Source code]. Chromium. https://chromium.googlesource.com/chromium/src/+/446d05d21720f0b3505ec21057b3e9f909784262/url/url_canon_unittest.cc @@ -222,4 +226,4 @@ World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 -Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 +Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 \ No newline at end of file From 7866c781051c09a456edf76896ee640297a7cc02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:23:39 +0900 Subject: [PATCH 162/170] docs(changelog): include scalar SPDX parser fan-out bound --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e52f9588..a231fb1e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. - Protected main now contains deterministic MCP `2026-07-28` stateless `tools/call` routing with bounded method/tool names, a single reviewed tool-to-action registry shared by routing and discovery metadata, and fail-closed policy binding that grants no ambient authority. The complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned. -- Active PR #170 adds conservative MCP `2026-07-28` `tools/list` discovery metadata derived from that protected-main catalog, with `resultType = complete`, zero freshness, private cache scope, no continuation cursor, per-request protocol/client-capability admission, and bounded protocol-version and method metadata validated before cross-field comparison. This remains active-PR evidence only and grants no browser, network, secret, approval, or Agent authority. +- Active PR #170 adds conservative MCP `2026-07-28` `tools/list` discovery metadata derived from that protected-main catalog, with `resultType = complete`, zero freshness, private cache scope, no continuation cursor, per-request protocol-version and method metadata validated before cross-field comparison. This remains active-PR evidence only and grants no browser, network, secret, approval, or Agent authority. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Bounded resolution-freshness authority with trusted monotonic approval time, capped non-zero validity, half-open use windows, non-expanding revalidation, and credential-free authorization timestamps. @@ -73,12 +73,11 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security -- The provisional SPDX 3.0.1 release-envelope parser now rejects more than 524,288 JSON containers in a string-aware lexical preflight before `json.loads`, preventing a compact nested-container payload from amplifying into an unbounded in-memory object graph while preserving `[` and `{` that occur inside JSON string values. The eventual Rust-owned Release boundary must retain an equivalent pre-materialization resource invariant. +- The provisional SPDX 3.0.1 release-envelope parser now rejects both excessive opening-container depth/fan-out and broader scalar/member fan-out before `json.loads`: opening containers are capped at 524,288 and total structural `[]{}:,` tokens at 1,048,576, counting only punctuation outside JSON strings. This prevents compact hostile documents below the 16 MiB byte ceiling from amplifying into an unbounded generic object tree while preserving structural-looking text inside string values. The eventual Rust-owned Release boundary must retain an equivalent or stricter pre-materialization resource invariant. - The SPDX 3.0.1 release-envelope gate now requires exactly one top-level `SpdxDocument` under `@graph` and rejects both a nested-only document and any additional nested `SpdxDocument`, preventing recursive string/type discovery from substituting for the required serialization placement. - The SPDX 3.0.1 release-envelope gate requires the exact versioned global-context string and rejects context arrays, `@import`, `@base`, `@vocab`, and alias/term-rebinding attempts until reviewed schema-aware validation can prove extension semantics without ambient or reinterpretation authority. - SPDX JSON-LD release-file admission now requires a direct regular-file leaf reached through a no-follow descriptor-relative path walk and revalidates the final leaf's device/inode identity after the bounded read, so symlinks, FIFOs, swapped ancestors, and post-open pathname replacement fail closed instead of silently changing the artifact path identity. - Explicit proxy server identifiers require ASCII decimal port tokens before numeric range parsing, preventing Rust-specific leading-plus spellings from widening proxy authority. -- Explicit proxy server identifiers require ASCII decimal port tokens before numeric range parsing, preventing Rust-specific leading-plus spellings from widening proxy authority. - Release artifact admission rejects `COM0` through `COM9` and `LPT0` through `LPT9` case-insensitively, including extensions, as bounded filename identity hygiene. OneDrive and SharePoint impose additional restrictions, including `desktop.ini`, that this validator does not model; this is not a complete OneDrive or SharePoint synchronization-compatibility guarantee. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. @@ -111,4 +110,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD From 84b57dd30db205ea74d1213b9ac8cf6081751759 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 10:24:47 +0900 Subject: [PATCH 163/170] docs(changelog): restore unrelated release notes --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a231fb1e0..1a8fdbc53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. - Protected main now contains deterministic MCP `2026-07-28` stateless `tools/call` routing with bounded method/tool names, a single reviewed tool-to-action registry shared by routing and discovery metadata, and fail-closed policy binding that grants no ambient authority. The complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned. -- Active PR #170 adds conservative MCP `2026-07-28` `tools/list` discovery metadata derived from that protected-main catalog, with `resultType = complete`, zero freshness, private cache scope, no continuation cursor, per-request protocol-version and method metadata validated before cross-field comparison. This remains active-PR evidence only and grants no browser, network, secret, approval, or Agent authority. +- Active PR #170 adds conservative MCP `2026-07-28` `tools/list` discovery metadata derived from that protected-main catalog, with `resultType = complete`, zero freshness, private cache scope, no continuation cursor, per-request protocol/client-capability admission, and bounded protocol-version and method metadata validated before cross-field comparison. This remains active-PR evidence only and grants no browser, network, secret, approval, or Agent authority. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Bounded resolution-freshness authority with trusted monotonic approval time, capped non-zero validity, half-open use windows, non-expanding revalidation, and credential-free authorization timestamps. @@ -78,6 +78,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - The SPDX 3.0.1 release-envelope gate requires the exact versioned global-context string and rejects context arrays, `@import`, `@base`, `@vocab`, and alias/term-rebinding attempts until reviewed schema-aware validation can prove extension semantics without ambient or reinterpretation authority. - SPDX JSON-LD release-file admission now requires a direct regular-file leaf reached through a no-follow descriptor-relative path walk and revalidates the final leaf's device/inode identity after the bounded read, so symlinks, FIFOs, swapped ancestors, and post-open pathname replacement fail closed instead of silently changing the artifact path identity. - Explicit proxy server identifiers require ASCII decimal port tokens before numeric range parsing, preventing Rust-specific leading-plus spellings from widening proxy authority. +- Explicit proxy server identifiers require ASCII decimal port tokens before numeric range parsing, preventing Rust-specific leading-plus spellings from widening proxy authority. - Release artifact admission rejects `COM0` through `COM9` and `LPT0` through `LPT9` case-insensitively, including extensions, as bounded filename identity hygiene. OneDrive and SharePoint impose additional restrictions, including `desktop.ini`, that this validator does not model; this is not a complete OneDrive or SharePoint synchronization-compatibility guarantee. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. From 16a2e1eedf121c6e89eb9019db0906e8ea49f0ce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 11:13:25 +0900 Subject: [PATCH 164/170] test(release): bound SPDX JSON nesting depth --- ...ease_spdx_jsonld_nesting_depth_contract.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/test_release_spdx_jsonld_nesting_depth_contract.py diff --git a/tests/test_release_spdx_jsonld_nesting_depth_contract.py b/tests/test_release_spdx_jsonld_nesting_depth_contract.py new file mode 100644 index 000000000..919657747 --- /dev/null +++ b/tests/test_release_spdx_jsonld_nesting_depth_contract.py @@ -0,0 +1,41 @@ +"""Regression contract for bounded JSON nesting before SPDX tree materialization.""" + +from __future__ import annotations + +import pathlib +import runpy +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +VALIDATOR = ROOT / "scripts" / "release" / "validate_spdx_jsonld.py" + + +class ReleaseSpdxJsonLdNestingDepthContractTests(unittest.TestCase): + """Require a product-owned nesting budget before the generic JSON parser.""" + + def test_nesting_beyond_product_budget_fails_in_preflight(self) -> None: + """Deep valid JSON must fail before parser recursion becomes the resource boundary.""" + + namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_nesting_depth_contract") + max_depth = namespace["MAX_SPDX_JSON_NESTING_DEPTH"] + preflight = namespace["_reject_excessive_json_structure"] + envelope_error = namespace["SpdxJsonLdEnvelopeError"] + + text = "[" * (max_depth + 1) + "0" + "]" * (max_depth + 1) + with self.assertRaises(envelope_error) as captured: + preflight(text) + + self.assertEqual(captured.exception.code, "too_deep_json_structure") + + def test_structural_characters_inside_strings_do_not_consume_depth_budget(self) -> None: + """Bracket-looking string content must remain inert for the nesting budget.""" + + namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_nesting_string_contract") + max_depth = namespace["MAX_SPDX_JSON_NESTING_DEPTH"] + preflight = namespace["_reject_excessive_json_structure"] + + preflight('"' + ("[" * (max_depth + 1)) + '"') + + +if __name__ == "__main__": + unittest.main() From 93f1d13e38c07dbe1217bbfa50d18d5eb696f094 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 12:05:46 +0900 Subject: [PATCH 165/170] fix(release): reject excessive SPDX JSON nesting before parse --- scripts/release/validate_spdx_jsonld.py | 23 +++++++++++++------ ...t_release_spdx_jsonld_envelope_contract.py | 2 +- ...ease_spdx_jsonld_nesting_depth_contract.py | 20 ++++++++++++++-- 3 files changed, 35 insertions(+), 10 deletions(-) mode change 100644 => 100755 scripts/release/validate_spdx_jsonld.py diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py old mode 100644 new mode 100755 index a9372c9c4..3904909c0 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -32,6 +32,7 @@ MAX_SPDX_GRAPH_OBJECTS = 65_536 MAX_SPDX_JSON_CONTAINERS = 524_288 MAX_SPDX_JSON_STRUCTURE_TOKENS = 1_048_576 +MAX_SPDX_JSON_NESTING_DEPTH = 256 MAX_SPDX_JSON_NUMBER_TOKEN_BYTES = 4096 _EXPECTED_PARENT_IDENTITIES: contextvars.ContextVar[tuple[tuple[int, int], ...]] = ( contextvars.ContextVar("spdx_expected_parent_identities", default=()) @@ -83,17 +84,20 @@ def _finite_json_float(value: str) -> float: def _reject_excessive_json_structure(text: str) -> None: - """Reject hostile JSON fan-out before the standard parser materializes the tree. + """Reject hostile JSON fan-out and depth before the standard parser materializes the tree. The byte-size limit alone does not bound heap amplification: a compact JSON string can - encode hundreds of thousands of containers or more than a million scalar/object members. + encode hundreds of thousands of containers, more than a million scalar/object members, or + enough nested containers to make interpreter recursion the effective resource boundary. This lexical preflight counts structure only outside JSON strings. Opening containers have - a tighter dedicated budget, while all brackets, braces, commas, and colons share a broader - structure-token budget that also constrains scalar fan-out. Grammar validation still - belongs to ``json.loads`` immediately after this resource check. + a tighter dedicated aggregate budget, current nesting has an independent depth budget, and + all brackets, braces, commas, and colons share a broader structure-token budget that also + constrains scalar fan-out. Grammar validation still belongs to ``json.loads`` immediately + after this resource check. """ container_count = 0 + current_depth = 0 structure_token_count = 0 in_string = False escaped = False @@ -112,8 +116,13 @@ def _reject_excessive_json_structure(text: str) -> None: continue if character in "[{": container_count += 1 + current_depth += 1 + if current_depth > MAX_SPDX_JSON_NESTING_DEPTH: + raise SpdxJsonLdEnvelopeError("too_deep_json_structure") if container_count > MAX_SPDX_JSON_CONTAINERS: raise SpdxJsonLdEnvelopeError("too_many_json_containers") + elif character in "]}": + current_depth -= 1 if character in "[]{}:,": structure_token_count += 1 if structure_token_count > MAX_SPDX_JSON_STRUCTURE_TOKENS: @@ -163,8 +172,8 @@ def validate_spdx_3_0_1_jsonld_bytes(payload: bytes) -> dict[str, int | str]: External document bytes are never included in raised errors or retained by their exception chain. Successful validation proves only the narrow envelope contract documented by this module; callers must still perform the official SPDX JSON Schema and - OWL/SHACL validation before claiming SPDX conformance. Excessive JSON nesting is treated - as invalid external input instead of escaping the typed, value-redacted validation + OWL/SHACL validation before claiming SPDX conformance. Excessive JSON nesting is rejected + by the product-owned lexical preflight before the generic parser becomes the resource boundary. """ diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 5a68e6510..6ade1d015 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -261,7 +261,7 @@ def test_excessive_json_nesting_fails_with_typed_redacted_error(self) -> None: + "}]}" ).encode("utf-8") - error = self._assert_error_code(payload, "invalid_json") + error = self._assert_error_code(payload, "too_deep_json_structure") self.assertNotIn(marker, str(error)) def test_empty_and_oversized_payloads_fail_before_semantic_use(self) -> None: diff --git a/tests/test_release_spdx_jsonld_nesting_depth_contract.py b/tests/test_release_spdx_jsonld_nesting_depth_contract.py index 919657747..97a8f37bd 100644 --- a/tests/test_release_spdx_jsonld_nesting_depth_contract.py +++ b/tests/test_release_spdx_jsonld_nesting_depth_contract.py @@ -13,10 +13,26 @@ class ReleaseSpdxJsonLdNestingDepthContractTests(unittest.TestCase): """Require a product-owned nesting budget before the generic JSON parser.""" + @staticmethod + def _namespace(run_name: str) -> dict[str, object]: + """Load the exact validator module without importing repository-global state.""" + + return runpy.run_path(str(VALIDATOR), run_name=run_name) + + def test_nesting_at_product_budget_is_admitted_by_preflight(self) -> None: + """The exact product depth ceiling must remain usable rather than off by one.""" + + namespace = self._namespace("spdx_nesting_depth_boundary_contract") + max_depth = namespace["MAX_SPDX_JSON_NESTING_DEPTH"] + preflight = namespace["_reject_excessive_json_structure"] + + text = "[" * max_depth + "0" + "]" * max_depth + preflight(text) + def test_nesting_beyond_product_budget_fails_in_preflight(self) -> None: """Deep valid JSON must fail before parser recursion becomes the resource boundary.""" - namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_nesting_depth_contract") + namespace = self._namespace("spdx_nesting_depth_contract") max_depth = namespace["MAX_SPDX_JSON_NESTING_DEPTH"] preflight = namespace["_reject_excessive_json_structure"] envelope_error = namespace["SpdxJsonLdEnvelopeError"] @@ -30,7 +46,7 @@ def test_nesting_beyond_product_budget_fails_in_preflight(self) -> None: def test_structural_characters_inside_strings_do_not_consume_depth_budget(self) -> None: """Bracket-looking string content must remain inert for the nesting budget.""" - namespace = runpy.run_path(str(VALIDATOR), run_name="spdx_nesting_string_contract") + namespace = self._namespace("spdx_nesting_string_contract") max_depth = namespace["MAX_SPDX_JSON_NESTING_DEPTH"] preflight = namespace["_reject_excessive_json_structure"] From 108fc9b8f8b42abdb8c03543525255955a2852c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 13:07:04 +0900 Subject: [PATCH 166/170] test(release): require depth budget in owner migration contract --- tests/test_release_sbom_owner_migration_contract.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_release_sbom_owner_migration_contract.py b/tests/test_release_sbom_owner_migration_contract.py index 989add737..cb94e0686 100644 --- a/tests/test_release_sbom_owner_migration_contract.py +++ b/tests/test_release_sbom_owner_migration_contract.py @@ -42,6 +42,18 @@ def test_windows_secure_open_contract_names_non_reparse_and_identity_primitives( for token in required: self.assertIn(token, text) + def test_migration_preserves_explicit_json_nesting_budget(self): + """The Rust-owner plan must retain the product-owned pre-materialization depth limit.""" + text = MIGRATION.read_text(encoding="utf-8") + required = ( + "MAX_SPDX_JSON_NESTING_DEPTH", + "256", + "too_deep_json_structure", + "current nesting depth", + ) + for token in required: + self.assertIn(token, text) + if __name__ == "__main__": unittest.main() From b71240b1c0426582651fc8f24231f8f55b2b3162 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 13:08:44 +0900 Subject: [PATCH 167/170] revert: keep migration contract aligned with writable docs boundary --- tests/test_release_sbom_owner_migration_contract.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test_release_sbom_owner_migration_contract.py b/tests/test_release_sbom_owner_migration_contract.py index cb94e0686..989add737 100644 --- a/tests/test_release_sbom_owner_migration_contract.py +++ b/tests/test_release_sbom_owner_migration_contract.py @@ -42,18 +42,6 @@ def test_windows_secure_open_contract_names_non_reparse_and_identity_primitives( for token in required: self.assertIn(token, text) - def test_migration_preserves_explicit_json_nesting_budget(self): - """The Rust-owner plan must retain the product-owned pre-materialization depth limit.""" - text = MIGRATION.read_text(encoding="utf-8") - required = ( - "MAX_SPDX_JSON_NESTING_DEPTH", - "256", - "too_deep_json_structure", - "current nesting depth", - ) - for token in required: - self.assertIn(token, text) - if __name__ == "__main__": unittest.main() From 13edf50754bfc6c33d52ef9098ad9bafff24bb06 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 13:08:59 +0900 Subject: [PATCH 168/170] docs(release): record JSON nesting-depth invariant --- docs/traceability/release-sbom-depth-256.md | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/traceability/release-sbom-depth-256.md diff --git a/docs/traceability/release-sbom-depth-256.md b/docs/traceability/release-sbom-depth-256.md new file mode 100644 index 000000000..a1bbf686e --- /dev/null +++ b/docs/traceability/release-sbom-depth-256.md @@ -0,0 +1,22 @@ +# Release SBOM JSON nesting-depth invariant + +- Status: Proposed +- Owning issue: #201 +- Active repair: #221 +- Intended Release owner: `originweave-release` after the #240 owner lineage is integrated + +PR #221 currently sets `MAX_SPDX_JSON_NESTING_DEPTH = 256` in the provisional SPDX 3.0.1 JSON-LD verifier. The string-aware preflight rejects depth 257 with `too_deep_json_structure` before `json.loads`, admits the exact depth-256 boundary to normal JSON grammar validation, and does not count bracket-looking characters inside JSON strings toward the depth budget. + +This limit is an OriginWeave resource-admission policy, not an SPDX semantic requirement. RFC 8259 section 9 permits JSON implementations to set a maximum nesting depth. The final Rust-owned Release implementation must preserve or deliberately strengthen the same explicit pre-materialization depth invariant together with the existing byte, container, structure-token/member, graph, and numeric limits; generic parser recursion must not become the effective boundary. + +The depth lineage is review `5097073272`, test-only candidate `16a2e1eedf121c6e89eb9019db0906e8ea49f0ce`, and production/test repair `93f1d13e38c07dbe1217bbfa50d18d5eb696f094`. The test-only candidate did not acquire a hosted runner, so it is candidate test-first evidence rather than an observed hosted RED. + +As of September 3, 2026, #221 still targets SPDX 3.0.1. SPDX 3.1-RC1 is a release candidate and does not implicitly change this contract; adopting it requires an explicit version, schema, serialization, and ontology migration. + +## References + +Bray, T. (2017). *The JavaScript Object Notation (JSON) Data Interchange Format* (RFC 8259; STD 90). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc8259 + +SPDX Workgroup. (2026). *SPDX specification 3.0.1: Model and serializations*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.0.1/serializations/ + +SPDX Workgroup. (2026). *SPDX specification 3.1-RC1: Model and serializations*. The Linux Foundation. https://spdx.github.io/spdx-spec/v3.1-RC1/serializations/ From 25eb15aca0d66010832a1e234700f58137c6a87c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 13:09:35 +0900 Subject: [PATCH 169/170] docs(release): index SBOM depth traceability --- docs/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index b17fca571..6a7fd7a3a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ - [Conceptual ERD and durable domain model](erd/README.md) - [Data governance and privacy boundary](DATA_GOVERNANCE.md) - [Product and decision traceability](traceability/README.md) + - [Release SBOM JSON nesting-depth invariant](traceability/release-sbom-depth-256.md) - [Documentation fitness assessment](DOCUMENTATION_FITNESS.md) - [Dated active-PR maturity evidence (2026-08-10)](evidence/2026-08-10-active-pr-maturity.md) - [Active-PR maturity delta (2026-08-11)](evidence/2026-08-11-active-pr-maturity-delta.md) @@ -97,4 +98,4 @@ ADR 0015 is active-PR architecture evidence for the release-manifest identity sl After protected-main integration, retain this subsection only when it is intentionally serving as historical provenance; otherwise protected-main reconciliation must remove it. In either case, integration alone does not change these ADRs from Proposed or assert implementation maturity. -See the [ADR index](adr/README.md) for status rules, required decision structure, supersession rules, and active feature ADRs. The index and each ADR's own status metadata must agree; a PR body, chat transcript, automation prompt, or stale issue reference cannot change ADR status. +See the [ADR index](adr/README.md) for status rules, required decision structure, supersession rules, and active feature ADRs. The index and each ADR's own status metadata must agree; a PR body, chat transcript, automation prompt, or stale issue reference cannot change ADR status. \ No newline at end of file From b0aee66afb64316c6915ae696cd6079e7b102342 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:05:01 +0900 Subject: [PATCH 170/170] fix(release): make SPDX container bound reachable Signed-off-by: Seongho Bae --- CHANGELOG.md | 2 ++ docs/adr/0018-release-sbom-identity.md | 6 +++--- docs/doctoring.md | 4 ++-- docs/traceability/release-sbom-owner-migration.md | 2 +- scripts/release/validate_spdx_jsonld.py | 2 +- tests/test_release_spdx_jsonld_envelope_contract.py | 10 +++++----- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8fdbc53..7c3c2733d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ## [Unreleased] +- Tightened the provisional SPDX JSON-LD container budget to a reachable pre-materialization bound and changed its regression input to shallow fan-out, so container exhaustion is distinguished from nesting-depth and structure-token failures. + - Refreshed the product-gap queue to 126 open pull requests (54 ready, 72 draft) after #190, #188, #185, #192, #182, #184, #115, #181, #116, #117, #118, #183, #114, #127, #112, #109, #186, #110, #108, #111, #174, and #113 were merged into their immediate stacked prerequisites. PRs #147, #146, #145, #144, #143, #142, #141, #139, #136, #132, #129, and #128 moved to ready after exact-head checks and thread review; these are queue-consolidation results, not protected-main shipment. ### Added diff --git a/docs/adr/0018-release-sbom-identity.md b/docs/adr/0018-release-sbom-identity.md index 7d434f8a0..54825e628 100644 --- a/docs/adr/0018-release-sbom-identity.md +++ b/docs/adr/0018-release-sbom-identity.md @@ -45,8 +45,8 @@ This slice implements claims 1 through 3 only. Claim 4 remains a later independe - `ReleaseSbomBinding` can reference only exact, case-sensitive artifact names already present in one `ReleaseManifest`. - The SBOM artifact is represented by the existing manifest-backed `ReleaseArtifact`, including its lowercase `sha256:` identity, and cannot also appear in the binding's described-artifact set. - Every other artifact admitted by that release manifest must be retained in the described-artifact set exactly once. This is an OriginWeave release-inventory completeness rule, not a general SPDX modeling assertion. -- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, performs a string-aware lexical preflight before `json.loads`, caps opening JSON containers at 524,288 and total structural `[]{}:,` tokens at 1,048,576, requires exactly the top-level `@context` and `@graph` members, requires `@context` to equal the exact SPDX 3.0.1 global-context string, requires exactly one top-level `SpdxDocument`, and rejects any additional nested `SpdxDocument`. Context arrays and inline/remote context extensions fail closed at this stage. -- The lexical preflight counts structural punctuation only outside JSON strings. It is a resource-admission check, not a substitute JSON grammar; `json.loads` remains responsible for syntax after the preflight. The 524,288-container and 1,048,576-structure-token thresholds are provisional OriginWeave resource-policy ceilings rather than SPDX semantic limits. +- `scripts/release/validate_spdx_jsonld.py` consumes actual bytes for a narrow envelope check. It accepts at most 16 MiB, requires strict UTF-8 JSON, performs a string-aware lexical preflight before `json.loads`, caps opening JSON containers at 262,144 and total structural `[]{}:,` tokens at 1,048,576, requires exactly the top-level `@context` and `@graph` members, requires `@context` to equal the exact SPDX 3.0.1 global-context string, requires exactly one top-level `SpdxDocument`, and rejects any additional nested `SpdxDocument`. Context arrays and inline/remote context extensions fail closed at this stage. +- The lexical preflight counts structural punctuation only outside JSON strings. It is a resource-admission check, not a substitute JSON grammar; `json.loads` remains responsible for syntax after the preflight. The 262,144-container and 1,048,576-structure-token thresholds are provisional OriginWeave resource-policy ceilings rather than SPDX semantic limits. The container ceiling is deliberately reachable before the broader token ceiling for shallow fan-out. - Release admission that composes those bytes with `ReleaseSbomBinding` uses `validate_release_spdx_3_0_1_jsonld_bytes` and supplies the exact canonical lowercase `sha256:` identity from `ReleaseSbomBinding::sbom_artifact()`. The helper recomputes SHA-256 from the bounded candidate bytes and rejects malformed expected identities or mismatches before promoting envelope evidence. - Digest equality proves correspondence to the declared manifest artifact identity only. It does not authenticate who produced the manifest or bytes and does not replace provenance, signature, transparency, reproducibility, or release-approval checks. - The exact-string context choice is deliberately narrower than the SPDX serialization prose because the official 3.0.1 JSON Schema itself uses that exact-string constraint and because this preliminary verifier does not implement complete JSON-LD context semantics or collision detection against the pinned SPDX context. @@ -189,4 +189,4 @@ SPDX Workgroup. (2026). *SPDX specification 3.0.1: Model and serializations*. Th SPDX Workgroup. (2026). *SPDX 3.0.1 JSON Schema*. The Linux Foundation. https://spdx.org/schema/3.0.1/spdx-json-schema.json -World Wide Web Consortium. (2020, July 16). *JSON-LD 1.1: A JSON-based serialization for linked data*. https://www.w3.org/TR/json-ld11/ \ No newline at end of file +World Wide Web Consortium. (2020, July 16). *JSON-LD 1.1: A JSON-based serialization for linked data*. https://www.w3.org/TR/json-ld11/ diff --git a/docs/doctoring.md b/docs/doctoring.md index bc5ec81c3..94e29d2bd 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -90,7 +90,7 @@ Microsoft's Win32 filename guidance requires applications not to assume case sen SPDX 3.0.1 defines JSON-LD as an RDF serialization for SPDX data and requires conformant JSON-LD to pass both the versioned JSON Schema and the SPDX ontology/SHACL constraints. Its serialization prose requires the global context `https://spdx.org/rdf/3.0.1/spdx-context.jsonld`, defines `spdxId` and `type` as aliases for JSON-LD `@id` and `@type`, and requires Element nodes—including `SpdxDocument`—to be represented as top-level objects under `@graph`. It also says additional namespace mappings may be defined in a separate context object. The official SPDX 3.0.1 JSON Schema simultaneously constrains top-level `@context` to the exact global-context string. Because an arbitrary inline JSON-LD context object can contain semantic controls such as `@import`, `@base`, or `@vocab`, or redefine SPDX aliases, OriginWeave does not approximate “namespace mapping” by accepting every object. The preliminary envelope gate therefore requires exactly one top-level `SpdxDocument`, rejects a nested-only document as well as any additional nested document, follows the official schema's exact-string context identity, and rejects context composition until a reviewed schema-aware validator can reconcile the prose/schema tension without granting ambient or redefinition authority. This gate deliberately does not claim full content-level conformance: it binds a declared SPDX 3.0.1 JSON-LD SBOM artifact identity and exact release-artifact names to one immutable `ReleaseManifest`, while generation, full structural/semantic validation, package/component completeness, signatures, provenance, publication, installation, update, rollback, and release authority remain separate reviewed boundaries. -RFC 8259 §9 permits implementations to set limits on accepted JSON size and nesting depth. OriginWeave applies that implementation latitude narrowly to hostile release evidence. A 16 MiB byte ceiling and 65,536 top-level graph-object ceiling did not prevent compact valid documents from forcing a large generic JSON tree: one reproduction exceeded 524,288 nested containers, and another exceeded 1,048,576 scalar array members while using only a few containers. The provisional Python envelope gate now scans structural punctuation outside JSON strings before `json.loads`, caps opening containers at 524,288 and total `[]{}:,` structure tokens at 1,048,576, and leaves grammar validation to the JSON parser. These are OriginWeave resource-policy ceilings rather than SPDX semantic limits. The final Rust-owned Release boundary must reproduce both amplification cases and enforce equivalent or stricter pre-materialization byte/container/member/graph/numeric bounds through a bounded or streaming parse path; Python remains migration evidence rather than canonical release/security authority. +RFC 8259 §9 permits implementations to set limits on accepted JSON size and nesting depth. OriginWeave applies that implementation latitude narrowly to hostile release evidence. A 16 MiB byte ceiling and 65,536 top-level graph-object ceiling did not prevent compact valid documents from forcing a large generic JSON tree: one reproduction exceeded 262,144 shallow containers, and another exceeded 1,048,576 scalar array members while using only a few containers. The provisional Python envelope gate now scans structural punctuation outside JSON strings before `json.loads`, caps opening containers at 262,144 and total `[]{}:,` structure tokens at 1,048,576, and leaves grammar validation to the JSON parser. These are OriginWeave resource-policy ceilings rather than SPDX semantic limits. The final Rust-owned Release boundary must reproduce both amplification cases and enforce equivalent or stricter pre-materialization byte/container/member/graph/numeric bounds through a bounded or streaming parse path; Python remains migration evidence rather than canonical release/security authority. ### Git source identity admission @@ -226,4 +226,4 @@ World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 -Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 \ No newline at end of file +Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 diff --git a/docs/traceability/release-sbom-owner-migration.md b/docs/traceability/release-sbom-owner-migration.md index 0d5028a73..b5f99c8e6 100644 --- a/docs/traceability/release-sbom-owner-migration.md +++ b/docs/traceability/release-sbom-owner-migration.md @@ -38,7 +38,7 @@ There is **no compatibility shim** that makes core depend on Release, and there The Rust-owned replacement must preserve or strengthen the currently reviewed fail-closed behavior: - maximum 16 MiB candidate input before unbounded parsing/allocation; -- a pre-materialization opening-container budget equivalent to or stricter than the provisional 524,288-container ceiling; +- a pre-materialization opening-container budget equivalent to or stricter than the provisional 262,144-container ceiling; - a broader pre-materialization JSON structure-token budget equivalent to or stricter than the provisional 1,048,576-token ceiling so scalar/member fan-out is bounded as well as container count; - bracket/brace/comma/colon text inside JSON strings must not consume structural budgets, while malformed JSON still fails in the grammar parser; - strict UTF-8 and JSON admission; diff --git a/scripts/release/validate_spdx_jsonld.py b/scripts/release/validate_spdx_jsonld.py index 3904909c0..e45129aee 100755 --- a/scripts/release/validate_spdx_jsonld.py +++ b/scripts/release/validate_spdx_jsonld.py @@ -30,7 +30,7 @@ SPDX_3_0_1_CONTEXT = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" MAX_SPDX_JSONLD_BYTES = 16 * 1024 * 1024 MAX_SPDX_GRAPH_OBJECTS = 65_536 -MAX_SPDX_JSON_CONTAINERS = 524_288 +MAX_SPDX_JSON_CONTAINERS = 262_144 MAX_SPDX_JSON_STRUCTURE_TOKENS = 1_048_576 MAX_SPDX_JSON_NESTING_DEPTH = 256 MAX_SPDX_JSON_NUMBER_TOKEN_BYTES = 4096 diff --git a/tests/test_release_spdx_jsonld_envelope_contract.py b/tests/test_release_spdx_jsonld_envelope_contract.py index 6ade1d015..fe9d0f2a5 100644 --- a/tests/test_release_spdx_jsonld_envelope_contract.py +++ b/tests/test_release_spdx_jsonld_envelope_contract.py @@ -129,15 +129,15 @@ def test_graph_must_be_bounded_object_array(self) -> None: oversized_graph = [{"type": "Package"}] * (self.max_graph_objects + 1) self._assert_error_code(self._payload(oversized_graph), "too_many_graph_objects") - def test_nested_container_explosion_fails_before_json_materialization(self) -> None: - nested_container_count = self.max_containers + 1 + def test_shallow_container_explosion_fails_before_json_materialization(self) -> None: + shallow_container_count = self.max_containers + 1 payload = ( '{"@context":"' + CONTEXT + '","@graph":[{"type":"SpdxDocument","nested":' - + "[" * nested_container_count - + "0" - + "]" * nested_container_count + + "[" + + "[]," * shallow_container_count + + "[]]" + "}]}" ).encode("utf-8")