From b265da2e0d01aef86a544503c82006cac8162e9a Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Wed, 20 Mar 2024 13:30:26 +0000 Subject: [PATCH] mixing: introduce module This introduces a new package and module containing code for supporting peer-to-peer transaction mixing derived from CoinShuffle++. The mixing package contains the function and type primitives that will be used by mixing clients to perform the slot reservation and XOR DC-nets. --- mixing/dcnet.go | 176 ++++++++++++++++++++ mixing/field.go | 17 ++ mixing/go.mod | 30 ++++ mixing/go.sum | 81 ++++++++++ mixing/internal/chacha20prng/prng.go | 53 ++++++ mixing/keyagreement.go | 234 +++++++++++++++++++++++++++ mixing/message.go | 34 ++++ mixing/scriptclass.go | 12 ++ mixing/sid.go | 21 +++ mixing/signatures.go | 56 +++++++ mixing/vec.go | 79 +++++++++ 11 files changed, 793 insertions(+) create mode 100644 mixing/dcnet.go create mode 100644 mixing/field.go create mode 100644 mixing/go.mod create mode 100644 mixing/go.sum create mode 100644 mixing/internal/chacha20prng/prng.go create mode 100644 mixing/keyagreement.go create mode 100644 mixing/message.go create mode 100644 mixing/scriptclass.go create mode 100644 mixing/sid.go create mode 100644 mixing/signatures.go create mode 100644 mixing/vec.go diff --git a/mixing/dcnet.go b/mixing/dcnet.go new file mode 100644 index 0000000000..e74542ce6e --- /dev/null +++ b/mixing/dcnet.go @@ -0,0 +1,176 @@ +package mixing + +import ( + "encoding/binary" + "math/big" + + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrd/wire" +) + +// SRMixPads creates a vector of exponential DC-net pads from a vector of +// shared secrets with each participating peer in the DC-net. +func SRMixPads(kp [][]byte, my uint32) []*big.Int { + h := blake256.New() + scratch := make([]byte, 8) + + pads := make([]*big.Int, len(kp)) + partialPad := new(big.Int) + for j := uint32(0); int(j) < len(kp); j++ { + pads[j] = new(big.Int) + for i := uint32(0); int(i) < len(kp); i++ { + if my == i { + continue + } + binary.LittleEndian.PutUint64(scratch, uint64(j)+1) + h.Reset() + h.Write(kp[i]) + h.Write(scratch) + digest := h.Sum(nil) + partialPad.SetBytes(digest) + if my > i { + pads[j].Add(pads[j], partialPad) + } else { + pads[j].Sub(pads[j], partialPad) + } + } + pads[j].Mod(pads[j], F) + } + return pads +} + +// SRMix creates the padded {m**1, m**2, ..., m**n} message exponentials +// vector. Message must be bounded by the field prime and must be unique to +// every exponential SR run in a mix session to ensure anonymity. +func SRMix(m *big.Int, pads []*big.Int) []*big.Int { + mix := make([]*big.Int, len(pads)) + exp := new(big.Int) + for i := int64(0); i < int64(len(mix)); i++ { + mexp := new(big.Int).Exp(m, exp.SetInt64(i+1), nil) + mix[i] = mexp.Add(mexp, pads[i]) + mix[i].Mod(mix[i], F) + } + return mix +} + +// IntVectorsFromBytes creates a 2-dimensional *big.Int slice from their absolute +// values as bytes. +func IntVectorsFromBytes(vs [][][]byte) [][]*big.Int { + ints := make([][]*big.Int, len(vs)) + for i := range vs { + ints[i] = make([]*big.Int, len(vs[i])) + for j := range vs[i] { + ints[i][j] = new(big.Int).SetBytes(vs[i][j]) + } + } + return ints +} + +// IntVectorsToBytes creates a 2-dimensional slice of big.Int absolute values as +// bytes. +func IntVectorsToBytes(ints [][]*big.Int) [][][]byte { + bytes := make([][][]byte, len(ints)) + for i := range ints { + bytes[i] = make([][]byte, len(ints[i])) + for j := range ints[i] { + bytes[i][j] = ints[i][j].Bytes() + } + } + return bytes +} + +// AddVectors sums each vector element over F, returning a new vector. When +// peers are honest (DC-mix pads sum to zero) this creates the unpadded vector +// of message power sums. +func AddVectors(vs ...[]*big.Int) []*big.Int { + sums := make([]*big.Int, len(vs)) + for i := range sums { + sums[i] = new(big.Int) + for j := range vs { + sums[i].Add(sums[i], vs[j][i]) + } + sums[i].Mod(sums[i], F) + } + return sums +} + +// Coefficients calculates a{0}..a{n} for the polynomial: +// +// g(x) = a{0} + a{1}x + a{2}x**2 + ... + a{n-1}x**(n-1) + a{n}x**n (mod F) +// +// where +// +// a{n} = -1 +// a{n-1} = -(1/1) * a{n}*S{0} +// a{n-2} = -(1/2) * (a{n-1}*S{0} + a{n}*S{1}) +// a{n-3} = -(1/3) * (a{n-2}*S{0} + a{n-1}*S{1} + a{n}*S{2}) +// ... +// +// The roots of this polynomial are the set of recovered messages. +// +// Note that the returned slice of coefficients is one element larger than the +// slice of partial sums. +func Coefficients(S []*big.Int) []*big.Int { + n := len(S) + 1 + a := make([]*big.Int, n) + a[len(a)-1] = big.NewInt(-1) + a[len(a)-1].Add(a[len(a)-1], F) // a{n} = -1 (mod F) = F - 1 + scratch := new(big.Int) + for i := 0; i < len(a)-1; i++ { + a[n-2-i] = new(big.Int) + for j := 0; j <= i; j++ { + a[n-2-i].Add(a[n-2-i], scratch.Mul(a[n-1-i+j], S[j])) + } + xinv := scratch.ModInverse(scratch.SetInt64(int64(i)+1), F) + xinv.Neg(xinv) + a[n-2-i].Mul(a[n-2-i], xinv) + a[n-2-i].Mod(a[n-2-i], F) + } + return a +} + +// IsRoot checks that the message m is a root of the polynomial with +// coefficients a (mod F) without solving for every root. +func IsRoot(m *big.Int, a []*big.Int) bool { + sum := new(big.Int) + scratch := new(big.Int) + for i := range a { + scratch.Exp(m, scratch.SetInt64(int64(i)), F) + scratch.Mul(scratch, a[i]) + sum.Add(sum, scratch) + } + sum.Mod(sum, F) + return sum.Sign() == 0 +} + +// DCMixPads creates the vector of DC-net pads from shared secrets with each mix +// participant. +func DCMixPads(kp []wire.MixVect, my uint32) Vec { + pads := make(Vec, len(kp)) + for i := range kp { + if i == int(my) { + continue + } + pads.Xor(pads, (Vec)(kp[i])) + } + return pads +} + +// DCMix creates the DC-net vector of message m xor'd into m's reserved +// anonymous slot position of the pads DC-net pads. Panics if len(m) is not the +// vector's message size. +func DCMix(pads Vec, m []byte, slot uint32) Vec { + if len(m) != Msize { + panic("m is not len Msize") + } + + dcmix := make(Vec, len(pads)) + for i := range pads { + dcmix[i] = pads[i] + } + slotm := dcmix[slot][:] + for i := range m { + slotm[i] ^= m[i] + } + return dcmix +} diff --git a/mixing/field.go b/mixing/field.go new file mode 100644 index 0000000000..6ff96b9f91 --- /dev/null +++ b/mixing/field.go @@ -0,0 +1,17 @@ +package mixing + +import ( + "math/big" +) + +// FieldPrime is the field prime 2**127 - 1. +var F *big.Int + +func init() { + F, _ = new(big.Int).SetString("7fffffffffffffffffffffffffffffff", 16) +} + +// InField returns whether x is bounded by the field F. +func InField(x *big.Int) bool { + return x.Sign() != -1 && x.Cmp(F) == -1 +} diff --git a/mixing/go.mod b/mixing/go.mod new file mode 100644 index 0000000000..6e53aeddb3 --- /dev/null +++ b/mixing/go.mod @@ -0,0 +1,30 @@ +module github.com/decred/dcrd/mixing + +go 1.17 + +require ( + decred.org/cspp/v2 v2.0.1-0.20230307024253-8a22691aa376 + github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a + github.com/davecgh/go-spew v1.1.1 + github.com/decred/dcrd/chaincfg/chainhash v1.0.4 + github.com/decred/dcrd/chaincfg/v3 v3.2.0 + github.com/decred/dcrd/crypto/blake256 v1.0.1 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 + github.com/decred/dcrd/dcrutil/v4 v4.0.1 + github.com/decred/dcrd/txscript/v4 v4.1.0 + github.com/decred/dcrd/wire v1.6.0 + github.com/decred/slog v1.2.0 + golang.org/x/crypto v0.7.0 +) + +require ( + github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect + github.com/dchest/siphash v1.2.3 // indirect + github.com/decred/base58 v1.0.5 // indirect + github.com/decred/dcrd/crypto/ripemd160 v1.0.2 // indirect + github.com/decred/dcrd/dcrec v1.0.1 // indirect + github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + golang.org/x/sys v0.6.0 // indirect + lukechampine.com/blake3 v1.2.1 // indirect +) diff --git a/mixing/go.sum b/mixing/go.sum new file mode 100644 index 0000000000..079364897d --- /dev/null +++ b/mixing/go.sum @@ -0,0 +1,81 @@ +decred.org/cspp/v2 v2.0.1-0.20230307024253-8a22691aa376 h1:739v8a7LMXuCTFNodcKYVpfj70CKWvJeE3NKFDn/65I= +decred.org/cspp/v2 v2.0.1-0.20230307024253-8a22691aa376/go.mod h1:+/9jr1RhVshWnc0U/eXxMlxfiu9/f7ia6TTyS0Oh5n0= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/companyzero/sntrup4591761 v0.0.0-20200131011700-2b0d299dbd22/go.mod h1:LoZJNGDWmVPqMEHmeJzj4Weq4Stjc6FKY6FVpY3Hem0= +github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a h1:clYxJ3Os0EQUKDDVU8M0oipllX0EkuFNBfhVQuIfyF0= +github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a/go.mod h1:z/9Ck1EDixEbBbZ2KH2qNHekEmDLTOZ+FyoIPWWSVOI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/decred/base58 v1.0.5 h1:hwcieUM3pfPnE/6p3J100zoRfGkQxBulZHo7GZfOqic= +github.com/decred/base58 v1.0.5/go.mod h1:s/8lukEHFA6bUQQb/v3rjUySJ2hu+RioCzLukAVkrfw= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +github.com/decred/dcrd/chaincfg/chainhash v1.0.4 h1:zRCv6tdncLfLTKYqu7hrXvs7hW+8FO/NvwoFvGsrluU= +github.com/decred/dcrd/chaincfg/chainhash v1.0.4/go.mod h1:hA86XxlBWwHivMvxzXTSD0ZCG/LoYsFdWnCekkTMCqY= +github.com/decred/dcrd/chaincfg/v3 v3.2.0 h1:6WxA92AGBkycEuWvxtZMvA76FbzbkDRoK8OGbsR2muk= +github.com/decred/dcrd/chaincfg/v3 v3.2.0/go.mod h1:2rHW1TKyFmwZTVBLoU/Cmf0oxcpBjUEegbSlBfrsriI= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/crypto/ripemd160 v1.0.2 h1:TvGTmUBHDU75OHro9ojPLK+Yv7gDl2hnUvRocRCjsys= +github.com/decred/dcrd/crypto/ripemd160 v1.0.2/go.mod h1:uGfjDyePSpa75cSQLzNdVmWlbQMBuiJkvXw/MNKRY4M= +github.com/decred/dcrd/dcrec v1.0.1 h1:gDzlndw0zYxM5BlaV17d7ZJV6vhRe9njPBFeg4Db2UY= +github.com/decred/dcrd/dcrec v1.0.1/go.mod h1:CO+EJd8eHFb8WHa84C7ZBkXsNUIywaTHb+UAuI5uo6o= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 h1:l/lhv2aJCUignzls81+wvga0TFlyoZx8QxRMQgXpZik= +github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3/go.mod h1:AKpV6+wZ2MfPRJnTbQ6NPgWrKzbe9RCIlCF/FKzMtM8= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrutil/v4 v4.0.1 h1:E+d2TNbpOj0f1L9RqkZkEm1QolFjajvkzxWC5WOPf1s= +github.com/decred/dcrd/dcrutil/v4 v4.0.1/go.mod h1:7EXyHYj8FEqY+WzMuRkF0nh32ueLqhutZDoW4eQ+KRc= +github.com/decred/dcrd/txscript/v4 v4.1.0 h1:uEdcibIOl6BuWj3AqmXZ9xIK/qbo6lHY9aNk29FtkrU= +github.com/decred/dcrd/txscript/v4 v4.1.0/go.mod h1:OVguPtPc4YMkgssxzP8B6XEMf/J3MB6S1JKpxgGQqi0= +github.com/decred/dcrd/wire v1.5.0/go.mod h1:fzAjVqw32LkbAZIt5mnrvBR751GTa3e0rRQdOIhPY3w= +github.com/decred/dcrd/wire v1.6.0 h1:YOGwPHk4nzGr6OIwUGb8crJYWDiVLpuMxfDBCCF7s/o= +github.com/decred/dcrd/wire v1.6.0/go.mod h1:XQ8Xv/pN/3xaDcb7sH8FBLS9cdgVctT7HpBKKGsIACk= +github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM= +github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jrick/wsrpc/v2 v2.3.4/go.mod h1:XPYs8BnRWl99lCvXRM5SLpZmTPqWpSOPkDIqYTwDPfU= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/mixing/internal/chacha20prng/prng.go b/mixing/internal/chacha20prng/prng.go new file mode 100644 index 0000000000..cca9a23574 --- /dev/null +++ b/mixing/internal/chacha20prng/prng.go @@ -0,0 +1,53 @@ +// Copyright (c) 2023 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package chacha20prng + +import ( + "encoding/binary" + "strconv" + + "golang.org/x/crypto/chacha20" +) + +// SeedSize is the required length of seeds for New. +const SeedSize = 32 + +// Reader is a ChaCha20 PRNG for a DC-net run. It implements io.Reader. +type Reader struct { + cipher *chacha20.Cipher +} + +// New creates a ChaCha20 PRNG seeded by a 32-byte key and a run iteration. The +// returned reader is not safe for concurrent access. This will panic if the +// length of seed is not SeedSize bytes. +func New(seed []byte, run uint32) *Reader { + if l := len(seed); l != SeedSize { + panic("chacha20prng: bad seed length " + strconv.Itoa(l)) + } + + nonce := make([]byte, chacha20.NonceSize) + binary.LittleEndian.PutUint32(nonce[:4], run) + + cipher, _ := chacha20.NewUnauthenticatedCipher(seed, nonce) + return &Reader{cipher: cipher} +} + +// Read implements io.Reader. +func (r *Reader) Read(b []byte) (int, error) { + // Zero the source such that the destination is written with just the + // keystream. Destination and source are allowed to overlap (exactly). + for i := range b { + b[i] = 0 + } + r.cipher.XORKeyStream(b, b) + return len(b), nil +} + +// Next returns the next n bytes from the reader. +func (r *Reader) Next(n int) []byte { + b := make([]byte, n) + r.cipher.XORKeyStream(b, b) + return b +} diff --git a/mixing/keyagreement.go b/mixing/keyagreement.go new file mode 100644 index 0000000000..e99098e84f --- /dev/null +++ b/mixing/keyagreement.go @@ -0,0 +1,234 @@ +// Copyright (c) 2023 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mixing + +import ( + cryptorand "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + + "github.com/companyzero/sntrup4591761" + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/mixing/internal/chacha20prng" + "github.com/decred/dcrd/wire" +) + +// Aliases for sntrup4591761 types +type ( + PQPublicKey = [sntrup4591761.PublicKeySize]byte + PQPrivateKey = [sntrup4591761.PrivateKeySize]byte + PQCiphertext = [sntrup4591761.CiphertextSize]byte + PQSharedKey = [sntrup4591761.SharedKeySize]byte +) + +func generateSecp256k1(rand io.Reader) (*secp256k1.PublicKey, *secp256k1.PrivateKey, error) { + if rand == nil { + rand = cryptorand.Reader + } + + privateKey, err := secp256k1.GeneratePrivateKeyFromRand(rand) + if err != nil { + return nil, nil, err + } + + publicKey := privateKey.PubKey() + + return publicKey, privateKey, nil +} + +// KX contains the client public and private keys to perform shared key exchange +// with other peers. +type KX struct { + ECDHPublicKey *secp256k1.PublicKey + ECDHPrivateKey *secp256k1.PrivateKey + PQPublicKey *PQPublicKey + PQPrivateKey *PQPrivateKey + PQCleartexts []PQSharedKey +} + +// NewKX generates a mixing identity's public and private keys for a interactive +// key exchange, with randomness read from a run's PRNG. +func NewKX(prng io.Reader) (*KX, error) { + ecdhPublic, ecdhPrivate, err := generateSecp256k1(prng) + if err != nil { + return nil, err + } + + pqPublic, pqPrivate, err := sntrup4591761.GenerateKey(prng) + if err != nil { + return nil, err + } + + kx := &KX{ + ECDHPublicKey: ecdhPublic, + ECDHPrivateKey: ecdhPrivate, + PQPublicKey: pqPublic, + PQPrivateKey: pqPrivate, + } + return kx, nil +} + +func (kx *KX) ecdhSharedKey(pub *secp256k1.PublicKey) []byte { + secret := secp256k1.GenerateSharedSecret(kx.ECDHPrivateKey, pub) + hash := blake256.Sum256(secret) + return hash[:] +} + +func (kx *KX) pqSharedKey(ciphertext *PQCiphertext) ([]byte, error) { + pqSharedKey, ok := sntrup4591761.Decapsulate(ciphertext, kx.PQPrivateKey) + if ok != 1 { + return nil, errors.New("sntrup4591761: decapsulate failure") + } + return pqSharedKey[:], nil +} + +// Encapsulate performs encapsulation for sntrup4591761 key exchanges with each +// other peer in the DC-net. It populates the PQCleartexts field of kx and +// return encrypted cyphertexts of these shared keys. +// +// Encapsulation in the DC-net requires randomness from a CSPRNG seeded by a +// committed secret; blame assignment is not possible otherwise. +func (kx *KX) Encapsulate(prng io.Reader, pubkeys []*PQPublicKey, my int) ([]PQCiphertext, error) { + cts := make([][sntrup4591761.CiphertextSize]byte, len(pubkeys)) + kx.PQCleartexts = make([][32]byte, len(pubkeys)) + + for i, pk := range pubkeys { + ciphertext, cleartext, err := sntrup4591761.Encapsulate(prng, pk) + if err != nil { + return nil, err + } + cts[i] = *ciphertext + kx.PQCleartexts[i] = *cleartext + } + + return cts, nil +} + +// RevealedKeys... +type RevealedKeys struct { + ECDHPublicKeys []*secp256k1.PublicKey + Ciphertexts []PQCiphertext + MyIndex uint32 +} + +type SharedSecrets struct { + SRSecrets [][][]byte + DCSecrets [][]wire.MixVect +} + +// SharedKeys creates the pairwise SR and DC shared secret keys for +// mcounts[k.MyIndex] mixes. ecdhPubs, cts, and mcounts must all share the same +// slice length. +func (kx *KX) SharedSecrets(k *RevealedKeys, sid []byte, run uint32, mcounts []uint32) (SharedSecrets, error) { + var s SharedSecrets + + if len(k.ECDHPublicKeys) != len(mcounts) { + err := fmt.Errorf("ECDH public key count (%d) must match peer count (%d)", + len(k.ECDHPublicKeys), len(mcounts)) + return s, err + } + if len(k.Ciphertexts) != len(mcounts) { + err := fmt.Errorf("ciphertext count (%d) must match peer count (%d)", + len(k.Ciphertexts), len(mcounts)) + return s, err + } + + mcount := mcounts[k.MyIndex] + var mtot uint32 + for i := range mcounts { + mtot += mcounts[i] + } + + h := blake256.New() + s.SRSecrets = make([][][]byte, mcount) + s.DCSecrets = make([][]wire.MixVect, mcount) + + for i := uint32(0); i < mcount; i++ { + s.SRSecrets[i] = make([][]byte, mtot) + s.DCSecrets[i] = make([]wire.MixVect, mtot) + var m int + for peer := uint32(0); int(peer) < len(mcounts); peer++ { + if peer == k.MyIndex && mcount == 1 { + m++ + continue + } + + sharedKey := kx.ecdhSharedKey(k.ECDHPublicKeys[peer]) + pqSharedKey, err := kx.pqSharedKey(&k.Ciphertexts[peer]) + if err != nil { + return s, err + } + + // XOR x25519 and both sntrup4591761 keys into a single + // shared key. If sntrup4591761 is discovered to be + // broken in the future, the security only reduces to + // that of x25519. + // If the message belongs to our own peer, only XOR + // the sntrup4591761 key once. The decapsulated and + // cleartext keys are equal in this case, and would + // cancel each other out otherwise. + xor := func(dst, src []byte) { + if len(dst) != len(src) { + panic("dcnet: different lengths in xor") + } + for i := range dst { + dst[i] ^= src[i] + } + } + xor(sharedKey, pqSharedKey[:]) + if peer != k.MyIndex { + xor(sharedKey, kx.PQCleartexts[peer][:]) + } + + // Create the prefix of a PRNG seed preimage. A counter + // will be appended before creating each PRNG, one for + // each message pair. + prngSeedPreimage := make([]byte, len(sid)+len(sharedKey)+4) + l := copy(prngSeedPreimage, sid) + l += copy(prngSeedPreimage[l:], sharedKey) + seedCounterBytes := prngSeedPreimage[l:] + + // Read from the PRNG to create shared keys for each + // message the peer is mixing. + for j := uint32(0); j < mcounts[peer]; j++ { + if k.MyIndex == peer && j == i { + m++ + continue + } + + // Create the PRNG seed using the combined shared key. + // A unique seed is generated for each message pair, + // determined using the message index of the peer with + // the lower peer index. The PRNG nonce is the message + // number of the peer with the higher peer index. + // When creating shared keys with our own peer, the PRNG + // seed counter and nonce must be reversed for the second + // half of our generated keys. + seedCounter := i + nonce := j + if k.MyIndex > peer || (k.MyIndex == peer && j > i) { + seedCounter = j + nonce = i + } + binary.LittleEndian.PutUint32(seedCounterBytes, uint32(seedCounter)) + + h.Reset() + h.Write(prngSeedPreimage) + prngSeed := h.Sum(nil) + prng := chacha20prng.New(prngSeed, uint32(nonce)) + + s.SRSecrets[i][m] = prng.Next(32) + s.DCSecrets[i][m] = wire.MixVect(randVec(mtot, prng)) + + m++ + } + } + } + + return s, nil +} diff --git a/mixing/message.go b/mixing/message.go new file mode 100644 index 0000000000..088d5f76a2 --- /dev/null +++ b/mixing/message.go @@ -0,0 +1,34 @@ +// Copyright (c) 2023 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mixing + +import ( + "hash" + + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/wire" +) + +// Message is a mixing message. In addition to the implementing wire encoding, +// these messages are signed by an ephemeral mixing participant identity, +// declare the previous messages that have been observed by a peer in a mixing +// session, and include expiry information to increase resilience to +// replay and denial-of-service attacks. +// +// All mixing messages satisify this interface, however, the pair request +// message returns nil for some fields that do not apply, as it is the first +// message in the protocol. +type Message interface { + wire.Message + + Pub() []byte + Sig() []byte + WriteHash(hash.Hash) + Hash() chainhash.Hash + WriteSignedData(hash.Hash) + PrevMsgs() []chainhash.Hash // PR returns nil + Sid() []byte // PR returns nil + GetRun() uint32 // PR returns 0 +} diff --git a/mixing/scriptclass.go b/mixing/scriptclass.go new file mode 100644 index 0000000000..849d67d8d4 --- /dev/null +++ b/mixing/scriptclass.go @@ -0,0 +1,12 @@ +package mixing + +// ScriptClass describes the type and format of scripts that can be used for +// mixed outputs. A mix may only be performed among all participants who agree +// on the same script class. +type ScriptClass string + +// Script class descriptors for the mixed outputs. +// Only secp256k1 P2PKH is allowed at this time. +const ( + ScriptClassP2PKHv0 ScriptClass = "P2PKH-secp256k1-v0" +) diff --git a/mixing/sid.go b/mixing/sid.go new file mode 100644 index 0000000000..60be5c0a08 --- /dev/null +++ b/mixing/sid.go @@ -0,0 +1,21 @@ +// Copyright (c) 2023-2024 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mixing + +import ( + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/crypto/blake256" +) + +// DeriveSessionID creates the mix session identifier from an initial sorted +// slice of PR message hashes. +func DeriveSessionID(seenPRs []chainhash.Hash) [32]byte { + h := blake256.New() + h.Write([]byte("decred-mix-session")) + for i := range seenPRs { + h.Write(seenPRs[i][:]) + } + return *(*[32]byte)(h.Sum(nil)) +} diff --git a/mixing/signatures.go b/mixing/signatures.go new file mode 100644 index 0000000000..ce0ad7289a --- /dev/null +++ b/mixing/signatures.go @@ -0,0 +1,56 @@ +// Copyright (c) 2023 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mixing + +import ( + "hash" + + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr" +) + +type Signed interface { + Pub() []byte + Sig() []byte + WriteSignedData(hash.Hash) +} + +func SignMessage(m Signed, priv *secp256k1.PrivateKey) error { + sig, err := sign(priv, m) + if err != nil { + return err + } + copy(m.Sig(), sig) + return nil +} + +func VerifyMessageSignature(m Signed) bool { + return verify(m.Pub(), m, m.Sig()) +} + +func sign(priv *secp256k1.PrivateKey, m Signed) ([]byte, error) { + h := blake256.New() + m.WriteSignedData(h) + sig, err := schnorr.Sign(priv, h.Sum(nil)) + if err != nil { + return nil, err + } + return sig.Serialize(), nil +} + +func verify(pk []byte, m Signed, sig []byte) bool { + pkParsed, err := secp256k1.ParsePubKey(pk) + if err != nil { + return false + } + sigParsed, err := schnorr.ParseSignature(sig) + if err != nil { + return false + } + h := blake256.New() + m.WriteSignedData(h) + return sigParsed.Verify(h.Sum(nil), pkParsed) +} diff --git a/mixing/vec.go b/mixing/vec.go new file mode 100644 index 0000000000..a98e6d7905 --- /dev/null +++ b/mixing/vec.go @@ -0,0 +1,79 @@ +package mixing + +import ( + "bytes" + "fmt" + "strings" + + "github.com/decred/dcrd/mixing/internal/chacha20prng" +) + +const Msize = 20 + +// Vec is a N-element vector of Msize []byte messages. +type Vec [][Msize]byte + +func randVec(n uint32, prng *chacha20prng.Reader) Vec { + v := make(Vec, n) + for i := range v { + prng.Read(v[i][:]) + } + return v +} + +// Equals returns whether the two vectors have equal dimensions and data. +func (v Vec) Equals(other Vec) bool { + if len(v) != len(other) { + return false + } + for i := range other { + if !bytes.Equal((v)[i][:], other[i][:]) { + return false + } + } + return true +} + +// M returns the i'th message of the vector. +// XXX: still necessary? +// func (v Vec) M(i uint32) []byte { +// return v[i][:] +// } + +func (v Vec) String() string { + b := new(strings.Builder) + b.Grow(2 + len(v)*(2*Msize+1)) + b.WriteString("[") + for i := range v { + if i != 0 { + b.WriteString(" ") + } + fmt.Fprintf(b, "%x", v[i][:]) + } + b.WriteString("]") + return b.String() +} + +// Xor writes the xor of each vector element of src1 and src2 into v. +// Source and destination vectors are allowed to be equal. +// Panics if vectors do not share identical dimensions. +func (v Vec) Xor(src1, src2 Vec) { + if len(v) != len(src1) || len(v) != len(src2) { + panic("dcnet: vectors do not share identical dimensions") + } + for i := range v { + for j := range v[i] { + v[i][j] = src1[i][j] ^ src2[i][j] + } + } +} + +// XorVectors calculates the xor of all vectors. +// Panics if vectors do not share identical dimensions. +func XorVectors(vs []Vec) Vec { + res := make(Vec, len(vs[0])) + for i := range vs { + res.Xor(res, vs[i]) + } + return res +}