Merge pull request #6 from INFURA/usage

Add examples and refine usage (key agreement in particular)
This commit is contained in:
Michael Muré
2025-06-18 11:11:42 +02:00
committed by GitHub
12 changed files with 196 additions and 38 deletions

View File

@@ -1,11 +1,58 @@
package did
package did_test
import (
"encoding/base64"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did"
_ "github.com/INFURA/go-did/methods/did-key"
"github.com/INFURA/go-did/verifications/x25519"
)
func ExampleSignature() {
// errors need to be handled
// 1) Parse the DID string into a DID object
d, _ := did.Parse("did:key:z6MknwcywUtTy2ADJQ8FH1GcSySKPyKDmyzT4rPEE84XREse")
// 2) Resolve to the DID Document
doc, _ := d.Document()
// 3) Use the appropriate verification method (ex: verify a signature for authentication purpose)
sig, _ := base64.StdEncoding.DecodeString("nhpkr5a7juUM2eDpDRSJVdEE++0SYqaZXHtuvyafVFUx8zsOdDSrij+vHmd/ARwUOmi/ysmSD+b3K9WTBtmmBQ==")
if ok, method := did.TryAllVerify(doc.Authentication(), []byte("message"), sig); ok {
fmt.Println("Signature is valid, verified with method:", method.Type(), method.ID())
} else {
fmt.Println("Signature is invalid")
}
// Output: Signature is valid, verified with method: Ed25519VerificationKey2020 did:key:z6MknwcywUtTy2ADJQ8FH1GcSySKPyKDmyzT4rPEE84XREse#z6MknwcywUtTy2ADJQ8FH1GcSySKPyKDmyzT4rPEE84XREse
}
func ExampleKeyAgreement() {
// errors need to be handled
// 1) We have a private key for Alice
privAliceBytes, _ := base64.StdEncoding.DecodeString("fNOf3xWjFZYGYWixorM5+JR+u/2Udnc9Zw5+9rSvjqo=")
privAlice, _ := x25519.PrivateKeyFromBytes(privAliceBytes)
// 2) We resolve the DID Document for Bob
dBob, _ := did.Parse("did:key:z6MkgRNXpJRbEE6FoXhT8KWHwJo4KyzFo1FdSEFpRLh5vuXZ")
docBob, _ := dBob.Document()
// 3) We perform the key agreement
key, method, _ := did.FindMatchingKeyAgreement(docBob.KeyAgreement(), privAlice)
fmt.Println("Shared key:", base64.StdEncoding.EncodeToString(key))
fmt.Println("Verification method used:", method.Type(), method.ID())
// Output: Shared key: 7G1qwS/gn5W1hxBtObHc3F0jA7m2vuXkLJJ32yBuHVQ=
// Verification method used: X25519KeyAgreementKey2020 did:key:z6MkgRNXpJRbEE6FoXhT8KWHwJo4KyzFo1FdSEFpRLh5vuXZ#z6LSjeQx2VkXz8yirhrYJv8uicu9BBaeYU3Q1D9sFBovhmPF
}
func TestHasValidDIDSyntax(t *testing.T) {
tests := []struct {
name string
@@ -38,7 +85,7 @@ func TestHasValidDIDSyntax(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
require.Equal(t, tt.expected, HasValidDIDSyntax(tt.input))
require.Equal(t, tt.expected, did.HasValidDIDSyntax(tt.input))
})
}
}
@@ -46,7 +93,7 @@ func TestHasValidDIDSyntax(t *testing.T) {
func BenchmarkHasValidDIDSyntax(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
HasValidDIDSyntax("did:example:abc:def:ghi:jkl%20mno%3Apqr%3Astuv")
did.HasValidDIDSyntax("did:example:abc:def:ghi:jkl%20mno%3Apqr%3Astuv")
}
}
@@ -78,7 +125,7 @@ func TestHasValidDidUrlSyntax(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, HasValidDidUrlSyntax(tt.input))
require.Equal(t, tt.expected, did.HasValidDidUrlSyntax(tt.input))
})
}
}
@@ -86,6 +133,6 @@ func TestHasValidDidUrlSyntax(t *testing.T) {
func BenchmarkHasValidDidUrlSyntax(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
HasValidDidUrlSyntax("did:example:123456789abcdefghi/path/to/resource?key=value#section1")
did.HasValidDidUrlSyntax("did:example:123456789abcdefghi/path/to/resource?key=value#section1")
}
}

1
go.mod
View File

@@ -8,7 +8,6 @@ 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,8 +14,6 @@ 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

