Skip to content

Require a signed tag, and show the badge that was earned - #47

Merged
ivanvyd merged 1 commit into
mainfrom
feat/require-signed-tags
Aug 6, 2026
Merged

Require a signed tag, and show the badge that was earned#47
ivanvyd merged 1 commit into
mainfrom
feat/require-signed-tags

Conversation

@ivanvyd

@ivanvyd ivanvyd commented Aug 6, 2026

Copy link
Copy Markdown
Owner

The gap this closes

The provenance attestation answers a narrower question than it appears to. It proves what built
an artifact — this workflow, this repository, this commit. It proves nothing about who authorised
the release
: anyone able to push a tag starts the workflow, and the attestation on the result
would be perfectly valid.

For a one-maintainer project the realistic supply-chain attack is a stolen GitHub account. Until
now, every link in the chain lived inside GitHub — branch protection, the environment gate, the
attestation. A signing key does not.

The release workflow now verifies the tag's SSH signature against .github/allowed_signers before
it builds anything.

Why it was nearly free

The key already existed. Commits in this repository are already SSH-signed with
~/.ssh/id_ed25519.pub; only tag.gpgSign was unset. No GPG, no keyring, no second key to manage.

Verified in both directions, before committing

signed tag   → Good "git" signature for ivan.vydrin.99@gmail.com with ED25519 key SHA256:Xrjfz…
unsigned tag → correctly REJECTED

The workflow step reproduces exactly that check and fails with an error naming the file to fix.

What RELEASING.md now says plainly

Two things a reader should know before relying on this:

  • A lost key blocks releases until a new public key is committed to .github/allowed_signers.
    The control is only as available as the key.
  • It is not absolute. Someone holding the GitHub account could open a PR removing the
    verification step and merge it. Signing makes that a multi-step attack recorded in git history
    rather than a single silent tag push. That is the realistic protection available, and claiming
    more would be false.

Also: the OpenSSF Best Practices badge

The project reached passing at 100% (project 13969).
Three criteria are deliberately recorded as unmet with justification rather than ticked: no
code-coverage metric is published, and no dynamic analysis or fuzzing is performed. Several others
are honest N/A — the project stores no passwords, generates no keys, and is written in a
memory-safe language.

Sequencing

This must merge before v0.1.0 is tagged. Once merged, git tag -a v0.1.0 signs automatically
(the local config is set) and the workflow will verify it. Tagging first would produce an unsigned
tag that this check then rejects.

Verification

229 tests green. release.yml parses as valid YAML with the new step second in the build job,
correctly gated on startsWith(github.ref, 'refs/tags/v') so manual rehearsal runs are unaffected.
All relative links resolve.

The provenance attestation answers a narrower question than it appears to.
It proves what built an artifact -- this workflow, this repository, this
commit -- and nothing about who authorised the release. Anyone able to push
a tag starts the workflow, and the attestation on the result is perfectly
valid. For a one-maintainer project the realistic supply-chain attack is a
stolen GitHub account, and until now every link in the chain lived inside
GitHub.

A signing key does not. The release workflow now verifies the tag's
signature against .github/allowed_signers before it builds anything, so
GitHub access alone is no longer enough to publish.

This cost almost nothing to add because the key already existed: commits
are already SSH-signed with it, so tag.gpgSign was the only setting missing
and no second key has to be managed.

Verified in both directions before committing. A signed tag reports a good
signature for the expected key; an unsigned tag is refused. The workflow
step reproduces that check with an explicit error naming the file to fix.

RELEASING.md documents the two things a reader should know before relying
on it: a lost key blocks releases until a new public key is committed, and
an attacker holding the GitHub account could still open a pull request
removing the check -- which makes it a multi-step attack recorded in git
history rather than a silent tag push, not an impossibility.

Also adds the OpenSSF Best Practices badge to the README. The project
reached the passing level at 100%, with three criteria deliberately
recorded as unmet rather than claimed: no coverage metric is published, and
no dynamic analysis is performed.
@ivanvyd
ivanvyd merged commit 953cf46 into main Aug 6, 2026
10 checks passed
@ivanvyd
ivanvyd deleted the feat/require-signed-tags branch August 6, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant