invocation: split out the delegation chain control

This commit is contained in:
Michael Muré
2024-11-13 16:58:48 +01:00
parent 92065ca0d3
commit 64b989452f
5 changed files with 162 additions and 82 deletions

View File

@@ -26,17 +26,17 @@ const Tag = "ucan/dlg@1.0.0-rc.1"
var schemaBytes []byte var schemaBytes []byte
var ( var (
once sync.Once once sync.Once
ts *schema.TypeSystem ts *schema.TypeSystem
err error errSchema error
) )
func mustLoadSchema() *schema.TypeSystem { func mustLoadSchema() *schema.TypeSystem {
once.Do(func() { once.Do(func() {
ts, err = ipld.LoadSchemaBytes(schemaBytes) ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
}) })
if err != nil { if errSchema != nil {
panic(fmt.Errorf("failed to load IPLD schema: %s", err)) panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
} }
return ts return ts
} }

View File

@@ -2,31 +2,36 @@ package invocation
import "errors" import "errors"
// Loading errors
var ( var (
// ErrDelegationExpired is returned if one of the delegations in the // ErrMissingDelegation
// proof chain has expired. ErrMissingDelegation = errors.New("loader missing delegation for proof chain")
ErrDelegationExpired = errors.New("delegation in proof chain has expired") )
// ErrDelegationInactive is returned if one of the delegations in the // Time bound errors
// proof chain is not yet active. var (
ErrDelegationInactive = errors.New("delegation in proof chain not yet active") // 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 // ErrLastNotRoot is returned if the last delegation token in the proof
// chain is not a root delegation token. // chain is not a root delegation token.
ErrLastNotRoot = errors.New("the last delegation token in proof chain must be a root token") ErrLastNotRoot = errors.New("the last delegation token in proof chain must be a root token")
// ErrMissingDelegation // ErrBrokenChain is returned when the Audience of a delegation is
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
// not the Issuer of the previous one. // 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")
) )

View File

@@ -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 { if len(tkn.nonce) == 0 {
tkn.nonce, err = nonce.Generate() tkn.nonce, err = nonce.Generate()
if err != nil { if err != nil {
@@ -140,10 +141,6 @@ func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) (
return &tkn, nil return &tkn, nil
} }
type DelegationLoader interface {
GetDelegation(cid cid.Cid) (*delegation.Token, error)
}
func (t *Token) ExecutionAllowed(loader DelegationLoader) (bool, error) { func (t *Token) ExecutionAllowed(loader DelegationLoader) (bool, error) {
return t.executionAllowed(loader, t.arguments) 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) { func (t *Token) executionAllowed(loader DelegationLoader, arguments *args.Args) (bool, error) {
// There must be at least one delegation referenced - 4a delegations, err := t.loadProofs(loader)
if len(t.proof) < 1 { if err != nil {
return false, ErrNoProof return false, err
} }
type chainer interface { if err := t.verifyProofs(delegations); err != nil {
Issuer() did.DID return false, err
Subject() did.DID // TODO: if the invocation token's Audience is nil, copy the subject into it
Command() command.Command
} }
// This starts as the invocation token but will be the root delegation if err := t.verifyTimeBound(delegations); err != nil {
// after the for loop below completes return false, err
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
} }
// The last prf value must be a root delegation (have the issuer field if err := t.verifyArgs(delegations, arguments); err != nil {
// match the Subject field) - 4g return false, err
if lastChainer.Issuer() != lastChainer.Subject() {
return false, fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, lastChainer.Subject(), lastChainer.Issuer())
} }
return true, nil return true, nil
@@ -304,6 +263,17 @@ func (t *Token) validate() error {
return errs 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. // tokenFromModel build a decoded view of the raw IPLD data.
// This function also serves as validation. // This function also serves as validation.
func tokenFromModel(m tokenPayloadModel) (*Token, error) { func tokenFromModel(m tokenPayloadModel) (*Token, error) {

105
token/invocation/proof.go Normal file
View File

@@ -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
}

View File

@@ -25,17 +25,17 @@ const Tag = "ucan/inv@1.0.0-rc.1"
var schemaBytes []byte var schemaBytes []byte
var ( var (
once sync.Once once sync.Once
ts *schema.TypeSystem ts *schema.TypeSystem
err error errSchema error
) )
func mustLoadSchema() *schema.TypeSystem { func mustLoadSchema() *schema.TypeSystem {
once.Do(func() { once.Do(func() {
ts, err = ipld.LoadSchemaBytes(schemaBytes) ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
}) })
if err != nil { if errSchema != nil {
panic(fmt.Errorf("failed to load IPLD schema: %s", err)) panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
} }
return ts return ts
} }