Vaidehi: The Sealed Covenant
By Team Cryptonite · 2 min read
About the CTF: Pinaka CTF
Forge a 3-of-3 MuSig Schnorr council seal for a forbidden revision. The signing session reuses nonces across revisions; two genuine seals yield the aggregate private key via nonce-reuse extraction. Validator:…
**Pinaka CTF 2026** · Cryptography · Team Cryptonite · All 44 Pinaka write-ups
Challenge
> Forge a 3-of-3 MuSig Schnorr council seal for a forbidden revision. The signing session reuses nonces across revisions; two genuine seals yield the aggregate private key via nonce-reuse extraction.
> Validator: https://pinaka-m05-cyber-xo-x.vercel.app/
We're given params.json, instance.json and pinaka_covenant.py.
It's a MuSig-style Schnorr multisignature over secp256k1, a 3-of-3 council. A revision is sealed by (R, s) and is valid for (docid, revision, body) when sG == R + eX, where X is the aggregate council key and e = H(R, X, docid, revision, body). We need a valid seal on a forbidden revision 7 that the council refused to sign, and we don't have any secret keys.
The weakness
params.json spells it out in session_rule: the council opens one signing session per document, and the pre-committed aggregate nonce R is fixed for that document and reused for every revision sealed in it.
That's fatal for Schnorr. Look at instance.json: revisions 1 and 2 are both genuine seals, and they have the identical R:
R.x = 78590942717428059269889991809638578455145461928612893688259669499439350188552
A Schnorr seal is s = k + e*x, where k is the nonce behind R and x is the aggregate secret. Two seals under the same k:
s1 = k + e1 * x
s2 = k + e2 * x
s1 - s2 = (e1 - e2) * x
So x = (s1 - s2) / (e1 - e2) mod n. That recovers the aggregate council private key outright. Once you have x, you can recover k = s1 - e1*x too, and then sign anything you want, including the forbidden revision.
Recovering the key
pinakacovenant.py is public arithmetic with no secrets in it, so I used its keyagg and challenge directly. First I ran its self-check to be sure the aggregate key and the two seals were real:
python3 pinaka_covenant.py
aggregate key reproduces params.aggregate_key: True
ratified revision 1 seal verifies: True
ratified revision 2 seal verifies: True
Then the extraction and the forge:
import pinaka_covenant as P
params = P.load_params("params.json")
inst = P.load_instance("instance.json")
X, _ = P.key_agg([m["pubkey"] for m in params["council"]])
N = P.N
doc_id = inst["doc_id"]
r1, r2 = inst["ratified_revisions"]
R = tuple(r1["seal"]["R"])
s1, s2 = r1["seal"]["s"], r2["seal"]["s"]
e1 = P.challenge(R, X, doc_id, r1["revision"], r1["body"])
e2 = P.challenge(R, X, doc_id, r2["revision"], r2["body"])
x = (s1 - s2) * pow(e1 - e2, -1, N) % N # aggregate secret
k = (s1 - e1 * x) % N # the reused nonce
t = inst["forbidden_target"]
e7 = P.challenge(R, X, doc_id, t["revision"], t["body"])
s7 = (k + e7 * x) % N
seal = {"R": list(R), "s": s7}
print("x*G == X:", P.mul(P.G, x) == X)
print("forged seal verifies:", P.verify_seal(X, doc_id, t["revision"], t["body"], seal))
print("s =", s7)
python3 forge.py
x*G == X: True
forged seal verifies: True
s = 94047503437372943715288006595592556461899117252581377777093910299313937128897
x*G == X is the proof the extraction worked: the recovered scalar really is the aggregate council secret. The reused R is kept as is, and the new s7 is signed over the revision-7 challenge.
Submitting
curl -X POST "https://pinaka-m05-cyber-xo-x.vercel.app/covenant/seal" \
-H "Content-Type: application/json" \
-d '{
"doc_id": "mithila-betrothal-covenant",
"revision": 7,
"body": "Revision 7: Mithila cedes Vaidehi and her dominion to the demon-king; the kingdom'\''s sovereignty passes to Lanka.",
"seal": {
"R": [78590942717428059269889991809638578455145461928612893688259669499439350188552, 86399799529526984061083292644132654262624075746664350706617581532843906706131],
"s": 94047503437372943715288006595592556461899117252581377777093910299313937128897
}
}'
{
"result": "accepted",
"revision": 7,
"flag": "PINAKA{one_session_one_nonce_the_council_seal_undone}",
"proclamation": "The sealed covenant the council refused now bears a valid seal."
}
Flag
PINAKA{one_session_one_nonce_the_council_seal_undone}
Summary
- The council reuses one nonce
Racross every revision in a document session, which is the classic Schnorr nonce-reuse hole. - Revisions 1 and 2 are genuine seals sharing that
R. Froms1 - s2 = (e1 - e2)*x, recover the aggregate secretx, then the noncek. - Sign the forbidden revision 7 with
xandk, reusing the sameR, and submit to ratify it.