95 lines
2.6 KiB
Python
95 lines
2.6 KiB
Python
"""GPG signing and SHA-256 hashing utilities for release artifacts."""
|
|
|
|
import hashlib
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
def gpg_sign_file(file_path: Path, passphrase: str) -> Path:
|
|
"""Create a detached ASCII-armored GPG signature for a file.
|
|
|
|
Args:
|
|
file_path: Path to the file to sign.
|
|
passphrase: GPG key passphrase.
|
|
|
|
Returns:
|
|
Path to the generated .asc signature file.
|
|
|
|
Raises:
|
|
subprocess.CalledProcessError: If GPG signing fails.
|
|
FileNotFoundError: If the input file does not exist.
|
|
"""
|
|
if not file_path.exists():
|
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
|
|
sig_path = file_path.with_suffix(file_path.suffix + ".asc")
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".pass", delete=True) as passfile:
|
|
passfile.write(passphrase)
|
|
passfile.flush()
|
|
|
|
subprocess.run(
|
|
[
|
|
"gpg",
|
|
"--batch",
|
|
"--yes",
|
|
"--passphrase-file",
|
|
passfile.name,
|
|
"--detach-sign",
|
|
"--armor",
|
|
str(file_path),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
print(f"Signed: {sig_path}")
|
|
return sig_path
|
|
|
|
|
|
def sha256_hash_file(file_path: Path) -> tuple[str, Path]:
|
|
"""Compute SHA-256 hash of a file and write a .sha256 checksum file.
|
|
|
|
Args:
|
|
file_path: Path to the file to hash.
|
|
|
|
Returns:
|
|
Tuple of (hex digest, path to .sha256 file).
|
|
|
|
Raises:
|
|
FileNotFoundError: If the input file does not exist.
|
|
"""
|
|
if not file_path.exists():
|
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
|
|
sha256 = hashlib.sha256()
|
|
with open(file_path, "rb") as fh:
|
|
for chunk in iter(lambda: fh.read(8192), b""):
|
|
sha256.update(chunk)
|
|
|
|
digest = sha256.hexdigest()
|
|
hash_line = f"{digest} {file_path.name}"
|
|
|
|
hash_path = file_path.parent / f"{file_path.stem}.sha256"
|
|
hash_path.write_text(hash_line, encoding="ascii")
|
|
|
|
print(f"SHA-256 ({file_path.name}): {digest}")
|
|
return digest, hash_path
|
|
|
|
|
|
def sign_and_hash(file_path: Path, passphrase: str) -> tuple[Path, Path]:
|
|
"""Sign a file with GPG and generate its SHA-256 checksum.
|
|
|
|
Args:
|
|
file_path: Path to the artifact to sign and hash.
|
|
passphrase: GPG key passphrase.
|
|
|
|
Returns:
|
|
Tuple of (signature path, hash file path).
|
|
"""
|
|
sig_path = gpg_sign_file(file_path, passphrase)
|
|
_, hash_path = sha256_hash_file(file_path)
|
|
return sig_path, hash_path
|