@@ -1,6 +1,7 @@
package did
import (
"crypto"
"encoding/json"
"net/url"
)
@@ -108,5 +109,21 @@ type VerificationMethodSignature interface {
type VerificationMethodKeyAgreement interface {
VerificationMethod
// TODO: function for key agreement
// PrivateKeyIsCompatible checks that the given PrivateKey is compatible with this method.
PrivateKeyIsCompatible(local PrivateKey) 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
}

View File

@@ -17,7 +17,7 @@ type document struct {
func (d document) MarshalJSON() ([]byte, error) {
// It's unclear where the KeyAgreement should be.
// Maybe it doesn't matter, but the spec contradict itself.
// Maybe it doesn't matter, but the spec contradicts itself.
// See https://github.com/w3c-ccg/did-key-spec/issues/71
return json.Marshal(struct {

View File

@@ -2,7 +2,6 @@ package didkey
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
@@ -20,8 +19,6 @@ func TestDocument(t *testing.T) {
bytes, err := json.MarshalIndent(doc, "", " ")
require.NoError(t, err)
fmt.Println(string(bytes))
// TODO: https://github.com/w3c-ccg/did-key-spec/issues/71
const expected = `{

View File

@@ -1,7 +1,6 @@
package didkey
import (
"crypto"
"fmt"
"strings"
@@ -65,7 +64,7 @@ func Decode(identifier string) (did.DID, error) {
return nil, fmt.Errorf("%w: unsupported did:key multicodec: 0x%x", did.ErrInvalidDid, code)
}
func FromPublicKey(pub PublicKey) (did.DID, error) {
func FromPublicKey(pub did.PublicKey) (did.DID, error) {
var err error
switch pub := pub.(type) {
case ed25519.PublicKey:
@@ -90,8 +89,8 @@ func FromPublicKey(pub PublicKey) (did.DID, error) {
}
}
func FromPrivateKey(priv PrivateKey) (did.DID, error) {
return FromPublicKey(priv.Public().(PublicKey))
func FromPrivateKey(priv did.PrivateKey) (did.DID, error) {
return FromPublicKey(priv.Public().(did.PublicKey))
}
func (d DidKey) Method() string {
@@ -120,18 +119,3 @@ func (d DidKey) Equal(d2 did.DID) bool {
}
return false
}
// ---------------
// Below are the interfaces for crypto.PublicKey and crypto.PrivateKey in the go standard library.
// They are not actually defined there for compatibility reasons.
// They are useful for did:key, it's unclear if it's useful elsewhere.
type PublicKey interface {
Equal(x crypto.PublicKey) bool
}
type PrivateKey interface {
Public() crypto.PublicKey
Equal(x crypto.PrivateKey) bool
}

View File

@@ -1,14 +1,47 @@
package didkey_test
import (
"encoding/base64"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did"
_ "github.com/INFURA/go-did/methods/did-key"
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))
// Make the associated did:key
dk, err := didkey.FromPrivateKey(priv)
if err != nil {
panic(err)
}
fmt.Println("Did:", dk.String())
// Produce a signature
msg := []byte("message")
sig := ed25519.Sign(priv, msg)
fmt.Println("Signature:", base64.StdEncoding.EncodeToString(sig))
// Resolve the DID and verify a signature
doc, err := dk.Document()
if err != nil {
panic(err)
}
ok, _ := did.TryAllVerify(doc.Authentication(), msg, sig)
fmt.Println("Signature verified:", ok)
}
func TestParseDIDKey(t *testing.T) {
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
d, err := did.Parse(str)

30
utilities.go Normal file
View File

@@ -0,0 +1,30 @@
package did
import (
"fmt"
)
// TryAllVerify tries to verify the signature with all the methods in the slice.
// It returns true if the signature is verified, and the method that verified it.
// If no method verifies the signature, it returns false and nil.
func TryAllVerify(methods []VerificationMethodSignature, data []byte, sig []byte) (bool, VerificationMethodSignature) {
for _, method := range methods {
if method.Verify(data, sig) {
return true, method
}
}
return false, nil
}
// 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) {
for _, method := range methods {
if method.PrivateKeyIsCompatible(priv) {
key, err := method.ECDH(priv)
return key, method, err
}
}
return nil, nil, fmt.Errorf("no matching key agreement found")
}

View File

@@ -12,12 +12,21 @@ import (
type PublicKey = ed25519.PublicKey
type PrivateKey = ed25519.PrivateKey
const PublicKeySize = ed25519.PublicKeySize
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 convert 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")
@@ -57,3 +66,18 @@ func PublicKeyToMultibase(pub PublicKey) string {
bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pub...))
return bytes
}
// PrivateKeyFromBytes convert a serialized public key to a PrivateKey.
// It errors if the slice is not the right size.
func PrivateKeyFromBytes(b []byte) (PrivateKey, error) {
if len(b) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid ed25519 private key size")
}
return b, nil
}
// Sign signs the message with privateKey and returns a signature.
// It will panic if len(privateKey) is not [PrivateKeySize].
func Sign(privateKey PrivateKey, message []byte) []byte {
return ed25519.Sign(privateKey, message)
}

View File

@@ -96,7 +96,21 @@ func (k KeyAgreementKey2020) JsonLdContext() string {
return JsonLdContext
}
// TODO: make it part of did.VerificationMethodKeyAgreement in some way
func (k KeyAgreementKey2020) KeyAgreement(priv PrivateKey) ([]byte, error) {
return priv.ECDH(k.pubkey)
func (k KeyAgreementKey2020) PrivateKeyIsCompatible(local did.PrivateKey) bool {
_, ok := local.(PrivateKey)
return ok
}
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)
}

View File

@@ -15,7 +15,14 @@ import (
type PublicKey = *ecdh.PublicKey
type PrivateKey = *ecdh.PrivateKey
const PublicKeySize = 32
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)
@@ -25,6 +32,8 @@ func GenerateKeyPair() (PublicKey, PrivateKey, error) {
return priv.Public().(PublicKey), priv, nil
}
// PublicKeyFromBytes convert 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)
}
@@ -115,6 +124,12 @@ func PublicKeyToMultibase(pub PublicKey) string {
return bytes
}
// PrivateKeyFromBytes convert 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)
}
func reverseBytes(b []byte) []byte {
r := make([]byte, len(b))
for i := 0; i < len(b); i++ {