add a crypto base layer that abstract various keypair type, + test/bench suite

This commit is contained in:
Michael Muré
2025-06-19 18:17:54 +02:00
parent 6bd03b4038
commit 5be4ab8175
15 changed files with 1341 additions and 0 deletions

30
crypto/ed25519/key.go Normal file
View File

@@ -0,0 +1,30 @@
package ed25519
import (
"crypto/ed25519"
"crypto/rand"
)
const (
// PublicKeySize is the size, in bytes, of public keys as used in this package.
PublicKeySize = ed25519.PublicKeySize
// PrivateKeySize is the size, in bytes, of private keys as used in this package.
PrivateKeySize = ed25519.PrivateKeySize
// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
SignatureSize = ed25519.SignatureSize
MultibaseCode = uint64(0xed)
)
func GenerateKeyPair() (PublicKey, PrivateKey, error) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return PublicKey{}, PrivateKey{}, err
}
return PublicKey{k: pub}, PrivateKey{k: priv}, nil
}
const (
pemPubBlockType = "PUBLIC KEY"
pemPrivBlockType = "PRIVATE KEY"
)

View File

@@ -0,0 +1,31 @@
package ed25519
import (
"testing"
"github.com/INFURA/go-did/crypto/internal"
)
var harness = helpers.TestHarness[PublicKey, PrivateKey]{
Name: "ed25519",
GenerateKeyPair: GenerateKeyPair,
PublicKeyFromBytes: PublicKeyFromBytes,
PublicKeyFromPublicKeyMultibase: PublicKeyFromPublicKeyMultibase,
PublicKeyFromX509DER: PublicKeyFromX509DER,
PublicKeyFromX509PEM: PublicKeyFromX509PEM,
PrivateKeyFromBytes: PrivateKeyFromBytes,
PrivateKeyFromPKCS8DER: PrivateKeyFromPKCS8DER,
PrivateKeyFromPKCS8PEM: PrivateKeyFromPKCS8PEM,
MultibaseCode: MultibaseCode,
PublicKeySize: PublicKeySize,
PrivateKeySize: PrivateKeySize,
SignatureSize: SignatureSize,
}
func TestSuite(t *testing.T) {
helpers.TestSuite(t, harness)
}
func BenchmarkSuite(b *testing.B) {
helpers.BenchSuite(b, harness)
}

90
crypto/ed25519/private.go Normal file
View File

@@ -0,0 +1,90 @@
package ed25519
import (
"crypto/ed25519"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/INFURA/go-did/crypto"
)
var _ crypto.SigningPrivateKey = &PrivateKey{}
type PrivateKey struct {
k ed25519.PrivateKey
}
// PrivateKeyFromBytes converts a serialized private 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) != PrivateKeySize {
return PrivateKey{}, fmt.Errorf("invalid ed25519 private key size")
}
// make a copy
return PrivateKey{k: append([]byte{}, b...)}, nil
}
// PrivateKeyFromPKCS8DER decodes a PKCS#8 DER (binary) encoded private key.
func PrivateKeyFromPKCS8DER(bytes []byte) (PrivateKey, error) {
priv, err := x509.ParsePKCS8PrivateKey(bytes)
if err != nil {
return PrivateKey{}, err
}
return PrivateKey{k: priv.(ed25519.PrivateKey)}, 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 PrivateKey{}, fmt.Errorf("failed to decode PEM block")
}
if block.Type != pemPrivBlockType {
return PrivateKey{}, 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.Equal(other.k)
}
return false
}
func (p PrivateKey) Public() crypto.PublicKey {
return PublicKey{k: p.k.Public().(ed25519.PublicKey)}
}
func (p PrivateKey) Sign(message []byte) ([]byte, error) {
return ed25519.Sign(p.k, message), nil
}
func (p PrivateKey) ToBytes() []byte {
// Copy the private key to a fixed size buffer that can get allocated on the
// caller's stack after inlining.
var buf [PrivateKeySize]byte
return append(buf[:0], p.k...)
}
func (p PrivateKey) ToPKCS8DER() []byte {
res, _ := x509.MarshalPKCS8PrivateKey(p.k)
return res
}
func (p PrivateKey) ToPKCS8PEM() string {
der := p.ToPKCS8DER()
return string(pem.EncodeToMemory(&pem.Block{
Type: pemPrivBlockType,
Bytes: der,
}))
}
// Seed returns the private key seed corresponding to priv. It is provided for
// interoperability with RFC 8032. RFC 8032's private keys correspond to seeds
// in this package.
func (p PrivateKey) Seed() []byte {
return p.k.Seed()
}

99
crypto/ed25519/public.go Normal file
View File

@@ -0,0 +1,99 @@
package ed25519
import (
"crypto/ed25519"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/internal"
)
var _ crypto.SigningPublicKey = &PublicKey{}
type PublicKey struct {
k ed25519.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) {
if len(b) != PublicKeySize {
return PublicKey{}, fmt.Errorf("invalid ed25519 public key size")
}
// make a copy
return PublicKey{k: append([]byte{}, b...)}, nil
}
// PublicKeyFromPublicKeyMultibase decodes the public key from its PublicKeyMultibase form
func PublicKeyFromPublicKeyMultibase(multibase string) (PublicKey, error) {
code, bytes, err := helpers.PublicKeyMultibaseDecode(multibase)
if err != nil {
return PublicKey{}, err
}
if code != MultibaseCode {
return PublicKey{}, fmt.Errorf("invalid code")
}
if len(bytes) != PublicKeySize {
return PublicKey{}, fmt.Errorf("invalid ed25519 public key size")
}
return PublicKeyFromBytes(bytes)
}
// PublicKeyFromX509DER decodes an X.509 DER (binary) encoded public key.
func PublicKeyFromX509DER(bytes []byte) (PublicKey, error) {
pub, err := x509.ParsePKIXPublicKey(bytes)
if err != nil {
return PublicKey{}, err
}
return PublicKey{k: pub.(ed25519.PublicKey)}, 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 PublicKey{}, fmt.Errorf("failed to decode PEM block")
}
if block.Type != pemPubBlockType {
return PublicKey{}, fmt.Errorf("incorrect PEM block type")
}
return PublicKeyFromX509DER(block.Bytes)
}
func (p PublicKey) ToBytes() []byte {
// Copy the private key to a fixed size buffer that can get allocated on the
// caller's stack after inlining.
var buf [PublicKeySize]byte
return append(buf[:0], p.k...)
}
func (p PublicKey) ToPublicKeyMultibase() string {
return helpers.PublicKeyMultibaseEncode(MultibaseCode, p.k)
}
func (p PublicKey) ToX509DER() []byte {
res, _ := x509.MarshalPKIXPublicKey(p.k)
return res
}
func (p PublicKey) ToX509PEM() string {
der := p.ToX509DER()
return string(pem.EncodeToMemory(&pem.Block{
Type: pemPubBlockType,
Bytes: der,
}))
}
func (p PublicKey) Equal(other crypto.PublicKey) bool {
if other, ok := other.(PublicKey); ok {
return p.k.Equal(other.k)
}
return false
}
func (p PublicKey) Verify(message, signature []byte) bool {
return ed25519.Verify(p.k, message, signature)
}

41
crypto/interface.go Normal file
View File

@@ -0,0 +1,41 @@
package crypto
type PublicKey interface {
Equal(other PublicKey) bool
ToBytes() []byte
ToPublicKeyMultibase() string
ToX509DER() []byte
ToX509PEM() string
}
type PrivateKey interface {
Equal(other PrivateKey) bool
Public() PublicKey
ToBytes() []byte
ToPKCS8DER() []byte
ToPKCS8PEM() string
}
type SigningPublicKey interface {
PublicKey
Verify(message, signature []byte) bool
}
type SigningPrivateKey interface {
PrivateKey
Sign(message []byte) ([]byte, error)
}
type KeyExchangePublicKey interface {
PublicKey
// PrivateKeyIsCompatible checks that the given PrivateKey is compatible to perform key exchange.
PrivateKeyIsCompatible(local PrivateKey) bool
// ECDH computes the shared key using the given PrivateKey.
ECDH(local PrivateKey) ([]byte, error)
}

View File

@@ -0,0 +1,35 @@
package helpers
import (
"fmt"
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
)
// PublicKeyMultibaseDecode is a helper for decoding multibase public keys.
func PublicKeyMultibaseDecode(multibase string) (uint64, []byte, error) {
baseCodec, bytes, err := mbase.Decode(multibase)
if err != nil {
return 0, nil, err
}
// the specification enforces that encoding
if baseCodec != mbase.Base58BTC {
return 0, nil, fmt.Errorf("not Base58BTC encoded")
}
code, read, err := varint.FromUvarint(bytes)
if err != nil {
return 0, nil, err
}
if read != 2 {
return 0, nil, fmt.Errorf("unexpected multibase")
}
return code, bytes[read:], nil
}
// PublicKeyMultibaseEncode is a helper for encoding multibase public keys.
func PublicKeyMultibaseEncode(code uint64, bytes []byte) string {
// can only fail with an invalid encoding, but it's hardcoded
res, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(code), bytes...))
return res
}

View File

