Compare commits

...

2 Commits

Author SHA1 Message Date
Evan Rusackas
f32194a58b address review: reject key extraction when GPG verification fails 2026-04-24 15:33:13 -07:00
Evan Rusackas
f0ed8e34d3 chore(releasing): fix email parsing in verify_release.py
The release verification script was emitting "Warning: No email address
found in GPG verification output" and then falling back to "RSA/EDDSA
key verified, but Email not available for verification" — so the email
portion of the KEYS check was effectively never exercised.

Two underlying issues:

1. If the signer's public key isn't in the local keyring, `gpg --verify`
   exits with "Can't check signature: No public key" and prints only
   the key ID — no signature is actually verified. The script silently
   accepted that state.
2. Even when verification succeeds, modern gpg output puts the email
   on the `Good signature from "Name <email>"` line. The script only
   matched `issuer "..."`, which isn't always present.

Fix:

- Auto-import the Apache Superset KEYS file if gpg reports no public
  key, then re-run verification so the signature is genuinely checked.
- Match email from `Good signature from`, `aka`, and `issuer` lines in
  that order of reliability.
- Print the email that was found and, when no public key is available
  even after the import attempt, show the manual import command as a
  hint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 15:13:37 -07:00

View File

@@ -56,8 +56,33 @@ def verify_sha512(filename: str) -> str:
# Part 2: Verify RSA key - this is the same as running `gpg --verify {release}.asc {release}` and comparing the RSA key and email address against the KEYS file # noqa: E501
KEYS_URL = "https://downloads.apache.org/superset/KEYS"
def ensure_keys_imported() -> None:
"""Import the Apache Superset KEYS file into the local GPG keyring.
Without this, `gpg --verify` returns "No public key" and the signature
cannot actually be verified — only the key ID in the signature metadata
is visible.
"""
try:
keys = requests.get(KEYS_URL, timeout=30)
except requests.RequestException as exc:
print(f"Warning: could not fetch KEYS file for import: {exc}")
return
if keys.status_code != 200:
print(f"Warning: could not fetch KEYS file (HTTP {keys.status_code})")
return
subprocess.run( # noqa: S603
["gpg", "--import"], # noqa: S607
input=keys.content,
capture_output=True,
)
def get_gpg_info(filename: str) -> tuple[Optional[str], Optional[str]]:
"""Run the GPG verify command and extract RSA key and email address."""
"""Run the GPG verify command and extract RSA/EDDSA key and email address."""
asc_filename = filename + ".asc"
result = subprocess.run( # noqa: S603
["gpg", "--verify", asc_filename, filename], # noqa: S607
@@ -65,23 +90,55 @@ def get_gpg_info(filename: str) -> tuple[Optional[str], Optional[str]]:
)
output = result.stderr.decode()
# If no public key was available, import KEYS and retry so that
# `Good signature from "Name <email>"` appears in the output.
if "No public key" in output:
ensure_keys_imported()
result = subprocess.run( # noqa: S603
["gpg", "--verify", asc_filename, filename], # noqa: S607
capture_output=True, # noqa: S607
)
output = result.stderr.decode()
# If the signature was not actually verified, do not trust the key ID or
# email pulled from signature metadata — returning them would let the
# caller report the release as "verified" when GPG never validated it.
if result.returncode != 0 or "Good signature" not in output:
print("Warning: GPG could not verify the signature.")
if "No public key" in output:
print(
"Hint: public key is not in your keyring. Import it with:\n"
f" curl -s {KEYS_URL} | gpg --import"
)
return None, None
rsa_key = re.search(r"RSA key ([0-9A-F]+)", output)
eddsa_key = re.search(r"EDDSA key ([0-9A-F]+)", output)
email = re.search(r'issuer "([^"]+)"', output)
# Try multiple patterns — `Good signature from` is the most reliable
# source of the email; `issuer` is a fallback for older gpg output.
email_patterns = (
r'Good signature from ".*?<([^>]+)>"',
r'aka ".*?<([^>]+)>"',
r'issuer "([^"]+)"',
)
email_result: Optional[str] = None
for pattern in email_patterns:
match = re.search(pattern, output)
if match:
email_result = match.group(1)
break
rsa_key_result = rsa_key.group(1) if rsa_key else None
eddsa_key_result = eddsa_key.group(1) if eddsa_key else None
email_result = email.group(1) if email else None
key_result = rsa_key_result or eddsa_key_result
# Debugging:
if key_result:
print("RSA or EDDSA Key found")
else:
print("Warning: No RSA or EDDSA key found in GPG verification output.")
if email_result:
print("email found")
print(f"Email found: {email_result}")
else:
print("Warning: No email address found in GPG verification output.")