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.

BinaryPurposeEntitlement
capsemCLI clientcom.apple.security.virtualization
capsem-serviceBackground daemoncom.apple.security.virtualization
capsem-processPer-VM processcom.apple.security.virtualization
capsem-mcpMCP servercom.apple.security.virtualization
capsem-gatewayHTTP gatewaycom.apple.security.virtualization
capsem-traySystem traycom.apple.security.virtualization
Capsem.appTauri desktop appcom.apple.security.virtualization
ContextSigningCommand
DevelopmentAd-hoc (--sign -)just build (automatic)
ReleaseDeveloper ID certificatecodesign --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
FieldValue
FormatSPDX 2.3 JSON
ScopeAll Rust crate dependencies
Published ascapsem-sbom.spdx.json in GitHub release
AttestationSBOM attested against the macOS .pkg artifact

VM base images publish an Operations Bill of Materials as CycloneDX JSON. CI generates it with cdxgen -t os against the exported Linux rootfs before EROFS cleanup, pins it in manifest.json, and publishes it with the profile assets.

FieldValue
FormatCycloneDX OBOM JSON
ScopeBase Linux VM image only
ExcludesUser session mutations, workspace writes, and post-boot state
Published as<arch>-obom.cdx.json with profile assets
IntegrityBLAKE3 hash stored in the materialized profile
Runtime APIGET /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 via actions/attest-build-provenance@v4:

ArtifactAttestation
.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
.pkgSBOM (SPDX 2.3)
<arch>-obom.cdx.jsonOBOM 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 verified via BLAKE3 hashes 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 hashes + 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, BLAKE3, HMAC metadata. 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>",
"hmac": "<hmac>",
"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>",
"hmac": "<hmac>",
"sbom_component": "SPDXRef-File-capsem"
}
],
"profiles": [
{
"id": "co-work",
"revision": "1.0.0-stable.20260702",
"sha256": "<sha256>",
"blake3": "<blake3>",
"hmac": "<hmac>"
}
]
}

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, HMACs, 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 HMAC verification of the release graph records.

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-origin.json, then postinstall runs capsem update --assets --manifest <URL> to hydrate the live installed manifest and assets. Status reports the installed manifest hash plus origin provenance. --manifest is URL-only so custom local manifests use file:// and hosted corporate channels use https:// or http://.

ControlImplementation
Rust toolchainStable, pinned via dtolnay/rust-toolchain@stable
Dependency auditcargo audit in CI test stage
npm auditpnpm audit in CI test stage
Docker base imagesResolved by the profile-derived Docker template rail
Compiler warningsTreated as errors (#[deny(warnings)] in all crates)
Auditable buildscargo-auditable embeds dependency info in binaries
Build context validationcapsem.builder.doctor.check_source_files() verifies completeness before release
Rootfs binary verificationRelease pipeline checks all required guest binaries exist in rootfs before packaging

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

BinaryPurpose
capsem-pty-agentPTY bridge and control channel
capsem-net-proxyHTTPS proxy bridge
capsem-mcp-serverGuest MCP relay
capsem-doctorIn-VM diagnostics
capsem-benchPerformance benchmarks
snapshotsSnapshot management