Skip to content
Closed
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
181 changes: 181 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
name: Binary attestations

on:
push:
tags: ['v*']
pull_request:
paths:
- '.github/workflows/release.yml'
- 'scripts/release/phase1*'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- '.tool-versions'
- '.cargo/**'
- 'edgezero.toml'
- 'crates/**'

permissions:
contents: read

concurrency:
group: binary-attestations-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
name: Build ${{ matrix.asset }}
runs-on: ${{ matrix.os }}
timeout-minutes: 90
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
package: trusted-server-adapter-fastly
target: wasm32-wasip1
asset: trusted-server-fastly.wasm
- os: ubuntu-24.04
package: trusted-server-cli
target: x86_64-unknown-linux-gnu
asset: ts-x86_64-unknown-linux-gnu
- os: macos-15
package: trusted-server-cli
target: aarch64-apple-darwin
asset: ts-aarch64-apple-darwin
- os: macos-15-intel
package: trusted-server-cli
target: x86_64-apple-darwin
asset: ts-x86_64-apple-darwin
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Read pinned toolchains
id: tools
shell: bash
run: |
set -euo pipefail
echo "node=$(awk '$1 == "nodejs" { print $2 }' .tool-versions)" >> "$GITHUB_OUTPUT"
echo "rust=$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: ${{ steps.tools.outputs.node }}
- name: Install the pinned Rust toolchain without build caches
env:
RUST_VERSION: ${{ steps.tools.outputs.rust }}
BUILD_TARGET: ${{ matrix.target }}
run: rustup toolchain install "$RUST_VERSION" --profile minimal --target "$BUILD_TARGET"
- name: Test local release checks
run: node --test scripts/release/phase1.test.mjs
- name: Build and generate the build inventory in a clean environment
shell: bash
env:
BUILD_PACKAGE: ${{ matrix.package }}
BUILD_TARGET: ${{ matrix.target }}
RELEASE_ASSET: ${{ matrix.asset }}
SOURCE_COMMIT: ${{ github.sha }}
run: |
set -euo pipefail
env -i PATH="$PATH" HOME="$HOME" \
CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}" \
RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}" \
TMPDIR="${TMPDIR:-/tmp}" RUNNER_TEMP="$RUNNER_TEMP" \
SOURCE_COMMIT="$SOURCE_COMMIT" \
bash scripts/release/phase1-build.sh "$BUILD_PACKAGE" "$BUILD_TARGET" "$RELEASE_ASSET"
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: release-${{ matrix.asset }}
path: release-assets/*
if-no-files-found: error
retention-days: 7

sign:
name: Attest ${{ matrix.asset }}
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
asset:
- trusted-server-fastly.wasm
- ts-x86_64-unknown-linux-gnu
- ts-aarch64-apple-darwin
- ts-x86_64-apple-darwin
steps:
# No checkout, build scripts or executables from the artifacts run here.
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: release-${{ matrix.asset }}
path: release-assets
- name: Attest SLSA build provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: release-assets/${{ matrix.asset }}
- name: Attest the binary's SBOM
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: release-assets/${{ matrix.asset }}
sbom-path: release-assets/${{ matrix.asset }}.cdx.json

release:
name: Publish the immutable release
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: sign
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: release-*
merge-multiple: true
path: release-assets
- name: Draft, upload, publish and verify immutability
shell: bash
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
# Treat all API errors except an explicit 404 as fatal.
if gh api "repos/$GH_REPO/releases/tags/$RELEASE_TAG" > existing.json 2> lookup-error.txt; then
echo '::error::A release already exists for this tag; refusing to overwrite it.'
exit 1
elif ! grep -q 'HTTP 404' lookup-error.txt; then
cat lookup-error.txt >&2
exit 1
fi
cd release-assets
expected=(trusted-server-fastly.wasm ts-x86_64-unknown-linux-gnu ts-aarch64-apple-darwin ts-x86_64-apple-darwin)
files=()
for asset in "${expected[@]}"; do
test -s "$asset"
test -s "$asset.cdx.json"
files+=("$asset" "$asset.cdx.json")
done
test "$(find . -mindepth 1 -maxdepth 1 -type f | wc -l)" -eq 8
sha256sum "${files[@]}" > SHA256SUMS
gh release create "$RELEASE_TAG" --repo "$GH_REPO" --draft --verify-tag \
--title "$RELEASE_TAG" --notes 'Attested release binaries. Verify provenance, SBOM and immutable asset membership before use; see the release verification guide at this tag.'
gh release upload "$RELEASE_TAG" "${files[@]}" SHA256SUMS --repo "$GH_REPO"
gh release edit "$RELEASE_TAG" --draft=false --repo "$GH_REPO"
immutable=$(gh api "repos/$GH_REPO/releases/tags/$RELEASE_TAG" --jq '.immutable')
if [ "$immutable" != true ]; then
echo '::error::Published release is NOT immutable. Maintainer action required; do not use these assets.'
exit 1
fi
gh release verify "$RELEASE_TAG" --repo "$GH_REPO"
for asset in "${files[@]}" SHA256SUMS; do
gh release verify-asset "$RELEASE_TAG" "$asset" --repo "$GH_REPO"
done
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added the Phase 1 binary-attestation workflow: tag-triggered Fastly wasm and Linux/macOS CLI builds, provenance and conservative Cargo/npm CycloneDX SBOM attestations, immutable release assets and operator verification/deployment instructions. Release enablement still requires maintainer settings and a sandbox signing/publication rehearsal.
- Added the `[auction].rewrite_creatives` (default `true`) and `[auction].sanitize_creatives` (default `false`) options. `rewrite_creatives` rewrites winning-bid adm to first-party endpoints across `POST /auction` and publisher SSAT/page-bids delivery (proxy/click URL conversion, bidder `<base>` removal; creative TSJS injection on `POST /auction` only). Enabling `sanitize_creatives` strips executable markup from winning-bid adm before delivery.
- `creative_opportunities.slot.gam_unit_path` is now a template supporting `{network_id}`, `{slot_id}`, and `{section}`, so a publisher whose ad unit varies by site section expresses it in one slot rule instead of one per (slot × section). `{section}` derives from the request path: `[creative_opportunities].section_segment` selects which path segment names the section (0-based, default `0`; set `1` for locale-prefixed URLs), and `section_root` supplies the value for paths with no such segment. `section_root` is required when a template uses `{section}`. Existing static and absent `gam_unit_path` configs are unchanged. Startup rejects a blank `gam_network_id` only when an absent/default path or `{network_id}` template consumes it. Trusted Server conservatively caps whole rendered dynamic paths at 100 UTF-8 bytes, informed by Google's 100-character per-ad-unit-code limit; an over-limit request-specific path omits that slot without failing the response. During typed/startup finalization, every placeholder-bearing template that omits `section_segment` materializes `section_segment = 0`, so an older binary rejects the blob loudly. Static and absent paths remain legacy-schema compatible only when both `section_root` and `section_segment` are omitted. Before rolling back below this feature, replace or remove dynamic paths, remove both keys, re-push and finalize the config, then roll back the binary.
- Added opt-in APS HTTP debug metadata for controlled test sites, exposing the direct request and response under `/auction` provider metadata using the Prebid Server `debug.httpcalls` shape.
Expand Down
32 changes: 32 additions & 0 deletions crates/trusted-server-cli/src/commands/dev/lint/domains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,17 @@ pub const REFERENCE_HOSTS: &[&str] = &[
"iabtechlab.com",
"iabtechlab.github.io",
"iabeurope.github.io",
// GitHub CLI: canonical verification command documentation.
"cli.github.com",
// Specs (supply chain)
"in-toto.io",
"rslstandard.org",
// SLSA: canonical provenance predicate and build specification references.
"slsa.dev",
// SPDX: canonical document predicate in the retained reviewed contract.
"spdx.dev",
// CycloneDX: canonical SBOM predicate used by Phase 1 verification.
"cyclonedx.org",
// Specs (other)
"webassembly.org",
// Fastly docs
Expand Down Expand Up @@ -311,6 +319,30 @@ mod allow_check_tests {
fn reference_hosts_allowed_everywhere() {
assert!(is_allowed("github.com", &nothing_suppressed()));
assert!(is_allowed("docs.rs", &nothing_suppressed()));
assert!(
is_allowed("slsa.dev", &nothing_suppressed()),
"should allow the canonical slsa.dev reference"
);
assert!(
is_allowed("spdx.dev", &nothing_suppressed()),
"should allow the canonical spdx.dev reference"
);
assert!(
is_allowed("cyclonedx.org", &nothing_suppressed()),
"should allow the canonical cyclonedx.org reference"
);
assert!(
is_allowed("cli.github.com", &nothing_suppressed()),
"should allow the canonical cli.github.com reference"
);
assert!(
!is_allowed("unapproved.slsa.dev", &nothing_suppressed()),
"should reject unapproved slsa.dev subdomains"
);
assert!(
!is_allowed("unapproved.cyclonedx.org", &nothing_suppressed()),
"should reject unapproved cyclonedx.org subdomains"
);
// But NOT subdomains of REFERENCE_HOSTS (exact-match).
assert!(!is_allowed("other.github.com", &nothing_suppressed()));
}
Expand Down
4 changes: 4 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export default withMermaid(
{ text: 'EdgeZero Lifecycle', link: '/guide/edgezero' },
{ text: 'Configuration', link: '/guide/configuration' },
{ text: 'CLI', link: '/guide/cli' },
{
text: 'Release Verification',
link: '/guide/release-verification',
},
{ text: 'Dev Proxy', link: '/guide/ts-dev-proxy' },
{ text: 'Testing', link: '/guide/testing' },
{ text: 'Auction Testing', link: '/guide/auction-testing' },
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/cli.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Trusted Server CLI

Prebuilt release binaries must pass the [release verification procedure](/guide/release-verification) before execution or deployment. That guide lists the Linux/macOS baselines and the manual Fastly route that preserves verified wasm bytes.

The Trusted Server CLI binary is `ts`. It is a host-target operator tool for
configuration, page audits, and EdgeZero-backed lifecycle commands.

Expand Down
Loading
Loading