Verifying Commits¶
This guide covers how to verify Git commit signatures using Auths, including single-commit checks, full-history verification, identity bundle verification for CI, and integrating verification into CI pipelines.
How auths verify Works¶
The auths verify command is a unified entry point that detects whether you are verifying a Git commit or an attestation file. When given a Git ref, commit hash, or range, it performs commit verification. When given a file path or - for stdin, it performs attestation verification.
Commit verification is KEL-native — there is no key list file to maintain. For each commit, auths verify:
- Reads the SSH signature embedded in the Git commit object
- Reads the
Auths-Devicetrailer (the signer'sdid:keri:identifier) - Resolves the signer's key state from their key event log (KEL) in the local identity store — or, opt-in, from a git remote (
--remote) or an OOBI HTTP endpoint (--oobi) - Verifies the signature cryptographically against the resolved key
- Optionally verifies the attestation chain (when
--identity-bundleis provided) - Optionally enforces witness quorum (
--require-witnesses,--witness-signatures)
Resolution is local-only by default (no network). A remote can only advance a signer's key state, never roll it back — the local store stays the trusted floor.
Who Verifies Green: Self-Trust and Pinned Roots¶
A cryptographically valid signature still needs a trust decision about the root identity behind it. Three sources make a root trusted:
- Self-trust — your own identity is always trusted for your own verifications. Commits and artifacts you signed verify on your machine with zero setup.
- The committed
.auths/rootsfile — the repo's trust declaration, seeded automatically by the first signed commit and shared with everyone who clones. - An identity bundle (
--identity-bundle) — trusted for that verification only.
A valid signature from a root in none of these fails with Root … is not a pinned
trusted root. Pin the signer (auths trust pin --did <did> or add the DID to
.auths/roots) and re-verify.
"Commit carries no Auths-Id/Auths-Device trailer"¶
This means the commit message lacks the identity trailers verification replays. The
prepare-commit-msg hook installed by auths init adds them on every commit. If a
repository sets its own core.hooksPath (hook managers like husky do), the hook is
bypassed there — run auths doctor to detect this. Commits made before the hook
existed can be backfilled with auths sign <ref> (note: it amends, so the SHA
changes; never backfill already-pushed commits without coordinating).
Verifying a Single Commit¶
Verify the latest commit:
This defaults to verifying HEAD. You can specify any commit ref or hash:
If the signer's KEL is not in your local store (e.g. a teammate's commit), fetch it from the repository's remote:
Output¶
On success:
On failure:
JSON Output¶
For machine-readable output, use the --json flag:
Returns a JSON object per commit:
(error and warnings fields appear when relevant.)
Verifying a Commit Range¶
Verify all commits in a range:
This resolves the range using git rev-list and verifies each commit individually, one line per commit. Exit code 0 means all commits are valid. Exit code 1 means at least one commit is invalid or unsigned.
Verifying Full Repository History¶
To verify all commits from the initial commit to HEAD:
# Find the root commit
ROOT=$(git rev-list --max-parents=0 HEAD)
# Verify every commit
auths verify "${ROOT}..HEAD"
For large repositories, this may take time since each commit is verified individually.
Identity Bundle Verification (Stateless / CI)¶
For CI environments that do not have access to identity repositories, you can verify against an identity bundle. The bundle contains the identity's public key and attestation chain, enabling stateless verification.
The signer exports a bundle:
The verifier uses it:
When an identity bundle is provided:
- The SSH signature is verified against the bundle's public key
- The attestation chain in the bundle is cryptographically verified
- Bundle freshness is checked (bundles have a
max_valid_for_secsTTL) - Attestation expiry warnings are emitted if any attestation expires within 30 days
Identity Bundle Format¶
An identity bundle is a JSON file with this structure:
{
"identity_did": "did:keri:E...",
"public_key_hex": "abcdef...",
"attestation_chain": [...],
"bundle_timestamp": "2026-01-01T00:00:00Z",
"max_valid_for_secs": 86400
}
Witness Verification¶
Witnesses provide additional assurance by countersigning attestations. To verify witness receipts alongside commit signatures:
auths verify HEAD \
--identity-bundle bundle.json \
--witness-signatures receipts.json \
--witnesses-required 2 \
--witness-keys "did:key:z6Mk...:abcd1234..."
The --witnesses-required specifies how many witness signatures must be valid. If the quorum is not met, verification fails. Pass --require-witnesses to fail closed when the signer's root KEL has not reached witness quorum (the default is to warn and continue).
CI Integration¶
GitHub Actions¶
Use the Auths verify action to block PRs with unsigned commits:
name: Verify Signatures
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history required for commit verification
- uses: auths-dev/verify@v1
with:
auths-version: '0.1.2' # pin — a verifier must not resolve `latest`
identity-bundle: '.auths/ci-bundle.json' # omit for KEL-native verification
fail-on-unsigned: 'true'
The action runs auths verify across the PR's commit range, writes a results table to the GitHub Step Summary, and fails the check if any commit is unsigned. The fetch-depth: 0 is required — shallow clones do not contain the commit objects needed for signature extraction.
To run the CLI directly instead:
GitLab CI¶
verify-signatures:
stage: test
script:
- cargo install auths-cli
- auths verify origin/main..HEAD --identity-bundle .auths/ci-bundle.json
variables:
GIT_DEPTH: 0
Exit Codes¶
The exit-code contract (also in auths verify --help):
| Code | Meaning |
|---|---|
0 |
Verified |
1 |
Verification failed — bad signature, missing trailers, or an untrusted/unresolvable signer |
2 |
Could not attempt — I/O error, malformed input, missing repository |
Using the Audit Command¶
For compliance reporting, the auths audit command generates structured reports of signing status across a repository:
# Table format (default)
auths audit --repo . --since 2026-01-01
# JSON format for CI processing
auths audit --repo . --format json --require-all-signed --exit-code
# CSV for spreadsheet import
auths audit --repo . --format csv -o audit-report.csv
# HTML report
auths audit --repo . --format html -o audit-report.html
Audit options:
| Flag | Purpose |
|---|---|
--since |
Start date (YYYY-MM-DD or YYYY-QN for quarter) |
--until |
End date (YYYY-MM-DD) |
--author |
Filter by author email |
--signer |
Filter by signing identity/device DID |
-n / --count |
Maximum number of commits (default: 100) |
--require-all-signed |
Require all commits to be signed |
--exit-code |
Return exit code 1 if any unsigned commits found |
Verifying Attestation Files¶
The auths verify command also verifies attestation JSON files:
# Verify an attestation file
auths verify attestation.json --signer-key abcdef1234...
# Verify from stdin
cat attestation.json | auths verify - --signer did:keri:E...
Verification Library (auths-verifier)¶
For programmatic verification in your own tools, the auths-verifier crate provides the underlying verification functions. It is designed to be lightweight and embeddable, with support for FFI and WASM.
use auths_verifier::{verify_chain, VerificationStatus};
let report = verify_chain(&attestations, &root_public_key).await?;
match report.status {
VerificationStatus::Valid => println!("Chain verified"),
VerificationStatus::Expired { at } => println!("Expired at {}", at),
VerificationStatus::InvalidSignature { step } => {
println!("Bad signature at step {}", step);
}
VerificationStatus::Revoked { at } => println!("Revoked"),
VerificationStatus::BrokenChain { missing_link } => {
println!("Missing link: {}", missing_link);
}
VerificationStatus::InsufficientWitnesses { required, verified } => {
println!("Witnesses: {}/{}", verified, required);
}
}
Capability-Scoped Verification¶
Verify that a device has a specific capability:
use auths_verifier::{verify_with_capability, Capability};
let report = verify_with_capability(&chain, Capability::SignCommit)?;
Troubleshooting¶
"Unknown identity ... and trust policy is 'explicit'"¶
The signer's identity is not in your trust store. Pin it explicitly:
Or fetch their KEL from the repository's remote:
"No signature found"¶
The commit was never signed. Verify the author's git config:
git config user.signingKey # should be auths:<alias>
git config commit.gpgSign # should be true
git config gpg.ssh.program # should be auths-sign
"GPG signatures not supported"¶
The commit uses a GPG signature instead of SSH. Auths only supports SSH signatures. Reconfigure Git:
"Shallow clone detected" in CI¶
Commit verification requires full commit objects. Add fetch-depth: 0 to your checkout step:
Next Steps¶
- Signing Configuration -- set up Git signing
- Team Workflows -- trust pinning and organization policies