invocation: rework the validation doc, fix missing invocation time check

This commit is contained in:
Michael Muré
2024-11-14 16:44:32 +01:00
parent 417ef78570
commit bb36d61d93
2 changed files with 53 additions and 43 deletions

View File

@@ -3,45 +3,6 @@
// from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that // from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that
// should most commonly be used for transport and storage. // should most commonly be used for transport and storage.
// //
// # Invocation token validation
//
// Per the specification, invocation Tokens must be validated before the
// command is executed. This validation can happen in multiple stages:
//
// 1. When the invocation is unsealed from its containing envelope:
// 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 a public key can be extracted from the did:key.
// e. The public key type is 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.
// h. The field key of the TokenPayload matches the expected tag.
// 2. When the invocation is created or passes step one:
// a. The Issuer field is not empty.
// b. The Subject field is not empty
// c. The Command field is not empty and the Command is not a wildcard.
// d. The Policy field is present (but may be empty).
// e. The Arguments field is present (but may be empty).
// 3. When an unsealed invocation passes steps one and two for execution:
// a. The invocation can not be expired.
// b. Invoked at should not be in the future.
// 4. When the proof chain is being validated:
// a. There must be at least one delegation in the proof chain.
// b. All referenced delegations must be available.
// c. The first proof must be issued to the Invoker (audience DID).
// d. The token must not be expired (expiration in the future or absent).
// e. The token must be active (nbf in the past or absent).
// f. The Issuer of each delegation must be the Audience in the next
// one.
// g. The last token must be a root delegation.
// h. The Subject of each delegation must equal the invocation's
// Audience field.
// i. The command of each delegation must "allow" the one before it.
// 5. If steps 1-4 pass:
// a. The policy must "match" the arguments.
// b. The nonce (if present) is not reused.
//
// [envelope]: https://github.com/ucan-wg/spec#envelope // [envelope]: https://github.com/ucan-wg/spec#envelope
// [invocation]: https://github.com/ucan-wg/invocation // [invocation]: https://github.com/ucan-wg/invocation
package invocation package invocation
@@ -152,6 +113,7 @@ 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) {
delegations, err := t.loadProofs(loader) delegations, err := t.loadProofs(loader)
if err != nil { if err != nil {
// All referenced delegations must be available - 4b
return false, err return false, err
} }
@@ -238,7 +200,7 @@ func (t *Token) IsValidNow() bool {
// IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields. // IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields.
// This does NOT do any other kind of verifications. // This does NOT do any other kind of verifications.
func (t *Token) IsValidAt(ti time.Time) bool { func (t *Token) IsValidAt(ti time.Time) bool {
if t.expiration == nil && ti.After(*t.expiration) { if t.expiration != nil && ti.After(*t.expiration) {
return false return false
} }
return true return true

View File

@@ -11,6 +11,44 @@ import (
"github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/delegation"
) )
// # Invocation token validation
//
// Per the specification, invocation Tokens must be validated before the command is executed.
// This validation effectively happens in multiple places in the codebase.
// Steps 1 and 2 are the same for all token types.
//
// 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.
// f. The Signature can be decoded per the VarsigHeader.
// g. The SigPayload can be verified using the Signature and 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):
// a. All required fields are present
// b. All populated fields respect their own rules (example: a policy is legal)
//
// 3. When an unsealed invocation passes steps one and two for execution (verifyTimeBound below):
// a. The invocation cannot be expired (expiration in the future or absent).
// b. All the delegation must not be expired (expiration in the future or absent).
// 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.
// 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.
// e. The last token must be a root delegation.
// f. The Subject of each delegation must equal the invocation's Audience field.
// g. The command of each delegation must "allow" the one before it.
//
// 5. If steps 1-4 pass:
// a. The policy must "match" the arguments. (verifyArgs below)
// b. The nonce (if present) is not reused. (out of scope for go-ucan)
type DelegationLoader interface { type DelegationLoader interface {
GetDelegation(cid cid.Cid) (*delegation.Token, error) GetDelegation(cid cid.Cid) (*delegation.Token, error)
} }
@@ -19,7 +57,7 @@ type DelegationLoader interface {
// - principal alignment // - principal alignment
// - command alignment // - command alignment
func (t *Token) verifyProofs(delegations []*delegation.Token) error { func (t *Token) verifyProofs(delegations []*delegation.Token) error {
// There must be at least one delegation referenced // There must be at least one delegation referenced - 4a
if len(delegations) < 1 { if len(delegations) < 1 {
return ErrNoProof return ErrNoProof
} }
@@ -35,23 +73,26 @@ func (t *Token) verifyProofs(delegations []*delegation.Token) error {
for i, dlgCid := range t.proof { for i, dlgCid := range t.proof {
dlg := delegations[i] dlg := delegations[i]
// The Subject of each delegation must equal the invocation's Audience field. - 4f
if dlg.Subject() != aud { if dlg.Subject() != aud {
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, aud, dlg.Subject()) return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, aud, dlg.Subject())
} }
// The first proof must be issued to the Invoker (audience DID). - 4c
// The Issuer of each delegation must be the Audience in the next one. - 4d
if dlg.Audience() != iss { if dlg.Audience() != iss {
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience()) return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience())
} }
iss = dlg.Issuer() iss = dlg.Issuer()
// The command of each delegation must "allow" the one before it. - 4g
if !dlg.Command().Covers(cmd) { if !dlg.Command().Covers(cmd) {
return fmt.Errorf("%w: delegation %s, %s doesn't cover %s", ErrCommandNotCovered, dlgCid, dlg.Command(), cmd) return fmt.Errorf("%w: delegation %s, %s doesn't cover %s", ErrCommandNotCovered, dlgCid, dlg.Command(), cmd)
} }
cmd = dlg.Command() cmd = dlg.Command()
} }
// The last prf value must be a root delegation (have the issuer field // The last prf value must be a root delegation (have the issuer field match the Subject field) - 4e
// match the Subject field) - 4g
if last := delegations[len(delegations)-1]; last.Issuer() != last.Subject() { if last := delegations[len(delegations)-1]; last.Issuer() != last.Subject() {
return fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, last.Subject(), last.Issuer()) return fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, last.Subject(), last.Issuer())
} }
@@ -64,9 +105,16 @@ func (t *Token) verifyTimeBound(dlgs []*delegation.Token) error {
} }
func (t *Token) verifyTimeBoundAt(at time.Time, delegations []*delegation.Token) error { func (t *Token) verifyTimeBoundAt(at time.Time, delegations []*delegation.Token) error {
// The invocation cannot be expired (expiration in the future or absent). - 3a
if !t.IsValidAt(at) {
return fmt.Errorf("%w: invocation", ErrTokenInvalidNow)
}
for i, dlgCid := range t.proof { for i, dlgCid := range t.proof {
dlg := delegations[i] dlg := delegations[i]
// All the delegation must not be expired (expiration in the future or absent). - 3b
// All the delegation must be active (nbf in the past or absent). - 3c
if !dlg.IsValidAt(at) { if !dlg.IsValidAt(at) {
return fmt.Errorf("%w: delegation %s", ErrTokenInvalidNow, dlgCid) return fmt.Errorf("%w: delegation %s", ErrTokenInvalidNow, dlgCid)
} }