Skip to content

Verifying Your First StageX Image

Target audience: Developers and security-auditors who want to verify StageX image authenticity Time to complete: ~15 minutes Goal: Pull a StageX image, verify its multi-signature attestations using GPG, and understand the quorum model.

Why This Tutorial?

In the Quick Start, you built a reproducible binary with StageX and verified that two independent builds produce the same digest. But how do you know the StageX image itself hasn't been tampered with?

The answer: every published StageX image is co-signed by at least two independent maintainers using PGP. Before any image is published, a quorum of maintainers must independently rebuild it, confirm the digest matches, and sign the result.

In this tutorial, you'll pull the pallet-rust image, check its digest, and verify the signatures against the StageX maintainer keyring. You'll see exactly how multi-party signing works in practice.


Prerequisites

  • Podman installed (or Docker — set ENGINE=docker)
  • GnuPG (gpg command) installed
  • Basic familiarity with PGP/GPG concepts

Step 1: Pull the StageX Image

Start by pulling the pallet-rust image — the same one you used in the Quick Start and CI Pipeline tutorials:

podman pull docker.io/stagex/pallet-rust@sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668

Output:

Trying to pull docker.io/stagex/pallet-rust@sha256:2fbe7...
Getting image source signatures
Copying blob sha256:...
Copying config sha256:...
Writing manifest to image destination
Storing signatures

Why pin by digest? Pinning by @sha256: (not by tag like :latest) ensures you get the exact image that was signed by maintainers. Tags can be reassigned; digests are cryptographic commitments.


Step 2: Check the Image Digest

Confirm you have the right image by checking its digest locally:

podman inspect docker.io/stagex/pallet-rust@sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668 --format '{{.Digest}}'

Output:

sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668

The digest matches the one you pinned — this confirms the image hasn't been modified since it was pulled. But the real question is: was this the image the maintainers built and signed?


Step 3: Understand the Multi-Sig Quorum Model

StageX uses a multi-party quorum model for trust. Here's how it works:

  1. Maintainer A builds the pallet-rust package from source on their machine
  2. Maintainer B independently builds the same package on a different machine (different CPU vendor, different location)
  3. Both compute the SHA-256 digest of the output image
  4. If the digests match, both sign — the artifact is proven reproducible
  5. At least 2 signatures are required before any image is published

This means:

"No artifact is considered trusted until at least two independent maintainers rebuild and verify it. Only then is it co-signed using PGP signatures."

StageX README

The quorum requirement is enforced automatically by the build system. The publish target in the StageX Makefile counts signatures and refuses to publish with fewer than 2:

signum="$$(ls -1 signatures/stagex/{stage}-{name}@sha256=$${{digest}} | wc -l)";
[ $${{signum}} -ge 2 ] || { echo "Error: Minimum signatures not met"; exit 1; };

A compromise would require simultaneously subverting two maintainers' independent build environments — across different hardware, different locations, and different verification processes.


Step 4: Fetch the Maintainer Keyring

Signatures are stored in the StageX Signatures repository. The repository includes a combined keyring of all active maintainer PGP public keys.

Download the keyring:

curl -LO https://codeberg.org/stagex/signatures/raw/branch/main/stagex-keyring.pgp

Import it into your local GPG keyring:

gpg --import stagex-keyring.pgp

Output (truncated):

gpg: key E90A401336C8AAA9: public key "Lance Vick <lance@lancevick.com>" imported
gpg: key DC4B7D1F52E0BA4D: public key "Anton Livaja <anton@antonlivaja.com>" imported
gpg: key 8E401478A3FBEF72: public key "Ryan Heywood <ryan@r6y.com>" imported
gpg: key B10116B8193F2DBD: public key "Danny Grove <danny@dannygrove.com>" imported
gpg: key 28E42797A19977AC: public key "Konstantinos Geles <conyel@conyel.com>" imported
gpg: key 2BDE9CDB6D0FAD15: public key "Matthew Brooks <matthew@matthewbrooks.com>" imported
gpg: key D5E8A1F82F3BDA9D: public key "Jakub Panek <panekj@panekj.dev>" imported
gpg: key 3FC71170CB9E8963: public key "Zoe Kron <zoe@zoe.team>" imported
gpg: Total number processed: 8
gpg:               imported: 8

The keyring includes all 8 active maintainers with their PGP keys at the time of writing. Each key is also available individually via:

  • WKD (Web Key Directory) — if the maintainer's domain supports it
  • Hagrid — the keys.openpgp.org keyserver
  • Keyoxide — some maintainers maintain self-certifying profiles

Security note: Verify you're downloading the keyring from the official repository URL. The signatures repo is hosted on Codeberg under the stagex organization — the same organization that maintains StageX itself. Cross-reference the fingerprints with the MAINTAINERS file in the StageX repo.


Step 5: Locate the Signatures

Each signed image has a directory in the signatures repository at:

stagex/<package>@sha256=<digest>/signature-<N>

For pallet-rust with the digest you pulled:

stagex/pallet-rust@sha256=2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668/
  signature-1
  signature-2
  ...

Each signature-<N> file is a binary PGP detached signature of a JSON payload in the Container Signature Format.

Let's download the signatures for this image:

# Create a directory for the signatures
mkdir -p pallet-rust-sigs
cd pallet-rust-sigs

# Download the signature directory listing to see what's available
# (or browse at https://codeberg.org/stagex/signatures/src/branch/main/stagex/)

You can browse the signatures at:

https://codeberg.org/stagex/signatures/src/branch/main/stagex/pallet-rust@sha256=2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668

Each signature file corresponds to one maintainer's attestation.


Step 6: Verify Signatures with GPG

Let's verify a signature file directly. First, reconstruct the JSON that was signed. The signature payload has this format:

