Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 9 additions & 1 deletion .github/workflows/self-hosted-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ jobs:
export SKILLWIRE_PUBLISHED_AT
SKILLWIRE_PUBLISHED_AT="$(git show -s --format=%cI "${GITHUB_SHA}")"
export SKILLWIRE_TRUST_SEQUENCE=1
pnpm exec tsx scripts/build-self-hosted-release.ts \
pnpm build:self-hosted \
"${RUNNER_TEMP}/payload-${ARCH}" \
"${RUNNER_TEMP}/release-${ARCH}" \
"${ARCH}"
Expand Down Expand Up @@ -448,6 +448,14 @@ jobs:
run: |
set -euo pipefail
base="${RUNNER_TEMP}/signed-assets/skillwire-${VERSION}-linux-${MATRIX_ARCH}"
pnpm verify:self-hosted \
--manifest "${base}.release.json" \
--bundle "${base}.release.sigstore.json" \
--archive "${base}.tar.zst" \
--policy "${RUNNER_TEMP}/signed-assets/skillwire-trust-policy-v1.json" \
--trusted-root "${PWD}/distribution/self-hosted/trusted-root.v1.json" \
--cosign "${RUNNER_TEMP}/cosign-independent" \
--architecture "${MATRIX_ARCH}"
result="$(pnpm exec tsx scripts/validate-self-hosted-quickstart.ts \
--manifest "${base}.release.json" \
--bundle "${base}.release.sigstore.json" \
Expand Down
2 changes: 1 addition & 1 deletion distribution/self-hosted/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Run the release verifier from a trusted source checkout, still with outbound
networking blocked:

```sh
pnpm exec tsx scripts/verify-self-hosted-release.ts \
pnpm verify:self-hosted \
--manifest skillwire-VERSION-linux-ARCH.release.json \
--bundle skillwire-VERSION-linux-ARCH.release.sigstore.json \
--archive skillwire-VERSION-linux-ARCH.tar.zst \
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"packageManager": "pnpm@11.21.0",
"scripts": {
"build": "tsc -p tsconfig.json",
"build:self-hosted": "tsx scripts/build-self-hosted-release.ts",
"verify:self-hosted": "tsx scripts/verify-self-hosted-release.ts",
"auth:admin": "tsx src/authentication/admin-cli.ts",
"advisory:verify": "tsx src/catalog/advisory-verify-cli.ts",
"catalog:publish": "tsx src/catalog/publish-cli.ts",
Expand Down
11 changes: 8 additions & 3 deletions scripts/build-self-hosted-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export interface BuiltRelease {
readonly manifest: ReleaseManifest;
}

const BUILD_USAGE =
"Usage: build-self-hosted-release <payload-root> <output-directory> <amd64|arm64>";

function sha256(bytes: Uint8Array): string {
return createHash("sha256").update(bytes).digest("hex");
}
Expand Down Expand Up @@ -296,6 +299,10 @@ export async function buildSelfHostedRelease(
}

async function main(): Promise<void> {
if (process.argv.length === 3 && process.argv[2] === "--help") {
process.stdout.write(`${BUILD_USAGE}\n`);
return;
}
const payloadRoot = process.argv[2];
const outputDirectory = process.argv[3];
const architecture = process.argv[4];
Expand All @@ -304,9 +311,7 @@ async function main(): Promise<void> {
outputDirectory === undefined ||
(architecture !== "amd64" && architecture !== "arm64")
) {
throw new Error(
"Usage: build-self-hosted-release <payload-root> <output-directory> <amd64|arm64>",
);
throw new Error(BUILD_USAGE);
}
const imagesJson = process.env["SKILLWIRE_RELEASE_IMAGES_JSON"];
if (imagesJson === undefined) {
Expand Down
10 changes: 7 additions & 3 deletions scripts/verify-self-hosted-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,12 @@ export async function verifySelfHostedReleasePolicy(
}

export async function verifyCandidateFromCommandLine(): Promise<void> {
const usage =
"Usage: verify-self-hosted-release --manifest PATH --bundle PATH --archive PATH --policy PATH --trusted-root PATH --cosign PATH --architecture amd64|arm64";
if (process.argv.length === 3 && process.argv[2] === "--help") {
process.stdout.write(`${usage}\n`);
return;
}
const manifestPath = argument("--manifest");
const bundlePaths = argumentsFor("--bundle");
const bundlePath = bundlePaths[0];
Expand All @@ -413,9 +419,7 @@ export async function verifyCandidateFromCommandLine(): Promise<void> {
cosign === undefined ||
(architecture !== "amd64" && architecture !== "arm64")
) {
throw new Error(
"Usage: verify-self-hosted-release --manifest PATH --bundle PATH --archive PATH --policy PATH --trusted-root PATH --cosign PATH --architecture amd64|arm64",
);
throw new Error(usage);
}
const verified = await verifySignedReleaseEnvelope({
manifestPath: resolve(manifestPath),
Expand Down
20 changes: 20 additions & 0 deletions specs/004-self-hosted-onboarding/contracts/release-and-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@ The bootstrap instructions in `distribution/self-hosted/README.md` require an in

Bootstrap runs `cosign verify-blob` with outbound network blocked, the local trusted root, external bundle, canonical release manifest, exact certificate identity/issuer, and the policy-required repository/workflow/tag/SHA claims. It then validates canonical encoding, policy/manifest sequences, archive size/digest, and deny lists before extraction. Once started, the CLI independently repeats the same release-pinned verification before any installation path, image, container, service secret, credential, client profile, or database mutation. Missing or stale trusted material is a blocking integrity result with bounded instructions for an explicit TUF refresh; verification never silently refreshes or performs an unbounded transparency lookup.

## Stable release entrypoints

The protected release workflow and trusted source checkout use these stable package commands:

```text
pnpm build:self-hosted <payload-root> <output-directory> <amd64|arm64>
pnpm verify:self-hosted \
--manifest <release-manifest> \
--bundle <sigstore-bundle> \
--archive <release-archive> \
--policy <trust-policy> \
--trusted-root <trusted-root> \
--cosign <policy-pinned-cosign> \
--architecture <amd64|arm64>
```

`build:self-hosted` consumes only explicit, immutable workflow inputs and deterministically emits the unsigned canonical archive and release manifest. It does not sign, publish, install, or mutate user or service state. Signing remains the separate protected-tag workflow step described below.

`verify:self-hosted` validates the complete published or extracted release contract: canonical manifest and policy bytes, archive identity and safe extraction, Bundle v0.3 evidence, signer claims, the local TrustedRoot, payload inventory, and release policy. It fails closed when any required asset or trust input is absent and performs offline verification without consulting mutable GitHub state. A locally built unsigned payload cannot satisfy this command or enter the production installation path.

## Signing and trust-policy contract

`.github/workflows/self-hosted-release.yml` is the only release signer. The protected-tag workflow accepts only `self-hosted-v<package.version>`, requires an annotated tag object, recursively peels it to the exact workflow SHA, proves the target is reachable from protected `main`, and checks manifest version/source identity before signing. It uses only command-scoped `safe.directory`, builds and completes all acceptance gates before signing, and grants only `contents: read` and `id-token: write`.
Expand Down
36 changes: 33 additions & 3 deletions specs/004-self-hosted-onboarding/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,43 @@ Expected: the compiled `skillwire` executable reaches every administrative comma

## 3. Build and verify the local release candidate

Build only the platform matching the current fixture:
Build only the platform matching the current fixture. The release workflow
first assembles `SW004_PAYLOAD_ROOT` with the exact application, bundled
runtime, verified Cosign binary, catalogs, migrations, distributions, and
integrations shown in `.github/workflows/self-hosted-release.yml`. The public
build entrypoint then deterministically creates the unsigned archive and
canonical external manifest; it does not sign or publish them:

```bash
pnpm build:self-hosted -- --platform linux-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
pnpm verify:self-hosted
export SW004_ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"
export SW004_RELEASE_OUTPUT="$SW004_ROOT/release-$SW004_ARCH"

