Skip to content

Commit

Permalink
(choria-io#109) Sign and Verify tasks in the client and cli
Browse files Browse the repository at this point in the history
Signed-off-by: R.I.Pienaar <[email protected]>
  • Loading branch information
ripienaar committed May 8, 2023
1 parent cf62095 commit 7b1744a
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 33 deletions.
86 changes: 58 additions & 28 deletions ajc/task_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"os"
"strings"
"time"

Expand Down Expand Up @@ -40,6 +39,8 @@ type taskCommand struct {
dependencies []string
loadDepResults bool
ed25519Seed string
ed25519PubKey string
optionalSigs bool

limit int
json bool
Expand All @@ -50,6 +51,8 @@ func configureTaskCommand(app *fisk.Application) {
c := &taskCommand{}

tasks := app.Command("tasks", "Manage Tasks").Alias("t").Alias("task")
tasks.Flag("sign", "Signs tasks using an ed25519 seed").StringVar(&c.ed25519Seed)
tasks.Flag("verify", "Verifies tasks using an ed25519 public key").StringVar(&c.ed25519PubKey)

add := tasks.Command("add", "Adds a new Task to a queue").Alias("new").Alias("a").Alias("enqueue").Action(c.addAction)
add.Arg("type", "The task type").Required().StringVar(&c.ttype)
Expand All @@ -59,7 +62,6 @@ func configureTaskCommand(app *fisk.Application) {
add.Flag("tries", "Sets the maximum amount of times this task may be tried").IntVar(&c.maxtries)
add.Flag("depends", "Sets IDs to depend on, comma sep or pass multiple times").StringsVar(&c.dependencies)
add.Flag("load", "Loads results from dependencies before executing task").BoolVar(&c.loadDepResults)
add.Flag("sign", "Signs the task using an ed25519 seed").StringVar(&c.ed25519Seed)

retry := tasks.Command("retry", "Retries delivery of a task currently in the Task Store").Action(c.retryAction)
retry.Arg("id", "The Task ID to view").Required().StringVar(&c.id)
Expand Down Expand Up @@ -106,8 +108,52 @@ func configureTaskCommand(app *fisk.Application) {
configureTaskCronCommand(tasks)
}

func (c *taskCommand) prepare(copts ...aj.ClientOpt) error {
sigOpts, err := c.clientOpts()
if err != nil {
return err
}

return prepare(append(copts, sigOpts...)...)
}

func (c *taskCommand) clientOpts() ([]aj.ClientOpt, error) {
var opts []aj.ClientOpt

if c.optionalSigs {
opts = append(opts, aj.TaskSignaturesOptional())
}

if c.ed25519Seed != "" {
if fileExist(c.ed25519Seed) {
opts = append(opts, aj.TaskSigningSeedFile(c.ed25519Seed))
} else {
seed, err := hex.DecodeString(c.ed25519Seed)
if err != nil {
return nil, err
}

opts = append(opts, aj.TaskSigningKey(ed25519.NewKeyFromSeed(seed)))
}
}

if c.ed25519PubKey != "" {
if fileExist(c.ed25519PubKey) {
opts = append(opts, aj.TaskVerificationKeyFile(c.ed25519PubKey))
} else {
pk, err := hex.DecodeString(c.ed25519PubKey)
if err != nil {
return nil, err
}
opts = append(opts, aj.TaskVerificationKey(pk))
}
}

return opts, nil
}

func (c *taskCommand) retryAction(_ *fisk.ParseContext) error {
err := prepare(aj.BindWorkQueue(c.queue))
err := c.prepare(aj.BindWorkQueue(c.queue))
if err != nil {
return err
}
Expand All @@ -121,7 +167,7 @@ func (c *taskCommand) retryAction(_ *fisk.ParseContext) error {
}

func (c *taskCommand) initAction(_ *fisk.ParseContext) error {
err := prepare(aj.NoStorageInit())
err := c.prepare(aj.NoStorageInit())
if err != nil {
return err
}
Expand All @@ -142,7 +188,7 @@ func (c *taskCommand) initAction(_ *fisk.ParseContext) error {
}

func (c *taskCommand) watchAction(_ *fisk.ParseContext) error {
err := prepare()
err := c.prepare()
if err != nil {
return err
}
Expand Down Expand Up @@ -208,7 +254,7 @@ func (c *taskCommand) processAction(_ *fisk.ParseContext) error {
copts = append(copts, aj.DiscardTaskStates(aj.TaskStateExpired))
}

err := prepare(copts...)
err := c.prepare(copts...)
if err != nil {
return err
}
Expand All @@ -227,7 +273,7 @@ func (c *taskCommand) processAction(_ *fisk.ParseContext) error {
}

func (c *taskCommand) purgeAction(_ *fisk.ParseContext) error {
err := prepare()
err := c.prepare()
if err != nil {
return err
}
Expand Down Expand Up @@ -260,7 +306,7 @@ func (c *taskCommand) purgeAction(_ *fisk.ParseContext) error {
}

func (c *taskCommand) configAction(_ *fisk.ParseContext) error {
err := prepare()
err := c.prepare()
if err != nil {
return err
}
Expand Down Expand Up @@ -291,7 +337,7 @@ func (c *taskCommand) configAction(_ *fisk.ParseContext) error {
}

func (c *taskCommand) lsAction(_ *fisk.ParseContext) error {
err := prepare()
err := c.prepare()
if err != nil {
return err
}
Expand Down Expand Up @@ -323,7 +369,7 @@ func (c *taskCommand) lsAction(_ *fisk.ParseContext) error {
}

func (c *taskCommand) rmAction(_ *fisk.ParseContext) error {
err := prepare()
err := c.prepare()
if err != nil {
return err
}
Expand All @@ -346,7 +392,7 @@ func (c *taskCommand) rmAction(_ *fisk.ParseContext) error {
}

func (c *taskCommand) viewAction(_ *fisk.ParseContext) error {
err := prepare()
err := c.prepare()
if err != nil {
return err
}
Expand Down Expand Up @@ -394,7 +440,7 @@ func (c *taskCommand) viewAction(_ *fisk.ParseContext) error {
}

func (c *taskCommand) addAction(_ *fisk.ParseContext) error {
err := prepare(aj.BindWorkQueue(c.queue))
err := c.prepare(aj.BindWorkQueue(c.queue))
if err != nil {
return err
}
Expand All @@ -420,22 +466,6 @@ func (c *taskCommand) addAction(_ *fisk.ParseContext) error {
opts = append(opts, aj.TaskMaxTries(c.maxtries))
}

if c.ed25519Seed != "" {
var seed []byte
if fileExist(c.ed25519Seed) {
seed, err = os.ReadFile(c.ed25519Seed)
if err != nil {
return err
}
} else {
seed, err = hex.DecodeString(c.ed25519Seed)
if err != nil {
return err
}
}
opts = append(opts, aj.TaskSigner(ed25519.NewKeyFromSeed(seed)))
}

task, err := aj.NewTask(c.ttype, c.payload, opts...)
if err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions ajc/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func createLogger() {

log = logrus.NewEntry(logger)
}

func prepare(copts ...asyncjobs.ClientOpt) error {
if client != nil {
return nil
Expand Down
5 changes: 5 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ func (c *Client) EnqueueTask(ctx context.Context, task *Task) error {
}

func (c *Client) verifyTaskSignature(task *Task) error {
// is disabled
if c.opts.publicKey == nil && c.opts.publicKeyFile == "" {
return nil
}

switch {
case !c.opts.optionalTaskSignatures && task.Signature == "":
return ErrTaskNotSigned
Expand Down
8 changes: 6 additions & 2 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ var _ = Describe("Client", func() {
})
})

It("Should support only loading signed messages", func() {
It("Should support loading signed and unsigned messages", func() {
withJetStream(func(nc *nats.Conn, mgr *jsm.Manager) {
client, err := NewClient(NatsConn(nc), TaskVerificationKey(pubk))
Expect(err).ToNot(HaveOccurred())
Expand All @@ -127,9 +127,13 @@ var _ = Describe("Client", func() {

id := task.ID
task, err = client.LoadTaskByID(id)

// all tasks should be signed by default
Expect(err).To(MatchError(ErrTaskNotSigned))

client.opts.optionalTaskSignatures = true
// but can be disabled
Expect(TaskSignaturesOptional()(client.opts)).To(Succeed())

task, err = client.LoadTaskByID(id)
Expect(err).ToNot(HaveOccurred())
Expect(task).ToNot(BeNil())
Expand Down
39 changes: 39 additions & 0 deletions docs/content/reference/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
+++
title = "Security"
toc = true
weight = 50
+++

Sometimes you want to run a handler in a insecure location and want to be sure it only executes tasks from trusted creators.

Tasks can be signed using ed25519 private keys and clients can be configured to only accept tasks created and signed using
a specific key. We support requiring all tasks are signed when keys are configured (the default), or accepting unsigned tasks
but requiring signed tasks are verified.

First we need to create some keys, these should be saved to a file encoded using `hex.Encode()`.

```go
pubk, prik, err = ed25519.GenerateKey(nil)
panicIfErr(err)
```

Then we can configure the client:

```go
client, err := asyncjobs.NewClient(
asyncjobs.NatsContext("AJC"),

// when tasks are created sign using this ed25519.PrivateKey, see also TaskSigningSeedFile()
asyncjobs.TaskSigningKey(prik),

// when loading tasks verify using this ed25519.PublicKey, see also TaskVerificationKeyFile()
asyncjobs.TaskVerificationKey(pubk),

// support loading unsigned tasks when a verification method is set, disabled by default
asyncjobs.TaskSignaturesOptional(),
)
panicIfErr(err)
```

On the command line the `ajc tasks` command has `--sign` and `--verify` flags which can either be hex encoded keys
or paths to files holding them in hex encoded format.
2 changes: 1 addition & 1 deletion docs/content/reference/terminology.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
+++
title = "Terminology"
toc = true
weight = 50
weight = 60
+++

Several terms are used in this system as outlined here.
Expand Down
3 changes: 2 additions & 1 deletion task.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package asyncjobs

import (
"crypto/ed25519"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -193,7 +194,7 @@ func (t *Task) signatureMessage() ([]byte, error) {
deadline = t.Deadline.UnixNano()
}

msg := fmt.Sprintf("%s:%s:%s:%d:%d:%d", t.ID, t.Queue, t.Type, t.MaxTries, t.CreatedAt.UnixNano(), deadline)
msg := fmt.Sprintf("%s:%s:%s:%d:%d:%d:%s", t.ID, t.Queue, t.Type, t.MaxTries, t.CreatedAt.UnixNano(), deadline, base64.StdEncoding.EncodeToString(t.Payload))

return []byte(msg), nil
}
Expand Down
2 changes: 1 addition & 1 deletion task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var _ = Describe("Tasks", func() {
task.Queue = "x"
msg, err = task.signatureMessage()
Expect(err).ToNot(HaveOccurred())
Expect(msg).To(HaveLen(77))
Expect(msg).To(HaveLen(102))
})
})
})

0 comments on commit 7b1744a

Please sign in to comment.