-
Notifications
You must be signed in to change notification settings - Fork 0
feat(core): bind release artifacts to canonical manifest identity #219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
seonghobae
wants to merge
53
commits into
main
Choose a base branch
from
feat/release-manifest-artifact-contract
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+904
−5
Open
Changes from all commits
Commits
Show all changes
53 commits
Select commit
Hold shift + click to select a range
0465a32
test(core): define release manifest artifact admission contract
seonghobae 296add6
test(core): format release manifest contract before semantic red
seonghobae b300d19
feat(core): add fail-closed release manifest identity primitive
seonghobae 4077ece
feat(core): expose release manifest identity contract
seonghobae 40a7a74
style(core): apply canonical release manifest formatting
seonghobae 3d13e54
test(core): close exact release manifest coverage branches
seonghobae 28d4d48
docs(changelog): record release manifest identity primitive
seonghobae 8444244
test(core): reject case-colliding release artifacts
seonghobae c9ce1f7
fix(core): reject case-colliding release artifacts
seonghobae 279d136
docs(changelog): record cross-platform artifact collision guard
seonghobae a5ef8ae
test(core): reject Windows device artifact names
seonghobae 4c159cb
fix(core): reject Windows device artifact names
seonghobae 19ee0a8
style(core): apply canonical release-manifest formatting
seonghobae f51c964
test(core): cover ordinary four-byte artifact basenames
seonghobae d3ce942
docs(adr): record release manifest identity boundary
seonghobae 222e622
docs: index proposed release manifest ADR
seonghobae 5f9fe3b
docs: index active release manifest ADR
seonghobae c02d46a
docs: trace release filename portability evidence
seonghobae 3d18700
test(core): require release build identity evidence
seonghobae cf3a4d3
test(core): canonicalize release build identity regression
seonghobae 78d31dc
feat(core): bind release build identity evidence
seonghobae 8e75f34
style(core): canonicalize release build identity formatting
seonghobae 520142a
docs(adr): bind release build identity evidence
seonghobae a902fb4
docs(changelog): record release build identity binding
seonghobae f2eef7e
test(core): satisfy strict release manifest Clippy contract
seonghobae 5fadaf9
test(release): reject unpinned Rust toolchains
seonghobae 3ac70fd
fix(release): bind exact pinned Rust toolchain
seonghobae 11d7e3f
docs(adr): bind release identity to pinned Rust baseline
seonghobae c3cb0fc
docs(changelog): record exact release toolchain binding
seonghobae 3e9bea4
test(release): require exact toolchain error contract
seonghobae e264711
test(release): decouple exact pin from obsolete token bound
seonghobae db06d75
fix(release): make pinned toolchain contract explicit
seonghobae 4de5adc
test(release): reject null Git source identity
seonghobae 813cc53
fix(release): reject null Git source identity
seonghobae 971b308
docs(release): reject Git zero-id as source identity
seonghobae 58ed003
docs(release): record non-null Git identity rationale
seonghobae bf2abb3
test(core): pin non-null release source error contract
seonghobae 7de7b8e
fix(core): align release source diagnostic with non-null identity
seonghobae 17903a6
docs(release): pin Git protocol references
seonghobae 58e7271
docs(release): pin doctoring Git references
seonghobae 21c48c8
test(release): reject Win32 COM0 and LPT0 aliases
seonghobae 7af2b25
fix(release): reject Win32 COM0 and LPT0 device aliases
seonghobae dea6277
docs(adr): record portable COM0 and LPT0 exclusion
seonghobae e0d7d5b
fix(release): satisfy strict ASCII digit lint
seonghobae 1f8729f
docs(doctoring): record portable COM0 and LPT0 exclusions
seonghobae a379a63
docs(changelog): record portable release device aliases
seonghobae 9c7bc54
test(release): pin manifest documentation truth
seonghobae 9255078
test(release): align Git citation revision contracts
seonghobae 47fcb74
docs(release): narrow manifest portability claims
seonghobae 2b0edc2
docs(release): align portability and Git evidence
seonghobae d438a4a
test(release): cover changelog portability truth
seonghobae a632575
docs(release): bound changelog portability claim
seonghobae 94d5e1f
docs(doctoring): merge portable device-exclusion citations
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,361 @@ | ||
| //! 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. | ||
|
|
||
| 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, | ||
| } | ||
|
|
||
| /// 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 build identity from the exact repository-pinned Rust toolchain and lock digest. | ||
| /// | ||
| /// 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, | ||
| ) -> Result<Self, ReleaseBuildIdentityError> { | ||
| 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 repository-pinned 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 does not match the exact repository-pinned baseline. | ||
| 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 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", | ||
| ), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Error for ReleaseBuildIdentityError {} | ||
|
|
||
| /// 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 or trailing punctuation, or Windows reserved device | ||
| /// basenames (including those basenames followed by extensions). | ||
| pub fn new(name: &str, sha256_digest: &str) -> Result<Self, ReleaseArtifactError> { | ||
| 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, | ||
| build_identity: ReleaseBuildIdentity, | ||
| artifacts: Vec<ReleaseArtifact>, | ||
| } | ||
|
|
||
| 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, 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<I>( | ||
| source_commit: &str, | ||
| chromium_revision: &str, | ||
| channel: ReleaseChannel, | ||
| build_identity: ReleaseBuildIdentity, | ||
| artifacts: I, | ||
| ) -> Result<Self, ReleaseManifestError> | ||
| where | ||
| I: IntoIterator<Item = ReleaseArtifact>, | ||
| { | ||
| 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); | ||
|
seonghobae marked this conversation as resolved.
|
||
| } | ||
| if !artifact_names.insert(artifact.name.to_ascii_lowercase()) { | ||
| return Err(ReleaseManifestError::DuplicateArtifactName); | ||
| } | ||
| admitted.push(artifact); | ||
| } | ||
|
seonghobae marked this conversation as resolved.
|
||
| if admitted.is_empty() { | ||
| return Err(ReleaseManifestError::MissingArtifacts); | ||
| } | ||
| admitted.sort_by(|left, right| left.name.cmp(&right.name)); | ||
|
seonghobae marked this conversation as resolved.
|
||
|
|
||
| Ok(Self { | ||
| source_commit: source_commit.to_owned(), | ||
| chromium_revision: chromium_revision.to_owned(), | ||
| channel, | ||
| build_identity, | ||
| 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 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] { | ||
| &self.artifacts | ||
| } | ||
| } | ||
|
|
||
| /// Validation error for release-manifest identity evidence. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub enum ReleaseManifestError { | ||
| /// 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, | ||
| /// No release artifacts were supplied. | ||
| MissingArtifacts, | ||
| /// Artifact inventory exceeds the bounded release-manifest limit. | ||
| TooManyArtifacts, | ||
| /// Artifact inventory repeats an ASCII-case-folded 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 a non-null 40-digit lowercase Git object identity", | ||
| ), | ||
| 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("..") | ||
| || windows_reserved_device_basename(name) | ||
| { | ||
| 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'-')) | ||
| } | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
|
|
||
| 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")) | ||
| && bytes[3].is_ascii_digit() | ||
|
seonghobae marked this conversation as resolved.
seonghobae marked this conversation as resolved.
|
||
| } | ||
|
seonghobae marked this conversation as resolved.
seonghobae marked this conversation as resolved.
|
||
|
|
||
| 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')) | ||
| && source_commit.bytes().any(|byte| byte != b'0') | ||
| } | ||
|
seonghobae marked this conversation as resolved.
|
||
|
|
||
| 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'@') | ||
| }) | ||
| } | ||
|
|
||
| fn valid_toolchain(toolchain: &str) -> bool { | ||
| toolchain == "1.97.1" | ||
| } | ||
|
seonghobae marked this conversation as resolved.
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.