good progress on did:key, x25519

This commit is contained in:
Michael Muré
2025-03-16 12:17:33 +01:00
parent ef067492f3
commit fe5b469bf9
14 changed files with 582 additions and 56 deletions

View File

@@ -22,7 +22,8 @@
This is an implementation of Decentralized Identifiers (DIDs) in go. It differs from the alternatives in the following ways:
- **simple**: made of shared reusable components and clear interfaces
- **fast**: while it supports DID Documents as JSON files, it's not unnecessary in the way (see below)
- **fast**: while it supports DID Documents as JSON files, it's not unnecessary in the way (see below)
- **battery included**: the corresponding cryptographic handling is implemented
- **support producing and using DIDs**: unlike some others, this all-in-one implementation is meant to create, manipulate and handle DIDs
- **extensible**: you can easily register your custom DID method

View File

@@ -1,4 +1,4 @@
package did_key
package didkey
import (
"encoding/json"
@@ -11,7 +11,8 @@ var _ did.Document = &document{}
type document struct {
id did.DID
verification did.VerificationMethod
signature did.VerificationMethodSignature
keyAgreement did.VerificationMethodKeyAgreement
}
func (d document) MarshalJSON() ([]byte, error) {
@@ -23,20 +24,24 @@ func (d document) MarshalJSON() ([]byte, error) {
VerificationMethod []did.VerificationMethod `json:"verificationMethod,omitempty"`
Authentication []string `json:"authentication,omitempty"`
AssertionMethod []string `json:"assertionMethod,omitempty"`
KeyAgreement []string `json:"keyAgreement,omitempty"`
KeyAgreement []did.VerificationMethod `json:"keyAgreement,omitempty"`
CapabilityInvocation []string `json:"capabilityInvocation,omitempty"`
CapabilityDelegation []string `json:"capabilityDelegation,omitempty"`
}{
Context: []string{did.JsonLdContext, d.verification.JsonLdContext()},
Context: stringSet(
did.JsonLdContext,
d.signature.JsonLdContext(),
d.keyAgreement.JsonLdContext(),
),
ID: d.id.String(),
AlsoKnownAs: nil,
Controller: d.id.String(),
VerificationMethod: []did.VerificationMethod{d.verification},
Authentication: []string{d.verification.ID()},
AssertionMethod: []string{d.verification.ID()},
KeyAgreement: []string{d.verification.ID()},
CapabilityInvocation: []string{d.verification.ID()},
CapabilityDelegation: []string{d.verification.ID()},
VerificationMethod: []did.VerificationMethod{d.signature, d.keyAgreement},
Authentication: []string{d.signature.ID()},
AssertionMethod: []string{d.signature.ID()},
KeyAgreement: []did.VerificationMethod{d.keyAgreement},
CapabilityInvocation: []string{d.signature.ID()},
CapabilityDelegation: []string{d.signature.ID()},
})
}
@@ -55,26 +60,41 @@ func (d document) AlsoKnownAs() []url.URL {
func (d document) VerificationMethods() map[string]did.VerificationMethod {
return map[string]did.VerificationMethod{
d.verification.ID(): d.verification,
d.signature.ID(): d.signature,
d.keyAgreement.ID(): d.keyAgreement,
}
}
func (d document) Authentication() []did.VerificationMethod {
return []did.VerificationMethod{d.verification}
func (d document) Authentication() []did.VerificationMethodSignature {
return []did.VerificationMethodSignature{d.signature}
}
func (d document) Assertion() []did.VerificationMethod {
return []did.VerificationMethod{d.verification}
func (d document) Assertion() []did.VerificationMethodSignature {
return []did.VerificationMethodSignature{d.signature}
}
func (d document) KeyAgreement() []did.VerificationMethod {
return []did.VerificationMethod{d.verification}
func (d document) KeyAgreement() []did.VerificationMethodKeyAgreement {
return []did.VerificationMethodKeyAgreement{d.keyAgreement}
}
func (d document) CapabilityInvocation() []did.VerificationMethod {
return []did.VerificationMethod{d.verification}
func (d document) CapabilityInvocation() []did.VerificationMethodSignature {
return []did.VerificationMethodSignature{d.signature}
}
func (d document) CapabilityDelegation() []did.VerificationMethod {
return []did.VerificationMethod{d.verification}
func (d document) CapabilityDelegation() []did.VerificationMethodSignature {
return []did.VerificationMethodSignature{d.signature}
}
func stringSet(values ...string) []string {
res := make([]string, 0, len(values))
loop:
for _, str := range values {
for _, item := range res {
if str == item {
continue loop
}
}
res = append(res, str)
}
return res
}

77
did-key/document_test.go Normal file
View File

@@ -0,0 +1,77 @@
package didkey
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/verifications/ed25519"
)
func TestDocument(t *testing.T) {
d, err := did.Parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
require.NoError(t, err)
doc, err := d.Document()
require.NoError(t, err)
bytes, err := json.MarshalIndent(doc, "", " ")
require.NoError(t, err)
fmt.Println(string(bytes))
const expected = `{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/ed25519-2020/v1",
"https://w3id.org/security/suites/x25519-2020/v1"
],
"id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"verificationMethod": [{
"id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"type": "Ed25519VerificationKey2020",
"controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}],
"authentication": [
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
],
"assertionMethod": [
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
],
"capabilityDelegation": [
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
],
"capabilityInvocation": [
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
],
"keyAgreement": [{
"id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p",
"type": "X25519KeyAgreementKey2020",
"controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"publicKeyMultibase": "z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p"
}]
}`
require.JSONEq(t, expected, string(bytes))
}
func TestJsonRoundTrip(t *testing.T) {
data := `{
"id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"type": "Ed25519VerificationKey2020",
"controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}`
var vm ed25519.VerificationKey2020
err := json.Unmarshal([]byte(data), &vm)
require.NoError(t, err)
bytes, err := json.Marshal(vm)
require.NoError(t, err)
require.JSONEq(t, data, string(bytes))
}

View File

@@ -1,6 +1,7 @@
package did_key
package didkey
import (
"crypto"
"fmt"
"net/url"
"strings"
@@ -10,10 +11,23 @@ import (
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/verifications/ed25519"
"github.com/INFURA/go-did/verifications/x25519"
)
// Specification: https://w3c-ccg.github.io/did-method-key/
func init() {
did.RegisterMethod("key", Decode)
}
var _ did.DID = &DidKey{}
type DidKey struct {
identifier string // cached value
signature did.VerificationMethodSignature
keyAgreement did.VerificationMethodKeyAgreement
}
func Decode(identifier string) (did.DID, error) {
const keyPrefix = "did:key:"
@@ -38,29 +52,49 @@ func Decode(identifier string) (did.DID, error) {
switch code {
case ed25519.MultibaseCode:
d.verification, err = ed25519.NewVerificationKey2020(identifier, bytes[read:], d)
d.signature, err = ed25519.NewVerificationKey2020(d.identifier, bytes[read:], d)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
xpub, err := x25519.PublicKeyFromEd25519(bytes[read:])
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
d.keyAgreement, err = x25519.NewKeyAgreementKey2020(d.identifier, xpub, d)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
// case P256: // TODO
// case Secp256k1: // TODO
// case RSA: // TODO
default:
return nil, fmt.Errorf("%w: unsupported did:key multicodec: 0x%x", did.ErrInvalidDid, code)
}
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
return d, nil
}
func init() {
did.RegisterMethod("key", Decode)
func FromPublicKey(pub PublicKey) (did.DID, error) {
var err error
switch pub := pub.(type) {
case ed25519.PublicKey:
d := DidKey{
identifier: ed25519.PublicKeyToMultibase(pub),
}
d.signature, err = ed25519.NewVerificationKey2020(d.identifier, pub, d)
if err != nil {
return nil, err
}
return d, nil
default:
return nil, fmt.Errorf("unsupported public key: %T", pub)
}
}
var _ did.DID = &DidKey{}
type DidKey struct {
identifier string // cached value
verification did.VerificationMethod
func FromPrivateKey(priv PrivateKey) (did.DID, error) {
return FromPublicKey(priv.Public().(PublicKey))
}
func (d DidKey) Method() string {
@@ -82,10 +116,29 @@ func (d DidKey) Fragment() string {
func (d DidKey) Document() (did.Document, error) {
return document{
id: d,
verification: d.verification,
signature: d.signature,
keyAgreement: d.keyAgreement,
}, nil
}
func (d DidKey) String() string {
return d.identifier
}
func (d DidKey) Equal(d2 did.DID) bool {
if d2, ok := d2.(DidKey); ok {
return d.identifier == d2.identifier
}
return false
}
// ---------------
type PublicKey interface {
Equal(x crypto.PublicKey) bool
}
type PrivateKey interface {
Public() crypto.PublicKey
Equal(x crypto.PrivateKey) bool
}

View File

@@ -1,4 +1,4 @@
package did_key_test
package didkey_test
import (
"testing"
@@ -36,6 +36,6 @@ func TestEquivalence(t *testing.T) {
did1, err := did.Parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
require.NoError(t, err)
require.True(t, did0A == did0B)
require.False(t, did0A == did1)
require.True(t, did0A.Equal(did0B))
require.False(t, did0A.Equal(did1))
}

5
go.mod
View File

@@ -1,11 +1,14 @@
module github.com/INFURA/go-did
go 1.23
go 1.23.0
toolchain go1.23.1
require (
github.com/multiformats/go-multibase v0.2.0
github.com/multiformats/go-varint v0.0.7
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.36.0
)
require (

2
go.sum
View File

@@ -14,6 +14,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -14,6 +14,8 @@ type DID interface {
Document() (Document, error)
String() string // return the full DID URL, with path, query, fragment
Equal(DID) bool
}
// Document is the interface for a DID document. It represents the "resolved" state of a DID.
@@ -34,25 +36,25 @@ type Document interface {
// Authentication defines how the DID is able to authenticate, for purposes such as logging into a website
// or engaging in any sort of challenge-response protocol.
Authentication() []VerificationMethod
Authentication() []VerificationMethodSignature
// Assertion specifies how the DID subject is expected to express claims, such as for the purposes of issuing
// a Verifiable Credential.
// See https://www.w3.org/TR/vc-data-model/
Assertion() []VerificationMethod
Assertion() []VerificationMethodSignature
// KeyAgreement specifies how an entity can generate encryption material in order to transmit confidential
// information intended for the DID subject, such as for the purposes of establishing a secure communication channel
// with the recipient.
KeyAgreement() []VerificationMethod
KeyAgreement() []VerificationMethodKeyAgreement
// CapabilityInvocation specifies a verification method that might be used by the DID subject to invoke a
// cryptographic capability, such as the authorization to update the DID Document.
CapabilityInvocation() []VerificationMethod
CapabilityInvocation() []VerificationMethodSignature
// CapabilityDelegation specifies a mechanism that might be used by the DID subject to delegate a cryptographic
// capability to another party, such as delegating the authority to access a specific HTTP API to a subordinate.
CapabilityDelegation() []VerificationMethod
CapabilityDelegation() []VerificationMethodSignature
// TODO: Service
// https://www.w3.org/TR/did-extensions-properties/#service-types
@@ -77,7 +79,22 @@ type VerificationMethod interface {
// JsonLdContext reports the JSON-LD context definition required for this verification method.
JsonLdContext() string
}
// VerificationMethodSignature is a VerificationMethod implementing signature verification.
// It can be used for Authentication, Assertion, CapabilityInvocation, CapabilityDelegation
// in a Document.
type VerificationMethodSignature interface {
VerificationMethod
// Verify checks that 'sig' is a valid signature of 'data'.
Verify(data []byte, sig []byte) bool
}
// VerificationMethodKeyAgreement is a VerificationMethod implementing a shared key agreement.
// It can be used for KeyAgreement in a Document.
type VerificationMethodKeyAgreement interface {
VerificationMethod
// TODO: function for key agreement
}

View File

@@ -2,6 +2,7 @@ package ed25519
import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
@@ -12,20 +13,22 @@ import (
"github.com/INFURA/go-did"
)
// 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"
)
var _ did.VerificationMethod = &VerificationKey2020{}
var _ did.VerificationMethodSignature = &VerificationKey2020{}
type VerificationKey2020 struct {
id string
pubkey ed25519.PublicKey
pubkey PublicKey
controller string
}
func NewVerificationKey2020(id string, pubkey []byte, controller did.DID) (*VerificationKey2020, error) {
func NewVerificationKey2020(id string, pubkey PublicKey, controller did.DID) (*VerificationKey2020, error) {
if len(pubkey) != ed25519.PublicKeySize {
return nil, errors.New("invalid ed25519 public key size")
}
@@ -47,7 +50,7 @@ func (v VerificationKey2020) MarshalJSON() ([]byte, error) {
ID: v.ID(),
Type: v.Type(),
Controller: v.Controller(),
PublicKeyMultibase: encodePubkey(v.pubkey),
PublicKeyMultibase: PublicKeyToMultibase(v.pubkey),
})
}
@@ -69,7 +72,7 @@ func (v *VerificationKey2020) UnmarshalJSON(bytes []byte) error {
if len(v.id) == 0 {
return errors.New("invalid id")
}
v.pubkey, err = decodePubkey(aux.PublicKeyMultibase)
v.pubkey, err = MultibaseToPublicKey(aux.PublicKeyMultibase)
if err != nil {
return fmt.Errorf("invalid publicKeyMultibase: %w", err)
}
@@ -100,16 +103,16 @@ func (v VerificationKey2020) Verify(data []byte, sig []byte) bool {
return ed25519.Verify(v.pubkey, data, sig)
}
// encodePubkey encodes the public key in a suitable way for publicKeyMultibase
func encodePubkey(pubkey ed25519.PublicKey) string {
// PublicKeyToMultibase encodes the public key in a suitable way for publicKeyMultibase
func PublicKeyToMultibase(pub PublicKey) string {
// can only fail with an invalid encoding, but it's hardcoded
bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pubkey...))
bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pub...))
return bytes
}
// decodePubkey decodes the public key from its publicKeyMultibase form
func decodePubkey(encoded string) (ed25519.PublicKey, error) {
baseCodec, bytes, err := mbase.Decode(encoded)
// MultibaseToPublicKey decodes the public key from its publicKeyMultibase form
func MultibaseToPublicKey(multibase string) (PublicKey, error) {
baseCodec, bytes, err := mbase.Decode(multibase)
if err != nil {
return nil, err
}
@@ -132,3 +135,12 @@ func decodePubkey(encoded string) (ed25519.PublicKey, error) {
}
return bytes[read:], nil
}
// ------------
type PublicKey = ed25519.PublicKey
type PrivateKey = ed25519.PrivateKey
func GenerateKeyPair() (PublicKey, PrivateKey, error) {
return ed25519.GenerateKey(rand.Reader)
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/INFURA/go-did/verifications/ed25519"
)
func TestJson(t *testing.T) {
func TestJsonRoundTrip(t *testing.T) {
data := `{
"id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"type": "Ed25519VerificationKey2020",
@@ -26,3 +26,19 @@ func TestJson(t *testing.T) {
require.NoError(t, err)
require.JSONEq(t, data, string(bytes))
}
// func TestSignature(t *testing.T) {
// d, err := didkey.Decode("did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2")
// require.NoError(t, err)
// doc, err := d.Document()
// require.NoError(t, err)
// method := doc.Authentication()[0]
// require.IsType(t, &ed25519.VerificationKey2020{}, method)
//
// require.True(t, method.Verify(
// []byte("node key test"),
// []byte("Tuhz8eG2jqYG4jUbxt14iMd3r2v2eNLftPTfrZfaaFYn5ta7wP3oYfC1rnDVJsLvHAK7j5CmVoXtGoYGL7Lnb5e"),
// ))
//
// // ed25519.NewVerificationKey2020(did, )
// }

View File

@@ -0,0 +1,131 @@
package x25519
import (
"encoding/json"
"errors"
"fmt"
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
"github.com/INFURA/go-did"
)
// Specification: https://w3c-ccg.github.io/did-method-key/#ed25519-x25519
const (
MultibaseCode = uint64(0xec)
JsonLdContext = "https://w3id.org/security/suites/x25519-2020/v1"
)
var _ did.VerificationMethodKeyAgreement = &KeyAgreementKey2020{}
type KeyAgreementKey2020 struct {
id string
pubkey PublicKey
controller string
}
func NewKeyAgreementKey2020(id string, pubkey PublicKey, controller did.DID) (*KeyAgreementKey2020, error) {
if len(pubkey) != PublicKeySize {
return nil, errors.New("invalid x25519 public key size")
}
return &KeyAgreementKey2020{
id: id,
pubkey: pubkey,
controller: controller.String(),
}, nil
}
func (k KeyAgreementKey2020) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
}{
ID: k.ID(),
Type: k.Type(),
Controller: k.Controller(),
PublicKeyMultibase: PublicKeyToMultibase(k.pubkey),
})
}
func (k *KeyAgreementKey2020) UnmarshalJSON(bytes []byte) error {
aux := struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
}{}
err := json.Unmarshal(bytes, &aux)
if err != nil {
return err
}
if aux.Type != k.Type() {
return errors.New("invalid type")
}
k.id = aux.ID
if len(k.id) == 0 {
return errors.New("invalid id")
}
k.pubkey, err = MultibaseToPublicKey(aux.PublicKeyMultibase)
if err != nil {
return fmt.Errorf("invalid publicKeyMultibase: %w", err)
}
k.controller = aux.Controller
if !did.HasValidSyntax(k.controller) {
return errors.New("invalid controller")
}
return nil
}
func (k KeyAgreementKey2020) ID() string {
return k.id
}
func (k KeyAgreementKey2020) Type() string {
return "X25519KeyAgreementKey2020"
}
func (k KeyAgreementKey2020) Controller() string {
return k.controller
}
func (k KeyAgreementKey2020) JsonLdContext() string {
return JsonLdContext
}
// PublicKeyToMultibase encodes the public key in a suitable way for publicKeyMultibase
func PublicKeyToMultibase(pub PublicKey) string {
// can only fail with an invalid encoding, but it's hardcoded
bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pub...))
return bytes
}
// MultibaseToPublicKey decodes the public key from its publicKeyMultibase form
func MultibaseToPublicKey(multibase string) (PublicKey, error) {
baseCodec, bytes, err := mbase.Decode(multibase)
if err != nil {
return nil, err
}
// the specification enforces that encoding
if baseCodec != mbase.Base58BTC {
return nil, fmt.Errorf("not Base58BTC encoded")
}
code, read, err := varint.FromUvarint(bytes)
if err != nil {
return nil, err
}
if code != MultibaseCode {
return nil, fmt.Errorf("invalid code")
}
if read != 2 {
return nil, fmt.Errorf("unexpected multibase")
}
if len(bytes)-read != PublicKeySize {
return nil, fmt.Errorf("invalid ed25519 public key size")
}
return bytes[read:], nil
}

View File

@@ -0,0 +1,27 @@
package x25519_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/verifications/x25519"
)
func TestJsonRoundTrip(t *testing.T) {
data := `{
"id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW",
"type": "X25519KeyAgreementKey2020",
"controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
"publicKeyMultibase": "z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW"
}`
var vm x25519.KeyAgreementKey2020
err := json.Unmarshal([]byte(data), &vm)
require.NoError(t, err)
bytes, err := json.Marshal(vm)
require.NoError(t, err)
require.JSONEq(t, data, string(bytes))
}

116
verifications/x25519/key.go Normal file
View File

@@ -0,0 +1,116 @@
package x25519
import (
"bytes"
"crypto"
"crypto/rand"
"fmt"
"io"
"golang.org/x/crypto/curve25519"
"github.com/INFURA/go-did/verifications/ed25519"
)
// This mirrors ed25519's structure for private/public "keys". jwx
// requires dedicated types for these as they drive
// serialization/deserialization logic, as well as encryption types.
//
// Note that with the x25519 scheme, the private key is a sequence of
// 32 bytes, while the public key is the result of X25519(private,
// basepoint).
//
// Portions of this file are from Go's ed25519.go, which is
// Copyright 2016 The Go Authors. All rights reserved.
// Originally taken from github.com/lestrrat-go/jwx/v2/x25519.
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 = 64
// SeedSize is the size, in bytes, of private key seeds. These are the private key representations used by RFC 8032.
SeedSize = 32
)
// PublicKey is the type of X25519 public keys
type PublicKey []byte
// Any methods implemented on PublicKey might need to also be implemented on
// PrivateKey, as the latter embeds the former and will expose its methods.
// Equal reports whether pub and x have the same value.
func (pub PublicKey) Equal(x crypto.PublicKey) bool {
xx, ok := x.(PublicKey)
if !ok {
return false
}
return bytes.Equal(pub, xx)
}
// PrivateKey is the type of X25519 private key
type PrivateKey []byte
// Public returns the PublicKey corresponding to priv.
func (priv PrivateKey) Public() crypto.PublicKey {
publicKey := make([]byte, PublicKeySize)
copy(publicKey, priv[SeedSize:])
return PublicKey(publicKey)
}
// Equal reports whether priv and x have the same value.
func (priv PrivateKey) Equal(x crypto.PrivateKey) bool {
xx, ok := x.(PrivateKey)
if !ok {
return false
}
return bytes.Equal(priv, xx)
}
// NewKeyFromSeed calculates a private key from a seed. It will return
// an error if len(seed) is not SeedSize. This function is provided
// for interoperability with RFC 7748. RFC 7748's private keys
// correspond to seeds in this package.
func NewKeyFromSeed(seed []byte) (PrivateKey, error) {
privateKey := make([]byte, PrivateKeySize)
if len(seed) != SeedSize {
return nil, fmt.Errorf("unexpected seed size: %d", len(seed))
}
copy(privateKey, seed)
public, err := curve25519.X25519(seed, curve25519.Basepoint)
if err != nil {
return nil, fmt.Errorf(`failed to compute public key: %w`, err)
}
copy(privateKey[SeedSize:], public)
return privateKey, nil
}
// GenerateKey generates a public/private key pair using entropy from rand.
// If rand is nil, crypto/rand.Reader will be used.
func GenerateKey() (PublicKey, PrivateKey, error) {
seed := make([]byte, SeedSize)
if _, err := io.ReadFull(rand.Reader, seed); err != nil {
return nil, nil, err
}
privateKey, err := NewKeyFromSeed(seed)
if err != nil {
return nil, nil, err
}
publicKey := make([]byte, PublicKeySize)
copy(publicKey, privateKey[SeedSize:])
return publicKey, privateKey, nil
}
func PublicKeyFromEd25519(pub ed25519.PublicKey) (PublicKey, error) {
// x, _ := curve25519.X25519(pub, curve25519.Basepoint)
publicKey := make([]byte, PublicKeySize)
copy(publicKey, pub)
publicKey[31] &= 0x7F
publicKey[31] |= 0x40
publicKey[0] &= 0xF8
return publicKey, nil
}

View File

@@ -0,0 +1,51 @@
package x25519_test
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did/verifications/x25519"
)
func TestGenerateKey(t *testing.T) {
t.Run("x25519.GenerateKey()", func(t *testing.T) {
_, _, err := x25519.GenerateKey()
require.NoError(t, err, `x25519.GenerateKey should work`)
})
t.Run("x25519.NewKeyFromSeed(wrongSeedLength)", func(t *testing.T) {
dummy := make([]byte, x25519.SeedSize-1)
_, err := x25519.NewKeyFromSeed(dummy)
require.Error(t, err, `wrong seed size should result in error`)
})
}
func TestNewKeyFromSeed(t *testing.T) {
// These test vectors are from RFC7748 Section 6.1
const alicePrivHex = `77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a`
const alicePubHex = `8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a`
const bobPrivHex = `5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb`
const bobPubHex = `de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f`
alicePrivSeed, err := hex.DecodeString(alicePrivHex)
require.NoError(t, err, `alice seed decoded`)
alicePriv, err := x25519.NewKeyFromSeed(alicePrivSeed)
require.NoError(t, err, `alice private key`)
alicePub := alicePriv.Public().(x25519.PublicKey)
require.Equal(t, hex.EncodeToString(alicePub), alicePubHex, `alice public key`)
bobPrivSeed, err := hex.DecodeString(bobPrivHex)
require.NoError(t, err, `bob seed decoded`)
bobPriv, err := x25519.NewKeyFromSeed(bobPrivSeed)
require.NoError(t, err, `bob private key`)
bobPub := bobPriv.Public().(x25519.PublicKey)
require.Equal(t, hex.EncodeToString(bobPub), bobPubHex, `bob public key`)
require.True(t, bobPriv.Equal(bobPriv), `bobPriv should equal bobPriv`)
require.True(t, bobPub.Equal(bobPub), `bobPub should equal bobPub`)
require.False(t, bobPriv.Equal(bobPub), `bobPriv should NOT equal bobPub`)
require.False(t, bobPub.Equal(bobPriv), `bobPub should NOT equal bobPriv`)
}