packslip

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.

Specification

packslip: a signed release manifest

Version 1, draft. Predicate types https://packslip.dev/release/v1 and https://packslip.dev/releases/v1. The canonical text is docs/spec/packslip.md in jdx/packslip; this page renders it with a field reference added. Schemas: release-v1.json, releases-v1.json.

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:

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:

Field reference

FieldTypeMeaning
_typestringrequiredAlways https://in-toto.io/Statement/v1.
subject[]arrayrequiredOne entry per artifact. Mirrors predicate.artifacts by name.
subject[].namestringrequiredThe artifact's file name.
subject[].digest.sha256stringrequiredSHA-256 of the file, lowercase hex.
subject[].digest.sha512stringoptionalSHA-512 of the file, lowercase hex.
predicateTypestringrequiredAlways https://packslip.dev/release/v1.
predicate.projectstringrequiredHost path naming the project, such as github.com/jdx/mise or github.com/oxc-project/oxc/oxlint.
predicate.versionstringrequiredThe vendor's version string. Opaque; never parsed for ordering.
predicate.published_atstringrequiredRFC 3339 UTC publish time.
predicate.prereleasebooleanoptionalNot for general use. Default false.
predicate.channelstringoptionalRelease track: stable, beta, nightly.
predicate.version_orderstringoptionalsource (default: the release list's order) or semver.
predicate.sourceobjectoptionalWhere the release was built from.
predicate.source.repostringrequiredSource repository URL.
predicate.source.commitstringoptionalCommit the release was built from.
predicate.source.tagstringoptionalTag the release was built from, as the vendor spells it.
predicate.artifacts[]arrayrequiredOne entry per artifact; at least one.
artifacts[].namestringrequiredFile name, matching a subject entry.
artifacts[].osstringoptionallinux, darwin, windows, freebsd.
artifacts[].archstringoptionalx86_64, aarch64, armv7, riscv64, i686.
artifacts[].libcstringoptionalgnu or musl; Linux only.
artifacts[].variantstringoptionalDistinguishes builds sharing os/arch/libc/format: fips, baseline, debug, installer, source.
artifacts[].sizeintegerrequiredFile size in bytes. Verified alongside the digest.
artifacts[].urlstringoptionalDownload URL.
artifacts[].formatstringoptionalArchive or installer type, or raw for a bare executable.
artifacts[].bin[]array of string or objectoptionalExecutables inside the artifact: a path, or { path, name } when the PATH name differs.
artifacts[].requiresobjectoptionalos_min and glibc_min.
artifacts[].provenance[]array of stringoptionalURLs of SLSA build provenance statements for this artifact.
predicate.identity.schemestringrequiredsigstore-oidc or sigstore-key.
predicate.identity.key_idstringrequiredThe certificate identity, or the key id in uppercase hex.
predicate.identity.issuerstringoptionalThe OIDC issuer, for sigstore-oidc.
predicate.attested_bystringoptionalvendor (default) or repackager.
predicate.evidence[]array of objectoptionalWhat a repackager checked: { kind, detail }.
predicate.notes_urlstringoptionalURL of the release notes.
predicate.sbomstringoptionalURL of a software bill of materials for the release.
predicate.supersedesstringoptionalThe 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.

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:

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:

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

  1. 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.
  2. 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.
  3. Enforce no-downgrade: refuse a release whose identity.scheme is weaker than the last accepted one, whose signer changed without a human saying so, whose attested_by went 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.
  4. Apply any minimum release age to the log's integration time, falling back to published_at only for an unlogged bundle you chose to accept.
  5. Treat supersedes as the ordering hint for rollback detection.
  6. 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 sequence is below the last one accepted; never select a yanked entry; skip prereleases unless asked for them; order as version_order says.
  7. 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.