Package signing¶
Status: v0.18 — sigstore-real wired. Default builds keep the v0.9 deterministic stub envelope (cross-platform, hermetic, no extra deps). Real keyless signing via Fulcio + Rekor is opt-in via the
mty-pkg/sigstore-realcargo feature and now produces standard Sigstore Bundle JSON that external tooling (cosign, rekor-cli) can verify directly. Seedev/history/notes/SIGSTORE_V0_18_NOTES.mdfor the v0.18 cut-over rationale andCLEANUP_V0_10_NOTES.mdfor the original v0.10 mode-dispatch shape.
Goals¶
- Every
mty pkg publishproduces a signed bundle so consumers can verify that the bytes they download match what the publisher pushed. - The on-disk artifact shape is forward-compatible with sigstore so we can swap the stub identity for a Fulcio cert later without breaking downstream verifiers.
- Verification is integrated into
mty pkg fetch, so users never have to invoke the signing layer directly.
Modes¶
The [registry.signing] table in mighty.toml selects the mode:
[registry.signing]
mode = "keyless" # or "stub" (default), or "off"
oidc_issuer = "https://oauth2.sigstore.dev/auth" # keyless only; optional
| Mode | Behaviour | Requires |
|---|---|---|
stub (default) |
Deterministic SHA-256 envelope. Detects tampering; no identity guarantee. | nothing extra |
keyless |
Real Fulcio cert + Rekor transparency-log entry. | sigstore-real feature and ambient OIDC identity |
off |
Skip signing. No .sig / .bundle sidecars written. |
nothing |
When mode = "keyless" but the binary was built without
sigstore-real, the publish command degrades to stub and prints
a one-line note. This keeps mty pkg publish working on hosts
where the sigstore dep graph won't compile (Windows + NASM is the
historical headache, see below).
Artifacts¶
For a bundle <pkg>-<version>.tar.gz, signing produces two
sidecars (in both stub and keyless modes):
| File | Format | Purpose |
|---|---|---|
<pkg>-<version>.tar.gz.sig |
text, one key:value per line | Compact human-readable signature record. |
<pkg>-<version>.tar.gz.bundle |
JSON | Sigstore-shaped envelope (mediaType, messageSignature, verificationMaterial). |
In off mode neither sidecar is written.
.sig format¶
mty-sig/1
bundle-sha256:<64-hex>
identity:<hex-or-cert-thumbprint>
signed-at:<unix-secs-or-0>
sig:<hex>
mty-sig/1is the format-version header. The verifier rejects files without it.bundle-sha256is the SHA-256 of the bundle bytes.identity(stub) isSHA-256("mty-stub-id:<pkg>:<version>").identity(keyless) is the SHA-256 of the Fulcio cert's serialised contents (kept hashed so the.sigtext stays a fixed-width line).signed-atis0in stub mode for byte-for-byte determinism. In keyless mode it's the system clock at signing time (approximating the OIDC token'siatclaim).sig(stub) isSHA-256("mty-stub-sig:" || bundle_sha256_hex || ":" || identity_hex).sig(keyless) is the raw ECDSA-P256 signature returned by Fulcio + Rekor, hex-encoded.
.bundle format (JSON)¶
{
"mediaType": "application/vnd.mty.bundle.v0.10+json",
"messageSignature": {
"messageDigest": { "algorithm": "SHA2_256", "digest": "<hex>" },
"signature": "<hex>"
},
"verificationMaterial": {
"identity": "<hex>",
"mode": "stub" | "keyless"
}
}
The shape mirrors the sigstore "bundle" media type. The
verificationMaterial.mode field tells future tooling whether the
cryptographic material is binding (keyless) or advisory (stub).
Media type bumped from …v0.9+json to …v0.10+json so verifiers
can distinguish the new mode-aware envelopes from the v0.9 ones at
a glance.
API¶
use mty_pkg::publish::bundle;
use mty_pkg::signing::{sign_bundle, sign_bundle_with_mode, verify_bundle, SigningMode};
let outcome = bundle(repo_root)?;
// Legacy entry point — always stub. Kept for back-compat.
let signed = sign_bundle(&outcome)?;
// Mode-aware entry point — the new v0.10 API.
let signed = sign_bundle_with_mode(&outcome, SigningMode::Keyless)?;
verify_bundle(&outcome.bundle_path)?;
sign_bundle(outcome)is the v0.9 surface preserved for back-compat. Always uses [SigningMode::Stub].sign_bundle_with_mode(outcome, mode)honours the requested mode.Keylessdegrades toStubwhen thesigstore-realfeature isn't compiled in;Offwrites no sidecars.verify_bundle(path)reads the.sigenvelope, re-hashes the bundle, recomputes the stub signature (for stub mode), and confirms the JSON envelope agrees. ReturnsOk(())on success.
Wire-up in mty pkg publish¶
mty_pkg::commands::publish calls signing::sign_bundle_with_mode
with the mode taken from [registry.signing]. The signed artefacts
are included in the "auth required" message so users with no token
still see all four file paths to upload manually. The reported
mode in the message reflects what was actually used (e.g.
"signature path.sig (mode: Stub)" when keyless was requested but
the feature wasn't enabled).
Determinism¶
The stub signature is a pure function of (pkg_name, pkg_version,
bundle_bytes). Two identical bundles produce identical
.sig + .bundle sidecars byte-for-byte — relied on by
signing::tests::signing_is_deterministic_for_same_input.
The keyless signature is not deterministic (signed-at uses
the system clock; the Fulcio cert is freshly issued per invocation
with a fresh ECDSA keypair). Reproducible-build workflows that
need byte-identical artefacts should pin mode = "stub" or
mode = "off".
Verification on fetch (v0.11)¶
mty_pkg::fetch will call verify_bundle after downloading a
registry tarball. v0.10 ships the verify primitive but does not yet
gate fetch on it — that's tracked under the v0.11 plan in
KNOWN_ISSUES.md.
Why sigstore-real is a feature flag¶
The sigstore Rust crate pulls in:
tonic+ a Fulcio gRPC client- a Rekor OpenAPI client
aws-lc-rs(required byrustls-webpki/aws-lc-rsfor thecertfeature) — needs NASM at build time on Windows- on some hosts, an
openssl-systransitive
…which would noticeably slow the v0.10 build + complicate CI on Windows. Fresh-clone confirmation (2026-05-23):
cargo build -p mty-pkg --features sigstore-real
# fails on Windows without NASM:
# panicked at nasm_builder.rs:138: NASM command not found! Build cannot continue.
The v0.10 policy is: ship the shape + mode dispatch + degraded
fallback, gate real signing behind a feature flag so default
builds stay fast + cross-platform. Linux CI runners get NASM
trivially (apt install nasm); Windows users either install NASM
or stick with stub mode.
v0.10 cut-over status¶
| Sub-task | Status |
|---|---|
Mode dispatch (SigningMode::{Stub, Keyless, Off}) |
shipped |
[registry.signing] config plumbed through publish |
shipped |
| Real Fulcio cert exchange (feature-gated) | shipped (v0.18) |
| GitHub Actions OIDC token fetch (feature-gated) | shipped (v0.18) |
| Rekor transparency-log upload | shipped (v0.18) |
Standard Sigstore Bundle embedded in .bundle |
shipped (v0.18) |
| Structural verify of keyless envelopes (digest cross-check) | shipped (v0.18) |
Verify against Rekor entry on fetch (full crypto verify) |
v0.19 follow-up |
| Device-flow OAuth for local signing | v0.19 follow-up |
| SLSA provenance attestations | v0.19 follow-up |
| MSRV-compatible sigstore version pin | tracked — sigstore 0.14 + workspace MSRV 1.85 |
See dev/history/notes/SIGSTORE_V0_18_NOTES.md for the v0.18
implementation choices and v0.19 follow-ups.
v0.18: real keyless flow (OIDC + Fulcio + Rekor)¶
When [registry.signing] mode = "keyless" is set and the
binary was built with --features mty-pkg/sigstore-real, the
publish path executes the full sigstore keyless flow:
┌─────────────────────┐
│ mty pkg publish │
└──────────┬──────────┘
│ bundle bytes + bundle SHA-256
▼
┌─────────────────────────────────────────┐
│ 1. OIDC token fetch │
│ GET $ACTIONS_ID_TOKEN_REQUEST_URL │
│ ?audience=sigstore │
│ Authorization: Bearer $..._TOKEN │
│ → JWT bound to workflow identity │
└──────────┬──────────────────────────────┘
│ JWT
▼
┌─────────────────────────────────────────┐
│ 2. SigningContext::async_production() │
│ Loads the public-good sigstore TUF │
│ trust root (CTFE keys + Fulcio root │
│ + Rekor public key). │
└──────────┬──────────────────────────────┘
│ ctx
▼
┌─────────────────────────────────────────┐
│ 3. ctx.signer(IdentityToken) │
│ Generates fresh ECDSA-P256 keypair │
│ locally; POSTs CSR + pubkey to │
│ https://fulcio.sigstore.dev/api/v1/ │
│ signingCert │
│ → short-lived (~10 min) x509 cert │
│ bound to the OIDC identity (sub, │
│ email) and a SCT proving CT log │
│ inclusion. │
└──────────┬──────────────────────────────┘
│ session (privkey + cert)
▼
┌─────────────────────────────────────────┐
│ 4. session.sign(bundle_reader) │
│ a. SHA-256(bundle bytes) │
│ b. ECDSA-P256 sign(digest) │
│ c. POST {sig, cert PEM, digest hex} │
│ to https://rekor.sigstore.dev/ │
│ api/v1/log/entries (hashedrekord) │
│ d. Rekor returns TransparencyLog- │
│ Entry with log index, integrated │
│ time, signed inclusion-proof. │
└──────────┬──────────────────────────────┘
│ SigningArtifact
▼
┌─────────────────────────────────────────┐
│ 5. artifact.to_bundle() │
│ Assembles the standard Sigstore │
│ Bundle (sigstore-bundle.v0.2 JSON) │
│ with cert chain, DER signature, │
│ message digest, Rekor entry. │
└──────────┬──────────────────────────────┘
│ Bundle JSON
▼
┌─────────────────────────────────────────┐
│ 6. Write <pkg>.tar.gz.sig + .bundle │
│ .bundle embeds the full Sigstore │
│ Bundle under │
│ verificationMaterial.sigstoreBundle│
│ so cosign / rekor-cli can verify │
│ the artefact directly. │
└─────────────────────────────────────────┘
Example .bundle envelope (keyless mode)¶
{
"mediaType": "application/vnd.mty.bundle.v0.10+json",
"messageSignature": {
"messageDigest": { "algorithm": "SHA2_256", "digest": "<bundle-hex>" },
"signature": "<ecdsa-sig-hex>"
},
"verificationMaterial": {
"identity": "<fulcio-cert-thumbprint-hex>",
"mode": "keyless",
"sigstoreBundle": {
"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2",
"verificationMaterial": {
"x509CertificateChain": {
"certificates": [ { "rawBytes": "<leaf-cert-DER-base64>" } ]
},
"tlogEntries": [
{
"logIndex": "<rekor-index>",
"logId": { "keyId": "<rekor-pubkey-id>" },
"kindVersion": { "kind": "hashedrekord", "version": "0.0.1" },
"integratedTime": "<unix-secs>",
"canonicalizedBody": "<base64>",
"inclusionProof": { ... },
"inclusionPromise": { ... }
}
]
},
"messageSignature": {
"messageDigest": { "algorithm": "SHA2_256", "digest": "<base64>" },
"signature": "<DER-base64>"
}
}
}
}
Verify path (v0.18)¶
verify_bundle(<bundle>.tar.gz) runs the following for keyless envelopes:
- Recompute the SHA-256 of the bundle bytes off disk.
- Parse the
.sigtext +.bundleJSON; confirm the top-level digest + signature appear in the JSON envelope (back-compat cross-check). - Decode the embedded
verificationMaterial.sigstoreBundle.messageSignature.messageDigest.digestfrom base64 and confirm it matches the recomputed SHA-256. - When built with
--features sigstore-real, additionally check thattlogEntries[]is non-empty and that an x509 cert chain is present in the embedded bundle (structural — full Rekor inclusion-proof crypto verify lands in v0.19).
CI integration¶
For GitHub Actions to expose the OIDC endpoint to mty pkg
publish, the workflow needs the id-token: write permission:
permissions:
contents: write # to upload the GitHub Release
id-token: write # to mint an OIDC JWT for sigstore
jobs:
publish:
runs-on: ubuntu-latest # Linux: NASM available out of the box
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo build --release -p mty-cli --features mty-pkg/sigstore-real
- run: ./target/release/mty pkg publish
With the permission granted, $ACTIONS_ID_TOKEN_REQUEST_URL and
$ACTIONS_ID_TOKEN_REQUEST_TOKEN are populated automatically. Outside
of CI (or when those env vars are absent), sign_keyless degrades
to stub mode and reports SigningMode::Stub in the returned
SignedBundle. Device-flow OAuth for local signing is a v0.19
follow-up.
Testing¶
| Test file | What it covers |
|---|---|
crates/mty-pkg/src/signing.rs::tests::* |
Round-trip stub sign+verify, tamper detection, deterministic re-sign, mode parser, off-mode no-sidecar, keyless-degrades-to-stub. Hermetic — runs on default features. |
crates/mty-pkg/tests/signing_real.rs::* (default) |
Public-API contract: keyless request must not error on default builds; off mode skips sidecars. |
crates/mty-pkg/tests/signing_real.rs::keyless_round_trip_via_fulcio_and_rekor |
#[ignore]d + #[cfg(feature = "sigstore-real")]. Real network round-trip against the public Sigstore deployment. Run with cargo test -p mty-pkg --features sigstore-real -- --ignored on a Linux CI runner with ambient OIDC. |