WIP secp256k1 support

This commit is contained in:
Michael Muré
2025-07-08 12:58:05 +02:00
parent 538ea436ca
commit b6db79f12e
4 changed files with 546 additions and 0 deletions

43
crypto/secp256k1/key.go Normal file
View File

@@ -0,0 +1,43 @@
package secp256k1
import (
"encoding/asn1"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
const (
// PublicKeyBytesSize is the size, in bytes, of public keys in raw bytes.
PublicKeyBytesSize = secp256k1.PubKeyBytesLenCompressed
// PrivateKeyBytesSize is the size, in bytes, of private keys in raw bytes.
PrivateKeyBytesSize = secp256k1.PrivKeyBytesLen
// SignatureBytesSize is the size, in bytes, of signatures in raw bytes.
SignatureBytesSize = 123456
MultibaseCode = uint64(0xe7)
// coordinateSize is the size, in bytes, of one coordinate in the elliptic curve.
coordinateSize = 32
)
func GenerateKeyPair() (*PublicKey, *PrivateKey, error) {
priv, err := secp256k1.GeneratePrivateKey()
if err != nil {
return nil, nil, err
}
pub := priv.PubKey()
return &PublicKey{k: pub}, &PrivateKey{k: priv}, nil
}
const (
pemPubBlockType = "PUBLIC KEY"
pemPrivBlockType = "PRIVATE KEY"
)
var (
// Elliptic curve public key (OID: 1.2.840.10045.2.1)
oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
// Curve is secp256k1 (OID: 1.3.132.0.10)
oidSecp256k1 = asn1.ObjectIdentifier{1, 3, 132, 0, 10}
)

View File

@@ -0,0 +1,82 @@
package secp256k1
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/_testsuite"
)
var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{
Name: "secp256k1",
GenerateKeyPair: GenerateKeyPair,
PublicKeyFromBytes: PublicKeyFromBytes,
PublicKeyFromPublicKeyMultibase: PublicKeyFromPublicKeyMultibase,
PublicKeyFromX509DER: PublicKeyFromX509DER,
PublicKeyFromX509PEM: PublicKeyFromX509PEM,
PrivateKeyFromBytes: PrivateKeyFromBytes,
PrivateKeyFromPKCS8DER: PrivateKeyFromPKCS8DER,
PrivateKeyFromPKCS8PEM: PrivateKeyFromPKCS8PEM,
MultibaseCode: MultibaseCode,
DefaultHash: crypto.SHA256,
OtherHashes: []crypto.Hash{crypto.KECCAK_256},
PublicKeyBytesSize: PublicKeyBytesSize,
PrivateKeyBytesSize: PrivateKeyBytesSize,
SignatureBytesSize: SignatureBytesSize,
}
func TestSuite(t *testing.T) {
testsuite.TestSuite(t, harness)
}
func BenchmarkSuite(b *testing.B) {
testsuite.BenchSuite(b, harness)
}
func TestPublicKeyX509(t *testing.T) {
// openssl ecparam -genkey -name secp256k1 | openssl pkcs8 -topk8 -nocrypt -out secp256k1-key.pem
// openssl pkey -in secp256k1-key.pem -pubout -out secp256k1-pubkey.pem
pem := `-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEFVP6HKjIReiiUgrC+t+FjG5u0PXIoBmN
V1MMmoOFfKlrD/HuWUjjlw0mDKZcG7AM7JKPTWMOCcvUR2B8BUO3VQ==
-----END PUBLIC KEY-----
`
pub, err := PublicKeyFromX509PEM(pem)
require.NoError(t, err)
rt := pub.ToX509PEM()
require.Equal(t, pem, rt)
}
func TestPrivateKeyPKCS8(t *testing.T) {
// openssl ecparam -genkey -name secp256k1 | openssl pkcs8 -topk8 -nocrypt -out secp256k1-key.pem
pem := `-----BEGIN PRIVATE KEY-----
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgZW9JcJ1kN+DW2IFgqKJu
KS+39/xVa0n2J+lCr7hYGTihRANCAAQVU/ocqMhF6KJSCsL634WMbm7Q9cigGY1X
Uwyag4V8qWsP8e5ZSOOXDSYMplwbsAzsko9NYw4Jy9RHYHwFQ7dV
-----END PRIVATE KEY-----
`
priv, err := PrivateKeyFromPKCS8PEM(pem)
require.NoError(t, err)
rt := priv.ToPKCS8PEM()
require.Equal(t, pem, rt)
}
func FuzzPrivateKeyFromPKCS8PEM(f *testing.F) {
f.Add(`-----BEGIN PRIVATE KEY-----
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgZW9JcJ1kN+DW2IFgqKJu
KS+39/xVa0n2J+lCr7hYGTihRANCAAQVU/ocqMhF6KJSCsL634WMbm7Q9cigGY1X
Uwyag4V8qWsP8e5ZSOOXDSYMplwbsAzsko9NYw4Jy9RHYHwFQ7dV
-----END PRIVATE KEY-----
`)
f.Fuzz(func(t *testing.T, data string) {
// looking for panics
_, _ = PrivateKeyFromPKCS8PEM(data)
})
}