{
  "critical": {
    "identity": {
      "docker-reference": "docker.io/stagex/pallet-rust"
    },
    "image": {
      "docker-manifest-digest": "sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668"
    },
    "type": "atomic container signature"
  },
  "optional": {}
}

To verify a binary signature against this JSON, use GPG's --verify with the --digest-algo flag:

# Assuming you downloaded signature-1
echo '{
  "critical": {
    "identity": {
      "docker-reference": "docker.io/stagex/pallet-rust"
    },
    "image": {
      "docker-manifest-digest": "sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668"
    },
    "type": "atomic container signature"
  },
  "optional": {}
}' > payload.json

gpg --verify signature-1 payload.json

If the signature is valid, you'll see something like:

gpg: Signature made Wed 01 Jan 2026 12:00:00 AM UTC
gpg:                using RSA key C92FE5A3FBD58DD3EC5AA26BB10116B8193F2DBD
gpg: Good signature from "Danny Grove <danny@dannygrove.com>"
gpg: WARNING: This key is not certified with a trusted signature!

The "Good signature" message confirms:

  • The JSON payload was signed by Danny Grove's PGP key (fingerprint ending in 3F2DBD)
  • The payload attests that the manifest digest sha256:2fbe7b1... is the correct digest for docker.io/stagex/pallet-rust

Repeat for each signature-<N> file to see how many maintainers have signed this image. Different signatures should come from different maintainers — for example, one from Danny Grove, one from Lance Vick, etc.

The WARNING: This key is not certified is normal — it just means you haven't personally signed Danny's key to establish a trust path. You can cross-reference the fingerprint against the MAINTAINERS file to manually verify it's the right key.


Step 7: Automated Verification with Podman

For day-to-day use, you don't need to manually verify signatures. Podman has built-in support for the Container Signature Format. Set it up once, and Podman verifies signatures on every pull.

Configure the Lookaside Server

Create ~/.config/containers/registries.d/default.yaml:

docker:
  docker.io/stagex:
    lookaside: https://sigs.stagex.tools
  quay.io/stagex:
    lookaside: https://sigs.stagex.tools

This tells Podman where to fetch signatures for StageX images.

Create a Verification Policy

Create ~/.config/containers/policy.json:

{
  "default": [
    {"type": "reject"}
  ],
  "transports": {
    "docker": {
      "docker.io/stagex": [
        {
          "type": "signedBy",
          "keyType": "GPGKeys",
          "keyPath": "/path/to/stagex-keyring.pgp",
          "signedIdentity": {
            "type": "matchRepoDigestOrExact"
          }
        }
      ],
      "quay.io/stagex": [
        {
          "type": "signedBy",
          "keyType": "GPGKeys",
          "keyPath": "/path/to/stagex-keyring.pgp",
          "signedIdentity": {
            "type": "remapIdentity",
            "prefix": "quay.io/stagex",
            "signedPrefix": "docker.io/stagex"
          }
        }
      ]
    }
  }
}

About the policy: - "default": [{"type": "reject"}] — refuse all images by default unless they match a transport rule - "signedBy" — require valid PGP signatures - "keyPath" — path to the stagex-keyring.pgp you downloaded - "matchRepoDigestOrExact" — verify that the image's repo/digest matches the signed identity

Test Verification

Now pull a StageX image:

podman pull docker.io/stagex/pallet-rust@sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668

If the image is already cached, force a re-check by removing it first:

podman rmi docker.io/stagex/pallet-rust@sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668
podman pull docker.io/stagex/pallet-rust@sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668

With the policy in place, Podman will:

  1. Look up signatures at https://sigs.stagex.tools
  2. Verify all signatures against the stagex-keyring.pgp
  3. Refuse to pull if fewer than 2 valid signatures exist (Podman requires at least one valid signature by default, but StageX policy requires 2)
  4. Refuse to pull if the image digest doesn't match what was signed

If verification fails, you'll see:

Error: Signature validation failed for docker.io/stagex/pallet-rust@sha256:...

If verification succeeds, the pull proceeds normally (this means the signatures checked out).


Step 8: Verify Locally Built Images

The same signature verification applies to images you build locally. After completing the Quick Start tutorial, you can verify your locally built binary matches a signed StageX image:

# Build your image
podman build -t hello-stagex:local .

# Record the digest
podman inspect hello-stagex:local --format '{{.Digest}}'

Then check if this digest exists in the published digests:

# Fetch the pallet digests
curl -s https://codeberg.org/stagex/stagex/raw/branch/main/digests/pallet.txt | grep pallet-rust

This shows you the digests that maintainers have signed. If your locally built digest matches a published one, you've independently reproduced the maintainer's build — the highest form of verification.


What You've Learned

Concept How StageX Handles It
Multi-sig quorum Minimum 2 maintainers must sign every artifact
Signature format PGP binary signatures of JSON Container Signature Format payload
Key distribution Combined keyring at stagex-keyring.pgp, individual keys on keys.openpgp.org
Podman verification Built-in support via policy.json and lookaside configuration
Verification methods Direct GPG verification or automated Podman policy
Chain of trust Cross-reference fingerprints with MAINTAINERS file and Keyoxide profiles

How Verification Prevents Attacks

Here's what each verification step protects against:

Attack How StageX Prevents It
Compromised registry Attacker replaces image but can't forge signatures from 2 maintainers
Man-in-the-middle Digest mismatch detected — signed payload binds image to specific digest
Maintainer key compromise Quorum of 2+ means attacker needs multiple keys
Malicious maintainer Other maintainers independently verify before co-signing
Backdoored compiler Full-source bootstrap chain makes hidden backdoors detectable

References

Next Steps