Skip to content

Commit df38375

Browse files
authored
feat: generate secp256k1 private key (#483)
* feat: nodekey input file * feat: generate heimdall-v2 priv validator key * chore: nit * docs: update
1 parent 79290a5 commit df38375

File tree

6 files changed

+364
-28
lines changed

6 files changed

+364
-28
lines changed

cmd/nodekey/nodekey.go

+42-12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package nodekey
22

33
import (
44
"bytes"
5+
"crypto/ecdsa"
56
"crypto/ed25519"
67
"crypto/rand"
78
"encoding/binary"
@@ -10,6 +11,7 @@ import (
1011
"fmt"
1112
"io"
1213
"net"
14+
"strings"
1315

1416
_ "embed"
1517

@@ -42,6 +44,7 @@ var (
4244
inputNodeKeyTCP *int
4345
inputNodeKeyUDP *int
4446
inputNodeKeyFile *string
47+
inputNodeKeyPrivateKey *string
4548
inputNodeKeySign *bool
4649
inputNodeKeySeed *uint64
4750
inputNodeKeyMarshalProtobuf *bool
@@ -67,11 +70,22 @@ var NodekeyCmd = &cobra.Command{
6770
var withSeed bool
6871
switch *inputNodeKeyProtocol {
6972
case "devp2p":
70-
var err error
71-
nko, err = generateDevp2pNodeKey()
72-
if err != nil {
73-
return err
73+
switch *inputNodeKeyType {
74+
case "ed25519":
75+
var err error
76+
nko, err = generateDevp2pNodeKey()
77+
if err != nil {
78+
return err
79+
}
80+
case "secp256k1":
81+
secret := []byte(strings.TrimPrefix(*inputNodeKeyPrivateKey, "0x"))
82+
secp256k1PrivateKey := generateSecp256k1PrivateKey(secret)
83+
if err := displayHeimdallV2PrivValidatorKey(secp256k1PrivateKey); err != nil {
84+
return err
85+
}
86+
return nil
7487
}
88+
7589
case "seed-libp2p":
7690
withSeed = true
7791
fallthrough
@@ -100,14 +114,15 @@ var NodekeyCmd = &cobra.Command{
100114
if len(args) != 0 {
101115
return fmt.Errorf("this command expects no arguments")
102116
}
117+
103118
validProtocols := []string{"devp2p", "libp2p", "seed-libp2p"}
104119
ok := slices.Contains(validProtocols, *inputNodeKeyProtocol)
105120
if !ok {
106121
return fmt.Errorf("the protocol %s is not implemented", *inputNodeKeyProtocol)
107122
}
108123

109124
if *inputNodeKeyProtocol == "devp2p" {
110-
invalidFlags := []string{"key-type", "seed", "marshal-protobuf"}
125+
invalidFlags := []string{"seed", "marshal-protobuf"}
111126
err := validateNodeKeyFlags(cmd, invalidFlags)
112127
if err != nil {
113128
return err
@@ -169,13 +184,26 @@ func keyTypeToInt(keyType string) (int, error) {
169184
}
170185

171186
func generateDevp2pNodeKey() (nodeKeyOut, error) {
172-
nodeKey, err := gethcrypto.GenerateKey()
187+
var nodeKey *ecdsa.PrivateKey
188+
var err error
173189

174-
if *inputNodeKeyFile != "" {
190+
switch {
191+
case *inputNodeKeyPrivateKey != "":
192+
privateKey := strings.TrimPrefix(*inputNodeKeyPrivateKey, "0x")
193+
nodeKey, err = gethcrypto.HexToECDSA(privateKey)
194+
if err != nil {
195+
return nodeKeyOut{}, fmt.Errorf("could not create ECDSA private key from given value: %s: %w", *inputNodeKeyPrivateKey, err)
196+
}
197+
case *inputNodeKeyFile != "":
175198
nodeKey, err = gethcrypto.LoadECDSA(*inputNodeKeyFile)
176-
}
177-
if err != nil {
178-
return nodeKeyOut{}, fmt.Errorf("could not generate key: %w", err)
199+
if err != nil {
200+
return nodeKeyOut{}, fmt.Errorf("could not load ECDSA private key from file %s: %w", *inputNodeKeyFile, err)
201+
}
202+
default:
203+
nodeKey, err = gethcrypto.GenerateKey()
204+
if err != nil {
205+
return nodeKeyOut{}, fmt.Errorf("could not generate ECDSA private key: %w", err)
206+
}
179207
}
180208

181209
nko := nodeKeyOut{}
@@ -250,6 +278,10 @@ func generateLibp2pNodeKey(keyType int, seed bool) (nodeKeyOut, error) {
250278
}
251279

252280
func init() {
281+
inputNodeKeyPrivateKey = NodekeyCmd.PersistentFlags().String("private-key", "", "Use the provided private key (in hex format)")
282+
inputNodeKeyFile = NodekeyCmd.PersistentFlags().StringP("file", "f", "", "A file with the private nodekey (in hex format)")
283+
NodekeyCmd.MarkFlagsMutuallyExclusive("private-key", "file")
284+
253285
inputNodeKeyProtocol = NodekeyCmd.PersistentFlags().String("protocol", "devp2p", "devp2p|libp2p|pex|seed-libp2p")
254286
inputNodeKeyType = NodekeyCmd.PersistentFlags().String("key-type", "ed25519", "ed25519|secp256k1|ecdsa|rsa")
255287
inputNodeKeyIP = NodekeyCmd.PersistentFlags().StringP("ip", "i", "0.0.0.0", "The IP to be associated with this address")
@@ -258,6 +290,4 @@ func init() {
258290
inputNodeKeySign = NodekeyCmd.PersistentFlags().BoolP("sign", "s", false, "Should the node record be signed?")
259291
inputNodeKeySeed = NodekeyCmd.PersistentFlags().Uint64P("seed", "S", 271828, "A numeric seed value")
260292
inputNodeKeyMarshalProtobuf = NodekeyCmd.PersistentFlags().BoolP("marshal-protobuf", "m", false, "If true the libp2p key will be marshaled to protobuf format rather than raw")
261-
262-
inputNodeKeyFile = NodekeyCmd.PersistentFlags().StringP("file", "f", "", "A file with the private nodekey in hex format")
263293
}

cmd/nodekey/secp256k1.go

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package nodekey
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"crypto/subtle"
7+
"fmt"
8+
"math/big"
9+
10+
secp256k1 "github.com/btcsuite/btcd/btcec/v2"
11+
"github.com/cometbft/cometbft/crypto"
12+
"github.com/cometbft/cometbft/privval"
13+
14+
cmtjson "github.com/cometbft/cometbft/libs/json"
15+
gethcrypto "github.com/ethereum/go-ethereum/crypto"
16+
)
17+
18+
const (
19+
PrivKeyName = "comet/PrivKeySecp256k1Uncompressed"
20+
PubKeyName = "comet/PubKeySecp256k1Uncompressed"
21+
22+
KeyType = "secp256k1"
23+
PrivKeySize = 32
24+
// PubKeySize (uncompressed) is composed of 65 bytes for two field elements (x and y)
25+
// and a prefix byte (0x04) to indicate that it is uncompressed.
26+
PubKeySize = 65
27+
// SigSize is the size of the ECDSA signature.
28+
SigSize = 65
29+
)
30+
31+
var _ crypto.PrivKey = PrivKey{}
32+
var _ crypto.PubKey = PubKey{}
33+
34+
// -------------------------------------
35+
// PrivKey type
36+
// -------------------------------------
37+
38+
type PrivKey []byte
39+
40+
// Bytes marshals the private key using amino encoding.
41+
func (privKey PrivKey) Bytes() []byte {
42+
return []byte(privKey)
43+
}
44+
45+
// PubKey performs the point-scalar multiplication from the privKey on the
46+
// generator point to get the pubkey.
47+
func (privKey PrivKey) PubKey() crypto.PubKey {
48+
privateObject, err := gethcrypto.ToECDSA(privKey)
49+
if err != nil {
50+
panic(err)
51+
}
52+
53+
pk := gethcrypto.FromECDSAPub(&privateObject.PublicKey)
54+
55+
return PubKey(pk)
56+
}
57+
58+
// Equals - you probably don't need to use this.
59+
// Runs in constant time based on length of the keys.
60+
func (privKey PrivKey) Equals(other crypto.PrivKey) bool {
61+
if otherSecp, ok := other.(PrivKey); ok {
62+
return subtle.ConstantTimeCompare(privKey[:], otherSecp[:]) == 1
63+
}
64+
return false
65+
}
66+
67+
func (privKey PrivKey) Type() string {
68+
return KeyType
69+
}
70+
71+
// Sign creates an ECDSA signature on curve Secp256k1, using SHA256 on the msg.
72+
// The returned signature will be of the form R || S || V (in lower-S form).
73+
func (privKey PrivKey) Sign(msg []byte) ([]byte, error) {
74+
privateObject, err := gethcrypto.ToECDSA(privKey)
75+
if err != nil {
76+
return nil, err
77+
}
78+
79+
return gethcrypto.Sign(gethcrypto.Keccak256(msg), privateObject)
80+
}
81+
82+
// -------------------------------------
83+
// PubKey type
84+
// -------------------------------------
85+
86+
type PubKey []byte
87+
88+
// Bytes returns the pubkey marshaled with amino encoding.
89+
func (pubKey PubKey) Bytes() []byte {
90+
return []byte(pubKey)
91+
}
92+
93+
// Address returns a Ethereym style addresses: Last_20_Bytes(KECCAK256(pubkey))
94+
func (pubKey PubKey) Address() crypto.Address {
95+
if len(pubKey) != PubKeySize {
96+
panic(fmt.Sprintf("length of pubkey is incorrect %d != %d", len(pubKey), PubKeySize))
97+
}
98+
return gethcrypto.Keccak256(pubKey[1:])[12:]
99+
}
100+
101+
func (pubKey PubKey) Equals(other crypto.PubKey) bool {
102+
if otherSecp, ok := other.(PubKey); ok {
103+
return bytes.Equal(pubKey[:], otherSecp[:])
104+
}
105+
return false
106+
}
107+
108+
func (pubKey PubKey) Type() string {
109+
return KeyType
110+
}
111+
112+
// VerifySignature verifies a signature of the form R || S || V.
113+
// It rejects signatures which are not in lower-S form.
114+
func (pubKey PubKey) VerifySignature(msg []byte, sigStr []byte) bool {
115+
if len(sigStr) != SigSize {
116+
117+
return false
118+
}
119+
120+
hash := gethcrypto.Keccak256(msg)
121+
return gethcrypto.VerifySignature(pubKey, hash, sigStr[:64])
122+
}
123+
124+
func init() {
125+
cmtjson.RegisterType(PubKey{}, PubKeyName)
126+
cmtjson.RegisterType(PrivKey{}, PrivKeyName)
127+
}
128+
129+
// Generate an secp256k1 private key from a secret.
130+
// Most of the logic has been copy/pasted from 0xPolygon/cometbft's fork.
131+
// https://github.com/0xPolygon/cometbft/blob/v0.1.2-beta-polygon/crypto/secp256k1/secp256k1.go
132+
// Notes:
133+
// - It is not possible to import the package yet because go.mod declares its path as github.com/cometbft/cometbft instead of github.com/0xpolygon/cometbft.
134+
// - This logic will need to be updated to support newer versions.
135+
func generateSecp256k1PrivateKey(secret []byte) PrivKey {
136+
// To guarantee that we have a valid field element, we use the approach of: "Suite B Implementer’s Guide to FIPS 186-3", A.2.1
137+
// https://apps.nsa.gov/iaarchive/library/ia-guidance/ia-solutions-for-classified/algorithm-guidance/suite-b-implementers-guide-to-fips-186-3-ecdsa.cfm
138+
// See also https://github.com/golang/go/blob/0380c9ad38843d523d9c9804fe300cb7edd7cd3c/src/crypto/ecdsa/ecdsa.go#L89-L101
139+
secretHash := sha256.Sum256(secret)
140+
fe := new(big.Int).SetBytes(secretHash[:])
141+
142+
one := new(big.Int).SetInt64(1)
143+
n := new(big.Int).Sub(secp256k1.S256().N, one)
144+
fe.Mod(fe, n)
145+
fe.Add(fe, one)
146+
147+
feB := fe.Bytes()
148+
privKey32 := make([]byte, PrivKeySize)
149+
// Copy feB over to fixed 32 byte privKey32 and pad (if necessary).
150+
copy(privKey32[32-len(feB):32], feB)
151+
152+
return PrivKey(privKey32)
153+
}
154+
155+
func displayHeimdallV2PrivValidatorKey(privKey crypto.PrivKey) error {
156+
nodeKey := privval.FilePVKey{
157+
Address: privKey.PubKey().Address(),
158+
PubKey: privKey.PubKey(),
159+
PrivKey: privKey,
160+
}
161+
jsonBytes, err := cmtjson.MarshalIndent(nodeKey, "", " ")
162+
if err != nil {
163+
return err
164+
}
165+
fmt.Println(string(jsonBytes))
166+
return nil
167+
}

cmd/nodekey/usage.md

+34
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,37 @@ $ polycli nodekey --protocol libp2p
1515
# Generate a networking keypair for edge.
1616
$ polycli nodekey --protocol libp2p --key-type secp256k1 --marshal-protobuf
1717
```
18+
19+
Generate an [ED25519](https://en.wikipedia.org/wiki/Curve25519) nodekey from a private key (in hex format).
20+
21+
```bash
22+
polycli nodekey --private-key 2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2 | jq
23+
```
24+
25+
```json
26+
{
27+
"PublicKey": "93e8717f46b146ebfb99159eb13a5d044c191998656c8b79007b16051bb1ff762d09884e43783d898dd47f6220af040206cabbd45c9a26bb278a522c3d538a1f",
28+
"PrivateKey": "2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2",
29+
"ENR": "enode://93e8717f46b146ebfb99159eb13a5d044c191998656c8b79007b16051bb1ff762d09884e43783d898dd47f6220af040206cabbd45c9a26bb278a522c3d538a1f@0.0.0.0:30303?discport=0"
30+
}
31+
```
32+
33+
Generate an [Secp256k1](https://en.bitcoin.it/wiki/Secp256k1) nodekey from a private key (in hex format).
34+
35+
```bash
36+
polycli nodekey --private-key 2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2 --key-type secp256k1 | jq
37+
```
38+
39+
```json
40+
{
41+
"address": "99AA9FC116C1E5E741E9EC18BD1FD232130A5C44",
42+
"pub_key": {
43+
"type": "comet/PubKeySecp256k1Uncompressed",
44+
"value": "BBNYN0nMJsgo0Fp3kVW85PRGBNe7Gdz1XBFuTWQ7D8FnKRb2JYO3i3FK2UiA5+gTSxYu1K66KdYjQYP1mOkH09g="
45+
},
46+
"priv_key": {
47+
"type": "comet/PrivKeySecp256k1Uncompressed",
48+
"value": "OP72E0D7GEi/4VySpolVudLW7uPJm+6PWEtFKJmvp1M="
49+
}
50+
}
51+
```

doc/polycli_nodekey.md

+45-10
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,54 @@ $ polycli nodekey --protocol libp2p
3737
$ polycli nodekey --protocol libp2p --key-type secp256k1 --marshal-protobuf
3838
```
3939

40+
Generate an [ED25519](https://en.wikipedia.org/wiki/Curve25519) nodekey from a private key (in hex format).
41+
42+
```bash
43+
polycli nodekey --private-key 2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2 | jq
44+
```
45+
46+
```json
47+
{
48+
"PublicKey": "93e8717f46b146ebfb99159eb13a5d044c191998656c8b79007b16051bb1ff762d09884e43783d898dd47f6220af040206cabbd45c9a26bb278a522c3d538a1f",
49+
"PrivateKey": "2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2",
50+
"ENR": "enode://93e8717f46b146ebfb99159eb13a5d044c191998656c8b79007b16051bb1ff762d09884e43783d898dd47f6220af040206cabbd45c9a26bb278a522c3d538a1f@0.0.0.0:30303?discport=0"
51+
}
52+
```
53+
54+
Generate an [Secp256k1](https://en.bitcoin.it/wiki/Secp256k1) nodekey from a private key (in hex format).
55+
56+
```bash
57+
polycli nodekey --private-key 2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2 --key-type secp256k1 | jq
58+
```
59+
60+
```json
61+
{
62+
"address": "99AA9FC116C1E5E741E9EC18BD1FD232130A5C44",
63+
"pub_key": {
64+
"type": "comet/PubKeySecp256k1Uncompressed",
65+
"value": "BBNYN0nMJsgo0Fp3kVW85PRGBNe7Gdz1XBFuTWQ7D8FnKRb2JYO3i3FK2UiA5+gTSxYu1K66KdYjQYP1mOkH09g="
66+
},
67+
"priv_key": {
68+
"type": "comet/PrivKeySecp256k1Uncompressed",
69+
"value": "OP72E0D7GEi/4VySpolVudLW7uPJm+6PWEtFKJmvp1M="
70+
}
71+
}
72+
```
73+
4074
## Flags
4175

4276
```bash
43-
-f, --file string A file with the private nodekey in hex format
44-
-h, --help help for nodekey
45-
-i, --ip string The IP to be associated with this address (default "0.0.0.0")
46-
--key-type string ed25519|secp256k1|ecdsa|rsa (default "ed25519")
47-
-m, --marshal-protobuf If true the libp2p key will be marshaled to protobuf format rather than raw
48-
--protocol string devp2p|libp2p|pex|seed-libp2p (default "devp2p")
49-
-S, --seed uint A numeric seed value (default 271828)
50-
-s, --sign Should the node record be signed?
51-
-t, --tcp int The tcp Port to be associated with this address (default 30303)
52-
-u, --udp int The udp Port to be associated with this address
77+
-f, --file string A file with the private nodekey (in hex format)
78+
-h, --help help for nodekey
79+
-i, --ip string The IP to be associated with this address (default "0.0.0.0")
80+
--key-type string ed25519|secp256k1|ecdsa|rsa (default "ed25519")
81+
-m, --marshal-protobuf If true the libp2p key will be marshaled to protobuf format rather than raw
82+
--private-key string Use the provided private key (in hex format)
83+
--protocol string devp2p|libp2p|pex|seed-libp2p (default "devp2p")
84+
-S, --seed uint A numeric seed value (default 271828)
85+
-s, --sign Should the node record be signed?
86+
-t, --tcp int The tcp Port to be associated with this address (default 30303)
87+
-u, --udp int The udp Port to be associated with this address
5388
```
5489

5590
The command also inherits flags from parent commands.

0 commit comments

Comments
 (0)