@@ -2,7 +2,9 @@ package validator_test
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
7+ "fmt"
68 "os"
79 "testing"
810 "time"
@@ -570,6 +572,159 @@ func TestValidate(t *testing.T) {
570572 })
571573}
572574
575+ // TestRelationshipFallback covers the optional capability relationships (DID
576+ // core §5.3): a document that expresses no capabilityInvocation /
577+ // capabilityDelegation authorizes all of its verification methods, while a
578+ // document that expresses one restricts verification to the methods listed.
579+ func TestRelationshipFallback (t * testing.T ) {
580+ crankWidget := testutil .Must (command .Parse ("/widget/crank" ))(t )
581+
582+ // plcShapedResolver serves a document parsed from JSON that carries only
583+ // verificationMethod — the shape did:plc directories emit.
584+ plcShapedResolver := did .ResolverFunc (func (_ context.Context , d did.DID ) (did.Document , error ) {
585+ docJSON := fmt .Sprintf (`{
586+ "id": %q,
587+ "verificationMethod": [{
588+ "id": "%s#key",
589+ "type": %q,
590+ "controller": %q,
591+ "publicKeyMultibase": %q
592+ }]
593+ }` , d , d , did .MultikeyVerificationMethodType , d , d .Identifier ())
594+ var doc did.Document
595+ if err := json .Unmarshal ([]byte (docJSON ), & doc ); err != nil {
596+ return did.Document {}, err
597+ }
598+ return doc , nil
599+ })
600+
601+ t .Run ("invocation verifies against a document without relationships" , func (t * testing.T ) {
602+ subject := testutil .RandomIssuer (t )
603+
604+ inv , err := invocation .Invoke (subject , subject .DID (), crankWidget , datamodel.Map {})
605+ require .NoError (t , err )
606+
607+ err = validator .ValidateInvocation (t .Context (), inv ,
608+ validator .WithDIDResolver (plcShapedResolver ))
609+ require .NoError (t , err )
610+ })
611+
612+ t .Run ("delegation in chain verifies against a document without relationships" , func (t * testing.T ) {
613+ subject := testutil .RandomIssuer (t )
614+ bob := testutil .RandomIssuer (t )
615+
616+ del , err := delegation .Delegate (subject , bob .DID (), subject .DID (), crankWidget )
617+ require .NoError (t , err )
618+ inv , err := invocation .Invoke (bob , subject .DID (), crankWidget , datamodel.Map {},
619+ invocation .WithProofs (del .Link ()))
620+ require .NoError (t , err )
621+
622+ err = validator .ValidateInvocation (t .Context (), inv ,
623+ validator .WithDIDResolver (plcShapedResolver ),
624+ validator .WithProofResolver (validator .ProofsFromContainer (
625+ container .New (container .WithDelegations (del )))),
626+ )
627+ require .NoError (t , err )
628+ })
629+
630+ t .Run ("populated relationship still restricts to its listed methods" , func (t * testing.T ) {
631+ subject := testutil .RandomIssuer (t )
632+ other := testutil .RandomIssuer (t ) // a key the subject does NOT sign with
633+
634+ resolver := did .ResolverFunc (func (_ context.Context , d did.DID ) (did.Document , error ) {
635+ doc := did .NewDocument (d )
636+ signerVM := did.VerificationMethod {
637+ ID : doc .Fragment ("signer" ),
638+ Controller : d ,
639+ Type : did .MultikeyVerificationMethodType ,
640+ Material : did.GenericMap {did .MultikeyPublicKeyMultibaseProp : d .Identifier ()},
641+ }
642+ otherVM := did.VerificationMethod {
643+ ID : doc .Fragment ("other" ),
644+ Controller : d ,
645+ Type : did .MultikeyVerificationMethodType ,
646+ Material : did.GenericMap {did .MultikeyPublicKeyMultibaseProp : other .DID ().Identifier ()},
647+ }
648+ if err := doc .VerificationMethods .Add (signerVM ); err != nil {
649+ return did.Document {}, err
650+ }
651+ // Only the non-signing key is authorized for capability invocation,
652+ // so verification must NOT fall back to the signer's method.
653+ if err := doc .CapabilityInvocation .Add (otherVM ); err != nil {
654+ return did.Document {}, err
655+ }
656+ return doc , nil
657+ })
658+
659+ inv , err := invocation .Invoke (subject , subject .DID (), crankWidget , datamodel.Map {})
660+ require .NoError (t , err )
661+
662+ err = validator .ValidateInvocation (t .Context (), inv ,
663+ validator .WithDIDResolver (resolver ))
664+ require .Error (t , err )
665+ })
666+
667+ t .Run ("relationship referencing a missing method does not fall back" , func (t * testing.T ) {
668+ subject := testutil .RandomIssuer (t )
669+
670+ // The document lists the signer under verificationMethod, but
671+ // capabilityInvocation references an ID with no matching method. The
672+ // relationship is present (not silent), so verification must restrict
673+ // to it — not widen to all of verificationMethod.
674+ resolver := did .ResolverFunc (func (_ context.Context , d did.DID ) (did.Document , error ) {
675+ docJSON := fmt .Sprintf (`{
676+ "id": %q,
677+ "verificationMethod": [{
678+ "id": "%s#key",
679+ "type": %q,
680+ "controller": %q,
681+ "publicKeyMultibase": %q
682+ }],
683+ "capabilityInvocation": ["%s#missing"]
684+ }` , d , d , did .MultikeyVerificationMethodType , d , d .Identifier (), d )
685+ var doc did.Document
686+ if err := json .Unmarshal ([]byte (docJSON ), & doc ); err != nil {
687+ return did.Document {}, err
688+ }
689+ return doc , nil
690+ })
691+
692+ inv , err := invocation .Invoke (subject , subject .DID (), crankWidget , datamodel.Map {})
693+ require .NoError (t , err )
694+
695+ err = validator .ValidateInvocation (t .Context (), inv ,
696+ validator .WithDIDResolver (resolver ))
697+ require .Error (t , err )
698+ })
699+
700+ t .Run ("nil relationships fall back without panicking" , func (t * testing.T ) {
701+ subject := testutil .RandomIssuer (t )
702+
703+ // A document built without NewDocument: relationship fields are nil.
704+ resolver := did .ResolverFunc (func (_ context.Context , d did.DID ) (did.Document , error ) {
705+ vms := did.VerificationMethods {}
706+ doc := did.Document {ID : d , VerificationMethods : & vms }
707+ vm := did.VerificationMethod {
708+ ID : doc .Fragment ("key" ),
709+ Controller : d ,
710+ Type : did .MultikeyVerificationMethodType ,
711+ Material : did.GenericMap {did .MultikeyPublicKeyMultibaseProp : d .Identifier ()},
712+ }
713+ if err := vms .Add (vm ); err != nil {
714+ return did.Document {}, err
715+ }
716+ return doc , nil
717+ })
718+
719+ inv , err := invocation .Invoke (subject , subject .DID (), crankWidget , datamodel.Map {})
720+ require .NoError (t , err )
721+
722+ err = validator .ValidateInvocation (t .Context (), inv ,
723+ validator .WithDIDResolver (resolver ))
724+ require .NoError (t , err )
725+ })
726+ }
727+
573728// expiredKeyResolver returns a DID resolver that serves a document for the
574729// issuer's DID with its Multikey VM marked expired or revoked as specified.
575730func expiredKeyResolver (t * testing.T , issuer ucan.Issuer , expires , revoked * did.DateTimeStamp ) did.Resolver {
0 commit comments