feat(ucan): functions to issue/delegate UCAN tokens

This commit is contained in:
Steve Moyer
2024-09-09 08:55:14 -04:00
parent 719837e3cd
commit b77f8d6bb0
5 changed files with 422 additions and 0 deletions

124
delegate.go Normal file
View 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
View 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
View File

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

31
issue/issue.go Normal file
View 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
View 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,
}
}