Use Sparkle signer for full appcasts
This commit is contained in:
parent
a84922f4f3
commit
1d45016a63
4 changed files with 63 additions and 17 deletions
|
|
@ -1,5 +1,10 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Sign Sparkle appcast enclosures with a Google Cloud KMS Ed25519 key."""
|
||||
"""Sign Sparkle appcast enclosures.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -20,13 +25,14 @@ DEFAULT_LOCATION = "global"
|
|||
DEFAULT_KEYRING = "burrow-identity"
|
||||
DEFAULT_KEY = "sparkle-ed25519"
|
||||
DEFAULT_VERSION = "1"
|
||||
DEFAULT_KMS_MAX_INPUT_BYTES = 65536
|
||||
|
||||
|
||||
def run(command: list[str]) -> None:
|
||||
subprocess.run(command, check=True)
|
||||
def run(command: list[str]) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(command, check=True, text=True, stdout=subprocess.PIPE)
|
||||
|
||||
|
||||
def kms_sign(args: argparse.Namespace, payload: Path) -> bytes:
|
||||
def kms_sign(args: argparse.Namespace, payload: Path) -> str:
|
||||
with tempfile.TemporaryDirectory(prefix="burrow-sparkle-kms.") as tmp:
|
||||
signature_path = Path(tmp) / "signature.bin"
|
||||
command = [
|
||||
|
|
@ -49,7 +55,33 @@ def kms_sign(args: argparse.Namespace, payload: Path) -> bytes:
|
|||
if args.gcloud_project:
|
||||
command.extend(["--project", args.gcloud_project])
|
||||
run(command)
|
||||
return base64.b64decode(signature_path.read_text(encoding="utf-8").strip())
|
||||
return base64.b64encode(signature_path.read_bytes()).decode("ascii")
|
||||
|
||||
|
||||
def sign_update(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
|
||||
|
||||
|
||||
def sparkle_signature(args: argparse.Namespace, payload: Path) -> str:
|
||||
if payload.stat().st_size <= args.kms_max_input_bytes:
|
||||
return kms_sign(args, payload)
|
||||
return sign_update(args, payload)
|
||||
|
||||
|
||||
def enclosure_path(enclosure: ET.Element, artifact_dir: Path) -> Path:
|
||||
|
|
@ -78,8 +110,7 @@ def sign_appcast(args: argparse.Namespace) -> None:
|
|||
|
||||
for enclosure in enclosures:
|
||||
artifact = enclosure_path(enclosure, artifact_dir)
|
||||
signature = kms_sign(args, artifact)
|
||||
enclosure.attrib[f"{{{SPARKLE_NS}}}edSignature"] = base64.b64encode(signature).decode("ascii")
|
||||
enclosure.attrib[f"{{{SPARKLE_NS}}}edSignature"] = sparkle_signature(args, artifact)
|
||||
|
||||
tree.write(appcast, encoding="utf-8", xml_declaration=True)
|
||||
print(f"signed {len(enclosures)} enclosure(s) in {appcast}")
|
||||
|
|
@ -94,7 +125,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|||
parser.add_argument("--kms-keyring", default=os.environ.get("BURROW_KMS_KEYRING", DEFAULT_KEYRING))
|
||||
parser.add_argument("--kms-key", default=os.environ.get("BURROW_SPARKLE_KMS_KEY", DEFAULT_KEY))
|
||||
parser.add_argument("--kms-version", default=os.environ.get("BURROW_SPARKLE_KMS_VERSION", DEFAULT_VERSION))
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,13 @@ pins iOS App Store profiles to that certificate. Existing App Store profiles
|
|||
with the same name are deleted and recreated if Apple reports that they contain
|
||||
a different distribution certificate.
|
||||
|
||||
Sparkle appcast signing uses the non-exportable `sparkle-ed25519` Google KMS
|
||||
key. macOS release builds embed public EdDSA key
|
||||
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
|
||||
full-archive EdDSA signature it verifies at update time. macOS release builds
|
||||
embed public EdDSA key
|
||||
`Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=` and point Sparkle metadata at
|
||||
`https://releases.burrow.net/sparkle/appcast.xml`. The appcast signer runs only
|
||||
when `BURROW_SPARKLE_SIGN_WITH_KMS=true` so unsigned local validation artifacts
|
||||
|
|
|
|||
|
|
@ -141,10 +141,14 @@ The same change also makes the forge itself the release authority: release tags
|
|||
sign the app/extension bundles with the patched `rcodesign` PKCS#11 path.
|
||||
- The signed iOS IPA is uploaded to App Store Connect, and the signed macOS
|
||||
ZIP is used as the Sparkle tester artifact.
|
||||
- Add a non-exportable Google KMS key named `sparkle-ed25519` for Sparkle
|
||||
appcast signatures. It uses `EC_SIGN_ED25519` with software protection level
|
||||
because Google KMS rejects Ed25519 for HSM protection. macOS builds embed
|
||||
public EdDSA key `Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=`.
|
||||
- Add a Google KMS key named `sparkle-ed25519` for small Sparkle signing probes
|
||||
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 `Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=`.
|
||||
- Use Namespace labels for platform lanes:
|
||||
- `namespace-profile-linux-medium`
|
||||
- `namespace-profile-macos-large`
|
||||
|
|
|
|||
|
|
@ -81,10 +81,13 @@ Its public key matches KMS key version
|
|||
|
||||
## Sparkle Appcast Signing
|
||||
|
||||
Sparkle appcasts use `sparkle-ed25519`, a non-exportable Google KMS key with
|
||||
algorithm `EC_SIGN_ED25519` and software protection level. Google KMS rejects
|
||||
Ed25519 at HSM protection level, so this key is intentionally separate from the
|
||||
HSM RSA keys used for Apple certificate CSRs and repository signing.
|
||||
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 public EdDSA key embedded in macOS release builds is:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue