feat(delegation): add validation/accessors
This commit is contained in:
@@ -4,7 +4,6 @@ package delegation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,13 +24,6 @@ func applyConfigOptions(c *config, options ...Option) error {
|
|||||||
return nil
|
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 {
|
func WithMeta(o map[string]datamodel.Node) Option {
|
||||||
return func(c *config) error {
|
return func(c *config) error {
|
||||||
c.Meta = o
|
c.Meta = o
|
||||||
@@ -39,31 +31,9 @@ func WithMeta(o map[string]datamodel.Node) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithNoExpiration(o bool) Option {
|
|
||||||
return func(c *config) error {
|
|
||||||
c.NoExpiration = o
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithNotBefore(o *time.Time) Option {
|
func WithNotBefore(o *time.Time) Option {
|
||||||
return func(c *config) error {
|
return func(c *config) error {
|
||||||
c.NotBefore = o
|
c.NotBefore = o
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithSubject is a did.DID representing the Subject.
|
|
||||||
func WithSubject(o *did.DID) Option {
|
|
||||||
return func(c *config) error {
|
|
||||||
c.Subject = o
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithPowerline(o bool) Option {
|
|
||||||
return func(c *config) error {
|
|
||||||
c.Powerline = o
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package delegation
|
package delegation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
@@ -23,30 +24,22 @@ type Delegation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//go:generate -command options go run github.com/selesy/go-options
|
//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/ucan-wg/go-ucan/did,github.com/ipld/go-ipld-prime/datamodel
|
//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 {
|
type config struct {
|
||||||
Expiration *time.Time
|
Meta map[string]datamodel.Node
|
||||||
Meta map[string]datamodel.Node
|
NotBefore *time.Time
|
||||||
NoExpiration bool
|
|
||||||
NotBefore *time.Time
|
|
||||||
// is a did.DID representing the Subject.
|
|
||||||
Subject *did.DID
|
|
||||||
Powerline bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required fields for delegation
|
func New(privKey crypto.PrivKey, aud did.DID, sub *did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, exp *time.Time, opts ...Option) (*Delegation, error) {
|
||||||
|
|
||||||
// Requirements for root
|
|
||||||
|
|
||||||
func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command, pol *policy.Policy, exp *time.Time, nonce []byte, opts ...Option) (*Delegation, error) {
|
|
||||||
cfg, err := newConfig(opts...)
|
cfg, err := newConfig(opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !iss.Defined() {
|
issuer, err := did.FromPrivKey(privKey)
|
||||||
return nil, fmt.Errorf("%w: %s", token.ErrMissingRequiredDID, "iss")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !aud.Defined() {
|
if !aud.Defined() {
|
||||||
@@ -55,8 +48,8 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command,
|
|||||||
audience := aud.String()
|
audience := aud.String()
|
||||||
|
|
||||||
var subject *string
|
var subject *string
|
||||||
if cfg.Subject != nil && cfg.Subject.Defined() {
|
if sub != nil {
|
||||||
s := cfg.Subject.String()
|
s := sub.String()
|
||||||
subject = &s
|
subject = &s
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +58,11 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce = []uint8(nonce)
|
var meta *token.Map__String__Any
|
||||||
|
if len(cfg.Meta) > 0 {
|
||||||
|
m := token.ToIPLDMapStringAny(cfg.Meta)
|
||||||
|
meta = &m
|
||||||
|
}
|
||||||
|
|
||||||
var notBefore *int
|
var notBefore *int
|
||||||
if cfg.NotBefore != nil {
|
if cfg.NotBefore != nil {
|
||||||
@@ -73,20 +70,14 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command,
|
|||||||
notBefore = &n
|
notBefore = &n
|
||||||
}
|
}
|
||||||
|
|
||||||
var meta *token.Map__String__Any
|
|
||||||
if len(cfg.Meta) > 0 {
|
|
||||||
m := token.ToIPLDMapStringAny(cfg.Meta)
|
|
||||||
meta = &m
|
|
||||||
}
|
|
||||||
|
|
||||||
var expiration *int
|
var expiration *int
|
||||||
if exp != nil && !cfg.NoExpiration {
|
if exp != nil {
|
||||||
e := int(cfg.NotBefore.Unix())
|
e := int(exp.Unix())
|
||||||
expiration = &e
|
expiration = &e
|
||||||
}
|
}
|
||||||
|
|
||||||
tkn := &token.Token{
|
tkn := &token.Token{
|
||||||
Issuer: iss.String(),
|
Issuer: issuer.String(),
|
||||||
Audience: &audience,
|
Audience: &audience,
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
Command: cmd.String(),
|
Command: cmd.String(),
|
||||||
@@ -111,16 +102,82 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command,
|
|||||||
return dlg, nil
|
return dlg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type validateFunc func() error
|
func Root(privKey crypto.PrivKey, aud did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, exp *time.Time, opts ...Option) (*Delegation, error) {
|
||||||
|
sub, err := did.FromPrivKey(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(privKey, aud, &sub, cmd, pol, nonce, exp, 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 {
|
func (d *Delegation) Validate() error {
|
||||||
return errors.Join(
|
return errors.Join(
|
||||||
d.validateDID("iss", &d.envel.TokenPayload().Issuer, false),
|
d.validateDID("iss", &d.envel.TokenPayload().Issuer, false),
|
||||||
d.validateDID("aud", d.envel.TokenPayload().Audience, false),
|
d.validateDID("aud", d.envel.TokenPayload().Audience, false),
|
||||||
d.validateDID("sub", d.envel.TokenPayload().Subject, true),
|
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 {
|
func (d *Delegation) validateDID(fieldName string, identity *string, nullableOrOptional bool) error {
|
||||||
if identity == nil && !nullableOrOptional {
|
if identity == nil && !nullableOrOptional {
|
||||||
return fmt.Errorf("a required DID is missing: %s", fieldName)
|
return fmt.Errorf("a required DID is missing: %s", fieldName)
|
||||||
@@ -137,3 +194,31 @@ func (d *Delegation) validateDID(fieldName string, identity *string, nullableOrO
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
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), &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), &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
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"}}]
|
||||||
Reference in New Issue
Block a user