218
crypto/secp256k1/private.go Normal file
View File

@@ -0,0 +1,218 @@
package secp256k1
import (
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"fmt"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
"github.com/INFURA/go-did/crypto"
)
var _ crypto.PrivateKeySigning = &PrivateKey{}
var _ crypto.PrivateKeyKeyExchange = &PrivateKey{}
type PrivateKey struct {
k *secp256k1.PrivateKey
}
// PrivateKeyFromBytes converts a serialized public key to a PrivateKey.
// This compact serialization format is the raw key material, without metadata or structure.
// It errors if the slice is not the right size.
func PrivateKeyFromBytes(b []byte) (*PrivateKey, error) {
if len(b) != PrivateKeyBytesSize {
return nil, fmt.Errorf("invalid secp256k1 private key size")
}
return &PrivateKey{k: secp256k1.PrivKeyFromBytes(b)}, nil
}
// PrivateKeyFromPKCS8DER decodes a PKCS#8 DER (binary) encoded private key.
func PrivateKeyFromPKCS8DER(bytes []byte) (*PrivateKey, error) {
// Parse the PKCS#8 structure
var pkcs8 struct {
Version int
Algo pkix.AlgorithmIdentifier
PrivateKey []byte
}
if _, err := asn1.Unmarshal(bytes, &pkcs8); err != nil {
return nil, fmt.Errorf("failed to parse PKCS#8 structure: %w", err)
}
// Check if this is an Elliptic curve public key (OID: 1.2.840.10045.2.1)
if !pkcs8.Algo.Algorithm.Equal(oidPublicKeyECDSA) {
return nil, fmt.Errorf("not an EC private key, got OID: %v", pkcs8.Algo.Algorithm)
}
// Extract the curve OID from parameters
var namedCurveOID asn1.ObjectIdentifier
if _, err := asn1.Unmarshal(pkcs8.Algo.Parameters.FullBytes, &namedCurveOID); err != nil {
return nil, fmt.Errorf("failed to parse curve parameters: %w", err)
}
// Check if the curve is secp256k1 (OID: 1.3.132.0.10)
if !namedCurveOID.Equal(oidSecp256k1) {
return nil, fmt.Errorf("unsupported curve, expected secp256k1 (1.3.132.0.10), got: %v", namedCurveOID)
}
// Parse the EC private key structure (RFC 5915)
var ecPrivKey struct {
Version int
PrivateKey []byte
PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"`
}
if _, err := asn1.Unmarshal(pkcs8.PrivateKey, &ecPrivKey); err != nil {
return nil, fmt.Errorf("failed to parse alliptic curve private key: %w", err)
}
// Validate the EC private key version
if ecPrivKey.Version != 1 {
return nil, fmt.Errorf("unsupported EC private key version: %d", ecPrivKey.Version)
}
// Validate private key length
if len(ecPrivKey.PrivateKey) != PrivateKeyBytesSize {
return nil, fmt.Errorf("invalid secp256k1 private key length: %d, expected %d", len(ecPrivKey.PrivateKey), PrivateKeyBytesSize)
}
// Create the secp256k1 private key
privKeySecp256k1 := secp256k1.PrivKeyFromBytes(ecPrivKey.PrivateKey)
return &PrivateKey{k: privKeySecp256k1}, nil
}
// PrivateKeyFromPKCS8PEM decodes an PKCS#8 PEM (string) encoded private key.
func PrivateKeyFromPKCS8PEM(str string) (*PrivateKey, error) {
block, _ := pem.Decode([]byte(str))
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
if block.Type != pemPrivBlockType {
return nil, fmt.Errorf("incorrect PEM block type")
}
return PrivateKeyFromPKCS8DER(block.Bytes)
}
func (p *PrivateKey) Equal(other crypto.PrivateKey) bool {
if other, ok := other.(*PrivateKey); ok {
return p.k.PubKey().IsEqual(other.k.PubKey())
}
return false
}
func (p *PrivateKey) Public() crypto.PublicKey {
return &PublicKey{k: p.k.PubKey()}
}
func (p *PrivateKey) ToBytes() []byte {
return p.k.Serialize()
}
func (p *PrivateKey) ToPKCS8DER() []byte {
pubkeyBytes := p.k.PubKey().SerializeUncompressed()
// Create the EC private key structure
// This follows RFC 5915 format for EC private keys
ecPrivateKey := struct {
Version int
PrivateKey []byte
Parameters asn1.RawValue `asn1:"optional,explicit,tag:0"`
PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"`
}{
Version: 1,
PrivateKey: p.k.Serialize(),
// Parameters are omitted since they're specified in the algorithm identifier
// Pubkey could be omitted, but we include it to match openssl behavior
PublicKey: asn1.BitString{
Bytes: pubkeyBytes,
BitLength: 8 * len(pubkeyBytes),
},
}
ecPrivKeyDER, err := asn1.Marshal(ecPrivateKey)
if err != nil {
panic(err) // This should not happen with valid key data
}
// Create the PKCS#8 structure
pkcs8 := struct {
Version int
Algo pkix.AlgorithmIdentifier
PrivateKey []byte
}{
Version: 0,
Algo: pkix.AlgorithmIdentifier{
// Elliptic curve public key (OID: 1.2.840.10045.2.1)
Algorithm: oidPublicKeyECDSA,
Parameters: asn1.RawValue{
FullBytes: must(asn1.Marshal(oidSecp256k1)),
},
},
PrivateKey: ecPrivKeyDER,
}
der, err := asn1.Marshal(pkcs8)
if err != nil {
panic(err) // This should not happen with valid key data
}
return der
}
func (p *PrivateKey) ToPKCS8PEM() string {
der := p.ToPKCS8DER()
return string(pem.EncodeToMemory(&pem.Block{
Type: pemPrivBlockType,
Bytes: der,
}))
}
// The default signing hash is SHA-256.
func (p *PrivateKey) SignToBytes(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
params := crypto.CollectSigningOptions(opts)
hasher := params.HashOrDefault(crypto.SHA256).New()
hasher.Write(message)
hash := hasher.Sum(nil)
// TODO
return ecdsa.SignCompact(p.k, hash[:], false), nil
}
// The default signing hash is SHA-256.
func (p *PrivateKey) SignToASN1(message []byte, opts ...crypto.SigningOption) ([]byte, error) {
// // Hash the message with SHA-256
// hash := sha256.Sum256(message)
//
// return ecdsa.SignASN1(rand.Reader, p.k.ToECDSA(), hash[:])
return nil, fmt.Errorf("not implemented")
}
func (p *PrivateKey) PublicKeyIsCompatible(remote crypto.PublicKey) bool {
if _, ok := remote.(*PublicKey); ok {
return true
}
return false
}
func (p *PrivateKey) KeyExchange(remote crypto.PublicKey) ([]byte, error) {
// if remote, ok := remote.(*PublicKey); ok {
// // First, we need to convert the ECDSA (signing only) to the equivalent ECDH keys
// ecdhPriv, err := p.k.ECDH()
// if err != nil {
// return nil, err
// }
// ecdhPub, err := remote.k.ECDH()
// if err != nil {
// return nil, err
// }
//
// return ecdhPriv.ECDH(ecdhPub)
// }
// return nil, fmt.Errorf("incompatible public key")
panic("not implemented")
}

