├── README.md ├── examples.md └── specification.md /README.md: -------------------------------------------------------------------------------- 1 | # Multitool 2 | 3 | The next-generation public key cryptography framework. 4 | 5 | * [Specification](specification.md) 6 | * [Example protocols](examples.md) 7 | -------------------------------------------------------------------------------- /examples.md: -------------------------------------------------------------------------------- 1 | # Multitool Examples 2 | 3 | **WARNING: this is a draft document and the examples come without security proofs. Beware of typos and vulnerabilities.** 4 | 5 | * [Orthogonal generators](#orthogonal-generators) 6 | * [Schnorr](#schnorr) 7 | * [VRF](#vrf) 8 | * [Designated Verifier VRF](#designated-verifier-vrf) 9 | * [Ring Signature](#ring-signature) 10 | * [Traceable Ring Signature](#traceable-ring-signature) 11 | * [Abstract Range Proof](#abstract-range-proof) 12 | * [Set Range Proof](#set-range-proof) 13 | * [ChainKD](#chainkd) 14 | 15 | 16 | ## Orthogonal generators 17 | 18 | Additional generators in the group domain-separated by a name can be generated as follows: 19 | 20 | Generator(name) { 21 | return PointHash({"Generator", name}, {protocol.group.base}, "") 22 | } 23 | 24 | Example: 25 | 26 | H := Generator<...>("H") 27 | J := Generator<...>("J") 28 | 29 | 30 | ## Schnorr 31 | 32 | Plain Schnorr uncompressed signature protocol (similar to EdDSA). 33 | 34 | * `x` — secret scalar representing a private key. 35 | * `P` — public key, such that `P == x·G`. 36 | 37 | Definition: 38 | 39 | protocol Schnorr(customlabel) { 40 | name := "Schnorr" 41 | group := "Ristretto" 42 | xof := "SHAKE128" 43 | extralabels := {customlabel} 44 | G := group.base 45 | F(x) := x·G 46 | 47 | genkey(entropy) { 48 | x := ScalarHash("", {entropy}, {}, "") 49 | P := F(x) 50 | return (P, x) 51 | } 52 | 53 | sign(x, entropy, P, msg) { 54 | e,r,R := Commit("", {F}, {entropy,x}, {P}, msg) 55 | s := Prove(e, {r}, {x}) 56 | return (R,s) 57 | } 58 | 59 | verify(R, s, P, msg) { 60 | e := ChallengeHash("", {R}, {P}, msg) 61 | _,R’ := Recommit("", {F}, e, {s}, {P}, {P}, msg) 62 | return group.equal(R, R’) 63 | } 64 | } 65 | 66 | 67 | ## VRF 68 | 69 | Simple VRF maps an arbitrary-length string `msg` to a verifiably random outut keyed with public key `P`. 70 | 71 | protocol VRF(customlabel) { 72 | name := "VRF" 73 | group := "Ristretto" 74 | xof := "SHAKE128" 75 | extralabels := {customlabel} 76 | 77 | G := group.base 78 | F0(x) := x·G 79 | F1(P,msg,x) := x·PointHash("", {P}, msg) 80 | 81 | commit(x, P, msg) { 82 | V := F1(P,msg,x) 83 | h := Compress("", group.encode(V)) 84 | return h 85 | } 86 | 87 | sign(x, P, entropy, msg) { 88 | P := F0(x) 89 | V := F1(P,msg,x) 90 | e,r,_,_ := Commit("", {F0, F1(P,msg)}, {entropy,x}, {P,V}, msg) 91 | s := Prove(e, {r}, {x}) 92 | return (V,e,s) 93 | } 94 | 95 | verify((V,e,s), P, msg) { 96 | e’,_,_ := Recommit("", {F0, F1(P,msg)}, e, {s}, {P,V}, {P,V}, msg) 97 | if e’ == e { 98 | h := Compress("", group.encode(V)) 99 | return h 100 | } else { 101 | return nil 102 | } 103 | } 104 | } 105 | 106 | ## Designated Verifier VRF 107 | 108 | This is a variant of VRF above, but with a 2-item ring signature that allows forgery by the designated verifier 109 | identified by the key pair `D,d` (`D == d·G`). 110 | 111 | protocol DVRF(customlabel) { 112 | name := "DVRF" 113 | group := "Ristretto" 114 | xof := "SHAKE128" 115 | extralabels := {customlabel} 116 | 117 | G := group.base 118 | F0(x) := x·G 119 | F1(P,msg,x) := x·PointHash("", {P}, msg) 120 | 121 | commit(x, P, msg) { 122 | V := F1(P,msg,x) 123 | h := Compress("", group.encode(V)) 124 | return h 125 | } 126 | 127 | sign(D, P, x, entropy, msg) { 128 | P := F0(x) 129 | V := F1(P,msg,x) 130 | e1,r,_,_ := Commit("prove", {F0, F1(P,msg)}, {entropy,x}, {D,P,V}, msg) 131 | z := ScalarHash("verifier signature forgery", {entropy,x}, {D,P,V}, msg) 132 | e0,_ := Recommit("forge", {F0}, e1, {z}, {D}, {D,P,V}, msg) 133 | s := Prove(e0, {r}, {x}) 134 | return (V,e0,s,z) 135 | } 136 | 137 | forge(D, P, V, d, entropy, msg) { 138 | e0,r,_ := Commit("forge", {F0}, {entropy,d}, {D,P,V}, msg) 139 | s := ScalarHash("signer signature forgery", {entropy,d}, {D,P,V}, msg) 140 | e1,_,_ := Recommit("prove", {F0, F1(P,msg)}, e0, {s}, {P,V}, {D,P,V}, msg) 141 | z := Prove(e1, {r}, {d}) 142 | return (V,e0,s,z) 143 | } 144 | 145 | verify((V,e0,s,z), D, P, msg) { 146 | e1,_,_ := Recommit("prove", {F0, F1(P,msg)}, e0, {s}, {P,V}, {D,P,V}, msg) 147 | e’,_ := Recommit("forge", {F0}, e1, {z}, {D}, {D,P,V}, msg) 148 | if e’ == e0 { 149 | h := Compress("", group.encode(V)) 150 | return h 151 | } else { 152 | return nil 153 | } 154 | } 155 | } 156 | 157 | 158 | ## Ring Signature 159 | 160 | The following shows a ring version of Schnorr signature, but using compressed signature form (`e,s[0],...,s[n-1]`) to align with unconditionally binding commitments in the [Set Range Proof](#set-range-proof). 161 | 162 | protocol RingSignature(customlabel) { 163 | name := "RingSignature" 164 | group := "Ristretto" 165 | xof := "SHAKE128" 166 | extralabels := {customlabel} 167 | 168 | G := group.base 169 | F(x) := x·G 170 | 171 | sign({P#n}, j, x[j], entropy, msg) { 172 | // Precommit 173 | e[j+1 mod n],r,_ := Commit(uint64le(j), {F}, {entropy, x[j]}, {P#n}, msg) 174 | 175 | // Forge all other elements 176 | {z[0],...,z[n-2]} := ScalarHash("forged signatures", {entropy, varint(j), x[j]}, {P#n}, msg) 177 | for step := 1..n-1 { 178 | i := (j + step) mod n 179 | s[i] := z[step-1] // forged elements 180 | e[i+1 mod n],_ := Recommit(uint64le(i), {F}, e[i], {s[i]}, {P[i]}, {P#n}, msg) 181 | } 182 | // Sign 183 | s[j] := Prove(e[j], {r}, {x[j]}) 184 | return (e[0], {s[0],...,s[n-1]}) 185 | } 186 | 187 | verify(e, {s#n}, {P#n}, msg) { 188 | e’ := e 189 | for i := 0..(n-1) { 190 | e’,_ := Recommit(uint64le(i), {F}, e’, {s[i]}, {P[i]}, {P#n}, msg) 191 | } 192 | return e == e’ 193 | } 194 | } 195 | 196 | 197 | ## Traceable Ring Signature 198 | 199 | This is a part of the CryptoNote/Monero protocol that is effectively a ring version of VRF where the message under commitment is the public key itself. 200 | 201 | * `G` — base point in Curve25519. 202 | * `x` — secret scalar representing a private key. 203 | * `P` — public key, such that `P == x·G`. 204 | * `I` — key image, such that `I == x·PointHash(P)`. 205 | 206 | Definition: 207 | 208 | protocol TraceableRingSignature(customlabel) { 209 | name := "TraceableRingSignature" 210 | group := "Ristretto" 211 | xof := "SHAKE128" 212 | extralabels := {customlabel} 213 | 214 | G := group.base 215 | B(P) := PointHash({}, {P}, "") 216 | F0(x) := x·G 217 | F1(P,x) := x·B(P) 218 | 219 | commit(P, x) { 220 | I := Commit({x}, {F1(P)}) 221 | I’:= group.encode(I) 222 | return I’ 223 | } 224 | 225 | sign({P#n}, j, x[j], entropy, msg) { 226 | {r#n} := ScalarHash("", {entropy, varint(j), x[j]}, {P#n}, msg) 227 | // all but r[0] will be used as forged s-elements 228 | 229 | // Precommit 230 | P[j] := Commit({x}, {F0}) 231 | I := Commit({x}, {F1(P[j])}) 232 | 233 | RG,RI := Commit({r[0]}, {F0, F1(P[j])}) 234 | e[j+1 mod n] := ChallengeHash(uint64le(j), {RG, RI}, {I, P#n...}, msg) 235 | 236 | // Forge all other elements 237 | for step := 1..n-1 { 238 | i := (j + step) mod n 239 | s[i] := r[step] // using r[i≠0] as a forged s-element 240 | RG,RI := Recommit(e[i], {s[i]}, {P[i]}, {F0,F1(P[i])}) 241 | e[i+1 mod n] := ChallengeHash(uint64le(i), {RG, RI}, {I, P#n...}, msg) 242 | } 243 | 244 | // Sign 245 | s[j] := Prove(e[j], {r[0]}, {x[j]}) 246 | return (e[0], {s[0],...,s[n-1]}) 247 | } 248 | 249 | verify(e, {s[i]}, I’, {P#n}, msg) { 250 | I := group.decode(I’) 251 | e’ := e 252 | for i := 0..(n-1) { 253 | RG,RI := Recommit(e’, {s[i]}, {P[i]}, {F0,F1(P[i])}) 254 | e’ := ChallengeHash(uint64le(i), {RG, RI}, {I, P#n...}, msg) 255 | } 256 | return e == e’ 257 | } 258 | } 259 | 260 | 261 | ## Abstract Borromean Ring Signature 262 | 263 | This is an abstract template intended for various rangeproofs. Using it standalone 264 | is not possible because it defers the choice of the commitments being signed (`{C}`) 265 | to the higher-level protocols (e.g. a [set range proof](#set-range-proof)) that must ensure 266 | that none of the commitments are malleable with respect to the proof. 267 | 268 | **Warning: this algorithm is not running in constant time.** 269 | 270 | * `NR` — number of rings 271 | * `NI` — number of items per ring 272 | * `NS` — number of statements per item 273 | * `NX` — number of secrets per item 274 | * `t = 0..NR-1` — index of a ring 275 | * `i = 0..NI-1` — index of an item 276 | * `j = 0..NS-1` — index of a statement 277 | * `k = 0..NX-1` — index of a secret within an item 278 | * `î[t] = 0..NI-1` - secret index of a non-formed item in ring `t` 279 | * `x[t,k]` - secret scalar for ring `t` with index `k` 280 | * `{s[t,i,k]}` — 3-dimensional array of s-elements of size `NR·NI·NX`. 281 | * `{P[t,i,j]}` — 3-dimensional array of commitments of size `NR·NI·NS`. 282 | * `{C}` — array of original commitments to be signed that themselves commit to `{P[t,i,j]}` (specified by the concrete protocol). 283 | * `{F[j]({x[k]})}` — a list of commitment functions of size `NS` over `NX` variables. 284 | 285 | Definition: 286 | 287 | AbstractBRS = protocol { 288 | name: ___, 289 | NR: ___, 290 | NI: ___, 291 | NS: ___, 292 | NX: ___, 293 | group: "Ristretto" 294 | xof: "SHAKE128" 295 | 296 | sign({x[t,k]}, {î[t]}, {P[t,i,j]}, {C}, {F[j]({x[k]})}, entropy, msg, label) { 297 | // Generate NR·NI·NX random scalars 298 | {r[t,i,k]} := ScalarHash(label, {entropy, x[0,0],...,x[NR-1,NX-1], î[0],...,î[NR-1]}, {C}, msg) 299 | 300 | // Precommit 301 | for t := 0..(NR-1) { 302 | i := î[t] 303 | for j := 0..(NS-1) { 304 | R[t,i,j] := F[j](r[t,i,0], ..., r[t,i,NX-1]) 305 | } 306 | e[t, i+1 mod NI] := ChallengeHash( 307 | label, 308 | // points are doubled to take advantage of Doppio, a batchable variant of Ristretto encoding 309 | {2·R[t,i,0],...,2·R[t,i,NS-1]}, 310 | {C}, uint64le(t) || uint64le(i) || msg 311 | ) 312 | } 313 | 314 | // First halves of the rings 315 | for t := 0..(NR-1) { 316 | for i := î[t]+1..(NI-1) { // Note: can be an empty loop if î[t] == NI-1 317 | for k := 0..(NX-1) { 318 | s[t,i,k] = r[t,i,k] // forged 319 | } 320 | for j := 0..(NS-1) { 321 | R[t,i,j] := F[j](s[t,i,0], ..., s[t,i,NX-1]) - e[t,i]·P[t,i,j] 322 | } 323 | e[t, i+1 mod NI] := ChallengeHash( 324 | label, 325 | {2·R[t,i,0],...,2·R[t,i,NS-1]}, 326 | {C}, uint64le(t) || uint64le(i) || msg 327 | ) 328 | } 329 | } 330 | 331 | // Shared challenge for all trailing items in each ring 332 | if NR == 1 { 333 | // special case for 1 ring to avoid unnecessary double-hashing 334 | ê := e[0,0] 335 | } else { 336 | ê := ChallengeHash(label, {}, e[0,0] || ... || e[NR-1,0]) 337 | } 338 | 339 | 340 | // Complete second halves of the rings 341 | for t := 0..(NR-1) { 342 | e[t,0] := ê 343 | for i := 0..î[t]-1 { // Note: can be an empty loop if î[t] == 0 344 | for k := 0..(NX-1) { 345 | s[t,i,k] = r[t,i,k] // forged 346 | } 347 | for j := 0..(NS-1) { 348 | R[t,i,j] := F[j](s[t,i,0], ..., s[t,i,NX-1]) - e[t,i]·P[t,i,j] 349 | } 350 | e[t, i+1 mod NI] := ChallengeHash( 351 | label, 352 | {2·R[t,i,0],...,2·R[t,i,NS-1]}, 353 | {C}, uint64le(t) || uint64le(i) || msg 354 | ) 355 | } 356 | } 357 | 358 | // Sign 359 | for t := 0..(NR-1) { 360 | i := î[t] 361 | for k := 0..(NX-1) { 362 | s[t,i,k] = r[t,i,k] + x[t,k]·e[t,i] mod group.order 363 | } 364 | } 365 | 366 | return (ê, {s[t,i,k]}) 367 | } 368 | 369 | verify(ê, {s[t,i,k]}, {P[t,i,j]}, {C}, {F[j]({x[k]})}, label, msg) { 370 | for t := 0..(NR-1) { 371 | e[t,0] := ê 372 | for i := 0..(NI-1) { 373 | for j := 0..(NS-1) { 374 | R[t,i,j] := F[j](s[t,i,0], ..., s[t,i,NX-1]) - e[t,0]·P[t,i,j] 375 | } 376 | e[t, i+1 mod NI] := ChallengeHash( 377 | label, 378 | // points are doubled to take advantage of Doppio, a batchable variant of Ristretto encoding 379 | {2·R[t,i,0],...,2·R[t,i,NS-1]}, 380 | {C}, 381 | uint64le(t) || uint64le(i) || msg 382 | ) 383 | } 384 | } 385 | if NR == 1 { 386 | // special case for 1 ring to avoid unnecessary double-hashing 387 | e’ := e[0,0] 388 | } else { 389 | e’ := ChallengeHash(label, {}, e[0,0] || ... || e[NR-1,0]) 390 | } 391 | return ê == e’ 392 | } 393 | } 394 | 395 | 396 | ## Set Range Proof 397 | 398 | Set range proof proves that a given ElGamal commitment belongs to a range of other ElGamal commitments. 399 | 400 | * `G,J` — orthogonal generators, first one is a standard base point. 401 | * `M` — group element for which commitment is created 402 | * `c’` — a blinding scalar. 403 | * `(H’,B’) = (M+c’·G, c’·J)` — non-trusted commitment to be proven to belong to the required range 404 | * `N` — number of items in the range 405 | * `i=0..N-1` — index of the item in the range 406 | * `î` — secret index of the commitment in the range, which is re-blinded as `H’,B’`. 407 | * `(H[i],B[i])` — the required range of `N` trusted commitments. 408 | 409 | Definition: 410 | 411 | SetRangeProof = protocol { 412 | name: "SetRangeProof", 413 | group: "Ristretto" 414 | xof: "SHAKE128" 415 | 416 | NR: 1, // rings 417 | NI: N, // items 418 | NS: 2, // statements 419 | NX: 1 // secrets 420 | 421 | sign(M, î, c’, c[î], {H[i],B[i]}, entropy, msg, label) { 422 | G := group.base 423 | J := Generator("J") 424 | 425 | // Blind 426 | H’:= M + c’·G 427 | B’:= c’·J 428 | 429 | // Prepare rangeproof configuration 430 | x := c’ - c[î] 431 | for i := 0..(N-1) { 432 | P[0,i,0] := H’ - H[i] 433 | P[0,i,1] := B’ - B[i] 434 | } 435 | F[0](x) := x·G 436 | F[1](x) := x·J 437 | 438 | // Sign 439 | return AbstractBRS.sign( 440 | {x}, {î}, 441 | {P[t,i,j]}, 442 | {H’,B’,H[0],B[0], ... H[N-1],B[N-1]}, 443 | {F[j](x)}, 444 | entropy, msg, label 445 | ) 446 | } 447 | 448 | verify(ê, {s[t,i,k]}, (H’,B’), {H[i],B[i]}, label, msg) { 449 | G := group.base 450 | J := Generator("J") 451 | 452 | // Prepare rangeproof configuration 453 | for i := 0..(N-1) { 454 | P[0,i,0] := H’ - H[i] 455 | P[0,i,1] := B’ - B[i] 456 | } 457 | F[0](x) := x·G 458 | F[1](x) := x·J 459 | 460 | // Verify 461 | return AbstractBRS.verify( 462 | ê, {s[t,i,k]}, 463 | {P[t,i,j]}, 464 | {H’,B’,H[0],B[0], ... H[N-1],B[N-1]}, 465 | {F[j](x)}, 466 | label, msg 467 | ) 468 | } 469 | } 470 | 471 | 472 | 473 | ## ChainKD 474 | 475 | ChainKD is a hierarchical key derivation (HKD) scheme inspired by BIP32. 476 | It is not a signature scheme per-se, but reuses the existing framework for deriving keys. 477 | 478 | ChainKD extends Schnorr public and private keys to xpubs and xprvs: extended public/private keys. 479 | Each key is encoded with additional 32-byte string `dk` used as symmetric “derivation key”. 480 | 481 | If the public or private key is stripped of `dk`, it cannot be used to derive or identify child keys. 482 | 483 | ChainKD = protocol { 484 | name: "ChainKD" 485 | group: "Ristretto" 486 | xof: "SHAKE128" 487 | 488 | generate(seed) { 489 | {x,dk} := ScalarHash("Generate", {seed}, {}, "") 490 | return x||dk 491 | } 492 | 493 | // Compute xpub for a given xprv. 494 | xpub(x||dk) { 495 | G := group.base 496 | P := x·G 497 | P’:= group.encode(P) 498 | return P’||dk 499 | } 500 | 501 | derive_xpub(P1’||dk1, selector) { 502 | G := group.base 503 | P1 := group.decode(P1’) 504 | {f,dk2} := ScalarHash("Derive", {dk1}, {P1}, selector) 505 | P2 := P1 + f·G 506 | P2’:= group.encode(P2) 507 | return P2’||dk2 508 | } 509 | 510 | derive_xprv(x1||dk1, selector) { 511 | G := group.base 512 | P1 := x1·G 513 | {f,dk2} := ScalarHash("Derive", {dk1}, {P1}, selector) 514 | x2 := x1 + f mod group.order 515 | return x2||dk2 516 | } 517 | 518 | derive_hardened(x||dk, selector) { 519 | {x’,dk’} := ScalarHash("DeriveH", {x,dk}, {}, selector) 520 | return x’||dk’ 521 | } 522 | } 523 | 524 | -------------------------------------------------------------------------------- /specification.md: -------------------------------------------------------------------------------- 1 | # Multitool (DRAFT) 2 | 3 | The public key cryptography framework. 4 | 5 | **WARNING: this is a very early draft, do not count on it. All opinions are welcome, especially yours.** 6 | 7 | * [Motivation](#motivation) 8 | * [Overview](#overview) 9 | * [Core Specification](#core-specification) 10 | * [Terminology](#terminology) 11 | * [Labelset](#labelset) 12 | * [Protocol](#protocol) 13 | * [Scalar Hash](#scalar-hash) 14 | * [Challenge Hash](#challenge-hash) 15 | * [Point Hash](#point-hash) 16 | * [Compress](#compress) 17 | * [Ristretto Specification](#ristretto-specification) 18 | * [Generic Curve Parameters](#generic-curve-parameters) 19 | * [Acknowledgements](#acknowledgements) 20 | 21 | ## Motivation 22 | 23 | Previously considered exotic cryptographic schemes today become rapidly productized 24 | due to high demand for end-to-end encrypted communication tools, blockchain networks 25 | and all related technology. Verifiable random functions, ring signatures, rangeproofs, 26 | hierarchical key derivation schemes share a few common bits that must be done correctly 27 | to ensure safety and avoid common pitfalls. Things like nonce generation, key derivation, 28 | fiat-shamir challenge generation etc. 29 | 30 | **Multitool** is a framework that allows building such zero-knowledge schemes in a safe and 31 | straightforward manner. Multitool is generalized to any prime order group where DLP is hard, 32 | but also has an instance “with batteries included” which uses a high-performance [Ristretto](#ristretto-specification) group based on Curve25519. 33 | 34 | 35 | ## Overview 36 | 37 | ### Synthetic nonces 38 | 39 | Generalized safe nonce-generation procedure that addresses safety and bikeshedding issues: 40 | 41 | 1. **Deterministic derivation** from secret scalars protects against RNG failures. 42 | 2. Additional **RNG entropy** protects against misuse (cross-protocol nonce reuse due to secret reuse) and glitches in the deterministic derivation. 43 | 3. Hashing defines a **customization scheme** that comes with an extensive security rationale and provides a standard yet flexible API for the custom protocols. 44 | 45 | ### Generalized challenge generation 46 | 47 | 1. Fiat-Shamir transform is generalized to support **variable number of secrets, statements and commitments** for a wide range of schemes, from simple DSA to designated-verifier VRFs and even borromean ring signatures. 48 | 2. **Challenge and nonce generation are aligned** to minimize the risk of misuse: when an additional input is added to the challenge, but not to the associated nonce. 49 | 3. Challenge hashing uses the same **customization scheme** as synthetic nonces. 50 | 51 | ### Prime order group 52 | 53 | Group elements used to implement commitments and public keys always belong to a prime order group. Protocols instantiated with elliptic curve groups having cofactor greater than 1 (such as Curve25519 and Curve448), **Ristretto** and **Decaf** schemes are used respectively to enforce that rule. 54 | 55 | ### Indirect commitments 56 | 57 | Some schemes derive public keys (or, generally speaking, commitments to secrets, statements of which are being proven) from other commitments. For example, range proofs with base-4 digits derive 4 “public keys” for each digit commitment deterministically. Encoding and hashing these public keys would be 4x more wasteful compared to just encoding a digit commitment and hashing it together with an index from 0 to 3. In order to facilitate these schemes, the challenge is designed to support **indirect commitments** that are defined by each specific scheme. 58 | 59 | ### Compressed and uncompressed signatures 60 | 61 | There are two ways to encode a Schnorr signature: by exposing a nonce-commitment (usually denoted as group element `R`) or by exposing a challenge (denoted by a scalar `e`) that is computed as a hash of the nonce-commitment (not mentioning the message and other associated data): 62 | 63 | Uncompressed signature Compressed signature 64 | 1. Receive (R,s) 1. Receive (e,s) 65 | 2. Compute e = H(R, msg) 2. Compute R = s*G - e*Pubkey 66 | 3. Verify R == s*G - H(R)*Pubkey 3. Verify e == H(encode(R, msg)) 67 | 68 | **Uncompressed form** is usually as compact as compressed one for the simple single-statement signatures, 69 | and allows batched verification of multiple signatures at once, plus avoids group element encoding overhead. 70 | 71 | **Compressed form**, however, is suitable for multi-statement or multi-ring signature because it avoids sending multiple `R` group elements. 72 | For instance, 32-digit base-4 range proofs save 31×32 bytes by compressing all `R` group elements from each ring in one shared challenge hash. 73 | 74 | Each protocol should statically decide which form it uses. 75 | 76 | ### Compatibility 77 | 78 | The specification aims to reuse most of existing codebases implementing Curve25519 and Curve448. 79 | Due to Ristretto encoding and related simplification of the schemes, 80 | we intentionally do not maintain compatibility with the existing EdDSA standard ([RFC8032](https://tools.ietf.org/html/rfc8032)). 81 | 82 | 83 | ## Core Specification 84 | 85 | ### Terminology 86 | 87 | Term | Description 88 | ----------|------------- 89 | `l` | Order of the prime order group used by the protocol. 90 | `|x|` | Maximum number of bytes necessary to represent the scalar `x`. 91 | `x` | Scalar, an integer between `0` and `l-1`. 92 | `G` | Base group element. 93 | `P` | Public key defined as `x·G`. 94 | `msg` | An arbitrary-length binary string being signed. 95 | `r` | Random scalar called "nonce" that blinds the secret, statements of which are being proven. 96 | `R` | Commitment to a nonce with a relevant base group element (e.g. `R = r·G`). 97 | `e` | Challenge scalar, a Fiat-Shamir transform for the sigma-protocol. 98 | `s` | Proof scalar, proving the statement about some secret `x`. Each secret has its own “s-value” (simpler schemes use only one secret). 99 | `entropy` | An arbitrary-length string representing randomness from a RNG. At least 128 bits of entropy are recommended. 100 | `{x#n}` | A list of `n` elements: `{x[0],...,x[n-1]}`. 101 | `{x#n,m}` | A list of `n` lists of `m` elements. 102 | `{x}` | A list of unspecified size (could be anything). 103 | `len(x)` | Number of items in the list `x` (if it's a byte string, number of bytes). 104 | `byte(x)` | Encoding of a integer `x` in range 0..255 as a single byte. 105 | `uint64le(x)` | Encoding of a non-negative integer `x` using little-endian notation as an 8-byte string. 106 | 107 | ### Labelset 108 | 109 | Customization is supported via extensible *labelset*. 110 | 111 | The labelset is used to provide independent random oracles in more complex protocols. 112 | It can contain identifiers for the protocol, a user-specified customization label, 113 | and identifiers for the hash instance. 114 | 115 | Labelset is encoded as follows: 116 | 117 | n || len(label1) || label1 || ... || len(label_n) || label_n 118 | 119 | Where `n` is a 1-byte encoding of the number of labels, and `len_i` is a 1-byte encoding of the length of the label that follows, in bytes. 120 | Number of labels and the length of each label in bytes is limited to 255. 121 | 122 | An empty labelset is encoded as a 1-byte string containing zero: `0x00`. 123 | 124 | A labelset can be extended with the additional labels that customize higher-level protocols: 125 | 126 | add_labels(labelset, x...) { 127 | foreach x { 128 | verify(len(x) <= 255) 129 | verify(labelset[0] < 255) 130 | labelset[0] += 1 131 | labelset := labelset || byte(len(x)) || x 132 | } 133 | return labelset 134 | } 135 | 136 | ### Protocol 137 | 138 | The framework requires protocol to specify a minimal set of parameters: 139 | 140 | protocol { 141 | name: "Schnorr" | "VRF" | ... 142 | group: "Ristretto" | "Decaf448" | "P256k1" | "P256r1" | ... 143 | xof: "SHAKE128" | ... 144 | extralabels: {...} 145 | } 146 | 147 | * `protocol.name` - a variable-length string, identifying a protocol name. 148 | * `protocol.group` - a variable-length string identifying an elliptic curve and related parameters. 149 | * `protocol.xof` - a variable-length string identifying an extensible-output hash function. 150 | * `protocol.extralabels` - an additional labelset (could be empty) that customizes the given protocol for the higher-level protocols 151 | 152 | Each `group` label defines the following parameters: 153 | 154 | * `protocol.group.order`, aka prime number `l`, the number of elements in the group. 155 | * `protocol.group.base`, aka `G`, a base group element. 156 | * `protocol.group.equal(a,b)->true`, equality check for a pair of group elements. 157 | * `protocol.group.encode(element)->string`, encoding a group element to a string. 158 | * `protocol.group.decode(string)->element`, decoding a group element from a string. 159 | * `protocol.group.mapToElement(string)`, a decoding procedure that maps a random string of length `|protocol.group.order|` to a group element. 160 | 161 | XOF takes an input string to be hashed and a number of bytes to be returned: 162 | 163 | protocol.xof(input, n) -> {byte[0], ..., byte[n-1]} 164 | 165 | Each protocol has a [labelset](#labelset) consisting of a single label: 166 | 167 | protocol.labelset = { __, extralabels... } // e.g. {"Schnorr_Ristretto_SHAKE128"} 168 | 169 | Underscores are used to be compatible with syntax for identifiers in most programming languages. 170 | 171 | 172 | ### Scalar Hash 173 | 174 | `ScalarHash` outputs a list of synthetic scalars generated using `k` secrets `{x[i]}` and `m` commitment group elements `{C[j]}`. 175 | 176 | The first secret in a list is recommended to be the high-entropy output of RNG to defend against cross-protocol misuse. 177 | 178 | Commitment strings could be the public keys, various commitments. 179 | 180 | * `labelset` is a list of labels that allow customization and domain-separation in the higher-level protocol. 181 | * `n` is the number of scalars to be produced, encoded as a little-endian 64-bit unsigned integer. 182 | * `pad` are distinct minimal all-zero strings (could be empty) that pad the input to the block size of the given XOF. 183 | * `x` is a list of `k` secret arbitrary-length strings (e.g. private key, secret indices). 184 | * `C` is a list of `m` commitment group elements (e.g. public keys). 185 | 186 | Algorithm: 187 | 188 | ScalarHash(label, {x#k}, {C#m}, msg) -> {r#n} { 189 | labelset’ := AddLabels(protocol.labelset, "ScalarHash", label) 190 | {r[0],...,r[n-1]} := protocol.xof( 191 | labelset’ || 192 | uint64le(n) || || 193 | x[0] || || 194 | ... 195 | x[k-1] || || 196 | labelset’ || 197 | uint64le(m) || 198 | protocol.group.encode(C[0]) || 199 | ... 200 | protocol.group.encode(C[m-1]) || 201 | msg, 202 | n * (|protocol.group.order|+16) 203 | ) 204 | foreach r[i] { 205 | r[i] := r[i] mod protocol.group.order 206 | } 207 | return {r[0],...,r[n-1]} 208 | } 209 | 210 | Rationale: 211 | 212 | 1. First input to the hash function is the labelset and the desired output length, which provides the domain separation between the protocols. This ensures that a nonce does not get reused between protocols by accident. 213 | 2. Padding the customized prefix to the nearest block allows pre-computation and reuse of the XOF instance for a given labelset. 214 | 3. Each secret is padded to the nearest block to turn XOF into a PRF keyed with the secret. When the first secret is an output from RNG, it randomizes the XOF against cross-protocol misuse. If the second secret is a static signing key, it provides a defense against faulty RNG by making the resulting nonce unpredictable. 215 | 4. Secrets are not length-prefixed as it’s expected they are independent and padding is enough to isolate permutations of each secret. TBD: review this closely to check if it's actually safe. 216 | 5. Commitments are all group elements to which the scalar must commit. For instance, public keys, Pedersen or ElGamal commitments and alike. Since the size of these is static for a given group, length prefixes are not used. 217 | 5. Output consists of extra 128 bits per scalar to make deviation from the uniform distribution of the resulting scalar after modular reduction negligible. 218 | 6. XOF is used instead of a fixed-output hash function for two reasons: to make one hash function work with groups of different order, and to avoid repeated hashing of the inputs which should not be pre-hashed due to collision-resilience requirement. 219 | 220 | 221 | ### Challenge Hash 222 | 223 | Challenge hash produces a single scalar `e` out of random commitments to nonces (`{R[i]}`) bound to the commitments (group elements) and the message (arbitrary-length string). 224 | 225 | ChallengeHash(label, {R#n}, {C#m}, msg) { 226 | labelset’ := AddLabels(protocol.labelset, "ChallengeHash", label) 227 | e := protocol.xof( 228 | labelset’ || || 229 | uint64le(n) || 230 | protocol.group.encode(R[0]) || 231 | ... 232 | protocol.group.encode(R[n-1]) || 233 | || 234 | labelset’ || 235 | uint64le(m) || 236 | protocol.group.encode(C[0]) || 237 | ... 238 | protocol.group.encode(C[m-1]) || 239 | msg, 240 | |protocol.group.order|+16 241 | ) 242 | e := e mod protocol.group.order 243 | return e 244 | } 245 | 246 | Rationale: 247 | 248 | 1. TBD: labelset provides domain separation. 249 | 2. TBD: nonce commitments are padded to a whole block to turn XOF into a PRF which is hard to find collisions with. 250 | 3. TBD: repeated labelset adds collision resilience 251 | 4. TBD: extra 16 bytes of XOF output to make bias less than 2^-128 after reducing the scalar mod l. 252 | 253 | ### Point Hash 254 | 255 | Point hash function hashes a list of commitments and an arbitrary-length message into a group element. 256 | 257 | PointHash(label, {C[i]}, msg) { 258 | labelset’ := AddLabels(protocol.labelset, "PointHash", label) 259 | m = len(C) 260 | h := protocol.xof( 261 | labelset’ || pad || 262 | uint64le(m) || 263 | protocol.group.encode(C[0]) || 264 | ... 265 | protocol.group.encode(C[m-1]) || 266 | msg, 267 | |protocol.group.order| 268 | ) 269 | return protocol.group.mapToElement(h) 270 | } 271 | 272 | ### Compress 273 | 274 | Compression hash function uses XOF customized with the protocol’s label set and produces a 32-byte output. 275 | 276 | Compress(label, msg) { 277 | labelset’ := AddLabels(protocol.labelset, "Compress", label) 278 | h := protocol.xof(labelset’ || || msg, 32) 279 | return h 280 | } 281 | 282 | 283 | ### Commit 284 | 285 | A generalized commitment algorithm that applies `m` scalars to `n` functions and returns `m` group elements. 286 | 287 | * `n` — number of statements represented by commitment functions. 288 | * `m` — number of scalars, knowledge of which is being proven. 289 | * `F({x#m})` is a function that takes `m` scalar arguments and returns a single group element. 290 | 291 | Definition: 292 | 293 | Commit(label, {F({x#m})#n}, {x}, {C}, msg) { 294 | {r#m} := ScalarHash(label, {x}, {C}, msg) 295 | for j := 0..(n-1) { 296 | R[j] := F[j]({r#m}) 297 | } 298 | e := ChallengeHash(label, {R#n}, {C}, msg) 299 | return e, {r#m}, {R#n} 300 | } 301 | 302 | ### Prove 303 | 304 | _Prove_ blinds `m` secret scalars `{x}` with secret nonces `{r}` using a challenge hash `e`. 305 | 306 | Prove(e, {r#m}, {x#m}) { 307 | for k := 0..(m-1) { 308 | s[k] = r[k] + e·x[k] mod protocol.group.order 309 | } 310 | return {s#m} 311 | } 312 | 313 | 314 | ### Recommit 315 | 316 | _Recommit_ reconstructs the commitments to `n` random nonces produced by _Commit_ using `m` signature elements `{s}`, challenge hash `e` and commitments to secrets `{P}`. 317 | 318 | * `n` — number of statements represented with commitment functions. 319 | * `m` — number of scalars, knowledge of which is being proven. 320 | * `F({x#m})` is a function that takes `m` scalar arguments and returns a single group element. 321 | * `{P#n}` — `n` group elements representing a commitment to the secrets. Not always the result of evaluation of `F` functions (e.g. range proofs modify that commitment). 322 | * `{C}` — all group elements to commit to (usually include all of `{P#n}` directly or indirectly). 323 | 324 | Definition: 325 | 326 | Recommit(label, {F({x#m})#n}, e, {s#m}, {P#n}, {C}, msg) { 327 | for j := 0..n { 328 | R[j] := F[j]({s#m}) - e·P[j] 329 | } 330 | e := ChallengeHash(label, {R#n}, {C}, msg) 331 | return e, {R#n} 332 | } 333 | 334 | 335 | 336 | 337 | ## Ristretto Specification 338 | 339 | ### Encode 340 | 341 | TBD. normal encoding 342 | 343 | TBD: Dual isogeny (aka Doppio), batchable 2*P encoding 344 | 345 | ### Decode 346 | 347 | TBD. 348 | 349 | ### Equality 350 | 351 | TBD. efficient equality check w/o full encoding 352 | 353 | 354 | ### MapToElement 355 | 356 | TBD: ristretto-flavored Elligator 357 | 358 | 359 | 360 | 361 | ## Generic Curve Parameters 362 | 363 | This is a recommended configuration for arbitrary curves with cofactor 1 suitable for curves P256k1 and P256r1. 364 | 365 | ### Equality 366 | 367 | TBD. simple encode and compare bit-wise 368 | 369 | ### MapToElement 370 | 371 | TBD: hash and pray in a loop using XOF with a const-time variant that squeezes 128 elements. 372 | 373 | 374 | 375 | 376 | ## Acknowledgements 377 | 378 | * Mike Hamburg, for Decaf, which returns us all on the path of sanity. 379 | * Trevor Perrin, for extremely helpful classification work and thorough explanation of various crypto caveats, all of which are reflected in this document. 380 | * Isis Agora Lovecruft and Henry De Valence, for the excellent implemention and meticulous documention of the high-speed Curve25519 crypto, including Ristretto and Elligator algorithms. 381 | 382 | --------------------------------------------------------------------------------