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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
17 changes: 17 additions & 0 deletions pkg/types/core/keystore.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package core

import (
"bytes"
"context"
"crypto"
"crypto/ed25519"
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -133,13 +138,25 @@ 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)
}
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))
}

Expand Down
26 changes: 26 additions & 0 deletions pkg/types/core/keystore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading