Compare commits
23 Commits
v1-fix-pol
...
envelope
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ca17ea63d | ||
|
|
658794041e | ||
|
|
f85ece49fa | ||
|
|
da9f2e7bec | ||
|
|
bd1775b2f5 | ||
|
|
285cb5f6e7 | ||
|
|
4c05d866f2 | ||
|
|
646127abe7 | ||
|
|
7cead1bf8d | ||
|
|
16f0f38b43 | ||
|
|
2f183aa6f4 | ||
|
|
6d1b7ee01f | ||
|
|
599c5d30b0 | ||
|
|
1c2f602f4d | ||
|
|
8441f99d5d | ||
|
|
1525aaa139 | ||
|
|
ab4018d218 | ||
|
|
39987eadaa | ||
|
|
b77f8d6bb0 | ||
|
|
719837e3cd | ||
|
|
2205d5d4ce | ||
|
|
3a542ecc85 | ||
|
|
ba1c45c088 |
46
delegation/delegatiom_options.go
Normal file
46
delegation/delegatiom_options.go
Normal 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
225
delegation/delegation.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
149
delegation/delegation_test.go
Normal file
149
delegation/delegation_test.go
Normal 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()
|
||||
}
|
||||
@@ -1,33 +1,113 @@
|
||||
package delegation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"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/ucan-wg/go-ucan/internal/envelope"
|
||||
)
|
||||
|
||||
func (p *PayloadModel) EncodeDagCbor() ([]byte, error) {
|
||||
return ipld.Marshal(dagcbor.Encode, p, PayloadType())
|
||||
}
|
||||
|
||||
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())
|
||||
// Encode marshals a Delegation to the format specified by the provided
|
||||
// codec.Encoder.
|
||||
func (d *Delegation) Encode(encFn codec.Encoder) ([]byte, error) {
|
||||
node, err := d.ToIPLD()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
|
||||
return ipld.Encode(node, encFn)
|
||||
}
|
||||
|
||||
func DecodeDagJson(data []byte) (*PayloadModel, error) {
|
||||
var p PayloadModel
|
||||
_, err := ipld.Unmarshal(data, dagjson.Decode, &p, PayloadType())
|
||||
// ToDagCbor marshals the Delegation to the DAG-CBOR format.
|
||||
func (d *Delegation) ToDagCbor() ([]byte, error) {
|
||||
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 {
|
||||
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
101
delegation/encoding_test.go
Normal 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))
|
||||
}
|
||||
@@ -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
1
delegation/testdata/new.dagjson
vendored
Normal 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
1
delegation/testdata/root.dagjson
vendored
Normal 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"}}]
|
||||
@@ -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
48
did/crypto.go
Normal 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
51
did/crypto_test.go
Normal 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
|
||||
}
|
||||
42
did/did.go
42
did/did.go
@@ -4,7 +4,9 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||
mbase "github.com/multiformats/go-multibase"
|
||||
"github.com/multiformats/go-multicodec"
|
||||
varint "github.com/multiformats/go-varint"
|
||||
)
|
||||
|
||||
@@ -13,12 +15,16 @@ const KeyPrefix = "did:key:"
|
||||
|
||||
const DIDCore = 0x0d1d
|
||||
const Ed25519 = 0xed
|
||||
const RSA = uint64(multicodec.RsaPub)
|
||||
|
||||
var MethodOffset = varint.UvarintSize(uint64(DIDCore))
|
||||
|
||||
//
|
||||
// [did:key format]: https://w3c-ccg.github.io/did-method-key/
|
||||
type DID struct {
|
||||
key bool
|
||||
str string
|
||||
key bool
|
||||
code uint64
|
||||
str string
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func (d DID) Code() uint64 {
|
||||
return d.code
|
||||
}
|
||||
|
||||
func (d DID) DID() DID {
|
||||
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.
|
||||
func (d DID) String() string {
|
||||
if d.key {
|
||||
@@ -54,8 +86,8 @@ func Decode(bytes []byte) (DID, error) {
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
if code == Ed25519 {
|
||||
return DID{str: string(bytes), key: true}, nil
|
||||
if code == Ed25519 || code == RSA {
|
||||
return DID{str: string(bytes), code: code, key: true}, nil
|
||||
} else if code == DIDCore {
|
||||
return DID{str: string(bytes)}, nil
|
||||
}
|
||||
@@ -82,5 +114,5 @@ func Parse(str string) (DID, error) {
|
||||
varint.PutUvarint(buf, DIDCore)
|
||||
suffix, _ := strings.CutPrefix(str, Prefix)
|
||||
buf = append(buf, suffix...)
|
||||
return DID{str: string(buf)}, nil
|
||||
return DID{str: string(buf), code: DIDCore}, nil
|
||||
}
|
||||
|
||||
5
doc.go
Normal file
5
doc.go
Normal 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
16
go.mod
@@ -1,8 +1,8 @@
|
||||
module github.com/ucan-wg/go-ucan
|
||||
|
||||
go 1.21
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.22.1
|
||||
toolchain go1.22.4
|
||||
|
||||
require (
|
||||
github.com/gobwas/glob v0.2.3
|
||||
@@ -11,23 +11,31 @@ require (
|
||||
github.com/libp2p/go-libp2p v0.36.2
|
||||
github.com/multiformats/go-multibase v0.2.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/selesy/go-options v0.0.0-20240912020512-ed2658318e52
|
||||
github.com/stretchr/testify v1.9.0
|
||||
gotest.tools/v3 v3.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
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/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.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/polydawn/refmt v0.89.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.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
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
|
||||
30
go.sum
30
go.sum
@@ -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/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/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/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
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/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
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/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
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/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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/go.mod h1:XO3joasRE4Eup8yCTTP/+kX+g92mOgRaadk46LmPhHY=
|
||||
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-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-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/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||
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/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
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/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||
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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
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/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
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/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/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
|
||||
54
internal/cmd/token/main.go
Normal file
54
internal/cmd/token/main.go
Normal 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)
|
||||
}
|
||||
309
internal/envelope/envelope.go
Normal file
309
internal/envelope/envelope.go
Normal 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()))
|
||||
})
|
||||
}
|
||||
200
internal/envelope/envelope_test.go
Normal file
200
internal/envelope/envelope_test.go
Normal 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
|
||||
}
|
||||
1
internal/envelope/testdata/example.dagcbor
vendored
Normal file
1
internal/envelope/testdata/example.dagcbor
vendored
Normal file
@@ -0,0 +1 @@
|
||||
‚X@=•zfˆîŒ— ©Ê¦Z.ÈÚàP óú¤‹Jr=n–¬;¡)Dñ÷™¹6îB;ò
|
||||
20
internal/envelope/testdata/example.dagjson
vendored
Normal file
20
internal/envelope/testdata/example.dagjson
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
24
internal/token/conversion.go
Normal file
24
internal/token/conversion.go
Normal 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
33
internal/token/doc.go
Normal 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
11
internal/token/errors.go
Normal 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
46
internal/token/schema.go
Normal 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())
|
||||
}
|
||||
@@ -1,13 +1,20 @@
|
||||
package delegation
|
||||
package token_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"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/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/internal/token"
|
||||
)
|
||||
|
||||
//go:embed token.ipldsch
|
||||
var schemaBytes []byte
|
||||
|
||||
func TestSchemaRoundTrip(t *testing.T) {
|
||||
const delegationJson = `
|
||||
{
|
||||
@@ -40,22 +47,27 @@ func TestSchemaRoundTrip(t *testing.T) {
|
||||
"sub":""
|
||||
}
|
||||
`
|
||||
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
||||
// function: DecodeDagJson() EncodeDagCbor() DecodeDagCbor() EncodeDagJson()
|
||||
// format: dagJson --> IPLD node --> token --> dagCbor --> IPLD node --> dagJson
|
||||
// function: Unwrap() Wrap()
|
||||
|
||||
p1, err := DecodeDagJson([]byte(delegationJson))
|
||||
n1, err := ipld.DecodeUsingPrototype([]byte(delegationJson), dagjson.Decode, token.Prototype())
|
||||
require.NoError(t, err)
|
||||
|
||||
cborBytes, err := p1.EncodeDagCbor()
|
||||
cborBytes, err := ipld.Encode(n1, dagcbor.Encode)
|
||||
require.NoError(t, err)
|
||||
fmt.Println("cborBytes length", len(cborBytes))
|
||||
fmt.Println("cbor", string(cborBytes))
|
||||
|
||||
p2, err := DecodeDagCbor(cborBytes)
|
||||
n2, err := ipld.DecodeUsingPrototype(cborBytes, dagcbor.Decode, token.Prototype())
|
||||
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)
|
||||
fmt.Println("readJson length", len(readJson))
|
||||
fmt.Println("json: ", string(readJson))
|
||||
@@ -65,6 +77,7 @@ func TestSchemaRoundTrip(t *testing.T) {
|
||||
|
||||
func BenchmarkSchemaLoad(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = ipld.LoadSchemaBytes(schemaBytes)
|
||||
}
|
||||
33
internal/token/token.go
Normal file
33
internal/token/token.go
Normal 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())
|
||||
}
|
||||
63
internal/token/token.ipldsch
Normal file
63
internal/token/token.ipldsch
Normal 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
|
||||
}
|
||||
31
internal/token/token_gen.go
Normal file
31
internal/token/token_gen.go
Normal 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
|
||||
}
|
||||
55
internal/token/token_test.go
Normal file
55
internal/token/token_test.go
Normal 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
7
internal/tools/tools.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build tools
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
_ "github.com/selesy/go-options"
|
||||
)
|
||||
@@ -58,7 +58,7 @@ var (
|
||||
//
|
||||
// [go-libp2p/core/crypto]: github.com/libp2p/go-libp2p/core/crypto
|
||||
func Decode(header []byte) (pb.KeyType, error) {
|
||||
keyType, ok := decMap[string(header)]
|
||||
keyType, ok := decMap[base64.RawStdEncoding.EncodeToString(header)]
|
||||
if !ok {
|
||||
return -1, fmt.Errorf("%w: %s", ErrUnknownHeader, header)
|
||||
}
|
||||
@@ -82,10 +82,10 @@ func Encode(keyType pb.KeyType) ([]byte, error) {
|
||||
return []byte(header), nil
|
||||
}
|
||||
|
||||
func keyTypeToHeader() map[pb.KeyType]string {
|
||||
func keyTypeToHeader() map[pb.KeyType][]byte {
|
||||
const rsaSigLen = 0x100
|
||||
|
||||
return map[pb.KeyType]string{
|
||||
return map[pb.KeyType][]byte{
|
||||
pb.KeyType_RSA: header(
|
||||
Prefix,
|
||||
multicodec.RsaPub,
|
||||
@@ -117,18 +117,18 @@ func headerToKeyType() map[string]pb.KeyType {
|
||||
out := make(map[string]pb.KeyType, len(encMap))
|
||||
|
||||
for keyType, header := range encMap {
|
||||
out[header] = keyType
|
||||
out[base64.RawStdEncoding.EncodeToString(header)] = keyType
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func header(vals ...multicodec.Code) string {
|
||||
func header(vals ...multicodec.Code) []byte {
|
||||
var buf []byte
|
||||
|
||||
for _, val := range vals {
|
||||
buf = binary.AppendUvarint(buf, uint64(val))
|
||||
}
|
||||
|
||||
return base64.RawStdEncoding.EncodeToString(buf)
|
||||
return buf
|
||||
}
|
||||
|
||||
@@ -21,7 +21,14 @@ func TestDecode(t *testing.T) {
|
||||
}
|
||||
|
||||
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())
|
||||
// Output:
|
||||
// RSA
|
||||
@@ -37,7 +44,7 @@ func TestEncode(t *testing.T) {
|
||||
|
||||
func ExampleEncode() {
|
||||
header, _ := varsig.Encode(pb.KeyType_RSA)
|
||||
fmt.Println(string(header))
|
||||
fmt.Println(base64.RawStdEncoding.EncodeToString(header))
|
||||
|
||||
// Output:
|
||||
// NIUkEoACcQ
|
||||
|
||||
Reference in New Issue
Block a user