203
crypto/secp256k1/public.go Normal file
View File

@@ -0,0 +1,203 @@
package secp256k1
import (
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"fmt"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/INFURA/go-did/crypto"
helpers "github.com/INFURA/go-did/crypto/internal"
)
var _ crypto.PublicKeySigning = &PublicKey{}
type PublicKey struct {
k *secp256k1.PublicKey
}
// PublicKeyFromBytes converts a serialized public key to a PublicKey.
// This compact serialization format is the raw key material, without metadata or structure.
// It errors if the slice is not the right size.
func PublicKeyFromBytes(b []byte) (*PublicKey, error) {
pub, err := secp256k1.ParsePubKey(b)
if err != nil {
return nil, err
}
return &PublicKey{k: pub}, nil
}
// PublicKeyFromXY converts x and y coordinates into a PublicKey.
func PublicKeyFromXY(x, y []byte) (*PublicKey, error) {
var xf, yf secp256k1.FieldVal
if xf.SetByteSlice(x) {
return nil, fmt.Errorf("invalid secp255k1 public key")
}
if yf.SetByteSlice(y) {
return nil, fmt.Errorf("invalid secp255k1 public key")
}
return &PublicKey{k: secp256k1.NewPublicKey(&xf, &yf)}, nil
}
// PublicKeyFromPublicKeyMultibase decodes the public key from its Multibase form
func PublicKeyFromPublicKeyMultibase(multibase string) (*PublicKey, error) {
code, bytes, err := helpers.PublicKeyMultibaseDecode(multibase)
if err != nil {
return nil, err
}
if code != MultibaseCode {
return nil, fmt.Errorf("invalid code")
}
return PublicKeyFromBytes(bytes)
}
// PublicKeyFromX509DER decodes an X.509 DER (binary) encoded public key.
func PublicKeyFromX509DER(bytes []byte) (*PublicKey, error) {
// Parse the X.509 SubjectPublicKeyInfo structure
var spki struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}
if _, err := asn1.Unmarshal(bytes, &spki); err != nil {
return nil, fmt.Errorf("failed to parse X.509 SubjectPublicKeyInfo: %w", err)
}
// Check if this is an Elliptic curve public key (OID: 1.2.840.10045.2.1)
if !spki.Algorithm.Algorithm.Equal(oidPublicKeyECDSA) {
return nil, fmt.Errorf("not an Elliptic curve public key, got OID: %v", spki.Algorithm.Algorithm)
}
// Extract the curve OID from parameters
var namedCurveOID asn1.ObjectIdentifier
if _, err := asn1.Unmarshal(spki.Algorithm.Parameters.FullBytes, &namedCurveOID); err != nil {
return nil, fmt.Errorf("failed to parse curve parameters: %w", err)
}
// Check if this is secp256k1 (OID: 1.3.132.0.10)
if !namedCurveOID.Equal(oidSecp256k1) {
return nil, fmt.Errorf("unsupported curve, expected secp256k1 (1.3.132.0.10), got: %v", namedCurveOID)
}
pubKey, err := secp256k1.ParsePubKey(spki.SubjectPublicKey.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse secp256k1 public key: %w", err)
}
return &PublicKey{k: pubKey}, nil
}
// PublicKeyFromX509PEM decodes an X.509 PEM (string) encoded public key.
func PublicKeyFromX509PEM(str string) (*PublicKey, error) {
block, _ := pem.Decode([]byte(str))
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
if block.Type != pemPubBlockType {
return nil, fmt.Errorf("incorrect PEM block type")
}
return PublicKeyFromX509DER(block.Bytes)
}
func (p *PublicKey) XBytes() []byte {
// fixed size buffer that can get allocated on the caller's stack after inlining.
var buf [coordinateSize]byte
p.k.X().FillBytes(buf[:])
return buf[:]
}
func (p *PublicKey) YBytes() []byte {
// fixed size buffer that can get allocated on the caller's stack after inlining.
var buf [coordinateSize]byte
p.k.Y().FillBytes(buf[:])
return buf[:]
}
func (p *PublicKey) Equal(other crypto.PublicKey) bool {
if other, ok := other.(*PublicKey); ok {
return p.k.IsEqual(other.k)
}
return false
}
func (p *PublicKey) ToBytes() []byte {
// 33-byte compressed format
return p.k.SerializeCompressed()
}
func (p *PublicKey) ToPublicKeyMultibase() string {
return helpers.PublicKeyMultibaseEncode(MultibaseCode, p.k.SerializeCompressed())
}
func (p *PublicKey) ToX509DER() []byte {
pubKeyBytes := p.k.SerializeUncompressed()
// Create the X.509 SubjectPublicKeyInfo structure
spki := struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}{
Algorithm: pkix.AlgorithmIdentifier{
Algorithm: oidPublicKeyECDSA,
Parameters: asn1.RawValue{
FullBytes: must(asn1.Marshal(oidSecp256k1)),
},
},
SubjectPublicKey: asn1.BitString{
Bytes: pubKeyBytes,
BitLength: len(pubKeyBytes) * 8,
},
}
der, err := asn1.Marshal(spki)
if err != nil {
panic(err) // This should not happen with valid key data
}
return der
}
func (p *PublicKey) ToX509PEM() string {
der := p.ToX509DER()
return string(pem.EncodeToMemory(&pem.Block{
Type: pemPubBlockType,
Bytes: der,
}))
}
/*
Note: signatures for the crypto.PrivateKeySigning interface assumes SHA256,
which should be correct almost always. If there is a need to use a different
hash function, we can add separate functions that have that flexibility.
*/
func (p *PublicKey) VerifyBytes(message, signature []byte, opts ...crypto.SigningOption) bool {
// if len(signature) != SignatureBytesSize {
// return false
// }
//
// // Hash the message with SHA-256
// hash := sha256.Sum256(message)
//
// r := new(big.Int).SetBytes(signature[:SignatureBytesSize/2])
// s := new(big.Int).SetBytes(signature[SignatureBytesSize/2:])
//
// return ecdsa.Verify(p.k, hash[:], r, s)
panic("not implemented")
}
func (p *PublicKey) VerifyASN1(message, signature []byte, opts ...crypto.SigningOption) bool {
// // Hash the message with SHA-256
// hash := sha256.Sum256(message)
//
// return ecdsa.VerifyASN1(p.k, hash[:], signature)
panic("not implemented")
}
func must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}