dotnet/efcore · critical · Exception

Signature verification failed: {result.stderr.decode('utf-8'

Error message

Signature verification failed: {result.stderr.decode('utf-8')}

What it means

Raised by fetch_release_file when gpgv exits non-zero verifying Release.gpg against Release with the supplied keyring. gpgv is used deliberately (instead of gpg) to avoid gpg-agent/keyboxd issues on GnuPG 2.4+ hosts. A failure means the detached signature does not validate the Release file using the keys in the keyring.

Source

Thrown at eng/common/cross/install-debs.py:135

    release_gpg_url = f"{mirror}/dists/{suite}/Release.gpg"

    with tempfile.NamedTemporaryFile() as release_file, tempfile.NamedTemporaryFile() as release_gpg_file:
        await download_file(session, release_url, release_file.name)
        await download_file(session, release_gpg_url, release_gpg_file.name)

        print("Verifying signature of Release with Release.gpg.")
        # Use gpgv rather than gpg for verification. gpgv verifies a detached
        # signature against a fixed keyring without involving gpg-agent or
        # keyboxd, which makes it robust on hosts running GnuPG 2.4+ (e.g. Azure
        # Linux) where "gpg --keyring" routes through keyboxd and can fail.
        verify_command = ["gpgv"]
        if keyring:
            verify_command += ["--keyring", keyring]
        verify_command += [release_gpg_file.name, release_file.name]
        result = subprocess.run(verify_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        if result.returncode != 0:
            raise Exception(f"Signature verification failed: {result.stderr.decode('utf-8')}")

        print("Signature verified successfully.")

        with open(release_file.name) as f:
            return f.read()

def parse_release_file(content, path):
    """Parses the Release file and returns sha256 checksum of the specified path."""

    # data looks like this:
    # <checksum>  <size>  <path>
    matches = re.findall(r'^ (\S*) +(\S*) +(\S*)$', content, re.MULTILINE)

    for entry in matches:
        # the file has both md5 and sha256 checksums, we want sha256 which has a length of 64
        if entry[2] == path and len(entry[0]) == 64:
            return entry[0]

View on GitHub (pinned to dbf9771522)

Solutions

  1. Install the matching archive keyring package for your distro/suite (e.g. debian-ports-archive-keyring, ubuntu-keyring, debian-archive-keyring) and point --keyring at it.
  2. Read {result.stderr} from the message - gpgv states 'NO_PUBKEY <keyid>' (missing key), 'BADSIG' (tampered), or 'EXPKEYSIG' (expired) which points to the exact cause.
  3. Sync the system clock (NTP) so signature time validity is evaluated correctly.
  4. If you intentionally trust the source, drop --force-check-gpg / pass --skipsigcheck to build-rootfs.sh rather than ignoring the failure.

Example fix

# before
python install-debs.py --force-check-gpg --keyring /usr/share/keyrings/debian-archive-keyring.gpg --mirror http://ftp.ports.debian.org/debian-ports ...
# after (ports mirror needs the ports keyring)
apt-get install debian-ports-archive-keyring
python install-debs.py --force-check-gpg --keyring /usr/share/keyrings/debian-ports-archive-keyring.gpg --mirror http://ftp.ports.debian.org/debian-ports ...
Defensive patterns

Strategy: validation

Validate before calling

# Validate the keyring actually contains the suite's signing key before calling
import subprocess, sys

def keyring_has_key(keyring, keyid):
    if not keyring or not __import__('os').path.exists(keyring):
        raise RuntimeError(f"Keyring missing or not found: {keyring!r}")
    out = subprocess.run(['gpgv', '--keyring', keyring, '--list-packets', '/dev/null'],
                         capture_output=True)  # smoke: gpgv accepts the keyring
    # Better: dump keys with gpg --no-default-keyring --keyring <keyring> --list-keys
    res = subprocess.run(['gpg', '--no-default-keyring', '--keyring', keyring, '--list-keys', '--with-colons'],
                         capture_output=True, text=True)
    if keyid not in res.stdout:
        raise RuntimeError(f"Keyring {keyring} lacks key {keyid}; install the right archive-keyring package")
    return True

Try / catch

# Signature failures are security-critical; do not retry silently
try:
    content = await fetch_release_file(session, mirror, suite, keyring)
except Exception as e:
    msg = str(e)
    if "Signature verification failed" in msg:
        if "NO_PUBKEY" in msg:
            hint = "Keyring is missing the signing key - install the matching archive-keyring package."
        elif "EXPKEYSIG" in msg or "EXP" in msg:
            hint = "Signing key expired - update the keyring package."
        elif "BADSIG" in msg:
            hint = "Release file tampered with - treat as integrity incident."
        else:
            hint = "Check system clock and keyring path."
        raise SystemExit(f"{msg}\nHint: {hint}")
    raise

Prevention

When it happens

Trigger: Running with --force-check-gpg and a --keyring that does not contain the signing key for the suite; the Release file has been modified after signing; the signing key is expired/revoked; the keyring path is wrong/empty; system clock is far off so signature validity windows are misjudged.

Common situations: Forgot to install the distro keyring package; passed an Ubuntu keyring against a Debian(-ports) mirror or vice versa; used an outdated keyring whose archive-signing key has rotated/expired; mis-typed the --keyring path; NTP off so gpgv rejects a not-yet-valid or expired signature.

Related errors


AI-assisted analysis of dotnet/efcore@dbf9771522 (2026-08-06). Data as JSON: /api/errors/412afeb8d4adc786. Report an issue: GitHub.