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:…

Vaidehi: The Sealed Covenant

**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

  1. The council reuses one nonce R across every revision in a document session, which is the classic Schnorr nonce-reuse hole.
  2. Revisions 1 and 2 are genuine seals sharing that R. From s1 - s2 = (e1 - e2)*x, recover the aggregate secret x, then the nonce k.
  3. Sign the forbidden revision 7 with x and k, reusing the same R, and submit to ratify it.

Read this post on CyberHx →