Skip to content

Commit

Permalink
OCR3 keyring support (#11956)
Browse files Browse the repository at this point in the history
* OCR3 keyring support

- Needed for LLO
- Only includes EVM implementation

* Rename to ocr2plus key

* Revert "Rename to ocr2plus key"

This reverts commit 19a204cc81e0cda916293e0ba604834efb998ca4.

* Add test

* Fix lint
  • Loading branch information
samsondav authored Feb 12, 2024
1 parent 0e564cd commit 0001494
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 52 deletions.
9 changes: 9 additions & 0 deletions core/internal/testutils/testutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,3 +446,12 @@ func MustDecodeBase64(s string) (b []byte) {
func SkipFlakey(t *testing.T, ticketURL string) {
t.Skip("Flakey", ticketURL)
}

func MustRandBytes(n int) (b []byte) {
b = make([]byte, n)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
return
}
59 changes: 38 additions & 21 deletions core/services/keystore/keys/ocr2key/cosmos_keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/hdevalence/ed25519consensus"
"github.com/pkg/errors"
"github.com/smartcontractkit/libocr/offchainreporting2/types"
"github.com/smartcontractkit/libocr/offchainreporting2plus/chains/evmutil"
ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"
"golang.org/x/crypto/blake2s"
Expand All @@ -29,11 +30,11 @@ func newCosmosKeyring(material io.Reader) (*cosmosKeyring, error) {
return &cosmosKeyring{pubKey: pubKey, privKey: privKey}, nil
}

func (tk *cosmosKeyring) PublicKey() ocrtypes.OnchainPublicKey {
return []byte(tk.pubKey)
func (ckr *cosmosKeyring) PublicKey() ocrtypes.OnchainPublicKey {
return []byte(ckr.pubKey)
}

func (tk *cosmosKeyring) reportToSigData(reportCtx ocrtypes.ReportContext, report ocrtypes.Report) ([]byte, error) {
func (ckr *cosmosKeyring) reportToSigData(reportCtx ocrtypes.ReportContext, report ocrtypes.Report) ([]byte, error) {
rawReportContext := evmutil.RawReportContext(reportCtx)
h, err := blake2s.New256(nil)
if err != nil {
Expand All @@ -49,48 +50,64 @@ func (tk *cosmosKeyring) reportToSigData(reportCtx ocrtypes.ReportContext, repor
return h.Sum(nil), nil
}

func (tk *cosmosKeyring) Sign(reportCtx ocrtypes.ReportContext, report ocrtypes.Report) ([]byte, error) {
sigData, err := tk.reportToSigData(reportCtx, report)
func (ckr *cosmosKeyring) Sign(reportCtx ocrtypes.ReportContext, report ocrtypes.Report) ([]byte, error) {
sigData, err := ckr.reportToSigData(reportCtx, report)
if err != nil {
return nil, err
}
signedMsg := ed25519.Sign(tk.privKey, sigData)
return ckr.signBlob(sigData)
}

func (ckr *cosmosKeyring) Sign3(digest types.ConfigDigest, seqNr uint64, r ocrtypes.Report) (signature []byte, err error) {
return nil, errors.New("not implemented")
}

func (ckr *cosmosKeyring) signBlob(b []byte) ([]byte, error) {
signedMsg := ed25519.Sign(ckr.privKey, b)
// match on-chain parsing (first 32 bytes are for pubkey, remaining are for signature)
return utils.ConcatBytes(tk.PublicKey(), signedMsg), nil
return utils.ConcatBytes(ckr.PublicKey(), signedMsg), nil
}

func (ckr *cosmosKeyring) Verify(publicKey ocrtypes.OnchainPublicKey, reportCtx ocrtypes.ReportContext, report ocrtypes.Report, signature []byte) bool {
hash, err := ckr.reportToSigData(reportCtx, report)
if err != nil {
return false
}
return ckr.verifyBlob(publicKey, hash, signature)
}

func (ckr *cosmosKeyring) Verify3(publicKey ocrtypes.OnchainPublicKey, cd ocrtypes.ConfigDigest, seqNr uint64, r ocrtypes.Report, signature []byte) bool {
return false
}

func (tk *cosmosKeyring) Verify(publicKey ocrtypes.OnchainPublicKey, reportCtx ocrtypes.ReportContext, report ocrtypes.Report, signature []byte) bool {
func (ckr *cosmosKeyring) verifyBlob(pubkey ocrtypes.OnchainPublicKey, b, sig []byte) bool {
// Ed25519 signatures are always 64 bytes and the
// public key (always prefixed, see Sign above) is always,
// 32 bytes, so we always require the max signature length.
if len(signature) != tk.MaxSignatureLength() {
if len(sig) != ckr.MaxSignatureLength() {
return false
}
if len(publicKey) != ed25519.PublicKeySize {
return false
}
hash, err := tk.reportToSigData(reportCtx, report)
if err != nil {
if len(pubkey) != ed25519.PublicKeySize {
return false
}
return ed25519consensus.Verify(ed25519.PublicKey(publicKey), hash, signature[32:])
return ed25519consensus.Verify(ed25519.PublicKey(pubkey), b, sig[32:])
}

func (tk *cosmosKeyring) MaxSignatureLength() int {
func (ckr *cosmosKeyring) MaxSignatureLength() int {
// Reference: https://pkg.go.dev/crypto/ed25519
return ed25519.PublicKeySize + ed25519.SignatureSize // 32 + 64
}

func (tk *cosmosKeyring) Marshal() ([]byte, error) {
return tk.privKey.Seed(), nil
func (ckr *cosmosKeyring) Marshal() ([]byte, error) {
return ckr.privKey.Seed(), nil
}

func (tk *cosmosKeyring) Unmarshal(in []byte) error {
func (ckr *cosmosKeyring) Unmarshal(in []byte) error {
if len(in) != ed25519.SeedSize {
return errors.Errorf("unexpected seed size, got %d want %d", len(in), ed25519.SeedSize)
}
privKey := ed25519.NewKeyFromSeed(in)
tk.privKey = privKey
tk.pubKey = privKey.Public().(ed25519.PublicKey)
ckr.privKey = privKey
ckr.pubKey = privKey.Public().(ed25519.PublicKey)
return nil
}
68 changes: 52 additions & 16 deletions core/services/keystore/keys/ocr2key/evm_keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package ocr2key
import (
"bytes"
"crypto/ecdsa"
"encoding/binary"
"io"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/smartcontractkit/libocr/offchainreporting2/types"
"github.com/smartcontractkit/libocr/offchainreporting2plus/chains/evmutil"
ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"
)
Expand All @@ -26,12 +28,16 @@ func newEVMKeyring(material io.Reader) (*evmKeyring, error) {
}

// XXX: PublicKey returns the address of the public key not the public key itself
func (ok *evmKeyring) PublicKey() ocrtypes.OnchainPublicKey {
address := ok.signingAddress()
func (ekr *evmKeyring) PublicKey() ocrtypes.OnchainPublicKey {
address := ekr.signingAddress()
return address[:]
}

func (ok *evmKeyring) reportToSigData(reportCtx ocrtypes.ReportContext, report ocrtypes.Report) []byte {
func (ekr *evmKeyring) Sign(reportCtx ocrtypes.ReportContext, report ocrtypes.Report) ([]byte, error) {
return ekr.signBlob(ekr.reportToSigData(reportCtx, report))
}

func (ekr *evmKeyring) reportToSigData(reportCtx ocrtypes.ReportContext, report ocrtypes.Report) []byte {
rawReportContext := evmutil.RawReportContext(reportCtx)
sigData := crypto.Keccak256(report)
sigData = append(sigData, rawReportContext[0][:]...)
Expand All @@ -40,38 +46,68 @@ func (ok *evmKeyring) reportToSigData(reportCtx ocrtypes.ReportContext, report o
return crypto.Keccak256(sigData)
}

func (ok *evmKeyring) Sign(reportCtx ocrtypes.ReportContext, report ocrtypes.Report) ([]byte, error) {
return crypto.Sign(ok.reportToSigData(reportCtx, report), &ok.privateKey)
func (ekr *evmKeyring) Sign3(digest types.ConfigDigest, seqNr uint64, r ocrtypes.Report) (signature []byte, err error) {
return ekr.signBlob(ekr.reportToSigData3(digest, seqNr, r))
}

func (ekr *evmKeyring) reportToSigData3(digest types.ConfigDigest, seqNr uint64, r ocrtypes.Report) []byte {
rawReportContext := RawReportContext3(digest, seqNr)
sigData := crypto.Keccak256(r)
sigData = append(sigData, rawReportContext[0][:]...)
sigData = append(sigData, rawReportContext[1][:]...)
return crypto.Keccak256(sigData)
}

func RawReportContext3(digest types.ConfigDigest, seqNr uint64) [2][32]byte {
seqNrBytes := [32]byte{}
binary.BigEndian.PutUint64(seqNrBytes[:], seqNr)
return [2][32]byte{
digest,
seqNrBytes,
}
}

func (ekr *evmKeyring) signBlob(b []byte) (sig []byte, err error) {
return crypto.Sign(b, &ekr.privateKey)
}

func (ekr *evmKeyring) Verify(publicKey ocrtypes.OnchainPublicKey, reportCtx ocrtypes.ReportContext, report ocrtypes.Report, signature []byte) bool {
hash := ekr.reportToSigData(reportCtx, report)
return ekr.verifyBlob(publicKey, hash, signature)
}

func (ekr *evmKeyring) Verify3(publicKey ocrtypes.OnchainPublicKey, cd ocrtypes.ConfigDigest, seqNr uint64, r ocrtypes.Report, signature []byte) bool {
hash := ekr.reportToSigData3(cd, seqNr, r)
return ekr.verifyBlob(publicKey, hash, signature)
}

func (ok *evmKeyring) Verify(publicKey ocrtypes.OnchainPublicKey, reportCtx ocrtypes.ReportContext, report ocrtypes.Report, signature []byte) bool {
hash := ok.reportToSigData(reportCtx, report)
authorPubkey, err := crypto.SigToPub(hash, signature)
func (ekr *evmKeyring) verifyBlob(pubkey types.OnchainPublicKey, b, sig []byte) bool {
authorPubkey, err := crypto.SigToPub(b, sig)
if err != nil {
return false
}
authorAddress := crypto.PubkeyToAddress(*authorPubkey)
return bytes.Equal(publicKey[:], authorAddress[:])
// no need for constant time compare since neither arg is sensitive
return bytes.Equal(pubkey[:], authorAddress[:])
}

func (ok *evmKeyring) MaxSignatureLength() int {
func (ekr *evmKeyring) MaxSignatureLength() int {
return 65
}

func (ok *evmKeyring) signingAddress() common.Address {
return crypto.PubkeyToAddress(*(&ok.privateKey).Public().(*ecdsa.PublicKey))
func (ekr *evmKeyring) signingAddress() common.Address {
return crypto.PubkeyToAddress(*(&ekr.privateKey).Public().(*ecdsa.PublicKey))
}

func (ok *evmKeyring) Marshal() ([]byte, error) {
return crypto.FromECDSA(&ok.privateKey), nil
func (ekr *evmKeyring) Marshal() ([]byte, error) {
return crypto.FromECDSA(&ekr.privateKey), nil
}

func (ok *evmKeyring) Unmarshal(in []byte) error {
func (ekr *evmKeyring) Unmarshal(in []byte) error {
privateKey, err := crypto.ToECDSA(in)
if err != nil {
return err
}
ok.privateKey = *privateKey
ekr.privateKey = *privateKey
return nil
}
36 changes: 36 additions & 0 deletions core/services/keystore/keys/ocr2key/evm_keyring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package ocr2key
import (
"bytes"
cryptorand "crypto/rand"
"math/rand"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/libocr/offchainreporting2/types"
ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"

"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
)

func TestEVMKeyring_SignVerify(t *testing.T) {
Expand Down Expand Up @@ -43,6 +47,38 @@ func TestEVMKeyring_SignVerify(t *testing.T) {
})
}

func TestEVMKeyring_Sign3Verify3(t *testing.T) {
kr1, err := newEVMKeyring(cryptorand.Reader)
require.NoError(t, err)
kr2, err := newEVMKeyring(cryptorand.Reader)
require.NoError(t, err)

digest, err := types.BytesToConfigDigest(testutils.MustRandBytes(32))
require.NoError(t, err)
seqNr := rand.Uint64()
r := ocrtypes.Report(testutils.MustRandBytes(rand.Intn(1024)))

t.Run("can verify", func(t *testing.T) {
sig, err := kr1.Sign3(digest, seqNr, r)
require.NoError(t, err)
t.Log(len(sig))
result := kr2.Verify3(kr1.PublicKey(), digest, seqNr, r, sig)
assert.True(t, result)
})

t.Run("invalid sig", func(t *testing.T) {
result := kr2.Verify3(kr1.PublicKey(), digest, seqNr, r, []byte{0x01})
assert.False(t, result)
})

t.Run("invalid pubkey", func(t *testing.T) {
sig, err := kr1.Sign3(digest, seqNr, r)
require.NoError(t, err)
result := kr2.Verify3([]byte{0x01}, digest, seqNr, r, sig)
assert.False(t, result)
})
}

func TestEVMKeyring_Marshalling(t *testing.T) {
kr1, err := newEVMKeyring(cryptorand.Reader)
require.NoError(t, err)
Expand Down
9 changes: 9 additions & 0 deletions core/services/keystore/keys/ocr2key/generic_key_bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
type (
keyring interface {
ocrtypes.OnchainKeyring
OCR3SignerVerifier
Marshal() ([]byte, error)
Unmarshal(in []byte) error
}
Expand Down Expand Up @@ -92,10 +93,18 @@ func (kb *keyBundle[K]) Sign(reportCtx ocrtypes.ReportContext, report ocrtypes.R
return kb.keyring.Sign(reportCtx, report)
}

func (kb *keyBundle[K]) Sign3(digest ocrtypes.ConfigDigest, seqNr uint64, r ocrtypes.Report) (signature []byte, err error) {
return kb.keyring.Sign3(digest, seqNr, r)
}

func (kb *keyBundle[K]) Verify(publicKey ocrtypes.OnchainPublicKey, reportCtx ocrtypes.ReportContext, report ocrtypes.Report, signature []byte) bool {
return kb.keyring.Verify(publicKey, reportCtx, report, signature)
}

func (kb *keyBundle[K]) Verify3(publicKey ocrtypes.OnchainPublicKey, cd ocrtypes.ConfigDigest, seqNr uint64, r ocrtypes.Report, signature []byte) bool {
return kb.keyring.Verify3(publicKey, cd, seqNr, r, signature)
}

// OnChainPublicKey returns public component of the keypair used on chain
func (kb *keyBundle[K]) OnChainPublicKey() string {
return hex.EncodeToString(kb.keyring.PublicKey())
Expand Down
8 changes: 8 additions & 0 deletions core/services/keystore/keys/ocr2key/key_bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ import (
"github.com/smartcontractkit/chainlink/v2/core/store/models"
)

type OCR3SignerVerifier interface {
Sign3(digest ocrtypes.ConfigDigest, seqNr uint64, r ocrtypes.Report) (signature []byte, err error)
Verify3(publicKey ocrtypes.OnchainPublicKey, cd ocrtypes.ConfigDigest, seqNr uint64, r ocrtypes.Report, signature []byte) bool
}

// nolint
type KeyBundle interface {
// OnchainKeyring is used for signing reports (groups of observations, verified onchain)
ocrtypes.OnchainKeyring
// OffchainKeyring is used for signing observations
ocrtypes.OffchainKeyring

OCR3SignerVerifier

ID() string
ChainType() chaintype.ChainType
Marshal() ([]byte, error)
Expand Down
Loading

0 comments on commit 0001494

Please sign in to comment.