crypto: reasonably complete the keypair absraction layer, and use it

This commit is contained in:
Michael Muré
2025-06-24 14:05:42 +02:00
parent 162aff3046
commit 371d9f55b2
22 changed files with 195 additions and 480 deletions

View File

@@ -1,43 +1,74 @@
package crypto
type PublicKey interface {
// Equal returns true if other is the same PublicKey
Equal(other PublicKey) bool
// ToBytes serializes the PublicKey into "raw bytes", without metadata or structure.
// This format can make some assumptions and may not be what you expect.
// Ideally, this format is defined by the same specification as the underlying crypto scheme.
ToBytes() []byte
// ToPublicKeyMultibase format the PublicKey into a string compatible with a PublicKeyMultibase field
// in a DID Document.
ToPublicKeyMultibase() string
// ToX509DER serializes the PublicKey into the X.509 DER (binary) format.
ToX509DER() []byte
// ToX509PEM serializes the PublicKey into the X.509 PEM (string) format.
ToX509PEM() string
}
type PrivateKey interface {
// Equal returns true if other is the same PrivateKey
Equal(other PrivateKey) bool
// Public returns the matching PublicKey.
Public() PublicKey
// ToBytes serializes the PrivateKey into "raw bytes", without metadata or structure.
// This format can make some assumptions and may not be what you expect.
// Ideally, this format is defined by the same specification as the underlying crypto scheme.
ToBytes() []byte
// ToPKCS8DER serializes the PrivateKey into the PKCS#8 DER (binary) format.
ToPKCS8DER() []byte
// ToPKCS8PEM serializes the PrivateKey into the PKCS#8 PEM (string) format.
ToPKCS8PEM() string
}
type SigningPublicKey interface {
PublicKey
// VerifyBytes checks a signature in the "raw bytes" format.
// This format can make some assumptions and may not be what you expect.
// Ideally, this format is defined by the same specification as the underlying crypto scheme.
VerifyBytes(message, signature []byte) bool
// VerifyASN1 checks a signature in the ASN.1 format.
VerifyASN1(message, signature []byte) bool
}
type SigningPrivateKey interface {
PrivateKey
// SignToBytes creates a signature in the "raw bytes" format.
// This format can make some assumptions and may not be what you expect.
// Ideally, this format is defined by the same specification as the underlying crypto scheme.
SignToBytes(message []byte) ([]byte, error)
// SignToASN1 creates a signature in the ASN.1 format.
SignToASN1(message []byte) ([]byte, error)
}
type KeyExchangePublicKey interface {
PublicKey
type KeyExchangePrivateKey interface {
PrivateKey
// PrivateKeyIsCompatible checks that the given PrivateKey is compatible to perform key exchange.
PrivateKeyIsCompatible(local PrivateKey) bool
// PublicKeyIsCompatible checks that the given PublicKey is compatible to perform key exchange.
PublicKeyIsCompatible(remote PublicKey) bool
// ECDH computes the shared key using the given PrivateKey.
ECDH(local PrivateKey) ([]byte, error)
// KeyExchange computes the shared key using the given PublicKey.
KeyExchange(remote PublicKey) ([]byte, error)
}

View File

@@ -229,25 +229,34 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
require.NoError(t, err)
pub2, priv2, err := harness.GenerateKeyPair()
require.NoError(t, err)
pub3, _, err := harness.GenerateKeyPair()
require.NoError(t, err)
kePub1, ok := (crypto.PublicKey(pub1)).(crypto.KeyExchangePublicKey)
kePriv1, ok := crypto.PrivateKey(priv1).(crypto.KeyExchangePrivateKey)
if !ok {
t.Skip("Key exchange is not implemented")
}
kePub2 := (crypto.PublicKey(pub2)).(crypto.KeyExchangePublicKey)
kePriv2 := crypto.PrivateKey(priv2).(crypto.KeyExchangePrivateKey)
// TODO: test with incompatible private keys
require.True(t, kePub1.PrivateKeyIsCompatible(priv2))
require.True(t, kePub2.PrivateKeyIsCompatible(priv1))
// TODO: test with incompatible public keys
require.True(t, kePriv1.PublicKeyIsCompatible(pub2))
require.True(t, kePriv2.PublicKeyIsCompatible(pub1))
k1, err := kePub1.ECDH(priv2)
// 1 --> 2
kA, err := kePriv1.KeyExchange(pub2)
require.NoError(t, err)
require.NotEmpty(t, k1)
k2, err := kePub2.ECDH(priv1)
require.NotEmpty(t, kA)
// 2 --> 1
kB, err := kePriv2.KeyExchange(pub1)
require.NoError(t, err)
require.NotEmpty(t, k2)
require.NotEmpty(t, kB)
// 2 --> 3
kC, err := kePriv2.KeyExchange(pub3)
require.NoError(t, err)
require.NotEmpty(t, kC)
require.Equal(t, k1, k2)
require.Equal(t, kA, kB)
require.NotEqual(t, kB, kC)
})
}
@@ -459,5 +468,24 @@ func BenchSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](b *testing.B, ha
})
})
// TODO: add key exchange benchmarks
b.Run("Key exchange", func(b *testing.B) {
if _, ok := (crypto.PrivateKey(*new(PrivT))).(crypto.KeyExchangePrivateKey); !ok {
b.Skip("Key echange is not implemented")
}
b.Run("KeyExchange", func(b *testing.B) {
_, priv1, err := harness.GenerateKeyPair()
require.NoError(b, err)
kePriv1 := (crypto.PrivateKey(priv1)).(crypto.KeyExchangePrivateKey)
pub2, _, err := harness.GenerateKeyPair()
require.NoError(b, err)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = kePriv1.KeyExchange(pub2)
}
})
})
}

View File

@@ -14,6 +14,7 @@ import (
)
var _ crypto.SigningPrivateKey = (*PrivateKey)(nil)
var _ crypto.KeyExchangePrivateKey = (*PrivateKey)(nil)
type PrivateKey ecdsa.PrivateKey
@@ -116,7 +117,29 @@ func (p *PrivateKey) SignToASN1(message []byte) ([]byte, error) {
// Hash the message with SHA-256
hash := sha256.Sum256(message)
// Use ecdsa.SignASN1 for direct ASN.1 DER encoding
return ecdsa.SignASN1(rand.Reader, (*ecdsa.PrivateKey)(p), hash[:])
}
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 := (*ecdsa.PrivateKey)(p).ECDH()
if err != nil {
return nil, err
}
ecdhPub, err := (*ecdsa.PublicKey)(remote).ECDH()
if err != nil {
return nil, err
}
return ecdhPriv.ECDH(ecdhPub)
}
return nil, fmt.Errorf("incompatible public key")
}

View File

@@ -113,7 +113,6 @@ func (p *PublicKey) VerifyBytes(message, signature []byte) bool {
r := new(big.Int).SetBytes(signature[:32])
s := new(big.Int).SetBytes(signature[32:])
// Use ecdsa.Verify
return ecdsa.Verify((*ecdsa.PublicKey)(p), hash[:], r, s)
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/INFURA/go-did/crypto/ed25519"
)
var _ crypto.PrivateKey = (*PrivateKey)(nil)
var _ crypto.KeyExchangePrivateKey = (*PrivateKey)(nil)
type PrivateKey ecdh.PrivateKey
@@ -95,3 +95,17 @@ func (p *PrivateKey) ToPKCS8PEM() string {
Bytes: der,
}))
}
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 local, ok := remote.(*PublicKey); ok {
return (*ecdh.PrivateKey)(p).ECDH((*ecdh.PublicKey)(local))
}
return nil, fmt.Errorf("incompatible public key")
}

View File

@@ -12,7 +12,7 @@ import (
helpers "github.com/INFURA/go-did/crypto/internal"
)
var _ crypto.KeyExchangePublicKey = (*PublicKey)(nil)
var _ crypto.PublicKey = (*PublicKey)(nil)
type PublicKey ecdh.PublicKey
@@ -145,20 +145,6 @@ func (p *PublicKey) ToX509PEM() string {
}))
}
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++ {