Getting started

Create and verify a signed release on your own machine. This walkthrough packages a small shell script, signs its release metadata with a local Ed25519 key, and checks the archive against that signed record. You will also change the archive to see verification fail.

After installing packslip, the example runs offline. It uses no CI identity and leaves the test signature out of Rekor, sigstore’s public transparency log. Publish a real release explains how to turn the example into a logged release.

If you want to install someone else’s software instead, install packslip below, then follow Install a tool. To check files you already downloaded, see Verify a release.

Install packslip

We recommend mise to install packslip and manage its version. You can also run the install script, copy packslip from its container image, download a release, or build from source. Release builds cover Linux and Windows on x64 and arm64, and macOS on arm64. On an Intel Mac, build from source. Linux users can also install from the signed APT and RPM repositories.

mise

With mise 2026.9.2 or newer installed and activated, run:

mise use -g packslip
packslip version

The registry entry for packslip uses mise’s packslip backend and names the workflow that signs packslip’s releases. mise checks each release against that workflow before installing it. -g makes packslip available globally; omit it to manage packslip in the current project instead.

Script

On Linux or macOS, run:

curl -fsSL https://packslip.sh | sh
packslip version

On Windows, in PowerShell:

irm https://packslip.sh/install.ps1 | iex

The script downloads the packslip executable for your platform and refuses it unless its SHA-256 matches the one written into the script when the release was built. It installs the executable in ~/.local/bin, or in /usr/local/bin when run as root, and ~\.local\bin on Windows. Set PACKSLIP_BIN_DIR to install somewhere else. The script says when that directory is not on PATH, and it downloads and runs nothing else.

Each release publishes its own copy of the scripts, and https://packslip.sh/vVERSION serves that release’s install.sh. Replace VERSION with a release number, such as 1.5.1, and SHA256 with the SHA-256 of that release’s script. One script covers every supported platform, so its checksum pins the bootstrap download across architectures. In a Dockerfile, Docker checks that script before running it:

ADD --checksum=sha256:SHA256 https://packslip.sh/vVERSION /tmp/install-packslip.sh
RUN sh /tmp/install-packslip.sh

The image needs curl or wget for the script’s download. To calculate the script’s checksum, download the versioned script and run sha256sum install.sh (Linux) or shasum -a 256 install.sh (macOS). The scripts are attested with the rest of the release, so you can check a copy you saved with gh attestation verify install.sh --repo jdx/packslip.

Container image

ghcr.io/jdx/packslip holds packslip and a CA bundle, for linux/amd64 and linux/arm64. In a Dockerfile, copy the executable out of it:

COPY --from=ghcr.io/jdx/packslip:VERSION@sha256:DIGEST /packslip /usr/local/bin/packslip

The digest pins every architecture at once, and the image you copy into needs no curl, tar, or hash tool. docker buildx imagetools inspect ghcr.io/jdx/packslip:VERSION shows the digest. The image is attested like the release files:

gh attestation verify oci://ghcr.io/jdx/packslip:VERSION --repo jdx/packslip

You can also run it directly, with your files mounted:

docker run --rm -v "$PWD:/work" -w /work ghcr.io/jdx/packslip:VERSION version

Download

Download the archive for your platform from GitHub releases: linux-x64, linux-arm64, and darwin-arm64 as .tar.xz, or windows-x64 and windows-arm64 as .zip. Extract the archive, put the packslip executable from its packslip-vVERSION-PLATFORM/ directory on PATH, then check the installation. Each release also has the executable alone, packslip-vVERSION-PLATFORM, with .exe on Windows:

packslip version

Build from source

The current release requires Rust 1.93 or newer:

cargo install packslip --locked
packslip version

This builds the latest release from crates.io. To pin the bootstrapper, add --version 1.5.1. To try development changes, clone the repository and run cargo install --path . --locked there, then return to an empty directory for the walkthrough. Cargo installs the executable in its bin directory, usually ~/.cargo/bin; make sure that directory is on PATH.

Create a sample release

The following commands use a POSIX shell and tar. Run them in an empty directory. The sample needs no platform-specific compiler. On Windows, use a POSIX environment such as WSL for these commands.

mkdir -p staging/bin dist
printf '#!/bin/sh\nprintf "hello from mytool\\n"\n' > staging/bin/mytool
chmod +x staging/bin/mytool
tar -czf dist/mytool-1.2.3.tar.gz -C staging bin
packslip keygen --out release.key

