From d3ad6715d9c83efbe71f562d0706d0c0681d039d Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Mon, 4 Nov 2024 16:07:11 -0500 Subject: [PATCH] feat(invocation): ipld unseal to invocation --- token/invocation/examples_test.go | 23 ++++++++--- token/invocation/invocation.go | 66 +++++++++++++++++++++++++++++-- token/invocation/ipld.go | 10 +++-- token/invocation/options.go | 3 +- token/invocation/schema.go | 7 +--- 5 files changed, 88 insertions(+), 21 deletions(-) diff --git a/token/invocation/examples_test.go b/token/invocation/examples_test.go index 530bcc0..484643b 100644 --- a/token/invocation/examples_test.go +++ b/token/invocation/examples_test.go @@ -23,7 +23,7 @@ import ( func ExampleNew() { privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew() if err != nil { - fmt.Errorf("failed to create setup: %w", err) + fmt.Println("failed to create setup:", err.Error()) return } @@ -39,21 +39,21 @@ func ExampleNew() { invocation.WithExpirationIn(time.Minute), invocation.WithoutInvokedAt()) if err != nil { - fmt.Errorf("failed to create invocation: %w", err) + fmt.Println("failed to create invocation:", err.Error()) return } data, cid, err := inv.ToSealed(privKey) if err != nil { - fmt.Errorf("failed to seal invocation: %w", err) + fmt.Println("failed to seal invocation:", err.Error()) return } json, err := prettyDAGJSON(data) if err != nil { - fmt.Errorf("failed to pretty DAG-JSON: %w", err) + fmt.Println("failed to pretty DAG-JSON:", err.Error()) return } @@ -139,6 +139,8 @@ func prettyDAGJSON(data []byte) (string, error) { return "", err } + fmt.Println(string(jsonData)) + var out bytes.Buffer if err := json.Indent(&out, jsonData, "", " "); err != nil { return "", err @@ -171,14 +173,19 @@ func setupExampleNew() (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Co if err != nil { errs = errors.Join(errs, fmt.Errorf("failed to build headers: %w", err)) } + + // ***** WARNING - do not change the order of these elements. DAG-CBOR + // will order them alphabetically and the unsealed + // result won't match if the input isn't also created in + // alphabetical order. payload, err := qp.BuildMap(basicnode.Prototype.Any, 4, func(ma datamodel.MapAssembler) { - qp.MapEntry(ma, "title", qp.String("UCAN for Fun and Profit")) qp.MapEntry(ma, "body", qp.String("UCAN is great")) + qp.MapEntry(ma, "draft", qp.Bool(true)) + qp.MapEntry(ma, "title", qp.String("UCAN for Fun and Profit")) qp.MapEntry(ma, "topics", qp.List(2, func(la datamodel.ListAssembler) { qp.ListEntry(la, qp.String("authz")) qp.ListEntry(la, qp.String("journal")) })) - qp.MapEntry(ma, "draft", qp.Bool(true)) }) if err != nil { errs = errors.Join(errs, fmt.Errorf("failed to build payload: %w", err)) @@ -202,6 +209,10 @@ func setupExampleNew() (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Co } } + // ***** WARNING - do not change the order of these elements. DAG-CBOR + // will order them alphabetically and the unsealed + // result won't match if the input isn't also created in + // alphabetical order. tags, err := qp.BuildList(basicnode.Prototype.Any, 3, func(la datamodel.ListAssembler) { qp.ListEntry(la, qp.String("blog")) qp.ListEntry(la, qp.String("post")) diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 0d43bc5..d5703a9 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -62,9 +62,8 @@ type Token struct { // With the exception of the WithMeta option, all other will overwrite // the previous contents of their target field. func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) (*Token, error) { - nonce := make([]byte, 12) - - if _, err := rand.Read(nonce); err != nil { + nonce, err := generateNonce() + if err != nil { return nil, err } @@ -141,6 +140,7 @@ func (t *Token) InvokedAt() *time.Time { return t.invokedAt } +// Cause returns the (optional) func (t *Token) Cause() *cid.Cid { return t.cause } @@ -170,9 +170,41 @@ func (t *Token) validate() error { func tokenFromModel(m tokenPayloadModel) (*Token, error) { var ( tkn Token + err error ) - // TODO + if tkn.issuer, err = did.Parse(m.Iss); err != nil { + return nil, err + } + + if tkn.subject, err = did.Parse(m.Sub); err != nil { + return nil, err + } + + if tkn.audience, err = parseOptionalDID(m.Aud); err != nil { + return nil, err + } + + if tkn.command, err = command.Parse(m.Cmd); err != nil { + return nil, err + } + + tkn.arguments = m.Args.Values + tkn.proof = m.Prf + tkn.meta = m.Meta + tkn.nonce = m.Nonce + + if tkn.expiration, err = parseOptionalTimestamp(m.Exp); err != nil { + return nil, err + } + + if tkn.invokedAt, err = parseOptionalTimestamp(m.Iat); err != nil { + return nil, err + } + + if tkn.cause, err = parseOptionalCID(m.Cause); err != nil { + return nil, err + } return &tkn, nil } @@ -187,3 +219,29 @@ func generateNonce() ([]byte, error) { } return res, nil } + +func parseOptionalCID(c *cid.Cid) (*cid.Cid, error) { + if c == nil { + return nil, nil + } + + return c, nil +} + +func parseOptionalDID(s *string) (did.DID, error) { + if s == nil { + return did.Undef, nil + } + + return did.Parse(*s) +} + +func parseOptionalTimestamp(sec *int64) (*time.Time, error) { + if sec == nil { + return nil, nil + } + + t := time.Unix(*sec, 0) + + return &t, nil +} diff --git a/token/invocation/ipld.go b/token/invocation/ipld.go index 467e2be..817b265 100644 --- a/token/invocation/ipld.go +++ b/token/invocation/ipld.go @@ -192,6 +192,11 @@ func FromIPLD(node datamodel.Node) (*Token, error) { return tkn, err } +type stringAny struct { + Keys []string + Values map[string]datamodel.Node +} + func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) { var aud *string @@ -220,10 +225,7 @@ func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) { i++ } - args := struct { - Keys []string - Values map[string]datamodel.Node - }{ + args := stringAny{ Keys: argsKey, Values: t.arguments, } diff --git a/token/invocation/options.go b/token/invocation/options.go index 0d232fe..13a28d0 100644 --- a/token/invocation/options.go +++ b/token/invocation/options.go @@ -86,6 +86,7 @@ func WithEmptyNonce() Option { // value of the provided time.Time. func WithExpiration(exp time.Time) Option { return func(t *Token) error { + exp = exp.Round(time.Second) t.expiration = &exp return nil @@ -96,7 +97,7 @@ func WithExpiration(exp time.Time) Option { // Now() plus the given duration. func WithExpirationIn(exp time.Duration) Option { return func(t *Token) error { - expTime := time.Now().Add(exp) + expTime := time.Now().Add(exp).Round(time.Minute) t.expiration = &expTime return nil diff --git a/token/invocation/schema.go b/token/invocation/schema.go index f438e1f..b2b99f2 100644 --- a/token/invocation/schema.go +++ b/token/invocation/schema.go @@ -7,7 +7,6 @@ import ( "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/bindnode" "github.com/ipld/go-ipld-prime/schema" @@ -46,7 +45,6 @@ func payloadType() schema.Type { var _ envelope.Tokener = (*tokenPayloadModel)(nil) -// TODO type tokenPayloadModel struct { // The DID of the Invoker Iss string @@ -58,10 +56,7 @@ type tokenPayloadModel struct { // The Command Cmd string // The Command's Arguments - Args struct { - Keys []string - Values map[string]datamodel.Node - } + Args stringAny // Delegations that prove the chain of authority Prf []cid.Cid