github.com/storacha/go-ucanto@v0.7.2/validator/session_test.go (about) 1 package validator 2 3 import ( 4 "context" 5 "encoding/base64" 6 "fmt" 7 "strings" 8 "testing" 9 10 "github.com/ipld/go-ipld-prime" 11 "github.com/ipld/go-ipld-prime/node/basicnode" 12 "github.com/storacha/go-ucanto/core/delegation" 13 "github.com/storacha/go-ucanto/core/result/failure" 14 "github.com/storacha/go-ucanto/core/schema" 15 "github.com/storacha/go-ucanto/did" 16 "github.com/storacha/go-ucanto/principal/absentee" 17 ed25519 "github.com/storacha/go-ucanto/principal/ed25519/signer" 18 "github.com/storacha/go-ucanto/principal/signer" 19 "github.com/storacha/go-ucanto/testing/fixtures" 20 "github.com/storacha/go-ucanto/testing/helpers" 21 "github.com/storacha/go-ucanto/ucan" 22 "github.com/stretchr/testify/require" 23 ) 24 25 var serviceDID = helpers.Must(did.Parse("did:web:example.com")) 26 var service = helpers.Must(signer.Wrap(fixtures.Service, serviceDID)) 27 28 type debugEchoCaveats struct { 29 Message *string 30 } 31 32 func (c debugEchoCaveats) ToIPLD() (ipld.Node, error) { 33 np := basicnode.Prototype.Any 34 nb := np.NewBuilder() 35 ma, _ := nb.BeginMap(1) 36 if c.Message != nil { 37 ma.AssembleKey().AssignString("message") 38 ma.AssembleValue().AssignString(*c.Message) 39 } 40 ma.Finish() 41 return nb.Build(), nil 42 } 43 44 var debugEchoTyp = helpers.Must(ipld.LoadSchemaBytes([]byte(` 45 type DebugEchoCaveats struct { 46 message optional String 47 } 48 `))) 49 50 var debugEcho = NewCapability( 51 "debug/echo", 52 schema.DIDString(schema.WithMethod("mailto")), 53 schema.Struct[debugEchoCaveats](debugEchoTyp.TypeByName("DebugEchoCaveats"), nil), 54 func(claimed, delegated ucan.Capability[debugEchoCaveats]) failure.Failure { 55 if claimed.With() != delegated.With() { 56 err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) 57 return failure.FromError(err) 58 } 59 return nil 60 }, 61 ) 62 63 type attestCaveats struct { 64 Proof ipld.Link 65 } 66 67 func (c attestCaveats) ToIPLD() (ipld.Node, error) { 68 np := basicnode.Prototype.Any 69 nb := np.NewBuilder() 70 ma, _ := nb.BeginMap(1) 71 ma.AssembleKey().AssignString("proof") 72 ma.AssembleValue().AssignLink(c.Proof) 73 ma.Finish() 74 return nb.Build(), nil 75 } 76 77 var attestTyp = helpers.Must(ipld.LoadSchemaBytes([]byte(` 78 type AttestCaveats struct { 79 proof Link 80 } 81 `))) 82 83 var attest = NewCapability( 84 "ucan/attest", 85 schema.DIDString(), 86 schema.Struct[attestCaveats](attestTyp.TypeByName("AttestCaveats"), nil), 87 func(claimed, delegated ucan.Capability[attestCaveats]) failure.Failure { 88 if claimed.With() != delegated.With() { 89 err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) 90 return failure.FromError(err) 91 } 92 return nil 93 }, 94 ) 95 96 func TestSession(t *testing.T) { 97 t.Run("validate mailto", func(t *testing.T) { 98 agent := fixtures.Alice 99 account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) 100 101 prf, err := debugEcho.Delegate( 102 account, 103 agent, 104 account.DID().String(), 105 debugEchoCaveats{}, 106 ) 107 require.NoError(t, err) 108 109 session, err := attest.Delegate( 110 service, 111 agent, 112 service.DID().String(), 113 attestCaveats{Proof: prf.Link()}, 114 ) 115 require.NoError(t, err) 116 117 msg := "Hello World" 118 nb := debugEchoCaveats{Message: &msg} 119 inv, err := debugEcho.Invoke( 120 agent, 121 service, 122 account.DID().String(), 123 nb, 124 delegation.WithProof( 125 delegation.FromDelegation(prf), 126 delegation.FromDelegation(session), 127 ), 128 ) 129 require.NoError(t, err) 130 131 vctx := NewValidationContext( 132 service.Verifier(), 133 debugEcho, 134 IsSelfIssued, 135 validateAuthOk, 136 ProofUnavailable, 137 parseEdPrincipal, 138 FailDIDKeyResolution, 139 NotExpiredNotTooEarly, 140 ) 141 142 a, x := Access(t.Context(), inv, vctx) 143 require.NoError(t, x) 144 require.Equal(t, debugEcho.Can(), a.Capability().Can()) 145 require.Equal(t, account.DID().String(), a.Capability().With()) 146 require.Equal(t, nb, a.Capability().Nb()) 147 }) 148 149 t.Run("validate mailto attested by another service", func(t *testing.T) { 150 agent := fixtures.Alice 151 account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) 152 othersvc := helpers.Must(ed25519.Generate()) 153 othersvc = helpers.Must(signer.Wrap(othersvc, helpers.Must(did.Parse("did:web:other.storage")))) 154 155 prf, err := debugEcho.Delegate( 156 account, 157 agent, 158 account.DID().String(), 159 debugEchoCaveats{}, 160 delegation.WithNoExpiration(), 161 ) 162 require.NoError(t, err) 163 164 session, err := attest.Delegate( 165 othersvc, 166 agent, 167 othersvc.DID().String(), 168 attestCaveats{Proof: prf.Link()}, 169 delegation.WithNoExpiration(), 170 ) 171 require.NoError(t, err) 172 173 msg := "Hello World" 174 inv, err := debugEcho.Invoke( 175 agent, 176 service, 177 account.DID().String(), 178 debugEchoCaveats{Message: &msg}, 179 delegation.WithProof( 180 delegation.FromDelegation(prf), 181 delegation.FromDelegation(session), 182 ), 183 delegation.WithNoExpiration(), 184 ) 185 require.NoError(t, err) 186 187 auth, err := attest.Delegate( 188 service, 189 othersvc, 190 service.DID().String(), 191 attestCaveats{Proof: session.Link()}, 192 delegation.WithNoExpiration(), 193 ) 194 require.NoError(t, err) 195 196 vctx := NewValidationContext( 197 service.Verifier(), 198 debugEcho, 199 IsSelfIssued, 200 validateAuthOk, 201 ProofUnavailable, 202 parseEdPrincipal, 203 func(ctx context.Context, d did.DID) (did.DID, UnresolvedDID) { 204 if d == othersvc.DID() { 205 return othersvc.(signer.WrappedSigner).Unwrap().DID(), nil 206 } 207 208 return FailDIDKeyResolution(ctx, d) 209 }, 210 NotExpiredNotTooEarly, 211 auth, 212 ) 213 214 a, x := Access(t.Context(), inv, vctx) 215 require.NoError(t, x) 216 require.Equal(t, debugEcho.Can(), a.Capability().Can()) 217 require.Equal(t, account.DID().String(), a.Capability().With()) 218 require.Equal(t, debugEchoCaveats{Message: &msg}, a.Capability().Nb()) 219 }) 220 221 t.Run("delegated ucan attest", func(t *testing.T) { 222 agent := fixtures.Alice 223 account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) 224 225 manager, err := ed25519.Generate() 226 require.NoError(t, err) 227 worker, err := ed25519.Generate() 228 require.NoError(t, err) 229 230 authority, err := delegation.Delegate( 231 manager, 232 worker, 233 []ucan.Capability[ucan.NoCaveats]{ 234 ucan.NewCapability("*", service.DID().String(), ucan.NoCaveats{}), 235 }, 236 delegation.WithNoExpiration(), 237 delegation.WithProof( 238 delegation.FromDelegation( 239 helpers.Must( 240 delegation.Delegate( 241 service, 242 manager, 243 []ucan.Capability[ucan.NoCaveats]{ 244 ucan.NewCapability("*", service.DID().String(), ucan.NoCaveats{}), 245 }, 246 ), 247 ), 248 ), 249 ), 250 ) 251 require.NoError(t, err) 252 253 prf, err := debugEcho.Delegate( 254 account, 255 agent, 256 account.DID().String(), 257 debugEchoCaveats{}, 258 delegation.WithNoExpiration(), 259 ) 260 require.NoError(t, err) 261 262 require.Equal( 263 t, 264 helpers.Must(base64.RawStdEncoding.DecodeString("gKADAA")), 265 prf.Signature().Bytes(), 266 "should have blank signature", 267 ) 268 269 session, err := attest.Delegate( 270 worker, 271 agent, 272 service.DID().String(), 273 attestCaveats{Proof: prf.Link()}, 274 delegation.WithProof(delegation.FromDelegation(authority)), 275 ) 276 require.NoError(t, err) 277 278 msg := "Hello World" 279 nb := debugEchoCaveats{Message: &msg} 280 inv, err := debugEcho.Invoke( 281 agent, 282 service, 283 account.DID().String(), 284 nb, 285 delegation.WithProof( 286 delegation.FromDelegation(session), 287 delegation.FromDelegation(prf), 288 ), 289 ) 290 require.NoError(t, err) 291 292 vctx := NewValidationContext( 293 service.Verifier(), 294 debugEcho, 295 IsSelfIssued, 296 validateAuthOk, 297 ProofUnavailable, 298 parseEdPrincipal, 299 FailDIDKeyResolution, 300 NotExpiredNotTooEarly, 301 ) 302 303 a, x := Access(t.Context(), inv, vctx) 304 require.NoError(t, x) 305 require.Equal(t, debugEcho.Can(), a.Capability().Can()) 306 require.Equal(t, account.DID().String(), a.Capability().With()) 307 require.Equal(t, nb, a.Capability().Nb()) 308 }) 309 310 t.Run("fail without proofs", func(t *testing.T) { 311 account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) 312 313 msg := "Hello World" 314 nb := debugEchoCaveats{Message: &msg} 315 inv, err := debugEcho.Invoke( 316 account, 317 service, 318 account.DID().String(), 319 nb, 320 ) 321 require.NoError(t, err) 322 323 vctx := NewValidationContext( 324 service.Verifier(), 325 debugEcho, 326 IsSelfIssued, 327 validateAuthOk, 328 ProofUnavailable, 329 parseEdPrincipal, 330 FailDIDKeyResolution, 331 NotExpiredNotTooEarly, 332 ) 333 334 a, x := Access(t.Context(), inv, vctx) 335 require.Nil(t, a) 336 require.Error(t, x) 337 require.Equal(t, x.Name(), "Unauthorized") 338 errmsg := strings.Join([]string{ 339 fmt.Sprintf("Claim %s is not authorized", debugEcho), 340 fmt.Sprintf(` - Unable to resolve '%s' key`, account.DID()), 341 }, "\n") 342 require.Equal(t, errmsg, x.Error()) 343 }) 344 345 t.Run("fail without session", func(t *testing.T) { 346 account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) 347 agent := fixtures.Alice 348 349 prf, err := debugEcho.Delegate( 350 account, 351 agent, 352 account.DID().String(), 353 debugEchoCaveats{}, 354 delegation.WithNoExpiration(), 355 ) 356 require.NoError(t, err) 357 358 msg := "Hello World" 359 nb := debugEchoCaveats{Message: &msg} 360 inv, err := debugEcho.Invoke( 361 account, 362 service, 363 account.DID().String(), 364 nb, 365 delegation.WithProof(delegation.FromDelegation(prf)), 366 ) 367 require.NoError(t, err) 368 369 vctx := NewValidationContext( 370 service.Verifier(), 371 debugEcho, 372 IsSelfIssued, 373 validateAuthOk, 374 ProofUnavailable, 375 parseEdPrincipal, 376 FailDIDKeyResolution, 377 NotExpiredNotTooEarly, 378 ) 379 380 a, x := Access(t.Context(), inv, vctx) 381 require.Nil(t, a) 382 require.Error(t, x) 383 require.Equal(t, x.Name(), "Unauthorized") 384 require.Contains(t, x.Error(), fmt.Sprintf(`Unable to resolve '%s'`, account.DID())) 385 }) 386 387 t.Run("fail invalid ucan/attest proof", func(t *testing.T) { 388 account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) 389 agent := fixtures.Alice 390 othersvc := helpers.Must(ed25519.Generate()) 391 392 prf, err := debugEcho.Delegate( 393 account, 394 agent, 395 account.DID().String(), 396 debugEchoCaveats{}, 397 delegation.WithNoExpiration(), 398 ) 399 require.NoError(t, err) 400 401 session, err := attest.Delegate( 402 othersvc, 403 agent, 404 service.DID().String(), 405 attestCaveats{Proof: prf.Link()}, 406 delegation.WithProof( 407 delegation.FromDelegation( 408 helpers.Must( 409 delegation.Delegate( 410 service, 411 othersvc, 412 []ucan.Capability[ucan.NoCaveats]{ 413 // Noting that this is a DID key, not did:web:example.com 414 // which is why session is invalid 415 ucan.NewCapability("*", service.Unwrap().DID().String(), ucan.NoCaveats{}), 416 }, 417 ), 418 ), 419 ), 420 ), 421 ) 422 require.NoError(t, err) 423 424 msg := "Hello World" 425 nb := debugEchoCaveats{Message: &msg} 426 inv, err := debugEcho.Invoke( 427 agent, 428 service, 429 account.DID().String(), 430 nb, 431 delegation.WithProof( 432 delegation.FromDelegation(prf), 433 delegation.FromDelegation(session), 434 ), 435 ) 436 require.NoError(t, err) 437 438 vctx := NewValidationContext( 439 service.Verifier(), 440 debugEcho, 441 IsSelfIssued, 442 validateAuthOk, 443 ProofUnavailable, 444 parseEdPrincipal, 445 FailDIDKeyResolution, 446 NotExpiredNotTooEarly, 447 ) 448 449 a, x := Access(t.Context(), inv, vctx) 450 require.Nil(t, a) 451 require.Error(t, x) 452 require.Equal(t, x.Name(), "Unauthorized") 453 require.Contains(t, x.Error(), "has an invalid session") 454 }) 455 456 t.Run("fail unknown ucan/attest proof", func(t *testing.T) { 457 account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) 458 agent := fixtures.Alice 459 othersvc := helpers.Must(ed25519.Generate()) 460 othersvc = helpers.Must(signer.Wrap(othersvc, helpers.Must(did.Parse("did:web:other.storage")))) 461 462 prf, err := debugEcho.Delegate( 463 account, 464 agent, 465 account.DID().String(), 466 debugEchoCaveats{}, 467 delegation.WithNoExpiration(), 468 ) 469 require.NoError(t, err) 470 471 session, err := attest.Delegate( 472 othersvc, 473 agent, 474 othersvc.DID().String(), 475 attestCaveats{Proof: prf.Link()}, 476 delegation.WithNoExpiration(), 477 ) 478 require.NoError(t, err) 479 480 msg := "Hello World" 481 inv, err := debugEcho.Invoke( 482 agent, 483 service, 484 account.DID().String(), 485 debugEchoCaveats{Message: &msg}, 486 delegation.WithProof( 487 delegation.FromDelegation(prf), 488 delegation.FromDelegation(session), 489 ), 490 delegation.WithNoExpiration(), 491 ) 492 require.NoError(t, err) 493 494 // authority is service, but attestation was issued by othersvc 495 vctx := NewValidationContext( 496 service.Verifier(), 497 debugEcho, 498 IsSelfIssued, 499 validateAuthOk, 500 ProofUnavailable, 501 parseEdPrincipal, 502 func(ctx context.Context, d did.DID) (did.DID, UnresolvedDID) { 503 if d == othersvc.DID() { 504 return othersvc.(signer.WrappedSigner).Unwrap().DID(), nil 505 } 506 507 return FailDIDKeyResolution(ctx, d) 508 }, 509 NotExpiredNotTooEarly, 510 ) 511 512 a, x := Access(t.Context(), inv, vctx) 513 require.Nil(t, a) 514 require.Error(t, x) 515 require.Equal(t, x.Name(), "Unauthorized") 516 require.Contains(t, x.Error(), "Unable to resolve 'did:mailto:web.mail:alice'") 517 }) 518 519 t.Run("resolve key", func(t *testing.T) { 520 accountDID := helpers.Must(did.Parse("did:mailto:web.mail:alice")) 521 account := helpers.Must(signer.Wrap(fixtures.Alice, accountDID)) 522 523 msg := "Hello World" 524 nb := debugEchoCaveats{Message: &msg} 525 inv, err := debugEcho.Invoke( 526 account, 527 service, 528 account.DID().String(), 529 nb, 530 ) 531 require.NoError(t, err) 532 533 vctx := NewValidationContext( 534 service.Verifier(), 535 debugEcho, 536 IsSelfIssued, 537 validateAuthOk, 538 ProofUnavailable, 539 parseEdPrincipal, 540 func(ctx context.Context, d did.DID) (did.DID, UnresolvedDID) { 541 return fixtures.Alice.DID(), nil 542 }, 543 NotExpiredNotTooEarly, 544 ) 545 546 a, x := Access(t.Context(), inv, vctx) 547 require.NoError(t, x) 548 require.Equal(t, debugEcho.Can(), a.Capability().Can()) 549 require.Equal(t, account.DID().String(), a.Capability().With()) 550 require.Equal(t, nb, a.Capability().Nb()) 551 }) 552 553 t.Run("service can not delegate access to account", func(t *testing.T) { 554 accountDID := helpers.Must(did.Parse("did:mailto:web.mail:alice")) 555 account := absentee.From(accountDID) 556 557 // service should not be able to delegate access to account resource 558 auth, err := debugEcho.Delegate( 559 service, 560 fixtures.Alice, 561 account.DID().String(), 562 debugEchoCaveats{}, 563 ) 564 require.NoError(t, err) 565 566 session, err := attest.Delegate( 567 service, 568 fixtures.Alice, 569 service.DID().String(), 570 attestCaveats{Proof: auth.Link()}, 571 delegation.WithProof(delegation.FromDelegation(auth)), 572 ) 573 require.NoError(t, err) 574 575 msg := "Hello World" 576 nb := debugEchoCaveats{Message: &msg} 577 inv, err := debugEcho.Invoke( 578 fixtures.Alice, 579 service, 580 account.DID().String(), 581 nb, 582 delegation.WithProof( 583 delegation.FromDelegation(auth), 584 delegation.FromDelegation(session), 585 ), 586 ) 587 require.NoError(t, err) 588 589 vctx := NewValidationContext( 590 service.Verifier(), 591 debugEcho, 592 IsSelfIssued, 593 validateAuthOk, 594 ProofUnavailable, 595 parseEdPrincipal, 596 FailDIDKeyResolution, 597 NotExpiredNotTooEarly, 598 ) 599 600 a, x := Access(t.Context(), inv, vctx) 601 require.Nil(t, a) 602 require.Error(t, x) 603 }) 604 605 t.Run("attest with an account DID", func(t *testing.T) { 606 accountDID := helpers.Must(did.Parse("did:mailto:web.mail:alice")) 607 account := absentee.From(accountDID) 608 609 // service should not be able to delegate access to account resource 610 auth, err := debugEcho.Delegate( 611 service, 612 fixtures.Alice, 613 account.DID().String(), 614 debugEchoCaveats{}, 615 ) 616 require.NoError(t, err) 617 618 session, err := attest.Delegate( 619 service, 620 fixtures.Alice, 621 // this should be an service did instead 622 account.DID().String(), 623 attestCaveats{Proof: auth.Link()}, 624 delegation.WithProof(delegation.FromDelegation(auth)), 625 ) 626 require.NoError(t, err) 627 628 msg := "Hello World" 629 nb := debugEchoCaveats{Message: &msg} 630 inv, err := debugEcho.Invoke( 631 fixtures.Alice, 632 service, 633 account.DID().String(), 634 nb, 635 delegation.WithProof( 636 delegation.FromDelegation(auth), 637 delegation.FromDelegation(session), 638 ), 639 ) 640 require.NoError(t, err) 641 642 vctx := NewValidationContext( 643 service.Verifier(), 644 debugEcho, 645 IsSelfIssued, 646 validateAuthOk, 647 ProofUnavailable, 648 parseEdPrincipal, 649 FailDIDKeyResolution, 650 NotExpiredNotTooEarly, 651 ) 652 653 a, x := Access(t.Context(), inv, vctx) 654 require.Nil(t, a) 655 require.Error(t, x) 656 }) 657 658 t.Run("service cannot delegate account resource", func(t *testing.T) { 659 accountDID := helpers.Must(did.Parse("did:mailto:web.mail:alice")) 660 account := absentee.From(accountDID) 661 662 prf, err := debugEcho.Delegate( 663 service, 664 fixtures.Alice, 665 account.DID().String(), 666 debugEchoCaveats{}, 667 ) 668 require.NoError(t, err) 669 670 msg := "Hello World" 671 nb := debugEchoCaveats{Message: &msg} 672 inv, err := debugEcho.Invoke( 673 fixtures.Alice, 674 service, 675 account.DID().String(), 676 nb, 677 delegation.WithProof(delegation.FromDelegation(prf)), 678 ) 679 require.NoError(t, err) 680 681 vctx := NewValidationContext( 682 service.Verifier(), 683 debugEcho, 684 IsSelfIssued, 685 validateAuthOk, 686 ProofUnavailable, 687 parseEdPrincipal, 688 FailDIDKeyResolution, 689 NotExpiredNotTooEarly, 690 ) 691 692 a, x := Access(t.Context(), inv, vctx) 693 require.Nil(t, a) 694 require.Error(t, x) 695 }) 696 697 t.Run("redundant proofs have no impact", func(t *testing.T) { 698 accountDID := helpers.Must(did.Parse("did:mailto:web.mail:alice")) 699 account := absentee.From(accountDID) 700 701 var logins delegation.Proofs 702 for i := range 6 { 703 dlg, err := delegation.Delegate( 704 account, 705 fixtures.Alice, 706 []ucan.Capability[ucan.NoCaveats]{ 707 ucan.NewCapability("*", "ucan:*", ucan.NoCaveats{}), 708 }, 709 delegation.WithNoExpiration(), 710 delegation.WithNonce(fmt.Sprint(i)), 711 ) 712 require.NoError(t, err) 713 logins = append(logins, delegation.FromDelegation(dlg)) 714 } 715 716 exp := ucan.Now() + 60*60*24*365 // 1 year 717 var attestations delegation.Proofs 718 for _, login := range logins { 719 dlg, err := attest.Delegate( 720 service, 721 fixtures.Alice, 722 service.DID().String(), 723 attestCaveats{Proof: login.Link()}, 724 delegation.WithExpiration(exp), 725 ) 726 require.NoError(t, err) 727 attestations = append(attestations, delegation.FromDelegation(dlg)) 728 } 729 730 var prfs delegation.Proofs 731 prfs = append(prfs, logins...) 732 prfs = append(prfs, attestations...) 733 734 msg := "Hello World" 735 nb := debugEchoCaveats{Message: &msg} 736 inv, err := debugEcho.Invoke( 737 fixtures.Alice, 738 service, 739 account.DID().String(), 740 nb, 741 delegation.WithProof(prfs...), 742 ) 743 require.NoError(t, err) 744 745 vctx := NewValidationContext( 746 service.Verifier(), 747 debugEcho, 748 IsSelfIssued, 749 validateAuthOk, 750 ProofUnavailable, 751 parseEdPrincipal, 752 FailDIDKeyResolution, 753 NotExpiredNotTooEarly, 754 ) 755 756 a, x := Access(t.Context(), inv, vctx) 757 require.NotEmpty(t, a) 758 require.NoError(t, x) 759 }) 760 }