Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,27 @@ func verifyTokenSignature(ctx context.Context, tok ucan.Token, cfg validationCon
return fmt.Errorf("unsupported token type: %T", tok)
}

// The capability relationships are optional (DID core §5.3): a document
// that expresses one restricts verification to the methods it lists, but
// a document that expresses nothing authorizes all of its verification
// methods — e.g. did:plc documents carry only verificationMethod.
// (Post-parse, an explicitly empty relationship is indistinguishable
// from an absent one and gets the same fallback.)
var vms []did.VerificationMethod
if verRel != nil {
vms = verRel.All()
}
if len(vms) == 0 && doc.VerificationMethods != nil {
for _, vm := range *doc.VerificationMethods {
vms = append(vms, vm)
}
}
Comment thread
alanshaw marked this conversation as resolved.

// Try each verification method, collecting rejection reasons for the error.
validationTime := time.Unix(int64(cfg.validationTime), 0)
var rejections []verrs.VMRejection

for _, vm := range verRel.All() {
for _, vm := range vms {
if vm.ExpiredAt(validationTime) {
Comment thread
alanshaw marked this conversation as resolved.
rejections = append(rejections, verrs.VMRejection{VM: vm, Reason: "expired"})
continue
Expand Down
122 changes: 122 additions & 0 deletions validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package validator_test

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"testing"
"time"
Expand Down Expand Up @@ -570,6 +572,126 @@ func TestValidate(t *testing.T) {
})
}

// TestRelationshipFallback covers the optional capability relationships (DID
// core §5.3): a document that expresses no capabilityInvocation /
// capabilityDelegation authorizes all of its verification methods, while a
// document that expresses one restricts verification to the methods listed.
Comment thread
alanshaw marked this conversation as resolved.
func TestRelationshipFallback(t *testing.T) {
crankWidget := testutil.Must(command.Parse("/widget/crank"))(t)

// plcShapedResolver serves a document parsed from JSON that carries only
// verificationMethod — the shape did:plc directories emit.
plcShapedResolver := did.ResolverFunc(func(_ context.Context, d did.DID) (did.Document, error) {
docJSON := fmt.Sprintf(`{
"id": %q,
"verificationMethod": [{
"id": "%s#key",
"type": %q,
"controller": %q,
"publicKeyMultibase": %q
}]
}`, d, d, did.MultikeyVerificationMethodType, d, d.Identifier())
var doc did.Document
if err := json.Unmarshal([]byte(docJSON), &doc); err != nil {
return did.Document{}, err
}
return doc, nil
})

t.Run("invocation verifies against a document without relationships", func(t *testing.T) {
subject := testutil.RandomIssuer(t)

inv, err := invocation.Invoke(subject, subject.DID(), crankWidget, datamodel.Map{})
require.NoError(t, err)

err = validator.ValidateInvocation(t.Context(), inv,
validator.WithDIDResolver(plcShapedResolver))
require.NoError(t, err)
})

t.Run("delegation in chain verifies against a document without relationships", func(t *testing.T) {
subject := testutil.RandomIssuer(t)
bob := testutil.RandomIssuer(t)

del, err := delegation.Delegate(subject, bob.DID(), subject.DID(), crankWidget)
require.NoError(t, err)
inv, err := invocation.Invoke(bob, subject.DID(), crankWidget, datamodel.Map{},
invocation.WithProofs(del.Link()))
require.NoError(t, err)

err = validator.ValidateInvocation(t.Context(), inv,
validator.WithDIDResolver(plcShapedResolver),
validator.WithProofResolver(validator.ProofsFromContainer(
container.New(container.WithDelegations(del)))),
)
require.NoError(t, err)
})

t.Run("populated relationship still restricts to its listed methods", func(t *testing.T) {
subject := testutil.RandomIssuer(t)
other := testutil.RandomIssuer(t) // a key the subject does NOT sign with

resolver := did.ResolverFunc(func(_ context.Context, d did.DID) (did.Document, error) {
doc := did.NewDocument(d)
signerVM := did.VerificationMethod{
ID: doc.Fragment("signer"),
Controller: d,
Type: did.MultikeyVerificationMethodType,
Material: did.GenericMap{did.MultikeyPublicKeyMultibaseProp: d.Identifier()},
}
otherVM := did.VerificationMethod{
ID: doc.Fragment("other"),
Controller: d,
Type: did.MultikeyVerificationMethodType,
Material: did.GenericMap{did.MultikeyPublicKeyMultibaseProp: other.DID().Identifier()},
}
if err := doc.VerificationMethods.Add(signerVM); err != nil {
return did.Document{}, err
}
// Only the non-signing key is authorized for capability invocation,
// so verification must NOT fall back to the signer's method.
if err := doc.CapabilityInvocation.Add(otherVM); err != nil {
return did.Document{}, err
}
return doc, nil
})

inv, err := invocation.Invoke(subject, subject.DID(), crankWidget, datamodel.Map{})
require.NoError(t, err)

err = validator.ValidateInvocation(t.Context(), inv,
validator.WithDIDResolver(resolver))
require.Error(t, err)
})

t.Run("nil relationships fall back without panicking", func(t *testing.T) {
subject := testutil.RandomIssuer(t)

// A document built without NewDocument: relationship fields are nil.
resolver := did.ResolverFunc(func(_ context.Context, d did.DID) (did.Document, error) {
vms := did.VerificationMethods{}
doc := did.Document{ID: d, VerificationMethods: &vms}
vm := did.VerificationMethod{
ID: doc.Fragment("key"),
Controller: d,
Type: did.MultikeyVerificationMethodType,
Material: did.GenericMap{did.MultikeyPublicKeyMultibaseProp: d.Identifier()},
}
if err := vms.Add(vm); err != nil {
return did.Document{}, err
}
return doc, nil
})

inv, err := invocation.Invoke(subject, subject.DID(), crankWidget, datamodel.Map{})
require.NoError(t, err)

err = validator.ValidateInvocation(t.Context(), inv,
validator.WithDIDResolver(resolver))
require.NoError(t, err)
})
}

// expiredKeyResolver returns a DID resolver that serves a document for the
// issuer's DID with its Multikey VM marked expired or revoked as specified.
func expiredKeyResolver(t *testing.T, issuer ucan.Issuer, expires, revoked *did.DateTimeStamp) did.Resolver {
Expand Down