Skip to content

Commit

Permalink
[secp256k1] Add ability to recover public key from signature
Browse files Browse the repository at this point in the history
  • Loading branch information
gregnazario committed Nov 25, 2024
1 parent 5606ce9 commit aef2eee
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 45 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ adheres to the format set out by [Keep a Changelog](https://keepachangelog.com/e

- Add AIP-80 support for Ed25519 and Secp256k1 private keys
- Add support for optional ledger version in FA functions and APT balance
- [`Breaking`] Change from the `go-ethereum/crypto` to `decred/dcrd` for secp256k1 signing
- [`Breaking`] Add checks for malleability to prevent duplicate secp256k1 signatures in verification and to ensure
correct on-chain behavior
- Adds functionality to recover public keys from secp256k1 signatures

# v1.2.0 (11/15/2024)

Expand Down
126 changes: 93 additions & 33 deletions crypto/secp256k1.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package crypto

import (
"crypto/ecdsa"
"fmt"

"github.com/aptos-labs/aptos-go-sdk/bcs"
"github.com/aptos-labs/aptos-go-sdk/internal/util"
ethCrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
)

//region Secp256k1PrivateKey
Expand All @@ -18,7 +18,7 @@ const Secp256k1PrivateKeyLength = 32
const Secp256k1PublicKeyLength = 65

// Secp256k1SignatureLength is the [Secp256k1Signature] length in bytes. It is a signature without the recovery bit.
const Secp256k1SignatureLength = ethCrypto.SignatureLength - 1
const Secp256k1SignatureLength = 64

// Secp256k1PrivateKey is a private key that can be used with [SingleSigner]. It cannot stand on its own.
//
Expand All @@ -29,12 +29,12 @@ const Secp256k1SignatureLength = ethCrypto.SignatureLength - 1
// - [bcs.Unmarshaler]
// - [bcs.Struct]
type Secp256k1PrivateKey struct {
Inner *ecdsa.PrivateKey // Inner is the actual private key
Inner *secp256k1.PrivateKey // Inner is the actual private key
}

// GenerateSecp256k1Key generates a new [Secp256k1PrivateKey]
func GenerateSecp256k1Key() (*Secp256k1PrivateKey, error) {
priv, err := ethCrypto.GenerateKey()
priv, err := secp256k1.GeneratePrivateKey()
if err != nil {
return nil, err
}
Expand All @@ -50,7 +50,7 @@ func GenerateSecp256k1Key() (*Secp256k1PrivateKey, error) {
// - [MessageSigner]
func (key *Secp256k1PrivateKey) VerifyingKey() VerifyingKey {
return &Secp256k1PublicKey{
&key.Inner.PublicKey,
key.Inner.PubKey(),
}
}

Expand All @@ -59,7 +59,7 @@ func (key *Secp256k1PrivateKey) VerifyingKey() VerifyingKey {
// Implements:
// - [MessageSigner]
func (key *Secp256k1PrivateKey) EmptySignature() Signature {
return &Secp256k1Signature{}
return &Secp256k1Signature{Inner: ecdsa.NewSignature(&secp256k1.ModNScalar{}, &secp256k1.ModNScalar{})}
}

// SignMessage signs a message and returns the raw [Signature] without a [PublicKey] for verification
Expand All @@ -68,16 +68,8 @@ func (key *Secp256k1PrivateKey) EmptySignature() Signature {
// - [MessageSigner]
func (key *Secp256k1PrivateKey) SignMessage(msg []byte) (sig Signature, err error) {
hash := util.Sha3256Hash([][]byte{msg})
// TODO: The eth library doesn't protect against malleability issues, so we need to handle those.
signature, err := ethCrypto.Sign(hash, key.Inner)
if err != nil {
return nil, err
}

// Strip the recovery bit
secpSig := &Secp256k1Signature{}
copy(secpSig.Inner[:], signature[:Secp256k1SignatureLength])
return secpSig, nil
signature := ecdsa.Sign(key.Inner, hash)
return &Secp256k1Signature{signature}, nil
}

//endregion
Expand All @@ -89,7 +81,7 @@ func (key *Secp256k1PrivateKey) SignMessage(msg []byte) (sig Signature, err erro
// Implements:
// - [CryptoMaterial]
func (key *Secp256k1PrivateKey) Bytes() []byte {
return ethCrypto.FromECDSA(key.Inner)
return key.Inner.Serialize()
}

// FromBytes populates the [Secp256k1PrivateKey] from bytes
Expand All @@ -106,11 +98,7 @@ func (key *Secp256k1PrivateKey) FromBytes(bytes []byte) (err error) {
if len(bytes) != Secp256k1PrivateKeyLength {
return fmt.Errorf("invalid secp256k1 private key size %d", len(bytes))
}
newKey, err := ethCrypto.ToECDSA(bytes)
if err != nil {
return err
}
key.Inner = newKey
key.Inner = secp256k1.PrivKeyFromBytes(bytes)
return nil
}

Expand Down Expand Up @@ -158,7 +146,7 @@ func (key *Secp256k1PrivateKey) FromHex(hexStr string) (err error) {
// - [bcs.Unmarshaler]
// - [bcs.Struct]
type Secp256k1PublicKey struct {
Inner *ecdsa.PublicKey // Inner is the actual public key
Inner *secp256k1.PublicKey // Inner is the actual public key
}

//region Secp256k1PublicKey VerifyingKey
Expand All @@ -173,8 +161,8 @@ func (key *Secp256k1PublicKey) Verify(msg []byte, sig Signature) bool {
switch sig := sig.(type) {
case *Secp256k1Signature:
// Verification requires to pass the SHA-256 hash of the message
msg = util.Sha3256Hash([][]byte{msg})
return ethCrypto.VerifySignature(key.Bytes(), msg, sig.Bytes())
hash := util.Sha3256Hash([][]byte{msg})
return sig.Inner.Verify(hash, key.Inner)
default:
return false
}
Expand All @@ -189,15 +177,15 @@ func (key *Secp256k1PublicKey) Verify(msg []byte, sig Signature) bool {
// Implements:
// - [CryptoMaterial]
func (key *Secp256k1PublicKey) Bytes() []byte {
return ethCrypto.FromECDSAPub(key.Inner)
return key.Inner.SerializeUncompressed()
}

// FromBytes sets the [Secp256k1PublicKey] to the given bytes
//
// Implements:
// - [CryptoMaterial]
func (key *Secp256k1PublicKey) FromBytes(bytes []byte) (err error) {
newKey, err := ethCrypto.UnmarshalPubkey(bytes)
newKey, err := secp256k1.ParsePubKey(bytes)
if err != nil {
return err
}
Expand Down Expand Up @@ -243,7 +231,8 @@ func (key *Secp256k1PublicKey) MarshalBCS(ser *bcs.Serializer) {
// - [bcs.Unmarshaler]
func (key *Secp256k1PublicKey) UnmarshalBCS(des *bcs.Deserializer) {
kb := des.ReadBytes()
pubKey, err := ethCrypto.UnmarshalPubkey(kb)
pubKey, err := secp256k1.ParsePubKey(kb)

if err != nil {
des.SetError(err)
return
Expand Down Expand Up @@ -336,17 +325,71 @@ func (ea *Secp256k1Authenticator) UnmarshalBCS(des *bcs.Deserializer) {
// - [bcs.Unmarshaler]
// - [bcs.Struct]
type Secp256k1Signature struct {
Inner [Secp256k1SignatureLength]byte // Inner is the actual signature
Inner *ecdsa.Signature // Inner is the actual signature
}

// RecoverPublicKey recovers the public key from the signature and message
//
// If you know the recovery bit (0-4), please provide it, otherwise, use [RecoverSecp256k1PublicKeyWithAuthenticationKey]
//
// Note that this only applies to an [Secp256k1Signature], all other signatures are not recoverable
func (e *Secp256k1Signature) RecoverPublicKey(message []byte, recoveryBit byte) (pubKey *Secp256k1PublicKey, err error) {
hash := util.Sha3256Hash([][]byte{message})
return e.recoverSecp256k1PublicKey(hash, recoveryBit)
}

// RecoverSecp256k1PublicKeyWithAuthenticationKey recovers the public key from the signature and message, and checks if it matches the authentication key
//
// Note that, the authentication key may be an address, but if the authentication key was rotated it will differ from the address
func (e *Secp256k1Signature) RecoverSecp256k1PublicKeyWithAuthenticationKey(message []byte, authKey *AuthenticationKey) (pubKey *Secp256k1PublicKey, err error) {
hash := util.Sha3256Hash([][]byte{message})

for i := byte(0); i < byte(4); i++ {
key, err := e.recoverSecp256k1PublicKey(hash, i)
if err != nil {
continue
}

// Check if the public key matches the authentication key
anyPubKey, err := ToAnyPublicKey(key)
if err != nil {
continue
}

if *anyPubKey.AuthKey() == *authKey {
return key, nil
}
}

return nil, fmt.Errorf("unable to recover public key from signature")
}

// / recoverSecp256k1PublicKey recovers the public key from the signature and message by building up the magic byte
func (e *Secp256k1Signature) recoverSecp256k1PublicKey(messageHash []byte, recoveryBit byte) (pubKey *Secp256k1PublicKey, err error) {
// Append magic 27 because of bitcoin, and the recovery byte in front
sigWithRecovery := append([]byte{byte(recoveryBit) + 27}, e.Bytes()...)
publicKey, _, err := ecdsa.RecoverCompact(sigWithRecovery, messageHash)
if err != nil {
return nil, err
}

return &Secp256k1PublicKey{Inner: publicKey}, nil
}

//region Secp256k1Signature CryptoMaterial

// Bytes returns the raw bytes of the [Secp256k1Signature]
// Bytes returns the raw bytes of the [Secp256k1Signature] without a recovery bit.
// It's used for signing and verification.
//
// Implements:
// - [CryptoMaterial]
func (e *Secp256k1Signature) Bytes() []byte {
return e.Inner[:]
r := e.Inner.R()
s := e.Inner.S()
rBytes := r.Bytes()
sBytes := s.Bytes()
// Strips the recovery bit
return append(rBytes[:], sBytes[:]...)
}

// FromBytes sets the [Secp256k1Signature] to the given bytes
Expand All @@ -359,7 +402,24 @@ func (e *Secp256k1Signature) FromBytes(bytes []byte) (err error) {
if len(bytes) != Secp256k1SignatureLength {
return fmt.Errorf("invalid secp256k1 signature size %d, expected %d", len(bytes), Secp256k1SignatureLength)
}
copy(e.Inner[:], bytes)
var rBytes [32]byte
copy(rBytes[:], bytes[0:32])
var sBytes [32]byte
copy(sBytes[:], bytes[32:64])

r := &secp256k1.ModNScalar{}
r.SetBytes(&rBytes)
s := &secp256k1.ModNScalar{}
s.SetBytes(&sBytes)

signature := ecdsa.NewSignature(r, s)

// Checks order of s to be low
sTyped := signature.S()
if sTyped.IsOverHalfOrder() {
return fmt.Errorf("invalid secp256k1 signature: s is over half order")
}
e.Inner = signature
return nil
}

Expand Down
77 changes: 77 additions & 0 deletions crypto/secp256k1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,80 @@ func TestGenerateSecp256k1Key(t *testing.T) {

assert.True(t, privateKey.VerifyingKey().Verify(msg, sig))
}

func TestSecp256k1Signature_RecoverPublicKey(t *testing.T) {
privateKey := &Secp256k1PrivateKey{}
err := privateKey.FromHex(testSecp256k1PrivateKey)
assert.NoError(t, err)
message := []byte("hello")

signature, err := privateKey.SignMessage(message)
assert.NoError(t, err)

// Recover the public key
recoveredKey, err := signature.(*Secp256k1Signature).RecoverPublicKey(message, 1)
assert.NoError(t, err)

// Verify the signature with the key
assert.True(t, recoveredKey.Verify(message, signature))
assert.Equal(t, privateKey.VerifyingKey().ToHex(), recoveredKey.ToHex())

// Also try with the recovery bit attached to the signature
anyPubKey, err := ToAnyPublicKey(privateKey.VerifyingKey())
assert.NoError(t, err)
recoveredKey2, err := signature.(*Secp256k1Signature).RecoverSecp256k1PublicKeyWithAuthenticationKey(message, anyPubKey.AuthKey())
assert.NoError(t, err)
assert.Equal(t, privateKey.VerifyingKey().ToHex(), recoveredKey2.ToHex())
}

func TestSecp256k1Signature_RecoverPublicKeyFromSignature(t *testing.T) {
privateKey := &Secp256k1PrivateKey{}
err := privateKey.FromHex(testSecp256k1PrivateKey)
assert.NoError(t, err)
publicKey := &Secp256k1PublicKey{}
err = publicKey.FromHex(testSecp256k1PublicKey)
assert.NoError(t, err)
message, err := util.ParseHex(testSecp256k1MessageEncoded)
assert.NoError(t, err)

assert.Equal(t, publicKey.ToHex(), privateKey.VerifyingKey().ToHex())

signature := &Secp256k1Signature{}
err = signature.FromHex(testSecp256k1Signature)
assert.NoError(t, err)

// Recover the public key
recoveryBit := byte(0)
recoveredKey, err := signature.RecoverPublicKey(message, recoveryBit)
assert.NoError(t, err)

// Verify the signature with the key
assert.True(t, recoveredKey.Verify(message, signature))
assert.Equal(t, publicKey.ToHex(), recoveredKey.ToHex())
}

func TestSecp256k1Signature_RecoverPublicKeyFromSignatureWithRecoveryBit(t *testing.T) {
privateKey := &Secp256k1PrivateKey{}
err := privateKey.FromHex(testSecp256k1PrivateKey)
assert.NoError(t, err)
publicKey := &Secp256k1PublicKey{}
err = publicKey.FromHex(testSecp256k1PublicKey)
assert.NoError(t, err)
message, err := util.ParseHex(testSecp256k1MessageEncoded)
assert.NoError(t, err)

assert.Equal(t, publicKey.ToHex(), privateKey.VerifyingKey().ToHex())

signature := &Secp256k1Signature{}
err = signature.FromHex(testSecp256k1Signature)
assert.NoError(t, err)

// Recover the public key
recoveryBit := byte(0)
recoveredKey, err := signature.RecoverPublicKey(message, recoveryBit)
assert.NoError(t, err)

// Verify the signature with the key
assert.True(t, recoveredKey.Verify(message, signature))
assert.Equal(t, publicKey.ToHex(), recoveredKey.ToHex())
}
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.22

require (
github.com/cucumber/godog v0.14.1
github.com/ethereum/go-ethereum v1.14.5
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/hasura/go-graphql-client v0.12.1
github.com/hdevalence/ed25519consensus v0.2.0
github.com/stretchr/testify v1.9.0
Expand All @@ -13,17 +13,15 @@ require (

require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/gofrs/uuid v4.3.1+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.4 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
Expand Down
Loading

0 comments on commit aef2eee

Please sign in to comment.