23 Commits
v1 ... envelope

Author SHA1 Message Date
Michael Muré
1ca17ea63d delegation: add decode function with an io.Reader 2024-09-17 16:51:26 +02:00
Steve Moyer
658794041e fix(delegation): make Expiration an Option 2024-09-17 08:08:13 -04:00
Steve Moyer
f85ece49fa feat(delegation): add validation/accessors 2024-09-16 17:18:16 -04:00
Steve Moyer
da9f2e7bec chore(ucan): clean up repository root directory 2024-09-16 14:02:01 -04:00
Steve Moyer
bd1775b2f5 Merge branch 'envelope' of github.com:ucan-wg/go-ucan into envelope 2024-09-16 13:55:23 -04:00
Steve Moyer
285cb5f6e7 feat(did): add accessor to report whether this DID is a did:key 2024-09-16 13:54:18 -04:00
Steve Moyer
4c05d866f2 fix(delegation): simplify package API and restore convenient encoding 2024-09-16 13:50:11 -04:00
Michael Muré
646127abe7 policy formatting 2024-09-16 11:07:57 +02:00
Steve Moyer
7cead1bf8d chore(envelope): refactor to decode token.Token to schema.TypedNode 2024-09-13 12:01:51 -04:00
Steve Moyer
16f0f38b43 feat(token): add generator for Go types included in schema 2024-09-11 09:39:32 -04:00
Steve Moyer
2f183aa6f4 docs(token): move schema comment to Go doc for package 2024-09-11 09:38:13 -04:00
Steve Moyer
6d1b7ee01f feat(envelope): no longer generic - handles only token.Token 2024-09-11 09:07:24 -04:00
Steve Moyer
599c5d30b0 feat(token) combined TokenPayload model for both Delegation and Invocation tokens 2024-09-11 08:50:24 -04:00
Steve Moyer
1c2f602f4d feat(did): add ToPubKey() and improve crypto tests 2024-09-11 07:12:54 -04:00
Steve Moyer
8441f99d5d fix(delegation): redelete view.go 2024-09-09 09:44:35 -04:00
Steve Moyer
1525aaa139 chore: fix module imports 2024-09-09 09:27:04 -04:00
Steve Moyer
ab4018d218 chore(v1): merge in other changes 2024-09-09 09:09:17 -04:00
Steve Moyer
39987eadaa feat(varsig): not specification compliant precalculated headers' 2024-09-09 08:56:33 -04:00
Steve Moyer
b77f8d6bb0 feat(ucan): functions to issue/delegate UCAN tokens 2024-09-09 08:55:14 -04:00
Steve Moyer
719837e3cd feat(envelope): wrap a Tokener in an Envelope 2024-09-09 08:52:57 -04:00
Steve Moyer
2205d5d4ce feat(did): add to/from public key 2024-09-09 08:50:15 -04:00
Steve Moyer
3a542ecc85 feat(delegation): use bindnode with typed prototype 2024-09-09 08:49:35 -04:00
Steve Moyer
ba1c45c088 chore(envelope): add codec/crypto dependencies 2024-09-04 08:55:32 -04:00
33 changed files with 1721 additions and 230 deletions

View File

@@ -0,0 +1,46 @@
package delegation
// Code generated by github.com/selesy/go-options. DO NOT EDIT.
import (
"github.com/ipld/go-ipld-prime/datamodel"
"time"
)
type Option func(c *config) error
func newConfig(options ...Option) (config, error) {
var c config
err := applyConfigOptions(&c, options...)
return c, err
}
func applyConfigOptions(c *config, options ...Option) error {
for _, o := range options {
if err := o(c); err != nil {
return err
}
}
return nil
}
func WithExpiration(o *time.Time) Option {
return func(c *config) error {
c.Expiration = o
return nil
}
}
func WithMeta(o map[string]datamodel.Node) Option {
return func(c *config) error {
c.Meta = o
return nil
}
}
func WithNotBefore(o *time.Time) Option {
return func(c *config) error {
c.NotBefore = o
return nil
}
}

225
delegation/delegation.go Normal file
View File

@@ -0,0 +1,225 @@
package delegation
import (
"crypto/rand"
"errors"
"fmt"
"time"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/capability/command"
"github.com/ucan-wg/go-ucan/capability/policy"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/internal/envelope"
"github.com/ucan-wg/go-ucan/internal/token"
)
const (
Tag = "ucan/dlg@1.0.0-rc.1"
)
type Delegation struct {
envel *envelope.Envelope
}
//go:generate -command options go run github.com/selesy/go-options
//go:generate options -type=config -prefix=With -output=delegatiom_options.go -cmp=false -stringer=false -imports=time,github.com/ipld/go-ipld-prime/datamodel
type config struct {
Expiration *time.Time
Meta map[string]datamodel.Node
NotBefore *time.Time
}
func New(privKey crypto.PrivKey, aud did.DID, sub *did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, opts ...Option) (*Delegation, error) {
cfg, err := newConfig(opts...)
if err != nil {
return nil, err
}
issuer, err := did.FromPrivKey(privKey)
if err != nil {
return nil, err
}
if !aud.Defined() {
return nil, fmt.Errorf("%w: %s", token.ErrMissingRequiredDID, "aud")
}
audience := aud.String()
var subject *string
if sub != nil {
s := sub.String()
subject = &s
}
policy, err := pol.ToIPLD()
if err != nil {
return nil, err
}
var meta *token.Map__String__Any
if len(cfg.Meta) > 0 {
m := token.ToIPLDMapStringAny(cfg.Meta)
meta = &m
}
var notBefore *int
if cfg.NotBefore != nil {
n := int(cfg.NotBefore.Unix())
notBefore = &n
}
var expiration *int
if cfg.Expiration != nil {
e := int(cfg.Expiration.Unix())
expiration = &e
}
tkn := &token.Token{
Issuer: issuer.String(),
Audience: &audience,
Subject: subject,
Command: cmd.String(),
Policy: &policy,
Nonce: &nonce,
Meta: meta,
NotBefore: notBefore,
Expiration: expiration,
}
envel, err := envelope.New(privKey, tkn, Tag)
if err != nil {
return nil, err
}
dlg := &Delegation{envel: envel}
if err := dlg.Validate(); err != nil {
return nil, err
}
return dlg, nil
}
func Root(privKey crypto.PrivKey, aud did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, opts ...Option) (*Delegation, error) {
sub, err := did.FromPrivKey(privKey)
if err != nil {
return nil, err
}
return New(privKey, aud, &sub, cmd, pol, nonce, opts...)
}
func (d *Delegation) Audience() did.DID {
id, _ := did.Parse(*d.envel.TokenPayload().Audience)
return id
}
func (d *Delegation) Command() *command.Command {
cmd, _ := command.Parse(d.envel.TokenPayload().Command)
return cmd
}
func (d *Delegation) IsPowerline() bool {
return d.envel.TokenPayload().Subject == nil
}
func (d *Delegation) IsRoot() bool {
return &d.envel.TokenPayload().Issuer == d.envel.TokenPayload().Subject
}
func (d *Delegation) Issuer() did.DID {
id, _ := did.Parse(d.envel.TokenPayload().Issuer)
return id
}
func (d *Delegation) Meta() map[string]datamodel.Node {
return d.envel.TokenPayload().Meta.Values
}
func (d *Delegation) Nonce() []byte {
return *d.envel.TokenPayload().Nonce
}
func (d *Delegation) Policy() policy.Policy {
pol, _ := policy.FromIPLD(*d.envel.TokenPayload().Policy)
return pol
}
func (d *Delegation) Subject() *did.DID {
if d.envel.TokenPayload().Subject == nil {
return nil
}
id, _ := did.Parse(*d.envel.TokenPayload().Subject)
return &id
}
func (d *Delegation) Validate() error {
return errors.Join(
d.validateDID("iss", &d.envel.TokenPayload().Issuer, false),
d.validateDID("aud", d.envel.TokenPayload().Audience, false),
d.validateDID("sub", d.envel.TokenPayload().Subject, true),
d.validateCommand(),
d.validatePolicy(),
d.validateNonce(),
)
}
func (d *Delegation) validateCommand() error {
_, err := command.Parse(d.envel.TokenPayload().Command)
return err
}
func (d *Delegation) validateDID(fieldName string, identity *string, nullableOrOptional bool) error {
if identity == nil && !nullableOrOptional {
return fmt.Errorf("a required DID is missing: %s", fieldName)
}
id, err := did.Parse(*identity)
if err != nil {
}
if !id.Defined() && !id.Key() {
return fmt.Errorf("a required DID is missing: %s", fieldName)
}
return nil
}
func (d *Delegation) validateNonce() error {
if d.envel.TokenPayload().Nonce == nil || len(*d.envel.TokenPayload().Nonce) < 1 {
return fmt.Errorf("nonce is required: must not be nil or empty")
}
return nil
}
func (d *Delegation) validatePolicy() error {
if d.envel.TokenPayload().Policy == nil {
return fmt.Errorf("the \"pol\" field is required")
}
_, err := policy.FromIPLD(*d.envel.TokenPayload().Policy)
return err
}
func Nonce() ([]byte, error) {
nonce := make([]byte, 32)
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
return nonce, nil
}

View File

@@ -1,29 +0,0 @@
type DID string
# The Delegation payload MUST describe the authorization claims, who is involved, and its validity period.
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
# The delegation policy
# It doesn't seem possible to represent it with a schema.
pol Any
# A unique, random nonce
nonce Bytes
# Arbitrary Metadata
meta {String : Any}
# "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
nbf optional Int
# The timestamp at which the Invocation becomes invalid
exp nullable Int
}

View File

@@ -0,0 +1,149 @@
package delegation_test
import (
"crypto/rand"
"testing"
"time"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/capability/command"
"github.com/ucan-wg/go-ucan/capability/policy"
"github.com/ucan-wg/go-ucan/delegation"
"github.com/ucan-wg/go-ucan/did"
"gotest.tools/v3/golden"
)
const (
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
AudiencePrivKeyCfg = "CAESQL1hvbXpiuk2pWr/XFbfHJcZNpJ7S90iTA3wSCTc/BPRneCwPnCZb6c0vlD6ytDWqaOt0HEOPYnqEpnzoBDprSM="
AudienceDID = "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv"
issuerPrivKeyCfg = "CAESQLSql38oDmQXIihFFaYIjb73mwbPsc7MIqn4o8PN4kRNnKfHkw5gRP1IV9b6d0estqkZayGZ2vqMAbhRixjgkDU="
issuerDID = "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"
subjectPrivKeyCfg = "CAESQL9RtjZ4dQBeXtvDe53UyvslSd64kSGevjdNiA1IP+hey5i/3PfRXSuDr71UeJUo1fLzZ7mGldZCOZL3gsIQz5c="
subjectDID = "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
subJectCmd = "/foo/bar"
subjectPol = `
[
[
"==",
".status",
"draft"
],
[
"all",
".reviewer",
[
"like",
".email",
"*@example.com"
]
],
[
"any",
".tags",
[
"or",
[
[
"==",
".",
"news"
],
[
"==",
".",
"press"
]
]
]
]
]
`
)
func TestConstructors(t *testing.T) {
t.Parallel()
privKey := privKey(t, issuerPrivKeyCfg)
aud, err := did.Parse(AudienceDID)
sub, err := did.Parse(subjectDID)
require.NoError(t, err)
cmd, err := command.Parse(subJectCmd)
require.NoError(t, err)
pol, err := policy.FromDagJson(subjectPol)
require.NoError(t, err)
exp := time.Time{}
meta := map[string]datamodel.Node{
"foo": basicnode.NewString("fooo"),
"bar": basicnode.NewString("barr"),
}
t.Run("New", func(t *testing.T) {
dlg, err := delegation.New(privKey, aud, &sub, cmd, pol, []byte(nonce), delegation.WithExpiration(&exp), delegation.WithMeta(meta))
require.NoError(t, err)
data, err := dlg.ToDagJson()
require.NoError(t, err)
t.Log(string(data))
golden.Assert(t, string(data), "new.dagjson")
})
t.Run("Root", func(t *testing.T) {
t.Parallel()
dlg, err := delegation.Root(privKey, aud, cmd, pol, []byte(nonce), delegation.WithExpiration(&exp), delegation.WithMeta(meta))
require.NoError(t, err)
data, err := dlg.ToDagJson()
require.NoError(t, err)
t.Log(string(data))
golden.Assert(t, string(data), "root.dagjson")
})
}
func privKey(t *testing.T, privKeyCfg string) crypto.PrivKey {
t.Helper()
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
require.NoError(t, err)
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
require.NoError(t, err)
return privKey
}
func TestKey(t *testing.T) {
t.Skip()
priv, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)
privMar, err := crypto.MarshalPrivateKey(priv)
require.NoError(t, err)
privCfg := crypto.ConfigEncodeKey(privMar)
t.Log(privCfg)
id, err := did.FromPubKey(priv.GetPublic())
require.NoError(t, err)
t.Log(id)
t.Fail()
}

View File

