github.com/storacha/go-ucanto@v0.7.2/validator/lib.go (about)

     1  package validator
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"sync"
     8  
     9  	"github.com/storacha/go-ucanto/core/dag/blockstore"
    10  	"github.com/storacha/go-ucanto/core/delegation"
    11  	"github.com/storacha/go-ucanto/core/invocation"
    12  	"github.com/storacha/go-ucanto/core/result/failure"
    13  	"github.com/storacha/go-ucanto/core/schema"
    14  	"github.com/storacha/go-ucanto/did"
    15  	"github.com/storacha/go-ucanto/principal"
    16  	"github.com/storacha/go-ucanto/principal/verifier"
    17  	"github.com/storacha/go-ucanto/ucan"
    18  	vdm "github.com/storacha/go-ucanto/validator/datamodel"
    19  	"github.com/ucan-wg/go-ucan/capability/policy"
    20  	"github.com/ucan-wg/go-ucan/capability/policy/literal"
    21  	"github.com/ucan-wg/go-ucan/capability/policy/selector"
    22  )
    23  
    24  func IsSelfIssued[Caveats any](capability ucan.Capability[Caveats], issuer did.DID) bool {
    25  	return capability.With() == issuer.DID().String()
    26  }
    27  
    28  func ProofUnavailable(ctx context.Context, p ucan.Link) (delegation.Delegation, UnavailableProof) {
    29  	return nil, NewUnavailableProofError(p, fmt.Errorf("no proof resolver configured"))
    30  }
    31  
    32  func FailDIDKeyResolution(ctx context.Context, d did.DID) (did.DID, UnresolvedDID) {
    33  	return did.Undef, NewDIDKeyResolutionError(d, fmt.Errorf("no DID resolver configured"))
    34  }
    35  
    36  func NotExpiredNotTooEarly(dlg delegation.Delegation) InvalidProof {
    37  	if ucan.IsExpired(dlg) {
    38  		return NewExpiredError(dlg)
    39  	}
    40  	if ucan.IsTooEarly(dlg) {
    41  		return NewNotValidBeforeError(dlg)
    42  	}
    43  
    44  	return nil
    45  }
    46  
    47  // PrincipalParser provides verifier instances that can validate UCANs issued
    48  // by a given principal.
    49  type PrincipalParser interface {
    50  	ParsePrincipal(str string) (principal.Verifier, error)
    51  }
    52  
    53  type PrincipalParserFunc func(str string) (principal.Verifier, error)
    54  
    55  // PrincipalResolver is used to resolve a key of the principal that is
    56  // identified by DID different from did:key method. It can be passed into a
    57  // UCAN validator in order to augmented it with additional DID methods support.
    58  type PrincipalResolver interface {
    59  	ResolveDIDKey(ctx context.Context, did did.DID) (did.DID, UnresolvedDID)
    60  }
    61  
    62  // PrincipalResolverFunc resolves the key of a principal that is identified by
    63  // DID different from did:key method.
    64  type PrincipalResolverFunc func(ctx context.Context, did did.DID) (did.DID, UnresolvedDID)
    65  
    66  // ProofResolver finds a delegations when external proof links are present in
    67  // UCANs. If a resolver is not provided the validator may not be able to explore
    68  // corresponding path within a proof chain.
    69  type ProofResolver interface {
    70  	// Resolve finds a delegation corresponding to an external proof link.
    71  	ResolveProof(ctx context.Context, proof ucan.Link) (delegation.Delegation, UnavailableProof)
    72  }
    73  
    74  // Resolve finds a delegation corresponding to an external proof link.
    75  type ProofResolverFunc func(ctx context.Context, proof ucan.Link) (delegation.Delegation, UnavailableProof)
    76  
    77  type CanIssuer[Caveats any] interface {
    78  	// CanIssue informs validator whether given capability can be issued by a
    79  	// given DID or whether it needs to be delegated to the issuer.
    80  	CanIssue(capability ucan.Capability[Caveats], issuer did.DID) bool
    81  }
    82  
    83  // CanIssue informs validator whether given capability can be issued by a
    84  // given DID or whether it needs to be delegated to the issuer.
    85  type CanIssueFunc[Caveats any] func(capability ucan.Capability[Caveats], issuer did.DID) bool
    86  
    87  // canissuer converts an CanIssuer[any] to CanIssuer[Caveats]
    88  type canissuer[Caveats any] struct {
    89  	canIssue CanIssueFunc[any]
    90  }
    91  
    92  func (ci canissuer[Caveats]) CanIssue(c ucan.Capability[Caveats], d did.DID) bool {
    93  	return ci.canIssue(ucan.NewCapability[any](c.Can(), c.With(), c.Nb()), d)
    94  }
    95  
    96  type RevocationChecker[Caveats any] interface {
    97  	// ValidateAuthorization validates that the passed authorization has not been
    98  	// revoked. It returns `nil` if not revoked.
    99  	ValidateAuthorization(ctx context.Context, auth Authorization[Caveats]) Revoked
   100  }
   101  
   102  // RevocationCheckerFunc validates that the passed authorization has not been
   103  // revoked. It returns `nil` if not revoked.
   104  type RevocationCheckerFunc[Caveats any] func(ctx context.Context, auth Authorization[Caveats]) Revoked
   105  
   106  // AuthorityProver provides a set of proofs of authority
   107  type AuthorityProver interface {
   108  	AuthorityProofs() []delegation.Delegation
   109  }
   110  
   111  // Validator must provide a [principal.Verifier] corresponding to local authority.
   112  //
   113  // A capability provider service will use one corresponding to own DID or it's
   114  // supervisor's DID if it acts under it's authority.
   115  //
   116  // This allows a service identified by non did:key e.g. did:web or did:dns to
   117  // pass resolved key so it does not need to be resolved at runtime.
   118  type Validator interface {
   119  	Authority() principal.Verifier
   120  }
   121  
   122  type TimeBoundsValidator interface {
   123  	ValidateTimeBounds(dlg delegation.Delegation) InvalidProof
   124  }
   125  
   126  type TimeBoundsValidatorFunc func(dlg delegation.Delegation) InvalidProof
   127  
   128  type ClaimContext interface {
   129  	Validator
   130  	RevocationChecker[any]
   131  	CanIssuer[any]
   132  	ProofResolver
   133  	PrincipalParser
   134  	PrincipalResolver
   135  	AuthorityProver
   136  	TimeBoundsValidator
   137  }
   138  
   139  type claimContext struct {
   140  	authority             principal.Verifier
   141  	canIssue              CanIssueFunc[any]
   142  	validateAuthorization RevocationCheckerFunc[any]
   143  	resolveProof          ProofResolverFunc
   144  	parsePrincipal        PrincipalParserFunc
   145  	resolveDIDKey         PrincipalResolverFunc
   146  	validateTimeBounds    TimeBoundsValidatorFunc
   147  	authorityProofs       []delegation.Delegation
   148  }
   149  
   150  func NewClaimContext(
   151  	authority principal.Verifier,
   152  	canIssue CanIssueFunc[any],
   153  	validateAuthorization RevocationCheckerFunc[any],
   154  	resolveProof ProofResolverFunc,
   155  	parsePrincipal PrincipalParserFunc,
   156  	resolveDIDKey PrincipalResolverFunc,
   157  	validateTimeBounds TimeBoundsValidatorFunc,
   158  	authorityProofs ...delegation.Delegation,
   159  ) ClaimContext {
   160  	return claimContext{
   161  		authority,
   162  		canIssue,
   163  		validateAuthorization,
   164  		resolveProof,
   165  		parsePrincipal,
   166  		resolveDIDKey,
   167  		validateTimeBounds,
   168  		authorityProofs,
   169  	}
   170  }
   171  
   172  func (cc claimContext) Authority() principal.Verifier {
   173  	return cc.authority
   174  }
   175  
   176  func (cc claimContext) CanIssue(capability ucan.Capability[any], issuer did.DID) bool {
   177  	return cc.canIssue(capability, issuer)
   178  }
   179  
   180  func (cc claimContext) ValidateAuthorization(ctx context.Context, auth Authorization[any]) Revoked {
   181  	return cc.validateAuthorization(ctx, auth)
   182  }
   183  
   184  func (cc claimContext) ResolveProof(ctx context.Context, proof ucan.Link) (delegation.Delegation, UnavailableProof) {
   185  	return cc.resolveProof(ctx, proof)
   186  }
   187  
   188  func (cc claimContext) ParsePrincipal(str string) (principal.Verifier, error) {
   189  	return cc.parsePrincipal(str)
   190  }
   191  
   192  func (cc claimContext) ResolveDIDKey(ctx context.Context, did did.DID) (did.DID, UnresolvedDID) {
   193  	return cc.resolveDIDKey(ctx, did)
   194  }
   195  
   196  func (cc claimContext) ValidateTimeBounds(dlg delegation.Delegation) InvalidProof {
   197  	return cc.validateTimeBounds(dlg)
   198  }
   199  
   200  func (cc claimContext) AuthorityProofs() []delegation.Delegation {
   201  	return cc.authorityProofs
   202  }
   203  
   204  type ValidationContext[Caveats any] interface {
   205  	ClaimContext
   206  	Capability() CapabilityParser[Caveats]
   207  }
   208  
   209  type validationContext[Caveats any] struct {
   210  	claimContext
   211  	capability CapabilityParser[Caveats]
   212  }
   213  
   214  func NewValidationContext[Caveats any](
   215  	authority principal.Verifier,
   216  	capability CapabilityParser[Caveats],
   217  	canIssue CanIssueFunc[any],
   218  	validateAuthorization RevocationCheckerFunc[any],
   219  	resolveProof ProofResolverFunc,
   220  	parsePrincipal PrincipalParserFunc,
   221  	resolveDIDKey PrincipalResolverFunc,
   222  	validateTimeBounds TimeBoundsValidatorFunc,
   223  	authorityProofs ...delegation.Delegation,
   224  ) ValidationContext[Caveats] {
   225  	return validationContext[Caveats]{
   226  		claimContext{
   227  			authority,
   228  			canIssue,
   229  			validateAuthorization,
   230  			resolveProof,
   231  			parsePrincipal,
   232  			resolveDIDKey,
   233  			validateTimeBounds,
   234  			authorityProofs,
   235  		},
   236  		capability,
   237  	}
   238  }
   239  
   240  func (vc validationContext[Caveats]) Capability() CapabilityParser[Caveats] {
   241  	return vc.capability
   242  }
   243  
   244  // Access finds a valid path in a proof chain of the given
   245  // [invocation.Invocation] by exploring every possible option. On success an
   246  // [Authorization] object is returned that illustrates the valid path. If no
   247  // valid path is found [Unauthorized] error is returned detailing all explored
   248  // paths and where they proved to fail.
   249  func Access[Caveats any](ctx context.Context, invocation invocation.Invocation, vctx ValidationContext[Caveats]) (Authorization[Caveats], Unauthorized) {
   250  	prf := []delegation.Proof{delegation.FromDelegation(invocation)}
   251  	return Claim(ctx, vctx.Capability(), prf, vctx)
   252  }
   253  
   254  // Claim attempts to find a valid proof chain for the claimed [CapabilityParser]
   255  // given set of `proofs`. On success an [Authorization] object with detailed
   256  // proof chain is returned and on failure [Unauthorized] error is returned with
   257  // details on paths explored and why they have failed.
   258  func Claim[Caveats any](ctx context.Context, capability CapabilityParser[Caveats], proofs []delegation.Proof, cctx ClaimContext) (Authorization[Caveats], Unauthorized) {
   259  	var sources []Source
   260  	var invalidprf []InvalidProof
   261  
   262  	delegations, rerrs := ResolveProofs(ctx, proofs, cctx)
   263  	for _, err := range rerrs {
   264  		invalidprf = append(invalidprf, err)
   265  	}
   266  
   267  	for _, prf := range delegations {
   268  		// Validate each proof if valid add each capability to the list of sources
   269  		// or collect the error.
   270  		validation, err := Validate(ctx, prf, delegations, cctx)
   271  		if err != nil {
   272  			invalidprf = append(invalidprf, err)
   273  			continue
   274  		}
   275  
   276  		for _, c := range validation.Capabilities() {
   277  			sources = append(sources, NewSource(c, prf))
   278  		}
   279  	}
   280  
   281  	// look for the matching capability
   282  	matches, dlgerrs, unknowns := capability.Select(sources)
   283  
   284  	var failedprf []InvalidClaim
   285  	for _, matched := range matches {
   286  		selector := matched.Prune(canissuer[Caveats]{canIssue: cctx.CanIssue})
   287  		if selector == nil {
   288  			auth := NewAuthorization(matched, nil)
   289  			revoked := cctx.ValidateAuthorization(ctx, ConvertUnknownAuthorization(auth))
   290  			if revoked != nil {
   291  				invalidprf = append(invalidprf, revoked)
   292  				continue
   293  			}
   294  			return auth, nil
   295  		}
   296  
   297  		a, err := Authorize(ctx, matched, cctx)
   298  		if err != nil {
   299  			failedprf = append(failedprf, err)
   300  			continue
   301  		}
   302  
   303  		auth := NewAuthorization(matched, []Authorization[Caveats]{a})
   304  		revoked := cctx.ValidateAuthorization(ctx, ConvertUnknownAuthorization(auth))
   305  		if revoked != nil {
   306  			invalidprf = append(invalidprf, revoked)
   307  			continue
   308  		}
   309  
   310  		return auth, nil
   311  	}
   312  
   313  	return nil, NewUnauthorizedError(capability, dlgerrs, unknowns, invalidprf, failedprf)
   314  }
   315  
   316  // ResolveProofs takes `proofs` from the delegation which may contain
   317  // a [delegation.Delegation] or a link to one and attempts to resolve links by
   318  // side loading them. It returns a set of resolved [delegation.Delegation]s and
   319  // errors for the proofs that could not be resolved.
   320  func ResolveProofs(ctx context.Context, proofs []delegation.Proof, resolver ProofResolver) (dels []delegation.Delegation, errs []UnavailableProof) {
   321  	for _, p := range proofs {
   322  		d, ok := p.Delegation()
   323  		if ok {
   324  			dels = append(dels, d)
   325  		} else {
   326  			d, err := resolver.ResolveProof(ctx, p.Link())
   327  			if err != nil {
   328  				errs = append(errs, err)
   329  				continue
   330  			}
   331  			dels = append(dels, d)
   332  		}
   333  	}
   334  	return
   335  }
   336  
   337  // Validate a delegation to check it is within the time bound and that it is
   338  // authorized by the issuer.
   339  func Validate(ctx context.Context, dlg delegation.Delegation, prfs []delegation.Delegation, cctx ClaimContext) (delegation.Delegation, InvalidProof) {
   340  	if invalid := cctx.ValidateTimeBounds(dlg); invalid != nil {
   341  		return nil, invalid
   342  	}
   343  
   344  	return VerifyAuthorization(ctx, dlg, prfs, cctx)
   345  }
   346  
   347  // VerifyAuthorization verifies that delegation has been authorized by the
   348  // issuer. If issued by the did:key principal checks that the signature is
   349  // valid. If issued by the root authority checks that the signature is valid. If
   350  // issued by the principal identified by other DID method attempts to resolve a
   351  // valid `ucan/attest` attestation from the authority, if attestation is not
   352  // found falls back to resolving did:key for the issuer and verifying its
   353  // signature.
   354  func VerifyAuthorization(ctx context.Context, dlg delegation.Delegation, prfs []delegation.Delegation, cctx ClaimContext) (delegation.Delegation, InvalidProof) {
   355  	issuer := dlg.Issuer().DID()
   356  	// If the issuer is a did:key we just verify a signature
   357  	if strings.HasPrefix(issuer.String(), "did:key:") {
   358  		vfr, err := cctx.ParsePrincipal(issuer.String())
   359  		if err != nil {
   360  			return nil, NewUnverifiableSignatureError(dlg, err)
   361  		}
   362  		return VerifySignature(dlg, vfr)
   363  	}
   364  
   365  	if dlg.Issuer().DID() == cctx.Authority().DID() {
   366  		return VerifySignature(dlg, cctx.Authority())
   367  	}
   368  
   369  	// If issuer is not a did:key principal nor configured authority, we
   370  	// attempt to resolve embedded authorization session from the authority
   371  	_, err := VerifySession(ctx, dlg, prfs, cctx)
   372  	if err != nil {
   373  		if len(err.FailedProofs()) > 0 {
   374  			return nil, NewSessionEscalationError(dlg, err)
   375  		}
   376  
   377  		// Otherwise we try to resolve did:key from the DID instead
   378  		// and use that to verify the signature
   379  		did, err := cctx.ResolveDIDKey(ctx, issuer)
   380  		if err != nil {
   381  			return nil, err
   382  		}
   383  
   384  		vfr, perr := cctx.ParsePrincipal(did.String())
   385  		if perr != nil {
   386  			return nil, NewUnverifiableSignatureError(dlg, perr)
   387  		}
   388  
   389  		wvfr, werr := verifier.Wrap(vfr, issuer)
   390  		if werr != nil {
   391  			return nil, NewUnverifiableSignatureError(dlg, perr)
   392  		}
   393  
   394  		return VerifySignature(dlg, wvfr)
   395  	}
   396  
   397  	return dlg, nil
   398  }
   399  
   400  // VerifySignature verifies the delegation was signed by the passed verifier.
   401  func VerifySignature(dlg delegation.Delegation, vfr principal.Verifier) (delegation.Delegation, BadSignature) {
   402  	ok, err := ucan.VerifySignature(dlg.Data(), vfr)
   403  	if err != nil {
   404  		return nil, NewUnverifiableSignatureError(dlg, err)
   405  	}
   406  	if !ok {
   407  		return nil, NewInvalidSignatureError(dlg, vfr)
   408  	}
   409  	return dlg, nil
   410  }
   411  
   412  // VerifySession attempts to find an authorization session - an `ucan/attest`
   413  // capability delegation where `with` matches `ctx.Authority()` and `nb.proof`
   414  // matches given delegation.
   415  //
   416  // https://github.com/storacha-network/specs/blob/main/w3-session.md#authorization-session
   417  func VerifySession(ctx context.Context, dlg delegation.Delegation, prfs []delegation.Delegation, cctx ClaimContext) (Authorization[vdm.AttestationModel], Unauthorized) {
   418  	// Recognize attestations from all authorized principals, not just authority
   419  	var withSchemas []schema.Reader[string, string]
   420  	for _, p := range cctx.AuthorityProofs() {
   421  		if p.Capabilities()[0].Can() == "ucan/attest" && p.Capabilities()[0].With() == cctx.Authority().DID().String() {
   422  			withSchemas = append(withSchemas, schema.Literal(p.Audience().DID().String()))
   423  		}
   424  	}
   425  
   426  	withSchema := schema.Literal(cctx.Authority().DID().String())
   427  	if len(withSchemas) > 0 {
   428  		withSchemas = append(withSchemas, schema.Literal(cctx.Authority().DID().String()))
   429  		withSchema = schema.Or(withSchemas...)
   430  	}
   431  
   432  	// Create a schema that will match an authorization for this exact delegation
   433  	attestation := NewCapability(
   434  		"ucan/attest",
   435  		withSchema,
   436  		schema.Struct[vdm.AttestationModel](
   437  			vdm.AttestationType(),
   438  			policy.Policy{
   439  				policy.Equal(selector.MustParse(".proof"), literal.Link(dlg.Link())),
   440  			},
   441  		),
   442  		func(claimed, delegated ucan.Capability[vdm.AttestationModel]) failure.Failure {
   443  			err := DefaultDerives(claimed, delegated)
   444  			if err != nil {
   445  				return err
   446  			}
   447  			if claimed.Nb().Proof != delegated.Nb().Proof {
   448  				return schema.NewSchemaError(fmt.Sprintf(`proof: %s violates %s`, claimed.Nb().Proof, delegated.Nb().Proof))
   449  			}
   450  			return nil
   451  		},
   452  	)
   453  
   454  	// We only consider attestations otherwise we will end up doing an
   455  	// exponential scan if there are other proofs that require attestations.
   456  	// Also filter any proofs that _are_ the delegation we're verifying so
   457  	// we don't recurse indefinitely.
   458  	var aprfs []delegation.Proof
   459  	for _, p := range prfs {
   460  		if p.Link().String() == dlg.Link().String() {
   461  			continue
   462  		}
   463  
   464  		if p.Capabilities()[0].Can() == "ucan/attest" {
   465  			aprfs = append(aprfs, delegation.FromDelegation(p))
   466  		}
   467  	}
   468  
   469  	return Claim(ctx, attestation, aprfs, cctx)
   470  }
   471  
   472  // Authorize verifies whether any of the delegated proofs grant capability.
   473  func Authorize[Caveats any](ctx context.Context, match Match[Caveats], cctx ClaimContext) (Authorization[Caveats], InvalidClaim) {
   474  	// load proofs from all delegations
   475  	sources, invalidprf := ResolveMatch(ctx, match, cctx)
   476  
   477  	matches, dlgerrs, unknowns := match.Select(sources)
   478  
   479  	var failedprf []InvalidClaim
   480  	for _, matched := range matches {
   481  		selector := matched.Prune(canissuer[Caveats]{canIssue: cctx.CanIssue})
   482  		if selector == nil {
   483  			return NewAuthorization(matched, nil), nil
   484  		}
   485  
   486  		auth, err := Authorize(ctx, selector, cctx)
   487  		if err != nil {
   488  			failedprf = append(failedprf, err)
   489  			continue
   490  		}
   491  
   492  		return NewAuthorization(matched, []Authorization[Caveats]{auth}), nil
   493  	}
   494  
   495  	return nil, NewInvalidClaimError(match, dlgerrs, unknowns, invalidprf, failedprf)
   496  }
   497  
   498  func ResolveMatch[Caveats any](ctx context.Context, match Match[Caveats], context ClaimContext) (sources []Source, errors []ProofError) {
   499  	includes := map[string]struct{}{}
   500  	var wg sync.WaitGroup
   501  	var lock sync.RWMutex
   502  	for _, source := range match.Source() {
   503  		id := source.Delegation().Link().String()
   504  		if _, ok := includes[id]; !ok {
   505  			includes[id] = struct{}{}
   506  			wg.Add(1)
   507  			go func(s Source) {
   508  				srcs, errs := ResolveSources(ctx, s, context)
   509  				lock.Lock()
   510  				defer lock.Unlock()
   511  				defer wg.Done()
   512  				sources = append(sources, srcs...)
   513  				errors = append(errors, errs...)
   514  			}(source)
   515  		}
   516  	}
   517  	wg.Wait()
   518  	return
   519  }
   520  
   521  func ResolveSources(ctx context.Context, source Source, cctx ClaimContext) (sources []Source, errors []ProofError) {
   522  	dlg := source.Delegation()
   523  	var prfs []delegation.Delegation
   524  
   525  	br, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(dlg.Blocks()))
   526  	if err != nil {
   527  		errors = append(errors, NewProofError(dlg.Link(), err))
   528  		return
   529  	}
   530  
   531  	dlgs, failedprf := ResolveProofs(
   532  		ctx,
   533  		delegation.NewProofsView(dlg.Proofs(), br),
   534  		cctx,
   535  	)
   536  
   537  	// All the proofs that failed to resolve are saved as proof errors.
   538  	for _, err := range failedprf {
   539  		errors = append(errors, NewProofError(err.Link(), err))
   540  	}
   541  
   542  	// All the proofs that resolved are checked for principal alignment. Ones that
   543  	// do not align are saved as proof errors.
   544  	for _, prf := range dlgs {
   545  		// If proof does not delegate to a matching audience save an proof error.
   546  		if dlg.Issuer().DID() != prf.Audience().DID() {
   547  			errors = append(errors, NewProofError(prf.Link(), NewPrincipalAlignmentError(dlg.Issuer(), prf)))
   548  		} else {
   549  			prfs = append(prfs, prf)
   550  		}
   551  	}
   552  	// In the second pass we attempt to proofs that were resolved and are aligned.
   553  	for _, prf := range prfs {
   554  		_, err := Validate(ctx, prf, prfs, cctx)
   555  
   556  		// If proof is not valid (expired, not active yet or has incorrect
   557  		// signature) save a corresponding proof error.
   558  		if err != nil {
   559  			errors = append(errors, NewProofError(prf.Link(), err))
   560  			continue
   561  		}
   562  
   563  		// Otherwise create source objects for it's capabilities, so we could
   564  		// track which proof in which capability the are from.
   565  		for _, cap := range prf.Capabilities() {
   566  			sources = append(sources, NewSource(cap, prf))
   567  		}
   568  	}
   569  	return
   570  }