Skip to content

Build Verification

Capsem’s release pipeline signs, notarizes, attests, and hash-verifies every artifact from source to installed binary.

graph LR
    A["Source<br/>(tagged commit)"] --> B["Build<br/>(per-arch)"]
    B --> C["Test<br/>(audit + coverage)"]
    C --> D["Codesign<br/>(Developer ID)"]
    D --> E["Notarize<br/>(Apple)"]
    E --> F["SBOM<br/>(SPDX 2.3)"]
    F --> G["Attest<br/>(SLSA + SBOM)"]
    G --> H["Publish manifest<br/>(BLAKE3 metadata)"]
    H --> I["Publish<br/>(GitHub release)"]

Every step is automated in .github/workflows/release.yaml. A preflight job validates signing credentials before any build starts.

All host binaries are codesigned with a Developer ID certificate. The com.apple.security.virtualization entitlement is required for Apple Virtualization.framework.

Binary Purpose Entitlement
capsem CLI client com.apple.security.virtualization
capsem-service Background daemon com.apple.security.virtualization
capsem-process Per-VM process com.apple.security.virtualization
capsem-mcp MCP server com.apple.security.virtualization
capsem-gateway HTTP gateway com.apple.security.virtualization
capsem-tray System tray com.apple.security.virtualization
Capsem.app Tauri desktop app com.apple.security.virtualization
Context Signing Command
Development Ad-hoc (--sign -) just build (automatic)
Release Developer ID certificate codesign --sign "$APPLE_SIGNING_IDENTITY" --entitlements entitlements.plist --force

Ad-hoc signing is sufficient for local development. The justfile handles this automatically on macOS.

Release builds are submitted to Apple for notarization, which scans for malware and validates the signature:

xcrun notarytool submit Capsem-$VERSION.pkg \
--key $APPLE_API_KEY_PATH \
--key-id $APPLE_API_KEY \
--issuer $APPLE_API_ISSUER \
--wait --timeout 30m
xcrun stapler staple Capsem-$VERSION.pkg

Stapling embeds the notarization ticket in the artifact so macOS can verify it offline.

Host binaries publish a Software Bill of Materials using cargo-sbom:

cargo sbom --output-format spdx_json_2_3 > capsem-sbom.spdx.json
Field Value
Format SPDX 2.3 JSON
Scope All Rust crate dependencies
Published as capsem-sbom.spdx.json in GitHub release
Attestation SBOM attested against the macOS .pkg artifact

VM base images publish an Operations Bill of Materials as CycloneDX JSON. CI generates it with pinned cdxgen -t os against the extracted exported Linux rootfs directory (never the build host /) before EROFS cleanup, pins it in manifest.json, and publishes it with the profile assets. That exact invocation is qualified against the complete Capsem filesystem, not only a tiny fixture.

Field Value
Format CycloneDX OBOM JSON
Scope Base Linux VM image only
Excludes User session mutations, workspace writes, and post-boot state
Published as <arch>-obom.cdx.json with profile assets
Integrity BLAKE3 hash stored in the materialized profile
Runtime API GET /profiles/{profile_id}/info and GET /profiles/{profile_id}/obom

The profile OBOM descriptor records the OBOM file URL, BLAKE3 hash, size, generator, generator version, and the rootfs BLAKE3 hash it describes. Runtime routes expose the descriptor as profile evidence; local OBOM documents are served only after size and BLAKE3 verification.

The per-architecture build-ledger.log is separate debug evidence. It records the inputs that produced the rootfs, including rendered Dockerfiles, build context hashes, EROFS settings, git/project version, profile root and install-script inputs, and declared package config. It is not uploaded as the release inventory and must not claim installed package state; installed component names and versions come from the OBOM.

Release artifacts receive SLSA build provenance attestation through a reviewed, full-commit-SHA pin of actions/attest-build-provenance:

Artifact Attestation
.pkg (macOS installer) Build provenance
.deb (Linux package) Build provenance
vmlinuz, initrd.img, rootfs.erofs, obom.cdx.json (arm64) VM asset build provenance
vmlinuz, initrd.img, rootfs.erofs, obom.cdx.json (x86_64) VM asset build provenance
.pkg SBOM (SPDX 2.3)
<arch>-obom.cdx.json OBOM document, hash-pinned in manifest.json

Attestations are published to the GitHub Attestations API and can be verified with gh attestation verify. The VM build-ledger.log and B3SUMS outputs remain debug evidence unless a future release intentionally publishes them as separate evidence artifacts.

VM assets (kernel, initrd, rootfs) are recorded with BLAKE3 and SHA-256 at the asset build boundary and verified via BLAKE3 identity at every stage from build to boot. The checked-in profile is materialized into target/config/ before runtime, so the service boots from a generated profile whose asset URLs, hashes, and sizes come directly from assets/manifest.json. Published GitHub Release blob names are arch-prefixed, for example arm64-rootfs.erofs; inside the manifest they remain bare names such as rootfs.erofs under the owning architecture.

assets/manifest.json is generated through capsem-admin manifest generate <assets_dir>. Release automation, local packaging, and corp custom builds use that same admin command; lower-level manifest generation internals are not a supported public path.

