feat(identity): add cycle-free ServiceIdentity + Hybrid (mnemonic-derived ML-DSA-65 service auth)

service_identity.go + hybrid.go are pure-function, I/O-free derivation (BIP-39
-> BIP-32 -> ML-DSA-65 NodeID) that imports only luxfi/crypto + go-bip32/39 +
ids — NO kms import, so no keys<->kms cycle. Previously these lived only
vendored-in-kms; consumers (hanzoai/kms/sdk, bank, kms-operator) referenced
keys.ServiceIdentity/NewServiceIdentity that the released keys lacked. This is
the canonical home. (LoadMnemonic stays out of keys — it needs kms/zapclient.)

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-06-30 15:13:50 -07:00
co-authored by Hanzo Dev
parent 964089cf57
commit 2313d84f08
4 changed files with 1054 additions and 96 deletions
+19 -28
View File
@@ -1,50 +1,41 @@
module github.com/luxfi/keys module github.com/luxfi/keys
go 1.26.1 go 1.26.4
require ( require (
github.com/luxfi/address v1.0.1 github.com/luxfi/address v1.0.1
github.com/luxfi/constants v1.4.3 github.com/luxfi/constants v1.5.8
github.com/luxfi/crypto v1.17.39 github.com/luxfi/crypto v1.20.0
github.com/luxfi/go-bip32 v1.0.2 github.com/luxfi/go-bip32 v1.1.0
github.com/luxfi/go-bip39 v1.1.2 github.com/luxfi/go-bip39 v1.2.0
github.com/luxfi/ids v1.2.9 github.com/luxfi/ids v1.3.0
github.com/luxfi/protocol v0.0.2 github.com/luxfi/protocol v0.0.2
github.com/luxfi/tls v1.0.3 github.com/luxfi/tls v1.0.3
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.52.0
) )
require ( require (
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcd/btcutil v1.1.6 // indirect github.com/btcsuite/btcd/btcutil v1.1.6 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/gorilla/rpc v1.2.1 // indirect github.com/gorilla/rpc v1.2.1 // indirect
github.com/holiman/uint256 v1.3.2 // indirect github.com/holiman/uint256 v1.3.2 // indirect
github.com/luxfi/cache v1.2.0 // indirect github.com/luxfi/accel v1.2.4 // indirect
github.com/luxfi/consensus v1.22.53 // indirect github.com/luxfi/cache v1.2.1 // indirect
github.com/luxfi/container v0.0.4 // indirect github.com/luxfi/container v0.0.4 // indirect
github.com/luxfi/formatting v1.0.1 // indirect github.com/luxfi/formatting v1.0.1 // indirect
github.com/luxfi/geth v1.16.69 // indirect github.com/luxfi/geth v1.16.98 // indirect
github.com/luxfi/math v1.2.3 // indirect github.com/luxfi/math v1.4.1 // indirect
github.com/luxfi/math/big v0.1.0 // indirect github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/metric v1.4.10 // indirect github.com/luxfi/metric v1.5.7 // indirect
github.com/luxfi/mock v0.1.0 // indirect github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/sampler v1.0.0 // indirect github.com/luxfi/sampler v1.1.0 // indirect
github.com/luxfi/vm v1.0.16 // indirect github.com/luxfi/vm v1.2.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/supranational/blst v0.3.16 // indirect github.com/supranational/blst v0.3.16 // indirect
go.uber.org/mock v0.6.0 // indirect go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect golang.org/x/sys v0.45.0 // indirect
golang.org/x/sys v0.41.0 // indirect gonum.org/v1/gonum v0.17.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )
+36 -68
View File
@@ -1,8 +1,4 @@
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 h1:TBzelXBdnzDy+HCrBMcomEnhrmigkWOI1/mIPCi2u4M=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
@@ -26,8 +22,6 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -39,8 +33,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -67,56 +61,46 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/luxfi/accel v1.2.4 h1:5VbIHyEvvfobn2zBiTFODxDw1CeqxCepZOLlvkuf9yQ=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/luxfi/accel v1.2.4/go.mod h1:ISIwAX+ZfsL/S5nsP2JvfldXN6Nc+QzoWf6Jtaq+xsQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/luxfi/address v1.0.1 h1:Sc4keyuVzBIvHr7uVeYZf2/WY9YDGUgDi/iiWenj49g= github.com/luxfi/address v1.0.1 h1:Sc4keyuVzBIvHr7uVeYZf2/WY9YDGUgDi/iiWenj49g=
github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0= github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0=
github.com/luxfi/cache v1.2.0 h1:VEk/ue13oi8w3sKBB/Dsv9fnjmfdXgb0NMibcPLVbss= github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.0/go.mod h1:Esn48WMB3JL/+UOdCEV2Chyjr1MG3SSckXWLhB/P080= github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/consensus v1.22.53 h1:iwrnImZJ7Ur6oV7rx0WSrR7n3X92pdnOX3mivCBCuZ4= github.com/luxfi/constants v1.5.8 h1:iNP9AWNUcM4Tps7jYnx49CwtCWAC9mYRxJfGou2za0g=
github.com/luxfi/consensus v1.22.53/go.mod h1:GicEGtNrPPG6063k/aRFusv87e17i0UwmgeY4D8Vn6s= github.com/luxfi/constants v1.5.8/go.mod h1:Pu5jWHdnUtQRbWC43yTUjU/pbIIKMDOd2a2yroSfo48=
github.com/luxfi/constants v1.4.3 h1:bcqUO8twMOhHWCgN/RbjfWulVPbvUQcxDkssaRrCx4g=
github.com/luxfi/constants v1.4.3/go.mod h1:ENkJ121cmDEkwQPDiKK4QhnTnW9u37PGpepbrdVcAmc=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM= github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek= github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/crypto v1.17.39 h1:dDmktYOD/sU6WjIpitIfuHp7mRbc3izOsyJrQ1c5eOQ= github.com/luxfi/crypto v1.20.0 h1:JNsQ25sVO6T8XuIHRue4akOpnt5pNmk1xg5hzmU6dNE=
github.com/luxfi/crypto v1.17.39/go.mod h1:mChLWmW4CLR1wAN6CeJTveCzUv0DTzGQnYgq01x3W0U= github.com/luxfi/crypto v1.20.0/go.mod h1:bLCBuIV/KDjPytld7jSYe1WbfWknPQXcivq88Qo96QU=
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo= github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc= github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
github.com/luxfi/geth v1.16.69 h1:CHO6xTZ+A+3itk94ts4uyVRJajNVP3RxWTjJp5qGOlk= github.com/luxfi/geth v1.16.98 h1:w187TtKuGStf3tm2bshuHVKBv2Frjx0lT54kQVXyNHA=
github.com/luxfi/geth v1.16.69/go.mod h1:8eEO1hW5sa6OH2VeCMaCPnRz28JBxFvCPBCWPLsU2ck= github.com/luxfi/geth v1.16.98/go.mod h1:6kEzSExdk9CPQDPXALt6P3HfQqBq7KF1Jrrv9gBpxbU=
github.com/luxfi/go-bip32 v1.0.2 h1:7vFbb+Wr4Z499q2tuCLdd7wWjtn8sH+HWBlx76mhH9Y= github.com/luxfi/go-bip32 v1.1.0 h1:zjy19WKa1KJnGamRNyOZM3l/MPtV/sax7M4NMPwLHZQ=
github.com/luxfi/go-bip32 v1.0.2/go.mod h1:bc7/LXDKAJQZ/F0Xjf5yXaTZxY9/ssLb4FC+Hxn/cDk= github.com/luxfi/go-bip32 v1.1.0/go.mod h1:QyDXlzWL3xRiCbMUi4Z3J52Q/SMoCvdRLXXlYvSZor8=
github.com/luxfi/go-bip39 v1.1.2 h1:p+wLMPGs6MLQh7q0YIsmy2EhHL7LHiELEGTJko6t/Jg= github.com/luxfi/go-bip39 v1.2.0 h1:fx3pFuSGawCG4In6pA4OLLStqbgIqD1j8EygFskoHzY=
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo= github.com/luxfi/go-bip39 v1.2.0/go.mod h1:if+2OVbG4k4jKIuBt/Rse1KV1kgWQM5j5xFbUtwbNtc=
github.com/luxfi/ids v1.2.9 h1:+yjdhXW99drnd2Zlp1u/p8k3G23W3/1btJQ4ogHawUI= github.com/luxfi/ids v1.3.0 h1:11xnwRDm6zQzbqcRnkFujOYkvhK4Fs/+g+sKRlRUNsU=
github.com/luxfi/ids v1.2.9/go.mod h1:khJOEdOPxd22yn0jcVrnbX1ADa0GHn5Y74gvCzN5BYc= github.com/luxfi/ids v1.3.0/go.mod h1:6vpdcdZW0qxeade+3xby8aLTutbcJ7O0r8+fNQrksGI=
github.com/luxfi/math v1.2.3 h1:BgvIFw/srPXFLbcqtoDhLJOfmBsn86GPA1iWgsoyUb4= github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
github.com/luxfi/math v1.2.3/go.mod h1:C8STnF2H+D6rqBPt248CiWY2TGuJgdtv/+4UqrT15iM= github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ= github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk= github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
github.com/luxfi/metric v1.4.10 h1:uWeLupMLxK7YK1c99gY23cT+hornWg7OmZgHNK4HNbw= github.com/luxfi/metric v1.5.7 h1:LoSPEUpak2SLcynF+LT2cXjl9ECp4nY+Lia9zudmDv4=
github.com/luxfi/metric v1.4.10/go.mod h1:3OIQ+e6mciesm3cik3Q4CSOAvTlBy4JA0ZoKzCB9APo= github.com/luxfi/metric v1.5.7/go.mod h1:CMguEhyuLi4YUWyXimJ+UHply99BDFrL0pxedB7rBqM=
github.com/luxfi/mock v0.1.0 h1:IwElfNu+T9sXvzFX6tudPDx1vqPuACRSRdxpD5lxW+o= github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
github.com/luxfi/mock v0.1.0/go.mod h1:izF+9K0gGzFC9zERn6Po37v46eLdPB+EIsDjL3GLk+U= github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/protocol v0.0.2 h1:TAuXMm1K4VDpG3B3brq1EE9Lb7XlbhQmVBObskgNhmc= github.com/luxfi/protocol v0.0.2 h1:TAuXMm1K4VDpG3B3brq1EE9Lb7XlbhQmVBObskgNhmc=
github.com/luxfi/protocol v0.0.2/go.mod h1:Yjn2VRwn5PXim0+JTalAyrVG6YbmuSnYOfeUXSbAsqA= github.com/luxfi/protocol v0.0.2/go.mod h1:Yjn2VRwn5PXim0+JTalAyrVG6YbmuSnYOfeUXSbAsqA=
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0= github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk= github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
github.com/luxfi/tls v1.0.3 h1:rK3nxSAxrUOOSHOZnKChwV4f6UJ+cfOl8KWJXAQx/SI= github.com/luxfi/tls v1.0.3 h1:rK3nxSAxrUOOSHOZnKChwV4f6UJ+cfOl8KWJXAQx/SI=
github.com/luxfi/tls v1.0.3/go.mod h1:dQqSiGE7YxXUxOwICoReUuIitBms9DYOaCeteBwmIWw= github.com/luxfi/tls v1.0.3/go.mod h1:dQqSiGE7YxXUxOwICoReUuIitBms9DYOaCeteBwmIWw=
github.com/luxfi/vm v1.0.16 h1:IOTuMZUAKJ5i40GcWHs9FqhnC4Dd/j2Rq34kdPPYb1U= github.com/luxfi/vm v1.2.0 h1:jTwQRHdC9VmyRZTPSn+IqjMju7f6xlxLc6P9CEg+Y2M=
github.com/luxfi/vm v1.0.16/go.mod h1:WVRRGWk2xbCDEMfsYmTrDIo839O1eIInlR8JT+BhZgo= github.com/luxfi/vm v1.2.0/go.mod h1:qasVIBRerVQuvy9vFGrX3H8X8pPMPG5un/KbZSyq5YY=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -129,16 +113,6 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -151,19 +125,15 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -179,16 +149,16 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -198,8 +168,6 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+355
View File
@@ -0,0 +1,355 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// hybrid.go — Bindel-Brendel-Fischlin (CCS 2021) + Cremers-Düzlü-Fiedler-
// Fischlin-Janson (Asiacrypt 2023) stronger-binding hybrid signature
// scheme for Lux validator identity.
//
// Construction (BBF21 N-Sig with CDFFJ23 joint-pk binding):
//
// m_bound = H_bind(pk_c, pk_pq, msg)
// = SHAKE256-384( "lux-hybrid-sig-v1"
// || left_encode(8·|pk_c|) || pk_c
// || left_encode(8·|pk_pq|) || pk_pq
// || left_encode(8·|msg|) || msg )
// sig_c = secp256k1.SignHash(sk_c, m_bound)
// sig_pq = mldsa65.SignCtx(sk_pq, m_bound, "lux-hybrid-sig-v1")
// Verify = AND( secp256k1.VerifyHash(pk_c, m_bound, sig_c),
// mldsa65.VerifyCtx(pk_pq, m_bound, sig_pq, "lux-hybrid-sig-v1") )
//
// Why this construction (vs raw concat):
//
// Raw concat `pk_c || pk_pq` and per-component signatures over `msg`
// reduces to MIN security under a non-honest-sign adversary
// (CDFFJ23 §4): an adversary that registers a malformed pk_pq can
// force the hybrid to drop to classical security only. By hashing
// BOTH pubkeys into m_bound, every signature is *cryptographically
// bound to the joint identity* — substituting either component
// invalidates the binding. This is BBF21 §3.2 Cons. 2 "Combined
// Concatenated" with the CDFFJ23 strengthening: m_bound includes
// both pubkeys so component swap is detectable.
//
// Why secp256k1 as classical (not ed25519 or X25519):
//
// (a) X25519 is a KEM, not a signature scheme — disqualified.
// (b) Lux P/X validator identity already anchors on secp256k1
// (see keys.ValidatorKey.ECPrivateKey + .PChainAddr). Picking
// secp256k1 means the hybrid is a pure superset — every existing
// validator's classical key continues to play the same role.
// (c) ed25519 would add a third primitive with no existing
// infrastructure: no NodeID derivation, no stake record format.
// secp256k1 is the only choice that preserves the one-and-only-one
// way of identifying a P/X validator.
//
// Security claim (BBF21 Thm. 1 + CDFFJ23 Thm. 3 corollary):
//
// The scheme is EUF-CMA under
// max( EUF-CMA_secp256k1, sEUF-CMA_MLDSA65 )
// in the chosen-message model where the adversary may submit
// adversarial pk_pq's during registration. This is strictly
// stronger than min(·, ·) which is all raw concatenation buys.
//
// Domain separation:
//
// The string "lux-hybrid-sig-v1" appears verbatim TWICE: once as
// the prefix of m_bound (binds the hash), once as the ML-DSA-65
// context (binds the PQ signature). This is intentional — a future
// v2 of the scheme (say, with ML-DSA-87 or different prefix bytes)
// would replace both occurrences atomically; no cross-version
// replay is possible.
package keys
import (
"crypto/rand"
"errors"
"fmt"
"io"
mldsa "github.com/luxfi/crypto/mldsa"
secp "github.com/luxfi/crypto/secp256k1"
"golang.org/x/crypto/sha3"
)
// HybridSigDomain is the canonical domain-separation string for the
// Lux hybrid signature scheme. Pinned at v1; bumping invalidates every
// prior hybrid signature, which is the correct behaviour for a hardfork
// of the binding encoding. The same string is used both as the H_bind
// prefix and as the ML-DSA-65 context.
const HybridSigDomain = "lux-hybrid-sig-v1"
// HybridBoundDigestLen is the byte length of the m_bound digest the
// scheme uses internally. 48 bytes = SHAKE256-384, the same hash size
// as ids.FullDigest — matches the Lux convention for identity digests.
const HybridBoundDigestLen = 48
// Typed errors. errors.Is friendly.
var (
// ErrHybridNilKey — either component of the hybrid key is nil.
// Refused early; we never want a half-hybrid signing path.
ErrHybridNilKey = errors.New("keys: hybrid key has nil component")
// ErrHybridClassicalSign — secp256k1 signing failed. The error
// wraps the underlying secp256k1 error.
ErrHybridClassicalSign = errors.New("keys: hybrid classical sign failed")
// ErrHybridPQSign — ML-DSA-65 signing failed. The error wraps
// the underlying mldsa error.
ErrHybridPQSign = errors.New("keys: hybrid PQ sign failed")
// ErrHybridClassicalVerify — secp256k1 verification failed.
// A valid hybrid signature requires BOTH components to verify.
ErrHybridClassicalVerify = errors.New("keys: hybrid classical verification failed")
// ErrHybridPQVerify — ML-DSA-65 verification failed. A valid
// hybrid signature requires BOTH components to verify.
ErrHybridPQVerify = errors.New("keys: hybrid PQ verification failed")
// ErrHybridNilSig — signature struct or one of its components is
// nil/empty. Refused before any expensive verification.
ErrHybridNilSig = errors.New("keys: hybrid signature has nil component")
)
// HybridPublicKey is the joint pubkey of the BBF-bound hybrid scheme.
// Both components are required for any verification; a half-hybrid is
// a programmer error.
type HybridPublicKey struct {
// Classical is the secp256k1 verification key. 33-byte compressed
// SEC1 encoding on the wire. This is the same primitive the
// existing Lux validator set uses for P/X identity.
Classical *secp.PublicKey
// PQ is the ML-DSA-65 verification key (FIPS 204). Bytes-on-the-
// wire form is the standard ML-DSA-65 public key encoding.
PQ *mldsa.PublicKey
}
// HybridPrivateKey is the joint signing key of the BBF-bound hybrid
// scheme. Owns both private components; the only legal use is internal
// Sign(). Call Wipe() when done.
type HybridPrivateKey struct {
// Classical is the secp256k1 signing key (32 bytes scalar).
Classical *secp.PrivateKey
// PQ is the ML-DSA-65 signing key (FIPS 204).
PQ *mldsa.PrivateKey
}
// HybridSignature is the joint signature of the BBF-bound hybrid
// scheme. BOTH components are required for verification. Neither is
// independently useful — that is the binding the scheme provides.
type HybridSignature struct {
// Classical is the secp256k1 recoverable signature (65 bytes).
Classical []byte
// PQ is the ML-DSA-65 signature (FIPS 204 standard size).
PQ []byte
}
// HybridPublic extracts the joint public key from the joint private
// key. Pure function: no allocation other than the returned struct.
func (sk *HybridPrivateKey) Public() *HybridPublicKey {
if sk == nil {
return nil
}
return &HybridPublicKey{
Classical: sk.Classical.PublicKey(),
PQ: sk.PQ.PublicKey,
}
}
// Wipe zeroes both private components in place. Idempotent. Safe to
// call from a defer on a nil receiver.
func (sk *HybridPrivateKey) Wipe() {
if sk == nil {
return
}
if sk.PQ != nil {
sk.PQ.Zeroize()
sk.PQ = nil
}
// secp256k1.PrivateKey holds key bytes in an unexported field;
// the canonical wipe is to drop the pointer reference and let the
// GC reclaim — luxfi/crypto/secp256k1 does not export a Zeroize
// (the scalar is consumed by SignHash internally and the bytes
// live in the heap). Dropping the reference is the best we can
// portably do without forking the upstream API.
sk.Classical = nil
}
// boundDigest computes m_bound — the canonical SHAKE256-384 commitment
// the scheme binds. This is the joint message every signature is
// computed over and every verifier reconstructs.
//
// Wire shape (SP 800-185 left_encode framed):
//
// left_encode(8·|domain|) || domain (= HybridSigDomain)
// left_encode(8·|pk_classical|) || pk_classical (33 bytes SEC1)
// left_encode(8·|pk_pq|) || pk_pq (ML-DSA-65 pubkey)
// left_encode(8·|msg|) || msg
//
// 48-byte SHAKE256-384 output. The encoding is unambiguous: an
// adversary cannot smuggle field bytes across boundaries because each
// field's length is prepended.
func boundDigest(pk *HybridPublicKey, msg []byte) ([]byte, error) {
if pk == nil || pk.Classical == nil || pk.PQ == nil {
return nil, ErrHybridNilKey
}
pkClassical := pk.Classical.CompressedBytes() // 33 bytes
pkPQ := pk.PQ.Bytes() // ML-DSA-65 pubkey bytes
h := sha3.NewShake256()
_, _ = h.Write(leftEncode(uint64(len(HybridSigDomain)) * 8))
_, _ = h.Write([]byte(HybridSigDomain))
_, _ = h.Write(leftEncode(uint64(len(pkClassical)) * 8))
_, _ = h.Write(pkClassical)
_, _ = h.Write(leftEncode(uint64(len(pkPQ)) * 8))
_, _ = h.Write(pkPQ)
_, _ = h.Write(leftEncode(uint64(len(msg)) * 8))
_, _ = h.Write(msg)
out := make([]byte, HybridBoundDigestLen)
_, _ = h.Read(out)
return out, nil
}
// HybridBoundDigest is the exported helper computing m_bound. Same
// algorithm as the internal boundDigest; exported so an out-of-band
// verifier (e.g. an on-chain precompile or a different language's
// implementation) can reconstruct the bound message verbatim.
//
// Pure function: no I/O, no randomness.
func HybridBoundDigest(pk *HybridPublicKey, msg []byte) ([]byte, error) {
return boundDigest(pk, msg)
}
// HybridSign produces a BBF-bound joint signature over msg under the
// given joint signing key. The classical signature uses secp256k1
// SignHash over m_bound; the PQ signature uses ML-DSA-65 SignCtx with
// HybridSigDomain as the context.
//
// Why two domain-separated bindings:
//
// (1) m_bound's prefix binds the SHAKE256 input to this scheme.
// (2) The ML-DSA context binds the FIPS 204 signature itself to
// this scheme — preventing cross-protocol replay of a future
// hybrid signature against any other ML-DSA-65 surface (KMS
// envelope, P-Chain block, etc.).
//
// rand is the randomness source for ML-DSA-65 hedged signing. If nil,
// crypto/rand is used. The secp256k1 signature uses RFC 6979
// deterministic-k — no randomness needed.
func HybridSign(sk *HybridPrivateKey, msg []byte, randSource io.Reader) (*HybridSignature, error) {
if sk == nil || sk.Classical == nil || sk.PQ == nil {
return nil, ErrHybridNilKey
}
if randSource == nil {
randSource = rand.Reader
}
pk := sk.Public()
mBound, err := boundDigest(pk, msg)
if err != nil {
return nil, err
}
// Classical: secp256k1 SignHash(m_bound). SignHash expects a
// 32-byte hash, but m_bound is 48 bytes (SHAKE256-384). The
// canonical thing to do is sign the first 32 bytes of the digest
// — keep the full SHAKE256-384 output for the PQ side (which
// can absorb arbitrary length via SignCtx), and use the prefix
// for the classical side which requires a 32-byte hash.
//
// This is sound: the SHAKE256-384 output is uniform random under
// the random-oracle assumption, so its 32-byte prefix is also
// uniform random — exactly what secp256k1 SignHash expects.
sigC, err := sk.Classical.SignHash(mBound[:32])
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrHybridClassicalSign, err)
}
// PQ: ML-DSA-65 SignCtx with full 48-byte m_bound + domain
// context. FIPS 204 hedged signing reads randomness from
// randSource. SignCtx absorbs the full digest.
sigPQ, err := sk.PQ.SignCtx(randSource, mBound, []byte(HybridSigDomain))
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrHybridPQSign, err)
}
return &HybridSignature{
Classical: sigC,
PQ: sigPQ,
}, nil
}
// HybridVerify checks a BBF-bound joint signature. Returns nil on
// success; returns a typed error identifying which component failed
// (or ErrHybridNilKey/ErrHybridNilSig for input errors).
//
// BOTH components MUST verify. This is the AND-mode binding: a
// signature where only one component verifies is a forgery (or a
// substitution attempt) and MUST be refused.
//
// The verification is order-independent: classical-first matches the
// natural reading of the construction, but a verifier could equally
// check PQ first. Either way, BOTH must pass.
func HybridVerify(pk *HybridPublicKey, msg []byte, sig *HybridSignature) error {
if pk == nil || pk.Classical == nil || pk.PQ == nil {
return ErrHybridNilKey
}
if sig == nil || len(sig.Classical) == 0 || len(sig.PQ) == 0 {
return ErrHybridNilSig
}
mBound, err := boundDigest(pk, msg)
if err != nil {
return err
}
// Classical: secp256k1 VerifyHash over m_bound[:32]. Mirrors the
// signing path's 32-byte prefix choice.
if !pk.Classical.VerifyHash(mBound[:32], sig.Classical) {
return ErrHybridClassicalVerify
}
// PQ: ML-DSA-65 VerifySignatureCtx over full m_bound + domain
// context. Mirrors the signing-side SignCtx call.
if !pk.PQ.VerifySignatureCtx(mBound, sig.PQ, []byte(HybridSigDomain)) {
return ErrHybridPQVerify
}
return nil
}
// HybridPublicKeyBytes returns the canonical wire encoding of the
// joint public key:
//
// left_encode(8·|pk_classical|) || pk_classical
// left_encode(8·|pk_pq|) || pk_pq
//
// This is the same framing m_bound uses for the joint pubkey, minus
// the domain prefix and msg. The encoding is unambiguous and can be
// reversed (each field's length is recoverable from its left_encode
// header).
//
// Used to compute a hybrid NodeID via the existing ids.NodeIDScheme
// derivation — the scheme produces NodeID = SHAKE256-384("NODE_ID_V1"
// || chainID || scheme_byte || pubkey)[:20], so passing the wire-form
// hybrid pubkey here yields a NodeID committed to BOTH components.
func HybridPublicKeyBytes(pk *HybridPublicKey) ([]byte, error) {
if pk == nil || pk.Classical == nil || pk.PQ == nil {
return nil, ErrHybridNilKey
}
pkClassical := pk.Classical.CompressedBytes()
pkPQ := pk.PQ.Bytes()
// Pre-size: header bytes for two left_encodes (<= 9 each) plus
// the two field payloads. The actual headers are typically 2-3
// bytes, but worst-case sizing keeps the allocation single.
out := make([]byte, 0, 18+len(pkClassical)+len(pkPQ))
out = append(out, leftEncode(uint64(len(pkClassical))*8)...)
out = append(out, pkClassical...)
out = append(out, leftEncode(uint64(len(pkPQ))*8)...)
out = append(out, pkPQ...)
return out, nil
}
+644
View File
@@ -0,0 +1,644 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// service_identity.go — mnemonic-derived service identity for in-cluster auth.
//
// Every Lux-derived service (kms-operator, hanzo-paas, hanzo-base,
// hanzo-commerce, …) authenticates to its peers by producing a signed
// envelope. Identity is derived from a BIP-39 mnemonic plus a stable
// servicePath — same mnemonic + path → same NodeID byte-for-byte across
// pods, reboots, machines.
//
// Pure-function derivation:
//
// seed = BIP-39 PBKDF2(mnemonic, "")
// master = BIP-32 master(seed)
// hardenedi = master · m/44'/9000'/serviceIndex'/0'/0'
// signing = ML-DSA-65(seed = SHAKE256(hardened-key || "lux-svc-mldsa-v1"))
// NodeID = SHAKE256-384("NODE_ID_V1" || chainID || 0x42 || pub)[:20]
//
// 9000 is the SLIP-0044 coin-type for Lux P/X (same value the wallet
// uses for staking and platform-chain HD derivation — see ~/work/lux/wallet
// /apps/web/src/lib/derive.ts and ~/work/lux/cli/cmd/keycmd). 0x42 is the
// canonical ML-DSA-65 NodeID scheme byte from luxfi/ids — the same scheme
// validator NodeIDs use under strict-PQ.
//
// serviceIndex is BIP-32-hardened from servicePath:
//
// serviceIndex = SHAKE256(servicePath)[:4] mod 2^31
//
// 31-bit space leaves the hardened-child top bit clear; collisions are
// astronomically unlikely for the few-hundred services any one cluster
// runs, but two distinct paths producing the same NodeID is a verifiable
// programmer error (NewServiceIdentity returns the same key — the caller
// chose colliding paths).
//
// Production:
//
// bootstrap = keys.NewServiceIdentity(bootstrapMnemonic, "luxd/staking-bootstrap")
// mnemonic = keys.LoadMnemonic(ctx, kmsAddr, "main", "/mnemonic", bootstrap)
// id, err = keys.NewServiceIdentity(mnemonic, "hanzo/kms-operator")
// if err != nil { … }
// defer id.Wipe()
// sig, err := id.Sign(envelope)
//
// Identity is exclusively a value type plus a pure-function constructor.
// It owns no I/O. The KMS/operator wires it to a `keys.LoadMnemonic` call;
// tests pass a known mnemonic and assert determinism.
package keys
import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
mldsa "github.com/luxfi/crypto/mldsa"
secp "github.com/luxfi/crypto/secp256k1"
bip32 "github.com/luxfi/go-bip32"
bip39 "github.com/luxfi/go-bip39"
"github.com/luxfi/ids"
"golang.org/x/crypto/hkdf"
"golang.org/x/crypto/sha3"
)
// HybridClassicalLeafIndex is the BIP-32 leaf index for the secp256k1
// (classical) component of a hybrid validator identity. Pinned at 0 so
// it coincides with the existing ML-DSA-65-only service identity leaf
// — a hybrid identity is a superset, never a different tree position.
const HybridClassicalLeafIndex uint32 = 0
// HybridPQLeafIndex is the BIP-32 leaf index for the ML-DSA-65 (PQ)
// component of a hybrid validator identity. Pinned at 1 so the PQ key
// derives at a sibling sub-path m/44'/9000'/serviceIndex'/0'/1' —
// distinct from the classical leaf and from the legacy ML-DSA-only
// leaf at index 0. The legacy path is preserved exactly; the new PQ
// leaf is purely additive.
const HybridPQLeafIndex uint32 = 1
// HybridClassicalDomain is the SHAKE256 customisation string for the
// secp256k1 component's scalar derivation. Pinned at v1.
const HybridClassicalDomain = "lux-hybrid-classical-secp256k1-v1"
// HybridPQDomain is the SHAKE256 customisation string for the ML-DSA-65
// component's seed derivation. Pinned at v1. Distinct from
// serviceIdentityDomain so a hybrid identity's PQ key is NEVER the same
// byte-string as the legacy ML-DSA-only identity at the same path —
// the two derivations are cryptographically separated.
const HybridPQDomain = "lux-hybrid-pq-mldsa65-v1"
// CoinTypeUTXO is the SLIP-0044 coin_type for the UTXO-layout DAG-like
// chains — X-Chain (AVM) + P-Chain. Service identities derive under
// m/44'/9000'/<serviceIndex>'/0'/0' so they share the coin-type tree
// with X/P staking keys but never collide with account-zero derivations
// (serviceIndex is hashed from servicePath, never 0 in practice for
// any well-formed path).
const CoinTypeUTXO = 9000
// BIP44Purpose is the BIP-44 purpose constant. Pinned to 44 even though
// service identities don't carry funds — staying inside the BIP-44
// purpose tree keeps the same mnemonic interoperable with the Lux
// wallet's existing derivation tree.
const BIP44Purpose = 44
// serviceIdentityDomain is the SHAKE256 customisation string for the
// ML-DSA-65 seed derivation. Pinned at v1; bumping invalidates every
// prior service NodeID, which is the correct behaviour for a hardfork
// of the derivation encoding.
const serviceIdentityDomain = "lux-svc-mldsa-v1"
// EnvelopeDomain is the customisation prefix mixed into every envelope
// signature so signatures from one envelope shape (a KMS opcode call)
// cannot be replayed against another (e.g. a future RPC over the same
// wire). Pinned at v1.
const EnvelopeDomain = "lux-svc-envelope-v1"
// ServiceChainID is the well-known chain identifier under which all
// service NodeIDs are derived. Distinct from any L1 chain ID so a
// service NodeID never accidentally validates against a chain's
// validator-set commitment. Set once and never bumped — the empty
// "service" string is the canonical seed.
//
// Use the helper ServiceChainIDForCluster if a deployment ever needs
// per-cluster service NodeID separation. The default (empty seed) is
// what every Hanzo cluster uses today.
var ServiceChainID = mustHashChainID("lux-service-identity")
// ErrInvalidServicePath is returned when the servicePath argument is
// empty after trim. Empty paths would collapse every service to the
// same NodeID, which would silently mask configuration drift.
var ErrInvalidServicePath = errors.New("keys: service path is required")
// ServiceIdentity binds a mnemonic-derived ML-DSA-65 signing key to
// its canonical NodeID. The struct owns the private key bytes; call
// Wipe() when done.
//
// Safe for concurrent use after construction — every field is read-only
// after NewServiceIdentity returns. Wipe is the only mutating method
// and the caller serialises it (typically a single defer).
type ServiceIdentity struct {
// ServicePath is the canonical path string (verbatim) used to derive
// the BIP-32 hardened index. Stored for diagnostics; the
// authoritative input is the derived NodeID.
ServicePath string
// NodeID is the 20-byte canonical NodeID derived under
// NodeIDSchemeMLDSA65. Map-key safe.
NodeID ids.NodeID
// TypedNodeID is the wire-form NodeID (scheme byte || NodeID).
// Travels in envelope headers so the receiver knows which verifier
// to dispatch.
TypedNodeID ids.TypedNodeID
// FullDigest is the 48-byte SHAKE256-384 commitment to the
// identity. Bound into envelope signatures to prevent cross-scheme
// confusion attacks.
FullDigest ids.FullDigest
// PublicKey is the ML-DSA-65 public key bytes.
PublicKey []byte
// privateKey is the ML-DSA-65 private key bytes. Never exposed via
// a getter; the only legal use is internal Sign().
privateKey []byte
}
// NewServiceIdentity is the canonical constructor. mnemonic must be a
// valid BIP-39 phrase; servicePath must be non-empty. Returns the
// derived ServiceIdentity ready to Sign().
//
// Pure function: given the same (mnemonic, servicePath) you get the
// same NodeID, byte-for-byte. No I/O, no randomness, no clock reads.
func NewServiceIdentity(mnemonic, servicePath string) (*ServiceIdentity, error) {
if !bip39.IsMnemonicValid(mnemonic) {
return nil, errors.New("keys: invalid BIP-39 mnemonic")
}
servicePath = trimServicePath(servicePath)
if servicePath == "" {
return nil, ErrInvalidServicePath
}
seed := bip39.NewSeed(mnemonic, "")
master, err := bip32.NewMasterKey(seed)
if err != nil {
return nil, fmt.Errorf("keys: bip32 master: %w", err)
}
// m/44' / 9000' / serviceIndex' / 0' / 0'
purpose, err := master.NewChildKey(bip32.FirstHardenedChild + BIP44Purpose)
if err != nil {
return nil, fmt.Errorf("keys: derive purpose: %w", err)
}
coin, err := purpose.NewChildKey(bip32.FirstHardenedChild + CoinTypeUTXO)
if err != nil {
return nil, fmt.Errorf("keys: derive coin: %w", err)
}
account, err := coin.NewChildKey(bip32.FirstHardenedChild + serviceIndex(servicePath))
if err != nil {
return nil, fmt.Errorf("keys: derive account: %w", err)
}
role, err := account.NewChildKey(bip32.FirstHardenedChild + 0)
if err != nil {
return nil, fmt.Errorf("keys: derive role: %w", err)
}
leaf, err := role.NewChildKey(bip32.FirstHardenedChild + 0)
if err != nil {
return nil, fmt.Errorf("keys: derive leaf: %w", err)
}
// ML-DSA-65 keygen is deterministic in its randomness stream: the
// same HKDF-Expand output drives circl's keygen to the same
// keypair byte-for-byte. We seed HKDF with the hardened BIP-32
// key plus a domain string + servicePath; if an attacker ever
// recovered the HKDF seed they still need the mnemonic to walk
// back the BIP-32 chain.
mldsaSeed := mldsaSeedFor(leaf.Key, servicePath)
reader := hkdf.New(sha3.New256, mldsaSeed, nil, []byte(serviceIdentityDomain))
priv, err := mldsa.GenerateKey(reader, mldsa.MLDSA65)
if err != nil {
return nil, fmt.Errorf("keys: mldsa keygen: %w", err)
}
pubBytes := priv.PublicKey.Bytes()
privBytes := append([]byte(nil), priv.Bytes()...)
scheme := ids.NodeIDSchemeMLDSA65
typed, full, err := ids.TypedNodeIDFromMLDSA(scheme, ServiceChainID, pubBytes)
if err != nil {
return nil, fmt.Errorf("keys: derive node id: %w", err)
}
return &ServiceIdentity{
ServicePath: servicePath,
NodeID: typed.NodeID,
TypedNodeID: typed,
FullDigest: full,
PublicKey: pubBytes,
privateKey: privBytes,
}, nil
}
// Sign produces a deterministic ML-DSA-65 signature over the envelope
// digest. The caller is responsible for serialising the envelope into
// canonical bytes before calling Sign — see SignEnvelope for the
// canonical (method, path, payload, timestamp, nonce) shape.
//
// The signed bytes are the SHAKE256 digest of:
//
// left_encode(|domain|·8) || EnvelopeDomain ||
// left_encode(|full_digest|·8) || FullDigest ||
// left_encode(|envelope|·8) || envelope
//
// Binding the FullDigest into the prehash means a verifier always
// rejects an envelope signed by a different identity — even a key with
// the same NodeID prefix.
func (s *ServiceIdentity) Sign(envelope []byte) ([]byte, error) {
if s == nil || len(s.privateKey) == 0 {
return nil, errors.New("keys: service identity is empty (wiped?)")
}
digest := envelopeDigest(s.FullDigest, envelope)
priv, err := mldsa.PrivateKeyFromBytes(mldsa.MLDSA65, s.privateKey)
if err != nil {
return nil, fmt.Errorf("keys: parse private key: %w", err)
}
// FIPS 204 §5.2 hedged sign — circl reads its own randomness
// internally. The EnvelopeDomain context byte string is bound
// into the signature so a cross-protocol replay of the same key
// against a different envelope shape rejects.
sig, err := priv.SignCtx(nil, digest, []byte(EnvelopeDomain))
if err != nil {
return nil, fmt.Errorf("keys: sign: %w", err)
}
return sig, nil
}
// VerifyServiceEnvelope verifies an ML-DSA-65 signature against an
// envelope produced by Sign. The caller supplies the signer's
// FullDigest (the 48-byte commitment carried in the envelope header)
// and public key bytes — both authenticated by the consensus layer.
//
// Pure function: no I/O, no time dependency.
func VerifyServiceEnvelope(pubKey []byte, fullDigest ids.FullDigest, envelope, sig []byte) error {
if len(pubKey) == 0 {
return errors.New("keys: empty public key")
}
if len(sig) == 0 {
return errors.New("keys: empty signature")
}
pub, err := mldsa.PublicKeyFromBytes(pubKey, mldsa.MLDSA65)
if err != nil {
return fmt.Errorf("keys: parse public key: %w", err)
}
digest := envelopeDigest(fullDigest, envelope)
if !pub.VerifySignatureCtx(digest, sig, []byte(EnvelopeDomain)) {
return errors.New("keys: envelope signature verification failed")
}
return nil
}
// Wipe zeroes the private key in place. Idempotent. Safe to call from
// a defer.
func (s *ServiceIdentity) Wipe() {
if s == nil {
return
}
for i := range s.privateKey {
s.privateKey[i] = 0
}
s.privateKey = nil
}
// envelopeDigest computes the canonical SHAKE256 digest a signature
// binds. Exported so a hand-rolled verifier (the kmsd consensus_auth
// path) can call the same helper.
func envelopeDigest(fullDigest ids.FullDigest, envelope []byte) []byte {
h := sha3.NewShake256()
_, _ = h.Write(leftEncode(uint64(len(EnvelopeDomain)) * 8))
_, _ = h.Write([]byte(EnvelopeDomain))
_, _ = h.Write(leftEncode(uint64(ids.FullDigestLen) * 8))
_, _ = h.Write(fullDigest[:])
_, _ = h.Write(leftEncode(uint64(len(envelope)) * 8))
_, _ = h.Write(envelope)
out := make([]byte, 32)
_, _ = h.Read(out)
return out
}
// mldsaSeedFor returns the 32-byte deterministic seed for ML-DSA-65
// key generation. SHAKE256 absorbs the BIP-32 hardened child key, the
// domain string, and the servicePath so the seed is unique per
// (mnemonic, path) tuple.
func mldsaSeedFor(hardenedKey []byte, servicePath string) []byte {
h := sha3.NewShake256()
_, _ = h.Write(leftEncode(uint64(len(serviceIdentityDomain)) * 8))
_, _ = h.Write([]byte(serviceIdentityDomain))
_, _ = h.Write(leftEncode(uint64(len(hardenedKey)) * 8))
_, _ = h.Write(hardenedKey)
_, _ = h.Write(leftEncode(uint64(len(servicePath)) * 8))
_, _ = h.Write([]byte(servicePath))
out := make([]byte, 32)
_, _ = h.Read(out)
return out
}
// serviceIndex hashes a servicePath into a hardened-safe BIP-32 child
// index. The top bit is masked off so adding bip32.FirstHardenedChild
// in the caller stays inside the hardened space without overflowing.
func serviceIndex(servicePath string) uint32 {
sum := sha256.Sum256([]byte(servicePath))
v := binary.BigEndian.Uint32(sum[:4])
return v & 0x7FFFFFFF
}
// trimServicePath collapses whitespace and leading/trailing slashes
// without rewriting embedded slashes. Empty input returns "".
func trimServicePath(p string) string {
// strip ASCII whitespace
out := make([]byte, 0, len(p))
for i := 0; i < len(p); i++ {
c := p[i]
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
continue
}
out = append(out, c)
}
// trim leading/trailing '/'
for len(out) > 0 && out[0] == '/' {
out = out[1:]
}
for len(out) > 0 && out[len(out)-1] == '/' {
out = out[:len(out)-1]
}
return string(out)
}
// mustHashChainID seeds the package-level ServiceChainID. Panics on
// the impossible (sha256 always succeeds); the panic surfaces a
// programmer error at init time rather than a runtime nil.
func mustHashChainID(seed string) ids.ID {
sum := sha256.Sum256([]byte(seed))
var id ids.ID
copy(id[:], sum[:])
return id
}
// leftEncode is the SP 800-185 §2.3.1 left_encode operation. Kept
// local so service_identity.go has no cross-package dependency on
// luxfi/ids's private helpers — same byte-for-byte algorithm.
func leftEncode(x uint64) []byte {
if x == 0 {
return []byte{0x01, 0x00}
}
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], x)
i := 0
for i < 7 && buf[i] == 0 {
i++
}
out := make([]byte, 0, 9-i)
out = append(out, byte(8-i))
out = append(out, buf[i:]...)
return out
}
// ServiceChainIDForCluster is a future hook for per-cluster NodeID
// separation. Today every Hanzo cluster shares ServiceChainID; if a
// future deployment ever needs to isolate two clusters' NodeID spaces
// (e.g. lux-mainnet vs hanzo-prod), the operator can override
// ServiceChainID at boot via this helper. Pure function — no global
// state mutated on call.
func ServiceChainIDForCluster(clusterSeed string) ids.ID {
if clusterSeed == "" {
return ServiceChainID
}
return mustHashChainID("lux-service-identity:" + clusterSeed)
}
// HybridIdentity binds a mnemonic-derived BBF-bound hybrid signing key
// (secp256k1 + ML-DSA-65) to its canonical NodeID. The struct owns the
// joint private key; call Wipe() when done.
//
// The NodeID is committed to the wire-form joint pubkey via the
// existing ids.NodeIDSchemeMLDSA65 derivation — chosen because the
// PQ component is the binding primitive (an adversary that holds only
// the classical key cannot mint a NodeID's matching FullDigest). Per
// cryptographer review the single-SHAKE-256-384 derivation is sound;
// no BTC-style double-hash is added.
type HybridIdentity struct {
// ServicePath is the canonical path string (verbatim) used to
// derive both hybrid components. Stored for diagnostics.
ServicePath string
// NodeID is the 20-byte canonical NodeID derived under
// NodeIDSchemeMLDSA65 over the wire-form hybrid public key.
NodeID ids.NodeID
// TypedNodeID is the wire-form NodeID (scheme byte || NodeID).
TypedNodeID ids.TypedNodeID
// FullDigest is the 48-byte SHAKE256-384 commitment to the
// hybrid identity. Bound into envelope signatures.
FullDigest ids.FullDigest
// PublicKey is the joint public key value. Both components are
// always populated.
PublicKey *HybridPublicKey
// PublicKeyBytes is the canonical wire encoding of the joint
// pubkey — useful for logging, storage, and on-chain commitment.
// Same value HybridPublicKeyBytes(PublicKey) returns.
PublicKeyBytes []byte
// privateKey is the joint signing key. Never exposed via a
// getter; the only legal use is internal Sign().
privateKey *HybridPrivateKey
}
// DeriveHybridIdentity is the canonical constructor for a BBF-bound
// hybrid validator identity. mnemonic must be a valid BIP-39 phrase;
// servicePath must be non-empty. Returns the derived HybridIdentity
// ready to Sign().
//
// Derivation tree:
//
// classical: m/44' / 9000' / serviceIndex' / 0' / 0' (secp256k1)
// pq: m/44' / 9000' / serviceIndex' / 0' / 1' (ML-DSA-65)
//
// The two leaves share the same hardened branches up to the role
// node; the only branch that distinguishes them is the leaf index
// (0 vs 1). Both leaves are hardened. The classical leaf at index 0
// COINCIDES with the legacy ML-DSA-only service identity path — this
// is intentional: a hybrid identity is a superset that adds a PQ
// component, never a different tree position. The classical leaf is
// then KDF-mixed with HybridClassicalDomain so the secp256k1 scalar
// is cryptographically separated from any ML-DSA-only seed at the
// same BIP-32 leaf — they share the leaf but not the seed.
//
// Pure function: given the same (mnemonic, servicePath) you get the
// same HybridIdentity, byte-for-byte. No I/O, no randomness, no clock
// reads.
func DeriveHybridIdentity(mnemonic, servicePath string) (*HybridIdentity, error) {
if !bip39.IsMnemonicValid(mnemonic) {
return nil, errors.New("keys: invalid BIP-39 mnemonic")
}
servicePath = trimServicePath(servicePath)
if servicePath == "" {
return nil, ErrInvalidServicePath
}
seed := bip39.NewSeed(mnemonic, "")
master, err := bip32.NewMasterKey(seed)
if err != nil {
return nil, fmt.Errorf("keys: bip32 master: %w", err)
}
// Walk to the shared role node: m/44'/9000'/serviceIndex'/0'.
purpose, err := master.NewChildKey(bip32.FirstHardenedChild + BIP44Purpose)
if err != nil {
return nil, fmt.Errorf("keys: derive purpose: %w", err)
}
coin, err := purpose.NewChildKey(bip32.FirstHardenedChild + CoinTypeUTXO)
if err != nil {
return nil, fmt.Errorf("keys: derive coin: %w", err)
}
account, err := coin.NewChildKey(bip32.FirstHardenedChild + serviceIndex(servicePath))
if err != nil {
return nil, fmt.Errorf("keys: derive account: %w", err)
}
role, err := account.NewChildKey(bip32.FirstHardenedChild + 0)
if err != nil {
return nil, fmt.Errorf("keys: derive role: %w", err)
}
// Classical leaf: m/44'/9000'/serviceIndex'/0'/0' (hardened).
classicalLeaf, err := role.NewChildKey(bip32.FirstHardenedChild + HybridClassicalLeafIndex)
if err != nil {
return nil, fmt.Errorf("keys: derive classical leaf: %w", err)
}
classicalKey, err := classicalScalarFor(classicalLeaf.Key, servicePath)
if err != nil {
return nil, fmt.Errorf("keys: classical keygen: %w", err)
}
// PQ leaf: m/44'/9000'/serviceIndex'/0'/1' (hardened).
pqLeaf, err := role.NewChildKey(bip32.FirstHardenedChild + HybridPQLeafIndex)
if err != nil {
return nil, fmt.Errorf("keys: derive pq leaf: %w", err)
}
pqKey, err := pqKeyFor(pqLeaf.Key, servicePath)
if err != nil {
return nil, fmt.Errorf("keys: pq keygen: %w", err)
}
sk := &HybridPrivateKey{
Classical: classicalKey,
PQ: pqKey,
}
pk := sk.Public()
pkBytes, err := HybridPublicKeyBytes(pk)
if err != nil {
return nil, fmt.Errorf("keys: marshal hybrid pubkey: %w", err)
}
// NodeID derivation: SHAKE256-384("NODE_ID_V1" || ServiceChainID
// || 0x42 || wire-form hybrid pubkey)[:20]. The 0x42 scheme byte
// matches the existing ML-DSA-65 scheme — the FullDigest is bound
// to the WHOLE hybrid pubkey (both components) so a hybrid NodeID
// is never the same as either component's NodeID-on-its-own.
scheme := ids.NodeIDSchemeMLDSA65
typed, full, err := ids.TypedNodeIDFromMLDSA(scheme, ServiceChainID, pkBytes)
if err != nil {
return nil, fmt.Errorf("keys: derive node id: %w", err)
}
return &HybridIdentity{
ServicePath: servicePath,
NodeID: typed.NodeID,
TypedNodeID: typed,
FullDigest: full,
PublicKey: pk,
PublicKeyBytes: pkBytes,
privateKey: sk,
}, nil
}
// Sign produces a BBF-bound joint signature over msg. Delegates to
// HybridSign — the identity owns the joint private key.
func (h *HybridIdentity) Sign(msg []byte) (*HybridSignature, error) {
if h == nil || h.privateKey == nil {
return nil, errors.New("keys: hybrid identity is empty (wiped?)")
}
// nil randSource → crypto/rand inside HybridSign.
return HybridSign(h.privateKey, msg, nil)
}
// Wipe zeroes the joint private key in place. Idempotent. Safe to call
// from a defer on a nil receiver.
func (h *HybridIdentity) Wipe() {
if h == nil {
return
}
if h.privateKey != nil {
h.privateKey.Wipe()
h.privateKey = nil
}
}
// classicalScalarFor returns a deterministic secp256k1 private key
// derived from a BIP-32 hardened leaf key + servicePath. SHAKE256
// absorbs the leaf key, the HybridClassicalDomain string, and the
// servicePath; the resulting 32-byte digest is reduced mod the
// secp256k1 group order by secp.ToPrivateKey, which rejects 0 and
// scalars >= n.
//
// Pure function: same (leafKey, servicePath) → same private key.
func classicalScalarFor(leafKey []byte, servicePath string) (*secp.PrivateKey, error) {
h := sha3.NewShake256()
_, _ = h.Write(leftEncode(uint64(len(HybridClassicalDomain)) * 8))
_, _ = h.Write([]byte(HybridClassicalDomain))
_, _ = h.Write(leftEncode(uint64(len(leafKey)) * 8))
_, _ = h.Write(leafKey)
_, _ = h.Write(leftEncode(uint64(len(servicePath)) * 8))
_, _ = h.Write([]byte(servicePath))
// secp256k1 group order n ≈ 2^256 - 2^32 - 977. The probability
// that a uniform 32-byte sample is >= n is ~2^-128 — astronomical.
// secp.ToPrivateKey rejects >= n and == 0; the loop below
// re-samples by extending the SHAKE squeeze. In practice the loop
// body runs exactly once for every well-formed input.
for {
var scalar [32]byte
_, _ = h.Read(scalar[:])
priv, err := secp.ToPrivateKey(scalar[:])
if err == nil {
return priv, nil
}
// Loop: SHAKE continues squeezing fresh bytes — this is the
// canonical resample idiom for rejection-sampled scalars.
}
}
// pqKeyFor returns a deterministic ML-DSA-65 private key derived from
// a BIP-32 hardened leaf key + servicePath. The PQ derivation mirrors
// the ML-DSA-only service identity: HKDF-Expand seeds circl's keygen
// to the same keypair byte-for-byte. The HybridPQDomain string is
// distinct from serviceIdentityDomain so a hybrid identity's PQ key
// is NEVER the same byte-string as a legacy ML-DSA-only identity at
// the same leaf.
//
// Pure function: same (leafKey, servicePath) → same private key.
func pqKeyFor(leafKey []byte, servicePath string) (*mldsa.PrivateKey, error) {
h := sha3.NewShake256()
_, _ = h.Write(leftEncode(uint64(len(HybridPQDomain)) * 8))
_, _ = h.Write([]byte(HybridPQDomain))
_, _ = h.Write(leftEncode(uint64(len(leafKey)) * 8))
_, _ = h.Write(leafKey)
_, _ = h.Write(leftEncode(uint64(len(servicePath)) * 8))
_, _ = h.Write([]byte(servicePath))
seed := make([]byte, 32)
_, _ = h.Read(seed)
reader := hkdf.New(sha3.New256, seed, nil, []byte(HybridPQDomain))
return mldsa.GenerateKey(reader, mldsa.MLDSA65)
}