Work-in-progress proposal. This is a version 1 draft, not an adopted standard: it is far from certain that mise or Omarchy will adopt it. It still changes between releases; expect breaking changes until it is declared stable.
Specificationpackslip: a signed release manifest
Goal
A vendor publishes one signed, machine-readable document per release that says what the artifacts are and how to verify them. Any consumer (mise, omapac and the Omarchy Package Repository, aqua, Homebrew, a corporate mirror) verifies it against a single pinned identity or key and gets checksums, platform mapping, executables, and provenance links, without per-vendor logic and without a registry entry. The name is neutral on purpose: a packing slip is the paper in the box listing exactly what shipped.
packslip deliberately invents as little as possible. The document is an in-toto statement in a sigstore bundle, the same shape GitHub artifact attestations, npm provenance, and Homebrew bottles use. Identity comes from sigstore's certificate authority and transparency log. What packslip adds is the predicate: the release-level manifest that a registry entry would otherwise hold.
Names
A project is named the way Go names a module: a host, optionally followed by a path. github.com/jdx/mise, gitlab.com/group/tool, mise.jdx.dev. No scheme, lowercase host with at least one dot, no empty or dot segments, no trailing slash.
The name is the location and, on a forge, the identity:
github.com/<owner>/<repo>: releases and their packslips are GitHub release assets, and the packslip is expected to be signed by a workflow of that repository through GitHub's OIDC issuer (https://token.actions.githubusercontent.com).gitlab.com/<path>: likewise, signed by a pipeline of that project throughhttps://gitlab.com. GitLab subgroups make paths arbitrary depth, so the whole path is the pin.- Any other host: the vendor controls the domain and publishes a release list at the well-known URL below, signed with the key or identity the consumer pins.
A consumer needs nothing else to start verifying a project on a known forge. A short-name alias table (mise for github.com/jdx/mise) is a convenience a consumer may add; it is not part of the format.
Monorepos
A repository that releases several tools names each one with a subpath, as Go names nested modules: github.com/oxc-project/oxc/oxlint, github.com/bazelbuild/buildtools/buildifier, github.com/biomejs/biome/cli. Each tool gets its own packslip per release, with its own version, and source.tag carries the real tag (oxlint_v1.0.0, cli/v1.9.4), so nobody has to guess how a tag maps to a version. The identity pin is still the repository: any workflow of oxc-project/oxc may sign a packslip for oxc-project/oxc/oxlint.
When several tools share one GitHub release, each ships its own bundle, named packslip.<subpath>.sigstore.json with / in the subpath replaced by - (packslip.oxlint.sigstore.json, packslip.crates-cli.sigstore.json). A repository's own packslip stays packslip.sigstore.json. Consumers do not trust the file name: they read the packslip*.sigstore.json assets of a release and keep the one whose predicateType is release/v1 and whose project is the name they asked for.
The file
A release ships one file per project, a sigstore bundle (v0.3) whose content is a DSSE envelope of type application/vnd.in-toto+json carrying the statement below, and whose verification material is either the signer's Fulcio certificate or a public-key hint, plus the Rekor transparency log entry for the signature.
Because the bundle carries the statement, there is no separate plain JSON file and no canonical-bytes rule: whatever bytes are in the payload are what was signed. packslip show prints them; so does jq -r .dsseEnvelope.payload | base64 -d. cosign and gh attestation understand the bundle as-is.
The release statement
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{ "name": "mise-v2026.9.1-linux-x64.tar.xz",
"digest": { "sha256": "...", "sha512": "..." } }
],
"predicateType": "https://packslip.dev/release/v1",
"predicate": {
"project": "github.com/jdx/mise",
"version": "2026.9.1",
"published_at": "2026-09-01T12:00:00Z",
"channel": "stable",
"source": { "repo": "https://github.com/jdx/mise", "commit": "...", "tag": "v2026.9.1" },
"artifacts": [
{
"name": "mise-v2026.9.1-linux-x64.tar.xz",
"os": "linux", "arch": "x86_64", "libc": "gnu",
"size": 12345678,
"url": "https://github.com/jdx/mise/releases/download/v2026.9.1/mise-v2026.9.1-linux-x64.tar.xz",
"format": "tar.xz",
"bin": ["mise/bin/mise"],
"requires": { "glibc_min": "2.31" },
"provenance": ["https://api.github.com/repos/jdx/mise/attestations/sha256:..."]
}
],
"identity": {
"scheme": "sigstore-oidc",
"key_id": "https://github.com/jdx/mise/.github/workflows/release.yml@refs/tags/v2026.9.1",
"issuer": "https://token.actions.githubusercontent.com"
},
"notes_url": "https://github.com/jdx/mise/releases/tag/v2026.9.1",
"sbom": "https://.../sbom.cdx.json",
"supersedes": "2026.9.0"
}
}
Rules:
subjectlists every artifact by file name with its digests;artifactscarries the same names with platform, size, download URL, format, executables, requirements, and provenance links. The two sets of names must match exactly, and neither may contain a duplicate. At least one artifact is required.sha256is required and is 64 lowercase hex characters;sha512is optional and 128.projectis a name as defined above.versionis the vendor's version string, compared as opaque text.published_atis RFC 3339 UTC.prerelease(default false) marks a release not meant for general use; consumers skip it unless asked for prereleases.channelis the vendor's own word for the release track (stable,beta,nightly).version_orderissource(default) orsemver; see Ordering versions.os,arch, andlibcuse the valueslinux,darwin,windows,freebsd;x86_64,aarch64,armv7,riscv64,i686;gnu,musl.formatis the archive or installer type:tar.xz,tar.gz,tar.zst,tar.bz2,tgz,zip,7z,deb,rpm,dmg,pkg,msi,msix,exe,appimage, orrawfor a bare executable.varianttells apart artifacts that share os, arch, libc, and format:fips,baseline,debug,installer,source. A vendor must not publish two artifacts that agree on all five;packslip createrefuses to. Consumers that find such a pair refuse to choose.binlists the executables inside the artifact. Each entry is a path relative to the archive root, or the artifact's own name when it is a bare executable. When the name to put on PATH differs from the file name, the entry is{ "path": "bin/oxlint-x86_64", "name": "oxlint" }. Windows entries carry their.exe.requiresstates what the host needs:os_minin the OS's own terms (12for macOS Monterey,10.0.17763for Windows) andglibc_minfor agnuLinux build.provenanceholds URLs of SLSA build provenance statements for that artifact. The packslip proves the manifest; verified provenance proves the build, at whatever SLSA build level its builder establishes.supersedesnames the release this one replaces, so a consumer can detect a rollback without a version-ordering scheme.notes_urlpoints at the release notes.identitysays how the document is signed and by whom, so a consumer can check what it pinned against what it received. Forsigstore-oidc,key_idis the certificate's subject identity (a workflow URI for CI, an email for a person) andissuerthe OIDC issuer. Forsigstore-key,key_idis the key id in uppercase hex.attested_byisvendor(default) orrepackager. See below.
Field reference
| Field | Type | Meaning | |
|---|---|---|---|
_type | string | required | Always https://in-toto.io/Statement/v1. |
subject[] | array | required | One entry per artifact. Mirrors predicate.artifacts by name. |
subject[].name | string | required | The artifact's file name. |
subject[].digest.sha256 | string | required | SHA-256 of the file, lowercase hex. |
subject[].digest.sha512 | string | optional | SHA-512 of the file, lowercase hex. |
predicateType | string | required | Always https://packslip.dev/release/v1. |
predicate.project | string | required | Host path naming the project, such as github.com/jdx/mise or github.com/oxc-project/oxc/oxlint. |
predicate.version | string | required | The vendor's version string. Opaque; never parsed for ordering. |
predicate.published_at | string | required | RFC 3339 UTC publish time. |
predicate.prerelease | boolean | optional | Not for general use. Default false. |
predicate.channel | string | optional | Release track: stable, beta, nightly. |
predicate.version_order | string | optional | source (default: the release list's order) or semver. |
predicate.source | object | optional | Where the release was built from. |
predicate.source.repo | string | required | Source repository URL. |
predicate.source.commit | string | optional | Commit the release was built from. |
predicate.source.tag | string | optional | Tag the release was built from, as the vendor spells it. |
predicate.artifacts[] | array | required | One entry per artifact; at least one. |
artifacts[].name | string | required | File name, matching a subject entry. |
artifacts[].os | string | optional | linux, darwin, windows, freebsd. |
artifacts[].arch | string | optional | x86_64, aarch64, armv7, riscv64, i686. |
artifacts[].libc | string | optional | gnu or musl; Linux only. |
artifacts[].variant | string | optional | Distinguishes builds sharing os/arch/libc/format: fips, baseline, debug, installer, source. |
artifacts[].size | integer | required | File size in bytes. Verified alongside the digest. |
artifacts[].url | string | optional | Download URL. |
artifacts[].format | string | optional | Archive or installer type, or raw for a bare executable. |
artifacts[].bin[] | array of string or object | optional | Executables inside the artifact: a path, or { path, name } when the PATH name differs. |
artifacts[].requires | object | optional | os_min and glibc_min. |
artifacts[].provenance[] | array of string | optional | URLs of SLSA build provenance statements for this artifact. |
predicate.identity.scheme | string | required | sigstore-oidc or sigstore-key. |
predicate.identity.key_id | string | required | The certificate identity, or the key id in uppercase hex. |
predicate.identity.issuer | string | optional | The OIDC issuer, for sigstore-oidc. |
predicate.attested_by | string | optional | vendor (default) or repackager. |
predicate.evidence[] | array of object | optional | What a repackager checked: { kind, detail }. |
predicate.notes_url | string | optional | URL of the release notes. |
predicate.sbom | string | optional | URL of a software bill of materials for the release. |
predicate.supersedes | string | optional | The version this release replaces. |
Repackager attestation
A repository or mirror that redistributes a vendor's artifacts, and whose vendor publishes no packslip, may sign one itself with "attested_by": "repackager". The project still names the vendor's project and the artifacts are still the vendor's files, but identity is the repackager's, and evidence says what it checked before signing:
"attested_by": "repackager",
"evidence": [
{ "kind": "apt-release-gpg", "detail": "3FEF9748469ADBE15DA7CA80AC2D62742012EA22" },
{ "kind": "pkgbuild-checksums" }
]
Documented kinds: pkgbuild-checksums (digests matched the packaging the repackager maintains), checksum-file-over-tls (the vendor's checksum file, unsigned), apt-release-gpg (an apt index signed with the given key), vendor-signature (a detached signature the vendor publishes), github-attestation (GitHub artifact attestations verified), none.
A repackager document proves that the repackager published exactly these digests and checked the listed evidence. It does not prove anything the vendor did not sign. Consumers rank it below a vendor document, and a consumer that already holds a vendor document for a project refuses to replace it with a repackager one without a human's say-so.
Signing
Both schemes produce the same file and are verified by the same code. A vendor should prefer the first.
sigstore-oidc: keyless. A CI job with an id-token permission signs with its own identity; Fulcio issues a short-lived certificate naming the workflow that ran, and Rekor logs the signature. There is no key to manage.packslip createdoes this by default when it finds an ambient CI credential (GitHub Actions, GitLab CI, and the others sigstore's clients know) or a token inSIGSTORE_ID_TOKEN.sigstore-key: a long-lived Ed25519 key frompackslip keygen, kept in minisign's key-file format. The bundle carries a public-key hint and the Rekor entry, whose verifier is the public key. For vendors who release outside a CI system with OIDC, or who want a stable key their consumers pin. Consumers pin the public key, never the hint.
A key-signed bundle may be produced without a log entry (create --no-log) for an air-gapped release. Consumers refuse such a bundle unless they explicitly allow it (verify --allow-unlogged), and a repository should record that choice per vendor.
Detached minisign signatures over a plain JSON file, which an earlier draft used, are not a scheme. A consumer that wants a dependency-free check still has one: the DSSE signature of a key-signed bundle is a raw Ed25519 signature over the pre-authentication encoding of the payload.
Discovery
Publish the bundle next to the artifacts: as a release asset, or under the version directory of a download site.
Every project has a release list, and it is what consumers order by:
- For
github.com/<owner>/<repo>[/<tool>]the list is the repository's releases endpoint. Its order is the vendor's order. A release counts when it is not a draft and carries a packslip whoseprojectmatches; to yank one, remove its packslip asset or the release. - Any other project publishes a signed list at
https://<host>/.well-known/packslip/<path>.json, where<path>is the project name after the host, orpackslip.jsondirectly under.well-knownwhen the name is a bare host.
The list is required: a consumer that finds none refuses the project rather than guessing at URLs. It is a bundle of the same shape as a packslip, with the releases/v1 predicate:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{ "name": "https://dl.example.com/2026.9.1/packslip.sigstore.json",
"digest": { "sha256": "...", "sha512": "..." } },
{ "name": "https://dl.example.com/2026.9.0/packslip.sigstore.json",
"digest": { "sha256": "..." } }
],
"predicateType": "https://packslip.dev/releases/v1",
"predicate": {
"project": "mise.jdx.dev",
"generated_at": "2026-09-01T12:00:00Z",
"expires_at": "2026-10-01T12:00:00Z",
"sequence": 42,
"identity": { "scheme": "sigstore-key", "key_id": "5A0A0B8B9C6D7E1F" },
"version_order": "semver",
"releases": [
{ "version": "2026.9.1", "published_at": "2026-09-01T12:00:00Z",
"packslip": "https://dl.example.com/2026.9.1/packslip.sigstore.json",
"security": true },
{ "version": "2026.9.0", "published_at": "2026-08-20T12:00:00Z",
"packslip": "https://dl.example.com/2026.9.0/packslip.sigstore.json",
"status": "yanked", "status_reason": "CVE-2026-1234" }
]
}
}
Each subject is a listed packslip's URL with the digest of that file, so the list pins the exact documents it points at. expires_at and sequence are borrowed from TUF's timestamp role: a consumer refuses a list that has expired, or whose sequence is lower than one it has already accepted, so a mirror cannot freeze or roll back the vendor's view.
Each entry may carry prerelease and channel (copied from the packslip), status: "yanked" with a status_reason when the vendor withdrew the release, and security: true when it fixes a vulnerability. A consumer never selects a yanked release, warns when it holds one, and may shorten its minimum release age for a security release.
packslip releases produces the list from local copies of the released bundles. The list separates the name from where the bytes live, the way a Go vanity import does: the identity is anchored to the domain, and the artifacts can be anywhere.
Ordering versions
version is the vendor's string and a consumer never infers a scheme from it: histories switch schemes, two-component versions parse and sort wrong, and date versions look like semver. The vendor declares how its versions order with version_order, on each release and on the list, in the two values mise's registry uses:
source(default): the release list's order, newest first, is the ranking. On GitHub that is the releases endpoint's order; on a vendor's own list it is the array order. "Latest" is the first eligible entry.semver: versions are strictMAJOR.MINOR.PATCH(calver such as2026.9.1qualifies) and sort as semver. "Latest" is the highest eligible version, so a backport such as 20.19.1 published after 22.0.0 never masquerades as the newest release, and range constraints (^1.2) have meaning.
Eligible means not yanked, not a prerelease unless prereleases were asked for, and in the requested channel when one was given. A requested version matches as a prefix on dot-separated components under either order, so 20 and 3.12 mean what people expect; range constraints are refused under source rather than guessed. supersedes remains a rollback hint and takes no part in ordering.
Consumer rules
- Pin the identity once. For a forge project, the name is the pin: accept only the forge's issuer and an identity under the repository. For other projects, pin the public key or identity from a list of pins you maintain, or from the well-known list on first use. Never take a key from the document itself, and never trust a bundle's key hint.
- Verify the bundle: signature, certificate chain and log entry as sigstore defines them, then the statement's structure, then the subject digest and size of every artifact you downloaded.
- Enforce no-downgrade: refuse a release whose
identity.schemeis weaker than the last accepted one, whose signer changed without a human saying so, whoseattested_bywent from vendor to repackager, or that dropped per-artifact provenance the last release carried. For a keyless signer, compare the workflow path, not the ref: a new tag of the same workflow is the same signer. - Apply any minimum release age to the log's integration time, falling back to
published_atonly for an unlogged bundle you chose to accept. - Treat
supersedesas the ordering hint for rollback detection. - Use the project's release list (GitHub's releases endpoint, or the signed list) and refuse a project that has neither. Refuse a signed list that has expired or whose
sequenceis below the last one accepted; never select a yanked entry; skip prereleases unless asked for them; order asversion_ordersays. - Select one artifact by os, arch, libc, format, and, when needed, variant. Refuse to guess between two artifacts that match.
What a verified packslip proves
A verified packslip proves that the named signer published exactly this list of artifacts, with these digests, at a time the log recorded. It does not by itself prove anything about how the artifacts were built. That is what SLSA provenance is for: an artifact whose linked provenance a consumer verifies earns the SLSA build level its builder establishes (GitHub-hosted runners with actions/attest-build-provenance give Build L3). Consumers record what they verified as a SLSA Verification Summary or in their own terms; packslip defines no level scale of its own.
packslip verify reports the scheme, the signer, who attested, the log time, and whether every artifact links provenance. It does not fetch or verify the provenance statements.
Tooling
The reference implementation is the packslip crate and binary in jdx/packslip, also usable as a GitHub Action.
- In a release job:
uses: jdx/packslip@v1withartifacts: dist/*attests build provenance for the artifacts, signs the packslip keylessly, links the provenance from it, verifies the result, and uploads the bundle to the release. A monorepo runs the step once per tool withproject: github.com/owner/repo/<tool>. packslip create --project NAME --version X --out dist --url-base URL --source-repo URL --tag vX --bin NAME artifact...digests the artifacts, infers platforms from file names (path:os/arch[/libc]overrides,@variantfor a second build of one platform), and writes the signed bundle.--bin NAME=PATHnames an executable differently from its file,--url FILENAME=URLsets one artifact's URL,--prerelease,--channel,--notes-url,--no-sha512,--attested-by repackagerwith--evidence KIND[=DETAIL]. Add--key release.keyto sign with a key;--no-logskips Rekor.packslip keygen -o release.keywrites an Ed25519 secret seed (mode 0600) andrelease.pub.packslip verify BUNDLE [--artifact file...]verifies and exits 1 on any failure;--jsonprints the result. A keyless bundle is checked against the policy its project name implies, or against--identity,--identity-prefix, and--issuer; a key-signed bundle against--pubkey.--allow-unloggedaccepts a bundle with no log entry;--trusted-rootreplaces the embedded sigstore root. The same command verifies a release list.packslip show BUNDLEprints the statement.packslip releases --project NAME --sequence N --valid-for 30d --release URL=PATH... [--yank URL=REASON] [--security URL] --key release.keywrites a signed release list.packslip schema [--releases]prints the JSON schemas.