diff --git a/crypto/interface.go b/crypto/interface.go index fef0e05..58f19c5 100644 --- a/crypto/interface.go +++ b/crypto/interface.go @@ -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) } diff --git a/crypto/internal/testSuite.go b/crypto/internal/testsuite.go similarity index 90% rename from crypto/internal/testSuite.go rename to crypto/internal/testsuite.go index c69a9e2..7d24a4c 100644 --- a/crypto/internal/testSuite.go +++ b/crypto/internal/testsuite.go @@ -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) + } + }) + }) } diff --git a/crypto/p256/private.go b/crypto/p256/private.go index f996458..7e0fa9d 100644 --- a/crypto/p256/private.go +++ b/crypto/p256/private.go @@ -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") } diff --git a/crypto/p256/public.go b/crypto/p256/public.go index 4ebbbd3..0ee4bd2 100644 --- a/crypto/p256/public.go +++ b/crypto/p256/public.go @@ -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) } diff --git a/crypto/x25519/private.go b/crypto/x25519/private.go index 0bd81f6..5aaf1cd 100644 --- a/crypto/x25519/private.go +++ b/crypto/x25519/private.go @@ -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") +} diff --git a/crypto/x25519/public.go b/crypto/x25519/public.go index 5df2f5f..8946786 100644 --- a/crypto/x25519/public.go +++ b/crypto/x25519/public.go @@ -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++ { diff --git a/did_test.go b/did_test.go index 605bc33..48187c0 100644 --- a/did_test.go +++ b/did_test.go @@ -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() { diff --git a/document/document_test.go b/document/document_test.go index cffecfb..4f9dd68 100644 --- a/document/document_test.go +++ b/document/document_test.go @@ -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) diff --git a/interfaces.go b/interfaces.go index 5a6e796..c3241b0 100644 --- a/interfaces.go +++ b/interfaces.go @@ -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) } diff --git a/methods/did-key/key.go b/methods/did-key/key.go index 57128b6..c99a2d0 100644 --- a/methods/did-key/key.go +++ b/methods/did-key/key.go @@ -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 { diff --git a/methods/did-key/key_test.go b/methods/did-key/key_test.go index 391b810..0cd2f65 100644 --- a/methods/did-key/key_test.go +++ b/methods/did-key/key_test.go @@ -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) + } +} diff --git a/utilities.go b/utilities.go index 404474c..1591c35 100644 --- a/utilities.go +++ b/utilities.go @@ -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 } } diff --git a/verifications/ed25519/VerificationKey2020.go b/verifications/ed25519/VerificationKey2020.go index f9bce75..0a1732e 100644 --- a/verifications/ed25519/VerificationKey2020.go +++ b/verifications/ed25519/VerificationKey2020.go @@ -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) } diff --git a/verifications/ed25519/VerificationKey2020_test.go b/verifications/ed25519/VerificationKey2020_test.go index 05b3108..2325852 100644 --- a/verifications/ed25519/VerificationKey2020_test.go +++ b/verifications/ed25519/VerificationKey2020_test.go @@ -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 { diff --git a/verifications/ed25519/key.go b/verifications/ed25519/key.go deleted file mode 100644 index c5502ac..0000000 --- a/verifications/ed25519/key.go +++ /dev/null @@ -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) -} diff --git a/verifications/ed25519/key_test.go b/verifications/ed25519/key_test.go deleted file mode 100644 index 6068694..0000000 --- a/verifications/ed25519/key_test.go +++ /dev/null @@ -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) -} diff --git a/verifications/internal/multibase.go b/verifications/internal/multibase.go deleted file mode 100644 index a2443de..0000000 --- a/verifications/internal/multibase.go +++ /dev/null @@ -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 -} diff --git a/verifications/json.go b/verifications/json.go index 6b17637..ec40b8b 100644 --- a/verifications/json.go +++ b/verifications/json.go @@ -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) } diff --git a/verifications/x25519/KeyAgreementKey2020.go b/verifications/x25519/KeyAgreementKey2020.go index ad7d594..efab589 100644 --- a/verifications/x25519/KeyAgreementKey2020.go +++ b/verifications/x25519/KeyAgreementKey2020.go @@ -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) } diff --git a/verifications/x25519/KeyAgreementKey2020_test.go b/verifications/x25519/KeyAgreementKey2020_test.go index 3e4cef4..a48f41c 100644 --- a/verifications/x25519/KeyAgreementKey2020_test.go +++ b/verifications/x25519/KeyAgreementKey2020_test.go @@ -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) diff --git a/verifications/x25519/key.go b/verifications/x25519/key.go deleted file mode 100644 index 83047ad..0000000 --- a/verifications/x25519/key.go +++ /dev/null @@ -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 -} diff --git a/verifications/x25519/key_test.go b/verifications/x25519/key_test.go deleted file mode 100644 index a213856..0000000 --- a/verifications/x25519/key_test.go +++ /dev/null @@ -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())) - } - }) -}