@@ -1,33 +1,113 @@
package delegation package delegation
import ( import (
"fmt"
"io"
"github.com/ipld/go-ipld-prime" "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/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ucan-wg/go-ucan/internal/envelope"
) )
func (p *PayloadModel) EncodeDagCbor() ([]byte, error) { // Encode marshals a Delegation to the format specified by the provided
return ipld.Marshal(dagcbor.Encode, p, PayloadType()) // codec.Encoder.
} func (d *Delegation) Encode(encFn codec.Encoder) ([]byte, error) {
node, err := d.ToIPLD()
func (p *PayloadModel) EncodeDagJson() ([]byte, error) {
return ipld.Marshal(dagjson.Encode, p, PayloadType())
}
func DecodeDagCbor(data []byte) (*PayloadModel, error) {
var p PayloadModel
_, err := ipld.Unmarshal(data, dagcbor.Decode, &p, PayloadType())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &p, nil
return ipld.Encode(node, encFn)
} }
func DecodeDagJson(data []byte) (*PayloadModel, error) { // ToDagCbor marshals the Delegation to the DAG-CBOR format.
var p PayloadModel func (d *Delegation) ToDagCbor() ([]byte, error) {
_, err := ipld.Unmarshal(data, dagjson.Decode, &p, PayloadType()) return d.Encode(dagcbor.Encode)
}
// ToDagJson marshals the Delegation to the DAG-JSON format.
func (d *Delegation) ToDagJson() ([]byte, error) {
return d.Encode(dagjson.Encode)
}
// ToIPLD wraps the Delegation in an IPLD datamodel.Node.
func (d *Delegation) ToIPLD() (datamodel.Node, error) {
return d.envel.Wrap()
}
// Decode unmarshals the input data using the format specified by the
// provided codec.Decoder into a Delegation.
//
// An error is returned if the conversion fails, or if the resulting
// Delegation is invalid.
func Decode(b []byte, decFn codec.Decoder) (*Delegation, error) {
node, err := ipld.Decode(b, decFn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &p, nil return FromIPLD(node)
}
// DecodeReader is the same as Decode, but accept an io.Reader.
func DecodeReader(r io.Reader, decFn codec.Decoder) (*Delegation, error) {
node, err := ipld.DecodeStreaming(r, decFn)
if err != nil {
return nil, err
}
return FromIPLD(node)
}
// FromDagCbor unmarshals the input data into a Delegation.
//
// An error is returned if the conversion fails, or if the resulting
// Delegation is invalid.
func FromDagCbor(data []byte) (*Delegation, error) {
return Decode(data, dagcbor.Decode)
}
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
func FromDagCborReader(r io.Reader) (*Delegation, error) {
return DecodeReader(r, dagcbor.Decode)
}
// FromDagJson unmarshals the input data into a Delegation.
//
// An error is returned if the conversion fails, or if the resulting
// Delegation is invalid.
func FromDagJson(data []byte) (*Delegation, error) {
return Decode(data, dagjson.Decode)
}
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
func FromDagJsonReader(r io.Reader) (*Delegation, error) {
return DecodeReader(r, dagjson.Decode)
}
// FromIPLD unwraps a Delegation from the provided IPLD datamodel.Node
//
// An error is returned if the conversion fails, or if the resulting
// Delegation is invalid.
func FromIPLD(node datamodel.Node) (*Delegation, error) {
envel, err := envelope.Unwrap(node)
if err != nil {
return nil, err
}
if envel.Tag() != Tag {
return nil, fmt.Errorf("wrong tag for TokenPayload: received %s but expected %s", envel.Tag(), Tag)
}
dlg := &Delegation{
envel: envel,
}
if err := dlg.Validate(); err != nil {
return nil, err
}
return dlg, nil
} }

101
delegation/encoding_test.go Normal file
View File

@@ -0,0 +1,101 @@
package delegation_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/delegation"
)
func TestEncodingRoundTrip(t *testing.T) {
const delegationJson = `
[
{
"/": {
"bytes": "QWr0Pk+sSWE1nszuBMQzggbHX4ofJb8QRdwrLJK/AGCx2p4s/xaCRieomfstDjsV4ezBzX1HARvcoNgdwDQ8Aw"
}
},
{
"h": {
"/": {
"bytes": "NO0BcQ"
}
},
"ucan/dlg@1.0.0-rc.1": {
"aud": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2",
"cmd": "/foo/bar",
"exp": -62135596800,
"iss": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2",
"meta": {
"bar": "barr",
"foo": "fooo"
},
"nbf": -62135596800,
"nonce": {
"/": {
"bytes": "X93ORvN1QIXrKPyEP5m5XoVK9VLX9nX8VV/+HlWrp9c"
}
},
"pol": [
[
"==",
".status",
"draft"
],
[
"all",
".reviewer",
[
"like",
".email",
"*@example.com"
]
],
[
"any",
".tags",
[
"or",
[
[
"==",
".",
"news"
],
[
"==",
".",
"press"
]
]
]
]
],
"sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
}
}
]
`
// format: dagJson --> Delegation --> dagCbor --> Delegation --> dagJson
// function: FromDagJson() ToDagCbor() FromDagCbor() ToDagJson()
p1, err := delegation.FromDagJson([]byte(delegationJson))
require.NoError(t, err)
cborBytes, err := p1.ToDagCbor()
require.NoError(t, err)
fmt.Println("cborBytes length", len(cborBytes))
fmt.Println("cbor", string(cborBytes))
p2, err := delegation.FromDagCbor(cborBytes)
require.NoError(t, err)
fmt.Println("read Cbor", p2)
readJson, err := p2.ToDagJson()
require.NoError(t, err)
fmt.Println("readJson length", len(readJson))
fmt.Println("json: ", string(readJson))
require.JSONEq(t, delegationJson, string(readJson))
}

View File

@@ -1,69 +0,0 @@
package delegation
import (
_ "embed"
"fmt"
"sync"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/schema"
)
//go:embed delegation.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")
}
type PayloadModel 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
// The delegation policy
Pol datamodel.Node
// A unique, random nonce
Nonce []byte
// Arbitrary Metadata
// optional: can be nil
Meta MetaModel
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
// optional: can be nil
Nbf *int64
// The timestamp at which the Invocation becomes invalid
// optional: can be nil
Exp *int64
}
type MetaModel struct {
Keys []string
Values map[string]datamodel.Node
}

1
delegation/testdata/new.dagjson vendored Normal file
View File

@@ -0,0 +1 @@
[{"/":{"bytes":"P2lPLfdMuZuc4NPZ0mbozU+/bn5xoWlJsu+Fvaxi4ICYXVJb9/wiTTht3WJEFqjxXLxfTl4BMZF3J1CNvMPqBg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":-62135596800,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"}}]

1
delegation/testdata/root.dagjson vendored Normal file
View File

@@ -0,0 +1 @@
[{"/":{"bytes":"0sjiwG9BOgpezz6qw5UiD+rqOeqFLn4+Qds1PvbnsUBoc3RhF6IVxIeoOXDh1ufv3RHaI/zg4wjYpUwAMpTACw"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":-62135596800,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"}}]

View File

@@ -1,87 +0,0 @@
package delegation
import (
"fmt"
"time"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ucan-wg/go-ucan/capability/command"
"github.com/ucan-wg/go-ucan/capability/policy"
"github.com/ucan-wg/go-ucan/did"
)
type View 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 map[string]datamodel.Node
// "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
}
// ViewFromModel build a decoded view of the raw IPLD data.
// This function also serves as validation.
func ViewFromModel(m PayloadModel) (*View, error) {
var view View
var err error
view.Issuer, err = did.Parse(m.Iss)
if err != nil {
return nil, fmt.Errorf("parse iss: %w", err)
}
view.Audience, err = did.Parse(m.Aud)
if err != nil {
return nil, fmt.Errorf("parse audience: %w", err)
}
if m.Sub != nil {
view.Subject, err = did.Parse(*m.Sub)
if err != nil {
return nil, fmt.Errorf("parse subject: %w", err)
}
} else {
view.Subject = did.Undef
}
view.Command, err = command.Parse(m.Cmd)
if err != nil {
return nil, fmt.Errorf("parse command: %w", err)
}
view.Policy, err = policy.FromIPLD(m.Pol)
if err != nil {
return nil, fmt.Errorf("parse policy: %w", err)
}
if len(m.Nonce) == 0 {
return nil, fmt.Errorf("nonce is required")
}
view.Nonce = m.Nonce
// TODO: copy?
view.Meta = m.Meta.Values
if m.Nbf != nil {
view.NotBefore = time.Unix(*m.Nbf, 0)
}
if m.Exp != nil {
view.Expiration = time.Unix(*m.Exp, 0)
}
return &view, nil
}

48
did/crypto.go Normal file
View File

@@ -0,0 +1,48 @@
package did
import (
"errors"
crypto "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/crypto/pb"
"github.com/multiformats/go-multicodec"
"github.com/multiformats/go-varint"
)
func FromPrivKey(privKey crypto.PrivKey) (DID, error) {
return FromPubKey(privKey.GetPublic())
}
func FromPubKey(pubKey crypto.PubKey) (DID, error) {
code, ok := map[pb.KeyType]multicodec.Code{
pb.KeyType_Ed25519: multicodec.Ed25519Pub,
pb.KeyType_RSA: multicodec.RsaPub,
pb.KeyType_Secp256k1: multicodec.Secp256k1Pub,
pb.KeyType_ECDSA: multicodec.Es256,
}[pubKey.Type()]
if !ok {
return Undef, errors.New("Blah")
}
buf := varint.ToUvarint(uint64(code))
pubBytes, err := pubKey.Raw()
if err != nil {
return Undef, err
}
return DID{
str: string(append(buf, pubBytes...)),
code: uint64(code),
key: true,
}, nil
}
func ToPubKey(s string) (crypto.PubKey, error) {
id, err := Parse(s)
if err != nil {
return nil, err
}
return id.PubKey()
}

51
did/crypto_test.go Normal file
View File

@@ -0,0 +1,51 @@
package did_test
import (
"testing"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/did"
)
const (
exampleDIDStr = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
examplePubKeyStr = "Lm/M42cB3HkUiODQsXRcweM6TByfzEHGO9ND274JcOY="
)
func TestFromPubKey(t *testing.T) {
t.Parallel()
id, err := did.FromPubKey(examplePubKey(t))
require.NoError(t, err)
require.Equal(t, exampleDID(t), id)
}
func TestToPubKey(t *testing.T) {
t.Parallel()
pubKey, err := did.ToPubKey(exampleDIDStr)
require.NoError(t, err)
require.Equal(t, examplePubKey(t), pubKey)
}
func exampleDID(t *testing.T) did.DID {
t.Helper()
id, err := did.Parse(exampleDIDStr)
require.NoError(t, err)
return id
}
func examplePubKey(t *testing.T) crypto.PubKey {
t.Helper()
pubKeyCfg, err := crypto.ConfigDecodeKey(examplePubKeyStr)
require.NoError(t, err)
pubKey, err := crypto.UnmarshalEd25519PublicKey(pubKeyCfg)
require.NoError(t, err)
return pubKey
}

View File

@@ -4,7 +4,9 @@ import (
"fmt" "fmt"
"strings" "strings"
crypto "github.com/libp2p/go-libp2p/core/crypto"
mbase "github.com/multiformats/go-multibase" mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-multicodec"
varint "github.com/multiformats/go-varint" varint "github.com/multiformats/go-varint"
) )
@@ -13,12 +15,16 @@ const KeyPrefix = "did:key:"
const DIDCore = 0x0d1d const DIDCore = 0x0d1d
const Ed25519 = 0xed const Ed25519 = 0xed
const RSA = uint64(multicodec.RsaPub)
var MethodOffset = varint.UvarintSize(uint64(DIDCore)) var MethodOffset = varint.UvarintSize(uint64(DIDCore))
//
// [did:key format]: https://w3c-ccg.github.io/did-method-key/
type DID struct { type DID struct {
key bool key bool
str string code uint64
str string
} }
// Undef can be used to represent a nil or undefined DID, using DID{} // Undef can be used to represent a nil or undefined DID, using DID{}
@@ -36,10 +42,36 @@ func (d DID) Bytes() []byte {
return []byte(d.str) return []byte(d.str)
} }
func (d DID) Code() uint64 {
return d.code
}
func (d DID) DID() DID { func (d DID) DID() DID {
return d return d
} }
func (d DID) Key() bool {
return d.key
}
func (d DID) PubKey() (crypto.PubKey, error) {
if !d.key {
return nil, fmt.Errorf("unsupported did type: %s", d.String())
}
unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{
multicodec.Ed25519Pub: crypto.UnmarshalEd25519PublicKey,
multicodec.RsaPub: crypto.UnmarshalRsaPublicKey,
multicodec.Secp256k1Pub: crypto.UnmarshalSecp256k1PublicKey,
multicodec.Es256: crypto.UnmarshalECDSAPublicKey,
}[multicodec.Code(d.code)]
if !ok {
return nil, fmt.Errorf("unsupported multicodec: %d", d.code)
}
return unmarshaler(d.Bytes()[varint.UvarintSize(d.code):])
}
// String formats the decentralized identity document (DID) as a string. // String formats the decentralized identity document (DID) as a string.
func (d DID) String() string { func (d DID) String() string {
if d.key { if d.key {
@@ -54,8 +86,8 @@ func Decode(bytes []byte) (DID, error) {
if err != nil { if err != nil {
return Undef, err return Undef, err
} }
if code == Ed25519 { if code == Ed25519 || code == RSA {
return DID{str: string(bytes), key: true}, nil return DID{str: string(bytes), code: code, key: true}, nil
} else if code == DIDCore { } else if code == DIDCore {
return DID{str: string(bytes)}, nil return DID{str: string(bytes)}, nil
} }
@@ -82,5 +114,5 @@ func Parse(str string) (DID, error) {
varint.PutUvarint(buf, DIDCore) varint.PutUvarint(buf, DIDCore)
suffix, _ := strings.CutPrefix(str, Prefix) suffix, _ := strings.CutPrefix(str, Prefix)
buf = append(buf, suffix...) buf = append(buf, suffix...)
return DID{str: string(buf)}, nil return DID{str: string(buf), code: DIDCore}, nil
} }

5
doc.go Normal file
View File

@@ -0,0 +1,5 @@
// Package ucan provides the core functionality required to grant and
// revoke privileges via [UCAN] tokens.
//
// [UCAN]: https://ucan.xyz
package ucan

16
go.mod
View File

@@ -1,8 +1,8 @@
module github.com/ucan-wg/go-ucan module github.com/ucan-wg/go-ucan
go 1.21 go 1.22.0
toolchain go1.22.1 toolchain go1.22.4
require ( require (
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
@@ -11,23 +11,31 @@ require (
github.com/libp2p/go-libp2p v0.36.2 github.com/libp2p/go-libp2p v0.36.2
github.com/multiformats/go-multibase v0.2.0 github.com/multiformats/go-multibase v0.2.0
github.com/multiformats/go-multicodec v0.9.0 github.com/multiformats/go-multicodec v0.9.0
github.com/multiformats/go-multihash v0.2.3
github.com/multiformats/go-varint v0.0.7 github.com/multiformats/go-varint v0.0.7
github.com/selesy/go-options v0.0.0-20240912020512-ed2658318e52
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
gotest.tools/v3 v3.5.1
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polydawn/refmt v0.89.0 // indirect github.com/polydawn/refmt v0.89.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
golang.org/x/crypto v0.25.0 // indirect golang.org/x/crypto v0.25.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/tools v0.25.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect

30
go.sum
View File

@@ -2,13 +2,19 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
@@ -23,6 +29,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
github.com/libp2p/go-libp2p v0.36.2 h1:BbqRkDaGC3/5xfaJakLV/BrpjlAuYqSB0lRvtzL3B/U= github.com/libp2p/go-libp2p v0.36.2 h1:BbqRkDaGC3/5xfaJakLV/BrpjlAuYqSB0lRvtzL3B/U=
github.com/libp2p/go-libp2p v0.36.2/go.mod h1:XO3joasRE4Eup8yCTTP/+kX+g92mOgRaadk46LmPhHY= github.com/libp2p/go-libp2p v0.36.2/go.mod h1:XO3joasRE4Eup8yCTTP/+kX+g92mOgRaadk46LmPhHY=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
@@ -33,6 +41,8 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ=
github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
@@ -48,6 +58,8 @@ github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/selesy/go-options v0.0.0-20240912020512-ed2658318e52 h1:poNWlojS+o3229ZuatLMzK9wFiLuLxo7O170Edggs0o=
github.com/selesy/go-options v0.0.0-20240912020512-ed2658318e52/go.mod h1:Cn8TrnJWCWd3dAmejFTpLN8tNVNKNoVVlZzL8ux5EWQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
@@ -63,13 +75,21 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvS
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
@@ -79,5 +99,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=

View File

@@ -0,0 +1,54 @@
package main
import (
"bytes"
"log/slog"
"os"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/node/bindnode"
)
const header = `// Code generated by internal/cmd/token - DO NOT EDIT.
package token
import "github.com/ipld/go-ipld-prime/datamodel"
`
func main() {
slog.Info("Generating Go types for token.ipldsch")
if err := Run(); err != nil {
slog.Error(err.Error())
slog.Error("Finished but failed to generate and write token_gen.go")
os.Exit(1)
}
slog.Info("Finished generating and writing token_gen.go")
os.Exit(0)
}
func Run() error {
schema, err := os.ReadFile("token.ipldsch")
if err != nil {
return err
}
slog.Debug(string(schema))
typeSystem, err := ipld.LoadSchemaBytes(schema)
if err != nil {
return err
}
buf := bytes.NewBufferString(header)
if err := bindnode.ProduceGoTypes(buf, typeSystem); err != nil {
return err
}
return os.WriteFile("token_gen.go", buf.Bytes(), 0o600)
}

View File

@@ -0,0 +1,309 @@
package envelope
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ipld/go-ipld-prime/node/bindnode"
crypto "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/crypto/pb"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/internal/token"
"github.com/ucan-wg/go-ucan/internal/varsig"
)
// [Envelope] is a signed enclosure for a UCAN v1 Token.
//
// While the types and functions in this package are not exported,
// the names used for types, fields, variables, etc generally use the
// names from the specification
//
// [Envelope]: https://github.com/ucan-wg/spec#envelope
type Envelope struct {
signature []byte
sigPayload *sigPayload
}
// New creates an Envelope containing a VarsigHeader and Signature for
// the data resulting from wrapping the provided Token in an IPLD
// datamodel.Node and encoding it using DAG-CBOR.
func New(privKey crypto.PrivKey, token *token.Token, tag string) (*Envelope, error) {
sigPayload, err := newSigPayload(privKey.Type(), token, tag)
if err != nil {
return nil, err
}
cbor, err := sigPayload.cbor()
if err != nil {
return nil, err
}
signature, err := privKey.Sign(cbor)
if err != nil {
return nil, err
}
return &Envelope{
signature: signature,
sigPayload: sigPayload,
}, nil
}
// Wrap is syntactic sugar for creating an Envelope and wrapping it as an
// IPLD datamodel.Node in a single operation.
//
// Since the Envelope itself isn't returned, use this method only when
// the IPLD datamodel.Node is used directly. If the Envelope is also
// required, use New followed by Envelope.Wrap to avoid the need to
// unwrap the newly created datamodel.Node.
func Wrap(privKey crypto.PrivKey, token *token.Token, tag string) (datamodel.Node, error) {
env, err := New(privKey, token, tag)
if err != nil {
return nil, err
}
return env.Wrap()
}
// Unwrap attempts to crate an Envelope from a datamodel.Node
//
// There are lots of ways that this can fail and therefore there are
// an almost excessive number of check included here and while
// attempting to extract the token.Token from one of the inner IPLD
// nodes.
func Unwrap(node datamodel.Node) (*Envelope, error) {
signatureNode, err := node.LookupByIndex(0)
if err != nil {
return nil, err
}
signature, err := signatureNode.AsBytes()
if err != nil {
return nil, err
}
sigPayloadNode, err := node.LookupByIndex(1)
if err != nil {
return nil, err
}
sigPayload, err := unwrapSigPayload(sigPayloadNode)
if err != nil {
return nil, err
}
envel := &Envelope{
signature: signature,
sigPayload: sigPayload,
}
if ok, err := envel.Verify(); !ok || err != nil {
return nil, fmt.Errorf("envelope was not signed by issuer")
}
return envel, nil
}
// Signature returns the cryptographic signature of the Envelope's
// SigPayload.
func (e *Envelope) Signature() []byte {
return e.signature
}
// Tag returns the key that's used to reference the TokenPayload within
// this Envelope.
func (e *Envelope) Tag() string {
return e.sigPayload.tag
}
// TokenPayload returns the *token.Token enclosed within this Envelope.
func (e *Envelope) TokenPayload() *token.Token {
return e.sigPayload.tokenPayload
}
// VarsigHeader is an accessor that returns the [VarsigHeader] from the
// underlying [SigPayload] from the [Envelope].
//
// [Envelope]: https://github.com/ucan-wg/spec#envelope
// [SigPayload]: https://github.com/ucan-wg/spec#envelope
// [VarsigHeader]: https://github.com/ucan-wg/spec#envelope
func (e *Envelope) VarsigHeader() []byte {
return e.sigPayload.varsigHeader
}
// Verify checks that the [Envelope]'s signature is correct for the
// data created by encoding the SigPayload as DAG-CBOR and the public
// key passed as the only argument.
//
// Note that for Delegation and Invocation tokens, the public key
// is retrieved from the DID's method specific identifier for the
// Issuer field.
//
// [Envelope]: https://github.com/ucan-wg/spec#envelope
func (e *Envelope) Verify() (bool, error) {
pubKey, err := did.ToPubKey(e.sigPayload.tokenPayload.Issuer)
if err != nil {
return false, err
}
cbor, err := e.sigPayload.cbor()
if err != nil {
return false, err
}
return pubKey.Verify(cbor, e.signature)
}
// Wrap encodes the Envelope as an IPLD datamodel.Node.
func (e *Envelope) Wrap() (datamodel.Node, error) {
spn, err := e.sigPayload.wrap()
if err != nil {
return nil, err
}
return qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
qp.ListEntry(la, qp.Bytes(e.signature))
qp.ListEntry(la, qp.Node(spn))
})
}
//
// The types below are strictly to make it easier to Wrap and Unwrap the
// Envelope with an IPLD datamodel.Node. The Envelope itself provides
// accessors to the internals of these types.
//
type sigPayload struct {
varsigHeader []byte
tokenPayload *token.Token
tag string
}
func newSigPayload(keyType pb.KeyType, token *token.Token, tag string) (*sigPayload, error) {
varsigHeader, err := varsig.Encode(keyType)
if err != nil {
return nil, err
}
return &sigPayload{
varsigHeader: varsigHeader,
tokenPayload: token,
tag: tag,
}, nil
}
func unwrapSigPayload(node datamodel.Node) (*sigPayload, error) {
// Normally we could look up the VarsigHeader and TokenPayload using
// node.LookupByString() - this works for the "h" key used for the
// VarsigHeader but not for the TokenPayload's key (tag) as all we
// know is that it starts with "ucan/" and as explained below, must
// decode to a schema.TypedNode for the representation provided by the
// token.Prototype().
// vvv
mi := node.MapIterator()
if mi == nil {
return nil, fmt.Errorf("the SigPayload node is not a map: %s", node.Kind().String())
}
var (
hdrNode datamodel.Node
tknNode datamodel.Node
tag string
)
keyCount := 0
for !mi.Done() {
k, v, err := mi.Next()
if err != nil {
return nil, err
}
kStr, err := k.AsString()
if err != nil {
return nil, fmt.Errorf("the SigPayload keys are not strings: %w", err)
}
keyCount++
if kStr == "h" {
hdrNode = v
continue
}
if strings.HasPrefix(kStr, "ucan/") {
tknNode = v
tag = kStr
}
}
if keyCount != 2 {
return nil, fmt.Errorf("the SigPayload map should have exactly two keys: %d", keyCount)
}
// ^^^
// Replaces the datamodel.Node in tokenPayloadNode with a
// schema.TypedNode so that we can cast it to a *token.Token after
// unwrapping it.
// vvv
nb := token.Prototype().Representation().NewBuilder()
err := nb.AssignNode(tknNode)
if err != nil {
return nil, err
}
tknNode = nb.Build()
// ^^^
tokenPayload := bindnode.Unwrap(tknNode)
if tokenPayload == nil {
return nil, errors.New("failed to Unwrap the TokenPayload")
}
tkn, ok := tokenPayload.(*token.Token)
if !ok {
return nil, errors.New("failed to assert the TokenPayload type as *token.Token")
}
hdr, err := hdrNode.AsBytes()
if err != nil {
return nil, err
}
return &sigPayload{
varsigHeader: hdr,
tokenPayload: tkn,
tag: tag,
}, nil
}
func (sp *sigPayload) cbor() ([]byte, error) {
node, err := sp.wrap()
if err != nil {
return nil, err
}
buf := &bytes.Buffer{}
if err = dagcbor.Encode(node, buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (sp *sigPayload) wrap() (datamodel.Node, error) {
tpn := bindnode.Wrap(sp.tokenPayload, token.Prototype().Type())
return qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, "h", qp.Bytes(sp.varsigHeader))
qp.MapEntry(ma, sp.tag, qp.Node(tpn.Representation()))
})
}

View File

@@ -0,0 +1,200 @@
package envelope_test
import (
"encoding/base64"
"testing"
"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/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/node/basicnode"
crypto "github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/internal/envelope"
"github.com/ucan-wg/go-ucan/internal/token"
"gotest.tools/v3/golden"
)
const (
exampleDID = "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"
examplePrivKeyCfg = "CAESQP9v2uqECTuIi45dyg3znQvsryvf2IXmOF/6aws6aCehm0FVrj0zHR5RZSDxWNjcpcJqsGym3sjCungX9Zt5oA4="
exampleSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg"
exampleTag = "ucan/example@v1.0.0-rc.1"
exampleVarsigHeaderStr = "NO0BcQ"
invalidSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBK"
exampleDAGCBORFilename = "example.dagcbor"
exampleDAGJSONFilename = "example.dagjson"
)
func TestNew(t *testing.T) {
t.Parallel()
envel := exampleEnvelope(t)
assert.NotZero(t, envel)
assert.Equal(t, exampleSignature(t), envel.Signature())
assert.Equal(t, exampleTag, envel.Tag())
assert.Equal(t, exampleVarsigHeader(t), envel.VarsigHeader())
assert.EqualValues(t, exampleGoldenTokenPayload(t), envel.TokenPayload())
}
func TestWrap(t *testing.T) {
t.Parallel()
node, err := envelope.Wrap(examplePrivKey(t), exampleToken(t), exampleTag)
require.NoError(t, err)
cbor, err := ipld.Encode(node, dagcbor.Encode)
require.NoError(t, err)
golden.AssertBytes(t, cbor, exampleDAGCBORFilename)
json, err := ipld.Encode(node, dagjson.Encode)
require.NoError(t, err)
golden.Assert(t, string(json), exampleDAGJSONFilename)
}
func TestEnvelope_Verify(t *testing.T) {
t.Parallel()
t.Run("valid signature by issuer", func(t *testing.T) {
t.Parallel()
envel := exampleEnvelope(t)
ok, err := envel.Verify()
require.NoError(t, err)
assert.True(t, ok)
})
t.Run("invalid signature by wrong issuer", func(t *testing.T) {
t.Parallel()
envel, err := envelope.Unwrap(invalidNodeFromGolden(t))
require.NoError(t, err)
ok, _ := envel.Verify()
assert.False(t, ok)
})
}
func TestEnvelope_Wrap(t *testing.T) {
t.Parallel()
envel := exampleEnvelope(t)
node, err := envel.Wrap()
require.NoError(t, err)
cbor, err := ipld.Encode(node, dagcbor.Encode)
require.NoError(t, err)
assert.Equal(t, golden.Get(t, exampleDAGCBORFilename), cbor)
}
func exampleGoldenEnvelope(t *testing.T) *envelope.Envelope {
t.Helper()
envel, err := envelope.Unwrap(exampleGoldenNode(t))
require.NoError(t, err)
return envel
}
func exampleGoldenNode(t *testing.T) datamodel.Node {
t.Helper()
cbor := golden.Get(t, exampleDAGCBORFilename)
node, err := ipld.Decode(cbor, dagcbor.Decode)
require.NoError(t, err)
return node
}
func exampleGoldenTokenPayload(t *testing.T) *token.Token {
t.Helper()
return exampleGoldenEnvelope(t).TokenPayload()
}
func examplePrivKey(t *testing.T) crypto.PrivKey {
t.Helper()
privKeyEnc, err := crypto.ConfigDecodeKey(examplePrivKeyCfg)
require.NoError(t, err)
privKey, err := crypto.UnmarshalPrivateKey(privKeyEnc)
require.NoError(t, err)
return privKey
}
func exampleEnvelope(t *testing.T) *envelope.Envelope {
t.Helper()
envel, err := envelope.New(examplePrivKey(t), exampleToken(t), exampleTag)
require.NoError(t, err)
return envel
}
func examplePubKey(t *testing.T) crypto.PubKey {
t.Helper()
return examplePrivKey(t).GetPublic()
}
func exampleSignature(t *testing.T) []byte {
t.Helper()
sig, err := base64.RawStdEncoding.DecodeString(exampleSignatureStr)
require.NoError(t, err)
return sig
}
func exampleToken(t *testing.T) *token.Token {
t.Helper()
id, err := did.FromPubKey(examplePubKey(t))
require.NoError(t, err)
return &token.Token{
Issuer: id.String(),
}
}
func exampleVarsigHeader(t *testing.T) []byte {
t.Helper()
hdr, err := base64.RawStdEncoding.DecodeString(exampleVarsigHeaderStr)
require.NoError(t, err)
return hdr
}
func invalidNodeFromGolden(t *testing.T) datamodel.Node {
t.Helper()
invalidSig, err := base64.RawStdEncoding.DecodeString(invalidSignatureStr)
require.NoError(t, err)
envelNode := exampleGoldenNode(t)
sigPayloadNode, err := envelNode.LookupByIndex(1)
require.NoError(t, err)
node, err := qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
qp.ListEntry(la, qp.Bytes(invalidSig))
qp.ListEntry(la, qp.Node(sigPayloadNode))
})
require.NoError(t, err)
return node
}

View File

@@ -0,0 +1 @@
X@=•zfˆîŒ— ©Ê¦Z.ÈÚàP óú¤Jr=n¬;¡)Dñ÷™¹6îB;ò