You now have dist/mytool-1.2.3.tar.gz, an archive with one executable at bin/mytool. packslip keygen also generated a signing key pair and printed its key ID (yours will differ):

wrote release.key and release.pub (key id C8B574447E4F0ACA)

release.key is the secret key; keep it out of source control. Consumers need only the public key, release.pub. The key ID names the signer in later output. keygen refuses to overwrite a key, so to run the walkthrough again, start in a new empty directory.

Create and sign the packslip

packslip create \
  --project mytool.example.com \
  --version 1.2.3 \
  --key release.key --no-log \
  --out dist \
  --url-base https://mytool.example.com/v1.2.3 \
  --bin mytool \
  dist/mytool-1.2.3.tar.gz:any

create hashes the archive and finds the executable inside it. It describes the release in a JSON statement, signs the statement with your key, and writes the signed bundle to dist/packslip.sigstore.json:

wrote dist/packslip.sigstore.json (1 artifact(s), signed by C8B574447E4F0ACA, unlogged)

Read the signed statement

packslip show prints the statement inside the bundle. It only decodes the statement; the next step checks the signature.

packslip show dist/packslip.sigstore.json

The statement’s core fields look like this (the sha256 digest is abbreviated):

{
  "subject": [{
    "name": "mytool-1.2.3.tar.gz",
    "digest": { "sha256": "..." }
  }],
  "predicateType": "https://packslip.dev/release/v1",
  "predicate": {
    "project": "mytool.example.com",
    "version": "1.2.3",
    "artifacts": [{
      "name": "mytool-1.2.3.tar.gz",
      "url": "https://mytool.example.com/v1.2.3/mytool-1.2.3.tar.gz",
      "format": "tar.gz",
      "bin": ["bin/mytool"]
    }]
  }
}
FieldWhat the consumer does with it
subjectChecks the downloaded file against its signed digest.
predicateTypeRecognizes the payload as a packslip release statement.
project and versionConfirms that this is the requested project and release.
artifacts[].nameConnects the artifact metadata to its entry in subject.
url and formatFinds the file and determines how to unpack it.
binFinds the executable at its actual archive path.

This sample omits os, arch, and libc because the archive is declared platform-independent. The full output also has a sha512 digest, the artifact’s size, the publication time, the signing identity (scheme and key_id), and the in-toto _type. The surrounding bundle carries the signature and verification material.

Verify the archive

packslip verify dist/packslip.sigstore.json \
  --pubkey release.pub --allow-unlogged \
  --artifact dist/mytool-1.2.3.tar.gz

On success it prints one line (your key ID and time will differ):

ok: mytool.example.com 1.2.3 published 2026-10-02T19:23:31.767734662Z signed by C8B574447E4F0ACA (sigstore-key) unlogged (1 of 1 artifact(s) checked)

A successful exit means the bundle passed verification against your key and the supplied archive matched its signed digest and size. --allow-unlogged is necessary because this example used --no-log. Without --artifact, the line ends (0 of 1 artifact(s) checked): the bundle was checked, the archive was not.

Compare the project and version in the ok: line with the release you expected. verify authenticates the document under your key but does not know which project or version you intended to install. Verify a release lists what a successful check does and does not establish.

To see a failure, change the archive and verify again:

printf x >> dist/mytool-1.2.3.tar.gz
packslip verify dist/packslip.sigstore.json \
  --pubkey release.pub --allow-unlogged \
  --artifact dist/mytool-1.2.3.tar.gz

The command exits with status 1 and names the mismatch (your digests will differ):

verification failed: artifact mytool-1.2.3.tar.gz: sha256 is d8e80700490e50d80579a4469b3dd9eed898634ed00204396da9707b2054c7f5, document says 96597cf66f34b2001b8a45786979bf3e167e8a36d6b65e5c277e8e3f59270447

Publish a real release

The walkthrough’s shortcuts are for trying the format. For a release people install:

On your own host, upload each artifact to the URL the statement records, and put the bundle beside it, for example at https://mytool.example.com/v1.2.3/packslip.sigstore.json. Consumers find a project named after its own domain, like mytool.example.com, only through a signed release list on that host, and they refuse the project without one. Publish a list with every release, as Manage release lists explains. To publish the releases and the list from GitHub Actions, see Host releases on your own domain. To adapt the example to your own files, see Artifact configuration.