diff --git a/keystore/encryptor_test.go b/keystore/encryptor_test.go index 775bb270a..78db216f7 100644 --- a/keystore/encryptor_test.go +++ b/keystore/encryptor_test.go @@ -28,7 +28,7 @@ func TestEncryptDecrypt(t *testing.T) { { name: "Non-existent encrypt key", remoteKeyType: "blah", - remotePubKey: th.KeysByType()[keystore.X25519][0].publicKey, + remotePubKey: th.KeysByType()[keystore.X25519][0].PublicKey, decryptKey: th.KeyName(keystore.X25519, 0), payload: []byte("hello world"), expectedEncryptError: keystore.ErrEncryptionFailed, @@ -36,21 +36,21 @@ func TestEncryptDecrypt(t *testing.T) { { name: "Empty payload x25519", remoteKeyType: keystore.X25519, - remotePubKey: th.KeysByType()[keystore.X25519][0].publicKey, + remotePubKey: th.KeysByType()[keystore.X25519][0].PublicKey, decryptKey: th.KeyName(keystore.X25519, 0), payload: []byte{}, }, { name: "Empty payload ecdh p256", remoteKeyType: keystore.ECDH_P256, - remotePubKey: th.KeysByType()[keystore.ECDH_P256][0].publicKey, + remotePubKey: th.KeysByType()[keystore.ECDH_P256][0].PublicKey, decryptKey: th.KeyName(keystore.ECDH_P256, 0), payload: []byte{}, }, { name: "Non-existent decrypt key", remoteKeyType: keystore.X25519, - remotePubKey: th.KeysByType()[keystore.X25519][0].publicKey, + remotePubKey: th.KeysByType()[keystore.X25519][0].PublicKey, decryptKey: "blah", payload: []byte("hello world"), expectedDecryptError: keystore.ErrDecryptionFailed, @@ -58,14 +58,14 @@ func TestEncryptDecrypt(t *testing.T) { { name: "Max payload", remoteKeyType: keystore.X25519, - remotePubKey: th.KeysByType()[keystore.X25519][0].publicKey, + remotePubKey: th.KeysByType()[keystore.X25519][0].PublicKey, decryptKey: th.KeyName(keystore.X25519, 0), payload: make([]byte, keystore.MaxEncryptionPayloadSize), }, { name: "Payload too large", remoteKeyType: keystore.X25519, - remotePubKey: th.KeysByType()[keystore.X25519][0].publicKey, + remotePubKey: th.KeysByType()[keystore.X25519][0].PublicKey, decryptKey: th.KeyName(keystore.X25519, 0), payload: make([]byte, keystore.MaxEncryptionPayloadSize+1), expectedEncryptError: keystore.ErrEncryptionFailed, @@ -74,7 +74,7 @@ func TestEncryptDecrypt(t *testing.T) { for encName, encKey := range th.KeysByName() { testName := fmt.Sprintf("Encrypt to %s", encName) var expectedEncryptError error - if encKey.keyType.IsEncryptionKeyType() { + if encKey.KeyType.IsEncryptionKeyType() { // Same key types should succeed expectedEncryptError = nil } else { @@ -84,8 +84,8 @@ func TestEncryptDecrypt(t *testing.T) { tt = append(tt, testCase{ name: testName, - remoteKeyType: encKey.keyType, - remotePubKey: encKey.publicKey, + remoteKeyType: encKey.KeyType, + remotePubKey: encKey.PublicKey, decryptKey: encName, expectedEncryptError: expectedEncryptError, payload: []byte("hello world"), @@ -157,7 +157,7 @@ func TestEncryptDecrypt_SharedSecret(t *testing.T) { t.Run(tt.name, func(t *testing.T) { _, err := th.Keystore.DeriveSharedSecret(ctx, keystore.DeriveSharedSecretRequest{ KeyName: tt.keyName, - RemotePubKey: th.KeysByType()[tt.keyType][0].publicKey, + RemotePubKey: th.KeysByType()[tt.keyType][0].PublicKey, }) if tt.expectedError != nil { require.Error(t, err) @@ -203,7 +203,7 @@ func FuzzEncryptDecryptRoundtrip(f *testing.F) { // Encrypt data using sender key to receiver's public key encryptResp, err := th.Keystore.Encrypt(ctx, keystore.EncryptRequest{ RemoteKeyType: keyType, - RemotePubKey: th.KeysByType()[keyType][1].publicKey, // receiver's public key + RemotePubKey: th.KeysByType()[keyType][1].PublicKey, // receiver's public key Data: data, }) require.NoError(t, err, "Encryption should succeed for keyType %s with data length %d", keyType, len(data)) diff --git a/keystore/helpers_test.go b/keystore/helpers_test.go index 99c2b94ec..f67c73339 100644 --- a/keystore/helpers_test.go +++ b/keystore/helpers_test.go @@ -11,8 +11,8 @@ import ( ) type Key struct { - keyType keystore.KeyType - publicKey []byte + KeyType keystore.KeyType + PublicKey []byte } type KeystoreTH struct { @@ -66,10 +66,10 @@ func (th *KeystoreTH) CreateTestKeys(t *testing.T) { }, }) require.NoError(t, err) - th.keysByName[keys.Keys[0].KeyInfo.Name] = Key{keyType: keys.Keys[0].KeyInfo.KeyType, publicKey: keys.Keys[0].KeyInfo.PublicKey} - th.keysByType[keyType] = append(th.keysByType[keyType], Key{keyType: keys.Keys[0].KeyInfo.KeyType, publicKey: keys.Keys[0].KeyInfo.PublicKey}) + th.keysByName[keys.Keys[0].KeyInfo.Name] = Key{KeyType: keys.Keys[0].KeyInfo.KeyType, PublicKey: keys.Keys[0].KeyInfo.PublicKey} + th.keysByType[keyType] = append(th.keysByType[keyType], Key{KeyType: keys.Keys[0].KeyInfo.KeyType, PublicKey: keys.Keys[0].KeyInfo.PublicKey}) - th.keysByName[keys.Keys[1].KeyInfo.Name] = Key{keyType: keys.Keys[1].KeyInfo.KeyType, publicKey: keys.Keys[1].KeyInfo.PublicKey} - th.keysByType[keyType] = append(th.keysByType[keyType], Key{keyType: keys.Keys[1].KeyInfo.KeyType, publicKey: keys.Keys[1].KeyInfo.PublicKey}) + th.keysByName[keys.Keys[1].KeyInfo.Name] = Key{KeyType: keys.Keys[1].KeyInfo.KeyType, PublicKey: keys.Keys[1].KeyInfo.PublicKey} + th.keysByType[keyType] = append(th.keysByType[keyType], Key{KeyType: keys.Keys[1].KeyInfo.KeyType, PublicKey: keys.Keys[1].KeyInfo.PublicKey}) } } diff --git a/keystore/signer.go b/keystore/signer.go index 5969d37d2..948e9f877 100644 --- a/keystore/signer.go +++ b/keystore/signer.go @@ -2,7 +2,17 @@ package keystore import ( "context" + "crypto/ed25519" + "errors" "fmt" + + gethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/smartcontractkit/chainlink-common/keystore/internal" +) + +var ( + ErrInvalidSignRequest = errors.New("invalid sign request") + ErrInvalidVerifyRequest = errors.New("invalid verify request") ) type SignRequest struct { @@ -15,7 +25,8 @@ type SignResponse struct { } type VerifyRequest struct { - KeyName string + KeyType KeyType + PublicKey []byte Data []byte Signature []byte } @@ -40,11 +51,74 @@ func (UnimplementedSigner) Verify(ctx context.Context, req VerifyRequest) (Verif return VerifyResponse{}, fmt.Errorf("Signer.Verify: %w", ErrUnimplemented) } -// TODO: Signer implementation. func (k *keystore) Sign(ctx context.Context, req SignRequest) (SignResponse, error) { - return SignResponse{}, nil + k.mu.RLock() + defer k.mu.RUnlock() + + key, ok := k.keystore[req.KeyName] + if !ok { + return SignResponse{}, fmt.Errorf("%s: %w", req.KeyName, ErrKeyNotFound) + } + switch key.keyType { + case Ed25519: + privateKey := ed25519.PrivateKey(internal.Bytes(key.privateKey)) + signature := ed25519.Sign(privateKey, req.Data) + return SignResponse{ + Signature: signature, + }, nil + case ECDSA_S256: + if len(req.Data) != 32 { + return SignResponse{}, fmt.Errorf("data must be 32 bytes for ECDSA_S256, got %d: %w", len(req.Data), ErrInvalidSignRequest) + } + privateKey, err := gethcrypto.ToECDSA(internal.Bytes(key.privateKey)) + if err != nil { + return SignResponse{}, fmt.Errorf("failed to convert private key to ECDSA private key: %w", err) + } + signature, err := gethcrypto.Sign(req.Data, privateKey) + if err != nil { + return SignResponse{}, err + } + return SignResponse{ + Signature: signature, + }, nil + default: + return SignResponse{}, fmt.Errorf("unsupported key type: %s", key.keyType) + } } func (k *keystore) Verify(ctx context.Context, req VerifyRequest) (VerifyResponse, error) { - return VerifyResponse{}, nil + // Note don't need the lock since this is a pure function. + switch req.KeyType { + case Ed25519: + if len(req.Signature) != 64 { + return VerifyResponse{}, fmt.Errorf("signature must be 64 bytes for Ed25519, got %d: %w", len(req.Signature), ErrInvalidVerifyRequest) + } + if len(req.PublicKey) != 32 { + return VerifyResponse{}, fmt.Errorf("public key must be 32 bytes for Ed25519, got %d: %w", len(req.PublicKey), ErrInvalidVerifyRequest) + } + publicKey := ed25519.PublicKey(req.PublicKey) + signature := ed25519.Verify(publicKey, req.Data, req.Signature) + return VerifyResponse{ + Valid: signature, + }, nil + case ECDSA_S256: + if len(req.Data) != 32 { + return VerifyResponse{}, fmt.Errorf("data must be 32 bytes for ECDSA_S256, got %d: %w", len(req.Data), ErrInvalidVerifyRequest) + } + // ECDSA_S256 public keys are in SEC1 (uncompressed) format + if len(req.PublicKey) != 65 { + return VerifyResponse{}, fmt.Errorf("public key must be 65 bytes for ECDSA_S256, got %d: %w", len(req.PublicKey), ErrInvalidVerifyRequest) + } + if len(req.Signature) != 65 { + return VerifyResponse{}, fmt.Errorf("signature must be 65 bytes for ECDSA_S256, got %d: %w", len(req.Signature), ErrInvalidVerifyRequest) + } + // VerifySignature expects 64 bytes [R || S] without the V byte + // Strip the V byte (last byte) from the 65-byte signature + valid := gethcrypto.VerifySignature(req.PublicKey, req.Data, req.Signature[:64]) + return VerifyResponse{ + Valid: valid, + }, nil + default: + return VerifyResponse{}, fmt.Errorf("unsupported key type: %s", req.KeyType) + } } diff --git a/keystore/signer_test.go b/keystore/signer_test.go new file mode 100644 index 000000000..ab5d9adb0 --- /dev/null +++ b/keystore/signer_test.go @@ -0,0 +1,70 @@ +package keystore_test + +import ( + "testing" + + "github.com/smartcontractkit/chainlink-common/keystore" + "github.com/stretchr/testify/require" +) + +func TestSigner(t *testing.T) { + ks := NewKeystoreTH(t) + ks.CreateTestKeys(t) + ctx := t.Context() + + var tt = []struct { + name string + keyName string + data []byte + signature []byte + expectedError error + }{ + { + name: "ECDSA_S256 sign/verify", + keyName: ks.KeyName(keystore.ECDSA_S256, 0), + data: make([]byte, 32), // 32 byte digest + }, + { + name: "ECDSA_S256 sign/verify no such key", + keyName: "no-such-key", + data: make([]byte, 32), // 32 byte digest + expectedError: keystore.ErrKeyNotFound, + }, + { + name: "ECDSA_S256 sign/verify wrong data length", + keyName: ks.KeyName(keystore.ECDSA_S256, 0), + data: make([]byte, 31), + expectedError: keystore.ErrInvalidSignRequest, + }, + { + name: "Ed25519 sign/verify", + keyName: ks.KeyName(keystore.Ed25519, 0), + data: []byte("test_data"), + }, + { + name: "Ed25519 sign/verify no such key", + keyName: "no-such-key", + data: make([]byte, 2), + expectedError: keystore.ErrKeyNotFound, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + signature, err := ks.Keystore.Sign(ctx, keystore.SignRequest{KeyName: tc.keyName, Data: tc.data}) + if tc.expectedError != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.expectedError) + return + } + require.NoError(t, err) + valid, err := ks.Keystore.Verify(ctx, keystore.VerifyRequest{ + KeyType: ks.KeysByName()[tc.keyName].KeyType, + PublicKey: ks.KeysByName()[tc.keyName].PublicKey, + Data: tc.data, + Signature: signature.Signature, + }) + require.NoError(t, err) + require.True(t, valid.Valid) + }) + } +}