From 3ac30995c09a44deece80130387a346f5348b2c9 Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Mon, 26 Feb 2024 22:41:27 -0800 Subject: [PATCH 01/10] feat: signing pkg for programmatic access --- pkg/signing/signing.go | 96 +++++++++++++++++++++++++++++ pkg/signing/signing_test.go | 119 ++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 pkg/signing/signing.go create mode 100644 pkg/signing/signing_test.go diff --git a/pkg/signing/signing.go b/pkg/signing/signing.go new file mode 100644 index 0000000..827edb1 --- /dev/null +++ b/pkg/signing/signing.go @@ -0,0 +1,96 @@ +package signing + +import ( + "bufio" + "crypto/ecdsa" + "encoding/hex" + "fmt" + "io" + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "golang.org/x/crypto/sha3" +) + +// Signer allows you to sign a big stream of bytes by calling Sum multiple times, then Sign. +type Signer struct { + state crypto.KeccakState + privateKey *ecdsa.PrivateKey +} + +// LoadPrivateKey creates an ecdsa.PrivateKey from a hex-encoded string. +func LoadPrivateKey(hexKey string) (*ecdsa.PrivateKey, error) { + return crypto.HexToECDSA(hexKey) +} + +// NewSigner creates a new signer with a private key and internal state. +func NewSigner(pk *ecdsa.PrivateKey) *Signer { + return &Signer{ + state: sha3.NewLegacyKeccak256().(crypto.KeccakState), + privateKey: pk, + } +} + +// Sum updates the hash state with a new chunk. +func (s *Signer) Sum(chunk []byte) { + s.state.Write(chunk) +} + +// Sign returns the signature of the hash state. +func (s *Signer) signState() ([]byte, error) { + var h common.Hash + _, _ = s.state.Read(h[:]) + signature, err := crypto.Sign(h.Bytes(), s.privateKey) + if err != nil { + return []byte{}, fmt.Errorf("failed to sign state: %s", err) + } + + return signature, nil +} + +// SignFile returns the signature of a signed file. +func (s *Signer) SignFile(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", fmt.Errorf("failed to open file: %s", err.Error()) + } + defer file.Close() + + // Check if the file is empty and return an error if it is + info, err := file.Stat() + if err != nil { + return "", fmt.Errorf("failed to get file info: %s", err.Error()) + } + if info.Size() == 0 { + return "", fmt.Errorf("failed to create signature: %s", "file is empty") + } + + nBytes, nChunks := int64(0), int64(0) + r := bufio.NewReader(file) + buf := make([]byte, 0, 4*1024) // 4KB buffer + for { + n, err := r.Read(buf[:cap(buf)]) + buf = buf[:n] + if n == 0 { + if err == nil { + continue + } + if err == io.EOF { + break + } + return "", fmt.Errorf("read error: %s", err) + } + nChunks++ + nBytes += int64(len(buf)) + + s.Sum(buf) + } + + signature, err := s.signState() + if err != nil { + return "", fmt.Errorf("failed to sign: %s", err) + } + + return hex.EncodeToString(signature), nil +} diff --git a/pkg/signing/signing_test.go b/pkg/signing/signing_test.go new file mode 100644 index 0000000..4c00eca --- /dev/null +++ b/pkg/signing/signing_test.go @@ -0,0 +1,119 @@ +package signing + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSigner(t *testing.T) { + privateKey, _ := LoadPrivateKey("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") + signer := NewSigner(privateKey) + + testCases := []struct { + name string + setup func() (filename string, cleanup func()) + wantErr string + expectedSignature string + }{ + { + name: "should sign with content", + setup: func() (filename string, cleanup func()) { + tmpFile, _ := os.CreateTemp("", "test_file") + name := tmpFile.Name() + content := []byte("data to be signed") + tmpFile.Write(content) + tmpFile.Close() + return name, func() { os.Remove(name) } + }, + wantErr: "", + expectedSignature: "6ddb61a19b9df71136b48c80b2e86e7e20313d5eec0de9210802335b300ba8df6c332d35a5d753a028d703769fd9b66d7ce5902d80369750cf55118b1679d84900", + }, + { + name: "should fail with empty file", + setup: func() (filename string, cleanup func()) { + tmpFile, _ := os.CreateTemp("", "test_file") + name := tmpFile.Name() + tmpFile.Close() + return name, func() { os.Remove(name) } + }, + wantErr: "failed to create signature: file is empty", + }, + { + name: "should fail with non-existent file", + setup: func() (filename string, cleanup func()) { + tmpFile, _ := os.CreateTemp("", "test_file") + name := tmpFile.Name() + tmpFile.Close() + os.Remove(name) + return name, func() {} + }, + wantErr: "failed to open file: open ", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + filename, cleanup := tc.setup() + defer cleanup() + + signature, err := signer.SignFile(filename) + if tc.wantErr != "" { + require.Error(t, err, "Expected an error for %v", tc.name) + require.Contains(t, err.Error(), tc.wantErr, "SignFile() error = %v, wantErr %v", err, tc.wantErr) + } else { + require.NoError(t, err, "SignFile() unexpected error = %v", err) + require.Equal(t, tc.expectedSignature, signature, "Signature mismatch") + } + }) + } +} + +func TestPrivateKey(t *testing.T) { + testCases := []struct { + name string + setup func() (pk string) + wantErr string + }{ + { + name: "should load a real private key", + setup: func() (pk string) { + pk = "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + return pk + }, + wantErr: "", + }, + { + name: "should fail to load 0x prefixed key", + setup: func() (pk string) { + pk = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + return pk + }, + wantErr: "invalid hex character 'x' in private key", + }, + { + name: "should fail to load random string", + setup: func() (pk string) { + pk = "1234abcd" + return pk + }, + wantErr: "invalid length, need 256 bits", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + pk := tc.setup() + + hex, err := LoadPrivateKey(pk) + if tc.wantErr != "" { + require.Error(t, err, "Expected an error for %v", tc.name) + require.EqualErrorf(t, err, tc.wantErr, "LoadPrivateKey() error = %v, wantErr %v", err, tc.wantErr) + } else { + require.NoError(t, err, "LoadPrivateKey() unexpected error = %v", err) + require.NotNil(t, hex, "LoadPrivateKey() returned nil") + } + }) + } +} From e1c64e4899ae1d4774844422eb661a00f7c99dfd Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Tue, 27 Feb 2024 18:46:08 -0800 Subject: [PATCH 02/10] refactor: move internal signing logic to signing pkg --- internal/app/uploader.go | 89 +++---------------------------------- pkg/signing/signing.go | 46 ++++++++++++------- pkg/signing/signing_test.go | 3 +- 3 files changed, 36 insertions(+), 102 deletions(-) diff --git a/internal/app/uploader.go b/internal/app/uploader.go index 078a736..ba0bfbe 100644 --- a/internal/app/uploader.go +++ b/internal/app/uploader.go @@ -1,19 +1,14 @@ package app import ( - "bufio" "context" "crypto/ecdsa" - "encoding/hex" "fmt" "io" - "log" "os" "strings" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "golang.org/x/crypto/sha3" + "github.com/tablelandnetwork/basin-cli/pkg/signing" ) // VaultsUploader contains logic of uploading Parquet files to Vaults Provider. @@ -48,8 +43,9 @@ func (bu *VaultsUploader) Upload( _ = f.Close() }() - signer := NewSigner(bu.privateKey) - signature, err := signer.SignFile(filepath) + signer := signing.NewSigner(bu.privateKey) + signatureBytes, err := signer.SignFile(filepath) + signature := signing.SignatureBytesToHex(signatureBytes) if err != nil { return fmt.Errorf("signing the file: %s", err) } @@ -66,7 +62,7 @@ func (bu *VaultsUploader) Upload( Content: f, Filename: filename, ProgressBar: progress, - Signature: hex.EncodeToString(signature), + Signature: signature, Size: sz, } @@ -76,78 +72,3 @@ func (bu *VaultsUploader) Upload( return nil } - -// Signer allows you to sign a big stream of bytes by calling Sum multiple times, then Sign. -type Signer struct { - state crypto.KeccakState - privateKey *ecdsa.PrivateKey -} - -// NewSigner creates a new signer. -func NewSigner(pk *ecdsa.PrivateKey) *Signer { - return &Signer{ - state: sha3.NewLegacyKeccak256().(crypto.KeccakState), - privateKey: pk, - } -} - -// Sum updates the hash state with a new chunk. -func (s *Signer) Sum(chunk []byte) { - s.state.Write(chunk) -} - -// Sign signs the internal state. -func (s *Signer) Sign() ([]byte, error) { - var h common.Hash - _, _ = s.state.Read(h[:]) - signature, err := crypto.Sign(h.Bytes(), s.privateKey) - if err != nil { - return []byte{}, fmt.Errorf("sign: %s", err) - } - - return signature, nil -} - -// SignFile signs an entire file. -func (s *Signer) SignFile(filename string) ([]byte, error) { - f, err := os.Open(filename) - if err != nil { - return []byte{}, fmt.Errorf("error to read [file=%v]: %v", filename, err.Error()) - } - - defer func() { - _ = f.Close() - }() - - nBytes, nChunks := int64(0), int64(0) - r := bufio.NewReader(f) - buf := make([]byte, 0, 4*1024) - for { - n, err := r.Read(buf[:cap(buf)]) - buf = buf[:n] - if n == 0 { - if err == nil { - continue - } - if err == io.EOF { - break - } - log.Fatal(err) - } - nChunks++ - nBytes += int64(len(buf)) - - s.Sum(buf) - - if err != nil && err != io.EOF { - log.Fatal(err) - } - } - - signature, err := s.Sign() - if err != nil { - log.Fatal("failed to sign") - } - - return signature, nil -} diff --git a/pkg/signing/signing.go b/pkg/signing/signing.go index 827edb1..f3583b0 100644 --- a/pkg/signing/signing.go +++ b/pkg/signing/signing.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "io" + "log" "os" "github.com/ethereum/go-ethereum/common" @@ -24,7 +25,7 @@ func LoadPrivateKey(hexKey string) (*ecdsa.PrivateKey, error) { return crypto.HexToECDSA(hexKey) } -// NewSigner creates a new signer with a private key and internal state. +// NewSigner creates a new signer. func NewSigner(pk *ecdsa.PrivateKey) *Signer { return &Signer{ state: sha3.NewLegacyKeccak256().(crypto.KeccakState), @@ -37,37 +38,39 @@ func (s *Signer) Sum(chunk []byte) { s.state.Write(chunk) } -// Sign returns the signature of the hash state. -func (s *Signer) signState() ([]byte, error) { +// Sign signs the internal state. +func (s *Signer) Sign() ([]byte, error) { var h common.Hash _, _ = s.state.Read(h[:]) signature, err := crypto.Sign(h.Bytes(), s.privateKey) if err != nil { - return []byte{}, fmt.Errorf("failed to sign state: %s", err) + return []byte{}, fmt.Errorf("sign: %s", err) } return signature, nil } -// SignFile returns the signature of a signed file. -func (s *Signer) SignFile(filePath string) (string, error) { - file, err := os.Open(filePath) +// SignFile signs an entire file, returning the signature as a byte slice. +func (s *Signer) SignFile(filename string) ([]byte, error) { + f, err := os.Open(filename) if err != nil { - return "", fmt.Errorf("failed to open file: %s", err.Error()) + return []byte{}, fmt.Errorf("error to read [file=%v]: %v", filename, err.Error()) } - defer file.Close() + defer func() { + _ = f.Close() + }() // Check if the file is empty and return an error if it is - info, err := file.Stat() + info, err := f.Stat() if err != nil { - return "", fmt.Errorf("failed to get file info: %s", err.Error()) + return []byte{}, fmt.Errorf("failed to get file info: %s", err.Error()) } if info.Size() == 0 { - return "", fmt.Errorf("failed to create signature: %s", "file is empty") + return []byte{}, fmt.Errorf("failed to create signature: %s", "file is empty") } nBytes, nChunks := int64(0), int64(0) - r := bufio.NewReader(file) + r := bufio.NewReader(f) buf := make([]byte, 0, 4*1024) // 4KB buffer for { n, err := r.Read(buf[:cap(buf)]) @@ -79,18 +82,27 @@ func (s *Signer) SignFile(filePath string) (string, error) { if err == io.EOF { break } - return "", fmt.Errorf("read error: %s", err) + log.Fatal(err) } nChunks++ nBytes += int64(len(buf)) s.Sum(buf) + + if err != nil && err != io.EOF { + log.Fatal(err) + } } - signature, err := s.signState() + signature, err := s.Sign() if err != nil { - return "", fmt.Errorf("failed to sign: %s", err) + log.Fatal("failed to sign") } - return hex.EncodeToString(signature), nil + return signature, nil +} + +// signatureBytesToHex converts a byte slice to a hex-encoded string. +func SignatureBytesToHex(b []byte) string { + return hex.EncodeToString(b) } diff --git a/pkg/signing/signing_test.go b/pkg/signing/signing_test.go index 4c00eca..a92341e 100644 --- a/pkg/signing/signing_test.go +++ b/pkg/signing/signing_test.go @@ -58,7 +58,8 @@ func TestSigner(t *testing.T) { filename, cleanup := tc.setup() defer cleanup() - signature, err := signer.SignFile(filename) + signatureBytes, err := signer.SignFile(filename) + signature := SignatureBytesToHex(signatureBytes) if tc.wantErr != "" { require.Error(t, err, "Expected an error for %v", tc.name) require.Contains(t, err.Error(), tc.wantErr, "SignFile() error = %v, wantErr %v", err, tc.wantErr) From 6c5d22e1c9edae4631a8992000831f212cedbf44 Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Tue, 27 Feb 2024 18:49:27 -0800 Subject: [PATCH 03/10] docs: fix stream typo --- cmd/vaults/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/vaults/commands.go b/cmd/vaults/commands.go index be8f5c0..ae90209 100644 --- a/cmd/vaults/commands.go +++ b/cmd/vaults/commands.go @@ -162,7 +162,7 @@ func newStreamCommand() *cli.Command { ArgsUsage: "", Description: "The daemon will continuously stream database changes (except deletions) \n" + "to the vault, as long as the daemon is actively running.\n\n" + - "EXAMPLE:\n\nvaults stream --vault my.vault --private-key 0x1234abcd", + "EXAMPLE:\n\nvaults stream --private-key 0x1234abcd my.vault", Flags: []cli.Flag{ &cli.StringFlag{ From 7ab08db762c48837cae27378cf04da9874370e7f Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Tue, 27 Feb 2024 20:21:55 -0800 Subject: [PATCH 04/10] feat: add 'sign' command | refactor signing pkg errors/names --- cmd/vaults/commands.go | 45 +++++++++++++++++++++++++++++++++++++ cmd/vaults/main.go | 1 + pkg/signing/signing.go | 15 ++++++------- pkg/signing/signing_test.go | 14 ++++++------ 4 files changed, 60 insertions(+), 15 deletions(-) diff --git a/cmd/vaults/commands.go b/cmd/vaults/commands.go index ae90209..67885a7 100644 --- a/cmd/vaults/commands.go +++ b/cmd/vaults/commands.go @@ -22,6 +22,7 @@ import ( "github.com/schollz/progressbar/v3" "github.com/tablelandnetwork/basin-cli/internal/app" "github.com/tablelandnetwork/basin-cli/pkg/pgrepl" + "github.com/tablelandnetwork/basin-cli/pkg/signing" "github.com/tablelandnetwork/basin-cli/pkg/vaultsprovider" "github.com/urfave/cli/v2" "gopkg.in/yaml.v3" @@ -588,6 +589,50 @@ func newListEventsCommand() *cli.Command { } } +func newSignCommand() *cli.Command { + var privateKey string + + return &cli.Command{ + Name: "sign", + Usage: "Sign a file with a private key", + ArgsUsage: "", + Description: "Signing a file with take a provide key and a path to the desired file\n" + + "to produce a hex encoded string (e.g., can be used in the HTTP API).\n\n" + + "EXAMPLE:\n\nvaults sign --private-key 0x1234abcd /path/to/file", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "private-key", + Aliases: []string{"k"}, + Category: "REQUIRED:", + Usage: "Ethereum wallet private key", + Destination: &privateKey, + Required: true, + }, + }, + Action: func(cCtx *cli.Context) error { + if cCtx.NArg() != 1 { + return errors.New("must provide a file path") + } + filepath := cCtx.Args().First() + + privateKey, err := crypto.HexToECDSA(privateKey) + if err != nil { + return err + } + + signer := signing.NewSigner(privateKey) + signatureBytes, err := signer.SignFile(filepath) + if err != nil { + return fmt.Errorf("failed to sign file: %s", err) + } + signature := signing.SignatureBytesToHex(signatureBytes) + fmt.Println(signature) + + return nil + }, + } +} + func newRetrieveCommand() *cli.Command { var output, provider string var timeout int64 diff --git a/cmd/vaults/main.go b/cmd/vaults/main.go index e4dc078..e94640e 100644 --- a/cmd/vaults/main.go +++ b/cmd/vaults/main.go @@ -36,6 +36,7 @@ func main() { newWriteCommand(), newListCommand(), newListEventsCommand(), + newSignCommand(), newRetrieveCommand(), newWalletCommand(), }, diff --git a/pkg/signing/signing.go b/pkg/signing/signing.go index f3583b0..98c909d 100644 --- a/pkg/signing/signing.go +++ b/pkg/signing/signing.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "fmt" "io" - "log" "os" "github.com/ethereum/go-ethereum/common" @@ -20,8 +19,8 @@ type Signer struct { privateKey *ecdsa.PrivateKey } -// LoadPrivateKey creates an ecdsa.PrivateKey from a hex-encoded string. -func LoadPrivateKey(hexKey string) (*ecdsa.PrivateKey, error) { +// HexToECDSA parses a hex encoded private key to an ECDSA private key. +func HexToECDSA(hexKey string) (*ecdsa.PrivateKey, error) { return crypto.HexToECDSA(hexKey) } @@ -54,7 +53,7 @@ func (s *Signer) Sign() ([]byte, error) { func (s *Signer) SignFile(filename string) ([]byte, error) { f, err := os.Open(filename) if err != nil { - return []byte{}, fmt.Errorf("error to read [file=%v]: %v", filename, err.Error()) + return []byte{}, fmt.Errorf("error reading [file=%v]: %v", filename, err.Error()) } defer func() { _ = f.Close() @@ -66,7 +65,7 @@ func (s *Signer) SignFile(filename string) ([]byte, error) { return []byte{}, fmt.Errorf("failed to get file info: %s", err.Error()) } if info.Size() == 0 { - return []byte{}, fmt.Errorf("failed to create signature: %s", "file is empty") + return []byte{}, fmt.Errorf("error with file: %s", "content is empty") } nBytes, nChunks := int64(0), int64(0) @@ -82,7 +81,7 @@ func (s *Signer) SignFile(filename string) ([]byte, error) { if err == io.EOF { break } - log.Fatal(err) + return []byte{}, fmt.Errorf("unexpected error reading file: %s", err.Error()) } nChunks++ nBytes += int64(len(buf)) @@ -90,13 +89,13 @@ func (s *Signer) SignFile(filename string) ([]byte, error) { s.Sum(buf) if err != nil && err != io.EOF { - log.Fatal(err) + return []byte{}, fmt.Errorf("error in buffer: %s", err.Error()) } } signature, err := s.Sign() if err != nil { - log.Fatal("failed to sign") + return []byte{}, fmt.Errorf("failed to sign [file=%v]: %s", filename, err.Error()) } return signature, nil diff --git a/pkg/signing/signing_test.go b/pkg/signing/signing_test.go index a92341e..34617a1 100644 --- a/pkg/signing/signing_test.go +++ b/pkg/signing/signing_test.go @@ -8,7 +8,7 @@ import ( ) func TestSigner(t *testing.T) { - privateKey, _ := LoadPrivateKey("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") + privateKey, _ := HexToECDSA("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") signer := NewSigner(privateKey) testCases := []struct { @@ -38,7 +38,7 @@ func TestSigner(t *testing.T) { tmpFile.Close() return name, func() { os.Remove(name) } }, - wantErr: "failed to create signature: file is empty", + wantErr: "error with file: content is empty", }, { name: "should fail with non-existent file", @@ -49,7 +49,7 @@ func TestSigner(t *testing.T) { os.Remove(name) return name, func() {} }, - wantErr: "failed to open file: open ", + wantErr: "error reading [file=", }, } @@ -107,13 +107,13 @@ func TestPrivateKey(t *testing.T) { t.Run(tc.name, func(t *testing.T) { pk := tc.setup() - hex, err := LoadPrivateKey(pk) + hex, err := HexToECDSA(pk) if tc.wantErr != "" { require.Error(t, err, "Expected an error for %v", tc.name) - require.EqualErrorf(t, err, tc.wantErr, "LoadPrivateKey() error = %v, wantErr %v", err, tc.wantErr) + require.EqualErrorf(t, err, tc.wantErr, "HexToECDSA() error = %v, wantErr %v", err, tc.wantErr) } else { - require.NoError(t, err, "LoadPrivateKey() unexpected error = %v", err) - require.NotNil(t, hex, "LoadPrivateKey() returned nil") + require.NoError(t, err, "HexToECDSA() unexpected error = %v", err) + require.NotNil(t, hex, "HexToECDSA() returned nil") } }) } From 84f8c8a1c84ccce301cc8030bd91dc46b242030f Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Tue, 27 Feb 2024 20:27:59 -0800 Subject: [PATCH 05/10] feat: get pk address from hex string or file path --- cmd/vaults/commands.go | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/cmd/vaults/commands.go b/cmd/vaults/commands.go index 67885a7..299dc15 100644 --- a/cmd/vaults/commands.go +++ b/cmd/vaults/commands.go @@ -732,19 +732,28 @@ func newWalletCommand() *cli.Command { { Name: "address", Usage: "Print the public key for an account's private key", - UsageText: "vaults account address ", - Description: "The result of the `vaults account create` command will write a private key to a file, \n" + - "and this lets you retrieve the public key value for use in other commands.\n\n" + - "EXAMPLE:\n\nvaults account address /path/to/file", + UsageText: "vaults account address ", + Description: "The result of the `vaults account create` command will write a private key to a file, and \n" + + "this lets you retrieve the public key value for the file, or a private key hex string.\n\n" + + "EXAMPLES:\n\nvaults account address /path/to/file\nvaults account address abcd1234", Action: func(cCtx *cli.Context) error { - filename := cCtx.Args().Get(0) - if filename == "" { - return errors.New("filename is empty") + arg := cCtx.Args().Get(0) + if arg == "" { + return errors.New("no argument provided") } - privateKey, err := crypto.LoadECDSA(filename) - if err != nil { - return fmt.Errorf("loading key: %s", err) + var privateKey *ecdsa.PrivateKey + // Try loading from file path first; else, try hex string + if _, err := os.Stat(arg); err == nil { + privateKey, err = crypto.LoadECDSA(arg) + if err != nil { + return fmt.Errorf("loading key from file: %s", err) + } + } else { + privateKey, err = crypto.HexToECDSA(arg) + if err != nil { + return fmt.Errorf("loading key from hex string: %s", err) + } } pubk, _ := privateKey.Public().(*ecdsa.PublicKey) From 7bc2673876a9b08a7c0882472502f44617864bc2 Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Tue, 27 Feb 2024 21:41:08 -0800 Subject: [PATCH 06/10] feat: add 'SignBytes' for signing bytes --- pkg/signing/signing.go | 18 +++++++++++++++- pkg/signing/signing_test.go | 43 +++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/pkg/signing/signing.go b/pkg/signing/signing.go index 98c909d..e26d15c 100644 --- a/pkg/signing/signing.go +++ b/pkg/signing/signing.go @@ -65,7 +65,7 @@ func (s *Signer) SignFile(filename string) ([]byte, error) { return []byte{}, fmt.Errorf("failed to get file info: %s", err.Error()) } if info.Size() == 0 { - return []byte{}, fmt.Errorf("error with file: %s", "content is empty") + return []byte{}, fmt.Errorf("error with file: content is empty") } nBytes, nChunks := int64(0), int64(0) @@ -101,6 +101,22 @@ func (s *Signer) SignFile(filename string) ([]byte, error) { return signature, nil } +// SignBytes signs the provided bytes, returning the signature as a byte slice. +func (s *Signer) SignBytes(data []byte) ([]byte, error) { + if len(data) == 0 { + return []byte{}, fmt.Errorf("error with data: content is empty") + } + + s.Sum(data) + + signature, err := s.Sign() + if err != nil { + return []byte{}, fmt.Errorf("failed to sign data: %s", err.Error()) + } + + return signature, nil +} + // signatureBytesToHex converts a byte slice to a hex-encoded string. func SignatureBytesToHex(b []byte) string { return hex.EncodeToString(b) diff --git a/pkg/signing/signing_test.go b/pkg/signing/signing_test.go index 34617a1..09e8821 100644 --- a/pkg/signing/signing_test.go +++ b/pkg/signing/signing_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestSigner(t *testing.T) { +func TestSignFile(t *testing.T) { privateKey, _ := HexToECDSA("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") signer := NewSigner(privateKey) @@ -18,7 +18,7 @@ func TestSigner(t *testing.T) { expectedSignature string }{ { - name: "should sign with content", + name: "should sign file with content", setup: func() (filename string, cleanup func()) { tmpFile, _ := os.CreateTemp("", "test_file") name := tmpFile.Name() @@ -71,6 +71,45 @@ func TestSigner(t *testing.T) { } } +func TestSignByes(t *testing.T) { + privateKey, _ := HexToECDSA("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") + signer := NewSigner(privateKey) + + testCases := []struct { + name string + content []byte + wantErr string + expectedSignature string + }{ + { + name: "should sign bytes", + content: []byte("data to be signed"), + wantErr: "", + expectedSignature: "6ddb61a19b9df71136b48c80b2e86e7e20313d5eec0de9210802335b300ba8df6c332d35a5d753a028d703769fd9b66d7ce5902d80369750cf55118b1679d84900", + }, + { + name: "should fail with empty bytes", + content: []byte(""), + wantErr: "error with data: content is empty", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + signatureBytes, err := signer.SignBytes(tc.content) + signature := SignatureBytesToHex(signatureBytes) + if tc.wantErr != "" { + require.Error(t, err, "Expected an error for %v", tc.name) + require.Contains(t, err.Error(), tc.wantErr, "SignBytes() error = %v, wantErr %v", err, tc.wantErr) + } else { + require.NoError(t, err, "SignBytes() unexpected error = %v", err) + require.Equal(t, tc.expectedSignature, signature, "Signature mismatch") + } + }) + } +} + func TestPrivateKey(t *testing.T) { testCases := []struct { name string From 766d2446506ece201b84d0607fb77cb1f9798ab5 Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Tue, 27 Feb 2024 22:07:46 -0800 Subject: [PATCH 07/10] feat: add pk loading from file --- pkg/signing/signing.go | 9 ++++++- pkg/signing/signing_test.go | 54 ++++++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/pkg/signing/signing.go b/pkg/signing/signing.go index e26d15c..0169087 100644 --- a/pkg/signing/signing.go +++ b/pkg/signing/signing.go @@ -19,11 +19,18 @@ type Signer struct { privateKey *ecdsa.PrivateKey } -// HexToECDSA parses a hex encoded private key to an ECDSA private key. +// HexToECDSA parses a hex-encoded secp256k1 private key string to an ECDSA +// private key. func HexToECDSA(hexKey string) (*ecdsa.PrivateKey, error) { return crypto.HexToECDSA(hexKey) } +// FileToECDSA parses a file path to a hex-encoded secp256k1 private key to an +// ECDSA private key. +func FileToECDSA(hexPath string) (*ecdsa.PrivateKey, error) { + return crypto.LoadECDSA(hexPath) +} + // NewSigner creates a new signer. func NewSigner(pk *ecdsa.PrivateKey) *Signer { return &Signer{ diff --git a/pkg/signing/signing_test.go b/pkg/signing/signing_test.go index 09e8821..d557a34 100644 --- a/pkg/signing/signing_test.go +++ b/pkg/signing/signing_test.go @@ -1,6 +1,7 @@ package signing import ( + "crypto/ecdsa" "os" "testing" @@ -71,7 +72,7 @@ func TestSignFile(t *testing.T) { } } -func TestSignByes(t *testing.T) { +func TestSignBytes(t *testing.T) { privateKey, _ := HexToECDSA("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") signer := NewSigner(privateKey) @@ -113,40 +114,69 @@ func TestSignByes(t *testing.T) { func TestPrivateKey(t *testing.T) { testCases := []struct { name string - setup func() (pk string) + setup func() (pk string, filename string, cleanup func()) wantErr string }{ { - name: "should load a real private key", - setup: func() (pk string) { + name: "should load a private key string", + setup: func() (pk string, filename string, cleanup func()) { pk = "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" - return pk + return pk, "", func() {} }, wantErr: "", }, { - name: "should fail to load 0x prefixed key", - setup: func() (pk string) { + name: "should load a private key file", + setup: func() (pk string, filename string, cleanup func()) { + tmpFile, _ := os.CreateTemp("", "test_file") + name := tmpFile.Name() + content := []byte("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") + tmpFile.Write(content) + tmpFile.Close() + return pk, name, func() { os.Remove(name) } + }, + wantErr: "", + }, + { + name: "should fail to load 0x prefixed string", + setup: func() (pk string, filename string, cleanup func()) { pk = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" - return pk + return pk, "", func() {} }, wantErr: "invalid hex character 'x' in private key", }, { name: "should fail to load random string", - setup: func() (pk string) { + setup: func() (pk string, filename string, cleanup func()) { pk = "1234abcd" - return pk + return pk, "", func() {} }, wantErr: "invalid length, need 256 bits", }, + { + name: "should fail to load empty private key file", + setup: func() (pk string, filename string, cleanup func()) { + tmpFile, _ := os.CreateTemp("", "test_file") + name := tmpFile.Name() + tmpFile.Close() + return pk, name, func() { os.Remove(name) } + }, + wantErr: "key file too short, want 64 hex characters", + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - pk := tc.setup() + pk, filename, cleanup := tc.setup() + defer cleanup() - hex, err := HexToECDSA(pk) + var hex *ecdsa.PrivateKey + var err error + if filename == "" { + hex, err = HexToECDSA(pk) + } else { + hex, err = FileToECDSA(filename) + } if tc.wantErr != "" { require.Error(t, err, "Expected an error for %v", tc.name) require.EqualErrorf(t, err, tc.wantErr, "HexToECDSA() error = %v, wantErr %v", err, tc.wantErr) From 1975344fcf0823ed519ffa4fed62e0a34dd8e60d Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Tue, 27 Feb 2024 22:26:05 -0800 Subject: [PATCH 08/10] chore: linting fixes --- pkg/signing/signing.go | 2 +- pkg/signing/signing_test.go | 59 +++++++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/pkg/signing/signing.go b/pkg/signing/signing.go index 0169087..b77d7aa 100644 --- a/pkg/signing/signing.go +++ b/pkg/signing/signing.go @@ -124,7 +124,7 @@ func (s *Signer) SignBytes(data []byte) ([]byte, error) { return signature, nil } -// signatureBytesToHex converts a byte slice to a hex-encoded string. +// SignatureBytesToHex converts a byte slice to a hex-encoded string. func SignatureBytesToHex(b []byte) string { return hex.EncodeToString(b) } diff --git a/pkg/signing/signing_test.go b/pkg/signing/signing_test.go index d557a34..7808ff5 100644 --- a/pkg/signing/signing_test.go +++ b/pkg/signing/signing_test.go @@ -24,20 +24,30 @@ func TestSignFile(t *testing.T) { tmpFile, _ := os.CreateTemp("", "test_file") name := tmpFile.Name() content := []byte("data to be signed") - tmpFile.Write(content) - tmpFile.Close() - return name, func() { os.Remove(name) } + _, err := tmpFile.Write(content) + require.NoError(t, err, "Error writing to file") + err = tmpFile.Close() + require.NoError(t, err, "Error closing file") + return name, func() { + err = os.Remove(name) + require.NoError(t, err, "Error removing file") + } }, - wantErr: "", - expectedSignature: "6ddb61a19b9df71136b48c80b2e86e7e20313d5eec0de9210802335b300ba8df6c332d35a5d753a028d703769fd9b66d7ce5902d80369750cf55118b1679d84900", + wantErr: "", + expectedSignature: "6ddb61a19b9df71136b48c80b2e86e7e20313d5eec0de9210802335b3" + + "00ba8df6c332d35a5d753a028d703769fd9b66d7ce5902d80369750cf55118b1679d84900", }, { name: "should fail with empty file", setup: func() (filename string, cleanup func()) { tmpFile, _ := os.CreateTemp("", "test_file") name := tmpFile.Name() - tmpFile.Close() - return name, func() { os.Remove(name) } + err := tmpFile.Close() + require.NoError(t, err, "Error closing file") + return name, func() { + err = os.Remove(name) + require.NoError(t, err, "Error removing file") + } }, wantErr: "error with file: content is empty", }, @@ -46,8 +56,10 @@ func TestSignFile(t *testing.T) { setup: func() (filename string, cleanup func()) { tmpFile, _ := os.CreateTemp("", "test_file") name := tmpFile.Name() - tmpFile.Close() - os.Remove(name) + err := tmpFile.Close() + require.NoError(t, err, "Error closing file") + err = os.Remove(name) + require.NoError(t, err, "Error removing file") return name, func() {} }, wantErr: "error reading [file=", @@ -83,10 +95,11 @@ func TestSignBytes(t *testing.T) { expectedSignature string }{ { - name: "should sign bytes", - content: []byte("data to be signed"), - wantErr: "", - expectedSignature: "6ddb61a19b9df71136b48c80b2e86e7e20313d5eec0de9210802335b300ba8df6c332d35a5d753a028d703769fd9b66d7ce5902d80369750cf55118b1679d84900", + name: "should sign bytes", + content: []byte("data to be signed"), + wantErr: "", + expectedSignature: "6ddb61a19b9df71136b48c80b2e86e7e20313d5eec0de9210802335b3" + + "00ba8df6c332d35a5d753a028d703769fd9b66d7ce5902d80369750cf55118b1679d84900", }, { name: "should fail with empty bytes", @@ -97,7 +110,6 @@ func TestSignBytes(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - signatureBytes, err := signer.SignBytes(tc.content) signature := SignatureBytesToHex(signatureBytes) if tc.wantErr != "" { @@ -131,9 +143,14 @@ func TestPrivateKey(t *testing.T) { tmpFile, _ := os.CreateTemp("", "test_file") name := tmpFile.Name() content := []byte("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d") - tmpFile.Write(content) - tmpFile.Close() - return pk, name, func() { os.Remove(name) } + _, err := tmpFile.Write(content) + require.NoError(t, err, "Error writing to file") + err = tmpFile.Close() + require.NoError(t, err, "Error closing file") + return pk, name, func() { + err = os.Remove(name) + require.NoError(t, err, "Error removing file") + } }, wantErr: "", }, @@ -158,8 +175,12 @@ func TestPrivateKey(t *testing.T) { setup: func() (pk string, filename string, cleanup func()) { tmpFile, _ := os.CreateTemp("", "test_file") name := tmpFile.Name() - tmpFile.Close() - return pk, name, func() { os.Remove(name) } + err := tmpFile.Close() + require.NoError(t, err, "Error closing file") + return pk, name, func() { + err = os.Remove(name) + require.NoError(t, err, "Error removing file") + } }, wantErr: "key file too short, want 64 hex characters", }, From 841771edf130baf0c7731581c69689bd9bf777d8 Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Wed, 28 Feb 2024 11:34:11 -0800 Subject: [PATCH 09/10] refactor: add vaults address flag for string pk --- cmd/vaults/commands.go | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/cmd/vaults/commands.go b/cmd/vaults/commands.go index 299dc15..d916483 100644 --- a/cmd/vaults/commands.go +++ b/cmd/vaults/commands.go @@ -694,6 +694,8 @@ func newRetrieveCommand() *cli.Command { } func newWalletCommand() *cli.Command { + var pkString string + return &cli.Command{ Name: "account", Usage: "Account management for an Ethereum-style wallet", @@ -732,28 +734,34 @@ func newWalletCommand() *cli.Command { { Name: "address", Usage: "Print the public key for an account's private key", - UsageText: "vaults account address ", + UsageText: "vaults account address [command options] ", Description: "The result of the `vaults account create` command will write a private key to a file, and \n" + - "this lets you retrieve the public key value for the file, or a private key hex string.\n\n" + - "EXAMPLES:\n\nvaults account address /path/to/file\nvaults account address abcd1234", + "this lets you retrieve the public key value for the file, or a private key hex string.\n" + + "If no `--string` flag is provided, then the presumption is the argument is a filepath.\n\n" + + "EXAMPLES:\n\nvaults account address /path/to/file\nvaults account address --string abcd1234", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "string", + Category: "OPTIONAL:", + Usage: "Specify if the argument is a hex string", + Destination: &pkString, + }, + }, Action: func(cCtx *cli.Context) error { - arg := cCtx.Args().Get(0) - if arg == "" { + pkFile := cCtx.Args().Get(0) + if pkFile == "" && pkString == "" { return errors.New("no argument provided") } var privateKey *ecdsa.PrivateKey - // Try loading from file path first; else, try hex string - if _, err := os.Stat(arg); err == nil { - privateKey, err = crypto.LoadECDSA(arg) - if err != nil { - return fmt.Errorf("loading key from file: %s", err) - } + var err error + if pkString == "" { + privateKey, err = crypto.LoadECDSA(pkFile) } else { - privateKey, err = crypto.HexToECDSA(arg) - if err != nil { - return fmt.Errorf("loading key from hex string: %s", err) - } + privateKey, err = crypto.HexToECDSA(pkString) + } + if err != nil { + return fmt.Errorf("loading key: %s", err) } pubk, _ := privateKey.Public().(*ecdsa.PublicKey) From 70c8c5b29b64949dadf3af5639307cb02dbfaa42 Mon Sep 17 00:00:00 2001 From: Dan Buchholz Date: Wed, 28 Feb 2024 11:48:38 -0800 Subject: [PATCH 10/10] refactor: remove unneeded methods, tests | err check fix Removed the `SignatureBytesToHex` since its just a wrapper on std hex lib method and refactored where needed. Removed failure tests for `HexToECDSA` and `FileToECDSA` since these are just wrappers for eth crypto. And made a small fix on order of err check in `uploader.go`. --- cmd/vaults/commands.go | 3 ++- internal/app/uploader.go | 3 ++- pkg/signing/signing.go | 6 ------ pkg/signing/signing_test.go | 35 +++-------------------------------- 4 files changed, 7 insertions(+), 40 deletions(-) diff --git a/cmd/vaults/commands.go b/cmd/vaults/commands.go index d916483..38ac8cc 100644 --- a/cmd/vaults/commands.go +++ b/cmd/vaults/commands.go @@ -3,6 +3,7 @@ package main import ( "context" "crypto/ecdsa" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -625,7 +626,7 @@ func newSignCommand() *cli.Command { if err != nil { return fmt.Errorf("failed to sign file: %s", err) } - signature := signing.SignatureBytesToHex(signatureBytes) + signature := hex.EncodeToString(signatureBytes) fmt.Println(signature) return nil diff --git a/internal/app/uploader.go b/internal/app/uploader.go index ba0bfbe..e6a6620 100644 --- a/internal/app/uploader.go +++ b/internal/app/uploader.go @@ -3,6 +3,7 @@ package app import ( "context" "crypto/ecdsa" + "encoding/hex" "fmt" "io" "os" @@ -45,10 +46,10 @@ func (bu *VaultsUploader) Upload( signer := signing.NewSigner(bu.privateKey) signatureBytes, err := signer.SignFile(filepath) - signature := signing.SignatureBytesToHex(signatureBytes) if err != nil { return fmt.Errorf("signing the file: %s", err) } + signature := hex.EncodeToString(signatureBytes) filename := filepath if strings.Contains(filepath, "/") { diff --git a/pkg/signing/signing.go b/pkg/signing/signing.go index b77d7aa..1e369f6 100644 --- a/pkg/signing/signing.go +++ b/pkg/signing/signing.go @@ -3,7 +3,6 @@ package signing import ( "bufio" "crypto/ecdsa" - "encoding/hex" "fmt" "io" "os" @@ -123,8 +122,3 @@ func (s *Signer) SignBytes(data []byte) ([]byte, error) { return signature, nil } - -// SignatureBytesToHex converts a byte slice to a hex-encoded string. -func SignatureBytesToHex(b []byte) string { - return hex.EncodeToString(b) -} diff --git a/pkg/signing/signing_test.go b/pkg/signing/signing_test.go index 7808ff5..f779751 100644 --- a/pkg/signing/signing_test.go +++ b/pkg/signing/signing_test.go @@ -2,6 +2,7 @@ package signing import ( "crypto/ecdsa" + "encoding/hex" "os" "testing" @@ -72,7 +73,7 @@ func TestSignFile(t *testing.T) { defer cleanup() signatureBytes, err := signer.SignFile(filename) - signature := SignatureBytesToHex(signatureBytes) + signature := hex.EncodeToString(signatureBytes) if tc.wantErr != "" { require.Error(t, err, "Expected an error for %v", tc.name) require.Contains(t, err.Error(), tc.wantErr, "SignFile() error = %v, wantErr %v", err, tc.wantErr) @@ -111,7 +112,7 @@ func TestSignBytes(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { signatureBytes, err := signer.SignBytes(tc.content) - signature := SignatureBytesToHex(signatureBytes) + signature := hex.EncodeToString(signatureBytes) if tc.wantErr != "" { require.Error(t, err, "Expected an error for %v", tc.name) require.Contains(t, err.Error(), tc.wantErr, "SignBytes() error = %v, wantErr %v", err, tc.wantErr) @@ -154,36 +155,6 @@ func TestPrivateKey(t *testing.T) { }, wantErr: "", }, - { - name: "should fail to load 0x prefixed string", - setup: func() (pk string, filename string, cleanup func()) { - pk = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" - return pk, "", func() {} - }, - wantErr: "invalid hex character 'x' in private key", - }, - { - name: "should fail to load random string", - setup: func() (pk string, filename string, cleanup func()) { - pk = "1234abcd" - return pk, "", func() {} - }, - wantErr: "invalid length, need 256 bits", - }, - { - name: "should fail to load empty private key file", - setup: func() (pk string, filename string, cleanup func()) { - tmpFile, _ := os.CreateTemp("", "test_file") - name := tmpFile.Name() - err := tmpFile.Close() - require.NoError(t, err, "Error closing file") - return pk, name, func() { - err = os.Remove(name) - require.NoError(t, err, "Error removing file") - } - }, - wantErr: "key file too short, want 64 hex characters", - }, } for _, tc := range testCases {