good progress on did:key, x25519
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
77
did-key/document_test.go
Normal 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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
5
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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, )
|
||||
// }
|
||||
|
||||
131
verifications/x25519/KeyAgreementKey2020.go
Normal file
131
verifications/x25519/KeyAgreementKey2020.go
Normal 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
|
||||
}
|
||||
27
verifications/x25519/KeyAgreementKey2020_test.go
Normal file
27
verifications/x25519/KeyAgreementKey2020_test.go
Normal 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
116
verifications/x25519/key.go
Normal 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
|
||||
}
|
||||
51
verifications/x25519/key_test.go
Normal file
51
verifications/x25519/key_test.go
Normal 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`)
|
||||
}
|
||||
Reference in New Issue
Block a user