#!/bin/sh # Install brig. # # curl -fsSL https://brig.sh/install | sh # # Downloads the newest release for this platform, checks it against the # published checksums, and installs brig and brigd. On macOS it also installs # hull -- the microVM runtime brig drives there, without which a first `brig # run` fails -- and cosign, which is what verifies the kernel, initrd and guest # agent every sandbox boots. # # On Linux the runtime is nerdctl over containerd with the urunc shim, none of # which is brig's to build, so this delegates to the bundle that packages it # (brig-standalone-linux). brig and brigd still come from the brig release: the # bundle's own copies are replaced with them, so the runtime and brig are # versioned separately. # # Homebrew is still the better path on macOS. It tracks upgrades, and it # installs the shell completions this script leaves in the archive for you # (docs/completions.md): # # brew tap brig-sh/brig && brew trust brig-sh/brig && brew install --cask brig # # Settings: # # BRIG_INSTALL_DIR where to install (default /usr/local/bin) # BRIG_VERSION a brig tag instead of the newest release # HULL_VERSION a hull tag instead of the newest release # BRIG_INSTALL_HULL=0 skip hull, and leave macOS without a runtime # BRIG_INSTALL_COSIGN=0 skip cosign, and leave the boot chain unverified. On # Linux the runtime bundle brings its own, so this only # stops the one fetched to check that release # BRIG_INSTALL_RUNTIME=0 skip the Linux runtime bundle, and install brig # alone, for a host with its own nerdctl, containerd # and urunc # BRIG_RUNTIME_VERSION a brig-standalone-linux tag instead of the pinned one # BRIG_INSTALL_ROOTLESS=1 install the rootless bundle on Linux, so any user # of the install can run `brig-ctl rootless` set -eu BRIG_REPO=brig-sh/brig HULL_REPO=brig-sh/hull INSTALL_RUNTIME="${BRIG_INSTALL_RUNTIME:-1}" DEST="${BRIG_INSTALL_DIR:-/usr/local/bin}" # cosign is pinned by version and by hash, which the two repos above are not. # Their checksums are fetched from the release that carries the archive; this # one is written down here instead, because cosign's own release cannot be # verified without cosign. Upstream publishes Sigstore bundles and no detached # signature, so there is nothing openssl can check, and a bundle we half-read # would look like verification without being it. Pinning moves the trust root # to this file, which is reviewed, rather than to whatever the CDN served. # # Bump both the version and the hashes together. docs/releasing.md carries the # grep that catches a version quoted in one place and not the other. # The Linux runtime bundle. Pinned here, and reviewed, for the same reason # cosign's version is: it decides what ends up running on the host. It does not # decide the brig version, since install_runtime replaces the brig the bundle # carries, so it moves only when the runtime itself does. # # v0.1.0-rc7 is the floor: it is the first release carrying the rootless # bundle, so BRIG_INSTALL_ROOTLESS against anything older fails naming the # asset it wanted. rc17 is the pin. It records the sha256 of its kernel and # initrd, and keeps that record with the release's signed checksums.txt where # brig checks it before a boot. Its guest kernel is the 6.18 LTS with Landlock, # its amd64 kernel brings up every vCPU --cpus asks for, and its urunc boots # arm64 guests under cloud-hypervisor. Its cloud-hypervisor is v54.0, which # keeps the virtio-net offloads off by default. RUNTIME_REPO=NOFireAI/brig-standalone-linux RUNTIME_VERSION=v0.1.0-rc17 RUNTIME_SIG_IDENTITY='^https://github.com/NOFireAI/brig-standalone-linux/.github/workflows/release.yml@refs/tags/' RUNTIME_SIG_ISSUER=https://token.actions.githubusercontent.com COSIGN_VERSION=v3.1.3 COSIGN_SHA_DARWIN_ARM64=5cf948c2f4dfe59687bdd0b8523709067383e03982cc543475c8a7dc70e92a76 COSIGN_SHA_LINUX_AMD64=4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71 COSIGN_SHA_LINUX_ARM64=c5d324e091826b0d7a78eb16fef316450b4eb9aaec045611c08ba06f5e73220a say() { printf 'brig-install: %s\n' "$1" >&2; } die() { say "$1"; exit 1; } command -v curl > /dev/null 2>&1 || die "curl is required" command -v tar > /dev/null 2>&1 || die "tar is required" # Pick the checksum tool before anything is downloaded, and stop if there is # none. sha256sum is absent on a stock macOS, where shasum covers it, and both # can be absent on a minimal Linux image -- which is a plausible place to pipe # an install script. Carrying on without a check is the one outcome a checksum # step exists to prevent, so this is a die and not a warning. if command -v sha256sum > /dev/null 2>&1; then SHA256_TOOL=sha256sum elif command -v shasum > /dev/null 2>&1; then SHA256_TOOL=shasum else die "need sha256sum or shasum to check what it downloads; install either one" fi sha256_check() { case "$SHA256_TOOL" in sha256sum) sha256sum -c - ;; shasum) shasum -a 256 -c - ;; esac } # verify checks one file against a checksums list in # the same directory. # # The expected line is pulled out and refused when absent, so a file the # release does not list cannot pass by handing an empty stream to the checksum # tool. "checksum ok" prints here and nowhere else, which is the only place it # is true. verify() { _line=$(grep " $2\$" "$1/$3" || true) [ -n "${_line}" ] || die "no checksum published for $2" ( cd "$1" && printf '%s\n' "${_line}" | sha256_check > /dev/null ) \ || die "checksum mismatch for $2" say "checksum ok: $2" } # newest_tag names the newest v-tagged release, prereleases included. # hull has no stable release yet, so /releases/latest answers 404 there. # # Only v-tags count. Each channel build (brig@main, hull@main) is a prerelease # of its own under a channel-* tag, so it is often the newest release of all. # Its archives are not named the way this script expects. # # The listing is split at commas before the match, so each field starts a line # whether GitHub's JSON arrives pretty-printed or on one line. Matched within # one long line, the first tag would be lost to the last. newest_tag() { curl -fsSL "https://api.github.com/repos/$1/releases?per_page=100" \ | tr ',' '\n' \ | sed -n 's/^[[:space:]{]*"tag_name": *"\(v[^"]*\)".*/\1/p' | head -1 } # install_from puts one executable in DEST. install_from() { if [ -w "$DEST" ]; then install -m 0755 "$1/$2" "$DEST/$2" else say "$DEST is not writable, using sudo" sudo install -m 0755 "$1/$2" "$DEST/$2" fi } os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$os" in darwin | linux) ;; *) die "unsupported OS: $os" ;; esac case "$(uname -m)" in arm64 | aarch64) arch=arm64 ;; x86_64 | amd64) arch=amd64 ;; *) die "unsupported architecture: $(uname -m)" ;; esac # An Intel Mac is refused here rather than later. The release does publish # darwin/amd64 archives of brig itself, so this used to install cleanly and # then fail on the first run with "brig drives hull on macOS, and none was # there" -- which reads as a dependency the user can go and install. They # cannot: hull drives Virtualization.framework on Apple silicon and has never # published an amd64 build. Say so now, while nothing has been written. if [ "$os" = darwin ] && [ "$arch" = amd64 ]; then die "brig needs Apple silicon on macOS: hull has no amd64 build, so there would be no runtime to drive (docs/support.md)" fi tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT # fetch_brig downloads the brig release archive, checks it and unpacks it into # $tmp/brig. It sets brig_version to the tag it fetched. fetch_brig() { brig_version="${BRIG_VERSION:-}" if [ -z "$brig_version" ]; then brig_version=$(newest_tag "$BRIG_REPO") [ -n "$brig_version" ] || die "could not work out the newest version; set BRIG_VERSION" fi bare="${brig_version#v}" archive="brig-${bare}-${os}-${arch}.tar.gz" base="https://github.com/$BRIG_REPO/releases/download/$brig_version" say "downloading $archive" curl -fsSL -o "$tmp/$archive" "$base/$archive" \ || die "no build for ${os}/${arch} in $brig_version" # The checksum file is signed with cosign as well. Verifying that signature # needs a cosign this script may be about to install, so the signature is # documented in the README rather than checked here. curl -fsSL -o "$tmp/brig-checksums.txt" "$base/checksums.txt" \ || die "could not fetch checksums.txt for $brig_version" verify "$tmp" "$archive" brig-checksums.txt # Both archives carry a LICENSE and a README.md, so each unpacks into its own # directory rather than over the other. mkdir -p "$tmp/brig" tar -xzf "$tmp/$archive" -C "$tmp/brig" } install_brig() { fetch_brig install_from "$tmp/brig" brig install_from "$tmp/brig" brigd say "installed $("$DEST/brig" version) to $DEST" } # hull ships one arm64 archive holding all three executables it needs: the CLI, # and the two runners that carry the entitlements their backends require -- # virtualization for vz-runner, hypervisor for hvi. They go into the same # directory on purpose: hull discovers a runner next to its own executable, and # splitting them breaks that. # # The archive is what the Homebrew cask installs too. hull.dmg holds the same # binaries in an app bundle for people who would rather drag it to # Applications, which is not a thing a script should be doing. install_hull() { hull_version="${HULL_VERSION:-}" if [ -z "$hull_version" ]; then hull_version=$(newest_tag "$HULL_REPO") [ -n "$hull_version" ] || die "could not work out hull's newest release; set HULL_VERSION" fi hull_bare="${hull_version#v}" hull_archive="hull-${hull_bare}-arm64.tar.gz" hull_base="https://github.com/$HULL_REPO/releases/download/$hull_version" say "downloading $hull_archive" curl -fsSL -o "$tmp/$hull_archive" "$hull_base/$hull_archive" \ || die "no hull build for arm64 in $hull_version" curl -fsSL -o "$tmp/hull-checksums.txt" "$hull_base/checksums.txt" \ || die "could not fetch hull's checksums.txt for $hull_version" verify "$tmp" "$hull_archive" hull-checksums.txt mkdir -p "$tmp/hull" tar -xzf "$tmp/$hull_archive" -C "$tmp/hull" install_from "$tmp/hull" hull install_from "$tmp/hull" vz-runner install_from "$tmp/hull" hvi say "installed $("$DEST/hull" --version) to $DEST" } # cosign, as a tool rather than as an install. install_runtime needs one to # check the runtime release's signature, and it needs it before that release is # trusted, so the bundle's own cosign is too late to use. On Linux nothing else # in this script writes to DEST, and DEST is not writable by a normal user, so # installing a second cosign there would ask for sudo on the one path that is # meant to need none. This one lands in $tmp and is verified by the pinned hash # above, exactly as install_cosign verifies the copy it installs. COSIGN_BIN="" resolve_cosign() { if command -v cosign > /dev/null 2>&1; then COSIGN_BIN=cosign return 0 fi [ "${BRIG_INSTALL_COSIGN:-1}" != 0 ] || return 1 case "$os/$arch" in darwin/arm64) cosign_sha=$COSIGN_SHA_DARWIN_ARM64 ;; linux/amd64) cosign_sha=$COSIGN_SHA_LINUX_AMD64 ;; linux/arm64) cosign_sha=$COSIGN_SHA_LINUX_ARM64 ;; *) return 1 ;; esac say "downloading cosign $COSIGN_VERSION to check the runtime release (about 130 MB)" curl -fsSL -o "$tmp/cosign" \ "https://github.com/sigstore/cosign/releases/download/$COSIGN_VERSION/cosign-${os}-${arch}" \ || return 1 printf '%s cosign\n' "$cosign_sha" > "$tmp/cosign-checksums.txt" verify "$tmp" cosign cosign-checksums.txt chmod 0755 "$tmp/cosign" COSIGN_BIN="$tmp/cosign" } # The Linux runtime, installed by the bundle repository's own install.sh. That # script is a release asset covered by the same checksums.txt the bundle is, so # it is verified the way every other download here is rather than piped from a # branch. The bundle installer then fetches and verifies its own tarball from # the same pinned release. # # It decides where the tree goes from its own euid: run under sudo it installs # the node-wide stack, run as a user it installs under $HOME. That is the same # answer this script gives for DEST, so the two agree without being told. install_runtime() { version="${BRIG_RUNTIME_VERSION:-$RUNTIME_VERSION}" # brig goes into the bundle's tree, behind its launchers, so there is no # directory of ours for BRIG_INSTALL_DIR to choose. [ -z "${BRIG_INSTALL_DIR:-}" ] \ || say "BRIG_INSTALL_DIR is ignored here: the bundle installs its own launchers" # A node-wide install is root's, and a brig an earlier run of this script # put in DEST knows nothing of the bundle's containerd. A user install # cannot remove anything there, so it is checked by PATH order instead, # once the launcher is in place. if [ "$(id -u)" = 0 ] && [ -x "$DEST/brig" ]; then say "$DEST/brig is from an earlier install, and may run instead of the bundle's launcher; remove it" fi base="https://github.com/$RUNTIME_REPO/releases/download/$version" say "downloading the Linux runtime installer ($RUNTIME_REPO $version)" curl -fsSL -o "$tmp/install.sh" "$base/install.sh" \ || die "no install.sh in $RUNTIME_REPO $version" curl -fsSL -o "$tmp/runtime-checksums.txt" "$base/checksums.txt" \ || die "could not fetch checksums.txt for $RUNTIME_REPO $version" verify "$tmp" install.sh runtime-checksums.txt # With cosign present the release's provenance is checkable, not just its # integrity, and the bundle installer is told to insist on the same for the # tarball it fetches. A failure here is fatal: it is what a tampered or # mis-signed release looks like. # # The version is pinned, and a release from that workflow carries both # signature assets. A fetch that does not find one is an anomaly, so it stops # the install. Without cosign there is nothing to check with, and that is the # one case that falls back to the hash. require_sig=false resolve_cosign || true if [ -z "$COSIGN_BIN" ]; then say "no cosign available, so the runtime release is checked by hash alone" else curl -fsSL -o "$tmp/runtime-checksums.txt.sig" "$base/checksums.txt.sig" \ || die "no checksums.txt.sig in $RUNTIME_REPO $version" curl -fsSL -o "$tmp/runtime-checksums.txt.pem" "$base/checksums.txt.pem" \ || die "no checksums.txt.pem in $RUNTIME_REPO $version" "$COSIGN_BIN" verify-blob "$tmp/runtime-checksums.txt" \ --certificate "$tmp/runtime-checksums.txt.pem" \ --signature "$tmp/runtime-checksums.txt.sig" \ --certificate-identity-regexp "$RUNTIME_SIG_IDENTITY" \ --certificate-oidc-issuer "$RUNTIME_SIG_ISSUER" > "$tmp/cosign.log" 2>&1 \ || { cat "$tmp/cosign.log" >&2 die "the runtime release's checksums.txt is not signed by $RUNTIME_REPO's release workflow"; } say "signature ok: $RUNTIME_REPO checksums.txt" require_sig=true fi case "${BRIG_INSTALL_ROOTLESS:-0}" in 1|true|yes) rootless=true ;; *) rootless=false ;; esac # A cosign fetched above sits in $tmp and is on nobody's PATH, so the bundle # installer -- which is being told to require a signature -- would refuse for # want of the very tool this just verified with. Hand it over. runtime_path="$PATH" case "$COSIGN_BIN" in */*) runtime_path="${COSIGN_BIN%/*}:$PATH" ;; esac # brig is fetched and checked before the runtime installer writes anything, # so a brig release that is missing or fails its checksum leaves the host # untouched. fetch_brig say "running the runtime installer" PATH="$runtime_path" \ INSTALL_BRIG_RELEASE_REPO="$RUNTIME_REPO" \ INSTALL_BRIG_RELEASE_VERSION="$version" \ INSTALL_BRIG_REQUIRE_SIGCHECK="$require_sig" \ INSTALL_BRIG_ROOTLESS="$rootless" \ sh "$tmp/install.sh" || die "the runtime installer failed" # The bundle's launchers exec $PREFIX/bin/brig and $PREFIX/bin/brigd. It # picks PREFIX from its euid, the same way it picks where to install: # /var/lib/brig for root, the XDG data directory for anyone else. if [ "$(id -u)" = 0 ]; then prefix=/var/lib/brig/data else prefix="${XDG_DATA_HOME:-$HOME/.local/share}/brig/data" fi [ -x "$prefix/bin/brig" ] \ || die "the runtime installer left no brig at $prefix/bin/brig to replace" install -m 0755 "$tmp/brig/brig" "$prefix/bin/brig" install -m 0755 "$tmp/brig/brigd" "$prefix/bin/brigd" # pins.env is what `brig-ctl version` reports, so it names the brig that is # there now. if [ -f "$prefix/pins.env" ]; then sed -i "s/^BRIG_VERSION=.*/BRIG_VERSION=$brig_version/" "$prefix/pins.env" fi say "installed brig $brig_version into the runtime at $prefix/bin" # A user install's launcher is in ~/.local/bin, and a brig earlier on PATH # runs instead. On a shared host that is usually the node-wide launcher in # /usr/local/bin, which this user cannot remove. if [ "$(id -u)" != 0 ]; then launchers="$HOME/.local/bin" if shadow=$(brig_before "$launchers"); then say "\`brig\` on your PATH is $shadow, so it runs instead of this install" say "put $launchers before ${shadow%/*} on PATH, for instance in ~/.profile:" say " export PATH=\"\$HOME/.local/bin:\$PATH\"" fi fi } # brig_before prints the first brig on PATH that comes ahead of , # and fails when there is none. missing from PATH counts as last. brig_before() { _ifs=$IFS IFS=: for _d in $PATH; do [ -n "$_d" ] || continue [ "$_d" = "$1" ] && break if [ -x "$_d/brig" ]; then IFS=$_ifs printf '%s\n' "$_d/brig" return 0 fi done IFS=$_ifs return 1 } # cosign is what turns hull's boot-asset check from a printed warning into an # answer. Without it on PATH the check reports "no tooling", and under the # default mode that is not a refusal: the kernel a sandbox boots is fetched, # written and booted unverified. Installing it is what makes HULL_VERIFY=require # and BRIG_VERIFY=require usable on a host without Homebrew. install_cosign() { if command -v cosign > /dev/null 2>&1; then say "cosign is already on PATH, leaving it alone" return fi case "$os/$arch" in darwin/arm64) cosign_sha=$COSIGN_SHA_DARWIN_ARM64 ;; linux/amd64) cosign_sha=$COSIGN_SHA_LINUX_AMD64 ;; linux/arm64) cosign_sha=$COSIGN_SHA_LINUX_ARM64 ;; *) say "no pinned cosign for $os/$arch, skipping it"; return ;; esac # Worth saying out loud: this is a 130 MB download for one subprocess, and it # is the largest thing this script pulls by an order of magnitude. say "downloading cosign $COSIGN_VERSION (about 130 MB)" curl -fsSL -o "$tmp/cosign" \ "https://github.com/sigstore/cosign/releases/download/$COSIGN_VERSION/cosign-${os}-${arch}" \ || die "could not fetch cosign $COSIGN_VERSION for ${os}/${arch}" printf '%s cosign\n' "$cosign_sha" > "$tmp/cosign-checksums.txt" verify "$tmp" cosign cosign-checksums.txt install_from "$tmp" cosign say "installed cosign $COSIGN_VERSION to $DEST" if [ "$os" = darwin ]; then # Everything else this script installs is Developer ID signed and # notarized. cosign's macOS build is ad-hoc signed upstream, so Gatekeeper # rejects it on its own and it runs here only because a curl download # carries no quarantine attribute. Say it rather than let someone find out. say "note: cosign's macOS build is ad-hoc signed upstream, unlike the rest of this install" fi } # On Linux brig and brigd run behind the bundle's launchers, which set the # environment that points brig at the private containerd. install_runtime puts # the release's brig in the bundle's tree rather than in DEST, since a second # copy on PATH would silently win over the launcher. The bundle carries cosign # too, on the PATH its launcher sets, so nothing here writes to DEST on that # path: an unprivileged install needs no sudo at all. if [ "$os" = linux ] && [ "$INSTALL_RUNTIME" != 0 ]; then install_runtime else # brig first. install_cosign dies on a failed download, and cosign is the # least important thing here, so fetching it before brig would let a sigstore # outage leave the host with nothing installed. install_brig if [ "${BRIG_INSTALL_COSIGN:-1}" != 0 ]; then install_cosign fi fi if [ "$os" = darwin ] && [ "${BRIG_INSTALL_HULL:-1}" != 0 ]; then install_hull fi # hull finds cosign on PATH, so a DEST that is not on it leaves the boot check # reporting "no tooling" even though the binary is installed. The runtime # bundle installs nothing in DEST and reports on its own launcher directory, so # this says nothing there. if [ "$os" = darwin ] || [ "$INSTALL_RUNTIME" = 0 ]; then case ":${PATH}:" in *":$DEST:"*) ;; *) say "$DEST is not on your PATH; add it, or hull will not find cosign there" ;; esac fi if [ "$os" = darwin ] && ! command -v hull > /dev/null 2>&1 \ && [ "${BRIG_INSTALL_HULL:-1}" = 0 ]; then say "hull was skipped, so brig has no runtime here:" say " brew tap brig-sh/brig && brew trust brig-sh/brig && brew install --cask hull" fi say "next: brig doctor"