Skip to content
Open
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
96 changes: 96 additions & 0 deletions pkg/crypto/secp256.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package crypto

import (
"crypto/ecdsa"
"crypto/elliptic"
"errors"
"math/big"
)

const (
secp256r1UncompressedPubKeyPrefix = 0x04
secp256r1RawPubKeySize = 64 // X(32) || Y(32)
secp256r1UncompressedPubKeySize = 1 + secp256r1RawPubKeySize // SEC1 0x04 || X(32) || Y(32)
sec2562r1P1363SignatureSize = 64
)

// Secp256Verify verifies ECDSA signature on NIST P-256 (aka secp256r1). X, Y, R, S are big-endian byte slices. Inputs:
//
// publicKey formats supported:
// - 65 bytes: uncompressed SEC1 form 0x04 || X(32) || Y(32)
// - 64 bytes: raw X(32) || Y(32)
//
// signature format supported:
// - 64 bytes P1363: R(32) || S(32)
//
// TODO:
// - what kind of signature formats to support? ASN.1 DER or P1363 (r||s)? should we support both?
// - should we enforce low-S signatures to prevent malleability?
func Secp256Verify(digest, publicKey, signature []byte) (bool, error) {
curve := elliptic.P256()

// ---- Parse public key ----
var x, y *big.Int
switch len(publicKey) {
case secp256r1UncompressedPubKeySize:
if publicKey[0] != secp256r1UncompressedPubKeyPrefix {
return false, errors.New("publicKey: expected uncompressed SEC1 prefix 0x04")
}
x = new(big.Int).SetBytes(publicKey[1:33])
y = new(big.Int).SetBytes(publicKey[33:65])
case secp256r1RawPubKeySize:
x = new(big.Int).SetBytes(publicKey[0:32])
y = new(big.Int).SetBytes(publicKey[32:64])
default:
return false, errors.New("publicKey: expected 64 or 65 bytes")
}

// Validate point is on curve (prevents invalid-curve / nonsense keys).
// TODO: does we need these validations? all tests pass without them.
if x.Sign() == 0 && y.Sign() == 0 {
return false, errors.New("publicKey: point at infinity / zero not allowed")
}
if !curve.IsOnCurve(x, y) {
return false, errors.New("publicKey: point is not on P-256 curve")
}

pub := ecdsa.PublicKey{Curve: curve, X: x, Y: y}

// ---- Parse signature (P1363 r||s) ----
if len(signature) != sec2562r1P1363SignatureSize {
return false, errors.New("signature: expected 64-byte P1363 signature (r||s)")
}
r := new(big.Int).SetBytes(signature[0:32])
s := new(big.Int).SetBytes(signature[32:64])

// ---- Validate r,s range: 1 <= r,s <= N-1 ----
// TODO: does we need these validations? all tests pass without them.
basePoitOrderN := curve.Params().N
if r.Sign() <= 0 || s.Sign() <= 0 {
return false, errors.New("signature: r or s is zero/negative")
}
if r.Cmp(basePoitOrderN) >= 0 || s.Cmp(basePoitOrderN) >= 0 {
return false, errors.New("signature: r or s >= curve order")
}

// ---- Validate digest size ----
if len(digest) != DigestSize {
return false, errors.New("digest: expected 32-byte digest")
}

// OPTIONAL (protocol-dependent): enforce low-S to prevent malleability.
// Many systems (Bitcoin, Ethereum, JOSE, etc.) require this.
// If you only want "pure ECDSA validity" (как в Wycheproof), comment out this block.
// Note: Wycheproof test vectors include both high-S and low-S signatures, so if you
// enable this check, some Wycheproof valid signatures will be rejected.
/*
halfN := new(big.Int).Rsh(new(big.Int).Set(N), 1)
if s.Cmp(halfN) == 1 {
return false, errors.New("signature: non-canonical (high-S)")
}
*/

// ---- Verify ----
ok := ecdsa.Verify(&pub, digest, r, s) // TODO: VerifyASN1 maybe? (most applications use ASN.1 DER signatures)
return ok, nil
}
142 changes: 142 additions & 0 deletions pkg/crypto/secp256_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package crypto

