invocation: split out the delegation chain control
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
105
token/invocation/proof.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user