feat: init docs

This commit is contained in:
Prad Nukala
2026-09-02 15:29:51 -04:00
parent d2390a8aad
commit 69425e2b7a
45 changed files with 11790 additions and 18 deletions
+475
View File
@@ -0,0 +1,475 @@
---
title: BBS+ Signatures
description: Sign a vector of attributes on BLS12-381, then prove possession of the signature while disclosing only the attributes you choose — plus blind signing so the issuer never sees part of what it signs.
sidebar:
order: 3
icon: eye-off
---
`signatures/bbs` implements the BBS+ signature scheme from
[eprint 2016/663](https://eprint.iacr.org/2016/663.pdf), section 4.3. A BBS+ signature covers an
ordered **vector** of scalar messages rather than one byte string, and that is the entire point: the
holder of a signature can later produce a zero-knowledge proof that says *"an issuer I can name
signed four attributes; here are attributes 3 and 4; I know the other two but I am not telling
you"*. The verifier learns nothing about the hidden attributes beyond the fact that they were signed.
This is the credential primitive. Reach for it when you are issuing something like a driver's
licence or a KYC attestation and the holder must be able to prove "over 21" to a bar without handing
over a birth date, a licence number, and an address. Do **not** reach for it when you just need to
sign a document — the machinery is heavy, verification runs pairings, and
[BLS](/signatures/bls) or ECDSA does that job far more cheaply.
:::danger[Do not deploy this package — message generators collide]
`MessageGenerators.Get(i)` returns the **same point for every index**. The method copies the internal
state array, writes the index into the copy, and then hashes the *original* — so the index never
reaches the hash. Every `H_i` for `i >= 1` is the identical point `h_0.Hash(state)`; only `Get(0)`
differs, returning `h_0` itself.
The consequence is a trivial forgery: because the signature commits to
`h_0^s · Π H_i^{m_i}` and all `H_i` are equal, the signature depends only on the **sum** of the
messages. Any permutation of the signed vector verifies, and so does any different vector with the
same sum. Verified against this repository:
```
original verifies: true
permuted verifies: true // [3,4,5,6] signature accepted for [6,5,4,3]
same-sum forgery verifies: true // ...and for [1,2,7,8]
```
Everything below describes the API as written. Nothing below is safe to rely on for
unforgeability or for selective disclosure until `signatures/bbs/message_generators.go` hashes the
mutated local copy. See [security notes](/reference/security).
:::
## Requirements
BBS+ needs a pairing, so it needs a `*curves.PairingCurve`. In practice that means BLS12-381:
```go
import (
"github.com/sonr-io/crypto/core/curves"
"github.com/sonr-io/crypto/signatures/bbs"
)
curve := curves.BLS12381(&curves.PointBls12381G2{})
```
The argument to `curves.BLS12381` chooses which group holds the **public key**. Passing
`&curves.PointBls12381G2{}` puts the key in G2 and signatures in G1 — the layout every test in the
package uses. See [the curve abstraction](/foundations/curves) for what `PairingCurve` provides.
Messages are `curves.Scalar`, not bytes. Convert with `curve.Scalar.Hash([]byte("..."))` for
free-form attributes, or `curve.Scalar.New(n)` for small integers.
## Keys and generators
<TypeTable
type={{
"NewKeys(curve *curves.PairingCurve)": {
type: "(*PublicKey, *SecretKey, error)",
description: "Generates a fresh keypair. Note the public key comes FIRST in the return order.",
},
"NewSecretKey(curve *curves.PairingCurve)": {
type: "(*SecretKey, error)",
description: "Just the signing key.",
},
"SecretKey.PublicKey()": {
type: "*PublicKey",
description: "Derives the verification key. No error return.",
},
"MessageGenerators.Init(w *PublicKey, length int)": {
type: "(*MessageGenerators, error)",
description: "Derives `length` message generators plus the blinding generator h0, deterministically from the public key. Errors only on negative length.",
},
"MessageGenerators.Get(i int)": {
type: "curves.PairingPoint",
description: "i <= 0 returns h0, the blinding generator. 1..length return message generators. Out of range returns nil (not an error). Currently broken — see the danger callout.",
},
}}
/>
Generators are **derived from the public key**, not stored with it. That is what lets one key sign
credentials of any width: you re-`Init` with a different `length` and get a different generator set.
It also means the verifier must `Init` with exactly the same `length` the signer used, or every
generator differs and nothing verifies.
`Get` is one-based for messages: message index `i` in your slice uses generator `Get(i + 1)`, and
`Get(0)` is the blinding generator `h_0`.
## Signing and verifying a full vector
Grounded in `TestSignatureWorks`.
```go sign.go
package main
import (
"fmt"
"log"
"github.com/sonr-io/crypto/core/curves"
"github.com/sonr-io/crypto/signatures/bbs"
)
func main() {
curve := curves.BLS12381(&curves.PointBls12381G2{})
pk, sk, err := bbs.NewKeys(curve)
if err != nil {
log.Fatal(err)
}
// One generator per attribute.
generators, err := new(bbs.MessageGenerators).Init(pk, 4)
if err != nil {
log.Fatal(err)
}
msgs := []curves.Scalar{
curve.Scalar.Hash([]byte("did:key:z6Mk...")),
curve.Scalar.Hash([]byte("Ada")),
curve.Scalar.Hash([]byte("Lovelace")),
curve.Scalar.New(36),
}
sig, err := sk.Sign(generators, msgs)
if err != nil {
log.Fatal(err)
}
// Verify returns error, not bool. nil means valid.
if err := pk.Verify(sig, generators, msgs); err != nil {
log.Fatal("invalid signature: ", err)
}
fmt.Println("signature valid")
}
```
`Sign` is **deterministic**: the internal `e` and `s` scalars come from a SHAKE256 DRBG seeded with
the secret key, the generators, and the messages. Signing the same vector twice with the same key
produces byte-identical output. There is no `io.Reader` parameter and no nonce to misuse.
`Sign` errors on an empty message slice, on `generators.length < len(msgs)`, and on a zero secret
key. `Verify` additionally rejects an identity public key and an identity signature point.
:::note[`Sign` tolerates a short vector; the proof path does not]
`sk.Sign` only requires `generators.length >= len(msgs)`, so you can sign 3 messages against
4 generators. `NewPokSignature` requires `len(msgs) == generators.length` exactly. Size your
generators to the credential, not to a round number.
:::
## Selective disclosure
This is the flow that makes BBS+ worth its cost. The holder turns their signature into a
`PokSignature`, derives a Fiat-Shamir challenge from a merlin transcript, and emits a
`PokSignatureProof`. The verifier rebuilds the same transcript from the proof and the messages it
was shown, recomputes the challenge, and checks the two match.
<Steps>
<Step title="Classify every message">
Build a `[]common.ProofMessage` with exactly one entry per generator, in signing order. Use
`common.RevealedMessage{Message: m}` for attributes the verifier will see and
`common.ProofSpecificMessage{Message: m}` for attributes it will not. Use
`common.SharedBlindingMessage{Message: m, Blinding: b}` only when the same hidden value must be
linked to another proof (a range proof over the same age, for example).
</Step>
<Step title="Commit">
`NewPokSignature(sig, generators, proofMsgs, reader)` randomises the signature and builds the
Schnorr commitments. The reader supplies the proof's randomness — pass `crand.Reader`.
</Step>
<Step title="Derive the challenge">
Create a merlin transcript with an application-specific label, feed it
`pok.GetChallengeContribution(transcript)`, append the verifier's nonce, extract 64 bytes and
reduce them with `curve.Scalar.SetBytesWide`.
</Step>
<Step title="Generate">
`pok.GenerateProof(challenge)` converts the blinding factors into response scalars and returns
the `*PokSignatureProof`. Send that, the challenge, the revealed messages, and the nonce.
</Step>
<Step title="Verify">
The verifier calls `pokSig.Verify(revealedMsgs, pk, generators, nonce, challenge, transcript)`
with a transcript constructed **identically** to the prover's.
</Step>
</Steps>
Grounded in `TestPokSignatureProofSomeMessagesRevealed`.
```go disclose.go
package main
import (
crand "crypto/rand"
"fmt"
"log"
"github.com/gtank/merlin"
"github.com/sonr-io/crypto/core/curves"
"github.com/sonr-io/crypto/signatures/bbs"
"github.com/sonr-io/crypto/signatures/common"
)
const transcriptLabel = "example.com/credential-presentation/v1"
func main() {
curve := curves.BLS12381(&curves.PointBls12381G2{})
pk, sk, err := bbs.NewKeys(curve)
if err != nil {
log.Fatal(err)
}
generators, err := new(bbs.MessageGenerators).Init(pk, 4)
if err != nil {
log.Fatal(err)
}
msgs := []curves.Scalar{
curve.Scalar.New(2), // holder id — keep hidden
curve.Scalar.New(3), // date of birth — keep hidden
curve.Scalar.New(4), // issuer — reveal
curve.Scalar.New(5), // credential type — reveal
}
sig, err := sk.Sign(generators, msgs)
if err != nil {
log.Fatal(err)
}
// ---- holder side ------------------------------------------------------
// One entry per generator, in signing order.
proofMsgs := []common.ProofMessage{
&common.ProofSpecificMessage{Message: msgs[0]},
&common.ProofSpecificMessage{Message: msgs[1]},
&common.RevealedMessage{Message: msgs[2]},
&common.RevealedMessage{Message: msgs[3]},
}
pok, err := bbs.NewPokSignature(sig, generators, proofMsgs, crand.Reader)
if err != nil {
log.Fatal(err)
}
nonce := curve.Scalar.Random(crand.Reader) // supplied by the verifier
transcript := merlin.NewTranscript(transcriptLabel)
pok.GetChallengeContribution(transcript)
transcript.AppendMessage([]byte("nonce"), nonce.Bytes())
okm := transcript.ExtractBytes([]byte("signature proof of knowledge"), 64)
challenge, err := curve.Scalar.SetBytesWide(okm)
if err != nil {
log.Fatal(err)
}
proof, err := pok.GenerateProof(challenge)
if err != nil {
log.Fatal(err)
}
// ---- verifier side ----------------------------------------------------
revealed := map[int]curves.Scalar{
2: msgs[2],
3: msgs[3],
}
vTranscript := merlin.NewTranscript(transcriptLabel) // same label, same order
ok := proof.Verify(revealed, pk, generators, nonce, challenge, vTranscript)
fmt.Println("presentation valid:", ok)
}
```
`revealed` is keyed by **zero-based message index**, matching the position in the original `msgs`
slice — not by generator index.
### What `Verify` actually checks, and what `VerifySigPok` does not
`PokSignatureProof.Verify` does two independent things:
1. **`VerifySigPok(pk)`** — a pairing check that the randomised signature is a real signature under
`pk`. You can call this on its own.
2. **Challenge equality** — it calls `GetChallengeContribution(generators, revealedMsgs, challenge,
transcript)`, re-extracts 64 bytes from the transcript, and compares the result to the challenge
you passed in. This is what binds the *revealed messages* to the proof.
Step 2 is why the transcript matters so much. If the verifier reveals a different message set, uses
a different transcript label, or appends the nonce at a different point, the recomputed challenge
differs and `Verify` returns `false`.
:::warning[A transcript mismatch is indistinguishable from a forgery]
Prover and verifier must construct the merlin transcript with the **same label, the same appended
messages, in the same order, with the same domain-separation byte strings**. Any divergence produces
a different challenge and `Verify` returns `false` — with no error, no diagnostic, and nothing to
distinguish it from an actual attack. Put the transcript construction in one shared function that
both sides call. `PokSignatureProof.Verify` returns a bare `bool`; there is no error channel at all.
:::
You can also drive the two halves manually — the test does exactly this to show BBS+ composing with
other sigma protocols that share the transcript:
```go
proof.GetChallengeContribution(generators, revealed, challenge, vTranscript)
// ...other protocols append their contributions to vTranscript here...
vTranscript.AppendMessage([]byte("nonce"), nonce.Bytes())
okm := vTranscript.ExtractBytes([]byte("signature proof of knowledge"), 64)
vChallenge, _ := curve.Scalar.SetBytesWide(okm)
valid := proof.VerifySigPok(pk) && challenge.Cmp(vChallenge) == 0
```
## Blind signing
The dual problem: the *issuer* must sign an attribute it is not allowed to see — a link secret, a
biometric template, a device key. The holder commits to those messages, proves knowledge of the
committed values, and the issuer signs the commitment together with the messages it does know.
<Steps>
<Step title="Holder commits">
`NewBlindSignatureContext(curve, hiddenMsgs, generators, nonce, reader)` returns the context to
send to the issuer **and** a `common.SignatureBlinding` the holder keeps. `hiddenMsgs` is a
`map[int]curves.Scalar` keyed by zero-based message index.
</Step>
<Step title="Issuer verifies the commitment">
`ctx.Verify(knownIndices, generators, nonce)` checks the holder's proof of knowledge of the
hidden values, so the issuer is not signing arbitrary garbage. `knownIndices` is the sorted list
of indices the *issuer* supplies.
</Step>
<Step title="Issuer signs">
`ctx.ToBlindSignature(knownMsgs, sk, generators, nonce)` produces a `*BlindSignature`. It calls
`Verify` internally, so a bad commitment fails here too.
</Step>
<Step title="Holder unblinds">
`blindSig.ToUnblinded(blinding)` adds the retained blinding factor back into the `s` component,
yielding an ordinary `*Signature` that verifies against the complete message vector.
</Step>
</Steps>
Grounded in `TestBlindSignatureContext`.
```go blind.go
package main
import (
crand "crypto/rand"
"fmt"
"log"
"github.com/sonr-io/crypto/core/curves"
"github.com/sonr-io/crypto/signatures/bbs"
)
func main() {
curve := curves.BLS12381(&curves.PointBls12381G2{})
pk, sk, err := bbs.NewKeys(curve)
if err != nil {
log.Fatal(err)
}
generators, err := new(bbs.MessageGenerators).Init(pk, 4)
if err != nil {
log.Fatal(err)
}
nonce := curve.Scalar.Random(crand.Reader)
// ---- holder: hide message 0 from the issuer ---------------------------
hidden := map[int]curves.Scalar{
0: curve.Scalar.Hash([]byte("link-secret")),
}
ctx, blinding, err := bbs.NewBlindSignatureContext(curve, hidden, generators, nonce, crand.Reader)
if err != nil {
log.Fatal(err)
}
// Send ctx (and nonce) to the issuer. Keep `blinding`.
// ---- issuer: signs only what it knows ---------------------------------
known := map[int]curves.Scalar{
1: curve.Scalar.Hash([]byte("firstname")),
2: curve.Scalar.Hash([]byte("lastname")),
3: curve.Scalar.Hash([]byte("age")),
}
blindSig, err := ctx.ToBlindSignature(known, sk, generators, nonce)
if err != nil {
log.Fatal(err)
}
// ---- holder: unblind and check ----------------------------------------
sig := blindSig.ToUnblinded(blinding)
full := []curves.Scalar{hidden[0], known[1], known[2], known[3]}
if err := pk.Verify(sig, generators, full); err != nil {
log.Fatal("unblinded signature invalid: ", err)
}
fmt.Println("blind-signed credential valid")
}
```
The issuer never sees `hidden[0]`. It only ever handles `ctx.commitment`, a group element, plus a
Schnorr proof that the holder knows the openings.
:::danger[Lose the blinding factor and the signature is dead]
`ToUnblinded` is the only way to turn a `*BlindSignature` into a verifiable `*Signature`, and it
requires the exact `common.SignatureBlinding` returned alongside the context. That value is random,
is never transmitted, and cannot be recovered from the signature, the context, or the issuer.
Persist it atomically with the blind signature or the credential is unusable and must be re-issued.
:::
:::warning[The index sets must partition the vector]
`hidden` and `known` are both keyed by zero-based message index, and between them they must cover
every position `0..length-1` exactly once. Nothing checks this. An index present in neither map
leaves a generator unaccounted for and the unblinded signature simply fails to verify; an index
present in both silently produces a signature over a value the holder did not intend.
:::
## Serialization
Every type here is a `BinaryMarshaler`, but the wire format does not carry its curve, so the
unmarshalling side needs `Init(curve)` first:
```go
data, err := sig.MarshalBinary()
restored := new(bbs.Signature).Init(curve)
err = restored.UnmarshalBinary(data)
```
The same pattern applies to `PublicKey`, `SecretKey`, `BlindSignature`, `BlindSignatureContext`, and
`PokSignatureProof`. Calling `UnmarshalBinary` on a zero-valued struct dereferences nil fields and
panics.
`BlindSignatureContext.MarshalBinary` writes the commitment point followed by the challenge and one
scalar per proof — `PointSize + (N + 1) * ScalarSize` bytes.
## Caveats
:::danger[Broken message binding]
Restated because it invalidates everything above: `MessageGenerators.Get` makes all generators
equal, so a signature binds only the *sum* of the message scalars.
:::
:::warning[Inconsistent failure signalling]
`PublicKey.Verify` and `BlindSignatureContext.Verify` return `error`. `PokSignatureProof.Verify` and
`VerifySigPok` return `bool`. `MessageGenerators.Get` returns a bare `nil` for an out-of-range index
rather than an error, so a bad index surfaces later as a nil-pointer dereference in whatever point
operation consumes it. Do not assume a uniform idiom across this package.
:::
:::note[Deterministic signing has a privacy consequence]
Because `Sign` derives its randomness from `(sk, generators, msgs)`, re-issuing the identical
credential yields the identical signature bytes. That is convenient for idempotent issuance and bad
for unlinkability if raw signatures ever leave the holder. Present via `PokSignatureProof`, which
re-randomises, rather than by forwarding the signature.
:::
## Related
<CardGroup cols={2}>
<Card title="Proof toolkit" href="/signatures" icon="wrench">
`signatures/common` — `ProofMessage`, `ProofCommittedBuilder`, `HmacDrbg`, and the scalar aliases
this page uses.
</Card>
<Card title="Schnorr proofs" href="/zero-knowledge/schnorr" icon="binary">
The standalone sigma protocol, for composing proofs of discrete-log knowledge alongside a
BBS+ presentation.
</Card>
<Card title="Accumulator" href="/zero-knowledge/accumulator" icon="layers">
Constant-size set membership on the same pairing curve — the usual companion for revocation.
</Card>
<Card title="Security notes" href="/reference/security" icon="shield">
Every defect found while documenting this library, in one place.
</Card>
</CardGroup>
+469
View File
@@ -0,0 +1,469 @@
---
title: BLS Signatures
description: Pairing-based signatures on BLS12-381 with aggregation, multi-signatures, proofs of possession, and non-interactive threshold key generation.
sidebar:
order: 2
icon: combine
---
`signatures/bls/bls_sig` implements the BLS signature scheme from
[draft-irtf-cfrg-bls-signature-03](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-03) on
BLS12-381. Its defining property is **aggregation**: any number of signatures can be combined into a
single group element that verifies against the corresponding set of public keys, and the combined
object is exactly the size of one signature.
Reach for BLS when you need to compress many signatures (block attestations, multi-party approvals,
certificate chains), or when you want `t`-of-`n` threshold signing **without an interactive
protocol** — BLS partial signatures combine by plain Lagrange interpolation, so signers never talk to
each other. Reach for something else if you need short verification time on constrained hardware
(pairings are expensive), or if your verifier is a chain that only knows secp256k1 or Ed25519 — in
that case see [threshold ECDSA](/threshold/threshold-ecdsa) or
[threshold Ed25519](/threshold/threshold-ed25519).
```go
import "github.com/sonr-io/crypto/signatures/bls/bls_sig"
```
:::note
This package does **not** use the [`core/curves`](/foundations/curves) `Curve` / `Point` / `Scalar`
abstraction. It calls the native `core/curves/native/bls12381` backend directly and is hard-wired to
BLS12-381 — there is no curve parameter anywhere in its API.
:::
## Two instantiations: `Vt` and non-`Vt`
BLS12-381 has two source groups, G1 and G2, and the pairing is asymmetric. You must decide which
group carries public keys and which carries signatures; whichever you put in G1 is the small one.
The package exposes both choices as two parallel type families that share a `SecretKey` type.
| | Non-`Vt` types | `Vt` types |
| --- | --- | --- |
| Public key group | **G1** (`PublicKey`) | **G2** (`PublicKeyVt`) |
| Signature group | **G2** (`Signature`) | **G1** (`SignatureVt`) |
| Compressed public key | 48 bytes (`PublicKeySize`) | 96 bytes (`PublicKeyVtSize`) |
| Compressed signature | 96 bytes (`SignatureSize`) | 48 bytes (`SignatureVtSize`) |
| Compressed PoP | 96 bytes (`ProofOfPossessionSize`) | 48 bytes (`ProofOfPossessionVtSize`) |
| Trade-off | minimal **public key** size | minimal **signature** size |
Secret keys are shared between the two families:
| Constant | Value | Meaning |
| --- | --- | --- |
| `SecretKeySize` | `32` | A scalar mod `r`, the subgroup order. Cannot be zero. |
| `SecretKeyShareSize` | `33` | A 32-byte share value followed by a 1-byte identifier at index 32. |
`SecretKeyShareSize` being 33 rather than 32 is why shares are self-describing: the trailing
identifier is the Shamir x-coordinate, so `CombineSignatures` can reconstruct the Lagrange
coefficients from the partials alone. It also caps you at 255 shares — identifier `0` is invalid.
Which one do you want? If your verifier stores many public keys and sees few signatures (an on-chain
validator registry), the non-`Vt` family is cheaper. If you publish many signatures against few keys
(per-block attestations), `Vt` is cheaper. Ethereum 2 uses the non-`Vt` layout — 48-byte pubkeys in
G1, 96-byte signatures in G2 — which is what `NewSigEth2()` gives you.
:::warning[The `Vt` doc comments are wrong in one place]
The source comment above `SigBasicVt` in `tiny_bls.go` says "minimal-pubkey-size"; it is a
copy-paste from the non-`Vt` file. `SigBasicVt` is minimal-*signature*-size, consistent with its
`SignatureVt` being the 48-byte G1 element. Trust the types and the constants, not that comment.
:::
## Three ciphersuites
Independently of the group choice, the draft defines three ciphersuites that differ only in what
gets hashed and what the caller must check. Each is a distinct Go type with its own constructor and
its own domain separation tag.
| Scheme | Constructor | Signature DST | Extra requirement |
| --- | --- | --- | --- |
| `SigBasic` | `NewSigBasic()` | `BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_` | All messages in an aggregate must be distinct |
| `SigAug` | `NewSigAug()` | `BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_` | Public key is prepended to the message before hashing |
| `SigPop` | `NewSigPop()` | `BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_` | Every key needs a verified proof of possession |
| `SigBasicVt` | `NewSigBasicVt()` | `BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_` | as above |
| `SigAugVt` | `NewSigAugVt()` | `BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_AUG_` | as above |
| `SigPopVt` | `NewSigPopVt()` | `BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_` | as above |
`SigPop` additionally carries a second DST used only for proof-of-possession *proofs*:
| Constant | Value |
| --- | --- |
| PoP proof DST (non-`Vt`) | `BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_` |
| PoP proof DST (`Vt`) | `BLS_POP_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_` |
The `G1`/`G2` token inside each DST names the group the *signature* lives in, which is why the `Vt`
tags say `G1`.
`SigEth2` is a plain Go type alias for `SigPop`, and `SigEth2Vt` for `SigPopVt`:
```go
type SigEth2 = SigPop
func NewSigEth2() *SigEth2 { return NewSigPop() }
```
They are naming conveniences, nothing more — `NewSigEth2()` and `NewSigPop()` return identical
values with identical DSTs.
### Overriding the DST
Every scheme has a `WithDst` constructor for interoperating with a system that chose different
domain separation:
```go
b := bls_sig.NewSigBasicWithDst("MY_APP_BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_")
// SigPop needs both tags, and rejects equal ones.
p, err := bls_sig.NewSigPopWithDst(
"MY_APP_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_",
"MY_APP_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_",
)
```
`NewSigPopWithDst` / `NewSigPopVtWithDst` are the only DST constructors that return an error: they
reject a signature DST equal to the PoP DST. The others accept any string, including an empty one.
### What each ciphersuite defends against
The threat is the **rogue-key attack**. Aggregate verification checks a product of pairings. An
attacker who is allowed to publish a public key *after* seeing honest keys can publish
`pk_evil = g^a · (Π pk_honest)^-1` and then produce an "aggregate" signature over a message the
honest parties never signed. The three ciphersuites each break this differently:
<Tabs>
<Tab title="Basic">
Nothing binds a key to its message beyond the message itself, so security rests on the caller
ensuring **every message in an aggregate is distinct**. `AggregateVerify` enforces this: it
rejects the batch if any two message byte strings are equal. Use Basic only when your messages
are naturally unique (they embed a nonce, a height, a hash).
</Tab>
<Tab title="Aug">
`Sign` prepends the signer's own compressed public key to the message before hashing:
`H(pk_bytes || msg)`. That makes each signer's hashed point key-dependent, so rogue keys cannot
cancel. `Verify` and `AggregateVerify` reproduce the same prefix. No caller discipline is
required, and messages may repeat. The cost is that verification needs the exact public key
bytes, and `SigAug.PartialSign` therefore takes an extra `*PublicKey` argument that the other
schemes do not.
</Tab>
<Tab title="Pop">
Each signer publishes a proof of possession — a signature over their own public key under a
separate DST — proving they know the secret behind the key. Once every key in a set has a
verified PoP, rogue keys are impossible by construction, and the fast path opens up:
`FastAggregateVerify` and `VerifyMultiSignature` verify N signatures over the *same* message
with a single pairing check. This is the Eth2 configuration.
</Tab>
</Tabs>
:::danger[Pop only defends you if you actually call `PopVerify`]
`FastAggregateVerify`, `AggregatePublicKeys`, and `VerifyMultiSignature` do **not** check proofs of
possession. Nothing in the library forces you to. If you aggregate a public key you have not
`PopVerify`'d, `SigPop` gives you no more rogue-key protection than `SigBasic` with duplicate
messages — which is to say, none. Verify the PoP at key-registration time and refuse to store keys
that fail.
:::
## Method set
All six scheme types share this core. Signatures are `(bool, error)` — **check both**, because a
verification that errored also returns `false`, and a nil error does not mean valid.
<TypeTable
type={{
"Keygen()": {
type: "(*PublicKey, *SecretKey, error)",
description: "Reads 32 bytes from crypto/rand and derives a keypair.",
},
"KeygenWithSeed(ikm []byte)": {
type: "(*PublicKey, *SecretKey, error)",
description: "Deterministic keygen via HKDF with salt \"BLS-SIG-KEYGEN-SALT-\". ikm MUST be at least 32 bytes; shorter input is an error.",
},
"Sign(sk, msg)": {
type: "(*Signature, error)",
description: "Hashes msg to a point and multiplies by the secret. Deterministic — no nonce, so no nonce-reuse failure mode. Basic and Pop accept an empty (but not nil) message; Aug rejects both.",
},
"Verify(pk, msg, sig)": {
type: "(bool, error)",
description: "Single-signature verification.",
},
"AggregateVerify(pks, msgs, sigs)": {
type: "(bool, error)",
description: "Aggregates sigs internally, then checks the product of pairings against every (pk, msg) pair. Errors on length mismatch. Basic and Pop reject duplicate messages.",
},
"ThresholdKeygen(threshold, total uint)": {
type: "(*PublicKey, []*SecretKeyShare, error)",
description: "Generates one public key and `total` Shamir shares of its secret. Errors when threshold is 0, threshold exceeds total, total is 1 or less, or either exceeds 255.",
},
"ThresholdKeygenWithSeed(ikm, threshold, total)": {
type: "(*PublicKey, []*SecretKeyShare, error)",
description: "Same, seeded deterministically.",
},
"PartialSign(sks, msg)": {
type: "(*PartialSignature, error)",
description: "One share's contribution. Rejects nil and empty messages in every scheme. SigAug and SigAugVt take an extra *PublicKey between the share and the message.",
},
"CombineSignatures(sigs ...*PartialSignature)": {
type: "(*Signature, error)",
description: "Lagrange-interpolates partials into a normal signature. Errors on fewer than 2 partials, more than 255, a nil partial, a duplicate share identifier, or a partial outside the correct subgroup. It does NOT know your threshold — see the caveats.",
},
}}
/>
`SigPop` and `SigPopVt` add:
<TypeTable
type={{
"PopProve(sk)": {
type: "(*ProofOfPossession, error)",
description: "Signs the key's own public key under the PoP DST.",
},
"PopVerify(pk, pop)": {
type: "(bool, error)",
description: "Checks a proof of possession. Run this before trusting a key in any aggregate.",
},
"AggregatePublicKeys(pks ...*PublicKey)": {
type: "(*MultiPublicKey, error)",
description: "Sums public keys into a single group element for same-message verification.",
},
"AggregateSignatures(sigs ...*Signature)": {
type: "(*MultiSignature, error)",
description: "Sums signatures over the same message.",
},
"VerifyMultiSignature(mpk, msg, msig)": {
type: "(bool, error)",
description: "Verifies a pre-aggregated key against a pre-aggregated signature. One pairing check.",
},
"FastAggregateVerify(pks, msg, asig)": {
type: "(bool, error)",
description: "Same-message verification where the signature is already aggregated but the keys are not.",
},
"FastAggregateVerifyConstituent(pks, msg, sigs)": {
type: "(bool, error)",
description: "Same, but takes the individual signatures and aggregates them for you.",
},
}}
/>
`AggregateVerify` (many distinct messages) and `FastAggregateVerify` (one shared message) are not
interchangeable. Passing the same message N times to `AggregateVerify` under `SigBasic` or `SigPop`
returns `false` by design.
## Aggregate verification
Grounded in `TestBasicAggregateVerifyG2Works` and its `generateBasicAggregateDataG2` helper.
```go aggregate.go
package main
import (
"crypto/rand"
"fmt"
"log"
"github.com/sonr-io/crypto/signatures/bls/bls_sig"
)
func main() {
bls := bls_sig.NewSigBasic()
const n = 10
pks := make([]*bls_sig.PublicKey, n)
sigs := make([]*bls_sig.Signature, n)
msgs := make([][]byte, n)
for i := 0; i < n; i++ {
ikm := make([]byte, 32)
if _, err := rand.Read(ikm); err != nil {
log.Fatal(err)
}
pk, sk, err := bls.KeygenWithSeed(ikm)
if err != nil {
log.Fatal(err)
}
// SigBasic requires every message in the batch to differ.
msg := []byte(fmt.Sprintf("attestation %d", i))
sig, err := bls.Sign(sk, msg)
if err != nil {
log.Fatal(err)
}
pks[i], sigs[i], msgs[i] = pk, sig, msg
}
ok, err := bls.AggregateVerify(pks, msgs, sigs)
if err != nil {
log.Fatal(err)
}
fmt.Println("aggregate valid:", ok)
}
```
Swap `NewSigBasic()` for `NewSigAug()` and the duplicate-message restriction disappears, at the cost
of `PartialSign` gaining a public-key argument.
## Threshold signing
Grounded in `TestBasicPartialSign`. Note there is no DKG here and no interaction between signers:
`ThresholdKeygen` produces the shares centrally, and each holder signs independently.
<Steps>
<Step title="Deal the shares">
`ThresholdKeygen(2, 4)` returns one public key plus four `*SecretKeyShare` values. The public
key is the ordinary BLS public key for the reconstructed secret — verifiers never learn that
threshold signing happened.
</Step>
<Step title="Sign independently">
Each holder calls `PartialSign(share, msg)`. No round trips, no shared state, no per-signature
nonce. Partials can be produced years apart.
</Step>
<Step title="Combine">
`CombineSignatures(partials...)` Lagrange-interpolates in the exponent. It rejects fewer than
two partials, duplicate share identifiers, and nil entries — but it has no idea what your
threshold was, so short-of-threshold input succeeds and yields a wrong signature.
</Step>
<Step title="Verify normally">
The result is an ordinary `*Signature`. `Verify(pk, msg, sig)` accepts it.
</Step>
</Steps>
```go threshold.go
package main
import (
"fmt"
"log"
"github.com/sonr-io/crypto/signatures/bls/bls_sig"
)
func main() {
bls := bls_sig.NewSigBasic()
// 2-of-4. pk is the ordinary public key for the (never assembled) secret.
pk, shares, err := bls.ThresholdKeygen(2, 4)
if err != nil {
log.Fatal(err)
}
msg := []byte("release the funds")
p1, err := bls.PartialSign(shares[0], msg)
if err != nil {
log.Fatal(err)
}
p2, err := bls.PartialSign(shares[2], msg)
if err != nil {
log.Fatal(err)
}
sig, err := bls.CombineSignatures(p1, p2)
if err != nil {
log.Fatal(err)
}
ok, err := bls.Verify(pk, msg, sig)
if err != nil {
log.Fatal(err)
}
fmt.Println("threshold signature valid:", ok) // true
}
```
`PartialSignature` is the only public-field type in the package:
```go
type PartialSignature struct {
Identifier byte
Signature bls12381.G2 // bls12381.G1 for PartialSignatureVt
}
```
Partials are not `BinaryMarshaler`s — if you need to ship them across a wire, serialize the
identifier and the group element yourself.
## Serialization
Every key, signature, PoP, multi-key, multi-signature, and secret-key share implements
`encoding.BinaryMarshaler` and `encoding.BinaryUnmarshaler`, using the standard compressed
[zcash BLS12-381 encoding](https://github.com/zcash/librustzcash/blob/master/pairing/src/bls12_381/README.md#serialization).
The unmarshalers validate length, reject the all-zero encoding, and check subgroup membership.
```go
raw, err := pk.MarshalBinary() // 48 bytes for PublicKey, 96 for PublicKeyVt
var restored bls_sig.PublicKey
err = restored.UnmarshalBinary(raw)
```
`SecretKey.UnmarshalBinary` requires exactly 32 bytes and rejects all-zero input.
`SecretKeyShare.UnmarshalBinary` requires exactly 33 and likewise rejects all-zero; the identifier
is the final byte.
## Caveats
:::danger[Both return values matter]
`Verify`, `AggregateVerify`, `FastAggregateVerify`, `VerifyMultiSignature`, and `PopVerify` all
return `(bool, error)`. Writing `if ok, _ := bls.Verify(...); ok` discards a real error, and writing
`if err == nil` accepts an invalid signature. Check the boolean **and** the error.
:::
:::warning[`SigBasic` and `SigPop` silently reject duplicate messages]
`AggregateVerify` returns `(false, nil)` — not an error — when two messages in the batch are byte
equal. If you are aggregating attestations that legitimately repeat, you want `SigAug`, or you want
the same-message path (`FastAggregateVerify`) under `SigPop`.
:::
:::warning[Mixing families does not compile, but mixing ciphersuites does]
`PublicKeyVt` and `PublicKey` are different types, so the compiler catches G1/G2 mistakes. Nothing
catches verifying a `SigAug` signature with `NewSigBasic()` — the DSTs differ, so you simply get
`false`. Store the ciphersuite alongside the key material.
:::
:::note[Keygen input length]
`KeygenWithSeed` and `ThresholdKeygenWithSeed` require `len(ikm) >= 32`. Shorter input returns an
error rather than stretching. An all-zero 32-byte `ikm` is accepted — the HKDF step still produces a
nonzero scalar — so a zeroed buffer will not fail loudly; it will produce a deterministic, publicly
derivable key.
:::
:::danger[`CombineSignatures` does not enforce your threshold]
`combineSigs` only checks that it received between 2 and 255 distinct, subgroup-valid partials. It
never learns the `threshold` you passed to `ThresholdKeygen`, so combining 2 partials of a 3-of-5
key returns a perfectly well-formed `*Signature` with `err == nil` that simply fails verification.
If your application distinguishes "not enough signers yet" from "a signer cheated", count the
partials yourself before combining.
:::
:::warning[`KeygenWithSeed` mutates the slice you hand it]
Key derivation does `ikm = append(ikm, 0)` before the HKDF call. When your `ikm` slice has spare
capacity — for example a sub-slice of a larger buffer — that append writes a zero byte into the
backing array past `len(ikm)`, clobbering whatever lived there. Pass a slice whose length equals its
capacity, or a fresh copy.
:::
:::note[Nil versus empty messages]
`SigBasic.Sign` and `SigPop.Sign` accept an empty non-nil slice but reject `nil`. `SigAug.Sign`
rejects both, because it checks `len(msg) == 0`. `PartialSign` rejects both in every scheme, for the
same reason — so a message that a full `Sign` accepts may be refused by the threshold path.
:::
:::note[Key derivation detail]
`Generate` follows draft-04's KeyGen: `salt = SHA-256("BLS-SIG-KEYGEN-SALT-")`, then
`HKDF-SHA256(ikm || 0x00, salt, info = I2OSP(48, 2))`, read 48 bytes, byte-reversed, reduced mod the
subgroup order. It does not implement the salt-rehashing loop from later drafts, so a zero result
would be returned rather than retried — an outcome with negligible probability, but not one the code
guards against.
:::
## Related
<CardGroup cols={2}>
<Card title="Secret sharing" href="/threshold/secret-sharing" icon="split">
Shamir, Feldman, and Pedersen sharing — the general machinery behind `ThresholdKeygen`.
</Card>
<Card title="Distributed key generation" href="/threshold/dkg" icon="users">
When no single party may ever hold the whole secret, even at dealing time.
</Card>
<Card title="Accumulator" href="/zero-knowledge/accumulator" icon="layers">
The other pairing-based primitive in this library, also on BLS12-381.
</Card>
<Card title="Security notes" href="/reference/security" icon="shield">
Known defects and unaudited paths across the library.
</Card>
</CardGroup>
+476
View File
@@ -0,0 +1,476 @@
---
title: Chain-Specific Schemes
description: Mina-protocol Schnorr over Pallas with Poseidon, and NEM's Keccak-512 flavoured Ed25519 — interop code for two specific networks, not general-purpose primitives.
sidebar:
order: 6
icon: link
---
Everything under `signatures/schnorr` exists to produce bytes that one particular blockchain will
accept. These are not primitives you choose on cryptographic merit; you use them because you are
talking to Mina or to NEM/Symbol and their consensus rules define the signature format down to the
hash function. Both live under a `schnorr` directory, but only Mina is actually Schnorr — NEM is
Ed25519 with a hash substitution.
If you are not integrating with those two networks, nothing on this page is for you. For general
signing see [BLS](/signatures/bls), [ECDSA utilities](/signatures/ecdsa), or
[threshold Ed25519](/threshold/threshold-ed25519).
## Mina: Schnorr over Pallas
```go
import "github.com/sonr-io/crypto/signatures/schnorr/mina"
```
Mina's signature scheme is Schnorr on the **Pallas** curve with the **Poseidon** algebraic hash. Both
choices exist because Mina's recursive SNARKs must verify signatures *inside* a circuit, where
SHA-256 is ruinously expensive and Poseidon is cheap. The package mirrors
[Mina's C reference signer](https://github.com/MinaProtocol/c-reference-signer) — the tests use that
project's key and transaction fixtures.
Signing computes `k` deterministically from the key, the public key, the network id, and the message
(`msgDerive`), negates `k` when `R` has an odd y-coordinate, and returns `(R.x, s)` where
`s = k + e·sk` and `e` is the Poseidon hash of the public key, `R.x`, the message, and the network
id. There is no randomness at signing time.
### Keys and addresses
<TypeTable
type={{
"NewKeys()": {
type: "(*PublicKey, *SecretKey, error)",
description: "Fresh keypair from crypto/rand. Public key first. Errors on a zero scalar or identity point.",
},
"NewKeysFromReader(reader io.Reader)": {
type: "(*PublicKey, *SecretKey, error)",
description: "Same, from a supplied reader — use for deterministic test fixtures.",
},
"SecretKey.GetPublicKey()": {
type: "*PublicKey",
description: "Scalar multiplication of the Pallas generator. No error return.",
},
"PublicKey.GenerateAddress()": {
type: "string",
description: "Base58 Mina address: 0xcb version byte, 0x01 non-zero-curve-point version, 0x01 compressed flag, the 32-byte x coordinate, a y-parity byte, and a 4-byte double-SHA-256 checksum — 40 bytes encoded. These are the strings beginning \"B62q\".",
},
"PublicKey.ParseAddress(b58 string)": {
type: "error",
description: "Decodes and validates length, all three version bytes, and the checksum (compared in constant time) before recovering the point.",
},
"SecretKey.MarshalBinary()": {
type: "([]byte, error)",
description: "32 bytes, the Fq scalar. UnmarshalBinary requires exactly 32.",
},
"PublicKey.MarshalBinary()": {
type: "([]byte, error)",
description: "Compressed affine Pallas point. Distinct from the address encoding.",
},
}}
/>
`SetPointPallas(*curves.PointPallas)` and `SetFq(*fq.Fq)` are the escape hatches that let a threshold
signer inject externally-produced key material — see the FROST bridge below.
### Signing
`SignTransaction` is the real API; `SignMessage` is a convenience for signing a plain string.
<TypeTable
type={{
"SecretKey.SignTransaction(txn *Transaction)": {
type: "(*Signature, error)",
description: "Builds a random-oracle input with 3 field elements and 75 bytes of packed data, then signs under txn.NetworkId.",
},
"SecretKey.SignMessage(message string)": {
type: "(*Signature, error)",
description: "Signs the raw string bytes. Non-standard — the Mina reference signer does the same thing. Hardcoded to MainNet.",
},
"PublicKey.VerifyTransaction(sig, txn)": {
type: "error",
description: "nil means valid. Uses txn.NetworkId.",
},
"PublicKey.VerifyMessage(sig, message)": {
type: "error",
description: "nil means valid. Also hardcoded to MainNet.",
},
}}
/>
`Signature` is the only struct here with exported fields:
```go
type Signature struct {
R *fp.Fp // x coordinate of the nonce point, base field
S *fq.Fq // response scalar, scalar field
}
```
`MarshalBinary` produces exactly 64 bytes, `R` then `S`; `UnmarshalBinary` requires exactly 64 and
validates both field elements.
Grounded in `TestSecretKeySignTransaction`.
```go mina.go
package main
import (
"fmt"
"log"
"github.com/sonr-io/crypto/signatures/schnorr/mina"
)
func main() {
pk, sk, err := mina.NewKeys()
if err != nil {
log.Fatal(err)
}
fmt.Println("address:", pk.GenerateAddress())
feePayer := new(mina.PublicKey)
if err := feePayer.ParseAddress("B62qiy32p8kAKnny8ZFwoMhYpBppM1DWVCqAPBYNcXnsAHhnfAAuXgg"); err != nil {
log.Fatal(err)
}
receiver := new(mina.PublicKey)
if err := receiver.ParseAddress("B62qrcFstkpqXww1EkSGrqMCwCNho86kuqBd4FrAAUsPxNKdiPzAUsy"); err != nil {
log.Fatal(err)
}
txn := &mina.Transaction{
Fee: 3,
FeeToken: 1,
Nonce: 200,
ValidUntil: 10000,
Memo: "this is a memo",
FeePayerPk: feePayer,
SourcePk: feePayer,
ReceiverPk: receiver,
TokenId: 1,
Amount: 42,
Locked: false,
Tag: [3]bool{false, false, false}, // all false = payment
NetworkId: mina.MainNet,
}
sig, err := sk.SignTransaction(txn)
if err != nil {
log.Fatal(err)
}
if err := sk.GetPublicKey().VerifyTransaction(sig, txn); err != nil {
log.Fatal("invalid: ", err)
}
raw, err := sig.MarshalBinary()
if err != nil {
log.Fatal(err)
}
fmt.Println("signature bytes:", len(raw)) // 64
}
```
Setting `Tag: [3]bool{false, false, true}` makes it a stake delegation instead of a payment, as in
`TestSecretKeySignTransactionStaking`.
### The `Transaction` type
<TypeTable
type={{
Fee: { type: "uint64", description: "Fee in nanomina." },
FeeToken: { type: "uint64", description: "Token id used to pay the fee — 1 for MINA." },
FeePayerPk: { type: "*PublicKey", required: true, description: "Must be non-nil; MarshalBinary dereferences it." },
Nonce: { type: "uint32", description: "Account nonce." },
ValidUntil: { type: "uint32", description: "Expiry slot." },
Memo: { type: "string", description: "At most 32 bytes — longer values are silently truncated. See the caveat below." },
Tag: { type: "[3]bool", description: "Transaction kind. {false,false,false} is a payment; {false,false,true} is a stake delegation." },
SourcePk: { type: "*PublicKey", required: true, description: "Sender. Must be non-nil." },
ReceiverPk: { type: "*PublicKey", required: true, description: "Recipient, or the new delegate. Must be non-nil." },
TokenId: { type: "uint64", description: "Token being moved." },
Amount: { type: "uint64", description: "Amount in nanomina. Zero for a delegation." },
Locked: { type: "bool", description: "Timelock flag." },
NetworkId: { type: "NetworkType", description: "Selects the Poseidon sponge IV and enters the nonce derivation. TestNet is the zero value." },
}}
/>
`MarshalBinary` writes a fixed **175-byte** layout: fee, fee token, fee-payer point, nonce, valid
until, a `0x01` marker, memo length, 32 memo bytes, three tag bytes, source point, receiver point,
token id, amount, locked flag, and finally the network id at offset 174. `UnmarshalBinary` reverses
it and requires that exact length. This encoding is what the FROST bridge parses.
### Network types
`NetworkType` selects the Poseidon sponge initialisation vector and is mixed into the nonce
derivation, so a signature made for one network is invalid on another — that is deliberate replay
protection.
| Constant | Value | Meaning |
| --- | --- | --- |
| `TestNet` | `0` | Mina testnet IV. Also the zero value of `NetworkType`, so a `Transaction` you forgot to fill in is a testnet transaction. |
| `MainNet` | `1` | Mina mainnet IV. |
| `NullNet` | `2` | Zero-initialised sponge state, no IV. Used by the Poseidon unit tests for raw-permutation vectors. |
### Poseidon internals
You do not need these to sign, but they are exported and occasionally useful for testing a circuit
against the same hash.
<TypeTable
type={{
"Permutation (int)": {
type: "ThreeW | FiveW | Three",
description: "Which Poseidon parameter set to run. Values 0, 1, 2. Every signing path in the package uses ThreeW.",
},
"SBox (int)": {
type: "Cube | Quint | Sept | Inverse",
description: "The exponentiation applied in each round: x^3, x^5, x^7, x^-1. Values 0..3. Selected by the parameter set, not by the caller. SBox.Exp(f *fp.Fp) mutates f in place.",
},
"Context": {
type: "struct",
description: "The Poseidon sponge. Init(pType, networkId) loads round constants, MDS matrix, and IV; Update(fields []*fp.Fp) absorbs, permuting whenever the rate fills; Digest() permutes a final time and returns state[0] reinterpreted as an Fq scalar.",
},
"BitVector": {
type: "struct",
description: "Variable-length bit buffer with Append, Insert, Delete, Set, Element, Length, Bytes. Used to pack transaction fields into field elements. Documented as not thread safe.",
},
"Permutation.Permute(ctx *Context)": {
type: "",
description: "Runs the permutation in place on a Context.",
},
}}
/>
```go
ctx := new(mina.Context).Init(mina.ThreeW, mina.MainNet)
ctx.Update(fields) // []*fp.Fp
digest := ctx.Digest() // *fq.Fq
```
:::warning[`Context.Init` reports failure by returning `nil`]
An out-of-range `Permutation` or `NetworkType` makes `Init` return a nil `*Context` rather than an
error. The very next `ctx.Update(...)` panics with a nil dereference. Check the return value.
:::
The task-facing type `roinput` — the random-oracle input builder that packs a transaction into field
elements and bits — is **unexported**. You cannot construct one, and the only way to reach that
packing logic is through `SignTransaction`, `SignMessage`, or `MinaTSchnorrHandler`.
### Bridging to threshold signing
`MinaTSchnorrHandler` adapts Mina's challenge derivation to the library's FROST-style threshold
Schnorr signer, so a Mina key can be split across parties. See
[threshold Ed25519](/threshold/threshold-ed25519) for the signer this plugs into.
```go
func (m MinaTSchnorrHandler) DeriveChallenge(
msg []byte,
pubKey curves.Point, // must be a *curves.PointPallas
r curves.Point, // must be a *curves.PointPallas
) (curves.Scalar, error)
```
`msg` is **not** an arbitrary message: the handler calls `Transaction.UnmarshalBinary(msg)` on it, so
it must be the 175-byte transaction encoding produced by `Transaction.MarshalBinary`. Anything else
returns "invalid byte sequence".
:::danger[`DeriveChallenge` ignores the transaction's network id]
The handler hardcodes `msgHash(pk, R.X(), input, ThreeW, MainNet)`. It parses `msg` into a
`Transaction`, which carries a `NetworkId` field, and then discards it. Threshold-signing a testnet
transaction through this handler produces a challenge computed with the **mainnet** sponge IV, so
the resulting signature will not verify with `VerifyTransaction` for any `NetworkId` other than
`MainNet`. There is no way to override this from the outside.
:::
### Mina caveats
:::danger[`Transaction.UnmarshalJSON` does not work]
The method type-asserts `Body[1]` from `any` directly to its concrete struct types
(`txnBodyPaymentJson`, `[2]any`). `encoding/json` decodes an unconstrained `any` into
`map[string]any` and `[]any`, so those assertions can never succeed. Every call returns
`unexpected type`. Confirmed against a well-formed payload built from the fixtures in
`keys_test.go`:
```
UnmarshalJSON err = unexpected type
SourcePk nil: true Amount: 0 TokenId: 0
```
Even if the assertions were fixed, the method never assigns `SourcePk`, `Amount`, `TokenId`,
`Locked`, or `Tag`; it computes a `sourcePk` local and drops it; it swallows a `ParseAddress` error
in the payment branch with a bare `return nil`; and it indexes the decoded memo as
`memo[2 : 2+memo[1]]` without a length check. There is no corresponding `MarshalJSON`. Build
`Transaction` values in Go and use `MarshalBinary` for the wire; do not route Mina transactions
through this JSON path.
:::
:::warning[Memos longer than 32 bytes corrupt the encoding]
`MarshalBinary` writes `out[57] = byte(len(txn.Memo))` and then `copy(out[58:90], txn.Memo)`. The
copy caps at the 32-byte destination, but the recorded length does not — so a 40-byte memo produces
a transaction claiming length 40 with only 32 bytes present, and a 256-byte memo records length 0.
Neither is rejected. Truncate memos to 32 bytes yourself before signing.
:::
:::warning[`SignMessage` and `VerifyMessage` are MainNet-only]
Both hardcode `MainNet`; there is no network parameter and no variant that takes one. Only
`SignTransaction` / `VerifyTransaction` honour `NetworkId`.
:::
:::note[Nil public keys panic]
`Transaction.MarshalBinary` dereferences `FeePayerPk`, `SourcePk`, and `ReceiverPk` without nil
checks. A partially-filled `Transaction` panics rather than returning an error.
:::
## NEM: Ed25519 with Keccak-512
```go
import "github.com/sonr-io/crypto/signatures/schnorr/nem"
```
NEM (and its successor Symbol) adopted Ed25519 before the standard settled and substituted
**Keccak-512** for SHA-512 in every hashing step — key expansion, nonce derivation, and challenge
computation. There is one further quirk: the seed is **byte-reversed** before hashing, which the
source comments call a "weird required step to get compatibility with the NEM test vectors".
Everything else is textbook Ed25519 over Edwards25519, and this package is unusually well grounded:
`ed25519_keccak_test.go` checks derivation and signing against fixtures pulled from
[symbol/test-vectors](https://github.com/symbol/test-vectors), with a comment noting that all 10000
vectors passed at the time of writing.
### Constants and API
| Constant | Value |
| --- | --- |
| `PublicKeySize` | `32` |
| `PrivateKeySize` | `64` |
| `SignatureSize` | `64` |
| `SeedSize` | `32` |
<TypeTable
type={{
"GenerateKey(rand io.Reader)": {
type: "(PublicKey, PrivateKey, error)",
description: "Reads a 32-byte seed (crypto/rand when rand is nil) and expands it. Public key first.",
},
"NewKeyFromSeed(seed []byte)": {
type: "(PrivateKey, error)",
description: "Deterministic derivation from a 32-byte seed. Reverses the seed, hashes with Keccak-512, clamps the low 32 bytes into a scalar, and stores seed ‖ publicKey.",
},
"Sign(privateKey PrivateKey, message []byte)": {
type: "([]byte, error)",
description: "64-byte signature. Errors — does not panic — on a wrong-length key, despite what the doc comment says.",
},
"Verify(publicKey PublicKey, message, sig []byte)": {
type: "(bool, error)",
description: "Note the two return values: check both.",
},
"Keccak512(data []byte)": {
type: "([]byte, error)",
description: "Exported because the surrounding NEM protocol hashes with it too — addresses, block hashes.",
},
"PrivateKey.Public()": {
type: "crypto.PublicKey",
description: "Returns a nem.PublicKey as crypto.PublicKey. Type-assert it: priv.Public().(nem.PublicKey).",
},
"PrivateKey.Seed()": {
type: "[]byte",
description: "A copy of the leading 32 bytes.",
},
"PrivateKey.Sign(rand, message, opts)": {
type: "([]byte, error)",
description: "The crypto.Signer interface. opts.HashFunc() must be crypto.Hash(0); rand is ignored because signing is deterministic.",
},
"PublicKey.Bytes()": {
type: "[]byte",
description: "The underlying slice.",
},
}}
/>
Grounded in `TestPrivToPubkey` and `TestSigs`.
```go nem.go
package main
import (
"encoding/hex"
"fmt"
"log"
"github.com/sonr-io/crypto/signatures/schnorr/nem"
)
func main() {
// A NEM test vector: private key and its expected public key.
seed, err := hex.DecodeString(
"575DBB3062267EFF57C970A336EBBC8FBCFE12C5BD3ED7BC11EB0481D7704CED")
if err != nil {
log.Fatal(err)
}
priv, err := nem.NewKeyFromSeed(seed)
if err != nil {
log.Fatal(err)
}
pub := priv.Public().(nem.PublicKey)
fmt.Println("public key:",
hex.EncodeToString(pub.Bytes()))
// c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844
// — the public key the symbol/test-vectors fixture pairs with that seed.
msg := []byte("transfer")
sig, err := nem.Sign(priv, msg)
if err != nil {
log.Fatal(err)
}
ok, err := nem.Verify(pub, msg, sig)
if err != nil {
log.Fatal(err)
}
fmt.Println("valid:", ok, "bytes:", len(sig)) // true 64
}
```
### NEM caveats
:::danger[These keys and signatures are NOT interchangeable with `crypto/ed25519`]
The hash function differs at every step, and the seed is reversed before expansion. Concretely:
- `nem.NewKeyFromSeed(seed)` and `ed25519.NewKeyFromSeed(seed)` derive **different public keys** from
the same seed.
- A `nem.PrivateKey` handed to `ed25519.Sign` produces a signature that does not verify under the
public key embedded in that same private key.
- A signature from `nem.Sign` will never verify with `ed25519.Verify`, and vice versa.
Both types are `[]byte` with identical 32/64-byte layouts, so nothing in the type system stops you
mixing them. Never share a seed, a key, or a signature between the two. If you need standards
Ed25519, use `crypto/ed25519` or [threshold Ed25519](/threshold/threshold-ed25519).
:::
:::warning[Doc comments promise panics the code does not deliver]
`Sign`, `Verify`, and `NewKeyFromSeed` carry doc comments inherited from the standard library saying
"It will panic if len(...) is not ...". The implementations return an `error` instead. Code written
against the comments — assuming a wrong length is unreachable and therefore ignoring the error — will
silently proceed with a `nil` signature.
:::
:::note[`Verify` returns `(bool, error)`]
Unlike `crypto/ed25519.Verify`, which returns a bare `bool`. The error path fires when the internal
Keccak write fails or the public key is malformed. Check both values.
:::
## Related
<CardGroup cols={2}>
<Card title="Threshold Ed25519" href="/threshold/threshold-ed25519" icon="users">
The FROST signer that `MinaTSchnorrHandler` plugs its challenge derivation into.
</Card>
<Card title="Curve abstraction" href="/foundations/curves" icon="git-branch">
Pallas, `PointPallas`, and `ScalarPallas` — the types the Mina package builds on.
</Card>
<Card title="Signature index" href="/signatures" icon="pen-tool">
Back to the scheme selection guide.
</Card>
<Card title="Security notes" href="/reference/security" icon="shield">
The defects on this page, collected with the rest.
</Card>
</CardGroup>
+377
View File
@@ -0,0 +1,377 @@
---
title: ECDSA Utilities
description: Canonical low-S form, malleability defence, fixed-width signature codecs, and RFC 6979-style deterministic signing on top of the standard library's crypto/ecdsa.
sidebar:
order: 4
icon: check-check
---
The `ecdsa` package is a thin layer of utilities over the standard library. It does not define a key
type, a curve, or a signature struct — it operates on `*ecdsa.PrivateKey`, `*ecdsa.PublicKey`,
`elliptic.Curve`, and raw `*big.Int` pairs from `crypto/ecdsa`. Two problems are solved here that
the standard library leaves to you:
1. **Malleability.** ECDSA signatures are not unique per message. Two different byte strings verify
equally well, so signature bytes cannot be used as an identifier.
2. **Nonce dependence.** `ecdsa.Sign` needs entropy at signing time, and a bad or repeated nonce
leaks the private key outright.
Reach for this package when you store, index, deduplicate, or compare ECDSA signatures, or when you
need signing to be reproducible on a device you do not trust to have a good RNG. Do **not** reach
for it for ordinary sign-and-verify: `crypto/ecdsa` already does that, correctly and with a
constant-time implementation. Everything here is `math/big` arithmetic and makes no constant-time
claim.
```go
import "github.com/sonr-io/crypto/ecdsa"
```
:::note
The import path collides with the standard library's `crypto/ecdsa`. In any file that uses both you
must alias one — the examples below alias the standard library as `stdecdsa`.
:::
## Malleability, and why canonical form matters
An ECDSA signature is a pair `(r, s)` over a curve of prime order `N`. Verification checks a
relation that is symmetric in the sign of `s`:
$$
(r,\; s) \text{ valid} \iff (r,\; N - s) \text{ valid}
$$
Anyone who observes a valid signature can therefore produce a *second*, different, equally valid
signature for the same message and the same key — without knowing the private key. The consequences
are practical, not theoretical:
- **Signature bytes are not an identifier.** Keying a database, a replay-protection cache, or a
transaction ID on raw signature bytes lets an attacker create an unbounded number of distinct
entries for one authorised action. This is the Bitcoin transaction-malleability bug.
- **Byte equality is not signature equality.** `bytes.Equal(sigA, sigB) == false` does not mean two
parties signed different things.
The fix everybody converged on is a **canonical form**: of the two valid `s` values, always use the
smaller one, `s <= N/2`. This package calls that "canonical" and provides both the coercion and the
strict rejection.
<TypeTable
type={{
"IsCanonical(s, N *big.Int)": {
type: "bool",
description: "True when s <= N/2. Does not range-check s, and returns false for nil inputs.",
},
"MakeCanonical(r, s, N *big.Int)": {
type: "(*big.Int, *big.Int)",
description: "Returns (r, min(s, N-s)). No validation and no error. Returns its inputs unchanged if any is nil.",
},
"IsSignatureCanonical(r, s *big.Int, curve elliptic.Curve)": {
type: "bool",
description: "Full check: r in [1, N-1] AND s in [1, N/2]. False on any nil argument.",
},
"CanonicalizeSignature(r, s, curve)": {
type: "(*big.Int, *big.Int, error)",
description: "Range-checks both scalars, then returns copies with s reduced to canonical form. Errors on nil arguments or out-of-range r or s.",
},
"NormalizeSignature(r, s, curve)": {
type: "(*big.Int, *big.Int, error)",
description: "Currently a direct pass-through to CanonicalizeSignature. The name suggests more; the body does not do more.",
},
"RejectNonCanonical(r, s, curve)": {
type: "error",
description: "Strict mode: returns an error instead of coercing. Use this on ingress when you want to refuse malleated signatures outright.",
},
"ValidateAndCanonicalizeSignature(pub, hash, r, s)": {
type: "(*big.Int, *big.Int, error)",
description: "Canonicalizes, then verifies against pub and hash. Falls back to verifying the original pair if the canonical one fails. Errors if neither verifies.",
},
"CompareSignatures(r1, s1, r2, s2, curve)": {
type: "(bool, error)",
description: "Canonicalizes both pairs and compares. This is the correct way to ask whether two signatures are the same signature.",
},
}}
/>
`MakeCanonical` and `IsCanonical` take a bare `*big.Int` order rather than a curve, which makes them
usable with secp256k1 or any other order you have on hand; the rest take an `elliptic.Curve`.
:::warning[`MakeCanonical` returns `r` by reference]
The internal helper returns the *same* `*big.Int` you passed for `r`, and returns your original `s`
pointer unchanged when it was already canonical. Only the flipped case allocates. Mutating the
result mutates your input. `CanonicalizeSignature` does not have this problem — it copies both
scalars before touching them.
:::
### Choosing between coerce and reject
<Tabs>
<Tab title="Coerce">
`CanonicalizeSignature` accepts a malleated signature and quietly normalises it. Right for a
verifier that must interoperate with signers you do not control, and for anything you are about
to store or hash.
</Tab>
<Tab title="Reject">
`RejectNonCanonical` refuses. Right for a consensus rule or a protocol where you have declared
that only canonical signatures are well-formed — coercion there would let two encodings of the
same intent both be "accepted", which is exactly the ambiguity you set out to remove.
</Tab>
</Tabs>
Grounded in `TestCanonicalizeSignature` and `TestIsSignatureCanonical`.
```go canonical.go
package main
import (
stdecdsa "crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"log"
"math/big"
"github.com/sonr-io/crypto/ecdsa"
)
func main() {
curve := elliptic.P256()
priv, err := stdecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Fatal(err)
}
digest := sha256.Sum256([]byte("transfer 100 to bob"))
r, s, err := stdecdsa.Sign(rand.Reader, priv, digest[:])
if err != nil {
log.Fatal(err)
}
// stdlib Sign does not normalise, so first pin down which of the pair is low-S.
N := curve.Params().N
rLow, sLow, err := ecdsa.CanonicalizeSignature(r, s, curve)
if err != nil {
log.Fatal(err)
}
// Anyone can produce this second, equally valid, non-canonical signature.
sHigh := new(big.Int).Sub(N, sLow)
fmt.Println("high-S still verifies:",
stdecdsa.Verify(&priv.PublicKey, digest[:], rLow, sHigh)) // true
// Both collapse to the same canonical pair...
same, err := ecdsa.CompareSignatures(rLow, sLow, rLow, sHigh, curve)
if err != nil {
log.Fatal(err)
}
fmt.Println("same signature:", same) // true
// ...and to the same fixed-width encoding.
a, err := ecdsa.SignatureBytes(rLow, sLow, curve)
if err != nil {
log.Fatal(err)
}
b, err := ecdsa.SignatureBytes(rLow, sHigh, curve)
if err != nil {
log.Fatal(err)
}
fmt.Println("identical bytes:", string(a) == string(b), len(a)) // true 64
// Strict ingress: refuse rather than repair.
fmt.Println("high-S accepted:", ecdsa.IsSignatureCanonical(rLow, sHigh, curve)) // false
if err := ecdsa.RejectNonCanonical(rLow, sHigh, curve); err != nil {
fmt.Println("rejected:", err) // signature is not in canonical form
}
}
```
## Fixed-width codecs
`SignatureBytes` and `SignatureFromBytes` are a canonical, length-prefixed-free alternative to ASN.1
DER. The layout is the concatenation of two big-endian, zero-padded scalars:
| Field | Offset | Length |
| --- | --- | --- |
| `r` | `0` | `byteSize` |
| `s` | `byteSize` | `byteSize` |
where `byteSize = (curve.Params().BitSize + 7) / 8`. For P-256 that is 32, so a signature is exactly
**64 bytes**; P-384 gives 96, P-521 gives 132.
```go
raw, err := ecdsa.SignatureBytes(r, s, curve) // canonicalizes, then encodes
r2, s2, err := ecdsa.SignatureFromBytes(raw, curve) // decodes, then canonicalizes
```
Both directions canonicalize, which is what makes the encoding a stable identifier: `(r, s)` and
`(r, N-s)` produce byte-identical output, and a decode always yields a canonical pair.
:::note[The size comes from `BitSize`, not from `N`]
`byteSize` is derived from the curve's field bit size, while `r` and `s` are reduced mod `N`. For
the NIST P-curves these agree. For a curve where the group order is meaningfully shorter than the
field, the encoding still uses the field width — so do not assume this format matches another
library's fixed-width convention without checking.
:::
:::warning[Not DER, not `[R || S]` with a recovery byte]
This is a bare 2×`byteSize` concatenation. It is not ASN.1 DER (what `ecdsa.SignASN1` emits), and it
carries no recovery id, so you cannot recover the public key from it the way Ethereum's 65-byte
format allows. Do not feed these bytes to a verifier expecting either of those.
:::
## Deterministic signing
`DeterministicSign` removes the randomness from ECDSA signing. Instead of drawing `k` from an RNG,
it derives `k` from the private key and the message digest through an HMAC-DRBG construction in the
style of [RFC 6979](https://datatracker.ietf.org/doc/html/rfc6979), using **HMAC-SHA-256** as the
fixed underlying primitive.
<TypeTable
type={{
"DeterministicSign(priv *ecdsa.PrivateKey, hash []byte)": {
type: "(*big.Int, *big.Int, error)",
description: "Derives k deterministically, signs, and returns an already-canonical (low-S) pair. Errors on a nil key, a nil D, or an empty hash.",
},
"VerifyDeterministic(pub *ecdsa.PublicKey, hash []byte, r, s *big.Int)": {
type: "bool",
description: "Range-checks r in [1, N-1] and s in [1, N/2], then delegates to crypto/ecdsa.Verify. Rejects a high-S signature that stdlib Verify would accept.",
},
}}
/>
Why determinism is worth having:
- **No entropy dependence at signing time.** An embedded device, a freshly-booted VM, or a
deterministic test environment can sign correctly without a seeded CSPRNG.
- **Reproducibility.** The same key and message always yield the same signature, so signatures can
be regenerated, diffed, and used as cache keys.
- **No silent RNG failure.** A subtly broken RNG produces biased nonces, and nonce bias leaks the
private key over enough signatures. Removing the RNG removes that failure mode.
Grounded in `TestDeterministicSign` and `TestCanonicalSignature`.
```go deterministic.go
package main
import (
stdecdsa "crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"log"
"github.com/sonr-io/crypto/ecdsa"
)
func main() {
priv, err := stdecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
digest := sha256.Sum256([]byte("test message for deterministic signing"))
r1, s1, err := ecdsa.DeterministicSign(priv, digest[:])
if err != nil {
log.Fatal(err)
}
r2, s2, err := ecdsa.DeterministicSign(priv, digest[:])
if err != nil {
log.Fatal(err)
}
fmt.Println("reproducible:", r1.Cmp(r2) == 0 && s1.Cmp(s2) == 0) // true
// Output is already low-S.
fmt.Println("canonical:", ecdsa.IsCanonical(s1, priv.Curve.Params().N)) // true
// Verifies with the standard library, and with the strict wrapper.
fmt.Println("stdlib ok:", stdecdsa.Verify(&priv.PublicKey, digest[:], r1, s1))
fmt.Println("strict ok:", ecdsa.VerifyDeterministic(&priv.PublicKey, digest[:], r1, s1))
}
```
The output is normalised to low-S inside `signWithK` before it is returned, so you never need to
call `MakeCanonical` on a `DeterministicSign` result.
:::danger[Deterministic does not mean "nonce reuse is now safe"]
Determinism eliminates the *accidental* nonce collision, not the consequence of one. If the same `k`
is ever used for two different messages under the same key, both signatures share an `r`, and
solving the two-equation system recovers the private key immediately:
$$
d = \frac{s_1 k - h_1}{r} \quad\text{with}\quad k = \frac{h_1 - h_2}{s_1 - s_2}
$$
The derivation binds `k` to both the private key and the message digest — `generateK` seeds the DRBG
with `priv.D` and `hashToInt(hash)` — so two *different* messages under one key can never collide,
which is the whole point. Two residual hazards remain:
- Signing the **same digest** twice returns byte-identical output. That is correct behaviour, but it
means a signature is a stable fingerprint of `(key, message)`; do not treat repeated signatures as
evidence of repeated intent.
- Deterministic signers are the standard target for **fault injection**: an attacker who can glitch
one of two signings of the same message obtains a correct and a faulted signature sharing `k`, and
the equation above applies. If your threat model includes physical access, pair determinism with
a verify-after-sign check.
:::
:::danger[Mostly RFC 6979-conformant — and the exception is silent]
The derivation implements RFC 6979 steps (a) through (j), but where the RFC specifies
`bits2octets(H(m))` — a **fixed-width**, mod-`q`-reduced octet string — the code feeds
`bits2int(H(m)).Bytes()` into steps (f) and (h). `big.Int.Bytes()` drops leading zero bytes and does
not reduce mod `q`.
In the common case that makes no difference, and the implementation reproduces RFC 6979 vectors
exactly. Checked against RFC 6979 A.2.5 (P-256 / SHA-256 / `"sample"`, key
`C9AFA9D8…120F6721`), this package returns
`r = EFD48B2AACB6A8FD1140DD9CD45E81D69D2C877B56AAF991C34D0EA84EAF3716` — the vector's value.
It diverges whenever the digest, read as an integer, has fewer than `byteSize` significant
bytes — that is, whenever `H(m)` begins with a zero byte, roughly one message in 256 on P-256.
Compared against a reference RFC 6979 derivation on the digest
`00EEECC1EB031E204A211DEC04B6B42B1F446802058873A1A8F36308FE62EC0D`:
```
rfc6979 r = 69E8682EEF48289BD67EE185E5756BC416F8D02900249AFC3AA19F9F1B28908F
package r = 5B0E3B459095B47BD231012F545A4300962B6044AC43B3CAC4892C143AA9207B
```
The signature is still perfectly valid ECDSA and verifies everywhere; only the *nonce derivation*
disagrees. But an intermittent, digest-dependent disagreement is worse than a consistent one: a
cross-implementation compatibility test will pass 255 times out of 256. `deterministic_test.go`
contains no RFC 6979 vectors at all — it only checks that repeated signing agrees with itself. Do
not build a protocol in which two different libraries must derive the same `k`.
:::
:::warning[Not constant time]
Every operation here is `math/big` arithmetic: `Div`, `Sub`, `Cmp`, `ModInverse`, `Mul`. `math/big`
makes no constant-time guarantee, and `signWithK` performs the scalar multiplication and the modular
inversion with ordinary variable-time code. On a machine where an attacker can measure your signing,
prefer `crypto/ecdsa.SignASN1`, whose P-256 path is constant time. See
[security notes](/reference/security).
:::
:::note[`VerifyDeterministic` is stricter than `stdecdsa.Verify`]
It rejects `s > N/2`. A perfectly valid signature produced by a signer that does not normalise will
fail here. That is deliberate — it is the strict-ingress policy applied to verification — but it
means `VerifyDeterministic` is not a drop-in replacement for the standard library verifier.
:::
## Related
<CardGroup cols={2}>
<Card title="Threshold ECDSA" href="/threshold/threshold-ecdsa" icon="users">
Produce an ECDSA signature from key shares that never combine. The canonicalization helpers here
apply to its output too.
</Card>
<Card title="MPC enclave" href="/identity/mpc-enclave" icon="fingerprint">
The two-party ECDSA wrapper this library ships as its headline API.
</Card>
<Card title="Curve abstraction" href="/foundations/curves" icon="git-branch">
The library's own curve types — distinct from the `crypto/elliptic` types this package uses.
</Card>
<Card title="Security notes" href="/reference/security" icon="shield">
Constant-time gaps and standards deviations across the library.
</Card>
</CardGroup>
+179
View File
@@ -0,0 +1,179 @@
---
title: Signatures
description: Choosing between BLS aggregation, BBS+ selective disclosure, ECDSA canonicalization, verifiable random functions, and the chain-specific Schnorr variants.
sidebar:
order: 1
icon: pen-tool
---
Five very different things live under this heading, and they are not interchangeable. Before you
pick one, decide which property you actually need: **aggregation** (many signatures collapse into
one), **selective disclosure** (a holder proves a subset of signed attributes), **determinism and
canonical encoding** (the same message always yields the same bytes), **verifiable randomness** (an
output nobody can predict but everybody can check), or **wire compatibility with a specific
blockchain**.
Every package here is a distinct construction with its own key type. There is no shared `Signer`
interface across them, and keys from one scheme are never valid in another.
## Pick a scheme
| Goal | Package | Page |
| --- | --- | --- |
| Collapse N signatures over N messages into one 96-byte object | `signatures/bls/bls_sig` | [BLS](/signatures/bls) |
| Multi-signature: N signers, one message, one aggregate check | `signatures/bls/bls_sig` (`SigPop`) | [BLS](/signatures/bls) |
| Split a signing key into `t`-of-`n` shares with no interaction | `signatures/bls/bls_sig` | [BLS](/signatures/bls) |
| Sign a vector of attributes; let the holder reveal only some | `signatures/bbs` | [BBS+](/signatures/bbs) |
| Issue a credential over messages the issuer must not see | `signatures/bbs` | [BBS+](/signatures/bbs) |
| Kill ECDSA signature malleability before storing or comparing | `ecdsa` | [ECDSA utilities](/signatures/ecdsa) |
| Sign with ECDSA without depending on runtime entropy | `ecdsa` | [ECDSA utilities](/signatures/ecdsa) |
| Unpredictable-but-verifiable per-message output (leader election, lotteries) | `vrf` | [VRF](/signatures/vrf) |
| Sign a Mina payment or delegation transaction | `signatures/schnorr/mina` | [Chain schemes](/signatures/chain-schemes) |
| Produce a NEM/Symbol Keccak-flavoured Ed25519 signature | `signatures/schnorr/nem` | [Chain schemes](/signatures/chain-schemes) |
Some adjacent things are documented elsewhere:
- The **interactive Schnorr proof of knowledge** (`zkp/schnorr`) is a ZKP, not a signature scheme —
see [zero-knowledge/schnorr](/zero-knowledge/schnorr).
- **Threshold ECDSA** and **threshold Ed25519** (FROST) produce ordinary ECDSA / Ed25519 signatures
from distributed shares — see [threshold ECDSA](/threshold/threshold-ecdsa) and
[threshold Ed25519](/threshold/threshold-ed25519). BLS threshold signing on this page is a
different, much simpler construction: it needs no rounds of interaction.
## What these packages assume about curves
`signatures/bbs` and the Mina scheme are written against the
[`core/curves`](/foundations/curves) `Curve` / `Point` / `Scalar` abstraction — BBS+ specifically
requires a `*curves.PairingCurve` (`curves.BLS12381(...)`). `signatures/bls/bls_sig` bypasses the
abstraction entirely and calls the low-level `core/curves/native/bls12381` backend directly, so it
is hard-wired to BLS12-381. The `ecdsa` package operates on stdlib `crypto/ecdsa` and
`crypto/elliptic` types, and `vrf` on a vendored Edwards25519 implementation.
## The shared proof toolkit: `signatures/common`
`signatures/common` holds the sigma-protocol plumbing that BBS+ (and code composing proofs with
BBS+) builds on. It is a building-block package — you rarely import it alone, but you will import it
to construct BBS+ proof messages.
| Symbol | Kind | Purpose |
| --- | --- | --- |
| `Challenge` | `= curves.Scalar` | Fiat-Shamir challenge value |
| `Commitment` | `= curves.Point` | Pedersen commitment to one or more scalars |
| `Nonce` | `= curves.Scalar` | Freshness / replay protection in a proof |
| `SignatureBlinding` | `= curves.PairingScalar` | Blinding factor for blind signing |
| `HmacDrbg` | struct | HMAC deterministic random bit generator, any hash, auto-reseeding |
| `ProofCommittedBuilder` | struct | Accumulates `(point, scalar)` commitments into Schnorr proofs |
| `ProofMessage` | interface | Classifies a signed message as revealed or hidden |
The four aliases are Go **type aliases**, not defined types: a `common.Nonce` *is* a
`curves.Scalar`, so no conversion is needed and the compiler will not stop you passing a challenge
where a nonce belongs. Treat the names as documentation, not as type safety.
### `ProofMessage` and its three implementations
`ProofMessage` is how a BBS+ prover declares, per message, whether it is disclosed:
```go
type ProofMessage interface {
IsHidden() bool
GetBlinding(reader io.Reader) curves.Scalar
GetMessage() curves.Scalar
}
```
<TypeTable
type={{
RevealedMessage: {
type: "struct { Message curves.Scalar }",
description: "IsHidden() == false. The verifier learns this message. GetBlinding returns nil.",
},
ProofSpecificMessage: {
type: "struct { Message curves.Scalar }",
description: "IsHidden() == true. A fresh random blinding factor is drawn from the reader, used only by this proof.",
},
SharedBlindingMessage: {
type: "struct { Message, Blinding curves.Scalar }",
description: "IsHidden() == true, but you supply the blinding factor so the same hidden value can be linked across several proofs (e.g. a BBS+ proof plus a range proof over the same attribute).",
},
}}
/>
### `ProofCommittedBuilder`
A small accumulator for Schnorr-style proofs of knowledge of a linear combination:
```go
import "github.com/sonr-io/crypto/signatures/common"
builder := common.NewProofCommittedBuilder(curve)
_ = builder.CommitRandom(basePoint, crand.Reader) // blinding for a secret you know
_ = builder.Commit(otherPoint, knownScalar) // fixed scalar
bytes := builder.GetChallengeContribution() // feed into your transcript
proofs, err := builder.GenerateProof(challenge, secrets)
```
`GetChallengeContribution` returns the compressed encoding of `SumOfProducts(points, scalars)` — the
aggregate commitment. `GenerateProof` then returns one response scalar per commitment, computed as
`secret*challenge + blinding`, and errors if `len(secrets)` does not match the number of
commitments. `Get(index)` retrieves the `(point, scalar)` pair at a position, returning `(nil, nil)`
out of range. The builder caps out at roughly 65535 commitments.
### `HmacDrbg`
```go
drbg := common.NewHmacDrbg(entropy, nonce, personalization, sha256.New)
buf := make([]byte, 64)
_, _ = drbg.Read(buf)
drbg.Reseed(moreEntropy)
```
It satisfies `io.Reader`, so it can be handed to any API here that takes a `reader` — which is how
you make an otherwise randomised proof reproducible in a test.
:::warning[These are internal building blocks]
`signatures/common` carries no package-level documentation and no tests of its own; it is exercised
only indirectly through `signatures/bbs`. If you use `ProofCommittedBuilder` to build a *new*
protocol rather than to compose with BBS+, you are on your own for soundness — nothing in this
repository validates that usage.
:::
## Caveats that apply across this section
:::danger[No audit, and no uniform error discipline]
None of these packages has a published security audit. They also differ in how they report failure:
BLS returns `(bool, error)` and you must check **both**; BBS+ `Verify` returns a plain `error`;
`PokSignatureProof.Verify` returns a bare `bool`; the VRF `Verify` returns a bare `bool`. Copying an
error-handling idiom from one page to another will silently drop failures. See
[security notes](/reference/security).
:::
:::note
Serialization is `encoding.BinaryMarshaler` / `BinaryUnmarshaler` throughout, but several types
(BBS+ `Signature`, `PokSignatureProof`, `BlindSignature`, `BlindSignatureContext`) need an
`Init(curve)` call before `UnmarshalBinary`, because the wire format does not name its curve.
:::
## Where to next
<CardGroup cols={2}>
<Card title="BLS" href="/signatures/bls" icon="combine">
Two instantiations, three ciphersuites, aggregation, multi-signatures, and non-interactive
threshold keygen on BLS12-381.
</Card>
<Card title="BBS+" href="/signatures/bbs" icon="eye-off">
Sign a vector of attributes, then prove possession while revealing only the ones you choose.
</Card>
<Card title="ECDSA utilities" href="/signatures/ecdsa" icon="check-check">
Malleability, canonical low-S form, fixed-width codecs, and deterministic nonce derivation.
</Card>
<Card title="VRF" href="/signatures/vrf" icon="dice-5">
Verifiable pseudorandom outputs over Edwards25519 with SHAKE256.
</Card>
<Card title="Chain schemes" href="/signatures/chain-schemes" icon="link">
Mina Schnorr over Pallas/Poseidon and NEM's Keccak-512 Ed25519 variant.
</Card>
<Card title="Curve abstraction" href="/foundations/curves" icon="git-branch">
The `Curve` / `Point` / `Scalar` triple that BBS+ and the Mina scheme are generic over.
</Card>
</CardGroup>
+8
View File
@@ -0,0 +1,8 @@
import { defineMeta } from "blume";
export default defineMeta({
title: "Signatures",
icon: "pen-tool",
order: 4,
pages: ["index", "bls", "bbs", "ecdsa", "vrf", "chain-schemes"],
});
+244
View File
@@ -0,0 +1,244 @@
---
title: Verifiable Random Function
description: A bespoke VRF over Edwards25519 using SHAKE256 and the Elligator map — unpredictable outputs that anyone holding the public key can verify.
sidebar:
order: 5
icon: dice-5
---
A verifiable random function is a keyed hash with a proof. Given a secret key and an input message,
it produces an output that looks uniformly random to anyone without the key, yet is **uniquely
determined** by the key and message, and comes with a proof that lets anyone holding the public key
confirm the output is the right one. It is the primitive you want whenever a system needs randomness
that participants cannot grind and cannot dispute.
```go
import "github.com/sonr-io/crypto/vrf"
```
## When to use one
- **Leader election.** Each validator computes `VRF_sk(round_seed)`. Whoever's output falls below a
threshold is the leader, and can prove it. Nobody can pre-compute another validator's output, and
nobody can retry with a different key without publishing that key.
- **Verifiable lotteries.** Draw a winner from a beacon value; the operator proves the draw was
honest without revealing the key.
- **Private lookup keys.** In a key-transparency directory (this construction's origin), the map
index for a username is `VRF_sk(username)`, so the directory can prove a name's absence without
its tree structure leaking the set of registered names to an enumerating client.
Do **not** reach for a VRF where a plain signature would do — this package offers no way to sign
arbitrary data, and verification only ever answers "is this the correct output for this message".
And do not treat the output as a commitment: it is a deterministic function of the message, so once
a proof is published anyone holding the public key can confirm a *guess* at the message by
re-verifying against it. A VRF hides the output from people without the key; it does not hide the
input from people who can guess it.
## The construction
The package doc comment states the scheme exactly. `E` is Curve25519 in Edwards coordinates, `h` is
SHA-3 (specifically SHAKE256 throughout the implementation), `f` is the Elligator map, and `8` is the
cofactor:
$$
H(n) = f(h(n))^8, \qquad \mathrm{VRF}_x(n) = h\!\left(n,\, H(n)^x\right)
$$
The proof is a ChaumPedersen style sigma protocol made non-interactive, proving that the same
secret `x` relates `g → g^x` and `H(n) → H(n)^x`:
$$
\mathrm{Prove}_x(n) = \bigl(c,\; t = r - c\cdot x,\; \mathit{ii} = H(n)^x\bigr)
$$
with `r = h(x, n)` supplying the proof's randomness — so proving, like computing, is fully
deterministic. Verification recomputes the challenge from `g^t · P^c` and `H(n)^t · ii^c` and checks
it equals the challenge carried in the proof, and separately checks that the claimed output equals
`h(n, ii)`.
Concretely, in `vrf.go`: `hashToCurve` runs `sha3.ShakeSum256` over the message, maps the digest with
`extra25519.HashToEdwards`, then applies three successive `GeDouble` calls — multiplication by the
cofactor 8 — to land in the prime-order subgroup. The challenge is
`SHAKE256(g ‖ H(n) ‖ pk ‖ H(n)^x ‖ g^r ‖ H(n)^r ‖ n)` reduced mod the group order. In the code the
challenge scalar is named `s`, which is why the proof layout below reads `s ‖ t ‖ ii` rather than
`c ‖ t ‖ ii`.
## Sizes and constants
| Constant | Value | Meaning |
| --- | --- | --- |
| `PublicKeySize` | `32` | Compressed Edwards point |
| `PrivateKeySize` | `64` | 32-byte seed followed by the 32-byte public key |
| `Size` | `32` | The VRF output |
| `ProofSize` | `96` | `s ‖ t ‖ H(n)^x`, three 32-byte values |
`ErrGetPubKey` is the package's only exported error value; it is declared but never returned by any
exported function in `vrf.go` — `Public()` signals failure through its boolean instead.
## API
<TypeTable
type={{
"GenerateKey(rnd io.Reader)": {
type: "(PrivateKey, error)",
description: "Reads 32 bytes of seed from rnd (crypto/rand when nil), expands it, and writes the derived public key into bytes 32..63. Returns a 64-byte PrivateKey.",
},
"PrivateKey.Public()": {
type: "(PublicKey, bool)",
description: "Returns the trailing 32 bytes of the private key. The bool reports whether the internal type assertion succeeded; in practice it is always true for a well-formed key.",
},
"PrivateKey.Compute(m []byte)": {
type: "[]byte",
description: "The 32-byte VRF output alone. One scalar multiplication plus a hash. No error return — a malformed key produces garbage rather than a failure.",
},
"PrivateKey.Prove(m []byte)": {
type: "(vrf, proof []byte)",
description: "The same 32-byte output plus a 96-byte proof. Roughly three scalar multiplications. Deterministic — no reader, no nonce.",
},
"PublicKey.Verify(m, vrfBytes, proof []byte)": {
type: "bool",
description: "Checks the output against the proof under this public key. Returns false on any length mismatch, any bad point encoding, and any check failure. No error channel.",
},
}}
/>
`Compute` and `Prove` return **the same output** for the same key and message — `Prove` just also
gives you the evidence. Use `Compute` when the holder needs the value locally (deciding whether it
even won a leader election, indexing its own directory) and `Prove` only when the value must be
published. That distinction is the main performance lever in the package: skipping the proof avoids
two of the three scalar multiplications.
## Example
Grounded in `TestHonestComplete` and `TestConvertPrivateKeyToPublicKey`.
```go vrf.go
package main
import (
"bytes"
"fmt"
"log"
"github.com/sonr-io/crypto/vrf"
)
func main() {
// nil reader means crypto/rand.
sk, err := vrf.GenerateKey(nil)
if err != nil {
log.Fatal(err)
}
pk, ok := sk.Public()
if !ok {
log.Fatal(vrf.ErrGetPubKey)
}
round := []byte("epoch-4711")
// Cheap path: the holder just wants the value.
out := sk.Compute(round)
// Publishing path: the value plus evidence.
outFromProof, proof := sk.Prove(round)
fmt.Println("Compute == Prove:", bytes.Equal(out, outFromProof)) // true
fmt.Println("output bytes:", len(out), "proof bytes:", len(proof)) // 32 96
// Anyone with pk can check it.
fmt.Println("verified:", pk.Verify(round, outFromProof, proof)) // true
// Any single flipped bit in the proof fails the check.
tampered := append([]byte(nil), proof...)
tampered[0] ^= 0x01
fmt.Println("tampered verified:", pk.Verify(round, outFromProof, tampered)) // false
}
```
`TestFlipBitForgery` in the package flips bits across the proof and asserts every variant fails.
## Properties a caller can rely on
| Property | What it means here |
| --- | --- |
| **Uniqueness** | For a fixed key and message there is exactly one output that will verify. A prover cannot shop for a favourable value. This is what a plain signature cannot give you. |
| **Pseudorandomness** | Without the secret key, the output is indistinguishable from a uniform 32-byte string, so future outputs cannot be predicted from past ones. |
| **Public verifiability** | Anyone with the 32-byte public key can check an output against its proof — no interaction with the prover, no shared secret. |
| **Determinism** | Both `Compute` and `Prove` derive all internal randomness from the key and message, so there is no RNG at evaluation time and nothing to fail open. |
## Caveats
:::danger[This is a bespoke construction with no standards claim]
The package doc names no RFC and no paper. It is **not** RFC 9381 (`draft-irtf-cfrg-vrf`) — that
standard specifies SHA-512 with `try-and-increment` or `hash_to_curve` for `ECVRF-EDWARDS25519-SHA512-*`
ciphersuites, and a differently structured proof and encoding. This package uses SHAKE256 throughout
and the Elligator map, and packs the proof as `s ‖ t ‖ ii`.
The design matches the VRF shipped with the CONIKS key-transparency work, but nothing in this
repository asserts conformance to any published specification, and there are no cross-implementation
test vectors — `vrf_test.go` contains three self-consistency tests and four benchmarks, nothing more.
**Assume zero interoperability** with any other VRF implementation. If your protocol requires a
counterparty running different software to verify these proofs, this package is the wrong choice.
:::
:::danger[The 64-byte key is not an Ed25519 key, despite looking like one]
`PrivateKey` is `[]byte` with the same 64-byte seed-then-public-key layout as
`crypto/ed25519.PrivateKey`, and `Public()` is implemented by converting to
`golang.org/x/crypto/ed25519.PrivateKey` and calling through. But `GenerateKey` derives the scalar by
expanding the seed with **SHAKE256**, where Ed25519 uses SHA-512. The public key written into bytes
32..63 therefore corresponds to a *different* scalar than standard Ed25519 would derive from the same
seed.
Measured against this repository, for one generated key:
```
ed25519.Sign with the vrf key, verified under its own embedded pubkey: false
ed25519.NewKeyFromSeed(sk[:32]) derives the same public key: false
```
The types will not stop you — both are `[]byte` with identical lengths. Never share a seed, a key,
or a signature between `vrf` and `crypto/ed25519`.
:::
:::warning[No error channel anywhere on the hot path]
`Compute` returns only `[]byte`; `Prove` returns only two slices; `Verify` returns only `bool`. A
truncated key, a wrong-length public key, or a corrupted proof all surface as `false` or as silently
wrong bytes. `Compute` in particular does no validation at all — calling it on a short or zero
`PrivateKey` will panic or return meaningless output rather than report anything. Validate lengths
against `PrivateKeySize` and `PublicKeySize` at your trust boundary.
:::
:::warning[Vendored curve code]
The implementation depends on `internal/ed25519/edwards25519` and `internal/ed25519/extra25519`,
vendored copies rather than the maintained `filippo.io/edwards25519`. `extra25519.HashToEdwards` is
the Elligator implementation, and the package doc notes the map "covers half of E" — the cofactor
clearing by three doublings is what brings the result into the prime-order subgroup. None of this
code is constant-time by construction, and none of it receives upstream security fixes. See
[security notes](/reference/security).
:::
:::note[`Verify` does not check that the public key is in the prime-order subgroup]
It calls `FromBytesBaseGroup` on the encoded point, which rejects non-canonical encodings, but the
protocol's security against a maliciously chosen public key rests on the cofactor clearing inside
`hashToCurve` rather than on validating the key. If public keys arrive from untrusted parties in your
protocol, validate them yourself before storing.
:::
## Related
<CardGroup cols={2}>
<Card title="Schnorr proofs" href="/zero-knowledge/schnorr" icon="binary">
The general sigma protocol this VRF's proof is a specialisation of.
</Card>
<Card title="Threshold Ed25519" href="/threshold/threshold-ed25519" icon="users">
Standards-conformant Ed25519 from distributed shares — the interoperable neighbour of this
package's non-standard key handling.
</Card>
<Card title="Curve abstraction" href="/foundations/curves" icon="git-branch">
The library's Ed25519 curve type, which this package deliberately bypasses.
</Card>
<Card title="Security notes" href="/reference/security" icon="shield">
Vendored code, non-standard constructions, and unvalidated inputs across the library.
</Card>
</CardGroup>