env \
SKILLWIRE_RELEASE_IMAGES_JSON="$SW004_RELEASE_IMAGES_JSON" \
SKILLWIRE_RELEASE_VERSION="$(node -p "require('./package.json').version")" \
SKILLWIRE_RELEASE_SEQUENCE="$SW004_RELEASE_SEQUENCE" \
SKILLWIRE_PUBLISHED_AT="$SW004_PUBLISHED_AT" \
GITHUB_SHA="$SW004_SOURCE_COMMIT" \
Comment thread
Lucenx9 marked this conversation as resolved.
SKILLWIRE_TRUST_SEQUENCE=1 \
pnpm build:self-hosted \
"$SW004_PAYLOAD_ROOT" "$SW004_RELEASE_OUTPUT" "$SW004_ARCH"

pnpm verify:self-hosted \
--manifest "$SW004_RELEASE_OUTPUT/skillwire-0.2.0-linux-$SW004_ARCH.release.json" \
--bundle "$SW004_RELEASE_OUTPUT/skillwire-0.2.0-linux-$SW004_ARCH.release.sigstore.json" \
--archive "$SW004_RELEASE_OUTPUT/skillwire-0.2.0-linux-$SW004_ARCH.tar.zst" \
--policy "$SW004_RELEASE_OUTPUT/skillwire-trust-policy-v1.json" \
Comment thread
Lucenx9 marked this conversation as resolved.
Outdated
--trusted-root "$SW004_PAYLOAD_ROOT/distribution/self-hosted/trusted-root.v1.json" \
--cosign "$SW004_PAYLOAD_ROOT/tools/cosign" \
--architecture "$SW004_ARCH"
```

`SW004_RELEASE_IMAGES_JSON`, `SW004_RELEASE_SEQUENCE`,
`SW004_PUBLISHED_AT`, and `SW004_SOURCE_COMMIT` are explicit immutable inputs
from the protected release workflow. The verifier requires the external
Bundle v0.3 and policy-pinned TrustedRoot; an unsigned local build therefore
cannot pass production verification or become an installation input.

The release job emits exactly four sibling assets for that platform:

```text
Expand Down
Loading
Loading