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++ {

View File

@@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/crypto/x25519"
_ "github.com/INFURA/go-did/methods/did-key"
"github.com/INFURA/go-did/verifications/x25519"
)
func ExampleSignature() {

View File

@@ -51,11 +51,11 @@ func TestRoundTrip(t *testing.T) {
// basic testing
require.Equal(t, "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", doc.ID())
require.Equal(t, ed25519.Type, doc.Authentication()[0].Type())
require.Equal(t, ed25519.Type, doc.Assertion()[0].Type())
require.Equal(t, x25519.Type, doc.KeyAgreement()[0].Type())
require.Equal(t, ed25519.Type, doc.CapabilityInvocation()[0].Type())
require.Equal(t, ed25519.Type, doc.CapabilityDelegation()[0].Type())
require.Equal(t, ed25519vm.Type, doc.Authentication()[0].Type())
require.Equal(t, ed25519vm.Type, doc.Assertion()[0].Type())
require.Equal(t, x25519vm.Type, doc.KeyAgreement()[0].Type())
require.Equal(t, ed25519vm.Type, doc.CapabilityInvocation()[0].Type())
require.Equal(t, ed25519vm.Type, doc.CapabilityDelegation()[0].Type())
roundtrip, err := json.Marshal(doc)
require.NoError(t, err)

View File

@@ -1,9 +1,10 @@
package did
import (
"crypto"
"encoding/json"
"net/url"
"github.com/INFURA/go-did/crypto"
)
// DID is a decoded (i.e. from a string) Decentralized Identifier.
@@ -110,20 +111,8 @@ type VerificationMethodKeyAgreement interface {
VerificationMethod
// PrivateKeyIsCompatible checks that the given PrivateKey is compatible with this method.
PrivateKeyIsCompatible(local PrivateKey) bool
PrivateKeyIsCompatible(local crypto.KeyExchangePrivateKey) bool
// ECDH computes the shared key using the given PrivateKey.
ECDH(local PrivateKey) ([]byte, error)
}
// Below are the interfaces for crypto.PublicKey and crypto.PrivateKey in the go standard library.
// They are not defined there for compatibility reasons, so we need to define them here.
type PublicKey interface {
Equal(x crypto.PublicKey) bool
}
type PrivateKey interface {
Public() crypto.PublicKey
Equal(x crypto.PrivateKey) bool
// KeyExchange computes the shared key using the given PrivateKey.
KeyExchange(local crypto.KeyExchangePrivateKey) ([]byte, error)
}

View File

@@ -8,6 +8,10 @@ import (
"github.com/multiformats/go-varint"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/ed25519"
"github.com/INFURA/go-did/crypto/p256"
"github.com/INFURA/go-did/crypto/x25519"
"github.com/INFURA/go-did/verifications/ed25519"
"github.com/INFURA/go-did/verifications/x25519"
)
@@ -55,8 +59,13 @@ func Decode(identifier string) (did.DID, error) {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
return FromPublicKey(pub)
case p256.MultibaseCode:
pub, err := p256.PublicKeyFromBytes(bytes[read:])
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
return FromPublicKey(pub)
// case P256: // TODO
// case Secp256k1: // TODO
// case RSA: // TODO
}
@@ -64,12 +73,12 @@ func Decode(identifier string) (did.DID, error) {
return nil, fmt.Errorf("%w: unsupported did:key multicodec: 0x%x", did.ErrInvalidDid, code)
}
func FromPublicKey(pub did.PublicKey) (did.DID, error) {
func FromPublicKey(pub crypto.PublicKey) (did.DID, error) {
var err error
switch pub := pub.(type) {
case ed25519.PublicKey:
d := DidKey{msi: ed25519.PublicKeyToMultibase(pub)}
d.signature, err = ed25519.NewVerificationKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, d.msi), pub, d)
d := DidKey{msi: pub.ToPublicKeyMultibase()}
d.signature, err = ed25519vm.NewVerificationKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, d.msi), pub, d)
if err != nil {
return nil, err
}
@@ -77,20 +86,22 @@ func FromPublicKey(pub did.PublicKey) (did.DID, error) {
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
xmsi := x25519.PublicKeyToMultibase(xpub)
d.keyAgreement, err = x25519.NewKeyAgreementKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, xmsi), xpub, d)
xmsi := xpub.ToPublicKeyMultibase()
d.keyAgreement, err = x25519vm.NewKeyAgreementKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, xmsi), xpub, d)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
return d, nil
// case *p256.PublicKey:
// d := DidKey{msi: pub.ToPublicKeyMultibase()}
default:
return nil, fmt.Errorf("unsupported public key: %T", pub)
}
}
func FromPrivateKey(priv did.PrivateKey) (did.DID, error) {
return FromPublicKey(priv.Public().(did.PublicKey))
func FromPrivateKey(priv crypto.PrivateKey) (did.DID, error) {
return FromPublicKey(priv.Public().(crypto.PublicKey))
}
func (d DidKey) Method() string {

View File

@@ -8,36 +8,31 @@ import (
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/crypto/ed25519"
didkey "github.com/INFURA/go-did/methods/did-key"
"github.com/INFURA/go-did/verifications/ed25519"
)
func ExampleGenerateKeyPair() {
// Generate a key pair
pub, priv, err := ed25519.GenerateKeyPair()
if err != nil {
panic(err)
}
fmt.Println("Public key:", ed25519.PublicKeyToMultibase(pub))
fmt.Println("Private key:", base64.StdEncoding.EncodeToString(priv))
handleErr(err)
fmt.Println("Public key:", pub.ToPublicKeyMultibase())
fmt.Println("Private key:", base64.StdEncoding.EncodeToString(priv.ToBytes()))
// Make the associated did:key
dk, err := didkey.FromPrivateKey(priv)
if err != nil {
panic(err)
}
handleErr(err)
fmt.Println("Did:", dk.String())
// Produce a signature
msg := []byte("message")
sig := ed25519.Sign(priv, msg)
sig, err := priv.SignToBytes(msg)
handleErr(err)
fmt.Println("Signature:", base64.StdEncoding.EncodeToString(sig))
// Resolve the DID and verify a signature
doc, err := dk.Document()
if err != nil {
panic(err)
}
handleErr(err)
ok, _ := did.TryAllVerify(doc.Authentication(), msg, sig)
fmt.Println("Signature verified:", ok)
}
@@ -72,3 +67,9 @@ func TestEquivalence(t *testing.T) {
require.True(t, did0A.Equal(did0B))
require.False(t, did0A.Equal(did1))
}
func handleErr(err error) {
if err != nil {
panic(err)
}
}

View File

@@ -2,6 +2,8 @@ package did
import (
"fmt"
"github.com/INFURA/go-did/crypto"
)
// TryAllVerify tries to verify the signature with all the methods in the slice.
@@ -19,10 +21,10 @@ func TryAllVerify(methods []VerificationMethodSignature, data []byte, sig []byte
// FindMatchingKeyAgreement tries to find a matching key agreement method for the given private key type.
// It returns the shared key as well as the selected method.
// If no matching method is found, it returns an error.
func FindMatchingKeyAgreement(methods []VerificationMethodKeyAgreement, priv PrivateKey) ([]byte, VerificationMethodKeyAgreement, error) {
func FindMatchingKeyAgreement(methods []VerificationMethodKeyAgreement, priv crypto.KeyExchangePrivateKey) ([]byte, VerificationMethodKeyAgreement, error) {
for _, method := range methods {
if method.PrivateKeyIsCompatible(priv) {
key, err := method.ECDH(priv)
key, err := method.KeyExchange(priv)
return key, method, err
}
}

View File

@@ -1,18 +1,17 @@
package ed25519
package ed25519vm
import (
"crypto/ed25519"
"encoding/json"
"errors"
"fmt"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/crypto/ed25519"
)
// Specification: https://w3c.github.io/cg-reports/credentials/CG-FINAL-di-eddsa-2020-20220724/
const (
MultibaseCode = uint64(0xed)
JsonLdContext = "https://w3id.org/security/suites/ed25519-2020/v1"
Type = "Ed25519VerificationKey2020"
)
@@ -21,15 +20,11 @@ var _ did.VerificationMethodSignature = &VerificationKey2020{}
type VerificationKey2020 struct {
id string
pubkey PublicKey
pubkey ed25519.PublicKey
controller string
}
func NewVerificationKey2020(id string, pubkey PublicKey, controller did.DID) (*VerificationKey2020, error) {
if len(pubkey) != PublicKeySize {
return nil, errors.New("invalid ed25519 public key size")
}
func NewVerificationKey2020(id string, pubkey ed25519.PublicKey, controller did.DID) (*VerificationKey2020, error) {
return &VerificationKey2020{
id: id,
pubkey: pubkey,
@@ -47,7 +42,7 @@ func (v VerificationKey2020) MarshalJSON() ([]byte, error) {
ID: v.ID(),
Type: v.Type(),
Controller: v.Controller(),
PublicKeyMultibase: PublicKeyToMultibase(v.pubkey),
PublicKeyMultibase: v.pubkey.ToPublicKeyMultibase(),
})
}
@@ -69,7 +64,7 @@ func (v *VerificationKey2020) UnmarshalJSON(bytes []byte) error {
if len(v.id) == 0 {
return errors.New("invalid id")
}
v.pubkey, err = PublicKeyFromMultibase(aux.PublicKeyMultibase)
v.pubkey, err = ed25519.PublicKeyFromPublicKeyMultibase(aux.PublicKeyMultibase)
if err != nil {
return fmt.Errorf("invalid publicKeyMultibase: %w", err)
}
@@ -97,5 +92,5 @@ func (v VerificationKey2020) JsonLdContext() string {
}
func (v VerificationKey2020) Verify(data []byte, sig []byte) bool {
return ed25519.Verify(v.pubkey, data, sig)
return v.pubkey.VerifyBytes(data, sig)
}

View File

@@ -1,4 +1,4 @@
package ed25519_test
package ed25519vm_test
import (
"encoding/hex"
@@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/crypto/ed25519"
_ "github.com/INFURA/go-did/methods/did-key"
"github.com/INFURA/go-did/verifications/ed25519"
)
@@ -20,7 +21,7 @@ func TestJsonRoundTrip(t *testing.T) {
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}`
var vk ed25519.VerificationKey2020
var vk ed25519vm.VerificationKey2020
err := json.Unmarshal([]byte(data), &vk)
require.NoError(t, err)
@@ -37,9 +38,9 @@ func TestSignature(t *testing.T) {
pk, err := ed25519.PublicKeyFromBytes(pkBytes)
require.NoError(t, err)
contDid := "did:key:" + ed25519.PublicKeyToMultibase(pk)
contDid := "did:key:" + pk.ToPublicKeyMultibase()
controller := did.MustParse(contDid)
vk, err := ed25519.NewVerificationKey2020("foo", pk, controller)
vk, err := ed25519vm.NewVerificationKey2020("foo", pk, controller)
require.NoError(t, err)
for _, tc := range []struct {

View File

@@ -1,69 +0,0 @@
package ed25519
import (
"crypto/ed25519"
"crypto/rand"
"fmt"
"github.com/INFURA/go-did/verifications/internal"
)
type PublicKey = ed25519.PublicKey
type PrivateKey = ed25519.PrivateKey
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
)
func GenerateKeyPair() (PublicKey, PrivateKey, error) {
return ed25519.GenerateKey(rand.Reader)
}
// PublicKeyFromBytes converts a serialized public key to a PublicKey.
// 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 ed25519 public key size")
}
return PublicKey(b), nil
}
// PublicKeyFromMultibase decodes the public key from its Multibase form
func PublicKeyFromMultibase(multibase string) (PublicKey, error) {
code, bytes, err := helpers.MultibaseDecode(multibase)
if err != nil {
return nil, err
}
if code != MultibaseCode {
return nil, fmt.Errorf("invalid code")
}
if len(bytes) != PublicKeySize {
return nil, fmt.Errorf("invalid ed25519 public key size")
}
return bytes, nil
}
// PublicKeyToMultibase encodes the public key in a suitable way for publicKeyMultibase
func PublicKeyToMultibase(pub PublicKey) string {
return helpers.MultibaseEncode(MultibaseCode, pub)
}
// PrivateKeyFromBytes converts a serialized public key to a PrivateKey.
// It errors if the slice is not the right size.
func PrivateKeyFromBytes(b []byte) (PrivateKey, error) {
if len(b) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid ed25519 private key size")
}
return b, nil
}
// Sign signs the message with privateKey and returns a signature.
// It will panic if len(privateKey) is not [PrivateKeySize].
func Sign(privateKey PrivateKey, message []byte) []byte {
return ed25519.Sign(privateKey, message)
}

View File

@@ -1,27 +0,0 @@
package ed25519_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/verifications/ed25519"
)
func TestGenerateKey(t *testing.T) {
pub, priv, err := ed25519.GenerateKeyPair()
require.NoError(t, err)
require.NotNil(t, pub)
require.NotNil(t, priv)
require.True(t, pub.Equal(priv.Public()))
}
func TestMultibaseRoundTrip(t *testing.T) {
pub, _, err := ed25519.GenerateKeyPair()
require.NoError(t, err)
mb := ed25519.PublicKeyToMultibase(pub)
rt, err := ed25519.PublicKeyFromMultibase(mb)
require.NoError(t, err)
require.Equal(t, pub, rt)
}

View File

@@ -1,35 +0,0 @@
package helpers
import (
"fmt"
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
)
// MultibaseDecode is a helper for decoding multibase public keys.
func MultibaseDecode(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
}
// MultibaseEncode is a helper for encoding multibase public keys.
func MultibaseEncode(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

@@ -19,10 +19,10 @@ func UnmarshalJSON(data []byte) (did.VerificationMethod, error) {
var res did.VerificationMethod
switch aux.Type {
case ed25519.Type:
res = &ed25519.VerificationKey2020{}
case x25519.Type:
res = &x25519.KeyAgreementKey2020{}
case ed25519vm.Type:
res = &ed25519vm.VerificationKey2020{}
case x25519vm.Type:
res = &x25519vm.KeyAgreementKey2020{}
default:
return nil, fmt.Errorf("unknown verification type: %s", aux.Type)
}

View File

@@ -1,18 +1,18 @@
package x25519
package x25519vm
import (
"crypto/ecdh"
"encoding/json"
"errors"
"fmt"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/x25519"
)
// Specification: https://w3c-ccg.github.io/did-method-key/#ed25519-x25519
const (
MultibaseCode = uint64(0xec)
JsonLdContext = "https://w3id.org/security/suites/x25519-2020/v1"
Type = "X25519KeyAgreementKey2020"
)
@@ -21,15 +21,11 @@ var _ did.VerificationMethodKeyAgreement = &KeyAgreementKey2020{}
type KeyAgreementKey2020 struct {
id string
pubkey PublicKey
pubkey *x25519.PublicKey
controller string
}
func NewKeyAgreementKey2020(id string, pubkey PublicKey, controller did.DID) (*KeyAgreementKey2020, error) {
if pubkey.Curve() != ecdh.X25519() {
return nil, errors.New("x25519 key curve must be X25519")
}
func NewKeyAgreementKey2020(id string, pubkey *x25519.PublicKey, controller did.DID) (*KeyAgreementKey2020, error) {
return &KeyAgreementKey2020{
id: id,
pubkey: pubkey,
@@ -47,7 +43,7 @@ func (k KeyAgreementKey2020) MarshalJSON() ([]byte, error) {
ID: k.ID(),
Type: k.Type(),
Controller: k.Controller(),
PublicKeyMultibase: PublicKeyToMultibase(k.pubkey),
PublicKeyMultibase: k.pubkey.ToPublicKeyMultibase(),
})
}
@@ -69,7 +65,7 @@ func (k *KeyAgreementKey2020) UnmarshalJSON(bytes []byte) error {
if len(k.id) == 0 {
return errors.New("invalid id")
}
k.pubkey, err = PublicKeyFromMultibase(aux.PublicKeyMultibase)
k.pubkey, err = x25519.PublicKeyFromPublicKeyMultibase(aux.PublicKeyMultibase)
if err != nil {
return fmt.Errorf("invalid publicKeyMultibase: %w", err)
}
@@ -96,21 +92,10 @@ func (k KeyAgreementKey2020) JsonLdContext() string {
return JsonLdContext
}
func (k KeyAgreementKey2020) PrivateKeyIsCompatible(local did.PrivateKey) bool {
_, ok := local.(PrivateKey)
return ok
func (k KeyAgreementKey2020) PrivateKeyIsCompatible(local crypto.KeyExchangePrivateKey) bool {
return local.PublicKeyIsCompatible(k.pubkey)
}
func (k KeyAgreementKey2020) ECDH(local did.PrivateKey) ([]byte, error) {
cast, ok := local.(PrivateKey)
if !ok {
return nil, errors.New("private key type doesn't match the public key type")
}
if cast == nil {
return nil, errors.New("invalid private key")
}
if k.pubkey.Curve() != cast.Curve() {
return nil, errors.New("key curves don't match")
}
return cast.ECDH(k.pubkey)
func (k KeyAgreementKey2020) KeyExchange(local crypto.KeyExchangePrivateKey) ([]byte, error) {
return local.KeyExchange(k.pubkey)
}

View File

@@ -1,4 +1,4 @@
package x25519_test
package x25519vm_test
import (
"encoding/json"
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/verifications/x25519"
x25519vm "github.com/INFURA/go-did/verifications/x25519"
)
func TestJsonRoundTrip(t *testing.T) {
@@ -17,7 +17,7 @@ func TestJsonRoundTrip(t *testing.T) {
"publicKeyMultibase": "z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW"
}`
var vm x25519.KeyAgreementKey2020
var vm x25519vm.KeyAgreementKey2020
err := json.Unmarshal([]byte(data), &vm)
require.NoError(t, err)

View File

@@ -1,151 +0,0 @@
package x25519
import (
"crypto/ecdh"
"crypto/rand"
"crypto/sha512"
"fmt"
"math/big"
"github.com/INFURA/go-did/verifications/ed25519"
helpers "github.com/INFURA/go-did/verifications/internal"
)
type PublicKey = *ecdh.PublicKey
type PrivateKey = *ecdh.PrivateKey
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
)
func GenerateKeyPair() (PublicKey, PrivateKey, error) {
priv, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
return priv.Public().(PublicKey), priv, nil
}
// PublicKeyFromBytes converts a serialized public key to a PublicKey.
// It errors if the slice is not the right size.
func PublicKeyFromBytes(b []byte) (PublicKey, error) {
return ecdh.X25519().NewPublicKey(b)
}
// 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
if len(pub) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid ed25519 public key size")
}
// Make a copy and 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.
pubCopy := make([]byte, ed25519.PublicKeySize)
copy(pubCopy, pub)
pubCopy[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(pubCopy))
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 ecdh.X25519().NewPublicKey(reverseBytes(res))
}
// PublicKeyFromMultibase decodes the public key from its Multibase form
func PublicKeyFromMultibase(multibase string) (PublicKey, error) {
code, bytes, err := helpers.MultibaseDecode(multibase)
if err != nil {
return nil, err
}
if code != MultibaseCode {
return nil, fmt.Errorf("invalid code")
}
return ecdh.X25519().NewPublicKey(bytes)
}
// PublicKeyToMultibase encodes the public key in a suitable way for publicKeyMultibase
func PublicKeyToMultibase(pub PublicKey) string {
return helpers.MultibaseEncode(MultibaseCode, pub.Bytes())
}
// PrivateKeyFromBytes converts a serialized public key to a PrivateKey.
// It errors if len(privateKey) is not [PrivateKeySize].
func PrivateKeyFromBytes(b []byte) (PrivateKey, error) {
return ecdh.X25519().NewPrivateKey(b)
}
// 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) {
if len(priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid ed25519 private key size")
}
// 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 ecdh.X25519().NewPrivateKey(h[:32])
}
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
}

View File

@@ -1,68 +0,0 @@
package x25519_test
import (
"crypto/ecdh"
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/verifications/ed25519"
"github.com/INFURA/go-did/verifications/x25519"
)
func TestGenerateKey(t *testing.T) {
pub, priv, err := x25519.GenerateKeyPair()
require.NoError(t, err)
require.NotNil(t, pub)
require.NotNil(t, priv)
require.Equal(t, ecdh.X25519(), pub.Curve())
require.Equal(t, ecdh.X25519(), priv.Curve())
require.True(t, pub.Equal(priv.Public()))
}
func TestMultibaseRoundTrip(t *testing.T) {
pub, _, err := x25519.GenerateKeyPair()
require.NoError(t, err)
mb := x25519.PublicKeyToMultibase(pub)
rt, err := x25519.PublicKeyFromMultibase(mb)
require.NoError(t, err)
require.Equal(t, pub, rt)
}
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.PublicKeyFromMultibase(tc.pubEdMultibase)
require.NoError(t, err)
pubX, err := x25519.PublicKeyFromEd25519(pubEd)
require.NoError(t, err)
require.Equal(t, tc.pubXMultibase, x25519.PublicKeyToMultibase(pubX))
})
}
// 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 := x25519.PublicKeyFromEd25519(pubEd)
require.NoError(t, err)
privX, err := x25519.PrivateKeyFromEd25519(privEd)
require.NoError(t, err)
require.True(t, pubX.Equal(privX.PublicKey()))
}
})
}