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