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  }