- go-varsig provides a varsig V1 implementation - go-did-it provides a complete and extensible DID implementation
286 lines
8.0 KiB
Go
286 lines
8.0 KiB
Go
// Package delegation implements the UCAN [delegation] 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.
|
|
//
|
|
// [delegation]: https://github.com/ucan-wg/delegation/tree/v1_ipld
|
|
// [envelope]: https://github.com/ucan-wg/spec#envelope
|
|
package delegation
|
|
|
|
// TODO: change the "delegation" link above when the specification is merged
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/MetaMask/go-did-it"
|
|
|
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
|
"github.com/ucan-wg/go-ucan/token/internal/nonce"
|
|
"github.com/ucan-wg/go-ucan/token/internal/parse"
|
|
)
|
|
|
|
// Token is an immutable type that holds the fields of a UCAN delegation.
|
|
type Token struct {
|
|
// Issuer DID (sender)
|
|
issuer did.DID
|
|
// Audience DID (receiver)
|
|
audience did.DID
|
|
// Principal that the chain is about (the Subject)
|
|
subject did.DID
|
|
// The Command to eventually invoke
|
|
command command.Command
|
|
// The delegation policy
|
|
policy policy.Policy
|
|
// A unique, random nonce
|
|
nonce []byte
|
|
// Arbitrary Metadata
|
|
meta *meta.Meta
|
|
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
|
notBefore *time.Time
|
|
// The timestamp at which the Invocation becomes invalid
|
|
expiration *time.Time
|
|
}
|
|
|
|
// New creates a validated delegation Token from the provided parameters and options.
|
|
// This is typically used to delegate a given power to another agent.
|
|
//
|
|
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on (subject)".
|
|
func New(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, sub did.DID, opts ...Option) (*Token, error) {
|
|
tkn := &Token{
|
|
issuer: iss,
|
|
audience: aud,
|
|
subject: sub,
|
|
command: cmd,
|
|
policy: pol,
|
|
meta: meta.NewMeta(),
|
|
nonce: nil,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
if err := opt(tkn); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var err error
|
|
if len(tkn.nonce) == 0 {
|
|
tkn.nonce, err = nonce.Generate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := tkn.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tkn, nil
|
|
}
|
|
|
|
// Root creates a validated UCAN delegation Token from the provided parameters and options.
|
|
// This is typically used to create and give power to an agent.
|
|
//
|
|
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on itself".
|
|
func Root(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
|
return New(iss, aud, cmd, pol, iss, opts...)
|
|
}
|
|
|
|
// Powerline creates a validated UCAN delegation Token from the provided parameters and options.
|
|
//
|
|
// Powerline is a pattern for automatically delegating all future delegations to another agent regardless of Subject.
|
|
// This is a very powerful pattern, use it only if you understand it.
|
|
// Powerline delegations MUST NOT be used as the root delegation to a resource
|
|
//
|
|
// A very common use case for Powerline is providing a stable DID across multiple agents (e.g. representing a user with
|
|
// multiple devices). This enables the automatic sharing of authority across their devices without needing to share keys
|
|
// or set up a threshold scheme. It is also flexible, since a Powerline delegation MAY be revoked.
|
|
//
|
|
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on anything".
|
|
func Powerline(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
|
return New(iss, aud, cmd, pol, nil, opts...)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Policy returns the capability's policy.Policy.
|
|
func (t *Token) Policy() policy.Policy {
|
|
return t.policy
|
|
}
|
|
|
|
// 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.ReadOnly {
|
|
return t.meta.ReadOnly()
|
|
}
|
|
|
|
// NotBefore returns the time at which the Token becomes "active".
|
|
func (t *Token) NotBefore() *time.Time {
|
|
return t.notBefore
|
|
}
|
|
|
|
// Expiration returns the time at which the Token expires.
|
|
func (t *Token) Expiration() *time.Time {
|
|
return t.expiration
|
|
}
|
|
|
|
// IsRoot tells if the token is a root delegation.
|
|
func (t *Token) IsRoot() bool {
|
|
return t.issuer.Equal(t.subject)
|
|
}
|
|
|
|
// IsPowerline tells if the token is a powerline delegation.
|
|
func (t *Token) IsPowerline() bool {
|
|
return t.subject == nil
|
|
}
|
|
|
|
// 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 {
|
|
if t.expiration != nil && ti.After(*t.expiration) {
|
|
return false
|
|
}
|
|
if t.notBefore != nil && ti.Before(*t.notBefore) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (t *Token) String() string {
|
|
var res strings.Builder
|
|
|
|
var kind string
|
|
switch {
|
|
case t.issuer == t.subject:
|
|
kind = " (root delegation)"
|
|
case t.subject == nil:
|
|
kind = " (powerline delegation)"
|
|
default:
|
|
kind = " (normal delegation)"
|
|
}
|
|
|
|
res.WriteString(fmt.Sprintf("Issuer: %s\n", t.Issuer()))
|
|
res.WriteString(fmt.Sprintf("Audience: %s\n", t.Audience()))
|
|
res.WriteString(fmt.Sprintf("Subject: %s%s\n", t.Subject(), kind))
|
|
res.WriteString(fmt.Sprintf("Command: %s\n", t.Command()))
|
|
res.WriteString(fmt.Sprintf("Policy: %s\n", t.Policy()))
|
|
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("NotBefore: %v\n", t.NotBefore()))
|
|
res.WriteString(fmt.Sprintf("Expiration: %v", t.Expiration()))
|
|
|
|
return res.String()
|
|
}
|
|
|
|
func (t *Token) validate() error {
|
|
var errs error
|
|
|
|
requiredDID := func(id did.DID, fieldname string) {
|
|
if id == nil {
|
|
errs = errors.Join(errs, fmt.Errorf(`a valid did is required for %s: %s`, fieldname, id.String()))
|
|
}
|
|
}
|
|
|
|
requiredDID(t.issuer, "Issuer")
|
|
requiredDID(t.audience, "Audience")
|
|
|
|
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
|
|
err error
|
|
)
|
|
|
|
tkn.issuer, err = did.Parse(m.Iss)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse iss: %w", err)
|
|
}
|
|
|
|
if tkn.audience, err = did.Parse(m.Aud); err != nil {
|
|
return nil, fmt.Errorf("parse audience: %w", err)
|
|
}
|
|
|
|
if tkn.subject, err = parse.OptionalDID(m.Sub); err != nil {
|
|
return nil, fmt.Errorf("parse subject: %w", err)
|
|
}
|
|
|
|
if tkn.command, err = command.Parse(m.Cmd); err != nil {
|
|
return nil, fmt.Errorf("parse command: %w", err)
|
|
}
|
|
|
|
if tkn.policy, err = policy.FromIPLD(m.Pol); err != nil {
|
|
return nil, fmt.Errorf("parse policy: %w", err)
|
|
}
|
|
|
|
if len(m.Nonce) == 0 {
|
|
return nil, fmt.Errorf("nonce is required")
|
|
}
|
|
tkn.nonce = m.Nonce
|
|
|
|
tkn.meta = m.Meta
|
|
if tkn.meta == nil {
|
|
tkn.meta = meta.NewMeta()
|
|
}
|
|
|
|
tkn.notBefore, err = parse.OptionalTimestamp(m.Nbf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse notBefore: %w", err)
|
|
}
|
|
|
|
tkn.expiration, err = parse.OptionalTimestamp(m.Exp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse expiration: %w", err)
|
|
}
|
|
|
|
if err := tkn.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &tkn, nil
|
|
}
|