@@ -0,0 +1,394 @@
package helpers
import (
"fmt"
"strings"
"testing"
"text/tabwriter"
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/crypto"
)
type TestHarness[PubT crypto.PublicKey, PrivT crypto.PrivateKey] struct {
Name string
GenerateKeyPair func() (PubT, PrivT, error)
PublicKeyFromBytes func(b []byte) (PubT, error)
PublicKeyFromPublicKeyMultibase func(multibase string) (PubT, error)
PublicKeyFromX509DER func(bytes []byte) (PubT, error)
PublicKeyFromX509PEM func(str string) (PubT, error)
PrivateKeyFromBytes func(b []byte) (PrivT, error)
PrivateKeyFromPKCS8DER func(bytes []byte) (PrivT, error)
PrivateKeyFromPKCS8PEM func(str string) (PrivT, error)
MultibaseCode uint64
PublicKeySize int
PrivateKeySize int
SignatureSize int
}
func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, harness TestHarness[PubT, PrivT]) {
stats := struct {
bytesPubSize int
bytesPrivSize int
x509DerPubSize int
pkcs8DerPrivSize int
x509PemPubSize int
pkcs8PemPrivSize int
}{}
t.Cleanup(func() {
out := strings.Builder{}
w := tabwriter.NewWriter(&out, 0, 0, 3, ' ', 0)
_, _ = fmt.Fprintln(w, "\tPublic key\tPrivate key")
_, _ = fmt.Fprintf(w, "Bytes\t%v\t%v\n", stats.bytesPubSize, stats.bytesPrivSize)
_, _ = fmt.Fprintf(w, "DER (pub:x509, priv:PKCS#8)\t%v\t%v\n", stats.x509DerPubSize, stats.pkcs8DerPrivSize)
_, _ = fmt.Fprintf(w, "PEM (pub:x509, priv:PKCS#8)\t%v\t%v\n", stats.x509PemPubSize, stats.pkcs8PemPrivSize)
_ = w.Flush()
t.Logf("Test result for %s:\n%s\n", harness.Name, out.String())
})
t.Run("GenerateKeyPair", func(t *testing.T) {
pub, priv, err := harness.GenerateKeyPair()
require.NoError(t, err)
require.NotNil(t, pub)
require.NotNil(t, priv)
require.True(t, pub.Equal(priv.Public()))
})
t.Run("Equality", func(t *testing.T) {
pub1, priv1, err := harness.GenerateKeyPair()
require.NoError(t, err)
pub2, priv2, err := harness.GenerateKeyPair()
require.NoError(t, err)
require.True(t, pub1.Equal(pub1))
require.True(t, priv1.Equal(priv1))
require.False(t, pub1.Equal(pub2))
require.False(t, priv1.Equal(priv2))
pub1copy, err := harness.PublicKeyFromBytes(pub1.ToBytes())
require.NoError(t, err)
require.True(t, pub1.Equal(pub1copy))
require.True(t, pub1copy.Equal(pub1))
priv1copy, err := harness.PrivateKeyFromBytes(priv1.ToBytes())
require.NoError(t, err)
require.True(t, priv1.Equal(priv1copy))
require.True(t, priv1copy.Equal(priv1))
})
t.Run("BytesRoundTrip", func(t *testing.T) {
pub, priv, err := harness.GenerateKeyPair()
require.NoError(t, err)
bytes := pub.ToBytes()
stats.bytesPubSize = len(bytes)
rtPub, err := harness.PublicKeyFromBytes(bytes)
require.NoError(t, err)
require.True(t, pub.Equal(rtPub))
bytes = priv.ToBytes()
stats.bytesPrivSize = len(bytes)
rtPriv, err := harness.PrivateKeyFromBytes(bytes)
require.NoError(t, err)
require.True(t, priv.Equal(rtPriv))
})
t.Run("MultibaseRoundTrip", func(t *testing.T) {
pub, _, err := harness.GenerateKeyPair()
require.NoError(t, err)
mb := pub.ToPublicKeyMultibase()
rt, err := harness.PublicKeyFromPublicKeyMultibase(mb)
require.NoError(t, err)
require.Equal(t, pub, rt)
encoding, bytes, err := mbase.Decode(mb)
require.NoError(t, err)
require.Equal(t, mbase.Base58BTC, int32(encoding)) // according to the DID spec
code, _, err := varint.FromUvarint(bytes)
require.NoError(t, err)
require.Equal(t, harness.MultibaseCode, code)
})
t.Run("PublicKeyX509RoundTrip", func(t *testing.T) {
pub, _, err := harness.GenerateKeyPair()
require.NoError(t, err)
der := pub.ToX509DER()
stats.x509DerPubSize = len(der)
rt, err := harness.PublicKeyFromX509DER(der)
require.NoError(t, err)
require.True(t, pub.Equal(rt))
pem := pub.ToX509PEM()
stats.x509PemPubSize = len(pem)
rt, err = harness.PublicKeyFromX509PEM(pem)
require.NoError(t, err)
require.True(t, pub.Equal(rt))
})
t.Run("PrivateKeyPKCS8RoundTrip", func(t *testing.T) {
pub, priv, err := harness.GenerateKeyPair()
require.NoError(t, err)
der := priv.ToPKCS8DER()
stats.pkcs8DerPrivSize = len(der)
rt, err := harness.PrivateKeyFromPKCS8DER(der)
require.NoError(t, err)
require.True(t, priv.Equal(rt))
require.True(t, pub.Equal(rt.Public()))
pem := priv.ToPKCS8PEM()
stats.pkcs8PemPrivSize = len(pem)
rt, err = harness.PrivateKeyFromPKCS8PEM(pem)
require.NoError(t, err)
require.True(t, priv.Equal(rt))
require.True(t, pub.Equal(rt.Public()))
})
t.Run("Signature", func(t *testing.T) {
pub, priv, err := harness.GenerateKeyPair()
require.NoError(t, err)
spub, ok := (crypto.PublicKey(pub)).(crypto.SigningPublicKey)
if !ok {
t.Skip("Signature is not implemented")
}
spriv, ok := (crypto.PrivateKey(priv)).(crypto.SigningPrivateKey)
if !ok {
t.Skip("Signature is not implemented")
}
msg := []byte("message")
sig, err := spriv.Sign(msg)
require.NoError(t, err)
require.NotEmpty(t, sig)
require.Equal(t, harness.SignatureSize, len(sig))
valid := spub.Verify(msg, sig)
require.True(t, valid)
valid = spub.Verify([]byte("wrong message"), sig)
require.False(t, valid)
})
t.Run("KeyExchange", func(t *testing.T) {
pub1, priv1, err := harness.GenerateKeyPair()
require.NoError(t, err)
pub2, priv2, err := harness.GenerateKeyPair()
require.NoError(t, err)
kePub1, ok := (crypto.PublicKey(pub1)).(crypto.KeyExchangePublicKey)
if !ok {
t.Skip("Key exchange is not implemented")
}
kePub2 := (crypto.PublicKey(pub2)).(crypto.KeyExchangePublicKey)
// TODO: test with incompatible private keys
require.True(t, kePub1.PrivateKeyIsCompatible(priv2))
require.True(t, kePub2.PrivateKeyIsCompatible(priv1))
k1, err := kePub1.ECDH(priv2)
require.NoError(t, err)
require.NotEmpty(t, k1)
k2, err := kePub2.ECDH(priv1)
require.NoError(t, err)
require.NotEmpty(t, k2)
require.Equal(t, k1, k2)
})
}
func BenchSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](b *testing.B, harness TestHarness[PubT, PrivT]) {
b.Run("GenerateKeyPair", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _, _ = harness.GenerateKeyPair()
}
})
b.Run("Bytes", func(b *testing.B) {
b.Run("PubToBytes", func(b *testing.B) {
pub, _, err := harness.GenerateKeyPair()
require.NoError(b, err)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = pub.ToBytes()
}
})
b.Run("PubFromBytes", func(b *testing.B) {
pub, _, err := harness.GenerateKeyPair()
require.NoError(b, err)
buf := pub.ToBytes()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = harness.PublicKeyFromBytes(buf)
}
})
b.Run("PrivToBytes", func(b *testing.B) {
_, priv, err := harness.GenerateKeyPair()
require.NoError(b, err)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = priv.ToBytes()
}
})
b.Run("PrivFromBytes", func(b *testing.B) {
_, priv, err := harness.GenerateKeyPair()
require.NoError(b, err)
buf := priv.ToBytes()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = harness.PrivateKeyFromBytes(buf)
}
})
})
b.Run("DER", func(b *testing.B) {
b.Run("PubToDER", func(b *testing.B) {
pub, _, err := harness.GenerateKeyPair()
require.NoError(b, err)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = pub.ToX509DER()
}
})
b.Run("PubFromDER", func(b *testing.B) {
pub, _, err := harness.GenerateKeyPair()
require.NoError(b, err)
buf := pub.ToX509DER()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = harness.PublicKeyFromX509DER(buf)
}
})
b.Run("PrivToDER", func(b *testing.B) {
_, priv, err := harness.GenerateKeyPair()
require.NoError(b, err)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = priv.ToPKCS8DER()
}
})
b.Run("PrivFromDER", func(b *testing.B) {
_, priv, err := harness.GenerateKeyPair()
require.NoError(b, err)
buf := priv.ToPKCS8DER()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = harness.PrivateKeyFromPKCS8DER(buf)
}
})
})
b.Run("PEM", func(b *testing.B) {
b.Run("PubToPEM", func(b *testing.B) {
pub, _, err := harness.GenerateKeyPair()
require.NoError(b, err)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = pub.ToX509PEM()
}
})
b.Run("PubFromPEM", func(b *testing.B) {
pub, _, err := harness.GenerateKeyPair()
require.NoError(b, err)
buf := pub.ToX509PEM()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = harness.PublicKeyFromX509PEM(buf)
}
})
b.Run("PrivToPEM", func(b *testing.B) {
_, priv, err := harness.GenerateKeyPair()
require.NoError(b, err)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = priv.ToPKCS8PEM()
}
})
b.Run("PrivFromPEM", func(b *testing.B) {
_, priv, err := harness.GenerateKeyPair()
require.NoError(b, err)
buf := priv.ToPKCS8PEM()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = harness.PrivateKeyFromPKCS8PEM(buf)
}
})
})
b.Run("Signatures", func(b *testing.B) {
if _, ok := (crypto.PublicKey(*new(PubT))).(crypto.SigningPublicKey); !ok {
b.Skip("Signature is not implemented")
}
b.Run("Sign", func(b *testing.B) {
_, priv, err := harness.GenerateKeyPair()
require.NoError(b, err)
spriv := (crypto.PrivateKey(priv)).(crypto.SigningPrivateKey)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
spriv.Sign([]byte("message"))
}
})
b.Run("Verify", func(b *testing.B) {
pub, priv, err := harness.GenerateKeyPair()
require.NoError(b, err)
spub := (crypto.PublicKey(pub)).(crypto.SigningPublicKey)
spriv := (crypto.PrivateKey(priv)).(crypto.SigningPrivateKey)
sig, err := spriv.Sign([]byte("message"))
require.NoError(b, err)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
spub.Verify([]byte("message"), sig)
}
})
})
// TODO: add key exchange benchmarks
}