graph TD
    A["Build assets<br/>capsem-admin manifest generate"] --> B["manifest.json<br/>(BLAKE3 + SHA-256 + sizes)"]
    B --> C["Release<br/>packages + arch-prefixed VM assets"]
    C --> D["Download<br/>profile/corp selected URL"]
    D --> E["Verify hashes<br/>BLAKE3 per-file check"]
    E --> F["Boot<br/>assets loaded from verified dir"]

The public update graph starts at https://release.capsem.org/channels.json. It lists channels such as stable and nightly. Each channel contains versioned manifest records, and every record has exactly one status enum value: current, supported, deprecated, or revoked. Manifest records carry version, URL, SHA-256, and BLAKE3 metadata. HMAC fields are not published. They remain present for auditability; absence from the channel list is removal.

The selected manifest is the compatibility and hash authority for one channel. It lists package artifacts separately from the per-binary inventory and points to profile catalogs:

{
"version": "1.4.0",
"channel": "stable",
"packages": [
{
"name": "Capsem-1.4.0.pkg",
"kind": "macos-pkg",
"url": "https://github.com/google/capsem/releases/download/v1.4.0/Capsem-1.4.0.pkg",
"sha256": "<sha256>",
"blake3": "<blake3>",
"bytes": 12345678,
"sbom": "https://github.com/google/capsem/releases/download/v1.4.0/capsem-sbom.spdx.json"
}
],
"binaries": [
{
"name": "capsem",
"version": "1.4.0",
"package": "Capsem-1.4.0.pkg",
"path": "/usr/local/bin/capsem",
"sha256": "<sha256>",
"blake3": "<blake3>",
"sbom_component": "SPDXRef-File-capsem"
}
],
"profiles": [
{
"id": "co-work",
"revision": "1.0.0-stable.20260702",
"sha256": "<sha256>",
"blake3": "<blake3>",
}
]
}

Profiles own profile images, config files, software inventory, and ABOM/OBOM evidence. A profile may declare min_capsem_version when its config or image requires newer client behavior, but it does not select the Capsem binary. The manifest selects package and binary metadata; profile entries inside that manifest select profile-owned image/config/evidence metadata.

Stable and nightly are independent channels. A stable-to-nightly switch is just choosing a different manifest URL, for example https://release.capsem.org/assets/stable/manifest.json or https://release.capsem.org/assets/nightly/manifest.json, and the release gate proves package, per-binary, profile image, config, and evidence data do not cross between channels.

BLAKE3 hashes are computed in 256 KB chunks:

pub fn hash_file(path: &Path) -> Result<String> {
let mut hasher = blake3::Hasher::new();
loop {
let n = file.read(&mut buf)?;
if n == 0 { break; }
hasher.update(&buf[..n]);
}
Ok(hasher.finalize().to_hex().to_string())
}

Validation rules:

  • Hash must be exactly 64 hex characters
  • Filenames must not contain /, \, or .. (path traversal prevention)
  • Version strings must not contain .., /, or \
  • Empty releases are rejected

Channels accumulate versioned manifest records across releases. Adding a new stable or nightly manifest does not require mutating profiles, packages, or other channels. Deprecating or revoking a manifest changes the record status; publishing no record at all means that manifest is removed from the public channel list. Runtime selection ignores revoked records.

manifest.json is channel metadata: package artifacts, per-binary inventory, profile catalog references, hashes, and compatibility. It is published with SBOM and provenance attestations. Runtime trust comes from the selected manifest URL, profile-owned file metadata, SHA-256/BLAKE3 verification of the downloaded bytes, and immutable release provenance. Channel assembly reuses the complete asset digests recorded at build time instead of reopening the same rootfs for each channel. Local blob copies hash and validate in their single copy stream; historical releases are never hydrated from current paths.

For a custom corp package, generate and verify the manifest from the built asset directory before packaging:

Terminal window
capsem-admin manifest generate /path/to/assets --version 1.3.corp.1 --json
capsem-admin manifest check /path/to/assets/manifest.json --json
bash scripts/build-pkg.sh --manifest file:///path/to/assets/manifest.json ...

The installer records that manifest URL in packaged manifest-metadata.json, then postinstall runs capsem update --assets --manifest <URL> to hydrate the live installed manifest and assets. Status reports the installed manifest hash plus metadata provenance. --manifest is URL-only so custom local manifests use file:// and hosted corporate channels use https:// or http://.

Control Implementation
Rust toolchain Rust 1.97.1, pinned consistently in the workspace, workflows, bootstrap, and host builder
Dependency audit Blocking cargo audit in local smoke/test, ordinary CI, both release lanes, and the scheduled/manual security workflow
npm audit Blocking npm bulk advisory API check in local smoke/test, ordinary CI, both release lanes, and the scheduled/manual security workflow
Docker base images Resolved by the profile-derived Docker template rail
Compiler warnings Treated as errors, with workspace dbg_macro and todo lints denied
Auditable builds cargo-auditable embeds dependency info in binaries
Build context validation capsem.builder.doctor.check_source_files() verifies completeness before release
Rootfs binary verification Release pipeline checks all required guest binaries exist in rootfs before packaging

The release pipeline verifies these binaries exist in the rootfs before packaging:

Binary Purpose
capsem-pty-agent PTY bridge and control channel
capsem-net-proxy HTTPS proxy bridge
capsem-mcp-server Guest MCP relay
capsem-doctor In-VM diagnostics
capsem-bench Performance benchmarks
snapshots Snapshot management