diff --git a/Scripts/sparkle/sign-appcast-kms.py b/Scripts/sparkle/sign-appcast-kms.py index cad4048..0e22c7c 100755 --- a/Scripts/sparkle/sign-appcast-kms.py +++ b/Scripts/sparkle/sign-appcast-kms.py @@ -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) diff --git a/docs/DISTRIBUTION.md b/docs/DISTRIBUTION.md index ccbae19..7ef29f5 100644 --- a/docs/DISTRIBUTION.md +++ b/docs/DISTRIBUTION.md @@ -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 diff --git a/evolution/proposals/BEP-0009-release-infrastructure-and-store-upload-pipeline.md b/evolution/proposals/BEP-0009-release-infrastructure-and-store-upload-pipeline.md index f4d6229..3736319 100644 --- a/evolution/proposals/BEP-0009-release-infrastructure-and-store-upload-pipeline.md +++ b/evolution/proposals/BEP-0009-release-infrastructure-and-store-upload-pipeline.md @@ -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` diff --git a/flake.nix b/flake.nix index a3d4b59..bdaa590 100644 --- a/flake.nix +++ b/flake.nix @@ -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 diff --git a/infra/identity/README.md b/infra/identity/README.md index a36239a..b774578 100644 --- a/infra/identity/README.md +++ b/infra/identity/README.md @@ -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: