feat(ucan): functions to issue/delegate UCAN tokens
This commit is contained in:
124
delegate.go
Normal file
124
delegate.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package ucan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/capability/command"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/capability/policy"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/delegation"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/did"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/internal/envelope"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/issue"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultExpiration = 30 * 24 * time.Hour
|
||||||
|
DefaultNonceLength = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate -command options go run github.com/launchdarkly/go-options
|
||||||
|
//go:generate options -type=authorityConfig -option=AuthorityOption -prefix=With -output=authority_options.go -cmp=false -new=false -imports=time
|
||||||
|
|
||||||
|
type authorityConfig struct {
|
||||||
|
expiration time.Duration
|
||||||
|
nonceLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Authority struct {
|
||||||
|
*authorityConfig
|
||||||
|
privKey crypto.PrivKey
|
||||||
|
did did.DID // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthority(privKey crypto.PrivKey, opts ...AuthorityOption) (*Authority, error) {
|
||||||
|
cfg := &authorityConfig{
|
||||||
|
expiration: DefaultExpiration,
|
||||||
|
nonceLength: DefaultNonceLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := applyAuthorityConfigOptions(cfg, opts...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := did.FromPubKey(privKey.GetPublic())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Authority{
|
||||||
|
authorityConfig: cfg,
|
||||||
|
privKey: privKey,
|
||||||
|
did: id,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) DID() did.DID {
|
||||||
|
return a.did
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) Expiration() time.Duration {
|
||||||
|
return a.expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) NonceLength() int {
|
||||||
|
return a.nonceLength
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) Delegate(aud did.DID, prf []delegation.Token, cmd *command.Command, pol *policy.Policy, exp *time.Time, opts ...delegation.Option) (*envelope.Envelope[*delegation.Token], error) {
|
||||||
|
nonce, err := a.Nonce()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn, err := delegation.New(a.DID(), aud, prf, cmd, pol, nil, nonce, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return envelope.New(a.privKey, tkn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authority) Nonce() ([]byte, error) {
|
||||||
|
nonce := make([]byte, a.nonceLength)
|
||||||
|
|
||||||
|
if _, err := rand.Read(nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonce, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue creates a root UCAN token that can later be delegated.
|
||||||
|
//
|
||||||
|
// A subject is required when creating a root UCAN delegation as root
|
||||||
|
// UCAN delegation tokens should never be "Powerlined" per the
|
||||||
|
// specification. Therefore, the inclusion of the WithSubject or WithPowerline
|
||||||
|
// options will result in an error.
|
||||||
|
//
|
||||||
|
// Issuing a root UCAN delegation token
|
||||||
|
// should be a relatively rare occurrence, so this method is not
|
||||||
|
// available via an Authority.
|
||||||
|
func Issue(privKey crypto.PrivKey, sub did.DID, cmd *command.Command, pol *policy.Policy, exp *time.Time, opts ...issue.Option) (*envelope.Envelope[*delegation.Token], error) { // TODO: cmd as pointer?
|
||||||
|
delOpts, err := issue.ToDelegateOptions(sub, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authority, err := NewAuthority(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return authority.Delegate(authority.DID(), nil, cmd, pol, nil, delOpts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Delegate(privKey crypto.PrivKey, aud did.DID, prf []delegation.Token, cmd *command.Command, pol *policy.Policy, exp *time.Time, opts ...delegation.Option) (*envelope.Envelope[*delegation.Token], error) {
|
||||||
|
authority, err := NewAuthority(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return authority.Delegate(aud, prf, cmd, pol, exp, opts...)
|
||||||
|
}
|
||||||
159
delegate_test.go
Normal file
159
delegate_test.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package ucan_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
"github.com/ipld/go-ipld-prime/schema"
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/capability/command"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/capability/policy"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/did"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ed25519PrivKeyCfg = "CAESQL1hvbXpiuk2pWr/XFbfHJcZNpJ7S90iTA3wSCTc/BPRneCwPnCZb6c0vlD6ytDWqaOt0HEOPYnqEpnzoBDprSM="
|
||||||
|
ed25519DID = "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 TestNewAuthority(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("with default configuration", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
authority := authority(t, ed25519PrivKeyCfg)
|
||||||
|
assert.Equal(t, ed25519DID, authority.DID().String())
|
||||||
|
assert.Equal(t, ucan.DefaultNonceLength, authority.NonceLength())
|
||||||
|
assert.Equal(t, ucan.DefaultExpiration, authority.Expiration())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthority_Nonce(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fixture := func(t *testing.T, exp int, opts ...ucan.AuthorityOption) {
|
||||||
|
authority := authority(t, ed25519PrivKeyCfg, opts...)
|
||||||
|
|
||||||
|
nonce, err := authority.Nonce()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, nonce, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("with default nonce length", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
fixture(t, ucan.DefaultNonceLength)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with custom nonce length", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
fixture(t, 64, ucan.WithNonceLength(64))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssue(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
privKey := privKey(t, issuerPrivKeyCfg)
|
||||||
|
|
||||||
|
id, 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)
|
||||||
|
|
||||||
|
now := time.Now().Add(ucan.DefaultExpiration)
|
||||||
|
|
||||||
|
// meta := map[string]any{
|
||||||
|
// "foo": "fooo",
|
||||||
|
// "bar": "barr",
|
||||||
|
// }
|
||||||
|
|
||||||
|
env, err := ucan.Issue(privKey, id, cmd, &pol, &now)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
node, err := env.Wrap()
|
||||||
|
|
||||||
|
typed, ok := node.(schema.TypedNode)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
json, err := ipld.Encode(typed.Representation(), dagjson.Encode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fmt.Println(string(json))
|
||||||
|
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
func authority(t *testing.T, privKeyCfg string, opts ...ucan.AuthorityOption) *ucan.Authority {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
privKey := privKey(t, ed25519PrivKeyCfg)
|
||||||
|
|
||||||
|
authority, err := ucan.NewAuthority(privKey, opts...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return authority
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
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
|
||||||
31
issue/issue.go
Normal file
31
issue/issue.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package issue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/delegation"
|
||||||
|
"github.com/ucan-wg/go-ucan/v1/did"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate -command options go run github.com/launchdarkly/go-options
|
||||||
|
//go:generate options -type=config -prefix=With -output=issue_options.go -cmp=false -imports=time
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Meta map[string]any
|
||||||
|
NoExpiration bool
|
||||||
|
NotBefore time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToDelegateOptions(sub did.DID, opts ...Option) ([]delegation.Option, error) {
|
||||||
|
cfg, err := newConfig(opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []delegation.Option{
|
||||||
|
delegation.WithSubject(&sub),
|
||||||
|
delegation.WithMeta(cfg.Meta),
|
||||||
|
delegation.WithNoExpiration(cfg.NoExpiration),
|
||||||
|
delegation.WithNotBefore(&cfg.NotBefore),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
103
issue/issue_options.go
Normal file
103
issue/issue_options.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package issue
|
||||||
|
|
||||||
|
// Code generated by github.com/launchdarkly/go-options. DO NOT EDIT.
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApplyOptionFunc func(c *config) error
|
||||||
|
|
||||||
|
func (f ApplyOptionFunc) apply(c *config) error {
|
||||||
|
return f(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.apply(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option interface {
|
||||||
|
apply(*config) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type withMetaImpl struct {
|
||||||
|
o map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o withMetaImpl) apply(c *config) error {
|
||||||
|
c.Meta = o.o
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o withMetaImpl) String() string {
|
||||||
|
name := "WithMeta"
|
||||||
|
|
||||||
|
// hack to avoid go vet error about passing a function to Sprintf
|
||||||
|
var value interface{} = o.o
|
||||||
|
return fmt.Sprintf("%s: %+v", name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMeta(o map[string]any) Option {
|
||||||
|
return withMetaImpl{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withNoExpirationImpl struct {
|
||||||
|
o bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o withNoExpirationImpl) apply(c *config) error {
|
||||||
|
c.NoExpiration = o.o
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o withNoExpirationImpl) String() string {
|
||||||
|
name := "WithNoExpiration"
|
||||||
|
|
||||||
|
// hack to avoid go vet error about passing a function to Sprintf
|
||||||
|
var value interface{} = o.o
|
||||||
|
return fmt.Sprintf("%s: %+v", name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNoExpiration(o bool) Option {
|
||||||
|
return withNoExpirationImpl{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withNotBeforeImpl struct {
|
||||||
|
o time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o withNotBeforeImpl) apply(c *config) error {
|
||||||
|
c.NotBefore = o.o
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o withNotBeforeImpl) String() string {
|
||||||
|
name := "WithNotBefore"
|
||||||
|
|
||||||
|
// hack to avoid go vet error about passing a function to Sprintf
|
||||||
|
var value interface{} = o.o
|
||||||
|
return fmt.Sprintf("%s: %+v", name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNotBefore(o time.Time) Option {
|
||||||
|
return withNotBeforeImpl{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user