Signing Model
There are two different questions a client can ask about a repository, and SOW answers them with two separate mechanisms. Confusing them is the most common source of “I signed it but dnf still complains”, so this page starts by pulling them apart.
Two independent trust chains
| Metadata signing | RPM package signing | |
|---|---|---|
| Question answered | “Is this index really from you, and unmodified?” | “Is this .rpm file really from you?” |
| Configured by | signing.rpm.metadata, signing.deb.metadata |
signing.rpm.packages |
| Produces | repodata/repomd.xml.asc, InRelease, Release.gpg |
an OpenPGP signature embedded in the package |
| Changes package bytes | no | yes |
| Client setting | dnf repo_gpgcheck=1, apt Signed-By |
dnf gpgcheck=1 |
| Available in Plain mode | no | yes, via create -S KEY |
They are configured separately and can be used separately. Metadata signing alone is usually the right starting point: it authenticates the whole index in one place and requires no change to the packages you received from upstream.
Managed metadata signing is controlled entirely by sow.yml. There is no CLI override, no --sign flag on build, and no way to sign one build differently from the next. That is deliberate — a repository’s signing identity is a property of the repository, not of the command that happened to update it.
Configuration
RPM and DEB metadata keys are declared separately, so you can use the same key for both (as above) or split them. Each metadata block accepts an optional passphrase reference alongside key.
With a metadata key configured, every build produces the signature files, including for an empty Dist:
- RPM, per architecture view:
repodata/repomd.xmlplus an ASCII-armoredrepodata/repomd.xml.asc - DEB, per Dist:
Releaseplus a clearsignedInReleaseand a detached armoredRelease.gpg
The clearsigned body of InRelease is identical to Release. Without a metadata key, neither signature file is generated at all — you get repomd.xml and Release and nothing else.
Four key-reference forms
A key reference is a URI, and the scheme decides who does the signing:
| Reference | Meaning | Signer |
|---|---|---|
keys/repo-signing.asc |
an ASCII-armored key at a path relative to the Workspace root | in-process Go signer |
file:///absolute/path.asc |
an ASCII-armored private key on disk | in-process Go signer |
env://VAR_NAME |
the armored key material in an environment variable | in-process Go signer |
agent://<fingerprint> |
a key held by the GPG agent in your environment | external gpg |
file:// and env:// need nothing installed — SOW signs metadata itself, which is why a repository with file:// metadata keys builds identically on macOS and inside a minimal container. agent:// delegates to your GPG agent, which is the right choice when the private key lives on a smartcard or must never touch a file. agent:// cannot be combined with a passphrase reference, because the agent owns that interaction.
A passphrase reference accepts a Workspace-relative path, file://, or env://; it
does not accept agent://.
Nothing secret is ever persisted. Configuration, SQLite, logs, JSON output, and error text hold only the reference string, the fingerprint, and the public verifier certificate. config show --all prints references and fingerprints, never key material. If a key reference is unresolvable or unusable for signing, config check says so before you run a build.
RPM package signing
Three modes:
| Mode | Behavior |
|---|---|
never |
keep the input bytes exactly as given |
fill |
sign when the package is unsigned or its signature is not trusted; keep the bytes when an existing signature verifies against trusted_keys |
always |
ensure the final package is validly signed by the configured key; keep the bytes if it already is, otherwise re-sign |
trusted_keys automatically includes the public half of the configured key. Without a key, never is the only legal mode. fill is the default when a key is present.
Trust rings are verified independently
For a signed RPM, SOW evaluates each retained single-key ring, the current policy ring, and the
combined trusted_keys ring independently. Every recognized OpenPGP signature packet must verify
inside the same candidate ring, and at least one verified path must authenticate the payload. SOW
never combines a packet accepted by one key with another packet accepted by a different single-key
ring to invent a retained signer.
This distinction matters during a deliberate dual-signing transition: the combined trusted ring may accept the package, while neither individual retained key is allowed to claim it alone. Historical CentOS OpenPGP v3/v4 signatures remain supported. All candidate rings share one signed-byte stream, so adding trusted keys changes authorization but does not multiply package reads.
Package signing always runs rpm --addsign or rpm --resign from your environment, on a
private staged copy. Your input file is never modified in place. After signing, SOW
re-parses the result and requires an embedded signature, unchanged signature-neutral
digest and NEVRA, and the exact configured public-key identity. The rpm and gpg
executables and a matching secret key in the GPG environment used by rpm are mandatory
for fill and always. A key reference identifies and verifies the signer; it does not
provision the secret key into that environment.
Because signatures embed a timestamp, signing is not reproducible — the same unsigned RPM signed twice gives different bytes. Re-adding a package you already added would therefore look like a content conflict. SOW avoids that with a signature-neutral payload digest: a SHA-256 over the immutable header and payload, excluding the RPM signature header. If the logical coordinate already exists and the neutral digest matches, and the existing object satisfies the current policy, SOW reuses the existing final bytes instead of signing again. Repeated add of the same package is a stable no-op.
That reuse is narrow on purpose. never requires a full byte match, since that mode promises to preserve input bytes. If the payload digest differs, or the stored object does not satisfy the current signing policy, it is a hard conflict — add will not quietly re-sign a package in place under an existing coordinate. There is no --replace; if re-signing changes the bytes, bump the release, or plan a proper key-rotation workflow.
Key changes make Dists dirty
A Dist’s Built configuration digest covers its format, canonical architectures, limit, exclude, and the frozen signing identity. Change a key reference or a fingerprint and the digest changes, so every affected Dist becomes dirty:
For a metadata key change, sow build signs the indexes with the new identity and
produces a new Generation.
RPM package bodies are immutable Package Objects. build does not silently re-sign an
existing object under the same coordinate. If current Desired RPMs do not satisfy the new
package-signing policy, build rejects the change. A staged rollover normally uses
fill, makes the new key current, and keeps the old public key in trusted_keys; existing
old-key packages retain their bytes while newly ingested packages use the new key. Remove
the old trust only after those package coordinates have been withdrawn or replaced by new
releases. Switching directly to always with a new key is valid only when every Desired
RPM is already signed by that key.
The exact public certificate identity of the current built metadata is recorded per Dist, and multiple certificate versions for the same primary fingerprint can coexist — so extending an expiry or adding a subkey does not invalidate what is already published.
Plain mode
Plain mode signs RPM package bodies only; it has no metadata signing. KEY is exactly 16, 40, or 64 hexadecimal characters, without an 0x prefix; it is normalized to uppercase and passed to rpm as the _gpg_name macro. Without --overwrite, only RPMs with no parseable embedded signature are signed. With it, every retained RPM is re-signed.
--sign-with requires at least one top-level RPM retained after --pigsty cleanup. A DEB-only directory, a missing rpm binary, or an unavailable key fails before anything public changes. Signing is an explicit slow path with necessary copy, signature-verification, and final-RPM parse reads; if interrupted, rerun from the current package directory rather than replaying a Plain journal. See Plain Flat Repositories.
What the client verifies
repo_gpgcheck=1 makes dnf verify repomd.xml.asc; gpgcheck=1 makes it verify each
package’s embedded signature. On the APT side, Signed-By makes apt verify InRelease.
Automated checks validate generated signatures directly. Complete signed Managed dnf/APT
acceptance must run with real clients in the target environment; see
Platforms & Integrations for the exact evidence.
sow check verifies every declared signature and file hash as part of its normal run, so a signing misconfiguration shows up before you ship rather than on a customer’s machine.
Next
- Sign Your Repository — generating a dedicated key and wiring up both chains
sow.ymlreference — the full signing schema and key reference grammar- Observability & Audit — how
checkproves the signatures