token: add invocation partial stub
This commit is contained in:
135
token/invocation/invocation.go
Normal file
135
token/invocation/invocation.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// Package delegation 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.
|
||||||
|
//
|
||||||
|
// [envelope]: https://github.com/ucan-wg/spec#envelope
|
||||||
|
// [invocation]: https://github.com/ucan-wg/invocation
|
||||||
|
package invocation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Token is an immutable type that holds the fields of a UCAN invocation.
|
||||||
|
type Token struct {
|
||||||
|
// Issuer DID (invoker)
|
||||||
|
issuer did.DID
|
||||||
|
// Audience DID (receiver/executor)
|
||||||
|
audience did.DID
|
||||||
|
// Subject DID (subject being invoked)
|
||||||
|
subject did.DID
|
||||||
|
// The Command to invoke
|
||||||
|
command command.Command
|
||||||
|
// TODO: args
|
||||||
|
// TODO: prf
|
||||||
|
// A unique, random nonce
|
||||||
|
nonce []byte
|
||||||
|
// Arbitrary Metadata
|
||||||
|
meta *meta.Meta
|
||||||
|
// The timestamp at which the Invocation becomes invalid
|
||||||
|
expiration *time.Time
|
||||||
|
// The timestamp at which the Invocation was created
|
||||||
|
invokedAt *time.Time
|
||||||
|
// TODO: cause
|
||||||
|
// The CID of the Token when enclosed in an Envelope and encoded to DAG-CBOR
|
||||||
|
cid cid.Cid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issuer returns the did.DID representing the Token's issuer.
|
||||||
|
func (t *Token) Issuer() did.DID {
|
||||||
|
return t.issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audience returns the did.DID representing the Token's audience.
|
||||||
|
func (t *Token) Audience() did.DID {
|
||||||
|
return t.audience
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject returns the did.DID representing the Token's subject.
|
||||||
|
//
|
||||||
|
// This field may be did.Undef for delegations that are [Powerlined] but
|
||||||
|
// must be equal to the value returned by the Issuer method for root
|
||||||
|
// tokens.
|
||||||
|
func (t *Token) Subject() did.DID {
|
||||||
|
return t.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command returns the capability's command.Command.
|
||||||
|
func (t *Token) Command() command.Command {
|
||||||
|
return t.command
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nonce returns the random Nonce encapsulated in this Token.
|
||||||
|
func (t *Token) Nonce() []byte {
|
||||||
|
return t.nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meta returns the Token's metadata.
|
||||||
|
func (t *Token) Meta() *meta.Meta {
|
||||||
|
return t.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expiration returns the time at which the Token expires.
|
||||||
|
func (t *Token) Expiration() *time.Time {
|
||||||
|
return t.expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
// CID returns the content identifier of the Token model when enclosed
|
||||||
|
// in an Envelope and encoded to DAG-CBOR.
|
||||||
|
// Returns cid.Undef if the token has not been serialized or deserialized yet.
|
||||||
|
func (t *Token) CID() cid.Cid {
|
||||||
|
return t.cid
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
if len(t.nonce) < 12 {
|
||||||
|
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
return &tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateNonce creates a 12-byte random nonce.
|
||||||
|
// TODO: some crypto scheme require more, is that our case?
|
||||||
|
func generateNonce() ([]byte, error) {
|
||||||
|
res := make([]byte, 12)
|
||||||
|
_, err := rand.Read(res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
23
token/invocation/invocation.ipldsch
Normal file
23
token/invocation/invocation.ipldsch
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
type DID string
|
||||||
|
|
||||||
|
# The Invocation Payload attaches sender, receiver, and provenance to the Task.
|
||||||
|
type Payload struct {
|
||||||
|
# Issuer DID (sender)
|
||||||
|
iss DID
|
||||||
|
# Audience DID (receiver)
|
||||||
|
aud DID
|
||||||
|
# Principal that the chain is about (the Subject)
|
||||||
|
sub optional DID
|
||||||
|
|
||||||
|
# The Command to eventually invoke
|
||||||
|
cmd String
|
||||||
|
|
||||||
|
# A unique, random nonce
|
||||||
|
nonce Bytes
|
||||||
|
|
||||||
|
# Arbitrary Metadata
|
||||||
|
meta {String : Any}
|
||||||
|
|
||||||
|
# The timestamp at which the Invocation becomes invalid
|
||||||
|
exp nullable Int
|
||||||
|
}
|
||||||
226
token/invocation/ipld.go
Normal file
226
token/invocation/ipld.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package invocation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 (t *Token) ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error) {
|
||||||
|
data, err := t.ToDagCbor(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cid.Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := envelope.CIDFromBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cid.Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSealedWriter is the same as ToSealed but accepts an io.Writer.
|
||||||
|
func (t *Token) ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, error) {
|
||||||
|
cidWriter := envelope.NewCIDWriter(w)
|
||||||
|
|
||||||
|
if err := t.ToDagCborWriter(cidWriter, privKey); err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cidWriter.CID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 FromSealed(data []byte) (*Token, error) {
|
||||||
|
tkn, err := FromDagCbor(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := envelope.CIDFromBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn.cid = id
|
||||||
|
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromSealedReader is the same as Unseal but accepts an io.Reader.
|
||||||
|
func FromSealedReader(r io.Reader) (*Token, error) {
|
||||||
|
cidReader := envelope.NewCIDReader(r)
|
||||||
|
|
||||||
|
tkn, err := FromDagCborReader(cidReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := cidReader.CID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn.cid = id
|
||||||
|
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode marshals a Token to the format specified by the provided
|
||||||
|
// codec.Encoder.
|
||||||
|
func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) {
|
||||||
|
node, err := t.toIPLD(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipld.Encode(node, encFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeWriter is the same as Encode, but accepts an io.Writer.
|
||||||
|
func (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error {
|
||||||
|
node, err := t.toIPLD(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipld.EncodeStreaming(w, node, encFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDagCbor marshals the Token to the DAG-CBOR format.
|
||||||
|
func (t *Token) ToDagCbor(privKey crypto.PrivKey) ([]byte, error) {
|
||||||
|
return t.Encode(privKey, dagcbor.Encode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDagCborWriter is the same as ToDagCbor, but it accepts an io.Writer.
|
||||||
|
func (t *Token) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error {
|
||||||
|
return t.EncodeWriter(w, privKey, dagcbor.Encode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDagJson marshals the Token to the DAG-JSON format.
|
||||||
|
func (t *Token) ToDagJson(privKey crypto.PrivKey) ([]byte, error) {
|
||||||
|
return t.Encode(privKey, dagjson.Encode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
|
||||||
|
func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error {
|
||||||
|
return t.EncodeWriter(w, privKey, dagjson.Encode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 Decode(b []byte, decFn codec.Decoder) (*Token, error) {
|
||||||
|
node, err := ipld.Decode(b, decFn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return FromIPLD(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeReader is the same as Decode, but accept an io.Reader.
|
||||||
|
func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error) {
|
||||||
|
node, err := ipld.DecodeStreaming(r, decFn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return FromIPLD(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromDagCbor unmarshals the input data into a Token.
|
||||||
|
//
|
||||||
|
// An error is returned if the conversion fails, or if the resulting
|
||||||
|
// Token is invalid.
|
||||||
|
func FromDagCbor(data []byte) (*Token, error) {
|
||||||
|
pay, err := envelope.FromDagCbor[*tokenPayloadModel](data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn, err := tokenFromModel(*pay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tkn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
||||||
|
func FromDagCborReader(r io.Reader) (*Token, error) {
|
||||||
|
return DecodeReader(r, dagcbor.Decode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromDagJson unmarshals the input data into a Token.
|
||||||
|
//
|
||||||
|
// An error is returned if the conversion fails, or if the resulting
|
||||||
|
// Token is invalid.
|
||||||
|
func FromDagJson(data []byte) (*Token, error) {
|
||||||
|
return Decode(data, dagjson.Decode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
||||||
|
func FromDagJsonReader(r io.Reader) (*Token, error) {
|
||||||
|
return DecodeReader(r, dagjson.Decode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromIPLD decode the given IPLD representation into a Token.
|
||||||
|
func FromIPLD(node datamodel.Node) (*Token, error) {
|
||||||
|
pay, err := envelope.FromIPLD[*tokenPayloadModel](node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn, err := tokenFromModel(*pay)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tkn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
||||||
|
var sub *string
|
||||||
|
|
||||||
|
if t.subject != did.Undef {
|
||||||
|
s := t.subject.String()
|
||||||
|
sub = &s
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
var exp *int64
|
||||||
|
if t.expiration != nil {
|
||||||
|
u := t.expiration.Unix()
|
||||||
|
exp = &u
|
||||||
|
}
|
||||||
|
|
||||||
|
model := &tokenPayloadModel{
|
||||||
|
Iss: t.issuer.String(),
|
||||||
|
Aud: t.audience.String(),
|
||||||
|
Sub: sub,
|
||||||
|
Cmd: t.command.String(),
|
||||||
|
Nonce: t.nonce,
|
||||||
|
Meta: *t.meta,
|
||||||
|
Exp: exp,
|
||||||
|
}
|
||||||
|
|
||||||
|
return envelope.ToIPLD(privKey, model)
|
||||||
|
}
|
||||||
77
token/invocation/schema.go
Normal file
77
token/invocation/schema.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package invocation
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||||
|
"github.com/ipld/go-ipld-prime/schema"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||||
|
)
|
||||||
|
|
||||||
|
// [Tag] is the string used as a key within the SigPayload that identifies
|
||||||
|
// that the TokenPayload is an invocation.
|
||||||
|
//
|
||||||
|
// [Tag]: https://github.com/ucan-wg/invocation#type-tag
|
||||||
|
const Tag = "ucan/inv@1.0.0-rc.1"
|
||||||
|
|
||||||
|
//go:embed invocation.ipldsch
|
||||||
|
var schemaBytes []byte
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
ts *schema.TypeSystem
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustLoadSchema() *schema.TypeSystem {
|
||||||
|
once.Do(func() {
|
||||||
|
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
||||||
|
}
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadType() schema.Type {
|
||||||
|
return mustLoadSchema().TypeByName("Payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ envelope.Tokener = (*tokenPayloadModel)(nil)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
type tokenPayloadModel struct {
|
||||||
|
// Issuer DID (sender)
|
||||||
|
Iss string
|
||||||
|
// Audience DID (receiver)
|
||||||
|
Aud string
|
||||||
|
// Principal that the chain is about (the Subject)
|
||||||
|
// optional: can be nil
|
||||||
|
Sub *string
|
||||||
|
|
||||||
|
// The Command to eventually invoke
|
||||||
|
Cmd string
|
||||||
|
|
||||||
|
// A unique, random nonce
|
||||||
|
Nonce []byte
|
||||||
|
|
||||||
|
// Arbitrary Metadata
|
||||||
|
Meta meta.Meta
|
||||||
|
|
||||||
|
// The timestamp at which the Invocation becomes invalid
|
||||||
|
// optional: can be nil
|
||||||
|
Exp *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *tokenPayloadModel) Prototype() schema.TypedPrototype {
|
||||||
|
return bindnode.Prototype((*tokenPayloadModel)(nil), payloadType())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*tokenPayloadModel) Tag() string {
|
||||||
|
return Tag
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/invocation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Decode unmarshals the input data using the format specified by the
|
// Decode unmarshals the input data using the format specified by the
|
||||||
@@ -74,6 +75,8 @@ func fromIPLD(node datamodel.Node) (Token, error) {
|
|||||||
switch tag {
|
switch tag {
|
||||||
case delegation.Tag:
|
case delegation.Tag:
|
||||||
return delegation.FromIPLD(node)
|
return delegation.FromIPLD(node)
|
||||||
|
case invocation.Tag:
|
||||||
|
return invocation.FromIPLD(node)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf(`unknown tag "%s"`, tag)
|
return nil, fmt.Errorf(`unknown tag "%s"`, tag)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user