Skip to content

Commit

Permalink
feat: generate secp256k1 private key (#483)
Browse files Browse the repository at this point in the history
* feat: nodekey input file

* feat: generate heimdall-v2 priv validator key

* chore: nit

* docs: update
  • Loading branch information
leovct authored Jan 28, 2025
1 parent 79290a5 commit df38375
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 28 deletions.
54 changes: 42 additions & 12 deletions cmd/nodekey/nodekey.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package nodekey

import (
"bytes"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"encoding/binary"
Expand All @@ -10,6 +11,7 @@ import (
"fmt"
"io"
"net"
"strings"

_ "embed"

Expand Down Expand Up @@ -42,6 +44,7 @@ var (
inputNodeKeyTCP *int
inputNodeKeyUDP *int
inputNodeKeyFile *string
inputNodeKeyPrivateKey *string
inputNodeKeySign *bool
inputNodeKeySeed *uint64
inputNodeKeyMarshalProtobuf *bool
Expand All @@ -67,11 +70,22 @@ var NodekeyCmd = &cobra.Command{
var withSeed bool
switch *inputNodeKeyProtocol {
case "devp2p":
var err error
nko, err = generateDevp2pNodeKey()
if err != nil {
return err
switch *inputNodeKeyType {
case "ed25519":
var err error
nko, err = generateDevp2pNodeKey()
if err != nil {
return err
}
case "secp256k1":
secret := []byte(strings.TrimPrefix(*inputNodeKeyPrivateKey, "0x"))
secp256k1PrivateKey := generateSecp256k1PrivateKey(secret)
if err := displayHeimdallV2PrivValidatorKey(secp256k1PrivateKey); err != nil {
return err
}
return nil
}

case "seed-libp2p":
withSeed = true
fallthrough
Expand Down Expand Up @@ -100,14 +114,15 @@ var NodekeyCmd = &cobra.Command{
if len(args) != 0 {
return fmt.Errorf("this command expects no arguments")
}

validProtocols := []string{"devp2p", "libp2p", "seed-libp2p"}
ok := slices.Contains(validProtocols, *inputNodeKeyProtocol)
if !ok {
return fmt.Errorf("the protocol %s is not implemented", *inputNodeKeyProtocol)
}

if *inputNodeKeyProtocol == "devp2p" {
invalidFlags := []string{"key-type", "seed", "marshal-protobuf"}
invalidFlags := []string{"seed", "marshal-protobuf"}
err := validateNodeKeyFlags(cmd, invalidFlags)
if err != nil {
return err
Expand Down Expand Up @@ -169,13 +184,26 @@ func keyTypeToInt(keyType string) (int, error) {
}

func generateDevp2pNodeKey() (nodeKeyOut, error) {
nodeKey, err := gethcrypto.GenerateKey()
var nodeKey *ecdsa.PrivateKey
var err error

if *inputNodeKeyFile != "" {
switch {
case *inputNodeKeyPrivateKey != "":
privateKey := strings.TrimPrefix(*inputNodeKeyPrivateKey, "0x")
nodeKey, err = gethcrypto.HexToECDSA(privateKey)
if err != nil {
return nodeKeyOut{}, fmt.Errorf("could not create ECDSA private key from given value: %s: %w", *inputNodeKeyPrivateKey, err)
}
case *inputNodeKeyFile != "":
nodeKey, err = gethcrypto.LoadECDSA(*inputNodeKeyFile)
}
if err != nil {
return nodeKeyOut{}, fmt.Errorf("could not generate key: %w", err)
if err != nil {
return nodeKeyOut{}, fmt.Errorf("could not load ECDSA private key from file %s: %w", *inputNodeKeyFile, err)
}
default:
nodeKey, err = gethcrypto.GenerateKey()
if err != nil {
return nodeKeyOut{}, fmt.Errorf("could not generate ECDSA private key: %w", err)
}
}

nko := nodeKeyOut{}
Expand Down Expand Up @@ -250,6 +278,10 @@ func generateLibp2pNodeKey(keyType int, seed bool) (nodeKeyOut, error) {
}

func init() {
inputNodeKeyPrivateKey = NodekeyCmd.PersistentFlags().String("private-key", "", "Use the provided private key (in hex format)")
inputNodeKeyFile = NodekeyCmd.PersistentFlags().StringP("file", "f", "", "A file with the private nodekey (in hex format)")
NodekeyCmd.MarkFlagsMutuallyExclusive("private-key", "file")

inputNodeKeyProtocol = NodekeyCmd.PersistentFlags().String("protocol", "devp2p", "devp2p|libp2p|pex|seed-libp2p")
inputNodeKeyType = NodekeyCmd.PersistentFlags().String("key-type", "ed25519", "ed25519|secp256k1|ecdsa|rsa")
inputNodeKeyIP = NodekeyCmd.PersistentFlags().StringP("ip", "i", "0.0.0.0", "The IP to be associated with this address")
Expand All @@ -258,6 +290,4 @@ func init() {
inputNodeKeySign = NodekeyCmd.PersistentFlags().BoolP("sign", "s", false, "Should the node record be signed?")
inputNodeKeySeed = NodekeyCmd.PersistentFlags().Uint64P("seed", "S", 271828, "A numeric seed value")
inputNodeKeyMarshalProtobuf = NodekeyCmd.PersistentFlags().BoolP("marshal-protobuf", "m", false, "If true the libp2p key will be marshaled to protobuf format rather than raw")

inputNodeKeyFile = NodekeyCmd.PersistentFlags().StringP("file", "f", "", "A file with the private nodekey in hex format")
}
167 changes: 167 additions & 0 deletions cmd/nodekey/secp256k1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package nodekey

import (
"bytes"
"crypto/sha256"
"crypto/subtle"
"fmt"
"math/big"

secp256k1 "github.com/btcsuite/btcd/btcec/v2"
"github.com/cometbft/cometbft/crypto"
"github.com/cometbft/cometbft/privval"

cmtjson "github.com/cometbft/cometbft/libs/json"
gethcrypto "github.com/ethereum/go-ethereum/crypto"
)

const (
PrivKeyName = "comet/PrivKeySecp256k1Uncompressed"
PubKeyName = "comet/PubKeySecp256k1Uncompressed"

KeyType = "secp256k1"
PrivKeySize = 32
// PubKeySize (uncompressed) is composed of 65 bytes for two field elements (x and y)
// and a prefix byte (0x04) to indicate that it is uncompressed.
PubKeySize = 65
// SigSize is the size of the ECDSA signature.
SigSize = 65
)

var _ crypto.PrivKey = PrivKey{}
var _ crypto.PubKey = PubKey{}

// -------------------------------------
// PrivKey type
// -------------------------------------

type PrivKey []byte

// Bytes marshals the private key using amino encoding.
func (privKey PrivKey) Bytes() []byte {
return []byte(privKey)
}

// PubKey performs the point-scalar multiplication from the privKey on the
// generator point to get the pubkey.
func (privKey PrivKey) PubKey() crypto.PubKey {
privateObject, err := gethcrypto.ToECDSA(privKey)
if err != nil {
panic(err)
}

pk := gethcrypto.FromECDSAPub(&privateObject.PublicKey)

return PubKey(pk)
}

// Equals - you probably don't need to use this.
// Runs in constant time based on length of the keys.
func (privKey PrivKey) Equals(other crypto.PrivKey) bool {
if otherSecp, ok := other.(PrivKey); ok {
return subtle.ConstantTimeCompare(privKey[:], otherSecp[:]) == 1
}
return false
}

func (privKey PrivKey) Type() string {
return KeyType
}

// Sign creates an ECDSA signature on curve Secp256k1, using SHA256 on the msg.
// The returned signature will be of the form R || S || V (in lower-S form).
func (privKey PrivKey) Sign(msg []byte) ([]byte, error) {
privateObject, err := gethcrypto.ToECDSA(privKey)
if err != nil {
return nil, err
}

return gethcrypto.Sign(gethcrypto.Keccak256(msg), privateObject)
}

// -------------------------------------
// PubKey type
// -------------------------------------

type PubKey []byte

// Bytes returns the pubkey marshaled with amino encoding.
func (pubKey PubKey) Bytes() []byte {
return []byte(pubKey)
}

// Address returns a Ethereym style addresses: Last_20_Bytes(KECCAK256(pubkey))
func (pubKey PubKey) Address() crypto.Address {
if len(pubKey) != PubKeySize {
panic(fmt.Sprintf("length of pubkey is incorrect %d != %d", len(pubKey), PubKeySize))
}
return gethcrypto.Keccak256(pubKey[1:])[12:]
}

func (pubKey PubKey) Equals(other crypto.PubKey) bool {
if otherSecp, ok := other.(PubKey); ok {
return bytes.Equal(pubKey[:], otherSecp[:])
}
return false
}

func (pubKey PubKey) Type() string {
return KeyType
}

// VerifySignature verifies a signature of the form R || S || V.
// It rejects signatures which are not in lower-S form.
func (pubKey PubKey) VerifySignature(msg []byte, sigStr []byte) bool {
if len(sigStr) != SigSize {

return false
}

hash := gethcrypto.Keccak256(msg)
return gethcrypto.VerifySignature(pubKey, hash, sigStr[:64])
}

func init() {
cmtjson.RegisterType(PubKey{}, PubKeyName)
cmtjson.RegisterType(PrivKey{}, PrivKeyName)
}

// Generate an secp256k1 private key from a secret.
// Most of the logic has been copy/pasted from 0xPolygon/cometbft's fork.
// https://github.com/0xPolygon/cometbft/blob/v0.1.2-beta-polygon/crypto/secp256k1/secp256k1.go
// Notes:
// - 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.
// - This logic will need to be updated to support newer versions.
func generateSecp256k1PrivateKey(secret []byte) PrivKey {
// 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
// https://apps.nsa.gov/iaarchive/library/ia-guidance/ia-solutions-for-classified/algorithm-guidance/suite-b-implementers-guide-to-fips-186-3-ecdsa.cfm
// See also https://github.com/golang/go/blob/0380c9ad38843d523d9c9804fe300cb7edd7cd3c/src/crypto/ecdsa/ecdsa.go#L89-L101
secretHash := sha256.Sum256(secret)
fe := new(big.Int).SetBytes(secretHash[:])

one := new(big.Int).SetInt64(1)
n := new(big.Int).Sub(secp256k1.S256().N, one)
fe.Mod(fe, n)
fe.Add(fe, one)

feB := fe.Bytes()
privKey32 := make([]byte, PrivKeySize)
// Copy feB over to fixed 32 byte privKey32 and pad (if necessary).
copy(privKey32[32-len(feB):32], feB)

return PrivKey(privKey32)
}

func displayHeimdallV2PrivValidatorKey(privKey crypto.PrivKey) error {
nodeKey := privval.FilePVKey{
Address: privKey.PubKey().Address(),
PubKey: privKey.PubKey(),
PrivKey: privKey,
}
jsonBytes, err := cmtjson.MarshalIndent(nodeKey, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonBytes))
return nil
}
34 changes: 34 additions & 0 deletions cmd/nodekey/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,37 @@ $ polycli nodekey --protocol libp2p
# Generate a networking keypair for edge.
$ polycli nodekey --protocol libp2p --key-type secp256k1 --marshal-protobuf
```

Generate an [ED25519](https://en.wikipedia.org/wiki/Curve25519) nodekey from a private key (in hex format).

```bash
polycli nodekey --private-key 2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2 | jq
```

```json
{
"PublicKey": "93e8717f46b146ebfb99159eb13a5d044c191998656c8b79007b16051bb1ff762d09884e43783d898dd47f6220af040206cabbd45c9a26bb278a522c3d538a1f",
"PrivateKey": "2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2",
"ENR": "enode://93e8717f46b146ebfb99159eb13a5d044c191998656c8b79007b16051bb1ff762d09884e43783d898dd47f6220af040206cabbd45c9a26bb278a522c3d538a1f@0.0.0.0:30303?discport=0"
}
```

Generate an [Secp256k1](https://en.bitcoin.it/wiki/Secp256k1) nodekey from a private key (in hex format).

```bash
polycli nodekey --private-key 2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2 --key-type secp256k1 | jq
```

```json
{
"address": "99AA9FC116C1E5E741E9EC18BD1FD232130A5C44",
"pub_key": {
"type": "comet/PubKeySecp256k1Uncompressed",
"value": "BBNYN0nMJsgo0Fp3kVW85PRGBNe7Gdz1XBFuTWQ7D8FnKRb2JYO3i3FK2UiA5+gTSxYu1K66KdYjQYP1mOkH09g="
},
"priv_key": {
"type": "comet/PrivKeySecp256k1Uncompressed",
"value": "OP72E0D7GEi/4VySpolVudLW7uPJm+6PWEtFKJmvp1M="
}
}
```
55 changes: 45 additions & 10 deletions doc/polycli_nodekey.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,54 @@ $ polycli nodekey --protocol libp2p
$ polycli nodekey --protocol libp2p --key-type secp256k1 --marshal-protobuf
```

Generate an [ED25519](https://en.wikipedia.org/wiki/Curve25519) nodekey from a private key (in hex format).

```bash
polycli nodekey --private-key 2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2 | jq
```

```json
{
"PublicKey": "93e8717f46b146ebfb99159eb13a5d044c191998656c8b79007b16051bb1ff762d09884e43783d898dd47f6220af040206cabbd45c9a26bb278a522c3d538a1f",
"PrivateKey": "2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2",
"ENR": "enode://93e8717f46b146ebfb99159eb13a5d044c191998656c8b79007b16051bb1ff762d09884e43783d898dd47f6220af040206cabbd45c9a26bb278a522c3d538a1f@0.0.0.0:30303?discport=0"
}
```

Generate an [Secp256k1](https://en.bitcoin.it/wiki/Secp256k1) nodekey from a private key (in hex format).

```bash
polycli nodekey --private-key 2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2 --key-type secp256k1 | jq
```

```json
{
"address": "99AA9FC116C1E5E741E9EC18BD1FD232130A5C44",
"pub_key": {
"type": "comet/PubKeySecp256k1Uncompressed",
"value": "BBNYN0nMJsgo0Fp3kVW85PRGBNe7Gdz1XBFuTWQ7D8FnKRb2JYO3i3FK2UiA5+gTSxYu1K66KdYjQYP1mOkH09g="
},
"priv_key": {
"type": "comet/PrivKeySecp256k1Uncompressed",
"value": "OP72E0D7GEi/4VySpolVudLW7uPJm+6PWEtFKJmvp1M="
}
}
```

## Flags

```bash
-f, --file string A file with the private nodekey in hex format
-h, --help help for nodekey
-i, --ip string The IP to be associated with this address (default "0.0.0.0")
--key-type string ed25519|secp256k1|ecdsa|rsa (default "ed25519")
-m, --marshal-protobuf If true the libp2p key will be marshaled to protobuf format rather than raw
--protocol string devp2p|libp2p|pex|seed-libp2p (default "devp2p")
-S, --seed uint A numeric seed value (default 271828)
-s, --sign Should the node record be signed?
-t, --tcp int The tcp Port to be associated with this address (default 30303)
-u, --udp int The udp Port to be associated with this address
-f, --file string A file with the private nodekey (in hex format)
-h, --help help for nodekey
-i, --ip string The IP to be associated with this address (default "0.0.0.0")
--key-type string ed25519|secp256k1|ecdsa|rsa (default "ed25519")
-m, --marshal-protobuf If true the libp2p key will be marshaled to protobuf format rather than raw
--private-key string Use the provided private key (in hex format)
--protocol string devp2p|libp2p|pex|seed-libp2p (default "devp2p")
-S, --seed uint A numeric seed value (default 271828)
-s, --sign Should the node record be signed?
-t, --tcp int The tcp Port to be associated with this address (default 30303)
-u, --udp int The udp Port to be associated with this address
```

The command also inherits flags from parent commands.
Expand Down
Loading

0 comments on commit df38375

Please sign in to comment.