NUT-00: add BLS12-381 (v3) protocol #371
No reviewers
Labels
No labels
breaking change
bug
documentation
enhancement
needs discussion
needs implementation
new nut
ready
wallet-only
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo-admin/nuts!371
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "robwoodgate/bls-protocol"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds Pairing-based BDHKE (BLS12-381) as the v3 Cashu blind-signature protocol for keysets with version byte
02, alongside the legacy secp256k1 protocol for00/01keysets.hash_to_curve_G1with the Cashu RFC 9380 DST, multiplicative blindingB_ = r·Y, blind signingC_ = a·B_, unblindingC = r^-1·C_, and verification bye(C, G2) == e(Y, K).Fr*, and explicit point-validation requirements to reject non-canonical encodings, identity points, off-curve points, and non-prime-order subgroup points before signing or verification.00/01keysets use compressed secp256k1 keys, while02keysets use compressed BLS12-381 G2 keys.02, G2 public keys in the preimage, lowercased units, and updated V3 test vectors.dleq; offline verification uses the pairing equality from NUT-00.BLS_FR_ORDERinstead of modular reduction.The JSON/wire shape of
BlindedMessage,BlindSignature,Proof, and TokenV4 remains unchanged; the keyset version selects the curve and byte widths forB_,C_,C, and mint public keys.ChatGPT Pro Review
1. Blocking: externally supplied BLS points need explicit subgroup validation
The new BLS section says compressed encodings are fixed-width and that infinity is never valid, but it does not explicitly require subgroup checks for
B_,C_,C_, andK. That is too loose for BLS12-381, because the serialized curve format validates curve points but does not, by itself, guarantee prime-order subgroup membership; the BLS draft definessubgroup_check(P)separately and requires public keys to be valid, non-identity points in the correct subgroup, andCoreVerifyperforms a signature subgroup check before pairing verification. ([IETF Datatracker][1])I’d add normative text along these lines in NUT-00:
This matters especially for
B_, because it is attacker-controlled input to the mint’s secret scalar multiplication.2. Blocking/high: batch “rejection sampling” is actually modulo reduction
The batch section says weights are derived “by rejection sampling,” but the pseudocode does:
and only rejects
r_i == 0. That is not rejection sampling; it is modulo reduction. With BLS12-381’s 255-bit scalar order and a 256-bit SHA-256 output, the bias is not as negligible as with secp256k1:2^256 = 2*q + rem, so some residues have three preimages and the rest have two. The total variation distance is about 7.5% by this derivation.I’d change this to one of:
or use a wider expand, for example 64 bytes via XMD/HKDF/expand_message, then reduce modulo
BLS_FR_ORDERand describe it as wide reduction, not rejection sampling. If you choose true 32-byte rejection sampling,u8(ctr)is also unnecessarily bounded; useu32_BE(ctr)or define behavior after 255 retries.The same issue is worth revisiting in NUT-13: the v3 deterministic blinding factor is currently
OS2IP(HMAC-SHA256(...)) mod BLS_FR_ORDER. That may be acceptable if the project explicitly accepts biased deterministic blinders, but it should be a deliberate choice, not an accidental carry-over from secp256k1 where the order is much closer to2^256.3. High: v3 keyset-ID test vectors reuse the same G2 key for multiple amounts
The new NUT-02 v3 test vectors use the exact same G2 public key for amount
1and2, and again for1,2,4, and8.That is risky as a canonical example. In this protocol, verification of a proof checks
e(C, G2) == e(Y, K)for the amount’s selected mint keyK; the amount itself is not cryptographically inside the signature. If two denominations share the same private/public key, a proof signed for one amount can be relabeled as another amount using the sameK.I’d either update the vectors to use distinct valid G2 public keys per amount, or add a strong rule in NUT-01/NUT-02:
The safer option is to do both.
4. Medium: the new test vectors do not actually expose
YorKThe NUT-00 v3 vector says it covers
hash_to_curve_G1, blinding, signing, and unblinding, but it only listssecret,r,a,B_,C_, andC; it does not listY = hash_to_curve_G1(secret)orK = a·G2.That makes it hard for independent implementations to isolate failures in hash-to-curve, serialization, signing, or unblinding. I decoded the provided G1 points and verified the scalar relations
C_ = 2·B_andC_ = 3·C; the impliedYfromB_ = 3·Yis:That still should be independently confirmed as the RFC 9380 hash-to-curve output for
test_messagewith the Cashu DST. RFC 9380 defineshash_to_curveas hashing to field, mapping, adding, and clearing the cofactor, and requires domain separation tags for these encodings. ([RFC Editor]2)I’d add:
and at least one batch-vector containing the transcript, challenge, per-proof weights, and expected pass/fail result.
5. Medium: version gating is inconsistent and may accidentally bind future versions
NUT-00 says the BLS scheme applies to keysets with version byte
02“onwards,” while NUT-01 says version byte02uses compressed BLS12-381 G2 keys.I’d avoid “onwards” unless the intent is to permanently reserve all future keyset versions for this exact BLS12-381 protocol and encoding. Safer wording:
6. Low: the serialization reference is pinned to an older draft
NUT-00 links to
draft-irtf-cfrg-pairing-friendly-curves-11. The Datatracker currently showsdraft-irtf-cfrg-pairing-friendly-curves-12as the latest revision, with revision date 2025-11-02. ([IETF Datatracker][3])Since the PR relies on the Zcash-style BLS12-381 compressed encoding, I’d either link the unversioned Datatracker document or explicitly cite the serialization format being used. The curves draft’s appendix describes compressed G1 as 48 bytes, compressed G2 as 96 bytes, and the three metadata bits, which matches the PR’s intended format. ([IETF Datatracker][4])
7. Low: the NUT-02 sample code is ambiguous around
serialize()The V3 derivation text says to concatenate lowercase compressed G2 public key hex strings, but the Python sample uses:
If
serialize()returns bytes, Python will hash theb'...'representation rather than the lowercase hex string.I’d make the sample explicit:
and also apply
.lower()to the unit in the sample, since the prose says the unit is lowercased.[1]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature "
[3]: https://datatracker.ietf.org/doc/draft-irtf-cfrg-pairing-friendly-curves/ "
[4]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-pairing-friendly-curves-11 "
I would still request changes due to the new Mint Scalar Construction section.
The formula
a = (H_G1 * a_raw) mod BLS_FR_ORDERdoes not make the resulting integer representative generally divisible byH_G1; reducing modulo the prime subgroup order destroys the property needed to annihilate torsion on arbitrary full-curve G1 inputs. SinceB_is attacker-controlled input to the signing oracle, the spec should make mint-side subgroup validation ofB_a MUST before computingC_ = a·B_.Suggested fix: remove the normative Mint Scalar Construction section and replace the critical note with an unambiguous requirement:
"Mints MUST deserialize and validate v3
B_using canonical compressed BLS12-381 G1 encoding, and MUST reject infinity, non-curve points, non-canonical encodings, and points not in the prime-order G1 subgroup before signing."The other updates look good: batch rejection sampling, NUT-13 v3 rejection sampling, distinct v3 amount keys, Y/K vectors, and batch challenge/weight vectors are all substantial improvements.
@Egge21M - Thank you for running the review bot. All issues raised have been addressed and this should be ready to review now.
cc: @callebtc @a1denvalu3 @thesimplekid
is this a correction or are we changing how we derive keysets ID v2?
same here
Unless I've misunderstood the python, it is just a correction to the example code to bring it in line with spec. Clanker says:
This is also intended to bring the python example into line with the existing spec. Although we assume unit is normalized lowercase elsewhere, I think the example should explicitly normalize.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.