feat(delegation): add validation/accessors

This commit is contained in:
Steve Moyer
2024-09-16 17:18:16 -04:00
parent da9f2e7bec
commit f85ece49fa
5 changed files with 264 additions and 58 deletions

View File

@@ -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
}
}

View File

@@ -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
}

View 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
View 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
View 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"}}]