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
|
#!/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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -20,13 +25,14 @@ DEFAULT_LOCATION = "global"
|
||||||
DEFAULT_KEYRING = "burrow-identity"
|
DEFAULT_KEYRING = "burrow-identity"
|
||||||
DEFAULT_KEY = "sparkle-ed25519"
|
DEFAULT_KEY = "sparkle-ed25519"
|
||||||
DEFAULT_VERSION = "1"
|
DEFAULT_VERSION = "1"
|
||||||
|
DEFAULT_KMS_MAX_INPUT_BYTES = 65536
|
||||||
|
|
||||||
|
|
||||||
def run(command: list[str]) -> None:
|
def run(command: list[str]) -> subprocess.CompletedProcess[str]:
|
||||||
subprocess.run(command, check=True)
|
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:
|
with tempfile.TemporaryDirectory(prefix="burrow-sparkle-kms.") as tmp:
|
||||||
signature_path = Path(tmp) / "signature.bin"
|
signature_path = Path(tmp) / "signature.bin"
|
||||||
command = [
|
command = [
|
||||||
|
|
@ -49,7 +55,33 @@ def kms_sign(args: argparse.Namespace, payload: Path) -> bytes:
|
||||||
if args.gcloud_project:
|
if args.gcloud_project:
|
||||||
command.extend(["--project", args.gcloud_project])
|
command.extend(["--project", args.gcloud_project])
|
||||||
run(command)
|
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:
|
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:
|
for enclosure in enclosures:
|
||||||
artifact = enclosure_path(enclosure, artifact_dir)
|
artifact = enclosure_path(enclosure, artifact_dir)
|
||||||
signature = kms_sign(args, artifact)
|
enclosure.attrib[f"{{{SPARKLE_NS}}}edSignature"] = sparkle_signature(args, artifact)
|
||||||
enclosure.attrib[f"{{{SPARKLE_NS}}}edSignature"] = base64.b64encode(signature).decode("ascii")
|
|
||||||
|
|
||||||
tree.write(appcast, encoding="utf-8", xml_declaration=True)
|
tree.write(appcast, encoding="utf-8", xml_declaration=True)
|
||||||
print(f"signed {len(enclosures)} enclosure(s) in {appcast}")
|
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-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-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-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("--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)
|
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
|
with the same name are deleted and recreated if Apple reports that they contain
|
||||||
a different distribution certificate.
|
a different distribution certificate.
|
||||||
|
|
||||||
Sparkle appcast signing uses the non-exportable `sparkle-ed25519` Google KMS
|
Sparkle appcast signing keeps the `sparkle-ed25519` Google KMS key available
|
||||||
key. macOS release builds embed public EdDSA key
|
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
|
`Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=` and point Sparkle metadata at
|
||||||
`https://releases.burrow.net/sparkle/appcast.xml`. The appcast signer runs only
|
`https://releases.burrow.net/sparkle/appcast.xml`. The appcast signer runs only
|
||||||
when `BURROW_SPARKLE_SIGN_WITH_KMS=true` so unsigned local validation artifacts
|
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.
|
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
|
- The signed iOS IPA is uploaded to App Store Connect, and the signed macOS
|
||||||
ZIP is used as the Sparkle tester artifact.
|
ZIP is used as the Sparkle tester artifact.
|
||||||
- Add a non-exportable Google KMS key named `sparkle-ed25519` for Sparkle
|
- Add a Google KMS key named `sparkle-ed25519` for small Sparkle signing probes
|
||||||
appcast signatures. It uses `EC_SIGN_ED25519` with software protection level
|
and future signing-service work. It uses `EC_SIGN_ED25519` with software
|
||||||
because Google KMS rejects Ed25519 for HSM protection. macOS builds embed
|
protection level because Google KMS rejects Ed25519 for HSM protection. Google
|
||||||
public EdDSA key `Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=`.
|
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:
|
- Use Namespace labels for platform lanes:
|
||||||
- `namespace-profile-linux-medium`
|
- `namespace-profile-linux-medium`
|
||||||
- `namespace-profile-macos-large`
|
- `namespace-profile-macos-large`
|
||||||
|
|
|
||||||
|
|
@ -81,10 +81,13 @@ Its public key matches KMS key version
|
||||||
|
|
||||||
## Sparkle Appcast Signing
|
## Sparkle Appcast Signing
|
||||||
|
|
||||||
Sparkle appcasts use `sparkle-ed25519`, a non-exportable Google KMS key with
|
Sparkle appcasts keep `sparkle-ed25519`, a Google KMS key with algorithm
|
||||||
algorithm `EC_SIGN_ED25519` and software protection level. Google KMS rejects
|
`EC_SIGN_ED25519` and software protection level, for small signing probes and
|
||||||
Ed25519 at HSM protection level, so this key is intentionally separate from the
|
future signing-service work. Google KMS rejects Ed25519 at HSM protection level
|
||||||
HSM RSA keys used for Apple certificate CSRs and repository signing.
|
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:
|
The public EdDSA key embedded in macOS release builds is:
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue