Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ real Linux CLI/runtime in an ephemeral container
| Windows 10/11 x64 + Docker Desktop (Linux containers) + PowerShell | **Supported** — this is the validated configuration |
| cmd.exe invocation of shims | Works for the common cases; less battle-tested than PowerShell |
| WSL2 | **Not yet supported.** The selected native-Linux frontend now has an explicit fail-closed runtime boundary; config/shim/state implementation and real Docker Desktop WSL qualification remain. See [docs/wsl.md](docs/wsl.md) |
| Windows 11 ARM64 | **CI/release-artifact qualified only, not supported yet.** Native tests/build/dispatch run on GitHub-hosted ARM64 hardware and the release workflow produces a reproducible ARM64 archive, but real Docker Desktop ARM64 E2E qualification remains |
| Windows 11 ARM64 | **CI/release-artifact/update-path qualified only, not supported yet.** Native tests/build/dispatch run on GitHub-hosted ARM64 hardware, the release workflow produces a reproducible ARM64 archive, and self-update selects and verifies that archive by `GOARCH`; real Docker Desktop ARM64 E2E qualification remains |
| Linux / macOS hosts | **Not supported.** The program is Go and cross-compiles, but shim installation, path mapping and doctor checks are Windows-specific |
| Windows containers | Not supported; images are Linux images |

Expand Down Expand Up @@ -878,18 +878,19 @@ a false failure; run `cb setup` to append the current default profiles.
### Self-update release selection

`cb self-update --check` is the first, read-only phase of transactional
self-update support. It compares a release-qualified Windows/amd64 build with
the latest stable release and reports the exact binary, archive, checksum and
provenance policy that later phases must verify. It does not download assets or
change any files, and development builds fail closed because their installed
version cannot be proved.
self-update support. It compares a release-qualified Windows/amd64 or
Windows/arm64 build with the latest stable release and reports the exact
artifact, archive, checksum and provenance policy that later phases must
verify. It does not download assets or change any files, and development builds
fail closed because their installed version cannot be proved.

Stable selection is the default. Use `--prerelease` to select the highest
canonical prerelease among the 30 most recent published releases, or
`--version vX.Y.Z` to inspect one exact published release; those two selectors
are mutually exclusive. Selecting a version older
than the running build is rejected unless `--allow-downgrade` is explicit. The
current slice supports only Windows/amd64, matching the release artifacts.
architecture comes only from Go's native `GOARCH`; any other platform fails
explicitly before network access.

### `cb self-test --json` report format

Expand Down Expand Up @@ -1077,3 +1078,10 @@ misinterpreted as a tool shim. Verify the exact executable or archive you
download. The ARM64 archive is release-provenance coverage, not a support
claim: full Windows ARM64 support still requires real Docker Desktop
qualification.

`cb self-update --check` selects the raw `cb.exe` on Windows amd64 and the
ARM64 archive on Windows arm64 directly from Go's native `GOARCH`; unsupported
architectures fail explicitly. The verifier authenticates the selected asset
before extracting the ARM64 `cb.exe`, accepts the legacy two-entry checksum
manifest for pre-ARM64 amd64 releases, and requires the canonical three-entry
manifest for dual-architecture releases.
8 changes: 7 additions & 1 deletion docs/release-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,19 @@ ARM64, runs the full unit suite, builds a release-style `cb.exe`, and dispatches
a copied `jq.exe` shim to a controlled, compiled `docker.exe` stub. That proves native
management execution, argv[0] shim dispatch and tool exit-code propagation on
ARM64 hardware. It still has no Docker Desktop engine, so it does not qualify
bind mounts, volumes, providers or self-update. The release workflow separately
bind mounts, volumes or providers. The release workflow separately
cross-builds an architecture-specific ARM64 executable and archive, reproduces
both byte-for-byte on an
independent runner, checksums them and includes them in release provenance.
That supply-chain coverage is not a Windows ARM64 support claim. Support remains
gated on real Windows ARM64 + Docker Desktop E2E evidence.

The self-update pipeline selects the ARM64 archive from native `GOARCH`,
verifies its canonical three-entry checksum manifest and GitHub provenance,
then extracts only the exact `cb.exe` entry inside the private staging
directory. This qualifies architecture selection and artifact handling, not
Docker-backed runtime behavior or the final user-facing apply/helper flow.

So CI validates compilation and pure/unit logic on a GitHub-hosted Windows
runner. The matrix is what validates the `docs/shell-contract.md` semantics on a
real Windows 11 + Docker Desktop host before a release.
Expand Down
10 changes: 7 additions & 3 deletions docs/roadmap-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,14 @@ known folder needed for GitHub CLI's signed-root cache, noninteractive settings,
and exactly one explicit `GH_TOKEN` or `GITHUB_TOKEN`. These host paths come
from Windows APIs rather than inherited variables. GitHub host, config-directory,
proxy, custom-CA and other inherited settings are not passed through. It
requires the canonical two-entry `SHA256SUMS` layout, invokes
accepts the legacy two-entry `SHA256SUMS` layout for pre-ARM64 amd64 releases
and requires the canonical three-entry layout for dual-architecture releases,
invokes
`gh attestation verify` with the repository, exact workflow-and-tag certificate
identity, tag ref and SLSA provenance predicate fixed in argv, validates the
reported subject digest and re-hashes `cb.exe` after verification. Authenticode
reported subject digest and re-hashes the selected artifact after verification.
For ARM64, it then extracts and hashes only the exact `cb.exe` entry.
Authenticode
checks run from the Windows directory with bounded, cancelable subprocesses.
Its opaque result binds the exact digest for the later replacement phase; any
missing verifier, policy mismatch, malformed output or file change fails closed
Expand Down Expand Up @@ -327,7 +331,7 @@ PR #86. Unmerged pull-request coverage is not completion.
- **lowest priority**;
- native hosted ARM64 CI is merged in PR #78;
- architecture-specific release packaging is merged in PR #86;
- ARM64 self-update selection remains;
- self-update selects and verifies the ARM64 archive from native `GOARCH`;
- support claim only after real Windows-on-Arm + Docker Desktop E2E.

## Dormant / recurring items
Expand Down
28 changes: 16 additions & 12 deletions docs/roadmap-implementation-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ The minimum delivery gate for a code change is:
| RM-26 Python global CLI exposure | **Completed in PR #74** | Stateful pipx + `cb expose pipx` shipped; plain pip `/venv/bin` remains intentionally unexposed |
| RM-29 Windows ARM64 | **Native CI and release packaging shipped / update and hardware work remain** | PR #78 added native hosted ARM64 CI and PR #86 added reproducible release packaging; ARM64 self-update selection and real Windows-on-Arm + Docker Desktop E2E remain |
| RM-30 Authenticode | **Design complete / externally blocked** | Provision real code-signing certificate and protected signing mechanism |
| RM-31 self-update | **Selection, staging and verification shipped** | PRs #76, #81 and #82 shipped the read-only plan, fail-closed staging and provenance verification; transactional apply and E2E remain |
| RM-31 self-update | **Selection, staging and verification shipped / ARM64 selection implemented** | PRs #76, #81 and #82 shipped the read-only plan, fail-closed staging and provenance verification; this tree adds ARM64 artifact selection, while transactional apply wiring and E2E remain |
| RM-34 Cargo expose enhancement | **Intentionally deferred** | Existing expose-all/explicit selection are sufficient; reopen only for concrete unmet use case |
| Linux/macOS hosts | **Demand-gated** | WSL may factor reusable Linux host code; standalone support needs its own demand and qualification |
| Enterprise policy | **Foundation and signed registry shipped / image trust remains** | PRs #75 and #84 shipped the machine-owned constraint layer and authenticated registry; image trust remains |
Expand Down Expand Up @@ -316,12 +316,14 @@ Desktop in Linux-container mode.
PR #78 shipped native hosted ARM64 CI for non-Docker qualification. PR #86
shipped the architecture-specific ARM64 archive, checksum coverage, independent
byte-for-byte reproduction and release provenance while preserving existing
amd64 asset names. PR #76's self-update foundation still deliberately rejects
architectures other than Windows/amd64. ARM64 install/update selection and real
Windows-on-Arm + Docker Desktop qualification therefore remain; CI and a
provenanced archive alone are not a full support claim. The first three bullets
below are the shipped PR #86 contract. Install/update selection and the
hardware-backed qualification record remain.
amd64 asset names. PR #76's self-update foundation originally rejected
architectures other than Windows/amd64. The update pipeline now selects the
ARM64 archive from native `GOARCH`, verifies its checksum and provenance before
exact extraction, and preserves legacy amd64 release compatibility. Real
Windows-on-Arm + Docker Desktop qualification remains; CI, update selection and
a provenanced archive are not a full support claim. The first three bullets
below are the shipped PR #86 contract. The hardware-backed qualification record
remains.
Comment thread
AviBackToBlack marked this conversation as resolved.

