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
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:
parent
1ef6fedeaf
commit
640d4693ac
5 changed files with 35 additions and 26 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
Google Cloud KMS Ed25519 signing can only sign small payloads directly. Sparkle
|
Google Cloud KMS Ed25519 signing can only sign small payloads directly. Sparkle
|
||||||
requires a standard EdDSA signature over the full update archive, so normal
|
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
|
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")
|
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
|
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():
|
if key_path is None or not key_path.is_file():
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
"Sparkle artifact is too large for direct Google KMS Ed25519 signing "
|
"Sparkle artifact is too large for direct Google KMS Ed25519 signing "
|
||||||
"and SPARKLE_EDDSA_KEY_PATH does not point to a key file"
|
"and SPARKLE_EDDSA_KEY_PATH does not point to a key file"
|
||||||
)
|
)
|
||||||
result = run([
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
args.sign_update,
|
|
||||||
"--ed-key-file",
|
seed = read_sparkle_seed(key_path)
|
||||||
str(key_path),
|
signature = Ed25519PrivateKey.from_private_bytes(seed).sign(payload.read_bytes())
|
||||||
"-p",
|
return base64.b64encode(signature).decode("ascii")
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def sparkle_signature(args: argparse.Namespace, payload: Path) -> str:
|
def sparkle_signature(args: argparse.Namespace, payload: Path) -> str:
|
||||||
if args.ed_key_path:
|
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:
|
if payload.stat().st_size <= args.kms_max_input_bytes:
|
||||||
return kms_sign(args, payload)
|
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:
|
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("--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("--gcloud", default=os.environ.get("GCLOUD", "gcloud"))
|
||||||
parser.add_argument("--ed-key-path", default=os.environ.get("SPARKLE_EDDSA_KEY_PATH"))
|
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)
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ a different distribution certificate.
|
||||||
Sparkle appcast signing keeps the `sparkle-ed25519` Google KMS key available
|
Sparkle appcast signing keeps the `sparkle-ed25519` Google KMS key available
|
||||||
for payloads small enough for direct KMS Ed25519 signing, but normal macOS
|
for payloads small enough for direct KMS Ed25519 signing, but normal macOS
|
||||||
release archives are larger than Google KMS accepts for direct Ed25519
|
release archives are larger than Google KMS accepts for direct Ed25519
|
||||||
messages. Those archives are signed with Sparkle's `sign_update` using the
|
messages. Those archives are signed directly from the decrypted release
|
||||||
decrypted release `SPARKLE_EDDSA_KEY_PATH` so Sparkle receives the standard
|
`SPARKLE_EDDSA_KEY_PATH` seed so Sparkle receives the standard
|
||||||
full-archive EdDSA signature it verifies at update time. macOS release builds
|
full-archive EdDSA signature it verifies at update time. macOS release builds
|
||||||
embed public EdDSA key
|
embed public EdDSA key
|
||||||
`uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=` and point Sparkle metadata at
|
`uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=` and point Sparkle metadata at
|
||||||
|
|
|
||||||
|
|
@ -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
|
and future signing-service work. It uses `EC_SIGN_ED25519` with software
|
||||||
protection level because Google KMS rejects Ed25519 for HSM protection. Google
|
protection level because Google KMS rejects Ed25519 for HSM protection. Google
|
||||||
KMS also rejects normal macOS release archives as direct Ed25519 messages, so
|
KMS also rejects normal macOS release archives as direct Ed25519 messages, so
|
||||||
the tester pipeline signs full-size Sparkle archives with Sparkle's
|
the tester pipeline signs full-size Sparkle archives directly from the
|
||||||
`sign_update` and the decrypted `SPARKLE_EDDSA_KEY_PATH` release secret until
|
decrypted `SPARKLE_EDDSA_KEY_PATH` release seed until a compatible KMS-backed
|
||||||
a compatible KMS-backed Sparkle signing service is introduced. macOS builds
|
Sparkle signing service is introduced. macOS builds embed public EdDSA key
|
||||||
embed public EdDSA key `uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=`.
|
`uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=`.
|
||||||
- Use Namespace labels for platform lanes:
|
- Use Namespace labels for platform lanes:
|
||||||
- `namespace-profile-linux-medium`
|
- `namespace-profile-linux-medium`
|
||||||
- `namespace-profile-macos-large`
|
- `namespace-profile-macos-large`
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@
|
||||||
pkgs.nodejs
|
pkgs.nodejs
|
||||||
else
|
else
|
||||||
pkgs.nodejs_20;
|
pkgs.nodejs_20;
|
||||||
|
python3WithCrypto = pkgs.python3.withPackages (pythonPackages: [
|
||||||
|
pythonPackages.cryptography
|
||||||
|
]);
|
||||||
optionalPackage = name: lib.optional (lib.hasAttr name pkgs) (lib.getAttr name pkgs);
|
optionalPackage = name: lib.optional (lib.hasAttr name pkgs) (lib.getAttr name pkgs);
|
||||||
googleKmsPkcs11Package =
|
googleKmsPkcs11Package =
|
||||||
if pkgs.stdenv.hostPlatform.isx86_64 && pkgs.stdenv.isLinux then
|
if pkgs.stdenv.hostPlatform.isx86_64 && pkgs.stdenv.isLinux then
|
||||||
|
|
@ -137,7 +140,7 @@ EOF
|
||||||
curl
|
curl
|
||||||
jq
|
jq
|
||||||
nodejsPkg
|
nodejsPkg
|
||||||
python3
|
python3WithCrypto
|
||||||
openssl
|
openssl
|
||||||
rsync
|
rsync
|
||||||
zip
|
zip
|
||||||
|
|
|
||||||
|
|
@ -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
|
`EC_SIGN_ED25519` and software protection level, for small signing probes and
|
||||||
future signing-service work. Google KMS rejects Ed25519 at HSM protection level
|
future signing-service work. Google KMS rejects Ed25519 at HSM protection level
|
||||||
and also rejects full-size macOS release archives as direct Ed25519 messages.
|
and also rejects full-size macOS release archives as direct Ed25519 messages.
|
||||||
The tester pipeline therefore signs normal Sparkle archives with Sparkle's
|
The tester pipeline therefore signs normal Sparkle archives directly from the
|
||||||
`sign_update` and the decrypted `SPARKLE_EDDSA_KEY_PATH` release secret so the
|
decrypted `SPARKLE_EDDSA_KEY_PATH` release seed so the appcast contains the
|
||||||
appcast contains the standard full-archive EdDSA signature Sparkle verifies.
|
standard full-archive EdDSA signature Sparkle verifies.
|
||||||
|
|
||||||
The public EdDSA key embedded in macOS release builds is:
|
The public EdDSA key embedded in macOS release builds is:
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue