diff --git a/token/delegation/delegation.go b/token/delegation/delegation.go index 565cba0..f096e51 100644 --- a/token/delegation/delegation.go +++ b/token/delegation/delegation.go @@ -238,7 +238,7 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) { tkn.issuer, err = did.Parse(m.Iss) if err != nil { - return nil, fmt.Errorf("parse iss: %w", err) + return nil, fmt.Errorf("parse issuer: %w", err) } if tkn.audience, err = did.Parse(m.Aud); err != nil { diff --git a/token/delegation/delegationtest/data/TokenAliceAlice.dagcbor b/token/delegation/delegationtest/data/TokenAliceAlice.dagcbor new file mode 100644 index 0000000..172f12e Binary files /dev/null and b/token/delegation/delegationtest/data/TokenAliceAlice.dagcbor differ diff --git a/token/delegation/delegationtest/data/TokenBobBob.dagcbor b/token/delegation/delegationtest/data/TokenBobBob.dagcbor new file mode 100644 index 0000000..417afdb Binary files /dev/null and b/token/delegation/delegationtest/data/TokenBobBob.dagcbor differ diff --git a/token/delegation/delegationtest/data/TokenCarolCarol.dagcbor b/token/delegation/delegationtest/data/TokenCarolCarol.dagcbor new file mode 100644 index 0000000..5e9c7e1 Binary files /dev/null and b/token/delegation/delegationtest/data/TokenCarolCarol.dagcbor differ diff --git a/token/delegation/delegationtest/data/TokenDanDan.dagcbor b/token/delegation/delegationtest/data/TokenDanDan.dagcbor new file mode 100644 index 0000000..d9a2470 Binary files /dev/null and b/token/delegation/delegationtest/data/TokenDanDan.dagcbor differ diff --git a/token/delegation/delegationtest/data/TokenErinErin.dagcbor b/token/delegation/delegationtest/data/TokenErinErin.dagcbor new file mode 100644 index 0000000..72936bf Binary files /dev/null and b/token/delegation/delegationtest/data/TokenErinErin.dagcbor differ diff --git a/token/delegation/delegationtest/data/TokenFrankFrank.dagcbor b/token/delegation/delegationtest/data/TokenFrankFrank.dagcbor new file mode 100644 index 0000000..d669f93 Binary files /dev/null and b/token/delegation/delegationtest/data/TokenFrankFrank.dagcbor differ diff --git a/token/delegation/delegationtest/generator/generator.go b/token/delegation/delegationtest/generator/generator.go index b9bcb83..6396fd6 100644 --- a/token/delegation/delegationtest/generator/generator.go +++ b/token/delegation/delegationtest/generator/generator.go @@ -71,6 +71,25 @@ type generator struct { chains []proof } +func (g *generator) createSelfDelegations(personas []didtest.Persona) error { + for _, persona := range personas { + _, err := g.createDelegation(newDelegationParams{ + privKey: persona.PrivKey(), + aud: persona.DID(), + cmd: delegationtest.NominalCommand, + pol: policytest.EmptyPolicy, + sub: persona.DID(), + opts: []delegation.Option{ + delegation.WithNonce(constantNonce), + }, + }, persona.Name()+persona.Name(), noopVariant()) + if err != nil { + return err + } + } + return nil +} + func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari variant) error { acc.name += personas[0].Name() diff --git a/token/delegation/delegationtest/generator/main.go b/token/delegation/delegationtest/generator/main.go index 09618f4..3dae485 100644 --- a/token/delegation/delegationtest/generator/main.go +++ b/token/delegation/delegationtest/generator/main.go @@ -6,7 +6,11 @@ import ( func main() { gen := &generator{} - err := gen.chainPersonas(didtest.Personas(), acc{}, noopVariant()) + err := gen.createSelfDelegations(didtest.Personas()) + if err != nil { + panic(err) + } + err = gen.chainPersonas(didtest.Personas(), acc{}, noopVariant()) if err != nil { panic(err) } diff --git a/token/delegation/delegationtest/token_gen.go b/token/delegation/delegationtest/token_gen.go index 5c798fc..e5f964a 100644 --- a/token/delegation/delegationtest/token_gen.go +++ b/token/delegation/delegationtest/token_gen.go @@ -8,6 +8,48 @@ import ( "github.com/ucan-wg/go-ucan/token/delegation" ) +var ( + TokenAliceAliceCID = cid.MustParse("bafyreiddqsv5rrpcormtcs3dg7hzwjr2grxyyozc2f2surxdbnctdqpfzi") + TokenAliceAliceSealed = mustGetBundle(TokenAliceAliceCID).Sealed + TokenAliceAliceBundle = mustGetBundle(TokenAliceAliceCID) + TokenAliceAlice = mustGetBundle(TokenAliceAliceCID).Decoded +) + +var ( + TokenBobBobCID = cid.MustParse("bafyreid4dwdov4yijvnb7xxhcndsxifzw5yry4sm4frex6relttlnledo4") + TokenBobBobSealed = mustGetBundle(TokenBobBobCID).Sealed + TokenBobBobBundle = mustGetBundle(TokenBobBobCID) + TokenBobBob = mustGetBundle(TokenBobBobCID).Decoded +) + +var ( + TokenCarolCarolCID = cid.MustParse("bafyreiekuehdsubdfllqecsat4gsfveyqq6442ejuiqfsgu3tplrus5l3e") + TokenCarolCarolSealed = mustGetBundle(TokenCarolCarolCID).Sealed + TokenCarolCarolBundle = mustGetBundle(TokenCarolCarolCID) + TokenCarolCarol = mustGetBundle(TokenCarolCarolCID).Decoded +) + +var ( + TokenDanDanCID = cid.MustParse("bafyreigzd442yhyizbx54kd76ewxssh5owuxv26ziittnblnj4h3a555dm") + TokenDanDanSealed = mustGetBundle(TokenDanDanCID).Sealed + TokenDanDanBundle = mustGetBundle(TokenDanDanCID) + TokenDanDan = mustGetBundle(TokenDanDanCID).Decoded +) + +var ( + TokenErinErinCID = cid.MustParse("bafyreigl5lbogpzq7iyz6qkzhicv4zscu26j62k4ydgcqogdiqmks5tz7q") + TokenErinErinSealed = mustGetBundle(TokenErinErinCID).Sealed + TokenErinErinBundle = mustGetBundle(TokenErinErinCID) + TokenErinErin = mustGetBundle(TokenErinErinCID).Decoded +) + +var ( + TokenFrankFrankCID = cid.MustParse("bafyreic6hgmqf2vwszboldlqeobpy2plpkcmj4dhhug76akcnafb2pt6em") + TokenFrankFrankSealed = mustGetBundle(TokenFrankFrankCID).Sealed + TokenFrankFrankBundle = mustGetBundle(TokenFrankFrankCID) + TokenFrankFrank = mustGetBundle(TokenFrankFrankCID).Decoded +) + var ( TokenAliceBobCID = cid.MustParse("bafyreifa35rjstdm37cjudzs72ab22rnh5blny725khtapox63fnsj6pbe") TokenAliceBobSealed = mustGetBundle(TokenAliceBobCID).Sealed @@ -170,6 +212,12 @@ var ( ) var AllTokens = []*delegation.Token{ + TokenAliceAlice, + TokenBobBob, + TokenCarolCarol, + TokenDanDan, + TokenErinErin, + TokenFrankFrank, TokenAliceBob, TokenBobCarol, TokenCarolDan, @@ -196,6 +244,12 @@ var AllTokens = []*delegation.Token{ } var AllBundles = []delegation.Bundle{ + TokenAliceAliceBundle, + TokenBobBobBundle, + TokenCarolCarolBundle, + TokenDanDanBundle, + TokenErinErinBundle, + TokenFrankFrankBundle, TokenAliceBobBundle, TokenBobCarolBundle, TokenCarolDanBundle, @@ -222,6 +276,12 @@ var AllBundles = []delegation.Bundle{ } var cidToName = map[cid.Cid]string{ + TokenAliceAliceCID: "TokenAliceAlice", + TokenBobBobCID: "TokenBobBob", + TokenCarolCarolCID: "TokenCarolCarol", + TokenDanDanCID: "TokenDanDan", + TokenErinErinCID: "TokenErinErin", + TokenFrankFrankCID: "TokenFrankFrank", TokenAliceBobCID: "TokenAliceBob", TokenBobCarolCID: "TokenBobCarol", TokenCarolDanCID: "TokenCarolDan", diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 6696214..d0a28ce 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -109,10 +109,23 @@ func New(iss did.DID, cmd command.Command, sub did.DID, prf []cid.Cid, opts ...O return &tkn, nil } +// NewSelfSigned is similar to New, but self-signs the invocation, and therefore does not require a proof. +// It's similar to having an invocation with a delegation from the invoker to itself. +// This can be useful in some protocols where the invoker is the same as the subject, or to prove ownership of a resource. +// +// You can read it as "(Issuer - I) executes (command) on itself". +func NewSelfSigned(iss did.DID, cmd command.Command, opts ...Option) (*Token, error) { + return New(iss, cmd, iss, nil, opts...) +} + +// ExecutionAllowed verifies that the invocation respects the rules and can be executed. +// IMPORTANT: this function does NOT verify that the subject (and audience if set) makes sense in your context. func (t *Token) ExecutionAllowed(loader delegation.Loader) error { return t.executionAllowed(loader, t.arguments) } +// ExecutionAllowedWithArgsHook is the same as ExecutionAllowed, but allows to modify the arguments before verifying them. +// IMPORTANT: this function does NOT verify that the subject (and audience if set) makes sense in your context. func (t *Token) ExecutionAllowedWithArgsHook(loader delegation.Loader, hook func(args args.ReadOnly) (*args.Args, error)) error { newArgs, err := hook(t.arguments.ReadOnly()) if err != nil { @@ -204,6 +217,11 @@ func (t *Token) Cause() *cid.Cid { return t.cause } +// IsSelfSigned returns true if the token is self-signed, ie it has the same issuer and subject. +func (t *Token) IsSelfSigned() bool { + return t.issuer.Equal(t.subject) +} + // IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields. // This does NOT do any other kind of verifications. func (t *Token) IsValidNow() bool { @@ -276,7 +294,7 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) { ) if tkn.issuer, err = did.Parse(m.Iss); err != nil { - return nil, fmt.Errorf("parse iss: %w", err) + return nil, fmt.Errorf("parse issuer: %w", err) } if tkn.subject, err = did.Parse(m.Sub); err != nil { diff --git a/token/invocation/invocation_test.go b/token/invocation/invocation_test.go index 72f798c..eb15889 100644 --- a/token/invocation/invocation_test.go +++ b/token/invocation/invocation_test.go @@ -3,6 +3,7 @@ package invocation_test import ( _ "embed" "testing" + "time" "github.com/MetaMask/go-did-it/didtest" "github.com/ipfs/go-cid" @@ -18,144 +19,255 @@ import ( //go:embed testdata/new.dagjson var newDagJson []byte -const ( - missingTknCIDStr = "bafyreigwypmw6eul6vadi6g6lnfbsfo2zck7gfzsbjoroqs3djhnzzc7mm" -) +//go:embed testdata/selfsigned.dagjson +var selfsignedDagJson []byte + +const missingTknCIDStr = "bafyreigwypmw6eul6vadi6g6lnfbsfo2zck7gfzsbjoroqs3djhnzzc7mm" var emptyArguments = args.New() func TestToken_ExecutionAllowed(t *testing.T) { - t.Parallel() + for _, tc := range []struct { + name string + issuer didtest.Persona + cmd command.Command + args *args.Args + proofs []cid.Cid + opts []invocation.Option + err error + }{ + // Passes + { + name: "passes - only root", + issuer: didtest.PersonaBob, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: delegationtest.ProofAliceBob, + err: nil, + }, + { + name: "passes - valid chain", + issuer: didtest.PersonaFrank, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank, + err: nil, + }, + { + name: "passes - proof chain attenuates command", + issuer: didtest.PersonaFrank, + cmd: delegationtest.AttenuatedCommand, + args: emptyArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand, + err: nil, + }, + { + name: "passes - invocation attenuates command", + issuer: didtest.PersonaFrank, + cmd: delegationtest.AttenuatedCommand, + args: emptyArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank, + err: nil, + }, + { + name: "passes - arguments satisfy empty policy", + issuer: didtest.PersonaFrank, + cmd: delegationtest.NominalCommand, + args: policytest.SpecValidArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank, + err: nil, + }, + { + name: "passes - arguments satisfy example policy", + issuer: didtest.PersonaFrank, + cmd: delegationtest.NominalCommand, + args: policytest.SpecValidArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy, + err: nil, + }, + { + name: "passes - self-signed invocation doesn't require proof", + issuer: didtest.PersonaAlice, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: nil, + err: nil, + }, + { + name: "passes - self-signed invocation accepts a delegation to itself", + issuer: didtest.PersonaAlice, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: []cid.Cid{delegationtest.TokenAliceAliceCID}, + err: nil, + }, - t.Run("passes - only root", func(t *testing.T) { - t.Parallel() + // Fails + { + name: "fails - no proof", + issuer: didtest.PersonaCarol, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: delegationtest.ProofEmpty, + err: invocation.ErrNoProof, + }, + { + name: "fails - missing referenced delegation", + issuer: didtest.PersonaCarol, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: []cid.Cid{cid.MustParse(missingTknCIDStr), delegationtest.TokenAliceBobCID}, + err: invocation.ErrMissingDelegation, + }, + { + name: "fails - referenced delegation expired", + issuer: didtest.PersonaFrank, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpired, + err: invocation.ErrTokenInvalidNow, + }, + { + name: "fails - referenced delegation inactive", + issuer: didtest.PersonaFrank, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank_InvalidInactive, + err: invocation.ErrTokenInvalidNow, + }, + { + name: "fails - last (or only) delegation not root", + issuer: didtest.PersonaFrank, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: []cid.Cid{delegationtest.TokenErinFrankCID, delegationtest.TokenDanErinCID, delegationtest.TokenCarolDanCID}, + err: invocation.ErrLastNotRoot, + }, + { + name: "fails - broken chain", + issuer: didtest.PersonaFrank, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: []cid.Cid{delegationtest.TokenCarolDanCID, delegationtest.TokenAliceBobCID}, + err: invocation.ErrBrokenChain, + }, + { + name: "fails - first not issued to invoker", + issuer: didtest.PersonaFrank, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: []cid.Cid{delegationtest.TokenBobCarolCID, delegationtest.TokenAliceBobCID}, + err: invocation.ErrBrokenChain, + }, + { + name: "fails - proof chain expands command", + issuer: didtest.PersonaFrank, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand, + err: invocation.ErrCommandNotCovered, + }, + { + name: "fails - invocation expands command", + issuer: didtest.PersonaFrank, + cmd: delegationtest.ExpandedCommand, + args: emptyArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank, + err: invocation.ErrCommandNotCovered, + }, + { + name: "fails - inconsistent subject", + issuer: didtest.PersonaFrank, + cmd: delegationtest.ExpandedCommand, + args: emptyArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank_InvalidSubject, + err: invocation.ErrWrongSub, + }, + { + name: "fails - arguments don't satisfy example policy", + issuer: didtest.PersonaFrank, + cmd: delegationtest.NominalCommand, + args: policytest.SpecInvalidArguments, + proofs: delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy, + err: invocation.ErrPolicyNotSatisfied, + }, + { + name: "fails - self-signed invocation refuses a delegation to itself for a different DID", + issuer: didtest.PersonaAlice, + cmd: delegationtest.NominalCommand, + args: emptyArguments, + proofs: []cid.Cid{delegationtest.TokenBobBobCID}, + err: invocation.ErrBrokenChain, + }, + } { + t.Run(tc.name, func(t *testing.T) { + tc.opts = append(tc.opts, invocation.WithArguments(tc.args)) - testPasses(t, didtest.PersonaBob, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBob) - }) + tkn, err := invocation.New(tc.issuer.DID(), tc.cmd, didtest.PersonaAlice.DID(), tc.proofs, tc.opts...) + require.NoError(t, err) - t.Run("passes - valid chain", func(t *testing.T) { - t.Parallel() + err = tkn.ExecutionAllowed(delegationtest.GetDelegationLoader()) - testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank) - }) + if tc.err != nil { + require.ErrorIs(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} - t.Run("passes - proof chain attenuates command", func(t *testing.T) { - t.Parallel() +const ( + nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0" + subjectCmd = "/foo/bar" +) - testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand) - }) +func TestConstructors(t *testing.T) { + cmd, err := command.Parse(subjectCmd) + require.NoError(t, err) - t.Run("passes - invocation attenuates command", func(t *testing.T) { - t.Parallel() + iat, err := time.Parse(time.RFC3339, "2100-01-01T00:00:00Z") + require.NoError(t, err) - testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank) - }) + exp, err := time.Parse(time.RFC3339, "2200-01-01T00:00:00Z") + require.NoError(t, err) - t.Run("passes - arguments satisfy empty policy", func(t *testing.T) { - t.Parallel() - - testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank) - }) - - t.Run("passes - arguments satify example policy", func(t *testing.T) { - t.Parallel() - - testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy) - }) - - t.Run("fails - no proof", func(t *testing.T) { - t.Parallel() - - testFails(t, invocation.ErrNoProof, didtest.PersonaCarol, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofEmpty) - }) - - t.Run("fails - missing referenced delegation", func(t *testing.T) { - t.Parallel() - - missingTknCID, err := cid.Parse(missingTknCIDStr) + t.Run("New", func(t *testing.T) { + tkn, err := invocation.New( + didtest.PersonaAlice.DID(), cmd, didtest.PersonaBob.DID(), + delegationtest.ProofAliceBob, + invocation.WithNonce([]byte(nonce)), + invocation.WithIssuedAt(iat), + invocation.WithExpiration(exp), + invocation.WithArgument("foo", "bar"), + invocation.WithMeta("baz", 123), + ) require.NoError(t, err) - prf := []cid.Cid{missingTknCID, delegationtest.TokenAliceBobCID} - testFails(t, invocation.ErrMissingDelegation, didtest.PersonaCarol, delegationtest.NominalCommand, emptyArguments, prf) + require.False(t, tkn.IsSelfSigned()) + + data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey()) + require.NoError(t, err) + + require.JSONEq(t, string(newDagJson), string(data)) }) - t.Run("fails - referenced delegation expired", func(t *testing.T) { - t.Parallel() + t.Run("Self-Signed", func(t *testing.T) { + tkn, err := invocation.NewSelfSigned( + didtest.PersonaAlice.DID(), cmd, + invocation.WithNonce([]byte(nonce)), + invocation.WithIssuedAt(iat), + invocation.WithExpiration(exp), + invocation.WithArgument("foo", "bar"), + invocation.WithMeta("baz", 123), + ) + require.NoError(t, err) - testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpired) + require.True(t, tkn.IsSelfSigned()) + data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey()) + require.NoError(t, err) + + require.JSONEq(t, string(selfsignedDagJson), string(data)) }) - - t.Run("fails - referenced delegation inactive", func(t *testing.T) { - t.Parallel() - - testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidInactive) - }) - - t.Run("fails - last (or only) delegation not root", func(t *testing.T) { - t.Parallel() - - prf := []cid.Cid{delegationtest.TokenErinFrankCID, delegationtest.TokenDanErinCID, delegationtest.TokenCarolDanCID} - testFails(t, invocation.ErrLastNotRoot, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, prf) - }) - - t.Run("fails - broken chain", func(t *testing.T) { - t.Parallel() - - prf := []cid.Cid{delegationtest.TokenCarolDanCID, delegationtest.TokenAliceBobCID} - testFails(t, invocation.ErrBrokenChain, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, prf) - }) - - t.Run("fails - first not issued to invoker", func(t *testing.T) { - t.Parallel() - - prf := []cid.Cid{delegationtest.TokenBobCarolCID, delegationtest.TokenAliceBobCID} - testFails(t, invocation.ErrBrokenChain, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, prf) - }) - - t.Run("fails - proof chain expands command", func(t *testing.T) { - t.Parallel() - - testFails(t, invocation.ErrCommandNotCovered, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand) - }) - - t.Run("fails - invocation expands command", func(t *testing.T) { - t.Parallel() - - testFails(t, invocation.ErrCommandNotCovered, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank) - }) - - t.Run("fails - inconsistent subject", func(t *testing.T) { - t.Parallel() - - testFails(t, invocation.ErrWrongSub, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidSubject) - }) - - t.Run("passes - arguments satisfy example policy", func(t *testing.T) { - t.Parallel() - - testFails(t, invocation.ErrPolicyNotSatisfied, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecInvalidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy) - }) - -} - -func test(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) error { - t.Helper() - - opts = append(opts, invocation.WithArguments(args)) - - tkn, err := invocation.New(persona.DID(), cmd, didtest.PersonaAlice.DID(), prf, opts...) - require.NoError(t, err) - - return tkn.ExecutionAllowed(delegationtest.GetDelegationLoader()) -} - -func testFails(t *testing.T, expErr error, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) { - err := test(t, persona, cmd, args, prf, opts...) - require.ErrorIs(t, err, expErr) -} - -func testPasses(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) { - err := test(t, persona, cmd, args, prf, opts...) - require.NoError(t, err) } diff --git a/token/invocation/proof.go b/token/invocation/proof.go index d94bc6a..f9e4884 100644 --- a/token/invocation/proof.go +++ b/token/invocation/proof.go @@ -18,11 +18,11 @@ import ( // 1. When a token is read/unsealed from its containing envelope (`envelope` package): // a. The envelope can be decoded. // b. The envelope contains a Signature, VarsigHeader and Payload. -// c. The Payload contains an iss field that contains a valid `did:key`. -// d. The public key can be extracted from the `did:key`. -// e. The public key type is supported by go-ucan. +// c. The Payload contains an iss field that contains a valid DID. +// d. One or more public keys can be derived from the DID. +// e. One or more public keys are supported by go-ucan. // f. The Signature can be decoded per the VarsigHeader. -// g. The SigPayload can be verified using the Signature and public key. +// g. The SigPayload can be verified using the Signature and one public key. // h. The field key of the TokenPayload matches the expected tag. // // 2. When the token is created or passes step one (token constructor or decoder): @@ -35,7 +35,7 @@ import ( // c. All the delegation must be active (nbf in the past or absent). // // 4. When the proof chain is being validated (verifyProofs below): -// a. There must be at least one delegation in the proof chain. +// a. Self-signed invocations (issuer == subject) are allowed and don't require further proof. Otherwise, proof is required. // b. All referenced delegations must be available. // c. The first proof must be issued to the Invoker (audience DID). // d. The Issuer of each delegation must be the Audience in the next one. @@ -51,8 +51,11 @@ import ( // - principal alignment // - command alignment func (t *Token) verifyProofs(delegations []*delegation.Token) error { - // There must be at least one delegation referenced - 4a - if len(delegations) < 1 { + // Self-signed invocations (issuer == subject) are allowed and don't require further proof. Otherwise, proof is required. - 4a + if len(delegations) == 0 && t.issuer.Equal(t.subject) { + return nil + } + if len(delegations) == 0 { return ErrNoProof } diff --git a/token/invocation/schema_test.go b/token/invocation/schema_test.go index 5a8c271..e94a02e 100644 --- a/token/invocation/schema_test.go +++ b/token/invocation/schema_test.go @@ -2,6 +2,7 @@ package invocation_test import ( "bytes" + _ "embed" "encoding/base64" "testing" @@ -14,9 +15,12 @@ import ( "github.com/ucan-wg/go-ucan/token/invocation" ) +//go:embed testdata/full_example.dagjson +var fullExampleDagJson []byte + const ( issuerPrivKeyCfg = "BeAgktAj8irGgWjp4PGk/fV67e5CcML/KRmmHSldco3etP5lRiuYQ+VVO/39ol3XXruJC8deSuBxoEXzgdYpYw==" - newCID = "zdpuB1NjhETofEUp5iYzoHjSc2KKgZvSoT6FBaLMoVzzsxiR1" + fullExampleCID = "zdpuB1NjhETofEUp5iYzoHjSc2KKgZvSoT6FBaLMoVzzsxiR1" ) func TestSchemaRoundTrip(t *testing.T) { @@ -30,12 +34,12 @@ func TestSchemaRoundTrip(t *testing.T) { // format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson // function: DecodeDagJson() Seal() Unseal() EncodeDagJson() - p1, err := invocation.FromDagJson(newDagJson) + p1, err := invocation.FromDagJson(fullExampleDagJson) require.NoError(t, err) cborBytes, id, err := p1.ToSealed(privKey) require.NoError(t, err) - assert.Equal(t, newCID, envelope.CIDToBase58BTC(id)) + assert.Equal(t, fullExampleCID, envelope.CIDToBase58BTC(id)) p2, c2, err := invocation.FromSealed(cborBytes) require.NoError(t, err) @@ -44,13 +48,13 @@ func TestSchemaRoundTrip(t *testing.T) { readJson, err := p2.ToDagJson(privKey) require.NoError(t, err) - assert.JSONEq(t, string(newDagJson), string(readJson)) + assert.JSONEq(t, string(fullExampleDagJson), string(readJson)) }) t.Run("via streaming", func(t *testing.T) { t.Parallel() - buf := bytes.NewBuffer(newDagJson) + buf := bytes.NewBuffer(fullExampleDagJson) // format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson // function: DecodeDagJson() Seal() Unseal() EncodeDagJson() @@ -61,7 +65,7 @@ func TestSchemaRoundTrip(t *testing.T) { cborBytes := &bytes.Buffer{} id, err := p1.ToSealedWriter(cborBytes, privKey) require.NoError(t, err) - assert.Equal(t, newCID, envelope.CIDToBase58BTC(id)) + assert.Equal(t, fullExampleCID, envelope.CIDToBase58BTC(id)) p2, c2, err := invocation.FromSealedReader(cborBytes) require.NoError(t, err) @@ -70,7 +74,7 @@ func TestSchemaRoundTrip(t *testing.T) { readJson := &bytes.Buffer{} require.NoError(t, p2.ToDagJsonWriter(readJson, privKey)) - assert.JSONEq(t, string(newDagJson), readJson.String()) + assert.JSONEq(t, string(fullExampleDagJson), readJson.String()) }) } diff --git a/token/invocation/testdata/full_example.dagjson b/token/invocation/testdata/full_example.dagjson new file mode 100644 index 0000000..00b1b1f --- /dev/null +++ b/token/invocation/testdata/full_example.dagjson @@ -0,0 +1 @@ +[{"/":{"bytes":"tRKNRahqwdyR6OpytuGIdcYI7HxXvKI5I594zznCLbN2C6WP5f8FIfIQlo0Nnqg4xFgKjJGAbIEVqeCZdib1Dw"}},{"h":{"/":{"bytes":"NAHtAe0BE3E"}},"ucan/inv@1.0.0-rc.1":{"args":{"headers":{"Content-Type":"application/json"},"payload":{"body":"UCAN is great","draft":true,"title":"UCAN for Fun and Profit","topics":["authz","journal"]},"uri":"https://example.com/blog/posts"},"cmd":"/crud/create","exp":1753965668,"iss":"did:key:z6MkuScdGeTmbWubyoWWpPmX9wkwdZAshkTcLKb1bf4Cyj8N","meta":{"env":"development","tags":["blog","post","pr#123"]},"nonce":{"/":{"bytes":"BBR5znl7VpRof4ac"}},"prf":[{"/":"bafyreigx3qxd2cndpe66j2mdssj773ecv7tqd7wovcnz5raguw6lj7sjoe"},{"/":"bafyreib34ira254zdqgehz6f2bhwme2ja2re3ltcalejv4x4tkcveujvpa"},{"/":"bafyreibkb66tpo2ixqx3fe5hmekkbuasrod6olt5bwm5u5pi726mduuwlq"}],"sub":"did:key:z6MkuQU8kqxCAUeurotHyrnMgkMUBtJN8ozYxkwctnop4zzB"}}] \ No newline at end of file diff --git a/token/invocation/testdata/new.dagjson b/token/invocation/testdata/new.dagjson index 00b1b1f..04615ff 100644 --- a/token/invocation/testdata/new.dagjson +++ b/token/invocation/testdata/new.dagjson @@ -1 +1 @@ -[{"/":{"bytes":"tRKNRahqwdyR6OpytuGIdcYI7HxXvKI5I594zznCLbN2C6WP5f8FIfIQlo0Nnqg4xFgKjJGAbIEVqeCZdib1Dw"}},{"h":{"/":{"bytes":"NAHtAe0BE3E"}},"ucan/inv@1.0.0-rc.1":{"args":{"headers":{"Content-Type":"application/json"},"payload":{"body":"UCAN is great","draft":true,"title":"UCAN for Fun and Profit","topics":["authz","journal"]},"uri":"https://example.com/blog/posts"},"cmd":"/crud/create","exp":1753965668,"iss":"did:key:z6MkuScdGeTmbWubyoWWpPmX9wkwdZAshkTcLKb1bf4Cyj8N","meta":{"env":"development","tags":["blog","post","pr#123"]},"nonce":{"/":{"bytes":"BBR5znl7VpRof4ac"}},"prf":[{"/":"bafyreigx3qxd2cndpe66j2mdssj773ecv7tqd7wovcnz5raguw6lj7sjoe"},{"/":"bafyreib34ira254zdqgehz6f2bhwme2ja2re3ltcalejv4x4tkcveujvpa"},{"/":"bafyreibkb66tpo2ixqx3fe5hmekkbuasrod6olt5bwm5u5pi726mduuwlq"}],"sub":"did:key:z6MkuQU8kqxCAUeurotHyrnMgkMUBtJN8ozYxkwctnop4zzB"}}] \ No newline at end of file +[{"/":{"bytes":"8BxXBbXtPVoqn/z804w2w2gZH9m6kT55ivv7u2kxqptAfDcFzlRWBu3YKE9ijfIezpa79Btq5ja0PpqwjfSLAw"}},{"h":{"/":{"bytes":"NAHtAe0BE3E"}},"ucan/inv@1.0.0-rc.1":{"args":{"foo":"bar"},"cmd":"/foo/bar","exp":7258118400,"iat":4102444800,"iss":"did:key:z6MknUz1mSj4pvS6aUUHekCHdUPv7HBhDyDBZQ2W3Vujc5qC","meta":{"baz":123},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"prf":[{"/":"bafyreifa35rjstdm37cjudzs72ab22rnh5blny725khtapox63fnsj6pbe"}],"sub":"did:key:z6Mkf4WtCwPDtamsZvBJA4eSVcE7vZuRPy5Skm4HaoQv81i1"}}] \ No newline at end of file diff --git a/token/invocation/testdata/selfsigned.dagjson b/token/invocation/testdata/selfsigned.dagjson new file mode 100644 index 0000000..a220d21 --- /dev/null +++ b/token/invocation/testdata/selfsigned.dagjson @@ -0,0 +1 @@ +[{"/":{"bytes":"ejXoQIdp3OGXewEkfQF4Z4Vd8c3H0XF319dsNh5DEP/2l9Nt9H1IhMpks1+HXoYFOKN3QmtxpPMoYmf/rhKaAQ"}},{"h":{"/":{"bytes":"NAHtAe0BE3E"}},"ucan/inv@1.0.0-rc.1":{"args":{"foo":"bar"},"cmd":"/foo/bar","exp":7258118400,"iat":4102444800,"iss":"did:key:z6MknUz1mSj4pvS6aUUHekCHdUPv7HBhDyDBZQ2W3Vujc5qC","meta":{"baz":123},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"prf":[],"sub":"did:key:z6MknUz1mSj4pvS6aUUHekCHdUPv7HBhDyDBZQ2W3Vujc5qC"}}] \ No newline at end of file