arrow_backUS Cyber Games 2026
Crypto / ECDSAhard

Twice Signed

by Pavel Knespl (capyplivl)

Halcyon Avionics' OTA signing host exports a P-256 ECDSA ledger, the release signing public key, and a sealed engineering build. Two ledger entries (HX-7 3.4.1 and 3.4.2) reuse the same ECDSA nonce k — identical r. Nonce reuse leaks the private signing key, and the build is sealed with AES-256-GCM under a key derived from that scalar.

Solve

  1. Two releases share r (3.4.1/3.4.2), so they signed with the same nonce k. The signed message is the ledger's signed_sha256 field (not image_sha256).
  2. With shared k, the two ECDSA equations s = k^-1(z + r·d) are linear in k and d:
k = (z1 - z2) * (s1 - s2)^-1 mod n
d = (s1*k - z1) * r^-1       mod n

Verify against the public key: d·G == Q. 3. The .sealed blob is not ECIES (no ephemeral point). Key is SHA256(d.to_bytes(32)); layout is nonce(12) || tag(16) || ciphertext (AES-256-GCM) — tag before the ciphertext, so naive nonce||ct||tag parsing fails. 4. solve.py (in the challenge dir) recovers d, derives the key, and opens the build:

import hashlib, json
from ecdsa import NIST256p, VerifyingKey
from Crypto.Cipher import AES

n, G = NIST256p.order, NIST256p.generator
led = json.load(open("halcyon_ota_ledger.json"))["releases"]
Q = VerifyingKey.from_pem(open("halcyon_release_signing.pub.pem").read()).pubkey.point

byr = {}
for e in led:
    byr.setdefault(e["signature"]["r"], []).append(e)
a, b = next(v for v in byr.values() if len(v) == 2)
r = int(a["signature"]["r"], 16)
s1, z1 = int(a["signature"]["s"], 16), int(a["signed_sha256"], 16)
s2, z2 = int(b["signature"]["s"], 16), int(b["signed_sha256"], 16)
k = ((z1 - z2) * pow((s1 - s2) % n, -1, n)) % n
d = ((s1 * k - z1) * pow(r, -1, n)) % n
assert d * G == Q

data = open("halcyon_eng_build_3.5.0-rc1.sealed", "rb").read()
key = hashlib.sha256(d.to_bytes(32, "big")).digest()
print(AES.new(key, AES.MODE_GCM, nonce=data[:12]).decrypt_and_verify(data[28:], data[12:28]).decode())
  1. The GCM tag verifies and prints the sealed manifest; the maintenance_unlock_token field holds the flag:
maintenance_unlock_token: SVIUSCG{n0nce_reuse_grounds_the_halcyon_fleet}

Flag: SVIUSCG{n0nce_reuse_grounds_the_halcyon_fleet}