diff --git a/go.mod b/go.mod index c688b6a37..74bece985 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20250822025801-598d3d86f873 github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 - github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358 + github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d github.com/stretchr/testify v1.10.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 go.opentelemetry.io/otel v1.37.0 diff --git a/go.sum b/go.sum index dc8fe4ba1..5b0d9c738 100644 --- a/go.sum +++ b/go.sum @@ -342,8 +342,8 @@ github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9 github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= -github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358 h1:+NVzR5LZVazRUunzVn34u+lwnpmn6NTVPCeZOVyQHLo= -github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358/go.mod h1:Acy3BTBxou83ooMESLO90s8PKSu7RvLCzwSTbxxfOK0= +github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d h1:LokA9PoCNb8mm8mDT52c3RECPMRsGz1eCQORq+J3n74= +github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d/go.mod h1:Acy3BTBxou83ooMESLO90s8PKSu7RvLCzwSTbxxfOK0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= diff --git a/pkg/types/core/keystore.go b/pkg/types/core/keystore.go index b6c0f1e77..b329e1987 100644 --- a/pkg/types/core/keystore.go +++ b/pkg/types/core/keystore.go @@ -1,6 +1,7 @@ package core import ( + "bytes" "context" "crypto" "crypto/ed25519" @@ -14,6 +15,7 @@ import ( "google.golang.org/grpc/status" "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/libocr/ragep2p/peeridhelper" ) // Implementations of this interface should embed the UnimplementedKeystore struct, @@ -79,6 +81,9 @@ func (s *Ed25519Signer) Sign(r io.Reader, digest []byte, opts crypto.SignerOpts) } var P2PAccountKey = "P2P_SIGNER" + +// Notice: when using the `StandardCapabilityAccount`, signing payloads must be prefixed using the +// `peeridhelper.MakePeerIDSignatureDomainSeparatedPayload` function. var StandardCapabilityAccount = "STANDARD_CAPABILITY_ACCOUNT" // singleAccountSigner implements Keystore for a single account. @@ -133,6 +138,8 @@ func (c *signerDecrypter) Accounts(ctx context.Context) ([]string, error) { return []string{c.account}, nil } +var genericPrefix = peeridhelper.MakePeerIDSignatureDomainSeparatedPayload("", []byte{}) + func (c *signerDecrypter) Sign(ctx context.Context, account string, data []byte) (signed []byte, err error) { if c.account != account { return nil, fmt.Errorf("account not found: %s", account) @@ -140,6 +147,16 @@ func (c *signerDecrypter) Sign(ctx context.Context, account string, data []byte) if c.signer == nil { return nil, fmt.Errorf("signer is nil") } + + // For the `StandardCapabilityAccount`, assert that the user is passing in correctly domain separated data. + // The first 97 bytes of any domain separated payload will match the generic prefix. + // Implicitly, this also requires the message length to be <= 1024 bytes. + if account == StandardCapabilityAccount { + if !bytes.HasPrefix(data, genericPrefix[:97]) { + return nil, fmt.Errorf("data does not have expected prefix") + } + } + return c.signer.Sign(rand.Reader, data, crypto.Hash(0)) } diff --git a/pkg/types/core/keystore_test.go b/pkg/types/core/keystore_test.go index 2f723a854..13931b707 100644 --- a/pkg/types/core/keystore_test.go +++ b/pkg/types/core/keystore_test.go @@ -14,6 +14,7 @@ import ( "golang.org/x/crypto/nacl/box" "github.com/smartcontractkit/chainlink-common/pkg/types/core" + "github.com/smartcontractkit/libocr/ragep2p/peeridhelper" ) // mockSigner implements crypto.Signer for testing @@ -216,6 +217,31 @@ func TestSingleAccountSignerDecrypter(t *testing.T) { assert.True(t, valid, "signature should be valid") }) + t.Run("real ed25519 keys integration with StandardCapabilityAccount", func(t *testing.T) { + privKey := ed25519.NewKeyFromSeed([]byte("test_seed_that_is_32_bytes_long!")) + account := core.StandardCapabilityAccount + singleSigner, err := core.NewSignerDecrypter(account, privKey, nil) + require.NoError(t, err) + + accounts, err := singleSigner.Accounts(t.Context()) + require.NoError(t, err) + assert.Equal(t, []string{account}, accounts) + + ctx := context.Background() + testData := []byte("integration test data") + prefixedTestData := peeridhelper.MakePeerIDSignatureDomainSeparatedPayload("test", testData) + + signature1, err := singleSigner.Sign(ctx, account, prefixedTestData) + require.NoError(t, err) + assert.NotEmpty(t, signature1) + + valid := ed25519.Verify(privKey.Public().(ed25519.PublicKey), prefixedTestData, signature1) + assert.True(t, valid, "signature should be valid") + + _, err = singleSigner.Sign(ctx, account, []byte("foobar")) + require.Equal(t, err.Error(), "data does not have expected prefix") + }) + t.Run("real nacl/box keys decrypt integration", func(t *testing.T) { pubKey, privKey, err := box.GenerateKey(rand.Reader) require.NoError(t, err)