did🔑 green test with the test vectors

This commit is contained in:
Michael Muré
2025-07-08 12:59:23 +02:00
parent ee695fc86d
commit 2284bd6487
6 changed files with 278 additions and 66 deletions

View File

@@ -9,6 +9,8 @@ import (
"github.com/INFURA/go-did/crypto/p256"
"github.com/INFURA/go-did/crypto/p384"
"github.com/INFURA/go-did/crypto/p521"
"github.com/INFURA/go-did/crypto/rsa"
"github.com/INFURA/go-did/crypto/secp256k1"
"github.com/INFURA/go-did/crypto/x25519"
)
@@ -17,6 +19,8 @@ var decoders = map[uint64]func(b []byte) (crypto.PublicKey, error){
p256.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return p256.PublicKeyFromBytes(b) },
p384.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return p384.PublicKeyFromBytes(b) },
p521.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return p521.PublicKeyFromBytes(b) },
rsa.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return rsa.PublicKeyFromPKCS1DER(b) },
secp256k1.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return secp256k1.PublicKeyFromBytes(b) },
x25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return x25519.PublicKeyFromBytes(b) },
}

View File

@@ -29,6 +29,9 @@ type TestHarness[PubT crypto.PublicKey, PrivT crypto.PrivateKey] struct {
MultibaseCode uint64
DefaultHash crypto.Hash
OtherHashes []crypto.Hash
PublicKeyBytesSize int
PrivateKeyBytesSize int
SignatureBytesSize int
@@ -198,10 +201,12 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
for _, tc := range []struct {
name string
signer func(msg []byte) ([]byte, error)
verifier func(msg []byte, sig []byte) bool
signer func(msg []byte, opts ...crypto.SigningOption) ([]byte, error)
verifier func(msg []byte, sig []byte, opts ...crypto.SigningOption) bool
expectedSize int
stats *int
defaultHash crypto.Hash
otherHashes []crypto.Hash
}{
{
name: "Bytes signature",
@@ -209,26 +214,52 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
verifier: spub.VerifyBytes,
expectedSize: harness.SignatureBytesSize,
stats: &stats.sigRawSize,
defaultHash: harness.DefaultHash,
otherHashes: harness.OtherHashes,
},
{
name: "ASN.1 signature",
signer: spriv.SignToASN1,
verifier: spub.VerifyASN1,
stats: &stats.sigAsn1Size,
defaultHash: harness.DefaultHash,
otherHashes: harness.OtherHashes,
},
} {
t.Run(tc.name, func(t *testing.T) {
msg := []byte("message")
sigNoParams, err := tc.signer(msg)
require.NoError(t, err)
require.NotEmpty(t, sigNoParams)
sigDefault, err := tc.signer(msg, crypto.WithSigningHash(tc.defaultHash))
require.NoError(t, err)
if tc.expectedSize > 0 {
require.Equal(t, tc.expectedSize, len(sigNoParams))
}
*tc.stats = len(sigNoParams)
// signatures might be different (i.e. non-deterministic), but they should verify the same way
valid := tc.verifier(msg, sigNoParams)
require.True(t, valid)
valid = tc.verifier(msg, sigDefault)
require.True(t, valid)
valid = tc.verifier([]byte("wrong message"), sigNoParams)
require.False(t, valid)
valid = tc.verifier([]byte("wrong message"), sigDefault)
require.False(t, valid)
})
for _, hash := range tc.otherHashes {
t.Run(fmt.Sprintf("%s-%s", tc.name, hash.String()), func(t *testing.T) {
msg := []byte("message")
sig, err := tc.signer(msg)
require.NoError(t, err)
require.NotEmpty(t, sig)
if tc.expectedSize > 0 {
require.Equal(t, tc.expectedSize, len(sig))
}
*tc.stats = len(sig)
valid := tc.verifier(msg, sig)
require.True(t, valid)
@@ -236,6 +267,7 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
require.False(t, valid)
})
}
}
})
t.Run("KeyExchange", func(t *testing.T) {

View File

@@ -20,6 +20,11 @@ func (d document) MarshalJSON() ([]byte, error) {
// Maybe it doesn't matter, but the spec contradicts itself.
// See https://github.com/w3c-ccg/did-key-spec/issues/71
vms := []did.VerificationMethod{d.signature}
if d.signature != did.VerificationMethod(d.keyAgreement) {
vms = append(vms, d.keyAgreement)
}
return json.Marshal(struct {
Context []string `json:"@context"`
ID string `json:"id"`
@@ -28,17 +33,17 @@ func (d document) MarshalJSON() ([]byte, error) {
VerificationMethod []did.VerificationMethod `json:"verificationMethod,omitempty"`
Authentication []string `json:"authentication,omitempty"`
AssertionMethod []string `json:"assertionMethod,omitempty"`
KeyAgreement []did.VerificationMethod `json:"keyAgreement,omitempty"`
KeyAgreement []string `json:"keyAgreement,omitempty"`
CapabilityInvocation []string `json:"capabilityInvocation,omitempty"`
CapabilityDelegation []string `json:"capabilityDelegation,omitempty"`
}{
Context: d.Context(),
ID: d.id.String(),
AlsoKnownAs: nil,
VerificationMethod: []did.VerificationMethod{d.signature},
VerificationMethod: vms,
Authentication: []string{d.signature.ID()},
AssertionMethod: []string{d.signature.ID()},
KeyAgreement: []did.VerificationMethod{d.keyAgreement},
KeyAgreement: []string{d.keyAgreement.ID()},
CapabilityInvocation: []string{d.signature.ID()},
CapabilityDelegation: []string{d.signature.ID()},
})
@@ -66,6 +71,11 @@ func (d document) AlsoKnownAs() []*url.URL {
}
func (d document) VerificationMethods() map[string]did.VerificationMethod {
if d.signature == did.VerificationMethod(d.keyAgreement) {
return map[string]did.VerificationMethod{
d.signature.ID(): d.signature,
}
}
return map[string]did.VerificationMethod{
d.signature.ID(): d.signature,
d.keyAgreement.ID(): d.keyAgreement,

View File

@@ -7,20 +7,19 @@ import (
"github.com/stretchr/testify/require"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/methods/did-key/testvectors"
)
func TestDocument(t *testing.T) {
d, err := did.Parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
require.NoError(t, err)
doc, err := d.Document()
doc, err := d.Document(did.WithResolutionHintVerificationMethod("Ed25519VerificationKey2020"))
require.NoError(t, err)
bytes, err := json.MarshalIndent(doc, "", " ")
require.NoError(t, err)
// TODO: https://github.com/w3c-ccg/did-key-spec/issues/71
const expected = `{
"@context": [
"https://www.w3.org/ns/did/v1",
@@ -54,8 +53,113 @@ func TestDocument(t *testing.T) {
}]
}`
require.JSONEq(t, expected, string(bytes))
requireDocEqual(t, expected, string(bytes))
}
// TODO: test vectors:
// https://github.com/w3c-ccg/did-key-spec/tree/main/test-vectors
func TestVectors(t *testing.T) {
for _, filename := range testvectors.AllFiles() {
t.Run(filename, func(t *testing.T) {
vectors, err := testvectors.LoadTestVectors(filename)
require.NoError(t, err)
for _, vector := range vectors {
t.Run(vector.DID, func(t *testing.T) {
t.Log("hint is", vector.ResolutionHint)
require.NotZero(t, vector.Document)
require.NotZero(t, vector.Pub)
require.NotZero(t, vector.Priv)
d, err := did.Parse(vector.DID)
require.NoError(t, err)
var opts []did.ResolutionOption
for _, hint := range vector.ResolutionHint {
opts = append(opts, did.WithResolutionHintVerificationMethod(hint))
}
doc, err := d.Document(opts...)
require.NoError(t, err)
bytes, err := json.MarshalIndent(doc, "", " ")
require.NoError(t, err)
requireDocEqual(t, vector.Document, string(bytes))
})
}
})
}
}
// Some variations in the DID document are legal, so we can't just require.JSONEq() to compare two of them.
// This function does its best to compare two documents, regardless of those variations.
func requireDocEqual(t *testing.T, expected, actual string) {
propsExpected := map[string]json.RawMessage{}
err := json.Unmarshal([]byte(expected), &propsExpected)
require.NoError(t, err)
propsActual := map[string]json.RawMessage{}
err = json.Unmarshal([]byte(actual), &propsActual)
require.NoError(t, err)
require.Equal(t, len(propsExpected), len(propsActual))
// if a VerificationMethod is defined inline in the properties below, we move it to vmExpected and replace it with the VM ID
var vmExpected []json.RawMessage
err = json.Unmarshal(propsExpected["verificationMethod"], &vmExpected)
require.NoError(t, err)
for _, s := range []string{"authentication", "assertionMethod", "keyAgreement", "capabilityInvocation", "capabilityDelegation"} {
var vms []json.RawMessage
err = json.Unmarshal(propsExpected[s], &vms)
require.NoError(t, err)
for _, vmBytes := range vms {
vm := map[string]json.RawMessage{}
if err := json.Unmarshal(vmBytes, &vm); err == nil {
vmExpected = append(vmExpected, vmBytes)
propsExpected[s] = append([]byte("[ "), append(vm["id"], []byte(" ]")...)...)
}
}
}
// Same for actual
var vmActual []json.RawMessage
err = json.Unmarshal(propsActual["verificationMethod"], &vmActual)
require.NoError(t, err)
for _, s := range []string{"authentication", "assertionMethod", "keyAgreement", "capabilityInvocation", "capabilityDelegation"} {
var vms []json.RawMessage
err = json.Unmarshal(propsActual[s], &vms)
require.NoError(t, err)
for _, vmBytes := range vms {
vm := map[string]json.RawMessage{}
if err := json.Unmarshal(vmBytes, &vm); err == nil {
vmActual = append(vmActual, vmBytes)
propsActual[s] = append([]byte("[ "), append(vm["id"], []byte(" ]")...)...)
}
}
}
for k, v := range propsExpected {
switch k {
case "verificationMethod":
// Convert to interface{} slices to normalize JSON formatting
expectedVMs := make([]interface{}, len(vmExpected))
for i, vm := range vmExpected {
var normalized interface{}
err := json.Unmarshal(vm, &normalized)
require.NoError(t, err)
expectedVMs[i] = normalized
}
actualVMs := make([]interface{}, len(vmActual))
for i, vm := range vmActual {
var normalized interface{}
err := json.Unmarshal(vm, &normalized)
require.NoError(t, err)
actualVMs[i] = normalized
}
require.ElementsMatch(t, expectedVMs, actualVMs, "--> on property \"%s\"", k)
default:
require.JSONEq(t, string(v), string(propsActual[k]), "--> on property \"%s\"", k)
}
}
}

View File

@@ -11,9 +11,14 @@ import (
"github.com/INFURA/go-did/crypto/p256"
"github.com/INFURA/go-did/crypto/p384"
"github.com/INFURA/go-did/crypto/p521"
"github.com/INFURA/go-did/crypto/rsa"
"github.com/INFURA/go-did/crypto/secp256k1"
"github.com/INFURA/go-did/crypto/x25519"
"github.com/INFURA/go-did/verifications/ed25519"
"github.com/INFURA/go-did/verifications/jsonwebkey"
"github.com/INFURA/go-did/verifications/multikey"
p256vm "github.com/INFURA/go-did/verifications/p256"
secp256k1vm "github.com/INFURA/go-did/verifications/secp256k1"
"github.com/INFURA/go-did/verifications/x25519"
)
@@ -23,12 +28,11 @@ func init() {
did.RegisterMethod("key", Decode)
}
var _ did.DID = &DidKey{}
var _ did.DID = DidKey{}
type DidKey struct {
msi string // method-specific identifier, i.e. "12345" in "did:key:12345"
signature did.VerificationMethodSignature
keyAgreement did.VerificationMethodKeyAgreement
pubkey crypto.PublicKey
}
func Decode(identifier string) (did.DID, error) {
@@ -44,38 +48,14 @@ func Decode(identifier string) (did.DID, error) {
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
d, err := FromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
return d, nil
return DidKey{msi: msi, pubkey: pub}, nil
}
func FromPublicKey(pub crypto.PublicKey) (did.DID, error) {
switch pub := pub.(type) {
case ed25519.PublicKey:
d := DidKey{msi: pub.ToPublicKeyMultibase()}
d.signature = ed25519vm.NewVerificationKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, d.msi), pub, d)
xpub, err := x25519.PublicKeyFromEd25519(pub)
if err != nil {
return nil, err
}
xmsi := xpub.ToPublicKeyMultibase()
d.keyAgreement = x25519vm.NewKeyAgreementKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, xmsi), xpub, d)
return d, nil
case *p256.PublicKey, *p384.PublicKey, *p521.PublicKey:
d := DidKey{msi: pub.ToPublicKeyMultibase()}
mk := multikey.NewMultiKey(fmt.Sprintf("did:key:%s#%s", d.msi, d.msi), pub, d)
d.signature = mk
d.keyAgreement = mk
return d, nil
default:
return nil, fmt.Errorf("unsupported public key: %T", pub)
}
func FromPublicKey(pub crypto.PublicKey) did.DID {
return DidKey{msi: pub.ToPublicKeyMultibase()}
}
func FromPrivateKey(priv crypto.PrivateKey) (did.DID, error) {
func FromPrivateKey(priv crypto.PrivateKey) did.DID {
return FromPublicKey(priv.Public().(crypto.PublicKey))
}
@@ -83,12 +63,92 @@ func (d DidKey) Method() string {
return "key"
}
func (d DidKey) Document() (did.Document, error) {
return document{
id: d,
signature: d.signature,
keyAgreement: d.keyAgreement,
}, nil
func (d DidKey) Document(opts ...did.ResolutionOption) (did.Document, error) {
params := did.CollectResolutionOpts(opts)
doc := document{id: d}
mainVmId := fmt.Sprintf("did:key:%s#%s", d.msi, d.msi)
switch pub := d.pubkey.(type) {
case ed25519.PublicKey:
xpub, err := x25519.PublicKeyFromEd25519(pub)
if err != nil {
return nil, err
}
xmsi := xpub.ToPublicKeyMultibase()
xVmId := fmt.Sprintf("did:key:%s#%s", d.msi, xmsi)
switch {
case params.HasVerificationMethodHint(jsonwebkey.Type):
doc.signature = jsonwebkey.NewJsonWebKey2020(mainVmId, pub, d)
doc.keyAgreement = jsonwebkey.NewJsonWebKey2020(xVmId, xpub, d)
case params.HasVerificationMethodHint(multikey.Type):
doc.signature = multikey.NewMultiKey(mainVmId, pub, d)
doc.keyAgreement = multikey.NewMultiKey(xVmId, xpub, d)
default:
if params.HasVerificationMethodHint(ed25519vm.Type2018) {
doc.signature = ed25519vm.NewVerificationKey2018(mainVmId, pub, d)
}
if params.HasVerificationMethodHint(x25519vm.Type2019) {
doc.keyAgreement = x25519vm.NewKeyAgreementKey2019(xVmId, xpub, d)
}
if doc.signature == nil {
doc.signature = ed25519vm.NewVerificationKey2020(mainVmId, pub, d)
}
if doc.keyAgreement == nil {
doc.keyAgreement = x25519vm.NewKeyAgreementKey2020(xVmId, xpub, d)
}
}
case *p256.PublicKey:
switch {
case params.HasVerificationMethodHint(jsonwebkey.Type):
jwk := jsonwebkey.NewJsonWebKey2020(mainVmId, pub, d)
doc.signature = jwk
doc.keyAgreement = jwk
case params.HasVerificationMethodHint(p256vm.Type2021):
vm := p256vm.NewKey2021(mainVmId, pub, d)
doc.signature = vm
doc.keyAgreement = vm
default:
mk := multikey.NewMultiKey(mainVmId, pub, d)
doc.signature = mk
doc.keyAgreement = mk
}
case *secp256k1.PublicKey:
switch {
case params.HasVerificationMethodHint(jsonwebkey.Type):
jwk := jsonwebkey.NewJsonWebKey2020(mainVmId, pub, d)
doc.signature = jwk
doc.keyAgreement = jwk
case params.HasVerificationMethodHint(secp256k1vm.Type):
vm := secp256k1vm.NewVerificationKey2019(mainVmId, pub, d)
doc.signature = vm
doc.keyAgreement = vm
default:
mk := multikey.NewMultiKey(mainVmId, pub, d)
doc.signature = mk
doc.keyAgreement = mk
}
case *p384.PublicKey, *p521.PublicKey, *rsa.PublicKey:
switch {
case params.HasVerificationMethodHint(jsonwebkey.Type):
jwk := jsonwebkey.NewJsonWebKey2020(mainVmId, pub, d)
doc.signature = jwk
doc.keyAgreement = jwk
default:
mk := multikey.NewMultiKey(mainVmId, pub, d)
doc.signature = mk
doc.keyAgreement = mk
}
default:
return nil, fmt.Errorf("unsupported public key: %T", pub)
}
return doc, nil
}
func (d DidKey) String() string {
@@ -103,5 +163,8 @@ func (d DidKey) Equal(d2 did.DID) bool {
if d2, ok := d2.(DidKey); ok {
return d.msi == d2.msi
}
if d2, ok := d2.(*DidKey); ok {
return d.msi == d2.msi
}
return false
}

View File

@@ -20,8 +20,7 @@ func ExampleGenerateKeyPair() {
fmt.Println("Private key:", base64.StdEncoding.EncodeToString(priv.ToBytes()))
// Make the associated did:key
dk, err := didkey.FromPrivateKey(priv)
handleErr(err)
dk := didkey.FromPrivateKey(priv)
fmt.Println("Did:", dk.String())
// Produce a signature