invocation

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 23, 2025 License: Apache-2.0, MIT Imports: 26 Imported by: 0

Documentation

Overview

Package invocation implements the UCAN invocation specification with 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.

Index

Examples

Constants

View Source
const Tag = "ucan/inv@1.0.0-rc.1"

Tag is the string used as a key within the SigPayload that identifies that the TokenPayload is an invocation.

Variables

View Source
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
	// chain is not a root delegation token.
	ErrLastNotRoot = errors.New("the last delegation token in proof chain must be a root token")

	// ErrBrokenChain is returned when the Audience of a delegation is
	// not the Issuer of the previous one.
	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")
)

Principal alignment errors

View Source
var (
	// ErrMissingDelegation
	ErrMissingDelegation = errors.New("loader missing delegation for proof chain")
)

Loading errors

View Source
var ErrPolicyNotSatisfied = errors.New("the following UCAN policy is not satisfied")

ErrPolicyNotSatisfied is returned when the provided Arguments don't satisfy one or more of the aggregated Policy Statements

View Source
var (
	// ErrTokenExpired is returned if a token is invalid at execution time
	ErrTokenInvalidNow = errors.New("token has expired")
)

Time bound errors

Functions

This section is empty.

Types

type Option

type Option func(*Token) error

Option is a type that allows optional fields to be set during the creation of an invocation Token.

func WithArgument

func WithArgument(key string, val any) Option

WithArgument adds a key/value pair to the Token's Arguments field.

func WithArguments

func WithArguments(args *args.Args) Option

WithArguments merges the provided arguments into the Token's existing arguments.

If duplicate keys are encountered, the new value is silently dropped without causing an error. Since duplicate keys can only be encountered due to previous calls to WithArgument or WithArguments, calling only this function to set the Token's arguments is equivalent to assigning the arguments to the Token.

func WithAudience

func WithAudience(aud did.DID) Option

WithAudience sets the Token's audience to the provided did.DID.

This can be used if the resource on which the token operates on is different from the subject. In that situation, the subject is akin to the "service" and the audience is akin to the resource.

If the provided did.DID is the same as the Token's subject, the audience is not set.

func WithCause

func WithCause(cause *cid.Cid) Option

WithCause sets the Token's cause field to the provided cid.Cid.

func WithEmptyNonce

func WithEmptyNonce() Option

WithEmptyNonce sets the Token's nonce to an empty byte slice as suggested by the UCAN spec for invocation tokens that represent idempotent operations.

func WithEncryptedMetaBytes

func WithEncryptedMetaBytes(key string, val, encryptionKey []byte) Option

WithEncryptedMetaBytes adds a key/value pair in the "meta" field. The []byte value is encrypted with the given aesKey.

func WithEncryptedMetaString

func WithEncryptedMetaString(key, val string, encryptionKey []byte) Option

WithEncryptedMetaString adds a key/value pair in the "meta" field. The string value is encrypted with the given aesKey.

func WithExpiration

func WithExpiration(exp time.Time) Option

WithExpiration set's the Token's optional "expiration" field to the value of the provided time.Time.

func WithExpirationIn

func WithExpirationIn(after time.Duration) Option

WithExpirationIn set's the Token's optional "expiration" field to Now() plus the given duration.

func WithInvokedAt

func WithInvokedAt(iat time.Time) Option

WithInvokedAt sets the Token's invokedAt field to the provided time.Time.

If this Option is not provided, the invocation Token's iat field will be set to the value of time.Now(). If you want to create an invocation Token without this field being set, use the WithoutInvokedAt Option.

func WithInvokedAtIn

func WithInvokedAtIn(after time.Duration) Option

WithInvokedAtIn sets the Token's invokedAt field to Now() plus the given duration.

func WithMeta

func WithMeta(key string, val any) Option

WithMeta adds a key/value pair in the "meta" field.

WithMeta can be used multiple times in the same call. Accepted types for the value are: bool, string, int, int32, int64, []byte, and ipld.Node.

func WithNonce

func WithNonce(nonce []byte) Option

WithNonce sets the Token's nonce with the given value.

If this option is not used, a random 12-byte nonce is generated for this required field. If you truly want to create an invocation Token without a nonce, use the WithEmptyNonce Option which will set the nonce to an empty byte array.

func WithoutInvokedAt

func WithoutInvokedAt() Option

WithoutInvokedAt clears the Token's invokedAt field.

type Token

type Token struct {
	// contains filtered or unexported fields
}

Token is an immutable type that holds the fields of a UCAN invocation.

func Decode

func Decode(b []byte, decFn codec.Decoder) (*Token, error)

Decode unmarshals the input data using the format specified by the provided codec.Decoder into a Token.

An error is returned if the conversion fails, or if the resulting Token is invalid.

func DecodeReader

func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error)

DecodeReader is the same as Decode, but accept an io.Reader.

func FromDagCbor

func FromDagCbor(data []byte) (*Token, error)

FromDagCbor unmarshals the input data into a Token.

An error is returned if the conversion fails, or if the resulting Token is invalid.

func FromDagCborReader

func FromDagCborReader(r io.Reader) (*Token, error)

FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.

func FromDagJson

func FromDagJson(data []byte) (*Token, error)

FromDagJson unmarshals the input data into a Token.

An error is returned if the conversion fails, or if the resulting Token is invalid.

func FromDagJsonReader

func FromDagJsonReader(r io.Reader) (*Token, error)

FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.

func FromIPLD

func FromIPLD(node datamodel.Node) (*Token, error)

FromIPLD decode the given IPLD representation into a Token.

func FromSealed

func FromSealed(data []byte) (*Token, cid.Cid, error)

FromSealed decodes the provided binary data from the DAG-CBOR format, verifies that the envelope's signature is correct based on the public key taken from the issuer (iss) field and calculates the CID of the incoming data.

func FromSealedReader

func FromSealedReader(r io.Reader) (*Token, cid.Cid, error)

FromSealedReader is the same as Unseal but accepts an io.Reader.

func New

func New(iss did.DID, cmd command.Command, sub did.DID, prf []cid.Cid, opts ...Option) (*Token, error)

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 WithInvokedAt or WithInvokedAtIn Options to specify a different time or the WithoutInvokedAt Option to clear the Token's invokedAt field.

With the exception of the WithMeta option, all others will overwrite the previous contents of their target field.

You can read it as "(Issuer - I) executes (command) on (subject)".

Example
package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/ipfs/go-cid"
	"github.com/ipld/go-ipld-prime"
	"github.com/ipld/go-ipld-prime/codec/dagcbor"
	"github.com/ipld/go-ipld-prime/codec/dagjson"
	"github.com/ipld/go-ipld-prime/node/basicnode"
	"github.com/libp2p/go-libp2p/core/crypto"

	"github.com/ucan-wg/go-ucan/did"
	"github.com/ucan-wg/go-ucan/pkg/command"
	"github.com/ucan-wg/go-ucan/token/invocation"
)

func main() {
	privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew()
	if err != nil {
		fmt.Println("failed to create setup:", err.Error())

		return
	}

	inv, err := invocation.New(iss, cmd, sub, prf,
		invocation.WithArgument("uri", args["uri"]),
		invocation.WithArgument("headers", args["headers"]),
		invocation.WithArgument("payload", args["payload"]),
		invocation.WithMeta("env", "development"),
		invocation.WithMeta("tags", meta["tags"]),
		invocation.WithExpirationIn(time.Minute),
		invocation.WithoutInvokedAt())
	if err != nil {
		fmt.Println("failed to create invocation:", err.Error())

		return
	}

	data, cid, err := inv.ToSealed(privKey)
	if err != nil {
		fmt.Println("failed to seal invocation:", err.Error())

		return
	}

	json, err := prettyDAGJSON(data)
	if err != nil {
		fmt.Println("failed to pretty DAG-JSON:", err.Error())

		return
	}

	fmt.Println("CID:", cid)
	fmt.Println("Token (pretty DAG-JSON):")
	fmt.Println(json)

	// Expected CID and DAG-JSON output:
	// CID: bafyreid2n5q45vk4osned7k5huocbe3mxbisonh5vujepqftc5ftr543ae
	// Token (pretty DAG-JSON):
	// [
	//   {
	// 	"/": {
	// 	  "bytes": "gvyL7kdSkgmaDpDU/Qj9ohRwxYLCHER52HFMSFEqQqEcQC9qr4JCPP1f/WybvGGuVzYiA0Hx4JO+ohNz8BxUAA"
	// 	}
	//   },
	//   {
	// 	"h": {
	// 	  "/": {
	// 		"bytes": "NO0BcQ"
	// 	  }
	// 	},
	// 	"ucan/inv@1.0.0-rc.1": {
	// 	  "args": {
	// 		"headers": {
	// 		  "Content-Type": "application/json"
	// 		},
	// 		"payload": {
	// 		  "body": "UCAN is great",
	// 		  "draft": true,
	// 		  "title": "UCAN for Fun and Profit",
	// 		  "topics": [
	// 			"authz",
	// 			"journal"
	// 		  ]
	// 		},
	// 		"uri": "https://example.com/blog/posts"
	// 	  },
	// 	  "cmd": "/crud/create",
	// 	  "exp": 1729788921,
	// 	  "iss": "did:key:z6MkhniGGyP88eZrq2dpMvUPdS2RQMhTUAWzcu6kVGUvEtCJ",
	// 	  "meta": {
	// 		"env": "development",
	// 		"tags": [
	// 		  "blog",
	// 		  "post",
	// 		  "pr#123"
	// 		]
	// 	  },
	// 	  "nonce": {
	// 		"/": {
	// 		  "bytes": "2xXPoZwWln1TfXIp"
	// 		}
	// 	  },
	// 	  "prf": [
	// 		{
	// 		  "/": "bafyreigx3qxd2cndpe66j2mdssj773ecv7tqd7wovcnz5raguw6lj7sjoe"
	// 		},
	// 		{
	// 		  "/": "bafyreib34ira254zdqgehz6f2bhwme2ja2re3ltcalejv4x4tkcveujvpa"
	// 		},
	// 		{
	// 		  "/": "bafyreibkb66tpo2ixqx3fe5hmekkbuasrod6olt5bwm5u5pi726mduuwlq"
	// 		}
	// 	  ],
	// 	  "sub": "did:key:z6MktWuvPvBe5UyHnDGuEdw8aJ5qrhhwLG6jy7cQYM6ckP6P"
	// 	}
	//   }
	// ]
}

func prettyDAGJSON(data []byte) (string, error) {
	var node ipld.Node

	node, err := ipld.Decode(data, dagcbor.Decode)
	if err != nil {
		return "", err
	}

	jsonData, err := ipld.Encode(node, dagjson.Encode)
	if err != nil {
		return "", err
	}

	var out bytes.Buffer
	if err := json.Indent(&out, jsonData, "", "  "); err != nil {
		return "", err
	}

	return out.String(), nil
}

func setupExampleNew() (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Command, args map[string]any, prf []cid.Cid, meta map[string]any, errs error) {
	var err error

	privKey, iss, err = did.GenerateEd25519()
	if err != nil {
		errs = errors.Join(errs, fmt.Errorf("failed to generate Issuer identity: %w", err))
	}

	_, sub, err = did.GenerateEd25519()
	if err != nil {
		errs = errors.Join(errs, fmt.Errorf("failed to generate Subject identity: %w", err))
	}

	cmd, err = command.Parse("/crud/create")
	if err != nil {
		errs = errors.Join(errs, fmt.Errorf("failed to parse command: %w", err))
	}

	headers := map[string]string{
		"Content-Type": "application/json",
	}

	payload := map[string]any{
		"body":   "UCAN is great",
		"draft":  true,
		"title":  "UCAN for Fun and Profit",
		"topics": []string{"authz", "journal"},
	}

	args = map[string]any{
		// you can also directly pass IPLD values
		"uri":     basicnode.NewString("https://example.com/blog/posts"),
		"headers": headers,
		"payload": payload,
	}

	prf = make([]cid.Cid, 3)
	for i, v := range []string{
		"zdpuAzx4sBrBCabrZZqXgvK3NDzh7Mf5mKbG11aBkkMCdLtCp",
		"zdpuApTCXfoKh2sB1KaUaVSGofCBNPUnXoBb6WiCeitXEibZy",
		"zdpuAoFdXRPw4n6TLcncoDhq1Mr6FGbpjAiEtqSBrTSaYMKkf",
	} {
		prf[i], err = cid.Parse(v)
		if err != nil {
			errs = errors.Join(errs, fmt.Errorf("failed to parse proof cid: %w", err))
		}
	}

	meta = map[string]any{
		"env":  basicnode.NewString("development"),
		"tags": []string{"blog", "post", "pr#123"},
	}

	return // WARNING: named return values
}
Output:

func (*Token) Arguments

func (t *Token) Arguments() args.ReadOnly

Arguments returns the arguments to be used when the command is invoked.

func (*Token) Audience

func (t *Token) Audience() did.DID

Audience returns the did.DID representing the Token's audience.

func (*Token) Cause

func (t *Token) Cause() *cid.Cid

Cause returns the Token's (optional) cause field which may specify which describes the Receipt that requested the invocation.

func (*Token) Command

func (t *Token) Command() command.Command

Command returns the capability's command.Command.

func (*Token) Encode

func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error)

Encode marshals a Token to the format specified by the provided codec.Encoder.

func (*Token) EncodeWriter

func (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error

EncodeWriter is the same as Encode, but accepts an io.Writer.

func (*Token) ExecutionAllowed

func (t *Token) ExecutionAllowed(loader delegation.Loader) error

func (*Token) ExecutionAllowedWithArgsHook

func (t *Token) ExecutionAllowedWithArgsHook(loader delegation.Loader, hook func(args args.ReadOnly) (*args.Args, error)) error

func (*Token) Expiration

func (t *Token) Expiration() *time.Time

Expiration returns the time at which the Token expires.

func (*Token) InvokedAt

func (t *Token) InvokedAt() *time.Time

InvokedAt returns the time.Time at which the invocation token was created.

func (*Token) IsValidAt

func (t *Token) IsValidAt(ti time.Time) 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 (*Token) IsValidNow

func (t *Token) IsValidNow() bool

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 (*Token) Issuer

func (t *Token) Issuer() did.DID

Issuer returns the did.DID representing the Token's issuer.

func (*Token) Meta

func (t *Token) Meta() meta.ReadOnly

Meta returns the Token's metadata.

func (*Token) Nonce

func (t *Token) Nonce() []byte

Nonce returns the random Nonce encapsulated in this Token.

func (*Token) Proof

func (t *Token) Proof() []cid.Cid

Proof() returns the ordered list of cid.Cid which reference the delegation Tokens that authorize this invocation. Ordering is from the leaf Delegation (with aud matching the invocation's iss) to the root delegation.

func (*Token) String

func (t *Token) String() string

func (*Token) Subject

func (t *Token) Subject() did.DID

Subject returns the did.DID representing the Token's subject.

func (*Token) ToDagCbor

func (t *Token) ToDagCbor(privKey crypto.PrivKey) ([]byte, error)

ToDagCbor marshals the Token to the DAG-CBOR format.

func (*Token) ToDagCborWriter

func (t *Token) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error

ToDagCborWriter is the same as ToDagCbor, but it accepts an io.Writer.

func (*Token) ToDagJson

func (t *Token) ToDagJson(privKey crypto.PrivKey) ([]byte, error)

ToDagJson marshals the Token to the DAG-JSON format.

func (*Token) ToDagJsonWriter

func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error

ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.

func (*Token) ToSealed

func (t *Token) ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error)

ToSealed wraps the invocation token in an envelope, generates the signature, encodes the result to DAG-CBOR and calculates the CID of the resulting binary data.

func (*Token) ToSealedWriter

func (t *Token) ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, error)

ToSealedWriter is the same as ToSealed but accepts an io.Writer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL