Skip to content
Merged
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
39 changes: 33 additions & 6 deletions pdp/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,32 @@ import (
"github.com/ipfs/go-cid"
)

// SignDigestFunc signs a 32-byte keccak digest and returns a 65-byte
// recoverable secp256k1 signature in [R || S || V] form, where V is the
// recovery ID (0 or 1). AuthHelper normalizes V to 27/28 internally
// before producing the AuthSignature for on-chain consumption.
type SignDigestFunc func(digest []byte) ([]byte, error)

// AuthHelper signs PDP extraData blobs (CreateDataSet, AddPieces, etc.)
// against the FWSS EIP-712 domain. It does not hold a key directly:
// callers supply a SignDigestFunc, which lets the EVMSigner abstraction
// (or an HSM, remote signer, etc.) own the key material.
type AuthHelper struct {
privateKey *ecdsa.PrivateKey
signDigest SignDigestFunc
address common.Address
warmStorageAddress common.Address
chainID *big.Int
domain apitypes.TypedDataDomain
}

func NewAuthHelper(privateKey *ecdsa.PrivateKey, warmStorageAddr common.Address, chainID *big.Int) *AuthHelper {
address := crypto.PubkeyToAddress(privateKey.PublicKey)

// NewAuthHelper builds an AuthHelper bound to the given signer, payer
// address, FWSS contract address, and chainID. The address is the
// recovered signer of every signature this helper produces; passing a
// mismatched (signDigest, address) pair results in signatures that
// FWSS will reject at eth_call time.
func NewAuthHelper(signDigest SignDigestFunc, address common.Address, warmStorageAddr common.Address, chainID *big.Int) *AuthHelper {
return &AuthHelper{
privateKey: privateKey,
signDigest: signDigest,
address: address,
warmStorageAddress: warmStorageAddr,
chainID: chainID,
Expand All @@ -37,6 +50,17 @@ func NewAuthHelper(privateKey *ecdsa.PrivateKey, warmStorageAddr common.Address,
}
}

// NewAuthHelperFromKey is a convenience for callers that hold a raw
// secp256k1 key (test fixtures, scripts, examples). Production code
// should plumb through an EVMSigner and use NewAuthHelper directly.
func NewAuthHelperFromKey(privateKey *ecdsa.PrivateKey, warmStorageAddr common.Address, chainID *big.Int) *AuthHelper {
address := crypto.PubkeyToAddress(privateKey.PublicKey)
signDigest := func(digest []byte) ([]byte, error) {
return crypto.Sign(digest, privateKey)
}
return NewAuthHelper(signDigest, address, warmStorageAddr, chainID)
}

func (a *AuthHelper) Address() common.Address {
return a.address
}
Expand Down Expand Up @@ -185,10 +209,13 @@ func (a *AuthHelper) signTypedData(primaryType string, message apitypes.TypedDat
rawData = append(rawData, messageHash...)
signedData := crypto.Keccak256Hash(rawData)

signature, err := crypto.Sign(signedData.Bytes(), a.privateKey)
signature, err := a.signDigest(signedData.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to sign: %w", err)
}
if len(signature) != 65 {
return nil, fmt.Errorf("signer returned %d bytes, expected 65", len(signature))
}

if signature[64] < 27 {
signature[64] += 27
Expand Down
62 changes: 61 additions & 1 deletion pdp/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func setupAuthHelper(t *testing.T) *AuthHelper {
contractAddr := common.HexToAddress(fixtures.ContractAddress)
chainID := big.NewInt(fixtures.ChainID)

return NewAuthHelper(privateKey, contractAddr, chainID)
return NewAuthHelperFromKey(privateKey, contractAddr, chainID)
}

func TestAuthHelper_SignCreateDataSet(t *testing.T) {
Expand Down Expand Up @@ -361,3 +361,63 @@ func TestAuthHelper_Address(t *testing.T) {
t.Errorf("Address() returned %s, expected %s", authHelper.Address().Hex(), expectedAddr.Hex())
}
}

// TestAuthHelper_SignDigestFunc verifies that NewAuthHelper accepts a
// SignDigestFunc and produces signatures that match the from-key path
// byte-for-byte (locks in the contract that the function-based
// constructor is just a thin wrapper).
func TestAuthHelper_SignDigestFunc(t *testing.T) {
privateKeyBytes, _ := hex.DecodeString(fixtures.PrivateKey)
privateKey, _ := crypto.ToECDSA(privateKeyBytes)
address := crypto.PubkeyToAddress(privateKey.PublicKey)
contractAddr := common.HexToAddress(fixtures.ContractAddress)
chainID := big.NewInt(fixtures.ChainID)

signFn := func(digest []byte) ([]byte, error) {
return crypto.Sign(digest, privateKey)
}

helperFromFn := NewAuthHelper(signFn, address, contractAddr, chainID)
helperFromKey := NewAuthHelperFromKey(privateKey, contractAddr, chainID)

clientDataSetID := big.NewInt(fixtures.Signatures.CreateDataSet.ClientDataSetID)
payee := common.HexToAddress(fixtures.Signatures.CreateDataSet.Payee)

sigA, err := helperFromFn.SignCreateDataSet(clientDataSetID, payee, fixtures.Signatures.CreateDataSet.Metadata)
if err != nil {
t.Fatalf("SignCreateDataSet (fn): %v", err)
}
sigB, err := helperFromKey.SignCreateDataSet(clientDataSetID, payee, fixtures.Signatures.CreateDataSet.Metadata)
if err != nil {
t.Fatalf("SignCreateDataSet (key): %v", err)
}

if hex.EncodeToString(sigA.Signature) != hex.EncodeToString(sigB.Signature) {
t.Errorf("SignDigestFunc and FromKey paths produced different signatures:\n fn: %x\n key: %x",
sigA.Signature, sigB.Signature)
}
if helperFromFn.Address() != address {
t.Errorf("Address mismatch: helper=%s want=%s", helperFromFn.Address().Hex(), address.Hex())
}
}

// TestAuthHelper_RejectsBadSignerOutput verifies the length check in
// signTypedData when the SignDigestFunc misbehaves.
func TestAuthHelper_RejectsBadSignerOutput(t *testing.T) {
contractAddr := common.HexToAddress(fixtures.ContractAddress)
chainID := big.NewInt(fixtures.ChainID)
dummyAddr := common.HexToAddress("0x1234567890123456789012345678901234567890")

badSignFn := func(digest []byte) ([]byte, error) {
return []byte{0x00, 0x01, 0x02}, nil // wrong length
}

helper := NewAuthHelper(badSignFn, dummyAddr, contractAddr, chainID)
_, err := helper.SignCreateDataSet(big.NewInt(1), dummyAddr, nil)
if err == nil {
t.Error("expected error from short signer output, got nil")
}
if !strings.Contains(err.Error(), "expected 65") {
t.Errorf("error did not mention expected length: %v", err)
}
}
2 changes: 1 addition & 1 deletion pdp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func testAuthHelper(t *testing.T) *AuthHelper {
contractAddr := common.HexToAddress("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f")
chainID := big.NewInt(31337)

return NewAuthHelper(privateKey, contractAddr, chainID)
return NewAuthHelperFromKey(privateKey, contractAddr, chainID)
}

func setupMockServer(t *testing.T, handler http.Handler) (*Server, *httptest.Server) {
Expand Down
14 changes: 12 additions & 2 deletions signer/secp256k1.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import (

blake2b "github.com/minio/blake2b-simd"

dcrdecdsa "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
dcrdsecp "github.com/decred/dcrd/dcrec/secp256k1/v4"
dcrdecdsa "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
)

// Secp256k1Signer implements EVMSigner backed by a secp256k1 private key.
// It can sign both Filecoin messages and Ethereum transactions.
type Secp256k1Signer struct {
// redundant with ecdsaKey.D but kept for simplicity; unexported, can be changed later
raw []byte // raw 32-byte scalar
raw []byte // raw 32-byte scalar
ecdsaKey *ecdsa.PrivateKey
filAddr address.Address
ethAddr common.Address
Expand Down Expand Up @@ -118,3 +118,13 @@ func (s *Secp256k1Signer) EVMAddress() common.Address {
func (s *Secp256k1Signer) Transactor(chainID *big.Int) (*bind.TransactOpts, error) {
return bind.NewKeyedTransactorWithChainID(s.ecdsaKey, chainID)
}

// SignDigest produces a 65-byte recoverable secp256k1 signature over the
// given 32-byte keccak digest. V is the recovery ID (0 or 1); callers
// requiring the historical Ethereum 27/28 form must add 27 themselves.
func (s *Secp256k1Signer) SignDigest(digest []byte) ([]byte, error) {
if len(digest) != 32 {
return nil, fmt.Errorf("digest must be 32 bytes, got %d", len(digest))
}
return ethcrypto.Sign(digest, s.ecdsaKey)
}
10 changes: 9 additions & 1 deletion signer/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ type Signer interface {
Sign(msg []byte) (*crypto.Signature, error)
}

// EVMSigner signs Ethereum/FEVM transactions. Only secp256k1 keys can do this.
// EVMSigner signs Ethereum/FEVM transactions and EIP-712 typed data.
// Only secp256k1 keys can do this.
//
// SignDigest produces a 65-byte recoverable secp256k1 signature over a
// 32-byte keccak digest, in [R || S || V] form with V = 0 or 1 (the
// go-ethereum crypto.Sign convention). Callers that need on-chain
// ECDSA recovery (e.g. PDP extraData) must normalize V to 27/28
// themselves; the digest-signer interface keeps the raw recovery ID.
type EVMSigner interface {
Signer
EVMAddress() common.Address
Transactor(chainID *big.Int) (*bind.TransactOpts, error)
SignDigest(digest []byte) ([]byte, error)
}
25 changes: 25 additions & 0 deletions signer/signer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,31 @@ func TestSecp256k1Signer_DualProtocol(t *testing.T) {
if opts.From != expectedEth {
t.Errorf("Transactor.From = %s, want %s", opts.From, expectedEth)
}

// SignDigest should produce a 65-byte signature recoverable to the same address
digest := ethcrypto.Keccak256([]byte("test digest input"))
sigBytes, err := s.SignDigest(digest)
if err != nil {
t.Fatalf("SignDigest: %v", err)
}
if len(sigBytes) != 65 {
t.Errorf("SignDigest length = %d, want 65", len(sigBytes))
}
if sigBytes[64] != 0 && sigBytes[64] != 1 {
t.Errorf("SignDigest V = %d, want 0 or 1", sigBytes[64])
}
recovered, err := ethcrypto.SigToPub(digest, sigBytes)
if err != nil {
t.Fatalf("SigToPub: %v", err)
}
if ethcrypto.PubkeyToAddress(*recovered) != expectedEth {
t.Errorf("recovered address %s != signer %s", ethcrypto.PubkeyToAddress(*recovered), expectedEth)
}

// SignDigest rejects non-32-byte input
if _, err := s.SignDigest([]byte("short")); err == nil {
t.Error("SignDigest should reject non-32-byte input")
}
}

func TestSecp256k1Signer_FromLotusExport(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions synapse.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func (c *Client) Storage() (*storage.Manager, error) {
return nil, fmt.Errorf("provider URL is required for storage operations")
}

authHelper := pdp.NewAuthHelper(c.privateKey, c.warmStorageAddress, big.NewInt(c.chainID))
authHelper := pdp.NewAuthHelperFromKey(c.privateKey, c.warmStorageAddress, big.NewInt(c.chainID))
pdpServer := pdp.NewServer(c.providerURL)

var opts []storage.ManagerOption
Expand Down Expand Up @@ -192,7 +192,7 @@ func (c *Client) Close() {
}

func (c *Client) NewAuthHelper() *pdp.AuthHelper {
return pdp.NewAuthHelper(c.privateKey, c.warmStorageAddress, big.NewInt(c.chainID))
return pdp.NewAuthHelperFromKey(c.privateKey, c.warmStorageAddress, big.NewInt(c.chainID))
}

func (c *Client) NewPDPServer(providerURL string) *pdp.Server {
Expand Down
Loading