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 }