30
crypto/p256/key.go Normal file
View File

@@ -0,0 +1,30 @@
package p256
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
)
const (
// TODO
PublicKeySize = 33
PrivateKeySize = 32
SignatureSize = 123456
MultibaseCode = uint64(0x1200)
)
func GenerateKeyPair() (*PublicKey, *PrivateKey, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, err
}
pub := priv.Public().(*ecdsa.PublicKey)
return (*PublicKey)(pub), (*PrivateKey)(priv), nil
}
const (
pemPubBlockType = "PUBLIC KEY"
pemPrivBlockType = "PRIVATE KEY"
)

31
crypto/p256/key_test.go Normal file
View File

@@ -0,0 +1,31 @@
package p256
import (
"testing"
"github.com/INFURA/go-did/crypto/internal"
)
var harness = helpers.TestHarness[*PublicKey, *PrivateKey]{
Name: "p256",
GenerateKeyPair: GenerateKeyPair,
PublicKeyFromBytes: PublicKeyFromBytes,
PublicKeyFromPublicKeyMultibase: PublicKeyFromPublicKeyMultibase,
PublicKeyFromX509DER: PublicKeyFromX509DER,
PublicKeyFromX509PEM: PublicKeyFromX509PEM,
PrivateKeyFromBytes: PrivateKeyFromBytes,
PrivateKeyFromPKCS8DER: PrivateKeyFromPKCS8DER,
PrivateKeyFromPKCS8PEM: PrivateKeyFromPKCS8PEM,
MultibaseCode: MultibaseCode,
PublicKeySize: PublicKeySize,
PrivateKeySize: PrivateKeySize,
SignatureSize: SignatureSize,
}
func TestSuite(t *testing.T) {
helpers.TestSuite(t, harness)
}
func BenchmarkSuite(b *testing.B) {
helpers.BenchSuite(b, harness)
}

94
crypto/p256/private.go Normal file
View File

@@ -0,0 +1,94 @@
package p256
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"github.com/INFURA/go-did/crypto"
)
var _ crypto.SigningPrivateKey = (*PrivateKey)(nil)
type PrivateKey ecdsa.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) != PrivateKeySize {
return nil, fmt.Errorf("invalid P-256 private key size")
}
res := &ecdsa.PrivateKey{
D: new(big.Int).SetBytes(b),
PublicKey: ecdsa.PublicKey{Curve: elliptic.P256()},
}
// recompute the public key
res.PublicKey.X, res.PublicKey.Y = res.PublicKey.Curve.ScalarBaseMult(b)
return (*PrivateKey)(res), nil
}
// PrivateKeyFromPKCS8DER decodes a PKCS#8 DER (binary) encoded private key.
func PrivateKeyFromPKCS8DER(bytes []byte) (*PrivateKey, error) {
priv, err := x509.ParsePKCS8PrivateKey(bytes)
if err != nil {
return nil, err
}
ecdsaPriv := priv.(*ecdsa.PrivateKey)
return (*PrivateKey)(ecdsaPriv), 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 (*ecdsa.PrivateKey)(p).Equal((*ecdsa.PrivateKey)(other))
}
return false
}
func (p *PrivateKey) Public() crypto.PublicKey {
ecdhPub := (*ecdsa.PrivateKey)(p).Public().(*ecdsa.PublicKey)
return (*PublicKey)(ecdhPub)
}
func (p *PrivateKey) ToBytes() []byte {
// fixed size buffer that can get allocated on the caller's stack after inlining.
var buf [PrivateKeySize]byte
((*ecdsa.PrivateKey)(p)).D.FillBytes(buf[:])
return buf[:]
}
func (p *PrivateKey) ToPKCS8DER() []byte {
res, _ := x509.MarshalPKCS8PrivateKey((*ecdsa.PrivateKey)(p))
return res
}
func (p *PrivateKey) ToPKCS8PEM() string {
der := p.ToPKCS8DER()
return string(pem.EncodeToMemory(&pem.Block{
Type: pemPrivBlockType,
Bytes: der,
}))
}
func (p *PrivateKey) Sign(message []byte) ([]byte, error) {
return (*ecdsa.PrivateKey)(p).Sign(rand.Reader, message, nil)
}

99
crypto/p256/public.go Normal file
View File

@@ -0,0 +1,99 @@
package p256
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/INFURA/go-did/crypto"
helpers "github.com/INFURA/go-did/crypto/internal"
)
var _ crypto.SigningPublicKey = (*PublicKey)(nil)
type PublicKey ecdsa.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) {
if len(b) != PublicKeySize {
return nil, fmt.Errorf("invalid P-256 public key size")
}
x, y := elliptic.UnmarshalCompressed(elliptic.P256(), b)
if x == nil {
return nil, fmt.Errorf("invalid P-256 public key")
}
return (*PublicKey)(&ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}), 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) {
pub, err := x509.ParsePKIXPublicKey(bytes)
if err != nil {
return nil, err
}
ecdsaPub := pub.(*ecdsa.PublicKey)
return (*PublicKey)(ecdsaPub), 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) Equal(other crypto.PublicKey) bool {
if other, ok := other.(*PublicKey); ok {
return (*ecdsa.PublicKey)(p).Equal((*ecdsa.PublicKey)(other))
}
return false
}
func (p *PublicKey) ToBytes() []byte {
ecdsaPub := (*ecdsa.PublicKey)(p)
return elliptic.MarshalCompressed(elliptic.P256(), ecdsaPub.X, ecdsaPub.Y)
}
func (p *PublicKey) ToPublicKeyMultibase() string {
ecdsaPub := (*ecdsa.PublicKey)(p)
bytes := elliptic.MarshalCompressed(elliptic.P256(), ecdsaPub.X, ecdsaPub.Y)
return helpers.PublicKeyMultibaseEncode(MultibaseCode, bytes)
}
func (p *PublicKey) ToX509DER() []byte {
res, _ := x509.MarshalPKIXPublicKey((*ecdsa.PublicKey)(p))
return res
}
func (p *PublicKey) ToX509PEM() string {
der := p.ToX509DER()
return string(pem.EncodeToMemory(&pem.Block{
Type: pemPubBlockType,
Bytes: der,
}))
}
func (p *PublicKey) Verify(message, signature []byte) bool {
panic("not implemented")
}

31
crypto/x25519/key.go Normal file
View File

@@ -0,0 +1,31 @@
package x25519
import (
"crypto/ecdh"
"crypto/rand"
)
const (
// PublicKeySize is the size, in bytes, of public keys as used in this package.
PublicKeySize = 32
// PrivateKeySize is the size, in bytes, of private keys as used in this package.
PrivateKeySize = 32
// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
SignatureSize = 32
MultibaseCode = uint64(0xec)
)
func GenerateKeyPair() (*PublicKey, *PrivateKey, error) {
priv, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
pub := priv.Public().(*ecdh.PublicKey)
return (*PublicKey)(pub), (*PrivateKey)(priv), nil
}
const (
pemPubBlockType = "PUBLIC KEY"
pemPrivBlockType = "PRIVATE KEY"
)

71
crypto/x25519/key_test.go Normal file
View File

@@ -0,0 +1,71 @@
package x25519
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/crypto/ed25519"
"github.com/INFURA/go-did/crypto/internal"
)
var harness = helpers.TestHarness[*PublicKey, *PrivateKey]{
Name: "x25519",
GenerateKeyPair: GenerateKeyPair,
PublicKeyFromBytes: PublicKeyFromBytes,
PublicKeyFromPublicKeyMultibase: PublicKeyFromPublicKeyMultibase,
PublicKeyFromX509DER: PublicKeyFromX509DER,
PublicKeyFromX509PEM: PublicKeyFromX509PEM,
PrivateKeyFromBytes: PrivateKeyFromBytes,
PrivateKeyFromPKCS8DER: PrivateKeyFromPKCS8DER,
PrivateKeyFromPKCS8PEM: PrivateKeyFromPKCS8PEM,
MultibaseCode: MultibaseCode,
PublicKeySize: PublicKeySize,
PrivateKeySize: PrivateKeySize,
SignatureSize: SignatureSize,
}
func TestSuite(t *testing.T) {
helpers.TestSuite(t, harness)
}
func BenchmarkSuite(b *testing.B) {
helpers.BenchSuite(b, harness)
}
func TestEd25519ToX25519(t *testing.T) {
// Known pubkey ed25519 --> x25519
for _, tc := range []struct {
pubEdMultibase string
pubXMultibase string
}{
{
// From https://w3c-ccg.github.io/did-key-spec/#ed25519-with-x25519
pubEdMultibase: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
pubXMultibase: "z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p",
},
} {
t.Run(tc.pubEdMultibase, func(t *testing.T) {
pubEd, err := ed25519.PublicKeyFromPublicKeyMultibase(tc.pubEdMultibase)
require.NoError(t, err)
pubX, err := PublicKeyFromEd25519(pubEd)
require.NoError(t, err)
require.Equal(t, tc.pubXMultibase, pubX.ToPublicKeyMultibase())
})
}
// Check that ed25519 --> x25519 match for pubkeys and privkeys
t.Run("ed25519 --> x25519 priv+pub are matching", func(t *testing.T) {
for i := 0; i < 10; i++ {
pubEd, privEd, err := ed25519.GenerateKeyPair()
require.NoError(t, err)
pubX, err := PublicKeyFromEd25519(pubEd)
require.NoError(t, err)
privX, err := PrivateKeyFromEd25519(privEd)
require.NoError(t, err)
require.True(t, pubX.Equal(privX.Public()))
}
})
}

97
crypto/x25519/private.go Normal file
View File

@@ -0,0 +1,97 @@
package x25519
import (
"crypto/ecdh"
"crypto/sha512"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/ed25519"
)
var _ crypto.PrivateKey = (*PrivateKey)(nil)
type PrivateKey ecdh.PrivateKey
// PrivateKeyFromBytes converts a serialized private key to a PrivateKey.
// This compact serialization format is the raw key material, without metadata or structure.
// It errors if len(privateKey) is not [PrivateKeySize].
func PrivateKeyFromBytes(b []byte) (*PrivateKey, error) {
// this already check the size of b
priv, err := ecdh.X25519().NewPrivateKey(b)
if err != nil {
return nil, err
}
return (*PrivateKey)(priv), nil
}
// PrivateKeyFromEd25519 converts an ed25519 private key to a x25519 private key.
// It errors if the slice is not the right size.
//
// This function is based on the algorithm described in https://datatracker.ietf.org/doc/html/draft-ietf-core-oscore-groupcomm#name-curve25519
func PrivateKeyFromEd25519(priv ed25519.PrivateKey) (*PrivateKey, error) {
// get the 32-byte seed (first half of the private key)
seed := priv.Seed()
h := sha512.Sum512(seed)
// clamp as per the X25519 spec
h[0] &= 248
h[31] &= 127
h[31] |= 64
return PrivateKeyFromBytes(h[:32])
}
// PrivateKeyFromPKCS8DER decodes a PKCS#8 DER (binary) encoded private key.
func PrivateKeyFromPKCS8DER(bytes []byte) (*PrivateKey, error) {
priv, err := x509.ParsePKCS8PrivateKey(bytes)
if err != nil {
return nil, err
}
ecdhPriv := priv.(*ecdh.PrivateKey)
return (*PrivateKey)(ecdhPriv), 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 (*ecdh.PrivateKey)(p).Equal((*ecdh.PrivateKey)(other))
}
return false
}
func (p *PrivateKey) Public() crypto.PublicKey {
ecdhPub := (*ecdh.PrivateKey)(p).Public().(*ecdh.PublicKey)
return (*PublicKey)(ecdhPub)
}
func (p *PrivateKey) ToBytes() []byte {
return (*ecdh.PrivateKey)(p).Bytes()
}
func (p *PrivateKey) ToPKCS8DER() []byte {
res, _ := x509.MarshalPKCS8PrivateKey((*ecdh.PrivateKey)(p))
return res
}
func (p *PrivateKey) ToPKCS8PEM() string {
der := p.ToPKCS8DER()
return string(pem.EncodeToMemory(&pem.Block{
Type: pemPrivBlockType,
Bytes: der,
}))
}

