From 64b989452ff9ce9910a209b18d56e7e682a18ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 13 Nov 2024 16:58:48 +0100 Subject: [PATCH] invocation: split out the delegation chain control --- token/delegation/schema.go | 12 ++-- token/invocation/errors.go | 43 ++++++++------ token/invocation/invocation.go | 72 +++++++--------------- token/invocation/proof.go | 105 +++++++++++++++++++++++++++++++++ token/invocation/schema.go | 12 ++-- 5 files changed, 162 insertions(+), 82 deletions(-) create mode 100644 token/invocation/proof.go diff --git a/token/delegation/schema.go b/token/delegation/schema.go index 1c6873c..13721e9 100644 --- a/token/delegation/schema.go +++ b/token/delegation/schema.go @@ -26,17 +26,17 @@ const Tag = "ucan/dlg@1.0.0-rc.1" var schemaBytes []byte var ( - once sync.Once - ts *schema.TypeSystem - err error + once sync.Once + ts *schema.TypeSystem + errSchema error ) func mustLoadSchema() *schema.TypeSystem { once.Do(func() { - ts, err = ipld.LoadSchemaBytes(schemaBytes) + ts, errSchema = ipld.LoadSchemaBytes(schemaBytes) }) - if err != nil { - panic(fmt.Errorf("failed to load IPLD schema: %s", err)) + if errSchema != nil { + panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema)) } return ts } diff --git a/token/invocation/errors.go b/token/invocation/errors.go index e58a8d1..5335763 100644 --- a/token/invocation/errors.go +++ b/token/invocation/errors.go @@ -2,31 +2,36 @@ package invocation import "errors" +// Loading errors var ( - // ErrDelegationExpired is returned if one of the delegations in the - // proof chain has expired. - ErrDelegationExpired = errors.New("delegation in proof chain has expired") + // ErrMissingDelegation + ErrMissingDelegation = errors.New("loader missing delegation for proof chain") +) - // ErrDelegationInactive is returned if one of the delegations in the - // proof chain is not yet active. - ErrDelegationInactive = errors.New("delegation in proof chain not yet active") +// Time bound errors +var ( + // ErrTokenExpired is returned if a token is invalid at execution time + ErrTokenInvalidNow = errors.New("token has expired") +) + +// Principal alignment errors +var ( + // ErrNoProof is returned when no delegations were provided to prove + // that the invocation should be executed. + ErrNoProof = errors.New("at least one delegation must be provided to validate the invocation") // ErrLastNotRoot is returned if the last delegation token in the proof // chain is not a root delegation token. ErrLastNotRoot = errors.New("the last delegation token in proof chain must be a root token") - // ErrMissingDelegation - ErrMissingDelegation = errors.New("loader missing delegation for proof chain") - - // ErrNoProof is returned when no delegations were provided to prove - // that the invocation should be executed. - ErrNoProof = errors.New("at least one delegation must be provided to validate the invocation") - - // ErrNotIssuedToInvoder is returned if the first delegation token's - // Audience DID is not the Invoker's Issuer DID. - ErrNotIssuedToInvoker = errors.New("first delegation token is not issued to invoker") - - // ErrBrokenChain is returned when the Audience of each delegation is + // ErrBrokenChain is returned when the Audience of a delegation is // not the Issuer of the previous one. - ErrBrokenChain = errors.New("delegation proof chain is broken") + ErrBrokenChain = errors.New("delegation proof chain doesn't connect the invocation to the subject") + + // ErrWrongSub is returned when the Subject of a delegation is not the invocation audience. + ErrWrongSub = errors.New("delegation subject need to match the invocation audience") + + // ErrCommandNotCovered is returned when a delegation command doesn't cover (identical or parent of) the + // next delegation or invocation's command. + ErrCommandNotCovered = errors.New("allowed command doesn't cover the next delegation or invocation") ) diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 816030c..b5cc5c3 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -126,6 +126,7 @@ func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) ( } } + var err error if len(tkn.nonce) == 0 { tkn.nonce, err = nonce.Generate() if err != nil { @@ -140,10 +141,6 @@ func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) ( return &tkn, nil } -type DelegationLoader interface { - GetDelegation(cid cid.Cid) (*delegation.Token, error) -} - func (t *Token) ExecutionAllowed(loader DelegationLoader) (bool, error) { return t.executionAllowed(loader, t.arguments) } @@ -153,59 +150,21 @@ func (t *Token) ExecutionAllowedWithArgsHook(loader DelegationLoader, hook func( } func (t *Token) executionAllowed(loader DelegationLoader, arguments *args.Args) (bool, error) { - // There must be at least one delegation referenced - 4a - if len(t.proof) < 1 { - return false, ErrNoProof + delegations, err := t.loadProofs(loader) + if err != nil { + return false, err } - type chainer interface { - Issuer() did.DID - Subject() did.DID // TODO: if the invocation token's Audience is nil, copy the subject into it - Command() command.Command + if err := t.verifyProofs(delegations); err != nil { + return false, err } - // This starts as the invocation token but will be the root delegation - // after the for loop below completes - var lastChainer chainer = t - - for i, dlgCid := range t.proof { - // The token must be present - 4b - dlg, err := loader.GetDelegation(dlgCid) - if err != nil { - return false, fmt.Errorf("%w: need %s", ErrMissingDelegation, dlgCid) - } - - // No tokens in the proof chain may be expired - 4d - if dlg.Expiration() != nil && dlg.Expiration().Before(time.Now()) { - return false, fmt.Errorf("%w: CID is %s", ErrDelegationExpired, dlgCid) - } - - // No tokens in the proof chain may be inactive - 4e - if dlg.NotBefore() != nil && dlg.NotBefore().After(time.Now()) { - return false, fmt.Errorf("%w: CID is %s", ErrDelegationInactive, dlgCid) - } - - // First proof must have the invoker's Issuer as the Audience - 4c - if i == 0 && dlg.Audience() != t.Issuer() { - return false, fmt.Errorf("%w: expected %s, got %s", ErrNotIssuedToInvoker, t.issuer, dlg.Audience()) - } - - // Tokens must form a chain with current issuer equal to the - // next audience - 4f - if lastChainer.Issuer() != dlg.Audience() { - return false, fmt.Errorf("%w: expected %s, got %s", ErrBrokenChain, lastChainer.Issuer(), dlg.Audience()) - } - - // TODO: Checking the subject consistency can happen here - 4h - // TODO: Checking the command equivalence or attenuation can happen here - 4i - - lastChainer = dlg + if err := t.verifyTimeBound(delegations); err != nil { + return false, err } - // The last prf value must be a root delegation (have the issuer field - // match the Subject field) - 4g - if lastChainer.Issuer() != lastChainer.Subject() { - return false, fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, lastChainer.Subject(), lastChainer.Issuer()) + if err := t.verifyArgs(delegations, arguments); err != nil { + return false, err } return true, nil @@ -304,6 +263,17 @@ func (t *Token) validate() error { return errs } +func (t *Token) loadProofs(loader DelegationLoader) (res []*delegation.Token, err error) { + res = make([]*delegation.Token, len(t.proof)) + for i, c := range t.proof { + res[i], err = loader.GetDelegation(c) + if err != nil { + return nil, fmt.Errorf("%w: need %s", ErrMissingDelegation, c) + } + } + return res, nil +} + // tokenFromModel build a decoded view of the raw IPLD data. // This function also serves as validation. func tokenFromModel(m tokenPayloadModel) (*Token, error) { diff --git a/token/invocation/proof.go b/token/invocation/proof.go new file mode 100644 index 0000000..e345f3c --- /dev/null +++ b/token/invocation/proof.go @@ -0,0 +1,105 @@ +package invocation + +import ( + "fmt" + "time" + + "github.com/ipfs/go-cid" + + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +type DelegationLoader interface { + GetDelegation(cid cid.Cid) (*delegation.Token, error) +} + +// verifyProofs controls that the proof chain allows the invocation: +// - principal alignment +// - command alignment +func (t *Token) verifyProofs(delegations []*delegation.Token) error { + cmd := t.command + iss := t.issuer + aud := t.audience + if !aud.Defined() { + aud = t.subject + } + + var last *delegation.Token + + // control from the invocation to the root + for i, dlgCid := range t.proof { + dlg := delegations[i] + + if dlg.Subject() != aud { + return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, aud, dlg.Subject()) + } + + if dlg.Audience() != iss { + return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience()) + } + iss = dlg.Audience() + + if !dlg.Command().Covers(cmd) { + return fmt.Errorf("%w: delegation %s, %s doesn't cover %s", ErrCommandNotCovered, dlgCid, dlg.Command(), cmd) + } + cmd = dlg.Command() + + last = dlg + } + + // There must be at least one delegation referenced + // (yes, it's an odd way to test this, but it allows for the static check to not be mad about "last" + // being possibly nil below). + if last == nil { + return ErrNoProof + } + + // The last prf value must be a root delegation (have the issuer field + // match the Subject field) - 4g + if last.Issuer() != last.Subject() { + return fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, last.Subject(), last.Issuer()) + } + + return nil +} + +func (t *Token) verifyTimeBound(dlgs []*delegation.Token) error { + return t.verifyTimeBoundAt(time.Now(), dlgs) +} + +func (t *Token) verifyTimeBoundAt(at time.Time, delegations []*delegation.Token) error { + for i, dlgCid := range t.proof { + dlg := delegations[i] + + if !dlg.IsValidAt(at) { + return fmt.Errorf("%w: delegation %s", ErrTokenInvalidNow, dlgCid) + } + } + return nil +} + +func (t *Token) verifyArgs(delegations []*delegation.Token, arguments *args.Args) error { + var count int + for i := range t.proof { + count += len(delegations[i].Policy()) + } + + policies := make(policy.Policy, 0, count) + for i := range t.proof { + policies = append(policies, delegations[i].Policy()...) + } + + argsIpld, err := arguments.ToIPLD() + if err != nil { + return err + } + + ok, statement := policies.Match(argsIpld) + if !ok { + return fmt.Errorf("the following UCAN policy is not satisfied: %v", statement.String()) + } + + return nil +} diff --git a/token/invocation/schema.go b/token/invocation/schema.go index d51cf4f..5a30e66 100644 --- a/token/invocation/schema.go +++ b/token/invocation/schema.go @@ -25,17 +25,17 @@ const Tag = "ucan/inv@1.0.0-rc.1" var schemaBytes []byte var ( - once sync.Once - ts *schema.TypeSystem - err error + once sync.Once + ts *schema.TypeSystem + errSchema error ) func mustLoadSchema() *schema.TypeSystem { once.Do(func() { - ts, err = ipld.LoadSchemaBytes(schemaBytes) + ts, errSchema = ipld.LoadSchemaBytes(schemaBytes) }) - if err != nil { - panic(fmt.Errorf("failed to load IPLD schema: %s", err)) + if errSchema != nil { + panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema)) } return ts }