import (
"bytes"
"embed"
"encoding/hex"
"encoding/json"
"fmt"
"path/filepath"
"slices"
"testing"

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

var (
//go:embed testdata/vectors_wycheproof.jsonl
vectorsWycheproof embed.FS
)

type testVectorView struct {
X string `json:"x"`
Y string `json:"y"`
R string `json:"r"`
S string `json:"s"`
Hash string `json:"hash"`
Valid bool `json:"valid"`
Msg string `json:"msg"`
Comment string `json:"comment"`
}

func unmarshalTestDataToView(t *testing.T, fs embed.FS, testFileName, keccakHexChecksum string) []testVectorView {
fileData, err := fs.ReadFile(filepath.Clean(testFileName))
require.NoError(t, err)
dataChecksum := hex.EncodeToString(MustKeccak256(fileData).Bytes())
require.Equal(t, keccakHexChecksum, dataChecksum, "test data checksum mismatch")
sep := []byte{'\n'}
n := bytes.Count(fileData, sep) // approx number of records
res := make([]testVectorView, 0, n)
for record := range bytes.SplitSeq(fileData, sep) {
record = bytes.TrimSpace(record)
if len(record) == 0 {
continue // skip empty lines
}
var tv testVectorView
jsErr := json.Unmarshal(record, &tv)
require.NoError(t, jsErr)
res = append(res, tv)
}
return res
}

type testVector struct {
PublicKey []byte // uncompressed SEC1, or raw (X||Y)
Signature []byte
Digest []byte
Valid bool
}

func appendRawPubKey(t *testing.T, out []byte, x, y string) []byte {
const coordSize = secp256r1RawPubKeySize / 2
out = slices.Grow(out, len(out)+secp256r1RawPubKeySize)
xBytes, err := hex.DecodeString(x)
require.NoError(t, err)
require.Len(t, xBytes, coordSize)
yBytes, err := hex.DecodeString(y)
require.NoError(t, err)
require.Len(t, yBytes, coordSize)
out = append(out, xBytes...)
out = append(out, yBytes...)
return out
}

func appendUncompressedPubKey(t *testing.T, out []byte, x, y string) []byte {
out = slices.Grow(out, len(out)+secp256r1UncompressedPubKeySize)
out = append(out, secp256r1UncompressedPubKeyPrefix)
return appendRawPubKey(t, out, x, y)
}

func appendSignature(t *testing.T, out []byte, r, s string) []byte {
out = slices.Grow(out, sec2562r1P1363SignatureSize)
rBytes, err := hex.DecodeString(r)
require.NoError(t, err)
sBytes, err := hex.DecodeString(s)
require.NoError(t, err)
out = append(out, rBytes...)
out = append(out, sBytes...)
require.Len(t, out, sec2562r1P1363SignatureSize)
return out
}

func transformViewsToVectors(t *testing.T, v []testVectorView) []testVector {
res := make([]testVector, 0, len(v)*2) // *2 for both pubkey formats
for _, tv := range v {
rawPK := appendRawPubKey(t, nil, tv.X, tv.Y)
uncompressedPK := appendUncompressedPubKey(t, nil, tv.X, tv.Y)
sig := appendSignature(t, nil, tv.R, tv.S)
digest, err := hex.DecodeString(tv.Hash)
require.NoError(t, err)
require.Len(t, digest, DigestSize)
res = append(res,
testVector{
PublicKey: rawPK,
Signature: sig,
Digest: digest,
Valid: tv.Valid,
},
testVector{
PublicKey: uncompressedPK,
Signature: sig,
Digest: digest,
Valid: tv.Valid,
},
)
}
return res
}

func TestSecp256Verify(t *testing.T) {
const (
testFileName = "testdata/vectors_wycheproof.jsonl"
vectorsWycheproofKeccakChecksum = "d7e23f35ae6e092eda970e14c53d3e30261eb84a18389cc65041466ba5cb4c98"
)
vectorsView := unmarshalTestDataToView(t, vectorsWycheproof, testFileName, vectorsWycheproofKeccakChecksum)
vectors := transformViewsToVectors(t, vectorsView)
for i, tv := range vectors {
t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
ok, err := Secp256Verify(tv.Digest, tv.PublicKey, tv.Signature)
if tv.Valid {
require.NoError(t, err, "valid vector should not return error")
require.True(t, ok, "valid vector should verify")
} else {
// Invalid vectors may return error or ok==false
require.False(t, ok, "valid vector should not verify")
if err != nil {
// Error is acceptable for invalid vector
t.Logf("invalid vector returned error as expected: %v", err)
}
}
})
}
}
Loading
Loading