GitoxideLabs/gitoxide · error
Commit at has an invalid or untrusted signature
Error message
Commit at {rev_spec} has an invalid or untrusted signature What it means
`verify_commit` ran GPG verification of the commit's signature; either the signature check completed but the result was invalid/untrusted (`outcome.is_valid() == false`), after stderr already received gpg's raw output. Distinguishes from the unsigned case, which raises a separate error.
Solutions
- Import the signer's public key (`gpg --import` or from a keyserver) and re-verify
- Check for expired/revoked keys and refresh (`gpg --refresh-keys`)
- Inspect the gpg output on stderr, which this function writes before failing, for the exact failure reason
- If the signature is genuinely bad, treat the commit as untrusted — do not bypass
Example fix
// before
verify(repo, rev_spec, &mut out)?;
// after
match verify(repo, rev_spec, &mut out) {
Ok(()) => println!("signature OK"),
Err(e) if e.to_string().contains("invalid or untrusted signature") => {
eprintln!("commit signature invalid — fetch signer key and retry");
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check the key exists locally
let out = std::process::Command::new("gpg").args(["--list-keys"]).output()?;
if !out.status.success() { anyhow::bail!("gpg keyring not initialized"); } Try / catch
match verify(repo, rev_spec, &mut err) {
Ok(()) => {},
Err(e) if e.to_string().contains("is not signed") => eprintln!("commit unsigned"),
Err(e) => return Err(e.into_error()),
} Prevention
- Import contributor signing keys ahead of verification
- Run gpg with an accessible TTY or loopback pinentry in CI
- Refresh keys periodically to avoid expiry-based failures
When it happens
Trigger: Calling `verify` on a commit whose signature fails cryptographic validation, was made with an unknown/expired/revoked key, or whose trust level is insufficient.
Common situations: Missing or expired GPG keys in the local keyring; signed commits from contributors whose keys aren't imported; verifying on a machine without `gpg` configured; pinentry issues in non-interactive CI.
Related errors
- Commit at is not signed
- Command failed
- must be written as Name
- Tried to use as tree, but was
- Tried to use as commit, but was
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/2cba2a14247ce688.
Report an issue: GitHub.
Appendix: source
Thrown at gitoxide-core/src/repository/commit.rs:28
objs::commit::SIGNATURE_FIELD_NAME,
};
/// Note that this is a quick implementation of commit signature verification that ignores a lot of what
/// git does and can do, while focussing on the gist of it.
/// For this to go into `gix`, one will have to implement many more options and various validation programs.
pub fn verify(repo: gix::Repository, rev_spec: Option<&str>) -> Result<()> {
let rev_spec = rev_spec.unwrap_or("HEAD");
let commit = repo
.rev_parse_single(format!("{rev_spec}^{{commit}}").as_str())?
.object()?
.into_commit();
let outcome = commit
.verify_signature()
.context("Could not verify commit signature")?
.ok_or_else(|| anyhow!("Commit at {rev_spec} is not signed"))?;
std::io::stderr().write_all(&outcome.output)?;
if !outcome.is_valid() {
bail!("Commit at {rev_spec} has an invalid or untrusted signature");
}
Ok(())
}
/// Note that this is a quick first prototype that lacks some of the features provided by `git verify-commit`.
pub fn sign(repo: gix::Repository, rev_spec: Option<&str>, mut out: impl std::io::Write) -> Result<()> {
let rev_spec = rev_spec.unwrap_or("HEAD");
let object = repo
.rev_parse_single(format!("{rev_spec}^{{commit}}").as_str())?
.object()?;
let mut commit_ref = object.to_commit_ref();
if commit_ref.extra_headers().pgp_signature().is_some() {
gix::trace::info!("The commit {id} is already signed, did nothing", id = object.id);
writeln!(out, "{id}", id = object.id)?;
return Ok(());
}
let mut cmd: std::process::Command = gix::command::prepare("gpg").into();View on GitHub (pinned to e73179060b)