invocation: add support for self-signed invocations (issuer=subject)
This commit is contained in:
@@ -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 {
|
||||
|
||||
BIN
token/delegation/delegationtest/data/TokenAliceAlice.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenAliceAlice.dagcbor
Normal file
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenBobBob.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenBobBob.dagcbor
Normal file
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenCarolCarol.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenCarolCarol.dagcbor
Normal file
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenDanDan.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenDanDan.dagcbor
Normal file
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenErinErin.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenErinErin.dagcbor
Normal file
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenFrankFrank.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenFrankFrank.dagcbor
Normal file
Binary file not shown.
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
t.Run("passes - valid chain", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
|
||||
})
|
||||
|
||||
t.Run("passes - proof chain attenuates command", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand)
|
||||
})
|
||||
|
||||
t.Run("passes - invocation attenuates command", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
|
||||
})
|
||||
|
||||
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)
|
||||
tkn, err := invocation.New(tc.issuer.DID(), tc.cmd, didtest.PersonaAlice.DID(), tc.proofs, tc.opts...)
|
||||
require.NoError(t, err)
|
||||
|
||||
prf := []cid.Cid{missingTknCID, delegationtest.TokenAliceBobCID}
|
||||
testFails(t, invocation.ErrMissingDelegation, didtest.PersonaCarol, delegationtest.NominalCommand, emptyArguments, prf)
|
||||
err = tkn.ExecutionAllowed(delegationtest.GetDelegationLoader())
|
||||
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails - referenced delegation expired", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpired)
|
||||
|
||||
})
|
||||
|
||||
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()
|
||||
const (
|
||||
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
||||
subjectCmd = "/foo/bar"
|
||||
)
|
||||
|
||||
opts = append(opts, invocation.WithArguments(args))
|
||||
|
||||
tkn, err := invocation.New(persona.DID(), cmd, didtest.PersonaAlice.DID(), prf, opts...)
|
||||
func TestConstructors(t *testing.T) {
|
||||
cmd, err := command.Parse(subjectCmd)
|
||||
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...)
|
||||
iat, err := time.Parse(time.RFC3339, "2100-01-01T00:00:00Z")
|
||||
require.NoError(t, err)
|
||||
|
||||
exp, err := time.Parse(time.RFC3339, "2200-01-01T00:00:00Z")
|
||||
require.NoError(t, err)
|
||||
|
||||
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)
|
||||
|
||||
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("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)
|
||||
|
||||
require.True(t, tkn.IsSelfSigned())
|
||||
|
||||
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.JSONEq(t, string(selfsignedDagJson), string(data))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
1
token/invocation/testdata/full_example.dagjson
vendored
Normal file
1
token/invocation/testdata/full_example.dagjson
vendored
Normal file
@@ -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"}}]
|
||||
2
token/invocation/testdata/new.dagjson
vendored
2
token/invocation/testdata/new.dagjson
vendored
@@ -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"}}]
|
||||
[{"/":{"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"}}]
|
||||
1
token/invocation/testdata/selfsigned.dagjson
vendored
Normal file
1
token/invocation/testdata/selfsigned.dagjson
vendored
Normal file
@@ -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"}}]
|
||||
Reference in New Issue
Block a user