From 6b3e8ad4b3523fe3c180d2cbe22160b8074b77ee Mon Sep 17 00:00:00 2001 From: Trung Nguyen <24930+trung@users.noreply.github.com> Date: Fri, 18 Feb 2022 09:04:58 -0500 Subject: [PATCH] command/crypto/key: add secp256k support for signing and verification --- command/crypto/key/sign.go | 60 ++++++++++++--- command/crypto/key/sign_test.go | 127 ++++++++++++++++++++++++++++++++ command/crypto/key/verify.go | 8 +- go.mod | 1 + go.sum | 3 + 5 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 command/crypto/key/sign_test.go diff --git a/command/crypto/key/sign.go b/command/crypto/key/sign.go index 9aa216d59..047d80eec 100644 --- a/command/crypto/key/sign.go +++ b/command/crypto/key/sign.go @@ -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" @@ -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 `, } @@ -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", @@ -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 @@ -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 } @@ -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) } @@ -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 { @@ -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 + } + return pemutil.Read(keyFile) +} diff --git a/command/crypto/key/sign_test.go b/command/crypto/key/sign_test.go new file mode 100644 index 000000000..ef7b4bb45 --- /dev/null +++ b/command/crypto/key/sign_test.go @@ -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 +} diff --git a/command/crypto/key/verify.go b/command/crypto/key/verify.go index 1de135282..9a97c6d89 100644 --- a/command/crypto/key/verify.go +++ b/command/crypto/key/verify.go @@ -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 { @@ -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 { @@ -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) diff --git a/go.mod b/go.mod index 25bed1bda..e87e20395 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 3ee50054e..68c2e7f3f 100644 --- a/go.sum +++ b/go.sum @@ -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=