diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index b5cc5c3..3faa0d3 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -3,45 +3,6 @@ // from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that // 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 // [invocation]: https://github.com/ucan-wg/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) { delegations, err := t.loadProofs(loader) if err != nil { + // All referenced delegations must be available - 4b 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. // This does NOT do any other kind of verifications. 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 true diff --git a/token/invocation/proof.go b/token/invocation/proof.go index 7791a2b..a644c17 100644 --- a/token/invocation/proof.go +++ b/token/invocation/proof.go @@ -11,6 +11,44 @@ import ( "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 { GetDelegation(cid cid.Cid) (*delegation.Token, error) } @@ -19,7 +57,7 @@ type DelegationLoader interface { // - principal alignment // - command alignment 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 { return ErrNoProof } @@ -35,23 +73,26 @@ func (t *Token) verifyProofs(delegations []*delegation.Token) error { for i, dlgCid := range t.proof { dlg := delegations[i] + // The Subject of each delegation must equal the invocation's Audience field. - 4f if dlg.Subject() != aud { 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 { return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience()) } iss = dlg.Issuer() + // The command of each delegation must "allow" the one before it. - 4g if !dlg.Command().Covers(cmd) { return fmt.Errorf("%w: delegation %s, %s doesn't cover %s", ErrCommandNotCovered, dlgCid, dlg.Command(), cmd) } cmd = dlg.Command() } - // The last prf value must be a root delegation (have the issuer field - // match the Subject field) - 4g + // The last prf value must be a root delegation (have the issuer field match the Subject field) - 4e if last := delegations[len(delegations)-1]; last.Issuer() != last.Subject() { 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 { + // 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 { 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) { return fmt.Errorf("%w: delegation %s", ErrTokenInvalidNow, dlgCid) }