Skip to content
This repository was archived by the owner on Jan 28, 2026. It is now read-only.

Commit abe96bd

Browse files
authored
Merge pull request #36 from tablelandnetwork/dtb/signer-lib
feat: signing pkg for programmatic access
2 parents 6044303 + 70c8c5b commit abe96bd

File tree

5 files changed

+383
-92
lines changed

5 files changed

+383
-92
lines changed

cmd/vaults/commands.go

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"crypto/ecdsa"
6+
"encoding/hex"
67
"encoding/json"
78
"errors"
89
"fmt"
@@ -22,6 +23,7 @@ import (
2223
"github.com/schollz/progressbar/v3"
2324
"github.com/tablelandnetwork/basin-cli/internal/app"
2425
"github.com/tablelandnetwork/basin-cli/pkg/pgrepl"
26+
"github.com/tablelandnetwork/basin-cli/pkg/signing"
2527
"github.com/tablelandnetwork/basin-cli/pkg/vaultsprovider"
2628
"github.com/urfave/cli/v2"
2729
"gopkg.in/yaml.v3"
@@ -162,7 +164,7 @@ func newStreamCommand() *cli.Command {
162164
ArgsUsage: "<vault_name>",
163165
Description: "The daemon will continuously stream database changes (except deletions) \n" +
164166
"to the vault, as long as the daemon is actively running.\n\n" +
165-
"EXAMPLE:\n\nvaults stream --vault my.vault --private-key 0x1234abcd",
167+
"EXAMPLE:\n\nvaults stream --private-key 0x1234abcd my.vault",
166168

167169
Flags: []cli.Flag{
168170
&cli.StringFlag{
@@ -588,6 +590,50 @@ func newListEventsCommand() *cli.Command {
588590
}
589591
}
590592

593+
func newSignCommand() *cli.Command {
594+
var privateKey string
595+
596+
return &cli.Command{
597+
Name: "sign",
598+
Usage: "Sign a file with a private key",
599+
ArgsUsage: "<file_path>",
600+
Description: "Signing a file with take a provide key and a path to the desired file\n" +
601+
"to produce a hex encoded string (e.g., can be used in the HTTP API).\n\n" +
602+
"EXAMPLE:\n\nvaults sign --private-key 0x1234abcd /path/to/file",
603+
Flags: []cli.Flag{
604+
&cli.StringFlag{
605+
Name: "private-key",
606+
Aliases: []string{"k"},
607+
Category: "REQUIRED:",
608+
Usage: "Ethereum wallet private key",
609+
Destination: &privateKey,
610+
Required: true,
611+
},
612+
},
613+
Action: func(cCtx *cli.Context) error {
614+
if cCtx.NArg() != 1 {
615+
return errors.New("must provide a file path")
616+
}
617+
filepath := cCtx.Args().First()
618+
619+
privateKey, err := crypto.HexToECDSA(privateKey)
620+
if err != nil {
621+
return err
622+
}
623+
624+
signer := signing.NewSigner(privateKey)
625+
signatureBytes, err := signer.SignFile(filepath)
626+
if err != nil {
627+
return fmt.Errorf("failed to sign file: %s", err)
628+
}
629+
signature := hex.EncodeToString(signatureBytes)
630+
fmt.Println(signature)
631+
632+
return nil
633+
},
634+
}
635+
}
636+
591637
func newRetrieveCommand() *cli.Command {
592638
var output, provider string
593639
var timeout int64
@@ -649,6 +695,8 @@ func newRetrieveCommand() *cli.Command {
649695
}
650696

651697
func newWalletCommand() *cli.Command {
698+
var pkString string
699+
652700
return &cli.Command{
653701
Name: "account",
654702
Usage: "Account management for an Ethereum-style wallet",
@@ -687,17 +735,32 @@ func newWalletCommand() *cli.Command {
687735
{
688736
Name: "address",
689737
Usage: "Print the public key for an account's private key",
690-
UsageText: "vaults account address <file_path>",
691-
Description: "The result of the `vaults account create` command will write a private key to a file, \n" +
692-
"and this lets you retrieve the public key value for use in other commands.\n\n" +
693-
"EXAMPLE:\n\nvaults account address /path/to/file",
738+
UsageText: "vaults account address [command options] <value>",
739+
Description: "The result of the `vaults account create` command will write a private key to a file, and \n" +
740+
"this lets you retrieve the public key value for the file, or a private key hex string.\n" +
741+
"If no `--string` flag is provided, then the presumption is the argument is a filepath.\n\n" +
742+
"EXAMPLES:\n\nvaults account address /path/to/file\nvaults account address --string abcd1234",
743+
Flags: []cli.Flag{
744+
&cli.StringFlag{
745+
Name: "string",
746+
Category: "OPTIONAL:",
747+
Usage: "Specify if the argument is a hex string",
748+
Destination: &pkString,
749+
},
750+
},
694751
Action: func(cCtx *cli.Context) error {
695-
filename := cCtx.Args().Get(0)
696-
if filename == "" {
697-
return errors.New("filename is empty")
752+
pkFile := cCtx.Args().Get(0)
753+
if pkFile == "" && pkString == "" {
754+
return errors.New("no argument provided")
698755
}
699756

700-
privateKey, err := crypto.LoadECDSA(filename)
757+
var privateKey *ecdsa.PrivateKey
758+
var err error
759+
if pkString == "" {
760+
privateKey, err = crypto.LoadECDSA(pkFile)
761+
} else {
762+
privateKey, err = crypto.HexToECDSA(pkString)
763+
}
701764
if err != nil {
702765
return fmt.Errorf("loading key: %s", err)
703766
}

cmd/vaults/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func main() {
3636
newWriteCommand(),
3737
newListCommand(),
3838
newListEventsCommand(),
39+
newSignCommand(),
3940
newRetrieveCommand(),
4041
newWalletCommand(),
4142
},

internal/app/uploader.go

Lines changed: 5 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
package app
22

33
import (
4-
"bufio"
54
"context"
65
"crypto/ecdsa"
76
"encoding/hex"
87
"fmt"
98
"io"
10-
"log"
119
"os"
1210
"strings"
1311

14-
"github.com/ethereum/go-ethereum/common"
15-
"github.com/ethereum/go-ethereum/crypto"
16-
"golang.org/x/crypto/sha3"
12+
"github.com/tablelandnetwork/basin-cli/pkg/signing"
1713
)
1814

1915
// VaultsUploader contains logic of uploading Parquet files to Vaults Provider.
@@ -48,11 +44,12 @@ func (bu *VaultsUploader) Upload(
4844
_ = f.Close()
4945
}()
5046

51-
signer := NewSigner(bu.privateKey)
52-
signature, err := signer.SignFile(filepath)
47+
signer := signing.NewSigner(bu.privateKey)
48+
signatureBytes, err := signer.SignFile(filepath)
5349
if err != nil {
5450
return fmt.Errorf("signing the file: %s", err)
5551
}
52+
signature := hex.EncodeToString(signatureBytes)
5653

5754
filename := filepath
5855
if strings.Contains(filepath, "/") {
@@ -66,7 +63,7 @@ func (bu *VaultsUploader) Upload(
6663
Content: f,
6764
Filename: filename,
6865
ProgressBar: progress,
69-
Signature: hex.EncodeToString(signature),
66+
Signature: signature,
7067
Size: sz,
7168
}
7269

@@ -76,78 +73,3 @@ func (bu *VaultsUploader) Upload(
7673

7774
return nil
7875
}
79-
80-
// Signer allows you to sign a big stream of bytes by calling Sum multiple times, then Sign.
81-
type Signer struct {
82-
state crypto.KeccakState
83-
privateKey *ecdsa.PrivateKey
84-
}
85-
86-
// NewSigner creates a new signer.
87-
func NewSigner(pk *ecdsa.PrivateKey) *Signer {
88-
return &Signer{
89-
state: sha3.NewLegacyKeccak256().(crypto.KeccakState),
90-
privateKey: pk,
91-
}
92-
}
93-
94-
// Sum updates the hash state with a new chunk.
95-
func (s *Signer) Sum(chunk []byte) {
96-
s.state.Write(chunk)
97-
}
98-
99-
// Sign signs the internal state.
100-
func (s *Signer) Sign() ([]byte, error) {
101-
var h common.Hash
102-
_, _ = s.state.Read(h[:])
103-
signature, err := crypto.Sign(h.Bytes(), s.privateKey)
104-
if err != nil {
105-
return []byte{}, fmt.Errorf("sign: %s", err)
106-
}
107-
108-
return signature, nil
109-
}
110-
111-
// SignFile signs an entire file.
112-
func (s *Signer) SignFile(filename string) ([]byte, error) {
113-
f, err := os.Open(filename)
114-
if err != nil {
115-
return []byte{}, fmt.Errorf("error to read [file=%v]: %v", filename, err.Error())
116-
}
117-
118-
defer func() {
119-
_ = f.Close()
120-
}()
121-
122-
nBytes, nChunks := int64(0), int64(0)
123-
r := bufio.NewReader(f)
124-
buf := make([]byte, 0, 4*1024)
125-
for {
126-
n, err := r.Read(buf[:cap(buf)])
127-
buf = buf[:n]
128-
if n == 0 {
129-
if err == nil {
130-
continue
131-
}
132-
if err == io.EOF {
133-
break
134-
}
135-
log.Fatal(err)
136-
}
137-
nChunks++
138-
nBytes += int64(len(buf))
139-
140-
s.Sum(buf)
141-
142-
if err != nil && err != io.EOF {
143-
log.Fatal(err)
144-
}
145-
}
146-
147-
signature, err := s.Sign()
148-
if err != nil {
149-
log.Fatal("failed to sign")
150-
}
151-
152-
return signature, nil
153-
}

pkg/signing/signing.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package signing
2+
3+
import (
4+
"bufio"
5+
"crypto/ecdsa"
6+
"fmt"
7+
"io"
8+
"os"
9+
10+
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/crypto"
12+
"golang.org/x/crypto/sha3"
13+
)
14+
15+
// Signer allows you to sign a big stream of bytes by calling Sum multiple times, then Sign.
16+
type Signer struct {
17+
state crypto.KeccakState
18+
privateKey *ecdsa.PrivateKey
19+
}
20+
21+
// HexToECDSA parses a hex-encoded secp256k1 private key string to an ECDSA
22+
// private key.
23+
func HexToECDSA(hexKey string) (*ecdsa.PrivateKey, error) {
24+
return crypto.HexToECDSA(hexKey)
25+
}
26+
27+
// FileToECDSA parses a file path to a hex-encoded secp256k1 private key to an
28+
// ECDSA private key.
29+
func FileToECDSA(hexPath string) (*ecdsa.PrivateKey, error) {
30+
return crypto.LoadECDSA(hexPath)
31+
}
32+
33+
// NewSigner creates a new signer.
34+
func NewSigner(pk *ecdsa.PrivateKey) *Signer {
35+
return &Signer{
36+
state: sha3.NewLegacyKeccak256().(crypto.KeccakState),
37+
privateKey: pk,
38+
}
39+
}
40+
41+
// Sum updates the hash state with a new chunk.
42+
func (s *Signer) Sum(chunk []byte) {
43+
s.state.Write(chunk)
44+
}
45+
46+
// Sign signs the internal state.
47+
func (s *Signer) Sign() ([]byte, error) {
48+
var h common.Hash
49+
_, _ = s.state.Read(h[:])
50+
signature, err := crypto.Sign(h.Bytes(), s.privateKey)
51+
if err != nil {
52+
return []byte{}, fmt.Errorf("sign: %s", err)
53+
}
54+
55+
return signature, nil
56+
}
57+
58+
// SignFile signs an entire file, returning the signature as a byte slice.
59+
func (s *Signer) SignFile(filename string) ([]byte, error) {
60+
f, err := os.Open(filename)
61+
if err != nil {
62+
return []byte{}, fmt.Errorf("error reading [file=%v]: %v", filename, err.Error())
63+
}
64+
defer func() {
65+
_ = f.Close()
66+
}()
67+
68+
// Check if the file is empty and return an error if it is
69+
info, err := f.Stat()
70+
if err != nil {
71+
return []byte{}, fmt.Errorf("failed to get file info: %s", err.Error())
72+
}
73+
if info.Size() == 0 {
74+
return []byte{}, fmt.Errorf("error with file: content is empty")
75+
}
76+
77+
nBytes, nChunks := int64(0), int64(0)
78+
r := bufio.NewReader(f)
79+
buf := make([]byte, 0, 4*1024) // 4KB buffer
80+
for {
81+
n, err := r.Read(buf[:cap(buf)])
82+
buf = buf[:n]
83+
if n == 0 {
84+
if err == nil {
85+
continue
86+
}
87+
if err == io.EOF {
88+
break
89+
}
90+
return []byte{}, fmt.Errorf("unexpected error reading file: %s", err.Error())
91+
}
92+
nChunks++
93+
nBytes += int64(len(buf))
94+
95+
s.Sum(buf)
96+
97+
if err != nil && err != io.EOF {
98+
return []byte{}, fmt.Errorf("error in buffer: %s", err.Error())
99+
}
100+
}
101+
102+
signature, err := s.Sign()
103+
if err != nil {
104+
return []byte{}, fmt.Errorf("failed to sign [file=%v]: %s", filename, err.Error())
105+
}
106+
107+
return signature, nil
108+
}
109+
110+
// SignBytes signs the provided bytes, returning the signature as a byte slice.
111+
func (s *Signer) SignBytes(data []byte) ([]byte, error) {
112+
if len(data) == 0 {
113+
return []byte{}, fmt.Errorf("error with data: content is empty")
114+
}
115+
116+
s.Sum(data)
117+
118+
signature, err := s.Sign()
119+
if err != nil {
120+
return []byte{}, fmt.Errorf("failed to sign data: %s", err.Error())
121+
}
122+
123+
return signature, nil
124+
}

0 commit comments

Comments
 (0)