Sign Sparkle appcasts in Python
Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Successful in 3m4s
Build Rust / Cargo Test (push) Successful in 4m5s
Build Site / Next.js Build (push) Failing after 4s
Infra: OpenTofu / OpenTofu (grafana) (push) Successful in 4s
Lint Governance / BEP Metadata (push) Successful in 1s

This commit is contained in:
Conrad Kramer 2026-06-07 16:56:18 -07:00
parent 1ef6fedeaf
commit 640d4693ac
5 changed files with 35 additions and 26 deletions

View file

@ -3,7 +3,7 @@
Google Cloud KMS Ed25519 signing can only sign small payloads directly. Sparkle
requires a standard EdDSA signature over the full update archive, so normal
release artifacts use Sparkle's signing tool with the decrypted release key.
release artifacts sign directly from the decrypted release seed.
"""
from __future__ import annotations
@ -58,32 +58,39 @@ def kms_sign(args: argparse.Namespace, payload: Path) -> str:
return base64.b64encode(signature_path.read_bytes()).decode("ascii")
def sign_update(args: argparse.Namespace, payload: Path) -> str:
def read_sparkle_seed(key_path: Path) -> bytes:
key_data = key_path.read_bytes().strip()
if len(key_data) == 32:
return key_data
try:
decoded = base64.b64decode(key_data, validate=True)
except ValueError as exc:
raise ValueError(f"Sparkle key is not raw or base64 Ed25519 seed data: {key_path}") from exc
if len(decoded) != 32:
raise ValueError(f"Sparkle key must decode to a 32-byte Ed25519 seed: {key_path}")
return decoded
def sign_with_release_seed(args: argparse.Namespace, payload: Path) -> str:
key_path = Path(args.ed_key_path) if args.ed_key_path else None
if key_path is None or not key_path.is_file():
raise FileNotFoundError(
"Sparkle artifact is too large for direct Google KMS Ed25519 signing "
"and SPARKLE_EDDSA_KEY_PATH does not point to a key file"
)
result = run([
args.sign_update,
"--ed-key-file",
str(key_path),
"-p",
str(payload),
])
signature = result.stdout.strip().splitlines()[-1].strip()
if not signature:
raise ValueError(f"Sparkle sign_update produced no signature for {payload}")
return signature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
seed = read_sparkle_seed(key_path)
signature = Ed25519PrivateKey.from_private_bytes(seed).sign(payload.read_bytes())
return base64.b64encode(signature).decode("ascii")
def sparkle_signature(args: argparse.Namespace, payload: Path) -> str:
if args.ed_key_path:
return sign_update(args, payload)
return sign_with_release_seed(args, payload)
if payload.stat().st_size <= args.kms_max_input_bytes:
return kms_sign(args, payload)
return sign_update(args, payload)
return sign_with_release_seed(args, payload)
def enclosure_path(enclosure: ET.Element, artifact_dir: Path) -> Path:
@ -130,7 +137,6 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
parser.add_argument("--kms-max-input-bytes", type=int, default=DEFAULT_KMS_MAX_INPUT_BYTES)
parser.add_argument("--gcloud", default=os.environ.get("GCLOUD", "gcloud"))
parser.add_argument("--ed-key-path", default=os.environ.get("SPARKLE_EDDSA_KEY_PATH"))
parser.add_argument("--sign-update", default=os.environ.get("SPARKLE_SIGN_UPDATE", "sign_update"))
return parser.parse_args(argv)

View file

@ -33,8 +33,8 @@ a different distribution certificate.
Sparkle appcast signing keeps the `sparkle-ed25519` Google KMS key available
for payloads small enough for direct KMS Ed25519 signing, but normal macOS
release archives are larger than Google KMS accepts for direct Ed25519
messages. Those archives are signed with Sparkle's `sign_update` using the
decrypted release `SPARKLE_EDDSA_KEY_PATH` so Sparkle receives the standard
messages. Those archives are signed directly from the decrypted release
`SPARKLE_EDDSA_KEY_PATH` seed so Sparkle receives the standard
full-archive EdDSA signature it verifies at update time. macOS release builds
embed public EdDSA key
`uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=` and point Sparkle metadata at

View file

@ -145,10 +145,10 @@ The same change also makes the forge itself the release authority: release tags
and future signing-service work. It uses `EC_SIGN_ED25519` with software
protection level because Google KMS rejects Ed25519 for HSM protection. Google
KMS also rejects normal macOS release archives as direct Ed25519 messages, so
the tester pipeline signs full-size Sparkle archives with Sparkle's
`sign_update` and the decrypted `SPARKLE_EDDSA_KEY_PATH` release secret until
a compatible KMS-backed Sparkle signing service is introduced. macOS builds
embed public EdDSA key `uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=`.
the tester pipeline signs full-size Sparkle archives directly from the
decrypted `SPARKLE_EDDSA_KEY_PATH` release seed until a compatible KMS-backed
Sparkle signing service is introduced. macOS builds embed public EdDSA key
`uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=`.
- Use Namespace labels for platform lanes:
- `namespace-profile-linux-medium`
- `namespace-profile-macos-large`

View file

@ -61,6 +61,9 @@
pkgs.nodejs
else
pkgs.nodejs_20;
python3WithCrypto = pkgs.python3.withPackages (pythonPackages: [
pythonPackages.cryptography
]);
optionalPackage = name: lib.optional (lib.hasAttr name pkgs) (lib.getAttr name pkgs);
googleKmsPkcs11Package =
if pkgs.stdenv.hostPlatform.isx86_64 && pkgs.stdenv.isLinux then
@ -137,7 +140,7 @@ EOF
curl
jq
nodejsPkg
python3
python3WithCrypto
openssl
rsync
zip

View file

@ -85,9 +85,9 @@ Sparkle appcasts keep `sparkle-ed25519`, a Google KMS key with algorithm
`EC_SIGN_ED25519` and software protection level, for small signing probes and
future signing-service work. Google KMS rejects Ed25519 at HSM protection level
and also rejects full-size macOS release archives as direct Ed25519 messages.
The tester pipeline therefore signs normal Sparkle archives with Sparkle's
`sign_update` and the decrypted `SPARKLE_EDDSA_KEY_PATH` release secret so the
appcast contains the standard full-archive EdDSA signature Sparkle verifies.
The tester pipeline therefore signs normal Sparkle archives directly from the
decrypted `SPARKLE_EDDSA_KEY_PATH` release seed so the appcast contains the
standard full-archive EdDSA signature Sparkle verifies.
The public EdDSA key embedded in macOS release builds is: