From 5be4ab8175f93238fa85e55bfd75e64958403446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 19 Jun 2025 18:17:54 +0200 Subject: [PATCH] add a crypto base layer that abstract various keypair type, + test/bench suite --- crypto/ed25519/key.go | 30 +++ crypto/ed25519/key_test.go | 31 +++ crypto/ed25519/private.go | 90 ++++++++ crypto/ed25519/public.go | 99 +++++++++ crypto/interface.go | 41 ++++ crypto/internal/multibase.go | 35 ++++ crypto/internal/testSuite.go | 394 +++++++++++++++++++++++++++++++++++ crypto/p256/key.go | 30 +++ crypto/p256/key_test.go | 31 +++ crypto/p256/private.go | 94 +++++++++ crypto/p256/public.go | 99 +++++++++ crypto/x25519/key.go | 31 +++ crypto/x25519/key_test.go | 71 +++++++ crypto/x25519/private.go | 97 +++++++++ crypto/x25519/public.go | 168 +++++++++++++++ 15 files changed, 1341 insertions(+) create mode 100644 crypto/ed25519/key.go create mode 100644 crypto/ed25519/key_test.go create mode 100644 crypto/ed25519/private.go create mode 100644 crypto/ed25519/public.go create mode 100644 crypto/interface.go create mode 100644 crypto/internal/multibase.go create mode 100644 crypto/internal/testSuite.go create mode 100644 crypto/p256/key.go create mode 100644 crypto/p256/key_test.go create mode 100644 crypto/p256/private.go create mode 100644 crypto/p256/public.go create mode 100644 crypto/x25519/key.go create mode 100644 crypto/x25519/key_test.go create mode 100644 crypto/x25519/private.go create mode 100644 crypto/x25519/public.go diff --git a/crypto/ed25519/key.go b/crypto/ed25519/key.go new file mode 100644 index 0000000..ce97eb8 --- /dev/null +++ b/crypto/ed25519/key.go @@ -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" +) diff --git a/crypto/ed25519/key_test.go b/crypto/ed25519/key_test.go new file mode 100644 index 0000000..2eaa3c5 --- /dev/null +++ b/crypto/ed25519/key_test.go @@ -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) +} diff --git a/crypto/ed25519/private.go b/crypto/ed25519/private.go new file mode 100644 index 0000000..67d4370 --- /dev/null +++ b/crypto/ed25519/private.go @@ -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() +} diff --git a/crypto/ed25519/public.go b/crypto/ed25519/public.go new file mode 100644 index 0000000..fedad65 --- /dev/null +++ b/crypto/ed25519/public.go @@ -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) +} diff --git a/crypto/interface.go b/crypto/interface.go new file mode 100644 index 0000000..2ddc5cb --- /dev/null +++ b/crypto/interface.go @@ -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) +} diff --git a/crypto/internal/multibase.go b/crypto/internal/multibase.go new file mode 100644 index 0000000..4fa08f9 --- /dev/null +++ b/crypto/internal/multibase.go @@ -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 +} diff --git a/crypto/internal/testSuite.go b/crypto/internal/testSuite.go new file mode 100644 index 0000000..d76068b --- /dev/null +++ b/crypto/internal/testSuite.go @@ -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 +} diff --git a/crypto/p256/key.go b/crypto/p256/key.go new file mode 100644 index 0000000..f7c862f --- /dev/null +++ b/crypto/p256/key.go @@ -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" +) diff --git a/crypto/p256/key_test.go b/crypto/p256/key_test.go new file mode 100644 index 0000000..b3aefea --- /dev/null +++ b/crypto/p256/key_test.go @@ -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) +} diff --git a/crypto/p256/private.go b/crypto/p256/private.go new file mode 100644 index 0000000..bdc50a2 --- /dev/null +++ b/crypto/p256/private.go @@ -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) +} diff --git a/crypto/p256/public.go b/crypto/p256/public.go new file mode 100644 index 0000000..5b4d895 --- /dev/null +++ b/crypto/p256/public.go @@ -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") +} diff --git a/crypto/x25519/key.go b/crypto/x25519/key.go new file mode 100644 index 0000000..7d03a8f --- /dev/null +++ b/crypto/x25519/key.go @@ -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" +) diff --git a/crypto/x25519/key_test.go b/crypto/x25519/key_test.go new file mode 100644 index 0000000..6a7bcba --- /dev/null +++ b/crypto/x25519/key_test.go @@ -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())) + } + }) +} diff --git a/crypto/x25519/private.go b/crypto/x25519/private.go new file mode 100644 index 0000000..0bd81f6 --- /dev/null +++ b/crypto/x25519/private.go @@ -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, + })) +} diff --git a/crypto/x25519/public.go b/crypto/x25519/public.go new file mode 100644 index 0000000..5df2f5f --- /dev/null +++ b/crypto/x25519/public.go @@ -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 +}