Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add es256k (secp256k1 curve) support #643

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions command/crypto/key/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import (
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"os"
"strings"

"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/pkg/errors"
"github.com/smallstep/cli/command"
"github.com/smallstep/cli/utils"
Expand Down Expand Up @@ -50,6 +53,9 @@ var hashAlgFlag = cli.StringFlag{

**md5**
: MD5 produces a 128-bit hash value

**es256k**
: ECDSA with the secp256k1 curve and the SHA-256 cryptographic hash function
`,
}

Expand Down Expand Up @@ -108,9 +114,10 @@ $ step crypto key sign --key rsa.key --pss file.txt
Name: "pss",
Usage: "Use RSA-PSS signature scheme.",
},
cli.BoolFlag{
Name: "raw",
Usage: "Print the raw bytes instead of the base64 format.",
cli.StringFlag{
Name: "format",
Value: "hex",
Usage: "Format the output: hex/b64/raw. Default is hex",
},
cli.StringFlag{
Name: "password-file",
Expand All @@ -120,6 +127,9 @@ $ step crypto key sign --key rsa.key --pss file.txt
}
}

// make it easy to unit-test
var output io.Writer = os.Stdout

func signAction(ctx *cli.Context) error {
if err := errs.MinMaxNumberOfArguments(ctx, 0, 1); err != nil {
return err
Expand All @@ -145,7 +155,7 @@ func signAction(ctx *cli.Context) error {
return errs.FileError(err, input)
}

key, err := pemutil.Read(keyFile)
key, err := readKey(keyFile, false, ctx)
if err != nil {
return err
}
Expand All @@ -169,6 +179,8 @@ func signAction(ctx *cli.Context) error {
digest = hash(crypto.SHA384, b)
case elliptic.P521():
digest = hash(crypto.SHA512, b)
case secp256k1.S256(): // using SHA-256
digest = hash(crypto.SHA256, b)
default:
return errors.Errorf("unsupported elliptic curve %s", k.Params().Name)
}
Expand All @@ -190,13 +202,20 @@ func signAction(ctx *cli.Context) error {
return errors.Wrap(err, "error signing message")
}

if ctx.Bool("raw") {
os.Stdout.Write(sig)
} else {
fmt.Println(base64.StdEncoding.EncodeToString(sig))
var outputValue interface{}
switch v := ctx.String("format"); v {
case "raw":
outputValue = sig
case "hex":
outputValue = hex.EncodeToString(sig)
case "b64":
outputValue = base64.StdEncoding.EncodeToString(sig)
default:
return errors.Errorf("unsupported output format %T", v)
}
_, err = fmt.Fprintln(output, outputValue)

return nil
return err
}

func hash(h crypto.Hash, data []byte) []byte {
Expand Down Expand Up @@ -237,3 +256,26 @@ func rsaHash(ctx *cli.Context) (crypto.SignerOpts, error) {

return h, nil
}

func readKey(keyFile string, isPubKey bool, ctx *cli.Context) (interface{}, error) {
if strings.ToLower(ctx.String("alg")) == "es256k" {
hexRaw, err := os.ReadFile(keyFile)
if err != nil {
return nil, errors.Wrap(err, "read file error")
}
raw, err := hex.DecodeString(strings.TrimPrefix(strings.TrimSpace(string(hexRaw)), "0x"))
if err != nil {
return nil, errors.Wrap(err, "file content is not in hex")
}
if isPubKey {
secp256k1Pk, err := secp256k1.ParsePubKey(raw)
if err != nil {
return nil, errors.Wrap(err, "unable to parse public key")
}
return secp256k1Pk.ToECDSA(), nil
}
secp256k1Pk := secp256k1.PrivKeyFromBytes(raw)
return secp256k1Pk.ToECDSA(), nil
}
Comment on lines +261 to +279
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to not use a PEM file for this?

$ openssl ecparam -name secp256k1 -genkey -noout
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIMUALqrFTe1KusIe3WlCZeRHBZoNoL1SbqzVDHdHo+7roAcGBSuBBAAK
oUQDQgAEkQ/Pj3MnBvwmNmsYEg0cCqgsXwV8yKYJHG099jfLPjTdmV3ZWkZg146Q
Nfm0RBXjvgEoVXhHy/g2vyptMmAaKQ==
-----END EC PRIVATE KEY-----

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there's no reason. We can certainly use PEM here.

Maybe we can add support for hex-encoded key and wallet (JSON, imported format). The support can be done directly in related commands or in the format command.

Blockchain client node implementations often import keys via those above methods.

return pemutil.Read(keyFile)
}
127 changes: 127 additions & 0 deletions command/crypto/key/sign_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package key

import (
"bytes"
"crypto"
"crypto/ecdsa"
"encoding/hex"
"flag"
"os"
"strings"
"testing"

"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli"
)

func TestReadKey_PrivateKey(t *testing.T) {
arbitraryPrivateKey := "0x157c3200d896c0595a205109c5b5656e82621e8214a12a9561727870f5867962"
f, err := prepareFile(arbitraryPrivateKey)
if err != nil {
t.Fatalf("%v", err)
}
defer func() {
_ = os.Remove(f.Name())
}()
flags := &flag.FlagSet{}
flags.String("alg", "secp256k1", "")
ctx := cli.NewContext(nil, flags, nil)

pk, err := readKey(f.Name(), false, ctx)
if err != nil {
t.Errorf("%v", err)
}

assert.IsType(t, &ecdsa.PrivateKey{}, pk)
}

func TestReadKey_PublicKey(t *testing.T) {
arbitraryPubKey := "02d1e996bf09686ca22e5303e7d3abda4ccbbcdee94f5eb3adf6cad7238f27f840"
f, err := prepareFile(arbitraryPubKey)
if err != nil {
t.Fatalf("%v", err)
}
defer func() {
_ = os.Remove(f.Name())
}()
flags := &flag.FlagSet{}
flags.String("alg", "secp256k1", "")
ctx := cli.NewContext(nil, flags, nil)

pk, err := readKey(f.Name(), true, ctx)
if err != nil {
t.Errorf("%v", err)
}

assert.IsType(t, &ecdsa.PublicKey{}, pk)
}

func TestSign(t *testing.T) {
var capturedOutput bytes.Buffer
output = &capturedOutput
defer func() {
output = os.Stdout
}()
arbitraryKeyPair := struct {
priv string
pub string
}{
priv: "0xb2cf8112327c38acc3b16b2cea56c684aa94580caae76e29dcb244f19bec88e2",
pub: "0224e7f25110dabeb26e1f94760dc9abe15fd35d5cd2f60ce99d5fe3f35b552fcc",
}
arbitraryData := "test data"
pkFile, err := prepareFile(arbitraryKeyPair.priv)
if err != nil {
t.Fatalf("%v", err)
}
defer func() {
_ = os.Remove(pkFile.Name())
}()
dataFile, err := prepareFile(arbitraryData)
if err != nil {
t.Fatalf("%v", err)
}
defer func() {
_ = os.Remove(dataFile.Name())
}()
flags := &flag.FlagSet{}
flags.String("alg", "secp256k1", "")
flags.String("key", pkFile.Name(), "")
flags.String("format", "hex", "")
_ = flags.Parse([]string{dataFile.Name()})
ctx := cli.NewContext(nil, flags, nil)

assert.NoError(t, signAction(ctx))

actual := strings.TrimSpace(capturedOutput.String())
sig, err := hex.DecodeString(actual)
if err != nil {
t.Fatalf("%v", err)
}
// now verify it
pubKey, err := hex.DecodeString(arbitraryKeyPair.pub)
if err != nil {
t.Fatalf("%v", err)
}
secpPubKey, err := secp256k1.ParsePubKey(pubKey)
if err != nil {
t.Fatalf("%v", err)
}
assert.True(t, ecdsa.VerifyASN1(secpPubKey.ToECDSA(), hash(crypto.SHA256, []byte(arbitraryData)), sig))
}

// remember to delete file after use
func prepareFile(s string) (*os.File, error) {
f, err := os.CreateTemp("", "test-")
if err != nil {
return nil, err
}
if _, err := f.WriteString(s); err != nil {
return nil, err
}
defer func() {
_ = f.Close()
}()
return f, nil
}
8 changes: 4 additions & 4 deletions command/crypto/key/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
"encoding/base64"
"fmt"

"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/pkg/errors"
"github.com/smallstep/cli/command"
"github.com/smallstep/cli/utils"
"github.com/urfave/cli"
"go.step.sm/cli-utils/errs"
"go.step.sm/crypto/pemutil"
)

func verifyCommand() cli.Command {
Expand Down Expand Up @@ -108,9 +108,9 @@ func verifyAction(ctx *cli.Context) error {
return errors.Wrap(err, "error decoding base64 signature")
}

key, err := pemutil.Read(keyFile)
key, err := readKey(keyFile, true, ctx)
if err != nil {
return err
return errors.Wrap(err, "unable to read key file")
}

printAndReturn := func(b bool) error {
Expand All @@ -127,7 +127,7 @@ func verifyAction(ctx *cli.Context) error {
switch k.Curve {
case elliptic.P224():
digest = hash(crypto.SHA224, b)
case elliptic.P256():
case elliptic.P256(), secp256k1.S256():
digest = hash(crypto.SHA256, b)
case elliptic.P384():
digest = hash(crypto.SHA384, b)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944
github.com/boombuler/barcode v1.0.1 // indirect
github.com/corpix/uarand v0.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/google/uuid v1.3.0
github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
Expand Down