Skip to content

Commit

Permalink
Merge pull request #36 from tablelandnetwork/dtb/signer-lib
Browse files Browse the repository at this point in the history
feat: signing pkg for programmatic access
  • Loading branch information
dtbuchholz authored Feb 28, 2024
2 parents 6044303 + 70c8c5b commit abe96bd
Show file tree
Hide file tree
Showing 5 changed files with 383 additions and 92 deletions.
81 changes: 72 additions & 9 deletions cmd/vaults/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
Expand All @@ -22,6 +23,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"
Expand Down Expand Up @@ -162,7 +164,7 @@ func newStreamCommand() *cli.Command {
ArgsUsage: "<vault_name>",
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{
Expand Down Expand Up @@ -588,6 +590,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: "<file_path>",
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 := hex.EncodeToString(signatureBytes)
fmt.Println(signature)

return nil
},
}
}

func newRetrieveCommand() *cli.Command {
var output, provider string
var timeout int64
Expand Down Expand Up @@ -649,6 +695,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",
Expand Down Expand Up @@ -687,17 +735,32 @@ func newWalletCommand() *cli.Command {
{
Name: "address",
Usage: "Print the public key for an account's private key",
UsageText: "vaults account address <file_path>",
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 [command options] <value>",
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" +
"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 {
filename := cCtx.Args().Get(0)
if filename == "" {
return errors.New("filename is empty")
pkFile := cCtx.Args().Get(0)
if pkFile == "" && pkString == "" {
return errors.New("no argument provided")
}

privateKey, err := crypto.LoadECDSA(filename)
var privateKey *ecdsa.PrivateKey
var err error
if pkString == "" {
privateKey, err = crypto.LoadECDSA(pkFile)
} else {
privateKey, err = crypto.HexToECDSA(pkString)
}
if err != nil {
return fmt.Errorf("loading key: %s", err)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/vaults/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func main() {
newWriteCommand(),
newListCommand(),
newListEventsCommand(),
newSignCommand(),
newRetrieveCommand(),
newWalletCommand(),
},
Expand Down
88 changes: 5 additions & 83 deletions internal/app/uploader.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
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.
Expand Down Expand Up @@ -48,11 +44,12 @@ 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)
if err != nil {
return fmt.Errorf("signing the file: %s", err)
}
signature := hex.EncodeToString(signatureBytes)

filename := filepath
if strings.Contains(filepath, "/") {
Expand All @@ -66,7 +63,7 @@ func (bu *VaultsUploader) Upload(
Content: f,
Filename: filename,
ProgressBar: progress,
Signature: hex.EncodeToString(signature),
Signature: signature,
Size: sz,
}

Expand All @@ -76,78 +73,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
}
124 changes: 124 additions & 0 deletions pkg/signing/signing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package signing

import (
"bufio"
"crypto/ecdsa"
"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
}

// 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{
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, returning the signature as a byte slice.
func (s *Signer) SignFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return []byte{}, fmt.Errorf("error reading [file=%v]: %v", filename, err.Error())
}
defer func() {
_ = f.Close()
}()

// Check if the file is empty and return an error if it is
info, err := f.Stat()
if err != nil {
return []byte{}, fmt.Errorf("failed to get file info: %s", err.Error())
}
if info.Size() == 0 {
return []byte{}, fmt.Errorf("error with file: content is empty")
}

nBytes, nChunks := int64(0), int64(0)
r := bufio.NewReader(f)
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 []byte{}, fmt.Errorf("unexpected error reading file: %s", err.Error())
}
nChunks++
nBytes += int64(len(buf))

s.Sum(buf)

if err != nil && err != io.EOF {
return []byte{}, fmt.Errorf("error in buffer: %s", err.Error())
}
}

signature, err := s.Sign()
if err != nil {
return []byte{}, fmt.Errorf("failed to sign [file=%v]: %s", filename, err.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
}
Loading

0 comments on commit abe96bd

Please sign in to comment.