2024-10-15 15:41:14 +02:00
|
|
|
// Package invocation implements the UCAN [invocation] specification with
|
2024-10-02 10:53:30 +02:00
|
|
|
// an immutable Token type as well as methods to convert the Token to and
|
|
|
|
|
// from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that
|
|
|
|
|
// should most commonly be used for transport and storage.
|
|
|
|
|
//
|
|
|
|
|
// [envelope]: https://github.com/ucan-wg/spec#envelope
|
|
|
|
|
// [invocation]: https://github.com/ucan-wg/invocation
|
|
|
|
|
package invocation
|
|
|
|
|
|
|
|
|
|
import (
|
2024-12-10 12:20:18 +01:00
|
|
|
"encoding/base64"
|
2024-10-02 10:53:30 +02:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2024-12-10 12:20:18 +01:00
|
|
|
"strings"
|
2024-10-02 10:53:30 +02:00
|
|
|
"time"
|
|
|
|
|
|
2024-10-24 10:44:38 -04:00
|
|
|
"github.com/ipfs/go-cid"
|
2024-11-05 17:39:39 +01:00
|
|
|
|
2024-10-02 10:53:30 +02:00
|
|
|
"github.com/ucan-wg/go-ucan/did"
|
2024-11-07 13:50:20 -05:00
|
|
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
2024-10-02 10:53:30 +02:00
|
|
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
|
|
|
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
2024-10-18 10:48:47 +02:00
|
|
|
"github.com/ucan-wg/go-ucan/token/delegation"
|
2024-11-12 10:38:25 +01:00
|
|
|
"github.com/ucan-wg/go-ucan/token/internal/nonce"
|
2024-11-05 17:39:39 +01:00
|
|
|
"github.com/ucan-wg/go-ucan/token/internal/parse"
|
2024-10-02 10:53:30 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Token is an immutable type that holds the fields of a UCAN invocation.
|
|
|
|
|
type Token struct {
|
2024-10-24 10:44:38 -04:00
|
|
|
// The DID of the Invoker
|
2024-10-02 10:53:30 +02:00
|
|
|
issuer did.DID
|
2024-10-24 10:44:38 -04:00
|
|
|
// The DID of Subject being invoked
|
2024-10-02 10:53:30 +02:00
|
|
|
subject did.DID
|
2024-10-24 10:44:38 -04:00
|
|
|
// The DID of the intended Executor if different from the Subject
|
|
|
|
|
audience did.DID
|
|
|
|
|
|
|
|
|
|
// The Command
|
2024-10-02 10:53:30 +02:00
|
|
|
command command.Command
|
2024-10-18 10:48:47 +02:00
|
|
|
// The Command's arguments
|
2024-11-07 13:50:20 -05:00
|
|
|
arguments *args.Args
|
2024-10-18 10:48:47 +02:00
|
|
|
// CIDs of the delegation.Token that prove the chain of authority
|
2024-11-13 14:50:59 +01:00
|
|
|
// They need to form a strictly linear chain, and being ordered starting from the
|
|
|
|
|
// leaf Delegation (with aud matching the invocation's iss), in a strict sequence
|
|
|
|
|
// where the iss of the previous Delegation matches the aud of the next Delegation.
|
2024-10-24 10:44:38 -04:00
|
|
|
proof []cid.Cid
|
2024-10-02 10:53:30 +02:00
|
|
|
// Arbitrary Metadata
|
|
|
|
|
meta *meta.Meta
|
2024-10-24 10:44:38 -04:00
|
|
|
|
|
|
|
|
// A unique, random nonce
|
|
|
|
|
nonce []byte
|
2024-10-02 10:53:30 +02:00
|
|
|
// The timestamp at which the Invocation becomes invalid
|
|
|
|
|
expiration *time.Time
|
|
|
|
|
// The timestamp at which the Invocation was created
|
|
|
|
|
invokedAt *time.Time
|
2024-10-24 10:44:38 -04:00
|
|
|
|
|
|
|
|
// An optional CID of the Receipt that enqueued the Task
|
|
|
|
|
cause *cid.Cid
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New creates an invocation Token with the provided options.
|
|
|
|
|
//
|
|
|
|
|
// If no nonce is provided, a random 12-byte nonce is generated. Use the
|
|
|
|
|
// WithNonce or WithEmptyNonce options to specify provide your own nonce
|
|
|
|
|
// or to leave the nonce empty respectively.
|
|
|
|
|
//
|
|
|
|
|
// If no invokedAt is provided, the current time is used. Use the
|
2024-11-05 09:35:55 -05:00
|
|
|
// WithInvokedAt or WithInvokedAtIn Options to specify a different time
|
|
|
|
|
// or the WithoutInvokedAt Option to clear the Token's invokedAt field.
|
2024-10-24 10:44:38 -04:00
|
|
|
//
|
2024-11-05 17:39:39 +01:00
|
|
|
// With the exception of the WithMeta option, all others will overwrite
|
2024-10-24 10:44:38 -04:00
|
|
|
// the previous contents of their target field.
|
2024-12-09 20:39:47 +01:00
|
|
|
//
|
|
|
|
|
// You can read it as "(Issuer - I) executes (command) on (subject)".
|
|
|
|
|
func New(iss did.DID, cmd command.Command, sub did.DID, prf []cid.Cid, opts ...Option) (*Token, error) {
|
2024-10-24 10:44:38 -04:00
|
|
|
iat := time.Now()
|
|
|
|
|
|
|
|
|
|
tkn := Token{
|
|
|
|
|
issuer: iss,
|
|
|
|
|
subject: sub,
|
|
|
|
|
command: cmd,
|
2024-11-07 13:50:20 -05:00
|
|
|
arguments: args.New(),
|
2024-10-24 10:44:38 -04:00
|
|
|
proof: prf,
|
2024-11-05 17:39:39 +01:00
|
|
|
meta: meta.NewMeta(),
|
|
|
|
|
nonce: nil,
|
2024-10-24 10:44:38 -04:00
|
|
|
invokedAt: &iat,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
if err := opt(&tkn); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-13 16:58:48 +01:00
|
|
|
var err error
|
2024-11-05 17:39:39 +01:00
|
|
|
if len(tkn.nonce) == 0 {
|
2024-11-12 10:38:25 +01:00
|
|
|
tkn.nonce, err = nonce.Generate()
|
2024-11-05 17:39:39 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tkn.validate(); err != nil {
|
|
|
|
|
return nil, err
|
2024-10-24 12:59:38 -04:00
|
|
|
}
|
|
|
|
|
|
2024-10-24 10:44:38 -04:00
|
|
|
return &tkn, nil
|
2024-10-02 10:53:30 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-20 12:34:24 +01:00
|
|
|
func (t *Token) ExecutionAllowed(loader delegation.Loader) error {
|
2024-10-18 10:48:47 +02:00
|
|
|
return t.executionAllowed(loader, t.arguments)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-20 12:34:24 +01:00
|
|
|
func (t *Token) ExecutionAllowedWithArgsHook(loader delegation.Loader, hook func(args args.ReadOnly) (*args.Args, error)) error {
|
|
|
|
|
newArgs, err := hook(t.arguments.ReadOnly())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return t.executionAllowed(loader, newArgs)
|
2024-10-18 10:48:47 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-20 12:34:24 +01:00
|
|
|
func (t *Token) executionAllowed(loader delegation.Loader, arguments *args.Args) error {
|
2024-11-13 16:58:48 +01:00
|
|
|
delegations, err := t.loadProofs(loader)
|
|
|
|
|
if err != nil {
|
2024-11-14 16:44:32 +01:00
|
|
|
// All referenced delegations must be available - 4b
|
2024-11-20 12:34:24 +01:00
|
|
|
return err
|
2024-11-12 15:16:43 -05:00
|
|
|
}
|
|
|
|
|
|
2024-11-13 16:58:48 +01:00
|
|
|
if err := t.verifyProofs(delegations); err != nil {
|
2024-11-20 12:34:24 +01:00
|
|
|
return err
|
2024-11-12 15:16:43 -05:00
|
|
|
}
|
|
|
|
|
|
2024-11-13 16:58:48 +01:00
|
|
|
if err := t.verifyTimeBound(delegations); err != nil {
|
2024-11-20 12:34:24 +01:00
|
|
|
return err
|
2024-11-12 15:16:43 -05:00
|
|
|
}
|
|
|
|
|
|
2024-11-13 16:58:48 +01:00
|
|
|
if err := t.verifyArgs(delegations, arguments); err != nil {
|
2024-11-20 12:34:24 +01:00
|
|
|
return err
|
2024-11-12 15:16:43 -05:00
|
|
|
}
|
|
|
|
|
|
2024-11-20 12:34:24 +01:00
|
|
|
return nil
|
2024-10-18 10:48:47 +02:00
|
|
|
}
|
|
|
|
|
|
2024-10-02 10:53:30 +02:00
|
|
|
// Issuer returns the did.DID representing the Token's issuer.
|
|
|
|
|
func (t *Token) Issuer() did.DID {
|
|
|
|
|
return t.issuer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Subject returns the did.DID representing the Token's subject.
|
|
|
|
|
func (t *Token) Subject() did.DID {
|
|
|
|
|
return t.subject
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-24 10:44:38 -04:00
|
|
|
// Audience returns the did.DID representing the Token's audience.
|
|
|
|
|
func (t *Token) Audience() did.DID {
|
|
|
|
|
return t.audience
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-02 10:53:30 +02:00
|
|
|
// Command returns the capability's command.Command.
|
|
|
|
|
func (t *Token) Command() command.Command {
|
|
|
|
|
return t.command
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 09:35:55 -05:00
|
|
|
// Arguments returns the arguments to be used when the command is
|
|
|
|
|
// invoked.
|
2024-11-20 12:34:24 +01:00
|
|
|
func (t *Token) Arguments() args.ReadOnly {
|
|
|
|
|
return t.arguments.ReadOnly()
|
2024-10-24 10:44:38 -04:00
|
|
|
}
|
|
|
|
|
|
2024-11-05 17:39:39 +01:00
|
|
|
// Proof() returns the ordered list of cid.Cid which reference the
|
2024-11-05 09:35:55 -05:00
|
|
|
// delegation Tokens that authorize this invocation.
|
2024-11-20 18:27:01 +01:00
|
|
|
// Ordering is from the leaf Delegation (with aud matching the invocation's iss)
|
|
|
|
|
// to the root delegation.
|
2024-10-24 10:44:38 -04:00
|
|
|
func (t *Token) Proof() []cid.Cid {
|
|
|
|
|
return t.proof
|
2024-10-02 10:53:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Meta returns the Token's metadata.
|
2024-11-06 15:17:35 +01:00
|
|
|
func (t *Token) Meta() meta.ReadOnly {
|
|
|
|
|
return t.meta.ReadOnly()
|
2024-10-02 10:53:30 +02:00
|
|
|
}
|
|
|
|
|
|
2024-10-24 10:44:38 -04:00
|
|
|
// Nonce returns the random Nonce encapsulated in this Token.
|
|
|
|
|
func (t *Token) Nonce() []byte {
|
|
|
|
|
return t.nonce
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-02 10:53:30 +02:00
|
|
|
// Expiration returns the time at which the Token expires.
|
|
|
|
|
func (t *Token) Expiration() *time.Time {
|
|
|
|
|
return t.expiration
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 09:35:55 -05:00
|
|
|
// InvokedAt returns the time.Time at which the invocation token was
|
|
|
|
|
// created.
|
2024-10-24 10:44:38 -04:00
|
|
|
func (t *Token) InvokedAt() *time.Time {
|
|
|
|
|
return t.invokedAt
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 09:35:55 -05:00
|
|
|
// Cause returns the Token's (optional) cause field which may specify
|
|
|
|
|
// which describes the Receipt that requested the invocation.
|
2024-10-24 10:44:38 -04:00
|
|
|
func (t *Token) Cause() *cid.Cid {
|
|
|
|
|
return t.cause
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-18 10:48:47 +02:00
|
|
|
// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
|
|
|
|
|
// This does NOT do any other kind of verifications.
|
|
|
|
|
func (t *Token) IsValidNow() bool {
|
|
|
|
|
return t.IsValidAt(time.Now())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2024-11-14 16:44:32 +01:00
|
|
|
if t.expiration != nil && ti.After(*t.expiration) {
|
2024-10-18 10:48:47 +02:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-10 12:20:18 +01:00
|
|
|
func (t *Token) String() string {
|
|
|
|
|
var res strings.Builder
|
|
|
|
|
|
|
|
|
|
res.WriteString(fmt.Sprintf("Issuer: %s\n", t.Issuer()))
|
|
|
|
|
res.WriteString(fmt.Sprintf("Audience: %s\n", t.Audience()))
|
|
|
|
|
res.WriteString(fmt.Sprintf("Subject: %v\n", t.Subject()))
|
|
|
|
|
res.WriteString(fmt.Sprintf("Command: %s\n", t.Command()))
|
|
|
|
|
res.WriteString(fmt.Sprintf("Args: %s\n", t.Arguments()))
|
|
|
|
|
res.WriteString(fmt.Sprintf("Proof: %v\n", t.Proof()))
|
|
|
|
|
res.WriteString(fmt.Sprintf("Nonce: %s\n", base64.StdEncoding.EncodeToString(t.Nonce())))
|
|
|
|
|
res.WriteString(fmt.Sprintf("Meta: %s\n", t.Meta()))
|
|
|
|
|
res.WriteString(fmt.Sprintf("Expiration: %v\n", t.Expiration()))
|
|
|
|
|
res.WriteString(fmt.Sprintf("Invoked At: %v\n", t.InvokedAt()))
|
|
|
|
|
res.WriteString(fmt.Sprintf("Cause: %v", t.Cause()))
|
|
|
|
|
|
|
|
|
|
return res.String()
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-02 10:53:30 +02:00
|
|
|
func (t *Token) validate() error {
|
|
|
|
|
var errs error
|
|
|
|
|
|
|
|
|
|
requiredDID := func(id did.DID, fieldname string) {
|
|
|
|
|
if !id.Defined() {
|
|
|
|
|
errs = errors.Join(errs, fmt.Errorf(`a valid did is required for %s: %s`, fieldname, id.String()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requiredDID(t.issuer, "Issuer")
|
2024-11-05 17:39:39 +01:00
|
|
|
requiredDID(t.subject, "Subject")
|
2024-10-02 10:53:30 +02:00
|
|
|
|
|
|
|
|
if len(t.nonce) < 12 {
|
|
|
|
|
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return errs
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-20 12:34:24 +01:00
|
|
|
func (t *Token) loadProofs(loader delegation.Loader) (res []*delegation.Token, err error) {
|
2024-11-13 16:58:48 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-02 10:53:30 +02:00
|
|
|
// tokenFromModel build a decoded view of the raw IPLD data.
|
|
|
|
|
// This function also serves as validation.
|
|
|
|
|
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
|
|
|
|
var (
|
|
|
|
|
tkn Token
|
2024-11-04 16:07:11 -05:00
|
|
|
err error
|
2024-10-02 10:53:30 +02:00
|
|
|
)
|
|
|
|
|
|
2024-11-04 16:07:11 -05:00
|
|
|
if tkn.issuer, err = did.Parse(m.Iss); err != nil {
|
2024-11-05 17:39:39 +01:00
|
|
|
return nil, fmt.Errorf("parse iss: %w", err)
|
2024-11-04 16:07:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tkn.subject, err = did.Parse(m.Sub); err != nil {
|
2024-11-05 17:39:39 +01:00
|
|
|
return nil, fmt.Errorf("parse subject: %w", err)
|
2024-11-04 16:07:11 -05:00
|
|
|
}
|
|
|
|
|
|
2024-11-05 17:39:39 +01:00
|
|
|
if tkn.audience, err = parse.OptionalDID(m.Aud); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("parse audience: %w", err)
|
2024-11-04 16:07:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tkn.command, err = command.Parse(m.Cmd); err != nil {
|
2024-11-05 17:39:39 +01:00
|
|
|
return nil, fmt.Errorf("parse command: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(m.Nonce) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("nonce is required")
|
2024-11-04 16:07:11 -05:00
|
|
|
}
|
2024-11-05 17:39:39 +01:00
|
|
|
tkn.nonce = m.Nonce
|
2024-11-04 16:07:11 -05:00
|
|
|
|
2024-11-07 13:50:20 -05:00
|
|
|
tkn.arguments = m.Args
|
2024-12-02 14:22:42 +01:00
|
|
|
if err := tkn.arguments.Validate(); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid arguments: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-04 16:07:11 -05:00
|
|
|
tkn.proof = m.Prf
|
2024-12-04 19:53:05 +01:00
|
|
|
|
2024-11-04 16:07:11 -05:00
|
|
|
tkn.meta = m.Meta
|
2024-12-04 19:53:05 +01:00
|
|
|
if tkn.meta == nil {
|
|
|
|
|
tkn.meta = meta.NewMeta()
|
|
|
|
|
}
|
2024-11-04 16:07:11 -05:00
|
|
|
|
2024-12-02 12:06:06 +01:00
|
|
|
tkn.expiration, err = parse.OptionalTimestamp(m.Exp)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("parse expiration: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tkn.invokedAt, err = parse.OptionalTimestamp(m.Iat)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("parse invokedAt: %w", err)
|
|
|
|
|
}
|
2024-11-04 16:07:11 -05:00
|
|
|
|
2024-11-05 17:39:39 +01:00
|
|
|
tkn.cause = m.Cause
|
2024-11-04 16:07:11 -05:00
|
|
|
|
2024-11-05 17:39:39 +01:00
|
|
|
if err := tkn.validate(); err != nil {
|
2024-11-04 16:07:11 -05:00
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-10-02 10:53:30 +02:00
|
|
|
|
|
|
|
|
return &tkn, nil
|
|
|
|
|
}
|