### Shipped release contract and remaining implementation

Expand Down Expand Up @@ -396,10 +398,12 @@ separate phases with explicit boundaries. GitHub documents both
and [artifact-attestation verification](https://docs.github.com/en/actions/concepts/security/artifact-attestations).

PR #76 shipped the command surface, release selection, check/dry-run behavior,
strict Windows/amd64 asset selection and bounded metadata rules. Unsupported
architectures still fail closed. Download staging, provenance verification,
transactional Windows apply, rollback, broader architecture support and release
E2E remain incomplete until their implementations merge.
strict Windows/amd64 asset selection and bounded metadata rules. Private
same-volume staging and provenance verification followed. The current pipeline
also selects, stages and verifies the Windows/arm64 archive from native
`GOARCH`; unsupported architectures still fail closed. Transactional helper
and user-facing apply wiring plus release E2E remain incomplete until their
implementations merge.

### Command and selection requirements

Expand Down Expand Up @@ -660,7 +664,7 @@ reproducible ARM64 release packaging in PR #86.
1. Per-project overlay trust foundation.
2. Signed-registry enterprise policy.
3. Image trust at lock time, after signed-registry policy merges.
4. Remaining RM-31 staging, verification, transactional apply and E2E.
4. Remaining RM-31 helper/user-facing transactional apply wiring and E2E.
5. Remaining WSL2 native layout, Docker Desktop integration and real E2E.
6. RM-30 Authenticode only after certificate/protected-signing prerequisites exist.
7. RM-29 ARM64 self-update selection and real Windows-on-Arm + Docker Desktop
Expand Down
9 changes: 6 additions & 3 deletions docs/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,20 @@ readable, and dangerous to let others edit.
network-disabled helper with a read-only container root; no archive member is
turned into a Windows host path.
- **Fail-closed self-update selection.** `cb self-update --check` accepts only a
release-qualified Windows/amd64 build, queries the canonical GitHub repository
release-qualified Windows/amd64 or Windows/arm64 build selected from native
`GOARCH`, queries the canonical GitHub repository
over HTTPS with a bounded response, and requires exact canonical release and
asset URLs, names and sizes. Downgrades and prereleases require explicit
flags. The command performs no asset download and changes no installed files.
A separate, not-yet-exposed staging phase downloads exact advertised bytes
for `cb.exe` and `SHA256SUMS` beside a supplied, existing installed
for the selected artifact and `SHA256SUMS` beside a supplied, existing installed
executable, accepting only the canonical URL or one HTTPS redirect to
GitHub's release-asset host. Staging applies a protected current-user-only
DACL on Windows and removes partial staging on any failure. Later phases must
require both checksums and GitHub provenance without a fallback before
replacement is possible.
replacement is possible. On ARM64, archive checksum and provenance are
verified before an exact three-file archive layout is parsed and `cb.exe` is
extracted; unexpected, duplicate or unsafe entries fail closed.

## What ContainerBin does NOT protect against

Expand Down
85 changes: 56 additions & 29 deletions internal/selfupdate/selfupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,16 @@ type Plan struct {
ExpectedRef string
Workflow string
downgradeAuthorization downgradeAuthorization
checksumLayout checksumLayout
}

type checksumLayout uint8

const (
checksumLayoutLegacyAMD64 checksumLayout = iota + 1
checksumLayoutDualArch
)

type downgradeAuthorization struct {
current string
target string
Expand Down Expand Up @@ -150,8 +158,8 @@ func (c checker) Plan(ctx context.Context, current, goos, goarch string, opts Op
if err != nil {
return Plan{}, err
}
if goos != "windows" || goarch != "amd64" {
return Plan{}, fmt.Errorf("self-update has no qualified artifact for %s/%s (supported: windows/amd64)", goos, goarch)
if goos != "windows" || (goarch != "amd64" && goarch != "arm64") {
return Plan{}, fmt.Errorf("self-update has no qualified artifact for %s/%s (supported: windows/amd64, windows/arm64)", goos, goarch)
}
selected, channel, err := c.selectRelease(ctx, current, opts)
if err != nil {
Expand All @@ -171,24 +179,25 @@ func (c checker) Plan(ctx context.Context, current, goos, goarch string, opts Op
case comparison > 0:
status = "DOWNGRADE AUTHORIZED (CHECK ONLY)"
}
binary, archive, checksums, err := validateRelease(selected, goos, goarch)
binary, archive, checksums, layout, err := validateRelease(selected, goarch)
if err != nil {
return Plan{}, err
}
plan := Plan{
Current: currentParsed.raw,
Target: target.raw,
Channel: channel,
Status: status,
OS: goos,
Arch: goarch,
ReleaseURL: selected.HTMLURL,
Binary: binary,
Archive: archive,
Checksums: checksums,
ExpectedRepo: "AviBackToBlack/container-bin",
ExpectedRef: "refs/tags/" + target.raw,
Workflow: ".github/workflows/release.yml",
Current: currentParsed.raw,
Target: target.raw,
Channel: channel,
Status: status,
OS: goos,
Arch: goarch,
ReleaseURL: selected.HTMLURL,
Binary: binary,
Archive: archive,
Checksums: checksums,
ExpectedRepo: "AviBackToBlack/container-bin",
ExpectedRef: "refs/tags/" + target.raw,
Workflow: ".github/workflows/release.yml",
checksumLayout: layout,
}
if comparison > 0 && opts.AllowDowngrade {
plan.downgradeAuthorization = downgradeAuthorization{current: currentParsed.raw, target: target.raw}
Expand Down Expand Up @@ -277,41 +286,57 @@ func (c checker) getJSON(ctx context.Context, endpoint, current string, dst any)
return nil
}

func validateRelease(selected release, goos, goarch string) (Asset, Asset, Asset, error) {
func validateRelease(selected release, goarch string) (Asset, Asset, Asset, checksumLayout, error) {
tag, err := parseVersion(selected.TagName)
if err != nil {
return Asset{}, Asset{}, Asset{}, err
return Asset{}, Asset{}, Asset{}, 0, err
}
wantReleaseURL := releaseWebRoot + "/tag/" + tag.raw
if selected.HTMLURL != wantReleaseURL {
return Asset{}, Asset{}, Asset{}, fmt.Errorf("release URL %q is outside the canonical release page", selected.HTMLURL)
return Asset{}, Asset{}, Asset{}, 0, fmt.Errorf("release URL %q is outside the canonical release page", selected.HTMLURL)
}
amd64Archive := fmt.Sprintf("container-bin-%s-windows-amd64.zip", tag.raw)
arm64Archive := fmt.Sprintf("container-bin-%s-windows-arm64.zip", tag.raw)
wanted := map[string]int64{
"cb.exe": maxBinarySize,
amd64Archive: maxArchiveSize,
arm64Archive: maxArchiveSize,
"SHA256SUMS": maxChecksumSize,
}
archiveName := fmt.Sprintf("container-bin-%s-%s-%s.zip", tag.raw, goos, goarch)
wanted := map[string]int64{"cb.exe": maxBinarySize, archiveName: maxArchiveSize, "SHA256SUMS": maxChecksumSize}
found := map[string]Asset{}
for _, candidate := range selected.Assets {
limit, required := wanted[candidate.Name]
if !required {
continue
}
if _, duplicate := found[candidate.Name]; duplicate {
return Asset{}, Asset{}, Asset{}, fmt.Errorf("release contains duplicate required asset %q", candidate.Name)
return Asset{}, Asset{}, Asset{}, 0, fmt.Errorf("release contains duplicate required asset %q", candidate.Name)
}
if candidate.Size <= 0 || candidate.Size > limit {
return Asset{}, Asset{}, Asset{}, fmt.Errorf("release asset %q has invalid size %d (limit %d)", candidate.Name, candidate.Size, limit)
return Asset{}, Asset{}, Asset{}, 0, fmt.Errorf("release asset %q has invalid size %d (limit %d)", candidate.Name, candidate.Size, limit)
}
wantURL := releaseWebRoot + "/download/" + tag.raw + "/" + candidate.Name
if candidate.BrowserDownloadURL != wantURL {
return Asset{}, Asset{}, Asset{}, fmt.Errorf("release asset %q has non-canonical download URL", candidate.Name)
return Asset{}, Asset{}, Asset{}, 0, fmt.Errorf("release asset %q has non-canonical download URL", candidate.Name)
}
found[candidate.Name] = Asset{Name: candidate.Name, URL: candidate.BrowserDownloadURL, Size: candidate.Size}
}
for name := range wanted {
for _, name := range []string{"cb.exe", amd64Archive, "SHA256SUMS"} {
if _, ok := found[name]; !ok {
return Asset{}, Asset{}, Asset{}, fmt.Errorf("release is missing required asset %q", name)
return Asset{}, Asset{}, Asset{}, 0, fmt.Errorf("release is missing required asset %q", name)
}
}
return found["cb.exe"], found[archiveName], found["SHA256SUMS"], nil
layout := checksumLayoutLegacyAMD64
if _, ok := found[arm64Archive]; ok {
layout = checksumLayoutDualArch
}
if goarch == "arm64" && layout != checksumLayoutDualArch {
return Asset{}, Asset{}, Asset{}, 0, fmt.Errorf("release is missing required asset %q", arm64Archive)
}
if goarch == "arm64" {
return found[arm64Archive], found[arm64Archive], found["SHA256SUMS"], layout, nil
}
return found["cb.exe"], found[amd64Archive], found["SHA256SUMS"], layout, nil
}

func printPlan(out io.Writer, plan Plan) {
Expand All @@ -322,8 +347,10 @@ func printPlan(out io.Writer, plan Plan) {
fmt.Fprintf(out, "status: %s\n", plan.Status)
fmt.Fprintf(out, "platform: %s/%s\n", plan.OS, plan.Arch)
fmt.Fprintf(out, "release: %s\n", plan.ReleaseURL)
fmt.Fprintf(out, "binary: %s (%d bytes)\n", plan.Binary.Name, plan.Binary.Size)
fmt.Fprintf(out, "archive: %s (%d bytes)\n", plan.Archive.Name, plan.Archive.Size)
fmt.Fprintf(out, "artifact: %s (%d bytes)\n", plan.Binary.Name, plan.Binary.Size)
if plan.Archive.Name != plan.Binary.Name {
fmt.Fprintf(out, "archive: %s (%d bytes)\n", plan.Archive.Name, plan.Archive.Size)
}
fmt.Fprintf(out, "checksums: %s (%d bytes)\n", plan.Checksums.Name, plan.Checksums.Size)
fmt.Fprintf(out, "verification: gh attestation verify; repository=%s workflow=%s ref=%s; checksums additionally required; no fallback\n", plan.ExpectedRepo, plan.Workflow, plan.ExpectedRef)
fmt.Fprintln(out, "apply: unavailable in this slice; verified download and transactional replacement are separate roadmap phases")
Expand Down
Loading
Loading