168
crypto/x25519/public.go Normal file
View File

@@ -0,0 +1,168 @@
package x25519
import (
"crypto/ecdh"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/ed25519"
helpers "github.com/INFURA/go-did/crypto/internal"
)
var _ crypto.KeyExchangePublicKey = (*PublicKey)(nil)
type PublicKey ecdh.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 := ecdh.X25519().NewPublicKey(b)
if err != nil {
return nil, err
}
return (*PublicKey)(pub), nil
}
// PublicKeyFromEd25519 converts an ed25519 public key to a x25519 public key.
// It errors if the slice is not the right size.
//
// This function is based on the algorithm described in https://datatracker.ietf.org/doc/html/draft-ietf-core-oscore-groupcomm#name-curve25519
func PublicKeyFromEd25519(pub ed25519.PublicKey) (*PublicKey, error) {
// Conversion formula is u = (1 + y) / (1 - y) (mod p)
// See https://datatracker.ietf.org/doc/html/draft-ietf-core-oscore-groupcomm#name-ecdh-with-montgomery-coordi
pubBytes := pub.ToBytes()
// Clear the sign bit (MSB of last byte)
// This is because ed25519 serialize as bytes with 255 bit for Y, and one bit for the sign.
// We only want Y, and the sign is irrelevant for the conversion.
pubBytes[ed25519.PublicKeySize-1] &= 0x7F
// ed25519 are little-endian, but big.Int expects big-endian
// See https://www.rfc-editor.org/rfc/rfc8032
y := new(big.Int).SetBytes(reverseBytes(pubBytes))
one := big.NewInt(1)
negOne := big.NewInt(-1)
if y.Cmp(one) == 0 || y.Cmp(negOne) == 0 {
return nil, fmt.Errorf("x25519 undefined for this public key")
}
// p = 2^255-19
//
// Equivalent to:
// two := big.NewInt(2)
// exp := big.NewInt(255)
// p := new(big.Int).Exp(two, exp, nil)
// p.Sub(p, big.NewInt(19))
//
p := new(big.Int).SetBytes([]byte{
0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xed,
})
onePlusY := new(big.Int).Add(one, y)
oneMinusY := new(big.Int).Sub(one, y)
oneMinusYInv := new(big.Int).ModInverse(oneMinusY, p)
u := new(big.Int).Mul(onePlusY, oneMinusYInv)
u.Mod(u, p)
// make sure we get 32 bytes, pad if necessary
uBytes := u.Bytes()
res := make([]byte, PublicKeySize)
copy(res[PublicKeySize-len(uBytes):], uBytes)
// x25519 are little-endian, but big.Int gives us big-endian.
// See https://www.ietf.org/rfc/rfc7748.txt
return PublicKeyFromBytes(reverseBytes(res))
}
// 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) {
pub, err := x509.ParsePKIXPublicKey(bytes)
if err != nil {
return nil, err
}
ecdhPub := pub.(*ecdh.PublicKey)
return (*PublicKey)(ecdhPub), 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) Equal(other crypto.PublicKey) bool {
if other, ok := other.(*PublicKey); ok {
return (*ecdh.PublicKey)(p).Equal((*ecdh.PublicKey)(other))
}
return false
}
func (p *PublicKey) ToBytes() []byte {
return (*ecdh.PublicKey)(p).Bytes()
}
func (p *PublicKey) ToPublicKeyMultibase() string {
return helpers.PublicKeyMultibaseEncode(MultibaseCode, (*ecdh.PublicKey)(p).Bytes())
}
func (p *PublicKey) ToX509DER() []byte {
res, _ := x509.MarshalPKIXPublicKey((*ecdh.PublicKey)(p))
return res
}
func (p *PublicKey) ToX509PEM() string {
der := p.ToX509DER()
return string(pem.EncodeToMemory(&pem.Block{
Type: pemPubBlockType,
Bytes: der,
}))
}
func (p *PublicKey) PrivateKeyIsCompatible(local crypto.PrivateKey) bool {
if _, ok := local.(*PrivateKey); ok {
return true
}
return false
}
func (p *PublicKey) ECDH(local crypto.PrivateKey) ([]byte, error) {
if local, ok := local.(*PrivateKey); ok {
return (*ecdh.PrivateKey)(local).ECDH((*ecdh.PublicKey)(p))
}
return nil, fmt.Errorf("incompatible private key")
}
func reverseBytes(b []byte) []byte {
r := make([]byte, len(b))
for i := 0; i < len(b); i++ {
r[i] = b[len(b)-1-i]
}
return r
}