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 (
gpgcommand) 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:
- Maintainer A builds the
pallet-rustpackage from source on their machine - Maintainer B independently builds the same package on a different machine (different CPU vendor, different location)
- Both compute the SHA-256 digest of the output image
- If the digests match, both sign — the artifact is proven reproducible
- 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."
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
stagexorganization — 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:
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 fordocker.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:
- Look up signatures at
https://sigs.stagex.tools - Verify all signatures against the stagex-keyring.pgp
- Refuse to pull if fewer than 2 valid signatures exist (Podman requires at least one valid signature by default, but StageX policy requires 2)
- 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
- StageX Signatures Repository — Browse all published signatures
- StageX MAINTAINERS File — Maintainer PGP fingerprints and hardware
- Container Signature Format Spec — Standard signature format
- Podman Signature Verification Docs — Policy.json format reference
- Keyoxide — Decentralized key verification
Next Steps
- Understanding Full-Source Bootstrapping — How StageX builds everything from 181 bytes of machine code
- Decentralized Multi-Sig Signing — Deep dive into StageX's trust model
- Reproducible Builds & Supply Chain Integrity — How deterministic builds make multi-sig verification possible