diff --git a/.gitignore b/.gitignore
index 5a46d357..75bf717e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
+./run_argon2
vendor/*/
diff --git a/argon2.go b/argon2.go
index 2209816c..be1daa85 100644
--- a/argon2.go
+++ b/argon2.go
@@ -25,6 +25,7 @@ import (
"math"
"runtime"
"sync"
+ "sync/atomic"
"time"
"golang.org/x/xerrors"
@@ -39,14 +40,24 @@ var (
runtimeNumCPU = runtime.NumCPU
)
-// SetArgon2KDF sets the KDF implementation for Argon2. The default here is
-// the null implementation which returns an error, so this will need to be
-// configured explicitly in order to use Argon2.
+// SetArgon2KDF sets the KDF implementation for Argon2 use from within secboot.
+// The default here is a null implementation which returns an error, so this
+// will need to be configured explicitly in order to be able to use Argon2 from
+// within secboot.
//
// Passing nil will configure the null implementation as well.
//
// This returns the currently set implementation.
+//
+// This function shouldn't be used in processes that have called
+// [SetIsArgon2HandlerProcess] to become an out-of-process handler process for
+// Argon2 requests, else it will panic. Applications should use only one of these
+// functions in a process.
func SetArgon2KDF(kdf Argon2KDF) Argon2KDF {
+ if atomic.LoadUint32(&argon2OutOfProcessStatus) > notArgon2HandlerProcess {
+ panic("cannot call SetArgon2KDF in a process where SetIsArgon2HandlerProcess has already been called")
+ }
+
argon2Mu.Lock()
defer argon2Mu.Unlock()
@@ -214,7 +225,7 @@ func (p *Argon2CostParams) internalParams() *argon2.CostParams {
}
// Argon2KDF is an interface to abstract use of the Argon2 KDF to make it possible
-// to delegate execution to a short-lived utility process where required.
+// to delegate execution to a short-lived handler process where required.
type Argon2KDF interface {
// Derive derives a key of the specified length in bytes, from the supplied
// passphrase and salt and using the supplied mode and cost parameters.
@@ -257,10 +268,25 @@ func (_ inProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams)
return argon2.KeyDuration(argon2.Mode(mode), params.internalParams()), nil
}
-// InProcessArgon2KDF is the in-process implementation of the Argon2 KDF. This
-// shouldn't be used in long-lived system processes - these processes should
-// instead provide their own KDF implementation which delegates to a short-lived
-// utility process which will use the in-process implementation.
+// InProcessArgon2KDF is the in-process implementation of the Argon2 KDF.
+//
+// This shouldn't be used in long-lived system processes. As Argon2 intentionally
+// allocates a lot of memory and go is garbage collected, it may be some time before
+// the large amounts of memory it allocates are freed and made available to other code
+// or other processes on the system. Consecutive calls can rapidly result in the
+// application being unable to allocate more memory, and even worse, may trigger the
+// kernel's OOM killer. Whilst implementations can call [runtime.GC], the sweep phase
+// has to happen with every goroutine stopped, which isn't a great experience and may
+// result in noticeable non-responsiveness.
+//
+// Processes instead should provide their own [Argon2KDF] implementation which proxies
+// requests to a short-lived handler process which will use this in-process implementation
+// once and then exit, immediately giving the allocated memory back to the kernel and
+// avoiding the need for garbage collection entirely.
+//
+// This package provides an example of this already ([NewOutOfProcessArgon2KDF]), as well
+// as a handler for use in the short-lived handler process
+// ([WaitForAndRunArgon2OutOfProcessRequest]).
var InProcessArgon2KDF = inProcessArgon2KDFImpl{}
type nullArgon2KDFImpl struct{}
diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go
new file mode 100644
index 00000000..51fa33ba
--- /dev/null
+++ b/argon2_out_of_process_support.go
@@ -0,0 +1,471 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2021-2024 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package secboot
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os/exec"
+ "sync/atomic"
+ "time"
+)
+
+// Argon2OutOfProcessCommand represents an argon2 command to run out of process.
+type Argon2OutOfProcessCommand string
+
+const (
+ // Argon2OutOfProcessCommandDerive requests to derive a key from a passphrase
+ Argon2OutOfProcessCommandDerive Argon2OutOfProcessCommand = "derive"
+
+ // Argon2OutOfProcessCommandTime requests the duration that the KDF took to
+ // execute. This excludes additional costs such as process startup.
+ Argon2OutOfProcessCommandTime Argon2OutOfProcessCommand = "time"
+)
+
+// Argon2OutOfProcessRequest is an input request for an argon2 operation in
+// a remote process.
+type Argon2OutOfProcessRequest struct {
+ Command Argon2OutOfProcessCommand `json:"command"` // The command to run
+ Passphrase string `json:"passphrase,omitempty"` // If the command is "derive, the passphrase
+ Salt []byte `json:"salt,omitempty"` // If the command is "derive", the salt
+ Keylen uint32 `json:"keylen,omitempty"` // If the command is "derive", the key length in bytes
+ Mode Argon2Mode `json:"mode"` // The Argon2 mode
+ Time uint32 `json:"time"` // The time cost
+ MemoryKiB uint32 `json:"memory"` // The memory cost in KiB
+ Threads uint8 `json:"threads"` // The number of threads to use
+}
+
+// Argon2OutOfProcessErrorType describes the type of error produced by [RunArgon2OutOfProcessRequest]
+// or [WaitForAndRunArgon2OutOfProcessRequest].
+type Argon2OutOfProcessErrorType string
+
+const (
+ // Argon2OutOfProcessErrorInvalidCommand means that an invalid command was supplied.
+ Argon2OutOfProcessErrorInvalidCommand Argon2OutOfProcessErrorType = "invalid-command"
+
+ // Argon2OutOfProcessErrorInvalidMode means that an invalid mode was supplied.
+ Argon2OutOfProcessErrorInvalidMode Argon2OutOfProcessErrorType = "invalid-mode"
+
+ // Argon2OutOfProcessErrorInvalidTimeCost means that an invalid time cost was supplied.
+ Argon2OutOfProcessErrorInvalidTimeCost Argon2OutOfProcessErrorType = "invalid-time-cost"
+
+ // Argon2OutOfProcessErrorInvalidThreads means that an invalid number of threads was supplied.
+ Argon2OutOfProcessErrorInvalidThreads Argon2OutOfProcessErrorType = "invalid-threads"
+
+ // Argon2OutOfProcessErrorConsumedProcess means that this process has already performed one
+ // execution of the KDF, and the process should exit and be replaced by a new one.
+ Argon2OutOfProcessErrorConsumedProcess Argon2OutOfProcessErrorType = "consumed-process"
+
+ // Argon2OutOfProcessErrorProcessNotConfigured means that nothing has called SetIsArgon2HandlerProcess
+ // to configure the process for handling an Argon2 request.
+ Argon2OutOfProcessErrorProcessNotConfigured Argon2OutOfProcessErrorType = "process-not-configured"
+
+ // Argon2OutOfProcessErrorUnexpected means that an unexpected error occurred when
+ // running the operation.
+ Argon2OutOfProcessErrorUnexpected Argon2OutOfProcessErrorType = "unexpected-error"
+
+ // Argon2OutOfProcessErrorUnexpectedInput means that there was an error with
+ // the supplied request input not covered by one of the more specific error types.
+ Argon2OutOfProcessErrorUnexpectedInput Argon2OutOfProcessErrorType = "unexpected-input"
+)
+
+// Argon2OutOfProcessResponse is the response to a request for an argon2
+// operation in a remote process.
+type Argon2OutOfProcessResponse struct {
+ Command Argon2OutOfProcessCommand `json:"command"` // The input command
+ Key []byte `json:"key,omitempty"` // The derived key, if the input command was "derive"
+ Duration time.Duration `json:"duration,omitempty"` // The duration, if the input command was "duration"
+ ErrorType Argon2OutOfProcessErrorType `json:"error-type,omitempty"` // The error type, if an error occurred
+ ErrorString string `json:"error-string,omitempty"` // The error string, if an error occurred
+}
+
+// Argon2OutOfProcessError is returned from [Argon2OutOfProcessResponse.Err]
+// if the response indicates an error, or directly from methods of the [Argon2KDF]
+// implementation created by [NewOutOfProcessKDF] when the received response indicates
+// that an error ocurred.
+type Argon2OutOfProcessError struct {
+ ErrorType Argon2OutOfProcessErrorType
+ ErrorString string
+}
+
+// Error implements the error interface.
+func (e *Argon2OutOfProcessError) Error() string {
+ str := new(bytes.Buffer)
+ fmt.Fprintf(str, "cannot process KDF request: %v", e.ErrorType)
+ if e.ErrorString != "" {
+ fmt.Fprintf(str, " (%s)", e.ErrorString)
+ }
+ return str.String()
+}
+
+// Err returns an error associated with the response if one occurred, or nil if no
+// error occurred. If the response indicates an error, the returned error will be a
+// *[Argon2OutOfProcessError].
+func (o *Argon2OutOfProcessResponse) Err() error {
+ if o.ErrorType == "" {
+ return nil
+ }
+ return &Argon2OutOfProcessError{
+ ErrorType: o.ErrorType,
+ ErrorString: o.ErrorString,
+ }
+}
+
+const (
+ argon2Unused uint32 = 0
+ argon2Expired uint32 = 1
+)
+
+var errArgon2OutOfProcessHandlerExpired = errors.New("argon2 out-of-process handler has alreay been used - a new process should be started to handle a new request")
+
+// argon2OutOfProcessHandler is an implementation of Argon2KDF that will
+// only process a single call before returning an error on subsequent calls.
+type argon2OutOfProcessHandler struct {
+ Status uint32
+ KDF Argon2KDF
+}
+
+// consume uses up the single request that this KDF can process, and returns true
+// if it can continue processing the request, or false if it should stop processing
+// the reqest because it has already processed a request in the past and the process
+// should be restarted.
+func (k *argon2OutOfProcessHandler) consume() bool {
+ return atomic.CompareAndSwapUint32(&k.Status, argon2Unused, argon2Expired)
+}
+
+func (k *argon2OutOfProcessHandler) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) {
+ if !k.consume() {
+ return nil, errArgon2OutOfProcessHandlerExpired
+ }
+ return k.KDF.Derive(passphrase, salt, mode, params, keyLen)
+}
+
+func (k *argon2OutOfProcessHandler) Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) {
+ if !k.consume() {
+ return 0, errArgon2OutOfProcessHandlerExpired
+ }
+ return k.KDF.Time(mode, params)
+}
+
+const (
+ notArgon2HandlerProcess uint32 = 0
+ becomingArgon2HandlerProcess uint32 = 1
+ readyArgon2HandlerProcess uint32 = 2
+)
+
+var (
+ argon2OutOfProcessStatus uint32 = notArgon2HandlerProcess
+)
+
+// SetIsArgon2HandlerProcess marks this process as being a process capable of handling and
+// processing an Argon2 request on behalf of another process, and executing it in this process
+// before returning a response to the caller.
+//
+// Note that this can only be called once in a process lifetime. Calling it more than once
+// results in a panic. It shouldn't be used alongside [SetArgon2KDF] - if this has already been
+// called, a panic will occur as well. Applications should only use one of these functions in a
+// process.
+//
+// Calling this sets the process-wide Argon2 implementation (the one normally set via
+// [SetArgon2KDF]) to a variation of [InProcessArgon2KDF] that will only process a single
+// request before responding with an error on subsequent requests.
+//
+// Calling this function is required in order to be able to use [RunArgon2OutOfProcessRequest]
+// and [WaitForAndRunArgon2OutOfProcessRequest].
+func SetIsArgon2HandlerProcess() {
+ // Mark process as becoming an argon2 handler process. This will ensure that new calls
+ // to SetArgon2KDF to panic.
+ if !atomic.CompareAndSwapUint32(&argon2OutOfProcessStatus, notArgon2HandlerProcess, becomingArgon2HandlerProcess) {
+ panic("cannot call SetIsArgon2HandlerProcess more than once")
+ }
+
+ // Take the lock that SetArgon2KDF uses to wait for existing calls to finish if there
+ // are any pending.
+ argon2Mu.Lock()
+ defer argon2Mu.Unlock()
+
+ // There currently aren't any callers inside SetArgon2KDF, and we have the lock. We
+ // own the global KDF now - we're going to set the global implementation, overwriting
+ // whatever was there previously. Any future calls to SetArgon2KDF will panic.
+ argon2Impl = &argon2OutOfProcessHandler{
+ Status: argon2Unused,
+ KDF: InProcessArgon2KDF,
+ }
+
+ // Mark this process as ready so that RunArgon2OutOfProcessRequest and
+ // WaitForAndRunArgon2OutOfProcessRequest will work.
+ atomic.StoreUint32(&argon2OutOfProcessStatus, readyArgon2HandlerProcess)
+}
+
+// RunArgon2OutOfProcessRequest runs the specified argon2 request, and returns a response. This
+// function can only be called once in a process. Subsequent calls in the same process will result
+// in an error response being returned.
+//
+// This function requires [SetIsArgon2HandlerProcess] to have already been called in this process,
+// else an error response will be returned.
+func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2OutOfProcessResponse {
+ if atomic.LoadUint32(&argon2OutOfProcessStatus) < readyArgon2HandlerProcess {
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorProcessNotConfigured,
+ ErrorString: "cannot handle out-of-process request in a process that isn't configured as an Argon2 handler process",
+ }
+ }
+
+ switch request.Mode {
+ case Argon2id, Argon2i:
+ // ok
+ default:
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorInvalidMode,
+ ErrorString: fmt.Sprintf("invalid mode: %q", string(request.Mode)),
+ }
+ }
+
+ costParams := &Argon2CostParams{
+ Time: request.Time,
+ MemoryKiB: request.MemoryKiB,
+ Threads: request.Threads,
+ }
+ if costParams.Time == 0 {
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorInvalidTimeCost,
+ ErrorString: "invalid time cost: cannot be zero",
+ }
+ }
+ if costParams.Threads == 0 {
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorInvalidThreads,
+ ErrorString: "invalid threads: cannot be zero",
+ }
+ }
+
+ switch request.Command {
+ case Argon2OutOfProcessCommandDerive:
+ key, err := argon2KDF().Derive(request.Passphrase, request.Salt, request.Mode, costParams, request.Keylen)
+ if err != nil {
+ errorType := Argon2OutOfProcessErrorUnexpected
+ if errors.Is(err, errArgon2OutOfProcessHandlerExpired) {
+ // This process has already processed a request, so it should be restarted.
+ errorType = Argon2OutOfProcessErrorConsumedProcess
+ }
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: errorType,
+ ErrorString: fmt.Sprintf("cannot run derive command: %v", err),
+ }
+ }
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ Key: key,
+ }
+ case Argon2OutOfProcessCommandTime:
+ if len(request.Passphrase) > 0 {
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorUnexpectedInput,
+ ErrorString: "cannot supply passphrase for \"time\" command",
+ }
+ }
+ if len(request.Salt) > 0 {
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorUnexpectedInput,
+ ErrorString: "cannot supply salt for \"time\" command",
+ }
+ }
+ if request.Keylen > 0 {
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorUnexpectedInput,
+ ErrorString: "cannot supply keylen for \"time\" command",
+ }
+ }
+
+ duration, err := argon2KDF().Time(request.Mode, costParams)
+ if err != nil {
+ errorType := Argon2OutOfProcessErrorUnexpected
+ if errors.Is(err, errArgon2OutOfProcessHandlerExpired) {
+ // This process has already processed a request, so it should be restarted.
+ errorType = Argon2OutOfProcessErrorConsumedProcess
+ }
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: errorType,
+ ErrorString: fmt.Sprintf("cannot run time command: %v", err),
+ }
+ }
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ Duration: duration,
+ }
+ default:
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorInvalidCommand,
+ ErrorString: fmt.Sprintf("invalid command: %q", string(request.Command)),
+ }
+ }
+}
+
+// WaitForAndRunArgon2OutOfProcessRequest waits for a [Argon2OutOfProcessRequest] request on the
+// supplied io.Reader before running it and sending a [Argon2OutOfProcessResponse] response back via
+// the supplied io.Writer. These will generally be connected to the process's os.Stdin and
+// os.Stdout - at least they will need to be when using [NewOutOfProcessKDF] on the parent side.
+//
+// This function can only be called once in a process. Subsequent calls in the same process will
+// result in an error response being returned via the io.Writer (after receiving a new request via
+// the io.Reader).
+//
+// This function requires [SetIsArgon2HandlerProcess] to have already been called in this process,
+// else an error response will be returned via the io.Writer.
+func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer) error {
+ var req *Argon2OutOfProcessRequest
+ dec := json.NewDecoder(in)
+ dec.DisallowUnknownFields()
+ if err := dec.Decode(&req); err != nil {
+ return fmt.Errorf("cannot decode request: %w", err)
+ }
+
+ rsp := RunArgon2OutOfProcessRequest(req)
+
+ enc := json.NewEncoder(out)
+ if err := enc.Encode(rsp); err != nil {
+ return fmt.Errorf("cannot encode response: %w", err)
+ }
+
+ return nil
+}
+
+// outOfProcessArgon2KDFImpl is an Argon2KDFImpl that runs the KDF in a short-lived
+// helper process, using the remote JSON protocol defined in this package.
+type outOfProcessArgon2KDFImpl struct {
+ newHandlerCmd func() (*exec.Cmd, error)
+}
+
+func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2OutOfProcessRequest) (rsp *Argon2OutOfProcessResponse, err error) {
+ cmd, err := k.newHandlerCmd()
+ if err != nil {
+ return nil, fmt.Errorf("cannot create new command: %w", err)
+ }
+
+ stdinPipe, err := cmd.StdinPipe()
+ if err != nil {
+ // This doesn't fail once the OS pipe is created, so there's no
+ // cleanup to do on failure paths.
+ return nil, fmt.Errorf("cannot create stdin pipe: %w", err)
+ }
+ stdoutPipe, err := cmd.StdoutPipe()
+ if err != nil {
+ // This doesn't fail once the OS pipe is created, so there's no
+ // cleanup to do on failure paths.
+ return nil, fmt.Errorf("cannot create stdout pipe: %w", err)
+ }
+
+ if err := cmd.Start(); err != nil {
+ return nil, fmt.Errorf("cannot start handler process: %w", err)
+ }
+ defer func() {
+ // Run Cmd.Wait in a defer so that we shut down on error paths too,
+ // and we capture the Wait error if there was no other error.
+ waitErr := cmd.Wait()
+ if waitErr != nil && err == nil {
+ rsp = nil
+ err = fmt.Errorf("cannot wait for remote process to finish: %w", waitErr)
+ }
+ }()
+
+ // Send the input params to the remote process.
+ enc := json.NewEncoder(stdinPipe)
+ if err := enc.Encode(req); err != nil {
+ return nil, fmt.Errorf("cannot encode request: %w", err)
+ }
+
+ // Wait for thre result from the remote process.
+ dec := json.NewDecoder(stdoutPipe)
+ if err := dec.Decode(&rsp); err != nil {
+ return nil, fmt.Errorf("cannot decode response: %w", err)
+ }
+
+ return rsp, nil
+}
+
+func (k *outOfProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) (key []byte, err error) {
+ req := &Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: passphrase,
+ Salt: salt,
+ Keylen: keyLen,
+ Mode: mode,
+ Time: params.Time,
+ MemoryKiB: params.MemoryKiB,
+ Threads: params.Threads,
+ }
+ rsp, err := k.sendRequestAndWaitForResponse(req)
+ if err != nil {
+ return nil, err
+ }
+ if rsp.Err() != nil {
+ return nil, rsp.Err()
+ }
+ return rsp.Key, nil
+}
+
+func (k *outOfProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (duration time.Duration, err error) {
+ req := &Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandTime,
+ Mode: mode,
+ Time: params.Time,
+ MemoryKiB: params.MemoryKiB,
+ Threads: params.Threads,
+ }
+ rsp, err := k.sendRequestAndWaitForResponse(req)
+ if err != nil {
+ return 0, err
+ }
+ if rsp.Err() != nil {
+ return 0, rsp.Err()
+ }
+ return rsp.Duration, nil
+}
+
+// NewOutOfProcessArgonKDF returns a new Argon2KDF that runs each KDF invocation in a
+// short-lived handler process, using a *[exec.Cmd] created by the supplied function,
+// and using a protocol compatibile with [WaitForAndRunArgon2OutOfProcessRequest]
+// in the handler process.
+//
+// The supplied function must not start the process, nor should it set the Stdin or
+// Stdout fields of the [exec.Cmd] structure, as 2 pipes will be created for sending
+// the request to the process via its stdin and receiving the response from the process
+// via its stdout.
+func NewOutOfProcessArgon2KDF(newHandlerCmd func() (*exec.Cmd, error)) Argon2KDF {
+ if newHandlerCmd == nil {
+ panic("newHandlerCmd cannot be nil")
+ }
+ return &outOfProcessArgon2KDFImpl{
+ newHandlerCmd: newHandlerCmd,
+ }
+}
diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go
new file mode 100644
index 00000000..106bbc76
--- /dev/null
+++ b/argon2_out_of_process_support_test.go
@@ -0,0 +1,624 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2024 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package secboot_test
+
+import (
+ "encoding/json"
+ "errors"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "sync"
+
+ . "github.com/snapcore/secboot"
+ "github.com/snapcore/secboot/internal/testutil"
+ . "gopkg.in/check.v1"
+)
+
+type argon2OutOfProcessSupportSuite struct{}
+
+func (s *argon2OutOfProcessSupportSuite) TearDownTest(c *C) {
+ ClearIsArgon2HandlerProcess()
+}
+
+var _ = Suite(&argon2OutOfProcessSupportSuite{})
+
+func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidProcess(c *C) {
+ out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: nil,
+ Keylen: 32,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ ErrorType: Argon2OutOfProcessErrorProcessNotConfigured,
+ ErrorString: "cannot handle out-of-process request in a process that isn't configured as an Argon2 handler process",
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidMode(c *C) {
+ SetIsArgon2HandlerProcess()
+ out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: nil,
+ Keylen: 32,
+ Mode: Argon2Mode("foo"),
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ ErrorType: Argon2OutOfProcessErrorInvalidMode,
+ ErrorString: "invalid mode: \"foo\"",
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidTime(c *C) {
+ SetIsArgon2HandlerProcess()
+ out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: nil,
+ Keylen: 32,
+ Mode: Argon2id,
+ Time: 0,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ ErrorType: Argon2OutOfProcessErrorInvalidTimeCost,
+ ErrorString: "invalid time cost: cannot be zero",
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidThreads(c *C) {
+ SetIsArgon2HandlerProcess()
+ out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: nil,
+ Keylen: 32,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 0,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ ErrorType: Argon2OutOfProcessErrorInvalidThreads,
+ ErrorString: "invalid threads: cannot be zero",
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidPassphrase(c *C) {
+ SetIsArgon2HandlerProcess()
+ out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandTime,
+ Passphrase: "foo",
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandTime,
+ ErrorType: Argon2OutOfProcessErrorUnexpectedInput,
+ ErrorString: "cannot supply passphrase for \"time\" command",
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidSalt(c *C) {
+ SetIsArgon2HandlerProcess()
+ out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandTime,
+ Salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandTime,
+ ErrorType: Argon2OutOfProcessErrorUnexpectedInput,
+ ErrorString: "cannot supply salt for \"time\" command",
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidKeylen(c *C) {
+ SetIsArgon2HandlerProcess()
+ out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandTime,
+ Keylen: 32,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandTime,
+ ErrorType: Argon2OutOfProcessErrorUnexpectedInput,
+ ErrorString: "cannot supply keylen for \"time\" command",
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessInvalidCommand(c *C) {
+ SetIsArgon2HandlerProcess()
+ out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommand("foo"),
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommand("foo"),
+ ErrorType: Argon2OutOfProcessErrorInvalidCommand,
+ ErrorString: "invalid command: \"foo\"",
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestArgon2OutOfProcessResponseErr(c *C) {
+ out := &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ ErrorType: Argon2OutOfProcessErrorProcessNotConfigured,
+ ErrorString: "cannot run in a process that isn't configured as an Argon2 remote process",
+ }
+ err := out.Err()
+ c.Check(err, ErrorMatches, `cannot process KDF request: process-not-configured \(cannot run in a process that isn't configured as an Argon2 remote process\)`)
+ var e *Argon2OutOfProcessError
+ c.Check(errors.As(err, &e), testutil.IsTrue)
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestCallingSetIsArgon2HandlerSucceeds(c *C) {
+ SetIsArgon2HandlerProcess()
+ c.Assert(GlobalArgon2KDF(), testutil.ConvertibleTo, &Argon2OutOfProcessHandler{})
+ c.Check(GlobalArgon2KDF().(*Argon2OutOfProcessHandler).KDF, Equals, InProcessArgon2KDF)
+ c.Check(GlobalArgon2KDF().(*Argon2OutOfProcessHandler).Status, Equals, uint32(0))
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestCallingSetIsArgon2HandlerMoreThanOncePanics(c *C) {
+ SetIsArgon2HandlerProcess()
+ c.Check(func() { SetIsArgon2HandlerProcess() }, PanicMatches, `cannot call SetIsArgon2HandlerProcess more than once`)
+}
+
+func (s *argon2OutOfProcessSupportSuite) TestCallingSetIsArgon2HandlerAfterSetArgon2KDFSucceeds(c *C) {
+ SetArgon2KDF(InProcessArgon2KDF)
+ SetIsArgon2HandlerProcess()
+ c.Assert(GlobalArgon2KDF(), testutil.ConvertibleTo, &Argon2OutOfProcessHandler{})
+ c.Check(GlobalArgon2KDF().(*Argon2OutOfProcessHandler).KDF, Equals, InProcessArgon2KDF)
+ c.Check(GlobalArgon2KDF().(*Argon2OutOfProcessHandler).Status, Equals, uint32(0))
+}
+
+type argon2OutOfProcessSupportSuiteExpensive struct {
+ runArgon2OutputDir string
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) runArgon2HandlerPath() string {
+ return filepath.Join(s.runArgon2OutputDir, "run_argon2")
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) SetUpSuite(c *C) {
+ if _, exists := os.LookupEnv("NO_ARGON2_TESTS"); exists {
+ c.Skip("skipping expensive argon2 tests")
+ }
+ s.runArgon2OutputDir = c.MkDir()
+ cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), "build", "-o", s.runArgon2OutputDir, "./cmd/run_argon2")
+ c.Assert(cmd.Run(), IsNil)
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) SetUpTest(c *C) {
+ SetIsArgon2HandlerProcess()
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TearDownTest(c *C) {
+ ClearIsArgon2HandlerProcess()
+}
+
+var _ = Suite(&argon2OutOfProcessSupportSuiteExpensive{})
+
+type testRunArgon2OutOfProcessRequestDeriveParams struct {
+ req *Argon2OutOfProcessRequest
+ expectedKey []byte
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) testRunArgon2OutOfProcessRequestDerive(c *C, params *testRunArgon2OutOfProcessRequestDeriveParams) {
+ rsp := RunArgon2OutOfProcessRequest(params.req)
+ c.Assert(rsp, NotNil)
+ c.Check(rsp.Command, Equals, Argon2OutOfProcessCommandDerive)
+ c.Check(rsp.Err(), IsNil)
+ c.Check(rsp.Key, DeepEquals, params.expectedKey)
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDerive(c *C) {
+ s.testRunArgon2OutOfProcessRequestDerive(c, &testRunArgon2OutOfProcessRequestDeriveParams{
+ req: &Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ Keylen: 32,
+ },
+ expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDeriveDifferentPassphrase(c *C) {
+ s.testRunArgon2OutOfProcessRequestDerive(c, &testRunArgon2OutOfProcessRequestDeriveParams{
+ req: &Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "bar",
+ Salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ Keylen: 32,
+ },
+ expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDeriveDifferentSalt(c *C) {
+ s.testRunArgon2OutOfProcessRequestDerive(c, &testRunArgon2OutOfProcessRequestDeriveParams{
+ req: &Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: []byte("zyxwtsrqponmlkjihgfedcba987654"),
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ Keylen: 32,
+ },
+ expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDeriveDifferentMode(c *C) {
+ s.testRunArgon2OutOfProcessRequestDerive(c, &testRunArgon2OutOfProcessRequestDeriveParams{
+ req: &Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ Mode: Argon2i,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ Keylen: 32,
+ },
+ expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDeriveDifferentParams(c *C) {
+ s.testRunArgon2OutOfProcessRequestDerive(c, &testRunArgon2OutOfProcessRequestDeriveParams{
+ req: &Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ Mode: Argon2id,
+ Time: 48,
+ MemoryKiB: 32 * 1024,
+ Threads: 4,
+ Keylen: 32,
+ },
+ expectedKey: testutil.DecodeHexString(c, "f83001f90fbbc24823773e56f65eeace261285ab7e1394efeb8348d2184c240c"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDeriveDifferentKeylen(c *C) {
+ s.testRunArgon2OutOfProcessRequestDerive(c, &testRunArgon2OutOfProcessRequestDeriveParams{
+ req: &Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ Keylen: 64,
+ },
+ expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestTime(c *C) {
+ res := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandTime,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32 * 1024,
+ Threads: 4,
+ })
+ c.Check(res.Err(), IsNil)
+
+ ClearIsArgon2HandlerProcess()
+ SetIsArgon2HandlerProcess()
+ res2 := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandTime,
+ Mode: Argon2id,
+ Time: 16,
+ MemoryKiB: 32 * 1024,
+ Threads: 4,
+ })
+ c.Check(res2.Err(), IsNil)
+ // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
+ // types of int64 kind
+ c.Check(res2.Duration > res.Duration, testutil.IsTrue)
+
+ ClearIsArgon2HandlerProcess()
+ SetIsArgon2HandlerProcess()
+ res2 = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandTime,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 128 * 1024,
+ Threads: 4,
+ })
+ c.Check(res2.Err(), IsNil)
+ // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
+ // types of int64 kind
+ c.Check(res2.Duration > res.Duration, testutil.IsTrue)
+
+ ClearIsArgon2HandlerProcess()
+ SetIsArgon2HandlerProcess()
+ res2 = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandTime,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32 * 1024,
+ Threads: 1,
+ })
+ c.Check(res2.Err(), IsNil)
+ // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
+ // types of int64 kind
+ c.Check(res2.Duration > res.Duration, testutil.IsTrue)
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDeriveConsumedProcess(c *C) {
+ out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: nil,
+ Keylen: 32,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, NotNil)
+
+ out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: nil,
+ Keylen: 32,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ ErrorType: Argon2OutOfProcessErrorConsumedProcess,
+ ErrorString: "cannot run derive command: argon2 out-of-process handler has alreay been used - a new process should be started to handle a new request",
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestTimeConsumedProcess(c *C) {
+ out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandTime,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, NotNil)
+
+ out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandTime,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandTime,
+ ErrorType: Argon2OutOfProcessErrorConsumedProcess,
+ ErrorString: "cannot run time command: argon2 out-of-process handler has alreay been used - a new process should be started to handle a new request",
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestWaitForAndRunOutOfProcessArgon2Request(c *C) {
+ var wg sync.WaitGroup
+ wg.Add(1)
+
+ reqR, reqW := io.Pipe()
+ rspR, rspW := io.Pipe()
+
+ go func() {
+ c.Check(WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW), IsNil)
+ wg.Done()
+ }()
+
+ enc := json.NewEncoder(reqW)
+ c.Check(enc.Encode(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4,
+ Keylen: 32,
+ }), IsNil)
+
+ dec := json.NewDecoder(rspR)
+ var rsp *Argon2OutOfProcessResponse
+ c.Check(dec.Decode(&rsp), IsNil)
+ c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ Key: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"),
+ })
+
+ wg.Wait()
+}
+
+type testOutOfProcessArgon2DeriveParams struct {
+ passphrase string
+ salt []byte
+ mode Argon2Mode
+ params *Argon2CostParams
+ keyLen uint32
+ expectedKey []byte
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) testOutOfProcessArgon2Derive(c *C, params *testOutOfProcessArgon2DeriveParams) {
+ kdf := NewOutOfProcessArgon2KDF(func() (*exec.Cmd, error) {
+ return exec.Command(s.runArgon2HandlerPath()), nil
+ })
+ key, err := kdf.Derive(params.passphrase, params.salt, params.mode, params.params, params.keyLen)
+ c.Check(err, IsNil)
+ c.Check(key, DeepEquals, params.expectedKey)
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2Derive(c *C) {
+ s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{
+ passphrase: "foo",
+ salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ mode: Argon2id,
+ params: &Argon2CostParams{
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4},
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentPassphrase(c *C) {
+ s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{
+ passphrase: "bar",
+ salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ mode: Argon2id,
+ params: &Argon2CostParams{
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4},
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentSalt(c *C) {
+ s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{
+ passphrase: "foo",
+ salt: []byte("zyxwtsrqponmlkjihgfedcba987654"),
+ mode: Argon2id,
+ params: &Argon2CostParams{
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4},
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentMode(c *C) {
+ s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{
+ passphrase: "foo",
+ salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ mode: Argon2i,
+ params: &Argon2CostParams{
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4},
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentParams(c *C) {
+ s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{
+ passphrase: "foo",
+ salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ mode: Argon2id,
+ params: &Argon2CostParams{
+ Time: 48,
+ MemoryKiB: 32 * 1024,
+ Threads: 4},
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "f83001f90fbbc24823773e56f65eeace261285ab7e1394efeb8348d2184c240c"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentKeyLen(c *C) {
+ s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{
+ passphrase: "foo",
+ salt: []byte("0123456789abcdefghijklmnopqrstuv"),
+ mode: Argon2id,
+ params: &Argon2CostParams{
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 4},
+ keyLen: 64,
+ expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"),
+ })
+}
+
+func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2Time(c *C) {
+ kdf := NewOutOfProcessArgon2KDF(func() (*exec.Cmd, error) {
+ return exec.Command(s.runArgon2HandlerPath()), nil
+ })
+
+ time1, err := kdf.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4})
+ c.Check(err, IsNil)
+
+ time2, err := kdf.Time(Argon2id, &Argon2CostParams{Time: 16, MemoryKiB: 32 * 1024, Threads: 4})
+ c.Check(err, IsNil)
+ // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
+ // types of int64 kind
+ c.Check(time2 > time1, testutil.IsTrue)
+
+ time2, err = kdf.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 128 * 1024, Threads: 4})
+ c.Check(err, IsNil)
+ // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
+ // types of int64 kind
+ c.Check(time2 > time1, testutil.IsTrue)
+
+ time2, err = kdf.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 1})
+ c.Check(err, IsNil)
+ // XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
+ // types of int64 kind
+ c.Check(time2 > time1, testutil.IsTrue)
+}
diff --git a/argon2_test.go b/argon2_test.go
index 34cc8ba0..13c2fc64 100644
--- a/argon2_test.go
+++ b/argon2_test.go
@@ -32,7 +32,6 @@ import (
. "gopkg.in/check.v1"
. "github.com/snapcore/secboot"
- "github.com/snapcore/secboot/internal/argon2"
"github.com/snapcore/secboot/internal/testutil"
)
@@ -249,16 +248,16 @@ func (s *argon2Suite) TestKDFParamsInvalidMemoryKiB(c *C) {
c.Check(err, ErrorMatches, `invalid memory cost 4294967295KiB`)
}
+func (s *argon2Suite) TestInProcessKDFDeriveNoParams(c *C) {
+ _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, nil, 32)
+ c.Check(err, ErrorMatches, `nil params`)
+}
+
func (s *argon2Suite) TestInProcessKDFDeriveInvalidMode(c *C) {
_, err := InProcessArgon2KDF.Derive("foo", nil, Argon2Default, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}, 32)
c.Check(err, ErrorMatches, `invalid mode`)
}
-func (s *argon2Suite) TestInProcessKDFDeriveInvalidParams(c *C) {
- _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, nil, 32)
- c.Check(err, ErrorMatches, `nil params`)
-}
-
func (s *argon2Suite) TestInProcessKDFDeriveInvalidTime(c *C) {
_, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1}, 32)
c.Check(err, ErrorMatches, `invalid time cost`)
@@ -269,16 +268,16 @@ func (s *argon2Suite) TestInProcessKDFDeriveInvalidThreads(c *C) {
c.Check(err, ErrorMatches, `invalid number of threads`)
}
+func (s *argon2Suite) TestInProcessKDFTimeNoParams(c *C) {
+ _, err := InProcessArgon2KDF.Time(Argon2id, nil)
+ c.Check(err, ErrorMatches, `nil params`)
+}
+
func (s *argon2Suite) TestInProcessKDFTimeInvalidMode(c *C) {
_, err := InProcessArgon2KDF.Time(Argon2Default, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1})
c.Check(err, ErrorMatches, `invalid mode`)
}
-func (s *argon2Suite) TestInProcessKDFTimeInvalidParams(c *C) {
- _, err := InProcessArgon2KDF.Time(Argon2id, nil)
- c.Check(err, ErrorMatches, `nil params`)
-}
-
func (s *argon2Suite) TestInProcessKDFTimeInvalidTime(c *C) {
_, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1})
c.Check(err, ErrorMatches, `invalid time cost`)
@@ -289,39 +288,47 @@ func (s *argon2Suite) TestInProcessKDFTimeInvalidThreads(c *C) {
c.Check(err, ErrorMatches, `invalid number of threads`)
}
-type argon2SuiteExpensive struct{}
+func (s *argon2Suite) TestSetArgon2KDFSuccess(c *C) {
+ SetArgon2KDF(InProcessArgon2KDF)
+ c.Check(GlobalArgon2KDF(), Equals, InProcessArgon2KDF)
+ c.Check(SetArgon2KDF(nil), Equals, InProcessArgon2KDF)
+ _, err := GlobalArgon2KDF().Time(Argon2id, nil)
+ c.Check(err, ErrorMatches, `no argon2 KDF: please call secboot.SetArgon2KDF`)
+}
+
+func (s *argon2Suite) TestSetArgon2KDFInHandlerProcessPanics(c *C) {
+ SetIsArgon2HandlerProcess()
+ defer ClearIsArgon2HandlerProcess()
+ c.Check(func() { SetArgon2KDF(InProcessArgon2KDF) }, PanicMatches, `cannot call SetArgon2KDF in a process where SetIsArgon2HandlerProcess has already been called`)
+}
+
+type argon2Expensive struct{}
-func (s *argon2SuiteExpensive) SetUpSuite(c *C) {
+var _ = Suite(&argon2Expensive{})
+
+func (s *argon2Expensive) SetUpSuite(c *C) {
if _, exists := os.LookupEnv("NO_ARGON2_TESTS"); exists {
c.Skip("skipping expensive argon2 tests")
}
}
-var _ = Suite(&argon2SuiteExpensive{})
-
type testInProcessArgon2KDFDeriveData struct {
passphrase string
salt []byte
mode Argon2Mode
params *Argon2CostParams
keyLen uint32
+
+ expectedKey []byte
}
-func (s *argon2SuiteExpensive) testInProcessKDFDerive(c *C, data *testInProcessArgon2KDFDeriveData) {
+func (s *argon2Expensive) testInProcessKDFDerive(c *C, data *testInProcessArgon2KDFDeriveData) {
key, err := InProcessArgon2KDF.Derive(data.passphrase, data.salt, data.mode, data.params, data.keyLen)
c.Check(err, IsNil)
- runtime.GC()
-
- expected := argon2.Key(data.passphrase, data.salt, argon2.Mode(data.mode), &argon2.CostParams{
- Time: data.params.Time,
- MemoryKiB: data.params.MemoryKiB,
- Threads: data.params.Threads}, data.keyLen)
- runtime.GC()
-
- c.Check(key, DeepEquals, expected)
+ c.Check(key, DeepEquals, data.expectedKey)
}
-func (s *argon2SuiteExpensive) TestInProcessKDFDerive(c *C) {
+func (s *argon2Expensive) TestInProcessKDFDerive(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "foo",
salt: []byte("0123456789abcdefghijklmnopqrstuv"),
@@ -330,10 +337,12 @@ func (s *argon2SuiteExpensive) TestInProcessKDFDerive(c *C) {
Time: 4,
MemoryKiB: 32,
Threads: 4},
- keyLen: 32})
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) {
+func (s *argon2Expensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "bar",
salt: []byte("0123456789abcdefghijklmnopqrstuv"),
@@ -342,22 +351,26 @@ func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) {
Time: 4,
MemoryKiB: 32,
Threads: 4},
- keyLen: 32})
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFiDeriveDifferentSalt(c *C) {
+func (s *argon2Expensive) TestInProcessKDFiDeriveDifferentSalt(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "foo",
- salt: []byte("zyxwvutsrqponmlkjihgfedcba987654"),
+ salt: []byte("zyxwtsrqponmlkjihgfedcba987654"),
mode: Argon2id,
params: &Argon2CostParams{
Time: 4,
MemoryKiB: 32,
Threads: 4},
- keyLen: 32})
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentMode(c *C) {
+func (s *argon2Expensive) TestInProcessKDFDeriveDifferentMode(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "foo",
salt: []byte("0123456789abcdefghijklmnopqrstuv"),
@@ -366,10 +379,12 @@ func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentMode(c *C) {
Time: 4,
MemoryKiB: 32,
Threads: 4},
- keyLen: 32})
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentParams(c *C) {
+func (s *argon2Expensive) TestInProcessKDFDeriveDifferentParams(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "foo",
salt: []byte("0123456789abcdefghijklmnopqrstuv"),
@@ -378,10 +393,12 @@ func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentParams(c *C) {
Time: 48,
MemoryKiB: 32 * 1024,
Threads: 4},
- keyLen: 32})
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "f83001f90fbbc24823773e56f65eeace261285ab7e1394efeb8348d2184c240c"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) {
+func (s *argon2Expensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "foo",
salt: []byte("0123456789abcdefghijklmnopqrstuv"),
@@ -390,30 +407,31 @@ func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) {
Time: 4,
MemoryKiB: 32,
Threads: 4},
- keyLen: 64})
+ keyLen: 64,
+ expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFTime(c *C) {
+func (s *argon2Expensive) TestInProcessKDFTime(c *C) {
time1, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4})
- runtime.GC()
c.Check(err, IsNil)
- time2, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 16, MemoryKiB: 32 * 1024, Threads: 4})
runtime.GC()
+ time2, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 16, MemoryKiB: 32 * 1024, Threads: 4})
c.Check(err, IsNil)
// XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
// types of int64 kind
c.Check(time2 > time1, testutil.IsTrue)
- time2, err = InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 128 * 1024, Threads: 4})
runtime.GC()
+ time2, err = InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 128 * 1024, Threads: 4})
c.Check(err, IsNil)
// XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
// types of int64 kind
c.Check(time2 > time1, testutil.IsTrue)
- time2, err = InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 1})
runtime.GC()
+ time2, err = InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 1})
c.Check(err, IsNil)
// XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
// types of int64 kind
diff --git a/cmd/run_argon2/main.go b/cmd/run_argon2/main.go
new file mode 100644
index 00000000..2b5a7a81
--- /dev/null
+++ b/cmd/run_argon2/main.go
@@ -0,0 +1,50 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2024-2024 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "github.com/snapcore/secboot"
+)
+
+func run() error {
+ if len(os.Args) != 1 {
+ return errors.New("usage: echo | run_argon2")
+ }
+
+ secboot.SetIsArgon2HandlerProcess()
+
+ err := secboot.WaitForAndRunArgon2OutOfProcessRequest(os.Stdin, os.Stdout)
+ if err != nil {
+ return fmt.Errorf("cannot run request: %w", err)
+ }
+
+ return nil
+}
+
+func main() {
+ if err := run(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+}
diff --git a/export_test.go b/export_test.go
index a284fddf..3acfb4f7 100644
--- a/export_test.go
+++ b/export_test.go
@@ -21,19 +21,23 @@ package secboot
import (
"io"
+ "runtime"
+ "sync/atomic"
"github.com/snapcore/secboot/internal/luks2"
"github.com/snapcore/secboot/internal/luksview"
)
var (
+ GlobalArgon2KDF = argon2KDF
UnmarshalV1KeyPayload = unmarshalV1KeyPayload
UnmarshalProtectedKeys = unmarshalProtectedKeys
)
type (
- KdfParams = kdfParams
- ProtectedKeys = protectedKeys
+ Argon2OutOfProcessHandler = argon2OutOfProcessHandler
+ KdfParams = kdfParams
+ ProtectedKeys = protectedKeys
)
func (o *Argon2Options) KdfParams(keyLen uint32) (*KdfParams, error) {
@@ -148,6 +152,19 @@ func MockHashAlgAvailable() (restore func()) {
}
}
+// ClearIsArgon2HandlerProcess does something that isn't possible in production code
+// and turns an argon2 handler process back into a process that isn't configured to
+// handle argon2 requests. The only reason to do this is to bypass the limitation that
+// a handler process can only handle one request, so we also run a garbage collection
+// here to ensure the test binary doesn't run out of memory. It's quite possible that this
+// function violates any safety provided by the atomic modifications to the
+// argon2OutOfProcessStatus global variable and introduces race conditions that aren't
+// present in production code.
+func ClearIsArgon2HandlerProcess() {
+ atomic.StoreUint32(&argon2OutOfProcessStatus, notArgon2HandlerProcess)
+ runtime.GC()
+}
+
func (d *KeyData) DerivePassphraseKeys(passphrase string) (key, iv, auth []byte, err error) {
return d.derivePassphraseKeys(passphrase)
}