View File

@@ -0,0 +1,20 @@
[
{
"/": {
"bytes": "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg"
}
},
{
"h": {
"/": {
"bytes": "NO0BcQ"
}
},
"ucan/example@v1.0.0-rc.1": {
"cmd": "",
"exp": null,
"iss": "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh",
"sub": null
}
}
]

View File

@@ -0,0 +1,24 @@
package token
import (
"github.com/ipld/go-ipld-prime/datamodel"
)
func ToIPLDMapStringAny(m map[string]datamodel.Node) Map__String__Any {
keys := make([]string, len(m))
i := 0
for k := range m {
keys[i] = k
i++
}
return Map__String__Any{
Keys: keys,
Values: m,
}
}
func FromIPLDMapStringAny(m Map__String__Any) map[string]datamodel.Node {
return m.Values
}

33
internal/token/doc.go Normal file
View File

@@ -0,0 +1,33 @@
// Package token provides a generic model of the [TokenPayload] required
// within an Envelope.
//
// # Field requirements
//
// While the Token object represents the wire format of both a UCAN
// Delegation token and a UCAN Invocation token, the delegation and
// invocation packages are, respectively, responsible for making sure
// required fields are included when creating a new Token or when
// validating the contents of an Envelope as it's received from
// another party. The following table shows the current (as of
// 2024-09-11) relationship between optional and nullable fields in
// the delegation and invocation views and the payload model:
//
// | Name | Delegation | Invocation | Token |
// | | Required | Nullable | Required | Nullable | |
// | ----- | -------- | -------- | -------- | -------- | -------- |
// | iss | Yes | No | Yes | No | |
// | aud | Yes | No | No | N/A | Optional |
// | sub | Yes | Yes | Yes | No | Nullable |
// | cmd | Yes | No | Yes | No | |
// | pol | Yes | No | X | X | Optional |
// | nonce | Yes | No | No | N/A | Optional |
// | meta | No | N/A | No | N/A | Optional |
// | nbf | No | N/A | X | X | Optional |
// | exp | Yes | Yes | Yes | Yes | |
// | args | X | X | Yes | No | Optional |
// | prf | X | X | Yes | No | Optional |
// | iat | X | X | No | N/A | Optional |
// | cause | X | X | No | N/A | Optional |
//
// [TokenPayload]: https://github.com/ucan-wg/spec?tab=readme-ov-file#envelope
package token

11
internal/token/errors.go Normal file
View File

@@ -0,0 +1,11 @@
package token
import "errors"
var ErrFailedSchemaLoad = errors.New("failed to load IPLD Schema")
var ErrNoSchemaType = errors.New("schema does not contain type")
var ErrNodeNotToken = errors.New("IPLD node is not a Token")
var ErrMissingRequiredDID = errors.New("a required DID is missing")

46
internal/token/schema.go Normal file
View File

@@ -0,0 +1,46 @@
package token
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"
)
const tokenTypeName = "Token"
//go:embed token.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("%w: %w", ErrFailedSchemaLoad, err))
}
tknType := ts.TypeByName(tokenTypeName)
if tknType == nil {
panic(fmt.Errorf("%w: %s", ErrNoSchemaType, tokenTypeName))
}
return ts
}
func tokenType() schema.Type {
return mustLoadSchema().TypeByName(tokenTypeName)
}
func Prototype() schema.TypedPrototype {
return bindnode.Prototype((*Token)(nil), tokenType())
}

View File

@@ -1,13 +1,20 @@
package delegation package token_test
import ( import (
_ "embed"
"fmt" "fmt"
"testing" "testing"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/internal/token"
) )
//go:embed token.ipldsch
var schemaBytes []byte
func TestSchemaRoundTrip(t *testing.T) { func TestSchemaRoundTrip(t *testing.T) {
const delegationJson = ` const delegationJson = `
{ {
@@ -40,22 +47,27 @@ func TestSchemaRoundTrip(t *testing.T) {
"sub":"" "sub":""
} }
` `
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson // format: dagJson --> IPLD node --> token --> dagCbor --> IPLD node --> dagJson
// function: DecodeDagJson() EncodeDagCbor() DecodeDagCbor() EncodeDagJson() // function: Unwrap() Wrap()
p1, err := DecodeDagJson([]byte(delegationJson)) n1, err := ipld.DecodeUsingPrototype([]byte(delegationJson), dagjson.Decode, token.Prototype())
require.NoError(t, err) require.NoError(t, err)
cborBytes, err := p1.EncodeDagCbor() cborBytes, err := ipld.Encode(n1, dagcbor.Encode)
require.NoError(t, err) require.NoError(t, err)
fmt.Println("cborBytes length", len(cborBytes)) fmt.Println("cborBytes length", len(cborBytes))
fmt.Println("cbor", string(cborBytes)) fmt.Println("cbor", string(cborBytes))
p2, err := DecodeDagCbor(cborBytes) n2, err := ipld.DecodeUsingPrototype(cborBytes, dagcbor.Decode, token.Prototype())
require.NoError(t, err) require.NoError(t, err)
fmt.Println("read Cbor", p2) fmt.Println("read Cbor", n2)
readJson, err := p2.EncodeDagJson() t1, err := token.Unwrap(n2)
require.NoError(t, err)
n3 := t1.Wrap()
readJson, err := ipld.Encode(n3, dagjson.Encode)
require.NoError(t, err) require.NoError(t, err)
fmt.Println("readJson length", len(readJson)) fmt.Println("readJson length", len(readJson))
fmt.Println("json: ", string(readJson)) fmt.Println("json: ", string(readJson))
@@ -65,6 +77,7 @@ func TestSchemaRoundTrip(t *testing.T) {
func BenchmarkSchemaLoad(b *testing.B) { func BenchmarkSchemaLoad(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, _ = ipld.LoadSchemaBytes(schemaBytes) _, _ = ipld.LoadSchemaBytes(schemaBytes)
} }

33
internal/token/token.go Normal file
View File

@@ -0,0 +1,33 @@
package token
import (
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/bindnode"
)
//go:generate go run ../cmd/token/...
// Unwrap creates a Token from an arbitrary IPLD node or returns an
// error if at least the required model fields are not present.
//
// It is the responsibility of the Delegation and Invocation views
// to further validate the presence of the required fields and the
// content as needed.
func Unwrap(node datamodel.Node) (*Token, error) {
iface := bindnode.Unwrap(node)
if iface == nil {
return nil, ErrNodeNotToken
}
tkn, ok := iface.(*Token)
if !ok {
return nil, ErrNodeNotToken
}
return tkn, nil
}
// Wrap creates an IPLD node representing the Token.
func (t *Token) Wrap() datamodel.Node {
return bindnode.Wrap(t, tokenType())
}

View File

@@ -0,0 +1,63 @@
type CID string
type Command string
type DID string
# Field requirements:
#
# | Name | Delegation | Invocation | Token |
# | | Required | Nullable | Required | Nullable | |
# | ----- | -------- | -------- | -------- | -------- | -------- |
# | iss | Yes | No | Yes | No | |
# | aud | Yes | No | No | N/A | Optional |
# | sub | Yes | Yes | Yes | No | Nullable |
# | cmd | Yes | No | Yes | No | |
# | pol | Yes | No | X | X | Optional |
# | nonce | Yes | No | No | N/A | Optional |
# | meta | No | N/A | No | N/A | Optional |
# | nbf | No | N/A | X | X | Optional |
# | exp | Yes | Yes | Yes | Yes | Nullable |
# | args | X | X | Yes | No | Optional |
# | prf | X | X | Yes | No | Optional |
# | iat | X | X | No | N/A | Optional |
# | cause | X | X | No | N/A | Optional |
type Token struct {
# Issuer DID (sender)
issuer DID (rename "iss")
# Audience DID (receiver)
audience optional DID (rename "aud")
# Principal that the chain is about (the Subject)
subject nullable DID (rename "sub")
# The Command to eventually invoke
command Command (rename "cmd")
# The delegation policy
# It doesn't seem possible to represent it with a schema.
policy optional Any (rename "pol")
# The invocation's arguments
arguments optional {String: Any} (rename "args")
# Delegations that prove the chain of authority
Proofs optional [CID] (rename "prf")
# A unique, random nonce
nonce optional Bytes
# Arbitrary Metadata
meta optional {String : Any}
# "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
notBefore optional Int (rename "nbf")
# The timestamp at which the delegation becomes invalid
expiration nullable Int (rename "exp")
# The timestamp at which the invocation was created
issuedAt optional Int
# An optional CID of the receipt that enqueued this invocation
cause optional CID
}

View File

@@ -0,0 +1,31 @@
// Code generated by internal/cmd/token - DO NOT EDIT.
package token
import "github.com/ipld/go-ipld-prime/datamodel"
type Map struct {
Keys []string
Values map[string]datamodel.Node
}
type List []datamodel.Node
type Map__String__Any struct {
Keys []string
Values map[string]datamodel.Node
}
type List__CID []string
type Token struct {
Issuer string
Audience *string
Subject *string
Command string
Policy *datamodel.Node
Arguments *Map__String__Any
Proofs *List__CID
Nonce *[]uint8
Meta *Map__String__Any
NotBefore *int
Expiration *int
IssuedAt *int
Cause *string
}

View File

@@ -0,0 +1,55 @@
package token_test
import (
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/internal/token"
)
func TestEncode(t *testing.T) {
t.Parallel()
tkn := &token.Token{}
node := tkn.Wrap()
json, err := ipld.Encode(node, dagjson.Encode)
require.NoError(t, err)
t.Log(string(json))
t.Fail()
}
func TestPrototype(t *testing.T) {
t.Parallel()
tkn := &token.Token{
Issuer: "blah",
}
n1 := tkn.Wrap()
json, err := ipld.Encode(n1, dagjson.Encode)
require.NoError(t, err)
t.Log(string(json))
n2, err := ipld.Decode(json, dagjson.Decode)
require.NoError(t, err)
nb := token.Prototype().Representation().NewBuilder()
require.NoError(t, nb.AssignNode(n2))
n3 := nb.Build()
tkn2, err := token.Unwrap(n3)
require.NoError(t, err)
t.Log(tkn2)
require.Equal(t, tkn, tkn2)
t.Fail()
}

7
internal/tools/tools.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build tools
package tools
import (
_ "github.com/selesy/go-options"
)

View File

@@ -58,7 +58,7 @@ var (
// //
// [go-libp2p/core/crypto]: github.com/libp2p/go-libp2p/core/crypto // [go-libp2p/core/crypto]: github.com/libp2p/go-libp2p/core/crypto
func Decode(header []byte) (pb.KeyType, error) { func Decode(header []byte) (pb.KeyType, error) {
keyType, ok := decMap[string(header)] keyType, ok := decMap[base64.RawStdEncoding.EncodeToString(header)]
if !ok { if !ok {
return -1, fmt.Errorf("%w: %s", ErrUnknownHeader, header) return -1, fmt.Errorf("%w: %s", ErrUnknownHeader, header)
} }
@@ -82,10 +82,10 @@ func Encode(keyType pb.KeyType) ([]byte, error) {
return []byte(header), nil return []byte(header), nil
} }
func keyTypeToHeader() map[pb.KeyType]string { func keyTypeToHeader() map[pb.KeyType][]byte {
const rsaSigLen = 0x100 const rsaSigLen = 0x100
return map[pb.KeyType]string{ return map[pb.KeyType][]byte{
pb.KeyType_RSA: header( pb.KeyType_RSA: header(
Prefix, Prefix,
multicodec.RsaPub, multicodec.RsaPub,
@@ -117,18 +117,18 @@ func headerToKeyType() map[string]pb.KeyType {
out := make(map[string]pb.KeyType, len(encMap)) out := make(map[string]pb.KeyType, len(encMap))
for keyType, header := range encMap { for keyType, header := range encMap {
out[header] = keyType out[base64.RawStdEncoding.EncodeToString(header)] = keyType
} }
return out return out
} }
func header(vals ...multicodec.Code) string { func header(vals ...multicodec.Code) []byte {
var buf []byte var buf []byte
for _, val := range vals { for _, val := range vals {
buf = binary.AppendUvarint(buf, uint64(val)) buf = binary.AppendUvarint(buf, uint64(val))
} }
return base64.RawStdEncoding.EncodeToString(buf) return buf
} }

View File

@@ -21,7 +21,14 @@ func TestDecode(t *testing.T) {
} }
func ExampleDecode() { func ExampleDecode() {
keyType, _ := varsig.Decode([]byte("NIUkEoACcQ")) hdr, err := base64.RawStdEncoding.DecodeString("NIUkEoACcQ")
if err != nil {
fmt.Println(err.Error())
return
}
keyType, _ := varsig.Decode(hdr)
fmt.Println(keyType.String()) fmt.Println(keyType.String())
// Output: // Output:
// RSA // RSA
@@ -37,7 +44,7 @@ func TestEncode(t *testing.T) {
func ExampleEncode() { func ExampleEncode() {
header, _ := varsig.Encode(pb.KeyType_RSA) header, _ := varsig.Encode(pb.KeyType_RSA)
fmt.Println(string(header)) fmt.Println(base64.RawStdEncoding.EncodeToString(header))
// Output: // Output:
// NIUkEoACcQ // NIUkEoACcQ