From 017287bc5671ab0bbcc9e3dd8f5c75de5458b09d Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Tue, 3 Sep 2024 21:54:37 +0100 Subject: [PATCH 01/40] argon2: Add helpers for running the KDF remotely As Argon2 is memory intensive, it's not suitable for multiple invocations in long-lived garbage collected processes. For this reason, Argon2 is abstracted with an interface (Argon2KDF), of which the application sets a global version of this which is intended to proxy KDF requests to a short-lived remote process which uses the real InProcessArgon2KDF. This adds some functionality to facilitate this. First of all, InProcessArgon2KDF is no longer a variable - it's a function. By default, it's methods return an error unless the application code has called SetIsArgon2RemoteProcess, which unlocks the real in-process KDF. Then there are JSON serializable types "Argon2RemoteInput" and "Argon2RemoteOutput". The input can be fed directly to RunArgon2RequestInRemoteProcess on the remote side, but this is a fairly low-level API. There is a higher level API - NewRemoteArgon2KDF, for use in the application process, and which returns an implementation of Argon2KDF which proxies requests to a short-lived remote helper process. The caller supplied a function to construct an appropriate exec.Cmd instance for this. This function is configured so that the remote process recieves a request on stdin and it expects a response on stdout. The remote process passes both os.Stdin and os.Stdout to WaitAndRunArgon2RequestInRemoteProcess, although it doesn't hardcode these descriptors for implementations that want to construct their own transport that doesn't rely on stdin and stdout. Once a remote process has completed a request, it should exit cleanly. Neither RunArgon2RequestInRemoteProcess or WaitAndRunArgon2RequestInRemoteProcess support being called more than once in the same process. The code in cmd/run_argon2 provides an example remote process, although this is mainly useful for unit testing (where it is currently used). It is envisaged that the remote process will be a special mode of snapd and snap-bootstrap in order to avoid adding an additional new go binary just for this. --- .gitignore | 1 + argon2.go | 38 -- argon2_remote_support.go | 460 ++++++++++++++++++++++++ argon2_remote_support_test.go | 643 ++++++++++++++++++++++++++++++++++ argon2_test.go | 173 --------- cmd/run_argon2/main.go | 50 +++ export_test.go | 7 + 7 files changed, 1161 insertions(+), 211 deletions(-) create mode 100644 argon2_remote_support.go create mode 100644 argon2_remote_support_test.go create mode 100644 cmd/run_argon2/main.go diff --git a/.gitignore b/.gitignore index 5a46d357..eb54f6c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +run_argon2 vendor/*/ diff --git a/argon2.go b/argon2.go index 2209816c..6d5aa507 100644 --- a/argon2.go +++ b/argon2.go @@ -225,44 +225,6 @@ type Argon2KDF interface { Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) } -type inProcessArgon2KDFImpl struct{} - -func (_ inProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) { - switch { - case mode != Argon2i && mode != Argon2id: - return nil, errors.New("invalid mode") - case params == nil: - return nil, errors.New("nil params") - case params.Time == 0: - return nil, errors.New("invalid time cost") - case params.Threads == 0: - return nil, errors.New("invalid number of threads") - } - - return argon2.Key(passphrase, salt, argon2.Mode(mode), params.internalParams(), keyLen), nil -} - -func (_ inProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) { - switch { - case mode != Argon2i && mode != Argon2id: - return 0, errors.New("invalid mode") - case params == nil: - return 0, errors.New("nil params") - case params.Time == 0: - return 0, errors.New("invalid time cost") - case params.Threads == 0: - return 0, errors.New("invalid number of threads") - } - - 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. -var InProcessArgon2KDF = inProcessArgon2KDFImpl{} - type nullArgon2KDFImpl struct{} func (_ nullArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) { diff --git a/argon2_remote_support.go b/argon2_remote_support.go new file mode 100644 index 00000000..d75cadbe --- /dev/null +++ b/argon2_remote_support.go @@ -0,0 +1,460 @@ +// -*- 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" + + "github.com/snapcore/secboot/internal/argon2" +) + +type nullInProcessArgon2KDFImpl struct{} + +func (_ nullInProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) { + return nil, errors.New("no argon2 KDF: please call secboot.SetIsArgon2RemoteProcess if the intention is to run Argon2 directly in this process") +} + +func (_ nullInProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) { + return 0, errors.New("no argon2 KDF: please call secboot.SetIsArgon2RemoteProcess if the intention is to run Argon2 directly in this process") +} + +type inProcessArgon2KDFImpl struct{} + +func (_ inProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) { + switch { + case mode != Argon2i && mode != Argon2id: + return nil, errors.New("invalid mode") + case params == nil: + return nil, errors.New("nil params") + case params.Time == 0: + return nil, errors.New("invalid time cost") + case params.Threads == 0: + return nil, errors.New("invalid number of threads") + } + + return argon2.Key(passphrase, salt, argon2.Mode(mode), params.internalParams(), keyLen), nil +} + +func (_ inProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) { + switch { + case mode != Argon2i && mode != Argon2id: + return 0, errors.New("invalid mode") + case params == nil: + return 0, errors.New("nil params") + case params.Time == 0: + return 0, errors.New("invalid time cost") + case params.Threads == 0: + return 0, errors.New("invalid number of threads") + } + + return argon2.KeyDuration(argon2.Mode(mode), params.internalParams()), nil +} + +// InProcessArgon2KDF returns 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 +// [Argon2KDF] implementation which proxies requests to a short-lived remote process which will +// use this in-process implementation once and then exit. This approach avoids memory exhaustion +// and the need to run a full GC mark and sweep ([runtime.GC]) between invocations, which has +// a significant time penalty. Argon2 isn't really compativle with garbage collected runtimes. +// +// There are plenty of helpers in this package to facilitate this, such as JSON serializable +// types ([Argon2RemoteInput] and [Argon2RemoteOutput]) and a function ([RunArgon2KDFInRemoteProcess]) +// that can process these types to run the KDF in-process using a request from a parent process. +// +// There are higher-level helpers too, such as an [Argon2KDF] implementation that can be created +// in the parent process and which creates new remote processes to send each command to (see +// [NewRemoteArgon2KDF]). The remote process is then able to process an incoming request by passing +// [os.Stdin] and [os.Stdout] directly to [WaitAndRunArgon2RequestInRemoteProcess]. +// +// It is indended that the remote process is a special mode of argv[0] (ie, snapd or snap-bootstrap) +// in order to avoid the bloat of adding additional go binaries. +// +// A process must call [SetIsArgon2RemoteProcess] before this returns anything other than a null +// implementation of the KDF. +// +// Note that whilst [WaitAndRunArgon2RequestInRemoteProcess] and [RunArgon2KDFInRemoteProcess] +// contain protections that only allow a single invocation of this KDF in the lifetime of a process, +// direct access via this API doesn't provde the same protections. +func InProcessArgon2KDF() Argon2KDF { + if atomic.LoadUint32(&argon2RemoteProcessStatus) > notArgon2RemoteProcess { + return inProcessArgon2KDFImpl{} + } + return nullInProcessArgon2KDFImpl{} +} + +// Argon2RemoteCommand represents the command to run. +type Argon2RemoteCommand string + +const ( + // Argon2RemoteCommandDerive requests to derive a key from a passphrase + Argon2RemoteCommandDerive Argon2RemoteCommand = "derive" + + // Argon2RemoteCommandTime requests the duration that the KDF took to + // execute. This excludes things like process startup. + Argon2RemoteCommandTime Argon2RemoteCommand = "time" +) + +// Argon2RemoteInput is an input request for an argon2 operation in +// a remote process. +type Argon2RemoteInput struct { + Command Argon2RemoteCommand `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 +} + +// Argon2RemoteErrorType describes the type of error produced by [RunArgon2RequestInRemoteProcess]. +type Argon2RemoteErrorType string + +const ( + // Argon2RemoteErrorInvalidCommand means that an invalid command was supplied. + Argon2RemoteErrorInvalidCommand Argon2RemoteErrorType = "invalid-command" + + // Argon2RemoteErrorInvalidMode means that an invalid mode was supplied. + Argon2RemoteErrorInvalidMode Argon2RemoteErrorType = "invalid-mode" + + // Argon2RemoteErrorInvalidTimeCost means that an invalid time cost was supplied. + Argon2RemoteErrorInvalidTimeCost Argon2RemoteErrorType = "invalid-time-cost" + + // Argon2RemoteErrorInvalidThreads means that an invalid number of threads was supplied. + Argon2RemoteErrorInvalidThreads Argon2RemoteErrorType = "invalid-threads" + + // Argon2RemoteErrorConsumedProcess means that this process has already performed one + // execution of the KDF, and a new process should replace it. + Argon2RemoteErrorConsumedProcess Argon2RemoteErrorType = "consumed-process" + + // Argon2RemoteErrorProcessNotConfigured means that nothing has called SetIsArgon2RemoteProcess. + Argon2RemoteErrorProcessNotConfigured Argon2RemoteErrorType = "process-not-configured" + + // Argon2RemoteErrorUnexpected means that an unexpected error occurred. + Argon2RemoteErrorUnexpected Argon2RemoteErrorType = "unexpected-error" + + // Argon2RemoteErrorUnexpectedInput means that there was an error with + // the supplied error not covered by one of the more specific error types.. + Argon2RemoteErrorUnexpectedInput Argon2RemoteErrorType = "unexpected-input" +) + +// Argon2RemoteOutput is the response to a request for an argon2 +// operation in a remote process. +type Argon2RemoteOutput struct { + Command Argon2RemoteCommand `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 Argon2RemoteErrorType `json:"error-type,omitempty"` // The error type, if an error occurred + ErrorString string `json:"error-string,omitempty"` // The error string, if an error occurred +} + +// Argon2RemoteError is returned from the [Argon2] implentation created be +// [NewRemoteArgon2KDF] when the received response indicates that an error +// ocurred. +type Argon2RemoteError struct { + ErrorType Argon2RemoteErrorType + ErrorString string +} + +func (e *Argon2RemoteError) 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() +} + +func (o *Argon2RemoteOutput) Err() error { + if o.ErrorType == "" { + return nil + } + return &Argon2RemoteError{ + ErrorType: o.ErrorType, + ErrorString: o.ErrorString, + } +} + +const ( + notArgon2RemoteProcess uint32 = 0 + readyArgon2RemoteProcess uint32 = 1 + expiredArgon2RemoteProcess uint32 = 2 +) + +var ( + argon2RemoteProcessStatus uint32 = notArgon2RemoteProcess +) + +// SetIsArgon2RemoteProcess marks this process as being a remote processs capable of running +// Argon2 in process. After calling this, [InProcessArgon2KDF] will return a real implementation +// that runs in process. +func SetIsArgon2RemoteProcess() { + if !atomic.CompareAndSwapUint32(&argon2RemoteProcessStatus, notArgon2RemoteProcess, readyArgon2RemoteProcess) { + panic("cannot call SetIsArgon2RemoteProcess more than once") + } +} + +// RunArgon2RequestInRemoteProcess 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. +func RunArgon2RequestInRemoteProcess(input *Argon2RemoteInput) *Argon2RemoteOutput { + if !atomic.CompareAndSwapUint32(&argon2RemoteProcessStatus, readyArgon2RemoteProcess, expiredArgon2RemoteProcess) { + switch atomic.LoadUint32(&argon2RemoteProcessStatus) { + case expiredArgon2RemoteProcess: + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorConsumedProcess, + ErrorString: "cannot run more than once in the same process", + } + default: + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorProcessNotConfigured, + ErrorString: "cannot run in a process that isn't configured as an Argon2 remote process", + } + } + } + + switch input.Mode { + case Argon2id, Argon2i: + // ok + default: + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorInvalidMode, + ErrorString: fmt.Sprintf("invalid mode: %q", string(input.Mode)), + } + } + + costParams := &Argon2CostParams{ + Time: input.Time, + MemoryKiB: input.MemoryKiB, + Threads: input.Threads, + } + if costParams.Time == 0 { + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorInvalidTimeCost, + ErrorString: "invalid time cost: cannot be zero", + } + } + if costParams.Threads == 0 { + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorInvalidThreads, + ErrorString: "invalid threads: cannot be zero", + } + } + + switch input.Command { + case Argon2RemoteCommandDerive: + key, err := InProcessArgon2KDF().Derive(input.Passphrase, input.Salt, input.Mode, costParams, input.Keylen) + if err != nil { + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorUnexpected, + ErrorString: fmt.Sprintf("cannot run derive command: %v", err), + } + + } + return &Argon2RemoteOutput{ + Command: input.Command, + Key: key, + } + case Argon2RemoteCommandTime: + if len(input.Passphrase) > 0 { + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorUnexpectedInput, + ErrorString: "cannot supply passphrase for \"time\" command", + } + } + if len(input.Salt) > 0 { + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorUnexpectedInput, + ErrorString: "cannot supply salt for \"time\" command", + } + } + if input.Keylen > 0 { + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorUnexpectedInput, + ErrorString: "cannot supply keylen for \"time\" command", + } + } + + duration, err := InProcessArgon2KDF().Time(input.Mode, costParams) + if err != nil { + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorUnexpected, + ErrorString: fmt.Sprintf("cannot run time command: %v", err), + } + } + return &Argon2RemoteOutput{ + Command: input.Command, + Duration: duration, + } + default: + return &Argon2RemoteOutput{ + Command: input.Command, + ErrorType: Argon2RemoteErrorInvalidCommand, + ErrorString: fmt.Sprintf("invalid command: %q", string(input.Command)), + } + } +} + +// WaitAndRunArgon2RequestInRemoteProcess waits for a [Argon2RemoteInput] request on the +// supplied io.Reader before running it and sending a [Argon2RemoteOutput] response back via +// the supplied io.Writer. These will generally be connected to the process's os.Stdin and +// os.Stdout when using - certainly when using [NewRemoteArgon2KDF] 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. +func WaitAndRunArgon2RequestInRemoteProcess(in io.Reader, out io.Writer) error { + var input *Argon2RemoteInput + dec := json.NewDecoder(in) + dec.DisallowUnknownFields() + if err := dec.Decode(&input); err != nil { + return fmt.Errorf("cannot decode input: %w", err) + } + + output := RunArgon2RequestInRemoteProcess(input) + + enc := json.NewEncoder(out) + if err := enc.Encode(output); err != nil { + return fmt.Errorf("cannot encode output: %w", err) + } + + return nil +} + +// remoteArgon2KDFImpl is an Argon2KDFImpl that runs the KDF in a remote process, +// using the remote JSON protocol defined in this package. +type remoteArgon2KDFImpl struct { + newRemoteCommand func() (*exec.Cmd, error) +} + +func (k *remoteArgon2KDFImpl) runInRemoteProcess(params *Argon2RemoteInput) (res *Argon2RemoteOutput, err error) { + cmd, err := k.newRemoteCommand() + if err != nil { + return nil, fmt.Errorf("cannot create remote process: %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 remote 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 { + res = 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(params); err != nil { + return nil, fmt.Errorf("cannot encode parameters: %w", err) + } + + // Wait for thre result from the remote process. + dec := json.NewDecoder(stdoutPipe) + if err := dec.Decode(&res); err != nil { + return nil, fmt.Errorf("cannot decode result: %w", err) + } + + return res, nil +} + +func (k *remoteArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) (key []byte, err error) { + remoteParams := &Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: passphrase, + Salt: salt, + Keylen: keyLen, + Mode: mode, + Time: params.Time, + MemoryKiB: params.MemoryKiB, + Threads: params.Threads, + } + res, err := k.runInRemoteProcess(remoteParams) + if err != nil { + return nil, err + } + if res.Err() != nil { + return nil, res.Err() + } + return res.Key, nil +} + +func (k *remoteArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (duration time.Duration, err error) { + remoteParams := &Argon2RemoteInput{ + Command: Argon2RemoteCommandTime, + Mode: mode, + Time: params.Time, + MemoryKiB: params.MemoryKiB, + Threads: params.Threads, + } + res, err := k.runInRemoteProcess(remoteParams) + if err != nil { + return 0, err + } + if res.Err() != nil { + return 0, res.Err() + } + return res.Duration, nil +} + +// NewRemoteArgon2KDF returns a new Argon2KDF that runs each KDF invocation in a +// short-lived remote process, using a *[exec.Cmd] created by the supplied function, +// and using a protocol compatibile with [WaitAndRunArgon2RequestInRemoteProcess] +// in the remote 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 +// and receiving, and these will be connected to stdin and stdout of the remote process. +func NewRemoteArgon2KDF(newRemoteCommand func() (*exec.Cmd, error)) Argon2KDF { + return &remoteArgon2KDFImpl{ + newRemoteCommand: newRemoteCommand, + } +} diff --git a/argon2_remote_support_test.go b/argon2_remote_support_test.go new file mode 100644 index 00000000..c17a6b89 --- /dev/null +++ b/argon2_remote_support_test.go @@ -0,0 +1,643 @@ +// -*- 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 ( + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + + . "github.com/snapcore/secboot" + "github.com/snapcore/secboot/internal/argon2" + "github.com/snapcore/secboot/internal/testutil" + . "gopkg.in/check.v1" +) + +type argon2RemoteSupportSuite struct{} + +func (s *argon2RemoteSupportSuite) TearDownTest(c *C) { + ClearIsArgon2RemoteProcess() +} + +var _ = Suite(&argon2RemoteSupportSuite{}) + +func (s *argon2RemoteSupportSuite) TestInProcessKDFDeriveNotSupported(c *C) { + _, err := InProcessArgon2KDF().Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}, 32) + c.Check(err, ErrorMatches, `no argon2 KDF: please call secboot.SetIsArgon2RemoteProcess if the intention is to run Argon2 directly in this process`) +} + +func (s *argon2RemoteSupportSuite) TestInProcessKDFDeriveNoParams(c *C) { + SetIsArgon2RemoteProcess() + _, err := InProcessArgon2KDF().Derive("foo", nil, Argon2id, nil, 32) + c.Check(err, ErrorMatches, `nil params`) +} + +func (s *argon2RemoteSupportSuite) TestInProcessKDFDeriveInvalidMode(c *C) { + SetIsArgon2RemoteProcess() + _, err := InProcessArgon2KDF().Derive("foo", nil, Argon2Default, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}, 32) + c.Check(err, ErrorMatches, `invalid mode`) +} + +func (s *argon2RemoteSupportSuite) TestInProcessKDFDeriveInvalidTime(c *C) { + SetIsArgon2RemoteProcess() + _, err := InProcessArgon2KDF().Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1}, 32) + c.Check(err, ErrorMatches, `invalid time cost`) +} + +func (s *argon2RemoteSupportSuite) TestInProcessKDFDeriveInvalidThreads(c *C) { + SetIsArgon2RemoteProcess() + _, err := InProcessArgon2KDF().Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 0}, 32) + c.Check(err, ErrorMatches, `invalid number of threads`) +} + +func (s *argon2RemoteSupportSuite) TestInProcessKDFTimeNotSupported(c *C) { + _, err := InProcessArgon2KDF().Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}) + c.Check(err, ErrorMatches, `no argon2 KDF: please call secboot.SetIsArgon2RemoteProcess if the intention is to run Argon2 directly in this process`) +} + +func (s *argon2RemoteSupportSuite) TestInProcessKDFTimeNoParams(c *C) { + SetIsArgon2RemoteProcess() + _, err := InProcessArgon2KDF().Time(Argon2id, nil) + c.Check(err, ErrorMatches, `nil params`) +} + +func (s *argon2RemoteSupportSuite) TestInProcessKDFTimeInvalidMode(c *C) { + SetIsArgon2RemoteProcess() + _, err := InProcessArgon2KDF().Time(Argon2Default, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}) + c.Check(err, ErrorMatches, `invalid mode`) +} + +func (s *argon2RemoteSupportSuite) TestInProcessKDFTimeInvalidTime(c *C) { + SetIsArgon2RemoteProcess() + _, err := InProcessArgon2KDF().Time(Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1}) + c.Check(err, ErrorMatches, `invalid time cost`) +} + +func (s *argon2RemoteSupportSuite) TestInProcessKDFTimeInvalidThreads(c *C) { + SetIsArgon2RemoteProcess() + _, err := InProcessArgon2KDF().Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 0}) + c.Check(err, ErrorMatches, `invalid number of threads`) +} + +func (s *argon2RemoteSupportSuite) TestRunArgon2RequestInRemoteProcessInvalidProcess(c *C) { + out := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: nil, + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2RemoteOutput{ + Command: Argon2RemoteCommandDerive, + ErrorType: Argon2RemoteErrorProcessNotConfigured, + ErrorString: "cannot run in a process that isn't configured as an Argon2 remote process", + }) +} + +func (s *argon2RemoteSupportSuite) TestRunArgon2RequestInRemoteProcessInvalidMode(c *C) { + SetIsArgon2RemoteProcess() + out := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: nil, + Keylen: 32, + Mode: Argon2Mode("foo"), + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2RemoteOutput{ + Command: Argon2RemoteCommandDerive, + ErrorType: Argon2RemoteErrorInvalidMode, + ErrorString: "invalid mode: \"foo\"", + }) +} + +func (s *argon2RemoteSupportSuite) TestRunArgon2RequestInRemoteProcessInvalidTime(c *C) { + SetIsArgon2RemoteProcess() + out := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: nil, + Keylen: 32, + Mode: Argon2id, + Time: 0, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2RemoteOutput{ + Command: Argon2RemoteCommandDerive, + ErrorType: Argon2RemoteErrorInvalidTimeCost, + ErrorString: "invalid time cost: cannot be zero", + }) +} + +func (s *argon2RemoteSupportSuite) TestRunArgon2RequestInRemoteProcessInvalidThreads(c *C) { + SetIsArgon2RemoteProcess() + out := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: nil, + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 0, + }) + c.Check(out, DeepEquals, &Argon2RemoteOutput{ + Command: Argon2RemoteCommandDerive, + ErrorType: Argon2RemoteErrorInvalidThreads, + ErrorString: "invalid threads: cannot be zero", + }) +} + +func (s *argon2RemoteSupportSuite) TestArgon2RemoteOutputErr(c *C) { + out := &Argon2RemoteOutput{ + Command: Argon2RemoteCommandDerive, + ErrorType: Argon2RemoteErrorProcessNotConfigured, + 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 *Argon2RemoteError + c.Check(errors.As(err, &e), testutil.IsTrue) +} + +type argon2RemoteSupportSuiteExpensive struct { + runArgon2RemoteDir string +} + +func (s *argon2RemoteSupportSuiteExpensive) runArgon2RemotePath() string { + return filepath.Join(s.runArgon2RemoteDir, "run_argon2") +} + +func (s *argon2RemoteSupportSuiteExpensive) SetUpSuite(c *C) { + if _, exists := os.LookupEnv("NO_ARGON2_TESTS"); exists { + c.Skip("skipping expensive argon2 tests") + } + s.runArgon2RemoteDir = c.MkDir() + cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), "build", "-o", s.runArgon2RemoteDir, "./cmd/run_argon2") + c.Assert(cmd.Run(), IsNil) +} + +func (s *argon2RemoteSupportSuiteExpensive) SetUpTest(c *C) { + SetIsArgon2RemoteProcess() +} + +func (s *argon2RemoteSupportSuiteExpensive) TearDownTest(c *C) { + ClearIsArgon2RemoteProcess() +} + +var _ = Suite(&argon2RemoteSupportSuiteExpensive{}) + +type testInProcessArgon2KDFDeriveData struct { + passphrase string + salt []byte + mode Argon2Mode + params *Argon2CostParams + keyLen uint32 + + expectedKey []byte +} + +func (s *argon2RemoteSupportSuiteExpensive) testInProcessKDFDerive(c *C, data *testInProcessArgon2KDFDeriveData) { + key, err := InProcessArgon2KDF().Derive(data.passphrase, data.salt, data.mode, data.params, data.keyLen) + c.Check(err, IsNil) + c.Check(key, DeepEquals, data.expectedKey) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFDerive(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"), + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "bar", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"), + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFiDeriveDifferentSalt(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "foo", + salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"), + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFDeriveDifferentMode(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2i, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"), + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFDeriveDifferentParams(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + 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 *argon2RemoteSupportSuiteExpensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 64, + expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"), + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFTime(c *C) { + time1, err := InProcessArgon2KDF().Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4}) + c.Check(err, IsNil) + + 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) + + 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) + + 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 + c.Check(time2 > time1, testutil.IsTrue) +} + +func (s *argon2RemoteSupportSuiteExpensive) testRunArgon2RequestInRemoteProcessDerive(c *C, input *Argon2RemoteInput) { + res := RunArgon2RequestInRemoteProcess(input) + c.Check(res.Command, Equals, Argon2RemoteCommandDerive) + c.Check(res.Err(), IsNil) + + runtime.GC() + + expected := argon2.Key(input.Passphrase, input.Salt, argon2.Mode(input.Mode), &argon2.CostParams{ + Time: input.Time, + MemoryKiB: input.MemoryKiB, + Threads: input.Threads}, input.Keylen) + c.Check(expected, DeepEquals, res.Key) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDerive(c *C) { + s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: []byte("0123456789abcdefghijklmnopqrstuv"), + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + Keylen: 32, + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDeriveDifferentPassphrase(c *C) { + s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "bar", + Salt: []byte("0123456789abcdefghijklmnopqrstuv"), + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + Keylen: 32, + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDeriveDifferentSalt(c *C) { + s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + Keylen: 32, + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDeriveDifferentMode(c *C) { + s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: []byte("0123456789abcdefghijklmnopqrstuv"), + Mode: Argon2i, + Time: 4, + MemoryKiB: 32, + Threads: 4, + Keylen: 32, + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDeriveDifferentParams(c *C) { + s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: []byte("0123456789abcdefghijklmnopqrstuv"), + Mode: Argon2id, + Time: 48, + MemoryKiB: 32 * 1024, + Threads: 4, + Keylen: 32, + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDeriveDifferentKeylen(c *C) { + s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: []byte("0123456789abcdefghijklmnopqrstuv"), + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + Keylen: 64, + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessTime(c *C) { + res := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ + Command: Argon2RemoteCommandTime, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32 * 1024, + Threads: 4, + }) + c.Check(res.Err(), IsNil) + + ClearIsArgon2RemoteProcess() + SetIsArgon2RemoteProcess() + res2 := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ + Command: Argon2RemoteCommandTime, + 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) + + ClearIsArgon2RemoteProcess() + SetIsArgon2RemoteProcess() + res2 = RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ + Command: Argon2RemoteCommandTime, + 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) + + ClearIsArgon2RemoteProcess() + SetIsArgon2RemoteProcess() + res2 = RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ + Command: Argon2RemoteCommandTime, + 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 *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessConsumedProcess(c *C) { + out := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: nil, + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, NotNil) + + out = RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ + Command: Argon2RemoteCommandDerive, + Passphrase: "foo", + Salt: nil, + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2RemoteOutput{ + Command: Argon2RemoteCommandDerive, + ErrorType: Argon2RemoteErrorConsumedProcess, + ErrorString: "cannot run more than once in the same process", + }) +} + +type testRemoteArgon2DeriveParams struct { + passphrase string + salt []byte + mode Argon2Mode + params *Argon2CostParams + keyLen uint32 + expectedKey []byte +} + +func (s *argon2RemoteSupportSuiteExpensive) testRemoteArgon2Derive(c *C, params *testRemoteArgon2DeriveParams) { + kdf := NewRemoteArgon2KDF(func() (*exec.Cmd, error) { + return exec.Command(s.runArgon2RemotePath()), 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 *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2Derive(c *C) { + s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"), + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2DeriveDifferentPassphrase(c *C) { + s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ + passphrase: "bar", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"), + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2DeriveDifferentSalt(c *C) { + s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ + passphrase: "foo", + salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"), + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2DeriveDifferentMode(c *C) { + s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2i, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"), + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2DeriveDifferentParams(c *C) { + s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ + 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 *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2DeriveDifferentKeyLen(c *C) { + s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 64, + expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"), + }) +} + +func (s *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2Time(c *C) { + kdf := NewRemoteArgon2KDF(func() (*exec.Cmd, error) { + return exec.Command(s.runArgon2RemotePath()), 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..ef9c5280 100644 --- a/argon2_test.go +++ b/argon2_test.go @@ -21,7 +21,6 @@ package secboot_test import ( "math" - "os" "runtime" "time" @@ -32,7 +31,6 @@ import ( . "gopkg.in/check.v1" . "github.com/snapcore/secboot" - "github.com/snapcore/secboot/internal/argon2" "github.com/snapcore/secboot/internal/testutil" ) @@ -248,174 +246,3 @@ func (s *argon2Suite) TestKDFParamsInvalidMemoryKiB(c *C) { _, err := opts.KdfParams(0) c.Check(err, ErrorMatches, `invalid memory cost 4294967295KiB`) } - -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`) -} - -func (s *argon2Suite) TestInProcessKDFDeriveInvalidThreads(c *C) { - _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 0}, 32) - c.Check(err, ErrorMatches, `invalid number of threads`) -} - -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`) -} - -func (s *argon2Suite) TestInProcessKDFTimeInvalidThreads(c *C) { - _, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 0}) - c.Check(err, ErrorMatches, `invalid number of threads`) -} - -type argon2SuiteExpensive struct{} - -func (s *argon2SuiteExpensive) 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 -} - -func (s *argon2SuiteExpensive) 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) -} - -func (s *argon2SuiteExpensive) TestInProcessKDFDerive(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32}) -} - -func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "bar", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32}) -} - -func (s *argon2SuiteExpensive) TestInProcessKDFiDeriveDifferentSalt(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "foo", - salt: []byte("zyxwvutsrqponmlkjihgfedcba987654"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32}) -} - -func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentMode(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2i, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32}) -} - -func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentParams(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 48, - MemoryKiB: 32 * 1024, - Threads: 4}, - keyLen: 32}) -} - -func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 64}) -} - -func (s *argon2SuiteExpensive) 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() - 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() - 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() - 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/cmd/run_argon2/main.go b/cmd/run_argon2/main.go new file mode 100644 index 00000000..c23334a4 --- /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.SetIsArgon2RemoteProcess() + + err := secboot.WaitAndRunArgon2RequestInRemoteProcess(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..635c8c77 100644 --- a/export_test.go +++ b/export_test.go @@ -21,6 +21,8 @@ package secboot import ( "io" + "runtime" + "sync/atomic" "github.com/snapcore/secboot/internal/luks2" "github.com/snapcore/secboot/internal/luksview" @@ -148,6 +150,11 @@ func MockHashAlgAvailable() (restore func()) { } } +func ClearIsArgon2RemoteProcess() { + atomic.StoreUint32(&argon2RemoteProcessStatus, notArgon2RemoteProcess) + runtime.GC() +} + func (d *KeyData) DerivePassphraseKeys(passphrase string) (key, iv, auth []byte, err error) { return d.derivePassphraseKeys(passphrase) } From bb46af889148437173e082d55176e1e0bd73c37d Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Sep 2024 13:19:28 +0100 Subject: [PATCH 02/40] argon2: address some review comments This includes some other small refactorigns, eg, the handler process makes use of the global Argon2KDF implementation now in the same way that the parent process does. --- .gitignore | 2 +- argon2.go | 72 ++- argon2_out_of_process_support.go | 471 +++++++++++++++++++ argon2_out_of_process_support_test.go | 495 ++++++++++++++++++++ argon2_remote_support.go | 460 ------------------ argon2_remote_support_test.go | 643 -------------------------- argon2_test.go | 191 ++++++++ cmd/run_argon2/main.go | 4 +- export_test.go | 18 +- 9 files changed, 1242 insertions(+), 1114 deletions(-) create mode 100644 argon2_out_of_process_support.go create mode 100644 argon2_out_of_process_support_test.go delete mode 100644 argon2_remote_support.go delete mode 100644 argon2_remote_support_test.go diff --git a/.gitignore b/.gitignore index eb54f6c3..75bf717e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -run_argon2 +./run_argon2 vendor/*/ diff --git a/argon2.go b/argon2.go index 6d5aa507..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. @@ -225,6 +236,59 @@ type Argon2KDF interface { Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) } +type inProcessArgon2KDFImpl struct{} + +func (_ inProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) { + switch { + case mode != Argon2i && mode != Argon2id: + return nil, errors.New("invalid mode") + case params == nil: + return nil, errors.New("nil params") + case params.Time == 0: + return nil, errors.New("invalid time cost") + case params.Threads == 0: + return nil, errors.New("invalid number of threads") + } + + return argon2.Key(passphrase, salt, argon2.Mode(mode), params.internalParams(), keyLen), nil +} + +func (_ inProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) { + switch { + case mode != Argon2i && mode != Argon2id: + return 0, errors.New("invalid mode") + case params == nil: + return 0, errors.New("nil params") + case params.Time == 0: + return 0, errors.New("invalid time cost") + case params.Threads == 0: + return 0, errors.New("invalid number of threads") + } + + 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. 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{} func (_ nullArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) { 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..68187ba5 --- /dev/null +++ b/argon2_out_of_process_support_test.go @@ -0,0 +1,495 @@ +// -*- 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 ( + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + + . "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) TestRunArgon2OutOfProcessRequestInvalidProcess(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) TestRunArgon2OutOfProcessRequestInvalidMode(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) TestRunArgon2OutOfProcessRequestInvalidTime(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) TestRunArgon2OutOfProcessRequestInvalidThreads(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) 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) TestRunArgon2OutOfProcessRequestConsumedProcess(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", + }) +} + +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_remote_support.go b/argon2_remote_support.go deleted file mode 100644 index d75cadbe..00000000 --- a/argon2_remote_support.go +++ /dev/null @@ -1,460 +0,0 @@ -// -*- 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" - - "github.com/snapcore/secboot/internal/argon2" -) - -type nullInProcessArgon2KDFImpl struct{} - -func (_ nullInProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) { - return nil, errors.New("no argon2 KDF: please call secboot.SetIsArgon2RemoteProcess if the intention is to run Argon2 directly in this process") -} - -func (_ nullInProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) { - return 0, errors.New("no argon2 KDF: please call secboot.SetIsArgon2RemoteProcess if the intention is to run Argon2 directly in this process") -} - -type inProcessArgon2KDFImpl struct{} - -func (_ inProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) { - switch { - case mode != Argon2i && mode != Argon2id: - return nil, errors.New("invalid mode") - case params == nil: - return nil, errors.New("nil params") - case params.Time == 0: - return nil, errors.New("invalid time cost") - case params.Threads == 0: - return nil, errors.New("invalid number of threads") - } - - return argon2.Key(passphrase, salt, argon2.Mode(mode), params.internalParams(), keyLen), nil -} - -func (_ inProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) { - switch { - case mode != Argon2i && mode != Argon2id: - return 0, errors.New("invalid mode") - case params == nil: - return 0, errors.New("nil params") - case params.Time == 0: - return 0, errors.New("invalid time cost") - case params.Threads == 0: - return 0, errors.New("invalid number of threads") - } - - return argon2.KeyDuration(argon2.Mode(mode), params.internalParams()), nil -} - -// InProcessArgon2KDF returns 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 -// [Argon2KDF] implementation which proxies requests to a short-lived remote process which will -// use this in-process implementation once and then exit. This approach avoids memory exhaustion -// and the need to run a full GC mark and sweep ([runtime.GC]) between invocations, which has -// a significant time penalty. Argon2 isn't really compativle with garbage collected runtimes. -// -// There are plenty of helpers in this package to facilitate this, such as JSON serializable -// types ([Argon2RemoteInput] and [Argon2RemoteOutput]) and a function ([RunArgon2KDFInRemoteProcess]) -// that can process these types to run the KDF in-process using a request from a parent process. -// -// There are higher-level helpers too, such as an [Argon2KDF] implementation that can be created -// in the parent process and which creates new remote processes to send each command to (see -// [NewRemoteArgon2KDF]). The remote process is then able to process an incoming request by passing -// [os.Stdin] and [os.Stdout] directly to [WaitAndRunArgon2RequestInRemoteProcess]. -// -// It is indended that the remote process is a special mode of argv[0] (ie, snapd or snap-bootstrap) -// in order to avoid the bloat of adding additional go binaries. -// -// A process must call [SetIsArgon2RemoteProcess] before this returns anything other than a null -// implementation of the KDF. -// -// Note that whilst [WaitAndRunArgon2RequestInRemoteProcess] and [RunArgon2KDFInRemoteProcess] -// contain protections that only allow a single invocation of this KDF in the lifetime of a process, -// direct access via this API doesn't provde the same protections. -func InProcessArgon2KDF() Argon2KDF { - if atomic.LoadUint32(&argon2RemoteProcessStatus) > notArgon2RemoteProcess { - return inProcessArgon2KDFImpl{} - } - return nullInProcessArgon2KDFImpl{} -} - -// Argon2RemoteCommand represents the command to run. -type Argon2RemoteCommand string - -const ( - // Argon2RemoteCommandDerive requests to derive a key from a passphrase - Argon2RemoteCommandDerive Argon2RemoteCommand = "derive" - - // Argon2RemoteCommandTime requests the duration that the KDF took to - // execute. This excludes things like process startup. - Argon2RemoteCommandTime Argon2RemoteCommand = "time" -) - -// Argon2RemoteInput is an input request for an argon2 operation in -// a remote process. -type Argon2RemoteInput struct { - Command Argon2RemoteCommand `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 -} - -// Argon2RemoteErrorType describes the type of error produced by [RunArgon2RequestInRemoteProcess]. -type Argon2RemoteErrorType string - -const ( - // Argon2RemoteErrorInvalidCommand means that an invalid command was supplied. - Argon2RemoteErrorInvalidCommand Argon2RemoteErrorType = "invalid-command" - - // Argon2RemoteErrorInvalidMode means that an invalid mode was supplied. - Argon2RemoteErrorInvalidMode Argon2RemoteErrorType = "invalid-mode" - - // Argon2RemoteErrorInvalidTimeCost means that an invalid time cost was supplied. - Argon2RemoteErrorInvalidTimeCost Argon2RemoteErrorType = "invalid-time-cost" - - // Argon2RemoteErrorInvalidThreads means that an invalid number of threads was supplied. - Argon2RemoteErrorInvalidThreads Argon2RemoteErrorType = "invalid-threads" - - // Argon2RemoteErrorConsumedProcess means that this process has already performed one - // execution of the KDF, and a new process should replace it. - Argon2RemoteErrorConsumedProcess Argon2RemoteErrorType = "consumed-process" - - // Argon2RemoteErrorProcessNotConfigured means that nothing has called SetIsArgon2RemoteProcess. - Argon2RemoteErrorProcessNotConfigured Argon2RemoteErrorType = "process-not-configured" - - // Argon2RemoteErrorUnexpected means that an unexpected error occurred. - Argon2RemoteErrorUnexpected Argon2RemoteErrorType = "unexpected-error" - - // Argon2RemoteErrorUnexpectedInput means that there was an error with - // the supplied error not covered by one of the more specific error types.. - Argon2RemoteErrorUnexpectedInput Argon2RemoteErrorType = "unexpected-input" -) - -// Argon2RemoteOutput is the response to a request for an argon2 -// operation in a remote process. -type Argon2RemoteOutput struct { - Command Argon2RemoteCommand `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 Argon2RemoteErrorType `json:"error-type,omitempty"` // The error type, if an error occurred - ErrorString string `json:"error-string,omitempty"` // The error string, if an error occurred -} - -// Argon2RemoteError is returned from the [Argon2] implentation created be -// [NewRemoteArgon2KDF] when the received response indicates that an error -// ocurred. -type Argon2RemoteError struct { - ErrorType Argon2RemoteErrorType - ErrorString string -} - -func (e *Argon2RemoteError) 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() -} - -func (o *Argon2RemoteOutput) Err() error { - if o.ErrorType == "" { - return nil - } - return &Argon2RemoteError{ - ErrorType: o.ErrorType, - ErrorString: o.ErrorString, - } -} - -const ( - notArgon2RemoteProcess uint32 = 0 - readyArgon2RemoteProcess uint32 = 1 - expiredArgon2RemoteProcess uint32 = 2 -) - -var ( - argon2RemoteProcessStatus uint32 = notArgon2RemoteProcess -) - -// SetIsArgon2RemoteProcess marks this process as being a remote processs capable of running -// Argon2 in process. After calling this, [InProcessArgon2KDF] will return a real implementation -// that runs in process. -func SetIsArgon2RemoteProcess() { - if !atomic.CompareAndSwapUint32(&argon2RemoteProcessStatus, notArgon2RemoteProcess, readyArgon2RemoteProcess) { - panic("cannot call SetIsArgon2RemoteProcess more than once") - } -} - -// RunArgon2RequestInRemoteProcess 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. -func RunArgon2RequestInRemoteProcess(input *Argon2RemoteInput) *Argon2RemoteOutput { - if !atomic.CompareAndSwapUint32(&argon2RemoteProcessStatus, readyArgon2RemoteProcess, expiredArgon2RemoteProcess) { - switch atomic.LoadUint32(&argon2RemoteProcessStatus) { - case expiredArgon2RemoteProcess: - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorConsumedProcess, - ErrorString: "cannot run more than once in the same process", - } - default: - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorProcessNotConfigured, - ErrorString: "cannot run in a process that isn't configured as an Argon2 remote process", - } - } - } - - switch input.Mode { - case Argon2id, Argon2i: - // ok - default: - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorInvalidMode, - ErrorString: fmt.Sprintf("invalid mode: %q", string(input.Mode)), - } - } - - costParams := &Argon2CostParams{ - Time: input.Time, - MemoryKiB: input.MemoryKiB, - Threads: input.Threads, - } - if costParams.Time == 0 { - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorInvalidTimeCost, - ErrorString: "invalid time cost: cannot be zero", - } - } - if costParams.Threads == 0 { - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorInvalidThreads, - ErrorString: "invalid threads: cannot be zero", - } - } - - switch input.Command { - case Argon2RemoteCommandDerive: - key, err := InProcessArgon2KDF().Derive(input.Passphrase, input.Salt, input.Mode, costParams, input.Keylen) - if err != nil { - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorUnexpected, - ErrorString: fmt.Sprintf("cannot run derive command: %v", err), - } - - } - return &Argon2RemoteOutput{ - Command: input.Command, - Key: key, - } - case Argon2RemoteCommandTime: - if len(input.Passphrase) > 0 { - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorUnexpectedInput, - ErrorString: "cannot supply passphrase for \"time\" command", - } - } - if len(input.Salt) > 0 { - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorUnexpectedInput, - ErrorString: "cannot supply salt for \"time\" command", - } - } - if input.Keylen > 0 { - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorUnexpectedInput, - ErrorString: "cannot supply keylen for \"time\" command", - } - } - - duration, err := InProcessArgon2KDF().Time(input.Mode, costParams) - if err != nil { - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorUnexpected, - ErrorString: fmt.Sprintf("cannot run time command: %v", err), - } - } - return &Argon2RemoteOutput{ - Command: input.Command, - Duration: duration, - } - default: - return &Argon2RemoteOutput{ - Command: input.Command, - ErrorType: Argon2RemoteErrorInvalidCommand, - ErrorString: fmt.Sprintf("invalid command: %q", string(input.Command)), - } - } -} - -// WaitAndRunArgon2RequestInRemoteProcess waits for a [Argon2RemoteInput] request on the -// supplied io.Reader before running it and sending a [Argon2RemoteOutput] response back via -// the supplied io.Writer. These will generally be connected to the process's os.Stdin and -// os.Stdout when using - certainly when using [NewRemoteArgon2KDF] 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. -func WaitAndRunArgon2RequestInRemoteProcess(in io.Reader, out io.Writer) error { - var input *Argon2RemoteInput - dec := json.NewDecoder(in) - dec.DisallowUnknownFields() - if err := dec.Decode(&input); err != nil { - return fmt.Errorf("cannot decode input: %w", err) - } - - output := RunArgon2RequestInRemoteProcess(input) - - enc := json.NewEncoder(out) - if err := enc.Encode(output); err != nil { - return fmt.Errorf("cannot encode output: %w", err) - } - - return nil -} - -// remoteArgon2KDFImpl is an Argon2KDFImpl that runs the KDF in a remote process, -// using the remote JSON protocol defined in this package. -type remoteArgon2KDFImpl struct { - newRemoteCommand func() (*exec.Cmd, error) -} - -func (k *remoteArgon2KDFImpl) runInRemoteProcess(params *Argon2RemoteInput) (res *Argon2RemoteOutput, err error) { - cmd, err := k.newRemoteCommand() - if err != nil { - return nil, fmt.Errorf("cannot create remote process: %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 remote 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 { - res = 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(params); err != nil { - return nil, fmt.Errorf("cannot encode parameters: %w", err) - } - - // Wait for thre result from the remote process. - dec := json.NewDecoder(stdoutPipe) - if err := dec.Decode(&res); err != nil { - return nil, fmt.Errorf("cannot decode result: %w", err) - } - - return res, nil -} - -func (k *remoteArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) (key []byte, err error) { - remoteParams := &Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: passphrase, - Salt: salt, - Keylen: keyLen, - Mode: mode, - Time: params.Time, - MemoryKiB: params.MemoryKiB, - Threads: params.Threads, - } - res, err := k.runInRemoteProcess(remoteParams) - if err != nil { - return nil, err - } - if res.Err() != nil { - return nil, res.Err() - } - return res.Key, nil -} - -func (k *remoteArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (duration time.Duration, err error) { - remoteParams := &Argon2RemoteInput{ - Command: Argon2RemoteCommandTime, - Mode: mode, - Time: params.Time, - MemoryKiB: params.MemoryKiB, - Threads: params.Threads, - } - res, err := k.runInRemoteProcess(remoteParams) - if err != nil { - return 0, err - } - if res.Err() != nil { - return 0, res.Err() - } - return res.Duration, nil -} - -// NewRemoteArgon2KDF returns a new Argon2KDF that runs each KDF invocation in a -// short-lived remote process, using a *[exec.Cmd] created by the supplied function, -// and using a protocol compatibile with [WaitAndRunArgon2RequestInRemoteProcess] -// in the remote 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 -// and receiving, and these will be connected to stdin and stdout of the remote process. -func NewRemoteArgon2KDF(newRemoteCommand func() (*exec.Cmd, error)) Argon2KDF { - return &remoteArgon2KDFImpl{ - newRemoteCommand: newRemoteCommand, - } -} diff --git a/argon2_remote_support_test.go b/argon2_remote_support_test.go deleted file mode 100644 index c17a6b89..00000000 --- a/argon2_remote_support_test.go +++ /dev/null @@ -1,643 +0,0 @@ -// -*- 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 ( - "errors" - "os" - "os/exec" - "path/filepath" - "runtime" - - . "github.com/snapcore/secboot" - "github.com/snapcore/secboot/internal/argon2" - "github.com/snapcore/secboot/internal/testutil" - . "gopkg.in/check.v1" -) - -type argon2RemoteSupportSuite struct{} - -func (s *argon2RemoteSupportSuite) TearDownTest(c *C) { - ClearIsArgon2RemoteProcess() -} - -var _ = Suite(&argon2RemoteSupportSuite{}) - -func (s *argon2RemoteSupportSuite) TestInProcessKDFDeriveNotSupported(c *C) { - _, err := InProcessArgon2KDF().Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}, 32) - c.Check(err, ErrorMatches, `no argon2 KDF: please call secboot.SetIsArgon2RemoteProcess if the intention is to run Argon2 directly in this process`) -} - -func (s *argon2RemoteSupportSuite) TestInProcessKDFDeriveNoParams(c *C) { - SetIsArgon2RemoteProcess() - _, err := InProcessArgon2KDF().Derive("foo", nil, Argon2id, nil, 32) - c.Check(err, ErrorMatches, `nil params`) -} - -func (s *argon2RemoteSupportSuite) TestInProcessKDFDeriveInvalidMode(c *C) { - SetIsArgon2RemoteProcess() - _, err := InProcessArgon2KDF().Derive("foo", nil, Argon2Default, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}, 32) - c.Check(err, ErrorMatches, `invalid mode`) -} - -func (s *argon2RemoteSupportSuite) TestInProcessKDFDeriveInvalidTime(c *C) { - SetIsArgon2RemoteProcess() - _, err := InProcessArgon2KDF().Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1}, 32) - c.Check(err, ErrorMatches, `invalid time cost`) -} - -func (s *argon2RemoteSupportSuite) TestInProcessKDFDeriveInvalidThreads(c *C) { - SetIsArgon2RemoteProcess() - _, err := InProcessArgon2KDF().Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 0}, 32) - c.Check(err, ErrorMatches, `invalid number of threads`) -} - -func (s *argon2RemoteSupportSuite) TestInProcessKDFTimeNotSupported(c *C) { - _, err := InProcessArgon2KDF().Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}) - c.Check(err, ErrorMatches, `no argon2 KDF: please call secboot.SetIsArgon2RemoteProcess if the intention is to run Argon2 directly in this process`) -} - -func (s *argon2RemoteSupportSuite) TestInProcessKDFTimeNoParams(c *C) { - SetIsArgon2RemoteProcess() - _, err := InProcessArgon2KDF().Time(Argon2id, nil) - c.Check(err, ErrorMatches, `nil params`) -} - -func (s *argon2RemoteSupportSuite) TestInProcessKDFTimeInvalidMode(c *C) { - SetIsArgon2RemoteProcess() - _, err := InProcessArgon2KDF().Time(Argon2Default, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}) - c.Check(err, ErrorMatches, `invalid mode`) -} - -func (s *argon2RemoteSupportSuite) TestInProcessKDFTimeInvalidTime(c *C) { - SetIsArgon2RemoteProcess() - _, err := InProcessArgon2KDF().Time(Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1}) - c.Check(err, ErrorMatches, `invalid time cost`) -} - -func (s *argon2RemoteSupportSuite) TestInProcessKDFTimeInvalidThreads(c *C) { - SetIsArgon2RemoteProcess() - _, err := InProcessArgon2KDF().Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 0}) - c.Check(err, ErrorMatches, `invalid number of threads`) -} - -func (s *argon2RemoteSupportSuite) TestRunArgon2RequestInRemoteProcessInvalidProcess(c *C) { - out := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: nil, - Keylen: 32, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32, - Threads: 4, - }) - c.Check(out, DeepEquals, &Argon2RemoteOutput{ - Command: Argon2RemoteCommandDerive, - ErrorType: Argon2RemoteErrorProcessNotConfigured, - ErrorString: "cannot run in a process that isn't configured as an Argon2 remote process", - }) -} - -func (s *argon2RemoteSupportSuite) TestRunArgon2RequestInRemoteProcessInvalidMode(c *C) { - SetIsArgon2RemoteProcess() - out := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: nil, - Keylen: 32, - Mode: Argon2Mode("foo"), - Time: 4, - MemoryKiB: 32, - Threads: 4, - }) - c.Check(out, DeepEquals, &Argon2RemoteOutput{ - Command: Argon2RemoteCommandDerive, - ErrorType: Argon2RemoteErrorInvalidMode, - ErrorString: "invalid mode: \"foo\"", - }) -} - -func (s *argon2RemoteSupportSuite) TestRunArgon2RequestInRemoteProcessInvalidTime(c *C) { - SetIsArgon2RemoteProcess() - out := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: nil, - Keylen: 32, - Mode: Argon2id, - Time: 0, - MemoryKiB: 32, - Threads: 4, - }) - c.Check(out, DeepEquals, &Argon2RemoteOutput{ - Command: Argon2RemoteCommandDerive, - ErrorType: Argon2RemoteErrorInvalidTimeCost, - ErrorString: "invalid time cost: cannot be zero", - }) -} - -func (s *argon2RemoteSupportSuite) TestRunArgon2RequestInRemoteProcessInvalidThreads(c *C) { - SetIsArgon2RemoteProcess() - out := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: nil, - Keylen: 32, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32, - Threads: 0, - }) - c.Check(out, DeepEquals, &Argon2RemoteOutput{ - Command: Argon2RemoteCommandDerive, - ErrorType: Argon2RemoteErrorInvalidThreads, - ErrorString: "invalid threads: cannot be zero", - }) -} - -func (s *argon2RemoteSupportSuite) TestArgon2RemoteOutputErr(c *C) { - out := &Argon2RemoteOutput{ - Command: Argon2RemoteCommandDerive, - ErrorType: Argon2RemoteErrorProcessNotConfigured, - 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 *Argon2RemoteError - c.Check(errors.As(err, &e), testutil.IsTrue) -} - -type argon2RemoteSupportSuiteExpensive struct { - runArgon2RemoteDir string -} - -func (s *argon2RemoteSupportSuiteExpensive) runArgon2RemotePath() string { - return filepath.Join(s.runArgon2RemoteDir, "run_argon2") -} - -func (s *argon2RemoteSupportSuiteExpensive) SetUpSuite(c *C) { - if _, exists := os.LookupEnv("NO_ARGON2_TESTS"); exists { - c.Skip("skipping expensive argon2 tests") - } - s.runArgon2RemoteDir = c.MkDir() - cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), "build", "-o", s.runArgon2RemoteDir, "./cmd/run_argon2") - c.Assert(cmd.Run(), IsNil) -} - -func (s *argon2RemoteSupportSuiteExpensive) SetUpTest(c *C) { - SetIsArgon2RemoteProcess() -} - -func (s *argon2RemoteSupportSuiteExpensive) TearDownTest(c *C) { - ClearIsArgon2RemoteProcess() -} - -var _ = Suite(&argon2RemoteSupportSuiteExpensive{}) - -type testInProcessArgon2KDFDeriveData struct { - passphrase string - salt []byte - mode Argon2Mode - params *Argon2CostParams - keyLen uint32 - - expectedKey []byte -} - -func (s *argon2RemoteSupportSuiteExpensive) testInProcessKDFDerive(c *C, data *testInProcessArgon2KDFDeriveData) { - key, err := InProcessArgon2KDF().Derive(data.passphrase, data.salt, data.mode, data.params, data.keyLen) - c.Check(err, IsNil) - c.Check(key, DeepEquals, data.expectedKey) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFDerive(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"), - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "bar", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"), - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFiDeriveDifferentSalt(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "foo", - salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"), - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFDeriveDifferentMode(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2i, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"), - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFDeriveDifferentParams(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - 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 *argon2RemoteSupportSuiteExpensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) { - s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 64, - expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"), - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestInProcessKDFTime(c *C) { - time1, err := InProcessArgon2KDF().Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4}) - c.Check(err, IsNil) - - 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) - - 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) - - 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 - c.Check(time2 > time1, testutil.IsTrue) -} - -func (s *argon2RemoteSupportSuiteExpensive) testRunArgon2RequestInRemoteProcessDerive(c *C, input *Argon2RemoteInput) { - res := RunArgon2RequestInRemoteProcess(input) - c.Check(res.Command, Equals, Argon2RemoteCommandDerive) - c.Check(res.Err(), IsNil) - - runtime.GC() - - expected := argon2.Key(input.Passphrase, input.Salt, argon2.Mode(input.Mode), &argon2.CostParams{ - Time: input.Time, - MemoryKiB: input.MemoryKiB, - Threads: input.Threads}, input.Keylen) - c.Check(expected, DeepEquals, res.Key) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDerive(c *C) { - s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: []byte("0123456789abcdefghijklmnopqrstuv"), - Mode: Argon2id, - Time: 4, - MemoryKiB: 32, - Threads: 4, - Keylen: 32, - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDeriveDifferentPassphrase(c *C) { - s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "bar", - Salt: []byte("0123456789abcdefghijklmnopqrstuv"), - Mode: Argon2id, - Time: 4, - MemoryKiB: 32, - Threads: 4, - Keylen: 32, - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDeriveDifferentSalt(c *C) { - s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), - Mode: Argon2id, - Time: 4, - MemoryKiB: 32, - Threads: 4, - Keylen: 32, - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDeriveDifferentMode(c *C) { - s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: []byte("0123456789abcdefghijklmnopqrstuv"), - Mode: Argon2i, - Time: 4, - MemoryKiB: 32, - Threads: 4, - Keylen: 32, - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDeriveDifferentParams(c *C) { - s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: []byte("0123456789abcdefghijklmnopqrstuv"), - Mode: Argon2id, - Time: 48, - MemoryKiB: 32 * 1024, - Threads: 4, - Keylen: 32, - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessDeriveDifferentKeylen(c *C) { - s.testRunArgon2RequestInRemoteProcessDerive(c, &Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: []byte("0123456789abcdefghijklmnopqrstuv"), - Mode: Argon2id, - Time: 4, - MemoryKiB: 32, - Threads: 4, - Keylen: 64, - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessTime(c *C) { - res := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ - Command: Argon2RemoteCommandTime, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4, - }) - c.Check(res.Err(), IsNil) - - ClearIsArgon2RemoteProcess() - SetIsArgon2RemoteProcess() - res2 := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ - Command: Argon2RemoteCommandTime, - 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) - - ClearIsArgon2RemoteProcess() - SetIsArgon2RemoteProcess() - res2 = RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ - Command: Argon2RemoteCommandTime, - 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) - - ClearIsArgon2RemoteProcess() - SetIsArgon2RemoteProcess() - res2 = RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ - Command: Argon2RemoteCommandTime, - 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 *argon2RemoteSupportSuiteExpensive) TestRunArgon2RequestInRemoteProcessConsumedProcess(c *C) { - out := RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: nil, - Keylen: 32, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32, - Threads: 4, - }) - c.Check(out, NotNil) - - out = RunArgon2RequestInRemoteProcess(&Argon2RemoteInput{ - Command: Argon2RemoteCommandDerive, - Passphrase: "foo", - Salt: nil, - Keylen: 32, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32, - Threads: 4, - }) - c.Check(out, DeepEquals, &Argon2RemoteOutput{ - Command: Argon2RemoteCommandDerive, - ErrorType: Argon2RemoteErrorConsumedProcess, - ErrorString: "cannot run more than once in the same process", - }) -} - -type testRemoteArgon2DeriveParams struct { - passphrase string - salt []byte - mode Argon2Mode - params *Argon2CostParams - keyLen uint32 - expectedKey []byte -} - -func (s *argon2RemoteSupportSuiteExpensive) testRemoteArgon2Derive(c *C, params *testRemoteArgon2DeriveParams) { - kdf := NewRemoteArgon2KDF(func() (*exec.Cmd, error) { - return exec.Command(s.runArgon2RemotePath()), 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 *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2Derive(c *C) { - s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"), - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2DeriveDifferentPassphrase(c *C) { - s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ - passphrase: "bar", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"), - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2DeriveDifferentSalt(c *C) { - s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ - passphrase: "foo", - salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"), - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2DeriveDifferentMode(c *C) { - s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2i, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"), - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2DeriveDifferentParams(c *C) { - s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ - 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 *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2DeriveDifferentKeyLen(c *C) { - s.testRemoteArgon2Derive(c, &testRemoteArgon2DeriveParams{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32, - Threads: 4}, - keyLen: 64, - expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"), - }) -} - -func (s *argon2RemoteSupportSuiteExpensive) TestRemoteArgon2Time(c *C) { - kdf := NewRemoteArgon2KDF(func() (*exec.Cmd, error) { - return exec.Command(s.runArgon2RemotePath()), 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 ef9c5280..13c2fc64 100644 --- a/argon2_test.go +++ b/argon2_test.go @@ -21,6 +21,7 @@ package secboot_test import ( "math" + "os" "runtime" "time" @@ -246,3 +247,193 @@ func (s *argon2Suite) TestKDFParamsInvalidMemoryKiB(c *C) { _, err := opts.KdfParams(0) 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) TestInProcessKDFDeriveInvalidTime(c *C) { + _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1}, 32) + c.Check(err, ErrorMatches, `invalid time cost`) +} + +func (s *argon2Suite) TestInProcessKDFDeriveInvalidThreads(c *C) { + _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 0}, 32) + 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) TestInProcessKDFTimeInvalidTime(c *C) { + _, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1}) + c.Check(err, ErrorMatches, `invalid time cost`) +} + +func (s *argon2Suite) TestInProcessKDFTimeInvalidThreads(c *C) { + _, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 0}) + c.Check(err, ErrorMatches, `invalid number of threads`) +} + +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{} + +var _ = Suite(&argon2Expensive{}) + +func (s *argon2Expensive) SetUpSuite(c *C) { + if _, exists := os.LookupEnv("NO_ARGON2_TESTS"); exists { + c.Skip("skipping expensive argon2 tests") + } +} + +type testInProcessArgon2KDFDeriveData struct { + passphrase string + salt []byte + mode Argon2Mode + params *Argon2CostParams + keyLen uint32 + + expectedKey []byte +} + +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) + c.Check(key, DeepEquals, data.expectedKey) +} + +func (s *argon2Expensive) TestInProcessKDFDerive(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"), + }) +} + +func (s *argon2Expensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "bar", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"), + }) +} + +func (s *argon2Expensive) TestInProcessKDFiDeriveDifferentSalt(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "foo", + salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"), + }) +} + +func (s *argon2Expensive) TestInProcessKDFDeriveDifferentMode(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2i, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"), + }) +} + +func (s *argon2Expensive) TestInProcessKDFDeriveDifferentParams(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + 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 *argon2Expensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) { + s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{ + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4}, + keyLen: 64, + expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"), + }) +} + +func (s *argon2Expensive) TestInProcessKDFTime(c *C) { + time1, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4}) + c.Check(err, IsNil) + + 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) + + 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) + + 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 + c.Check(time2 > time1, testutil.IsTrue) +} diff --git a/cmd/run_argon2/main.go b/cmd/run_argon2/main.go index c23334a4..2b5a7a81 100644 --- a/cmd/run_argon2/main.go +++ b/cmd/run_argon2/main.go @@ -31,9 +31,9 @@ func run() error { return errors.New("usage: echo | run_argon2") } - secboot.SetIsArgon2RemoteProcess() + secboot.SetIsArgon2HandlerProcess() - err := secboot.WaitAndRunArgon2RequestInRemoteProcess(os.Stdin, os.Stdout) + err := secboot.WaitForAndRunArgon2OutOfProcessRequest(os.Stdin, os.Stdout) if err != nil { return fmt.Errorf("cannot run request: %w", err) } diff --git a/export_test.go b/export_test.go index 635c8c77..3acfb4f7 100644 --- a/export_test.go +++ b/export_test.go @@ -29,13 +29,15 @@ import ( ) 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) { @@ -150,8 +152,16 @@ func MockHashAlgAvailable() (restore func()) { } } -func ClearIsArgon2RemoteProcess() { - atomic.StoreUint32(&argon2RemoteProcessStatus, notArgon2RemoteProcess) +// 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() } From 93292cd8fad85658c5eb4d2a782a636f126e276b Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Sep 2024 14:03:28 +0100 Subject: [PATCH 03/40] argon2: add in-process test for WaitForAndRunArgon2OutOfProcessRequest --- argon2_out_of_process_support_test.go | 139 +++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 5 deletions(-) diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 68187ba5..106bbc76 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -20,11 +20,14 @@ 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" @@ -39,7 +42,7 @@ func (s *argon2OutOfProcessSupportSuite) TearDownTest(c *C) { var _ = Suite(&argon2OutOfProcessSupportSuite{}) -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessRequestInvalidProcess(c *C) { +func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidProcess(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -57,7 +60,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessRequestInvalid }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessRequestInvalidMode(c *C) { +func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidMode(c *C) { SetIsArgon2HandlerProcess() out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -76,7 +79,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessRequestInvalid }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessRequestInvalidTime(c *C) { +func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidTime(c *C) { SetIsArgon2HandlerProcess() out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -95,7 +98,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessRequestInvalid }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessRequestInvalidThreads(c *C) { +func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidThreads(c *C) { SetIsArgon2HandlerProcess() out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -114,6 +117,73 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessRequestInvalid }) } +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, @@ -335,7 +405,7 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque c.Check(res2.Duration > res.Duration, testutil.IsTrue) } -func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestConsumedProcess(c *C) { +func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDeriveConsumedProcess(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -365,6 +435,65 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque }) } +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 From 844021f55293a91f761a761cae35604d796858b0 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Mon, 9 Sep 2024 11:57:58 +0100 Subject: [PATCH 04/40] [WIP] address review comments --- argon2_out_of_process_support.go | 679 +++++++++++++++++++++++--- argon2_out_of_process_support_test.go | 319 +++++++++--- cmd/run_argon2/main.go | 15 +- 3 files changed, 858 insertions(+), 155 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 51fa33ba..9aeadaf1 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -21,13 +21,19 @@ package secboot import ( "bytes" + "crypto" + "crypto/hmac" + "crypto/rand" "encoding/json" "errors" "fmt" "io" "os/exec" + "sync" "sync/atomic" "time" + + "gopkg.in/tomb.v2" ) // Argon2OutOfProcessCommand represents an argon2 command to run out of process. @@ -40,19 +46,26 @@ const ( // Argon2OutOfProcessCommandTime requests the duration that the KDF took to // execute. This excludes additional costs such as process startup. Argon2OutOfProcessCommandTime Argon2OutOfProcessCommand = "time" + + // Argon2OutOfProcessCommandWatchdog requests a watchdog ping, when using + // [WaitForAndRunArgon2OutOfProcessRequest]. This does not work with + // [RunArgon2OutOfProcessRequest], which runs the supplied request synchronously + // in the current go routine. + Argon2OutOfProcessCommandWatchdog Argon2OutOfProcessCommand = "watchdog" ) // 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 + 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 + WatchdogChallenge []byte `json:"watchdog-challenge,omitempty"` // A challenge value for watchdog pings (when the command is "watchdog") } // Argon2OutOfProcessErrorType describes the type of error produced by [RunArgon2OutOfProcessRequest] @@ -72,31 +85,28 @@ const ( // 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" + // Argon2OutOfProcessErrorUnexpectedInput means that there was an error with the combination + // of inputs associated with the supplied request. + Argon2OutOfProcessErrorUnexpectedInput Argon2OutOfProcessErrorType = "unexpected-input" - // Argon2OutOfProcessErrorProcessNotConfigured means that nothing has called SetIsArgon2HandlerProcess - // to configure the process for handling an Argon2 request. - Argon2OutOfProcessErrorProcessNotConfigured Argon2OutOfProcessErrorType = "process-not-configured" + // Argon2OutOfProcessErrorRestartProcess means that this process has already performed one + // execution of the KDF, and the process should exit and be replaced by a new one. + Argon2OutOfProcessErrorRestartProcess Argon2OutOfProcessErrorType = "restart-process" // 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 + 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" + WatchdogResponse []byte `json:"watchdog-response,omitempty"` // The response to a watchdog ping, if the input command was "watchdog" + 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] @@ -136,7 +146,7 @@ const ( 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") +var errArgon2OutOfProcessHandlerExpired = errors.New("argon2 out-of-process handler has already been used to process a request - a new process should be started to handle another request") // argon2OutOfProcessHandler is an implementation of Argon2KDF that will // only process a single call before returning an error on subsequent calls. @@ -145,23 +155,24 @@ type argon2OutOfProcessHandler struct { 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 { +// canHandleRequest returns whether this KDF can be used to handle a request. +// It will only ever return true once. If it returns false, the pending KDF +// request must be rejected. On the single occasion that it returns true +// true, then the pending KDF request can be handled, but subsequent calls to +// this function will always return false. +func (k *argon2OutOfProcessHandler) canHandleRequest() 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() { + if !k.canHandleRequest() { 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() { + if !k.canHandleRequest() { return 0, errArgon2OutOfProcessHandlerExpired } return k.KDF.Time(mode, params) @@ -194,7 +205,7 @@ var ( // and [WaitForAndRunArgon2OutOfProcessRequest]. func SetIsArgon2HandlerProcess() { // Mark process as becoming an argon2 handler process. This will ensure that new calls - // to SetArgon2KDF to panic. + // to both this function and SetArgon2KDF will panic. if !atomic.CompareAndSwapUint32(&argon2OutOfProcessStatus, notArgon2HandlerProcess, becomingArgon2HandlerProcess) { panic("cannot call SetIsArgon2HandlerProcess more than once") } @@ -206,7 +217,9 @@ func SetIsArgon2HandlerProcess() { // 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. + // whatever was there previously. Any future calls to SetArgon2KDF will panic. The + // implementation we set is a version of InProcessArgon2KDF that can only run a single + // time. argon2Impl = &argon2OutOfProcessHandler{ Status: argon2Unused, KDF: InProcessArgon2KDF, @@ -223,15 +236,27 @@ func SetIsArgon2HandlerProcess() { // // This function requires [SetIsArgon2HandlerProcess] to have already been called in this process, // else an error response will be returned. +// +// This is quite a low-level function, suitable for implementations that want to manage their own +// transport. In general, implementations will use [WaitForAndRunArgon2OutOfProcessRequest]. +// +// This function does not service watchdog requests, as the KDF request happens synchronously in the +// current go routine. If this is required, it needs to be implemented in supporting code that makes +// use of other go routines. [WaitForAndRunArgon2OutOfProcessRequest] already does this. +// +// Unfortunately, there is no way to interrupt this function once it has been called. because the +// low-level crypto library does not support this. func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2OutOfProcessResponse { if atomic.LoadUint32(&argon2OutOfProcessStatus) < readyArgon2HandlerProcess { + // SetIsArgon2HandlerProcess hasn't been called, or hasn't completed yet. 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", + ErrorType: Argon2OutOfProcessErrorUnexpected, + ErrorString: "cannot handle request in a process that isn't configured as an Argon2 handler process, try calling SetIsArgon2HandlerProcess", } } + // Perform checks of arguments that are common to call requests switch request.Mode { case Argon2id, Argon2i: // ok @@ -263,19 +288,30 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out } } + if len(request.WatchdogChallenge) > 0 { + // This function does everything in the same go routine, and therefore + // has no ability to service a watchdog. + return &Argon2OutOfProcessResponse{ + Command: request.Command, + ErrorType: Argon2OutOfProcessErrorUnexpectedInput, + ErrorString: "invalid watchdog challenge: cannot service a watchdog", + } + } + switch request.Command { case Argon2OutOfProcessCommandDerive: + // Perform key derivation 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 + errorType = Argon2OutOfProcessErrorRestartProcess } return &Argon2OutOfProcessResponse{ Command: request.Command, ErrorType: errorType, - ErrorString: fmt.Sprintf("cannot run derive command: %v", err), + ErrorString: fmt.Sprintf("cannot run \"derive\" command: %v", err), } } return &Argon2OutOfProcessResponse{ @@ -283,6 +319,7 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out Key: key, } case Argon2OutOfProcessCommandTime: + // Make sure that redundant parameters haven't been set. if len(request.Passphrase) > 0 { return &Argon2OutOfProcessResponse{ Command: request.Command, @@ -305,17 +342,18 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out } } + // Perform timing of the supplied cost parameters. 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 + errorType = Argon2OutOfProcessErrorRestartProcess } return &Argon2OutOfProcessResponse{ Command: request.Command, ErrorType: errorType, - ErrorString: fmt.Sprintf("cannot run time command: %v", err), + ErrorString: fmt.Sprintf("cannot run \"time\" command: %v", err), } } return &Argon2OutOfProcessResponse{ @@ -323,6 +361,8 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out Duration: duration, } default: + // This is an unrecognized commmand. This includes watchdog requests, which must be handled by + // a higher level function. return &Argon2OutOfProcessResponse{ Command: request.Command, ErrorType: Argon2OutOfProcessErrorInvalidCommand, @@ -331,6 +371,63 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out } } +// Argon2OutOfProcessWatchdogHandler defines the behaviour of the watchdog +// service handler for out-of-process [Argon2KDF] implementations, and is on +// the remote side of the implementation (the process that generally calls +// [SetIsArgon2HandlerProcess]) if it using one of the built in functions. +// It is intended to be compatible with [WaitForAndRunArgon2OutOfProcessRequest]. +// +// The out-of-process Argon2 implementation is expected to call this inline on +// the same goroutine that loops over received requests, as +// [WaitForAndRunArgon2OutOfProcessRequest] does. +// +// The out-of-process Argon2 implementation is expected to only send watchdog +// requests to this function, as [WaitForAndRunArgon2OutOfProcessRequest] does. +// +// Implementations of this should return the response or an error. If an error +// is returned, the calling loop should break and the remote process implementation +// should begin terminating (as [WaitForAndRunArgon2OutOfProcessRequest] does). +// +// Implementations must not return a nil response and a nil error. +type Argon2OutOfProcessWatchdogHandler = func(req *Argon2OutOfProcessRequest) (*Argon2OutOfProcessResponse, error) + +// Argon2OutOfProcessWatchdogHandlerHMACSHA256 is the counterpart to +// [Argon2OutOfProcessWatchgogMonitorHMACSHA256]. It generates a HMAC-SHA256 of +// the received challenge, keyed with the previously transmitted response. +func Argon2OutOfProcessWatchdogHandlerHMACSHA256() Argon2OutOfProcessWatchdogHandler { + lastWatchdogResponse := make([]byte, 32) + + return func(req *Argon2OutOfProcessRequest) (*Argon2OutOfProcessResponse, error) { + if req.Command != Argon2OutOfProcessCommandWatchdog { + return nil, fmt.Errorf("unexpected command %q", req.Command) + } + + h := hmac.New(crypto.SHA256.New, lastWatchdogResponse) + h.Write(req.WatchdogChallenge) + + // Generate the response structure + rsp := &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandWatchdog, + WatchdogResponse: h.Sum(nil), + } + + // Save the response we just sent because it's needed for the next + // watchdog request. + lastWatchdogResponse = rsp.WatchdogResponse + + return rsp, nil + } +} + +func NoArgon2OutOfProcessWatchdogHandler(req *Argon2OutOfProcessRequest) (*Argon2OutOfProcessResponse, error) { + return nil, errors.New("unexpected watchdog request: no handler service for it") +} + +// ErrKDFNotRequested is returned from [WaitForAndRunArgon2OutOfProcessRequest] +// if the supplied io.Reader is closed by the parent before a [Argon2OutOfProcessRequest] +// has been received. +var ErrKDFNotRequested = errors.New("no KDF request was received") + // 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 @@ -340,77 +437,504 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out // result in an error response being returned via the io.Writer (after receiving a new request via // the io.Reader). // +// This function can service watchdog requests from the parent process by providing an appropriate +// watchdog handler. If no watchdog handler is supplied, [NoArgon2OutOfProcessWatchdogHandler] is used. +// +// Unfortunately, KDF requests cannot be interrupted once they have started because the low-level crypto +// library does not provide this functionality. +// // 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) +// +// Most errors are sent back to the parent process via the supplied io.Writer. In some cases, errors +// returned from go routines that are created during the handling of a request may be returned directly +// from this function. +// +// Note that this function won't return until the supplied io.Reader is closed (it returns [io.EOF]), +// and all created go routines terminate. As long as no go routines return an error and the io.Reader is +// closed after receiving a KDF request, then no error will be returned. If the io.Reader is closed before +// a KDF request is received, a [ErrKDFNotRequested] error will be returned. +// +// As this function cannot be used again, the calling process is expected to exit once this returns. +func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdog Argon2OutOfProcessWatchdogHandler) error { + if watchdog == nil { + watchdog = NoArgon2OutOfProcessWatchdogHandler } - rsp := RunArgon2OutOfProcessRequest(req) + tmb := new(tomb.Tomb) + + // rspChan is sent a response which is received by another goroutine, + // which serializes it and sends it to the parent process via the + // supplied io.Writer. + rspChan := make(chan *Argon2OutOfProcessResponse) + + // wdRspChan is send watchdog responses which are received by another + // goroutine, which serializes them and sends them to the parent process + // via the supplied io.Writer. + wdRspChan := make(chan *Argon2OutOfProcessResponse) + + // Spin-up a routine for receiving incoming requests - this will include the initial + // request and periodic watchdog requests if these are in use. + tmb.Go(func() error { + // Also spin-up the routine for sending outgoing responses that are generated internally. + // This handles the receive ends of rspChan and wdRspChan, and serializes them to the + // supplied io.Writer. + tmb.Go(func() error { + for { + // Wait for a response from somewhere or wait for the tomb to + // begin dying. + var jsonRsp *Argon2OutOfProcessResponse + select { + case rsp := <-rspChan: + jsonRsp = rsp + case rsp := <-wdRspChan: + jsonRsp = rsp + case <-tmb.Dying(): + // The tomb has begun dying - this is a normal part of shutdown, + // so return now. Senders to the channels we receive need to be + // careful about handling this routine terminating before they + // do in order to avoid deadlocks with unbuffered channels. + return nil + } + // We've got a response from somewhere. Encode it send the + // response out on the io.Writer. If this fails, return an error, + // which begins the dying of this tomb and will result in an error + // being returned to the caller. + enc := json.NewEncoder(out) + if err := enc.Encode(jsonRsp); err != nil { + return fmt.Errorf("cannot encode response: %w", err) + } + } + panic("not reached") + }) + + // kdfRequestReceived indicates that a KDF request was received. If one has been received, + // it's not an error for the parent to close its side of the incoming channel. + kdfRequestReceived := false + + // Run a loop for receiving incoming requests from the io.Reader as long + // as the tomb remains alive. + for tmb.Alive() { + // Wait for a request from the io.Reader. The only way to unblock this is + // if the parent sends something or closes its end of the OS pipe. If it's + // closed before we've received a KDF request, then we terminate this routine + // with an error to begin the process of the tomb dying and the function + // eventually returning an error. + var req *Argon2OutOfProcessRequest + dec := json.NewDecoder(in) + dec.DisallowUnknownFields() + if err := dec.Decode(&req); err != nil { + if errors.Is(err, io.EOF) { + if !kdfRequestReceived { + // The parent has closed their end of the connection before + // sending a request, so we return an error here to begin + // the shutdown of the entire tomb and return an appropriate + // error to the caller. + return ErrKDFNotRequested + } + // In any case, if the parent has closed their end of the pipe, there's + // nothing else for us to process so we can break from this loop in order + // to shutdown of this routine. We can't receive any more watchdog requests. + break + } + return fmt.Errorf("cannot decode request: %w", err) + } - enc := json.NewEncoder(out) - if err := enc.Encode(rsp); err != nil { - return fmt.Errorf("cannot encode response: %w", err) - } + switch req.Command { + case Argon2OutOfProcessCommandWatchdog: + // Special case to handle watchdog requests + rsp, err := watchdog(req) + if err != nil { + return fmt.Errorf("cannot run watchdog handler: %w", err) + } + + // Send the response structure to be serialized and sent to the parent. + // This blocks until the response is received by the other end of the + // channel, or the tomb enters a dying state. + select { + case wdRspChan <- rsp: + // The watchdog response was delivered to the receiving end ok + case <-tmb.Dying(): + // The watchdog response was not delivered because the tomb entered + // a dying state and the receive end of wdRspChan has already gone. + } + default: + // Anything else is considered a KDF request + kdfRequestReceived = true + + // Spin up a new routine to handle the request, as it blocks and is long running, + // and we still want to be able to service watchdog requests. Block the current + // routine until we know the new routine has started so that the watchdog handler + // will fail if the new routine doesn't begin properly. + var startupWg sync.WaitGroup + startupWg.Add(1) + tmb.Go(func() error { + startupWg.Done() // Unblock the main routine. + + // Run the KDF request. + rsp := RunArgon2OutOfProcessRequest(req) + + // Send the response structure to be serialized and sent to the parent. + // This blocks until the response is received by the other end of the + // channel, or the tomb enters a dying state. + select { + case rspChan <- rsp: + // The response was delivered to the receiving end ok. + case <-tmb.Dying(): + // The response was not delivered because the tomb entered + // a dying state and the receive end of rspChan has already gone. + } + + // As we only handle a single request, mark the tomb as dying to begin its + // clean shutdown if it isn't shutting down already. + tmb.Kill(nil) + return nil + }) + + // Wait until the routine we spun up to run the KDF request is running before processing + // watchdog requests. If we end up blocked here then the watchdog handler will fail + // to respond. + startupWg.Wait() + } + } + return nil + }) + + // Wait for the tomb to die and return the first error that occurred to the caller if there was one. + return tmb.Wait() +} +// Argon2OutOfProcessWatchdogMonitor defines the behaviour of a watchdog monitor +// for out-of-process [Argon2KDF] implementations, and is managed on the parent side +// of an implementation of [Argon2KDF]. +// +// It is expected to be called in its own dedicated go routine that is tracked +// by the supplied tomb, as with the implementation of [Argon2KDF] returned by +// [NewOutOfProcessArgon2KDF]. +// +// Implementations define their own protocol, with limitations. All requests and +// responses use the watchdog command [Argon2OutOfProcessCommandWatchdog]. The +// [Argon2OutOfProcessRequest] type has a WatchdogChallenge field. The +// [Argon2OutOfProcessResponse] type has a WatchdogResponse field. It's up +// to the implementation how they choose to use these fields. +// +// If the watchdog isn't serviced by the remote process correctly or within some +// time limit, the implementation is expected to return an error. +// +// The [Argon2KDF] implementation that manages this watchdog should kill the remote +// process in the event that the monitor implementation returns an error, as +// the one returned by [NewOutOfProcessArgon2KDF] does. +// +// The implementation of this should not close reqChan. It should also expect reqChan +// to be blocking (ie, not buffered), as is the case with the [Argon2KDF] implementaton +// returned from [NewOutOfProcessArgon2KDF]. +// +// It is expected that the [Argon2KDF] implementation that manages this watchdog +// only sends watchdog requests via the rspChan channel (ie, it's verified that the +// Command field in the [Argon2OutOfProcessResponse] is [Argon2OutOfProcessCommandWatchdog]). +// The implementation returned by [NewOutOfProcessArgon2KDF] makes this assurance. +// +// It is expected that the [Argon2KDF] implementation doesn't close the suppled rspChan, +// as the one returned by [NewOutOfProcessArgon2KDF] doesn't. +// +// The implementation should continue to process requests as long as the supplied tomb +// is alive ([Tomb.Alive] returns true) before returning, unless an error occurs. +type Argon2OutOfProcessWatchdogMonitor = func(tmb *tomb.Tomb, reqChan chan<- *Argon2OutOfProcessRequest, rspChan <-chan *Argon2OutOfProcessResponse) error + +func NoArgon2OutOfProcessWatchdogMonitor(tmb *tomb.Tomb, reqChan chan<- *Argon2OutOfProcessRequest, rspChan <-chan *Argon2OutOfProcessResponse) error { + for { + select { + case <-rspChan: + // We never sent a request, so this is an error + return errors.New("unexpected watchdog response: no monitor sending requests") + case <-tmb.Dying(): + return nil + } + } 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. +// Argon2OutOfProcessWatchdogMonitorHMACSHA256 returns a watchdog monitor that generates a +// challenge, computes a HMAC-SHA256 of this challenge, keyed with previously received +// watchdog response. It It stops and returns an error if we don't receive a respose +func Argon2OutOfProcessWatchdogMonitorHMACSHA256(period, timeout time.Duration) Argon2OutOfProcessWatchdogMonitor { + lastWatchdogResponse := make([]byte, 32) + + return func(tmb *tomb.Tomb, reqChan chan<- *Argon2OutOfProcessRequest, rspChan <-chan *Argon2OutOfProcessResponse) error { + for { + timer := time.NewTimer(period) + select { + case <-timer.C: // run it every defined period + case <-tmb.Dying(): // exit early if the tomb is dying + timer.Stop() // Make sure the timer can be collected. Not necessary from go 1.23 + return nil + } + + // Generate a new 32-byte challenge and calculate the expected response + challenge := make([]byte, 32) + if _, err := rand.Read(challenge); err != nil { + return fmt.Errorf("cannot generate new watchdog challenge: %w", err) + } + // The expected response is the HMAC-SHA256 of the challenge, keyed with the + // last response. + h := hmac.New(crypto.SHA256.New, lastWatchdogResponse) + h.Write(challenge) + expectedWatchdogResponse := h.Sum(nil) + + req := &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandWatchdog, + WatchdogChallenge: challenge, + } + + // Send the request. If reqChan is unbuffered, this blocks until the + // request is received by the other end or the tomb enters a dying state. + select { + case reqChan <- req: + // The watchdog request was sent ok. + case <-tmb.Dying(): + // The watchdog request was not sent ok because the tomb is + // in a dying state and receiving end has already gone away. + // Just return now as we won't run another iteration of this + // loop anyway. + return nil + } + + // Wait for the response from the remote process. + timer = time.NewTimer(timeout) + select { + case <-timer.C: // Give it up to the time defined by the timeout + return errors.New("timeout waiting for watchdog response from remote process") + case rsp := <-rspChan: + // We got a response from the remote process. + timer.Stop() // Make sure the timer can be collected. Not necessary from go 1.23 + if err := rsp.Err(); err != nil { + // We got an error response, so just return the error. + return fmt.Errorf("cannot process watchdog response from remote process: %w", rsp.Err()) + } + if !bytes.Equal(rsp.WatchdogResponse, expectedWatchdogResponse) { + // We got an unexpected response, so return an error. + return errors.New("unexpected watchdog response value from remote process") + } + // The response was good so save the value for the next iteration. + lastWatchdogResponse = rsp.WatchdogResponse + case <-tmb.Dying(): + // Don't need to wait any more as the tomb has begun dying. + timer.Stop() // Make sure the timer can be collected. Not necessary from go 1.23 + return nil + } + } + return nil + } +} + +// outOfProcessArgon2KDFImpl is an Argon2KDF implementation that runs the KDF in a short-lived +// remote process, using the remote JSON protocol defined in this package. type outOfProcessArgon2KDFImpl struct { newHandlerCmd func() (*exec.Cmd, error) + watchdog Argon2OutOfProcessWatchdogMonitor } -func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2OutOfProcessRequest) (rsp *Argon2OutOfProcessResponse, err error) { +func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2OutOfProcessRequest) (*Argon2OutOfProcessResponse, error) { + // Use ther user-supplied function to create a new *exec.Cmd structure. cmd, err := k.newHandlerCmd() if err != nil { return nil, fmt.Errorf("cannot create new command: %w", err) } + // Configure an OS pipe for stdin for sending requests. 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) } + + // Configure an OS pipe for stdout for receiving responses. 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. + // cleanup to do on failure paths other than closing the stdinPipe + stdinPipe.Close() return nil, fmt.Errorf("cannot create stdout pipe: %w", err) } + // Start the remote process. 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) + + var actualRsp *Argon2OutOfProcessResponse + tmb := new(tomb.Tomb) + + // Spin up a routine to handle communications with the remote process and wait + // for it to exit. + tmb.Go(func() error { + // wdReqChan is sent requests from the watchdog monitor which are then + // received by another goroutine, which serializes them and sends them to + // the remote process via its stdin. + wdReqChan := make(chan *Argon2OutOfProcessRequest) + + // wdRspChan is sent watchdog responses received from the remote process + // via stdout, and they are subsequently received by the watchdog monitor. + wdRspChan := make(chan *Argon2OutOfProcessResponse) + + // reqChan is sent the main KDF request, which is received by another goroutine, + // which serializes it and sends it to the remote process via its stdin. + reqChan := make(chan *Argon2OutOfProcessRequest) + + // Spin up a routine for sending requests to the remote process via stdinPipe. + tmb.Go(func() error { + for { + var jsonReq *Argon2OutOfProcessRequest + + // Handle serializing and sending requests until we begin the process of + // dying. + select { + case req := <-reqChan: + // We have the main KDF request to send. + jsonReq = req + case req := <-wdReqChan: + // We have a request from the watchdog monitor to send. + jsonReq = req + case <-tmb.Dying(): + // The tomb has begun dying - this is a normal part of shutdown, + // so return now. Senders to the channels we receive need to be + // careful about handling this routine terminating before they + // do in order to avoid deadlocks with unbuffered channels. + return nil + } + + // Send the request to the remote process via its stdin + enc := json.NewEncoder(stdinPipe) + if err := enc.Encode(jsonReq); err != nil { + return fmt.Errorf("cannot encode request: %w", err) + } + + } + panic("not reached") + }) + + // Send the initial request. This will block until the request + // is received by the other end (the previously started go + // routine), or until the tomb enters a dying state. + select { + case reqChan <- req: + // Initial request sent ok. + case <-tmb.Dying(): + // Initial request was not sent, because the tomb is already + // dying and the receiving end has already gone away. + // + // This is an abnormal condition - nothing should have + // put the tomb into a dying state at this point. There + // should already be an error associated with whatever + // caused this, but just in case there isn't, we'll return + // our own error from the current routine. + return errors.New("internal error: cannot send KDF request because the tomb is already in a dying state for an unknown reason") } - }() - // 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) - } + // Spin up another routine to run the supplied watchdog implementation. + tmb.Go(func() error { + err := k.watchdog(tmb, wdReqChan, wdRspChan) + if err != nil { + // If the watchdog returns an error, then we kill the remote process. + if err := cmd.Process.Kill(); err != nil { + return fmt.Errorf("cannot kill remote process that stopped responding to watchdog: %w", err) + } + } + return err + }) + + // Wait for responses from the remote process whilst the tomb is alive. + for tmb.Alive() { + // Wait for a response from the io.Reader. The only way to unblock this is + // if the remote process sends something, closes its end of the OS pipe + // explicitly, or is terminated (either by its own choice or by us). + // + // In general, the last response received is the response to the initial KDF + // request. When we receive this, we put the tomb into a dying state and therefore + // there won't be another iteration of this loop and this code won't be reached + // again. + // + // Of course, if this instance is created with a proper watchdog implementation, + // we can kill processes that we know are genuinely not responding as well. + dec := json.NewDecoder(stdoutPipe) + dec.DisallowUnknownFields() + var rsp *Argon2OutOfProcessResponse + if err := dec.Decode(&rsp); err != nil { + return fmt.Errorf("cannot decode response: %w", err) + } + + switch rsp.Command { + case Argon2OutOfProcessCommandWatchdog: + // Direct watchdog responses to wdRspChan so they can be received by + // the watchdog monitor. This blocks until the watchdog monitor receives + // the response or the tomb enters a dying state. + select { + case wdRspChan <- rsp: + // Watchdog response sent to watchdog monitor ok. + case <-tmb.Dying(): + // Watchdog response was not sent to the watchdog + // monitor because the tomb is in a dying state and + // the receiving end has already gone away (watchdog + // implementations are meant to end once the tomb enters + // a dying state). + // + // There's nothing else to do as the outer loop will just + // not run another iteration again. + } + default: + // For any other response, first of all make sure that the command value is + // consistent with the sent command. + if rsp.Command != req.Command { + // Unexpected command. Return an appropriate error to begin the process + // of the tomb dying + return fmt.Errorf("received a response with an unexpected command value (got %q, expected %q)", rsp.Command, req.Command) + } + // If it is consistent, save the response to return to the caller and begin a clean + // shutdown of the tomb. + actualRsp = rsp + tmb.Kill(nil) + } + } + + // Set up a routine to kill the process if it doesn't terminate within 2 seconds. + waitedChan := make(chan struct{}) + tmb.Go(func() error { + timer := time.NewTimer(2 * time.Second) + select { + case <-timer.C: + // The process has not terminated cleanly, so kill it. + if err := cmd.Process.Kill(); err != nil { + return fmt.Errorf("cannot kill remote process that hasn't terminated properly: %w", err) + } + case <-waitedChan: + // The process terminated cleanly + timer.Stop() // Make sure the timer can be collected. Not necessary from go 1.23 + } + return nil + }) - // 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) + // Closing the stdin pipe might be necessary to unblock a stuck goroutine in the + // remote process. It is necessary for WaitForAndRunArgon2OutOfProcessRequest. + if err := stdinPipe.Close(); err != nil { + return fmt.Errorf("cannot close stdin pipe: %w", err) + } + // We can wait for the remote process to exit now. + if err := cmd.Wait(); err != nil { + return fmt.Errorf("an error occurred whilst waiting for the remote process to finish: %w", err) + } + close(waitedChan) + return nil + }) + + // Wait for all go routines to finish. + if err := tmb.Wait(); err != nil { + // Don't wrap this error - this will be the first non-nil error passed + // to Tomb.Kill. There's no benefit to adding additional context here. + return nil, err } - return rsp, nil + return actualRsp, nil } func (k *outOfProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) (key []byte, err error) { @@ -461,11 +985,20 @@ func (k *outOfProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostPara // 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 { +// +// An optional watchdog monitor can be supplied. If nil, the +// [NoArgon2OutOfProcessWatchdogMonitor] is used by default, providing no watchdog +// functionality. +func NewOutOfProcessArgon2KDF(newHandlerCmd func() (*exec.Cmd, error), watchdog Argon2OutOfProcessWatchdogMonitor) Argon2KDF { if newHandlerCmd == nil { panic("newHandlerCmd cannot be nil") } + if watchdog == nil { + watchdog = NoArgon2OutOfProcessWatchdogMonitor + } + return &outOfProcessArgon2KDFImpl{ newHandlerCmd: newHandlerCmd, + watchdog: watchdog, } } diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 106bbc76..d8f35610 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -28,10 +28,12 @@ import ( "path/filepath" "runtime" "sync" + "time" . "github.com/snapcore/secboot" "github.com/snapcore/secboot/internal/testutil" . "gopkg.in/check.v1" + "gopkg.in/tomb.v2" ) type argon2OutOfProcessSupportSuite struct{} @@ -50,13 +52,13 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestI Keylen: 32, Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, 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", + ErrorType: Argon2OutOfProcessErrorUnexpected, + ErrorString: "cannot handle request in a process that isn't configured as an Argon2 handler process, try calling SetIsArgon2HandlerProcess", }) } @@ -69,7 +71,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestI Keylen: 32, Mode: Argon2Mode("foo"), Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 10224, Threads: 4, }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ @@ -88,7 +90,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestI Keylen: 32, Mode: Argon2id, Time: 0, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ @@ -107,7 +109,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestI Keylen: 32, Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 0, }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ @@ -124,7 +126,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInv Passphrase: "foo", Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ @@ -141,7 +143,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInv Salt: []byte("0123456789abcdefghijklmnopqrstuv"), Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ @@ -158,7 +160,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInv Keylen: 32, Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ @@ -174,7 +176,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessInvalidCommand Command: Argon2OutOfProcessCommand("foo"), Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ @@ -187,11 +189,11 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessInvalidCommand func (s *argon2OutOfProcessSupportSuite) TestArgon2OutOfProcessResponseErr(c *C) { out := &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, - ErrorType: Argon2OutOfProcessErrorProcessNotConfigured, + ErrorType: Argon2OutOfProcessErrorUnexpected, 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\)`) + c.Check(err, ErrorMatches, `cannot process KDF request: unexpected-error \(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) } @@ -216,6 +218,79 @@ func (s *argon2OutOfProcessSupportSuite) TestCallingSetIsArgon2HandlerAfterSetAr c.Check(GlobalArgon2KDF().(*Argon2OutOfProcessHandler).Status, Equals, uint32(0)) } +func (s *argon2OutOfProcessSupportSuite) TestCallingSetArgon2KDFAfterSetIsArgon2HandlerPanics(c *C) { + SetIsArgon2HandlerProcess() + c.Check(func() { SetArgon2KDF(InProcessArgon2KDF) }, PanicMatches, `cannot call SetArgon2KDF in a process where SetIsArgon2HandlerProcess has already been called`) +} + +func (s *argon2OutOfProcessSupportSuite) TestNoArgon2OutOfProcessWatchdogHandler(c *C) { + req := &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandWatchdog, + WatchdogChallenge: []byte{1, 2, 3, 4}, + } + rsp, err := NoArgon2OutOfProcessWatchdogHandler(req) + c.Check(rsp, IsNil) + c.Check(err, ErrorMatches, `unexpected watchdog request: no handler service for it`) +} + +func (s *argon2OutOfProcessSupportSuite) TestArgon2OutOfProcessWatchdogHandlerHMACSHA256InvalidCommand(c *C) { + handler := Argon2OutOfProcessWatchdogHandlerHMACSHA256() + + req := &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandTime, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + } + rsp, err := handler(req) + c.Check(rsp, IsNil) + c.Check(err, ErrorMatches, `unexpected command "time"`) +} + +func (s *argon2OutOfProcessSupportSuite) TestArgon2OutOfProcessWatchdogHandlerHMACSHA256(c *C) { + handler := Argon2OutOfProcessWatchdogHandlerHMACSHA256() + + req := &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandWatchdog, + WatchdogChallenge: testutil.DecodeHexString(c, "79e7d47fed15d6eef1e7e5f54cbb69d37169378527d65d2ba809a364930e94e3"), + } + rsp, err := handler(req) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandWatchdog, + WatchdogResponse: testutil.DecodeHexString(c, "1fed49cb3f22b3ddc895a7837833d84b181bda9a1a6f098a297d163729a33c58"), + }) + + req = &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandWatchdog, + WatchdogChallenge: testutil.DecodeHexString(c, "3c1de58760e53cac4facc2d5409b362fcf9b81f9b611479f5956abdb0227e567"), + } + rsp, err = handler(req) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandWatchdog, + WatchdogResponse: testutil.DecodeHexString(c, "cb487ce254d115cf91f282c8c82a6c5d01b16db99f71175242d060f455fc4624"), + }) +} + +func (s *argon2OutOfProcessSupportSuite) TestNoArgon2OutOfProcessWatchdogMonitorUnexpectedResponse(c *C) { + reqChan := make(chan *Argon2OutOfProcessRequest) + rspChan := make(chan *Argon2OutOfProcessResponse) + + tmb := new(tomb.Tomb) + tmb.Go(func() error { + // Run a routine for running NoArgon2OutOfProcessWatchdogMonitor. + tmb.Go(func() error { + return NoArgon2OutOfProcessWatchdogMonitor(tmb, reqChan, rspChan) + }) + + rspChan <- new(Argon2OutOfProcessResponse) + return nil + }) + c.Check(tmb.Wait(), ErrorMatches, `unexpected watchdog response: no monitor sending requests`) +} + type argon2OutOfProcessSupportSuiteExpensive struct { runArgon2OutputDir string } @@ -243,6 +318,62 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TearDownTest(c *C) { var _ = Suite(&argon2OutOfProcessSupportSuiteExpensive{}) +func (s *argon2OutOfProcessSupportSuiteExpensive) TestNoArgon2OutOfProcessWatchdogMonitor(c *C) { + reqChan := make(chan *Argon2OutOfProcessRequest) + rspChan := make(chan *Argon2OutOfProcessResponse) + + doneEarlyChan := make(chan struct{}) + + tmb := new(tomb.Tomb) + tmb.Go(func() error { + // Run a routine for running NoArgon2OutOfProcessWatchdogMonitor. + tmb.Go(func() error { + defer func() { + // What we want to do here is ensure NoArgon2OutOfProcessWatchdogMonitor + // doesn't return until the provided tomb enters a dying state. This check + // is a bit racy because there's a small window where NoArgon2OutOfProcessWatchdogMonitor + // returns and then the tomb enters a dying state before this defer runs. + // I think that this is ok for now. + select { + case <-tmb.Dying(): + // do nothing + default: + // Tomb is still alive + close(doneEarlyChan) + } + }() + return NoArgon2OutOfProcessWatchdogMonitor(tmb, reqChan, rspChan) + }) + + // Run another routine to make sure we get no watchdog requests and to detect whether + // NoArgon2OutOfProcessWatchdogMonitor returns early. + tmb.Go(func() error { + for tmb.Alive() { + select { + case <-reqChan: + return errors.New("unexpected watchdog request") + case <-doneEarlyChan: + return errors.New("watchdog monitor returned whilst tomb is still alive") + case <-tmb.Dying(): + } + } + return nil + }) + + // Run the test for 1 second. + select { + case <-time.After(1 * time.Second): + // Kill the tomb to finish the test + tmb.Kill(nil) + case <-tmb.Dying(): + // Something else already failed - there's no point in waiting for the timeout here + return nil + } + return nil + }) + c.Check(tmb.Wait(), IsNil) +} + type testRunArgon2OutOfProcessRequestDeriveParams struct { req *Argon2OutOfProcessRequest expectedKey []byte @@ -264,11 +395,11 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque Salt: []byte("0123456789abcdefghijklmnopqrstuv"), Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, Keylen: 32, }, - expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"), + expectedKey: testutil.DecodeHexString(c, "b47ad96075d64cb92cdc7678e6bbb85f496da6e84d7ea05fbc0092dfb0ac3e13"), }) } @@ -280,11 +411,11 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque Salt: []byte("0123456789abcdefghijklmnopqrstuv"), Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, Keylen: 32, }, - expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"), + expectedKey: testutil.DecodeHexString(c, "e5081bdbb5dc709ecd789ad6da76ce6c49d2bc3b958dda4a93c6b4140def877e"), }) } @@ -296,11 +427,11 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque Salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, Keylen: 32, }, - expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"), + expectedKey: testutil.DecodeHexString(c, "bd962af1e81debad7966d3c0ca1dd9398dc231a3c25c96de54a1df97233d1a49"), }) } @@ -312,11 +443,11 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque Salt: []byte("0123456789abcdefghijklmnopqrstuv"), Mode: Argon2i, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, Keylen: 32, }, - expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"), + expectedKey: testutil.DecodeHexString(c, "0d781d62896d7bb71830251af01be0323f2006770beb917e62a2ea3330693625"), }) } @@ -328,11 +459,11 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque Salt: []byte("0123456789abcdefghijklmnopqrstuv"), Mode: Argon2id, Time: 48, - MemoryKiB: 32 * 1024, + MemoryKiB: 64 * 1024, Threads: 4, Keylen: 32, }, - expectedKey: testutil.DecodeHexString(c, "f83001f90fbbc24823773e56f65eeace261285ab7e1394efeb8348d2184c240c"), + expectedKey: testutil.DecodeHexString(c, "ba935d605f3f021c6cad26c8e2c0316fcc23814b2aa580e33e0ddb040692fb77"), }) } @@ -344,11 +475,11 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque Salt: []byte("0123456789abcdefghijklmnopqrstuv"), Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, Keylen: 64, }, - expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"), + expectedKey: testutil.DecodeHexString(c, "385251574d5dfa3c25eb5fa2ad99f74cba39c284a16999b2d8e6908ad2304225e1f706dc860867179759ca058c9e0b961f6a4ec88f0eb38ba825d655bf892116"), }) } @@ -405,7 +536,7 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque c.Check(res2.Duration > res.Duration, testutil.IsTrue) } -func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDeriveConsumedProcess(c *C) { +func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDeriveRestartProcess(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -413,7 +544,7 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque Keylen: 32, Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, }) c.Check(out, NotNil) @@ -425,22 +556,22 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque Keylen: 32, Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, 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", + ErrorType: Argon2OutOfProcessErrorRestartProcess, + ErrorString: "cannot run \"derive\" command: argon2 out-of-process handler has already been used to process a request - a new process should be started to handle another request", }) } -func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestTimeConsumedProcess(c *C) { +func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestTimeRestartProcess(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, }) c.Check(out, NotNil) @@ -449,13 +580,13 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessReque Command: Argon2OutOfProcessCommandTime, Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, 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", + ErrorType: Argon2OutOfProcessErrorRestartProcess, + ErrorString: "cannot run \"time\" command: argon2 out-of-process handler has already been used to process a request - a new process should be started to handle another request", }) } @@ -467,7 +598,7 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestWaitForAndRunOutOfProcessA rspR, rspW := io.Pipe() go func() { - c.Check(WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW), IsNil) + c.Check(WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW, nil), IsNil) wg.Done() }() @@ -478,7 +609,7 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestWaitForAndRunOutOfProcessA Salt: []byte("0123456789abcdefghijklmnopqrstuv"), Mode: Argon2id, Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4, Keylen: 32, }), IsNil) @@ -488,118 +619,150 @@ func (s *argon2OutOfProcessSupportSuiteExpensive) TestWaitForAndRunOutOfProcessA c.Check(dec.Decode(&rsp), IsNil) c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, - Key: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"), + Key: testutil.DecodeHexString(c, "b47ad96075d64cb92cdc7678e6bbb85f496da6e84d7ea05fbc0092dfb0ac3e13"), }) + c.Check(reqW.Close(), IsNil) wg.Wait() } type testOutOfProcessArgon2DeriveParams struct { - passphrase string - salt []byte - mode Argon2Mode - params *Argon2CostParams - keyLen uint32 - expectedKey []byte + watchdogMonitor Argon2OutOfProcessWatchdogMonitor + watchdogHandler string + 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 - }) + return exec.Command(s.runArgon2HandlerPath(), params.watchdogHandler), nil + }, params.watchdogMonitor) key, err := kdf.Derive(params.passphrase, params.salt, params.mode, params.params, params.keyLen) c.Check(err, IsNil) c.Check(key, DeepEquals, params.expectedKey) + c.Logf("%x", key) } func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2Derive(c *C) { s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, + watchdogMonitor: Argon2OutOfProcessWatchdogMonitorHMACSHA256(50*time.Millisecond, 50*time.Millisecond), + watchdogHandler: "hmac-sha256", + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, params: &Argon2CostParams{ Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4}, keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"), + expectedKey: testutil.DecodeHexString(c, "b47ad96075d64cb92cdc7678e6bbb85f496da6e84d7ea05fbc0092dfb0ac3e13"), }) } func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentPassphrase(c *C) { s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - passphrase: "bar", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, + watchdogMonitor: Argon2OutOfProcessWatchdogMonitorHMACSHA256(50*time.Millisecond, 50*time.Millisecond), + watchdogHandler: "hmac-sha256", + passphrase: "bar", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, params: &Argon2CostParams{ Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4}, keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"), + expectedKey: testutil.DecodeHexString(c, "e5081bdbb5dc709ecd789ad6da76ce6c49d2bc3b958dda4a93c6b4140def877e"), }) } func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentSalt(c *C) { s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - passphrase: "foo", - salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), - mode: Argon2id, + watchdogMonitor: Argon2OutOfProcessWatchdogMonitorHMACSHA256(50*time.Millisecond, 50*time.Millisecond), + watchdogHandler: "hmac-sha256", + passphrase: "foo", + salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), + mode: Argon2id, params: &Argon2CostParams{ Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4}, keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"), + expectedKey: testutil.DecodeHexString(c, "bd962af1e81debad7966d3c0ca1dd9398dc231a3c25c96de54a1df97233d1a49"), }) } func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentMode(c *C) { s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2i, + watchdogMonitor: Argon2OutOfProcessWatchdogMonitorHMACSHA256(50*time.Millisecond, 50*time.Millisecond), + watchdogHandler: "hmac-sha256", + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2i, params: &Argon2CostParams{ Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4}, keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"), + expectedKey: testutil.DecodeHexString(c, "0d781d62896d7bb71830251af01be0323f2006770beb917e62a2ea3330693625"), }) } func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentParams(c *C) { s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, + watchdogMonitor: Argon2OutOfProcessWatchdogMonitorHMACSHA256(50*time.Millisecond, 50*time.Millisecond), + watchdogHandler: "hmac-sha256", + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, params: &Argon2CostParams{ Time: 48, - MemoryKiB: 32 * 1024, + MemoryKiB: 64 * 1024, + Threads: 4}, + keyLen: 32, + expectedKey: testutil.DecodeHexString(c, "ba935d605f3f021c6cad26c8e2c0316fcc23814b2aa580e33e0ddb040692fb77"), + }) +} + +func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveLongDuration(c *C) { + s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ + watchdogMonitor: Argon2OutOfProcessWatchdogMonitorHMACSHA256(50*time.Millisecond, 50*time.Millisecond), + watchdogHandler: "hmac-sha256", + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, + params: &Argon2CostParams{ + Time: 100, + MemoryKiB: 512 * 1024, Threads: 4}, keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "f83001f90fbbc24823773e56f65eeace261285ab7e1394efeb8348d2184c240c"), + expectedKey: testutil.DecodeHexString(c, "f3dc0bc830b9530adc647b136765b4266a41a62d90b9ce6b7a784b91b1566ab5"), }) } -func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentKeyLen(c *C) { +func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentKeylen(c *C) { s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, + watchdogMonitor: Argon2OutOfProcessWatchdogMonitorHMACSHA256(50*time.Millisecond, 50*time.Millisecond), + watchdogHandler: "hmac-sha256", + passphrase: "foo", + salt: []byte("0123456789abcdefghijklmnopqrstuv"), + mode: Argon2id, params: &Argon2CostParams{ Time: 4, - MemoryKiB: 32, + MemoryKiB: 32 * 1024, Threads: 4}, keyLen: 64, - expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"), + expectedKey: testutil.DecodeHexString(c, "385251574d5dfa3c25eb5fa2ad99f74cba39c284a16999b2d8e6908ad2304225e1f706dc860867179759ca058c9e0b961f6a4ec88f0eb38ba825d655bf892116"), }) } func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2Time(c *C) { kdf := NewOutOfProcessArgon2KDF(func() (*exec.Cmd, error) { - return exec.Command(s.runArgon2HandlerPath()), nil - }) + return exec.Command(s.runArgon2HandlerPath(), "hmac-sha256"), nil + }, Argon2OutOfProcessWatchdogMonitorHMACSHA256(50*time.Millisecond, 50*time.Millisecond)) time1, err := kdf.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4}) c.Check(err, IsNil) diff --git a/cmd/run_argon2/main.go b/cmd/run_argon2/main.go index 2b5a7a81..91ce9e2a 100644 --- a/cmd/run_argon2/main.go +++ b/cmd/run_argon2/main.go @@ -27,13 +27,20 @@ import ( ) func run() error { - if len(os.Args) != 1 { - return errors.New("usage: echo | run_argon2") + if len(os.Args) != 2 { + return errors.New("usage: echo | run_argon2 ") } - secboot.SetIsArgon2HandlerProcess() - err := secboot.WaitForAndRunArgon2OutOfProcessRequest(os.Stdin, os.Stdout) + var watchdog secboot.Argon2OutOfProcessWatchdogHandler + switch os.Args[1] { + case "none": + watchdog = secboot.NoArgon2OutOfProcessWatchdogHandler + case "hmac-sha256": + watchdog = secboot.Argon2OutOfProcessWatchdogHandlerHMACSHA256() + } + + err := secboot.WaitForAndRunArgon2OutOfProcessRequest(os.Stdin, os.Stdout, watchdog) if err != nil { return fmt.Errorf("cannot run request: %w", err) } From fe291c53181b47817b6daf06775940a13d7fd86b Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Wed, 27 Nov 2024 01:33:43 +0000 Subject: [PATCH 05/40] Argon2 remoting cleanups --- argon2.go | 66 +- argon2_out_of_process_support.go | 631 +++++------ argon2_out_of_process_support_sync.go | 247 +++++ argon2_out_of_process_support_sync_test.go | 150 +++ argon2_out_of_process_support_test.go | 1117 ++++++++++++++++---- argon2_out_of_process_support_test.go.bak | 785 -------------- argon2_test.go | 6 - cmd/run_argon2/main.go | 16 +- export_test.go | 42 +- internal/paths/paths.go | 11 +- 10 files changed, 1748 insertions(+), 1323 deletions(-) create mode 100644 argon2_out_of_process_support_sync.go create mode 100644 argon2_out_of_process_support_sync_test.go delete mode 100644 argon2_out_of_process_support_test.go.bak diff --git a/argon2.go b/argon2.go index eea3505b..e2610ebe 100644 --- a/argon2.go +++ b/argon2.go @@ -25,7 +25,6 @@ import ( "math" "runtime" "sync" - "sync/atomic" "time" "golang.org/x/xerrors" @@ -34,8 +33,8 @@ import ( ) var ( - argon2Mu sync.Mutex - argon2Impl Argon2KDF = nullArgon2KDFImpl{} + argon2Mu sync.Mutex // Protects access to argon2Impl + argon2Impl Argon2KDF = nullArgon2KDFImpl{} // The Argon2KDF implementation used by functions in this package runtimeNumCPU = runtime.NumCPU ) @@ -47,17 +46,14 @@ var ( // // Passing nil will configure the null implementation as well. // -// This returns the currently set implementation. +// This function returns the previously configured Argon2KDF instance. // -// 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. +// This exists to facilitate running Argon2 operations in short-lived helper +// processes (see [InProcessArgon2KDF]), because Argon2 doesn't interact very +// well with Go's garbage collector, and is an algorithm that is only really +// suited to languages / runtimes with explicit memory allocation and +// de-allocation primitves. 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() @@ -71,17 +67,16 @@ func SetArgon2KDF(kdf Argon2KDF) Argon2KDF { } // argon2KDF returns the global [Argon2KDF] implementation set for this process. This -// can be set via calls to [SetArgon2KDF] in parent processes, or calls to -// [SetIsArgon2HandlerProcess] from remote, short-lieved utility processes that handle -// Argon2 requests on behalf of a longer lived process in order to avoid the problems -// associated with garbage collection. +// can be set via calls to [SetArgon2KDF]. func argon2KDF() Argon2KDF { argon2Mu.Lock() defer argon2Mu.Unlock() return argon2Impl } -// Argon2Mode describes the Argon2 mode to use. +// Argon2Mode describes the Argon2 mode to use. Note that the +// fully data-dependent mode is not supported because the underlying +// argon2 implementation lacks support for it. type Argon2Mode = argon2.Mode const ( @@ -135,6 +130,7 @@ func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) { mode := o.Mode if mode == Argon2Default { + // mode = Argon2id } @@ -142,7 +138,7 @@ func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) { case o.ForceIterations > 0: // The non-benchmarked path. Ensure that ForceIterations // and MemoryKiB fit into an int32 so that it always fits - // into an int + // into an int, because the retuned kdfParams uses ints. switch { case o.ForceIterations > math.MaxInt32: return nil, fmt.Errorf("invalid iterations count %d", o.ForceIterations) @@ -173,12 +169,15 @@ func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) { return params, nil default: + // The benchmarked path, where we determing what cost paramters to + // use in order to obtain the desired execution time. benchmarkParams := &argon2.BenchmarkParams{ MaxMemoryCostKiB: 1 * 1024 * 1024, // the default maximum memory cost is 1GiB. TargetDuration: 2 * time.Second, // the default target duration is 2s. } if o.MemoryKiB != 0 { + // The memory cost has been spcified expli benchmarkParams.MaxMemoryCostKiB = o.MemoryKiB // this is capped to 4GiB by internal/argon2. } if o.TargetDuration != 0 { @@ -188,6 +187,7 @@ func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) { benchmarkParams.Threads = o.Parallel // this is capped to 4 by internal/argon2. } + // Run the benchmark, which relies on the global Argon2KDF implementation. params, err := argon2.Benchmark(benchmarkParams, func(params *argon2.CostParams) (time.Duration, error) { return argon2KDF().Time(mode, &Argon2CostParams{ Time: params.Time, @@ -234,7 +234,8 @@ 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 handler process where required. See -// [SetArgon2KDF]. +// [SetArgon2KDF] and [InProcessArgon2KDF]. Implementations should be thread-safe +// (ie, they should be able to handle calls from different goroutines). 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. @@ -270,21 +271,20 @@ func (_ inProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) // 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. +// kernel's OOM killer. Whilst implementations can call [runtime.GC] between calls, +// go's sweep implementation stops the world, which makes interaction with goroutines +// and the scheduler poor, and will likely result in noticeable periods of +// unresponsiveness. Rather than using this directly, it's better to pass requests to +// a short-lived helper process where this can be used, and let the kernel deal with +// reclaiming memory when the short-lived process exits instead. // -// This package provides an example of this already ([NewOutOfProcessArgon2KDF]), as well -// as a handler for use in the short-lived handler process -// ([WaitForAndRunArgon2OutOfProcessRequest]). In order to save space, it is possible that -// the functionality be duplicated in the same executable (ie, using the same executable such -// as snapd or snap-bootstrap to provide the parent [Argon2KDF] and the remote, short-lived -// KDF helper). +// This package provides APIs to support this architecture already - +// [NewOutOfProcessArgon2KDF] for the parent side, and [WaitForAndRunArgon2OutOfProcessRequest] +// for the remote side, which runs in a short-lived process. In order to save storage +// space that would be consumed by another go binary, it is reasonable that the parent +// side (the one that calls [SetArgon2KDF]) and the remote side (which calls +// [WaitForAndRunArgon2OutOfProcessRequest]) could live in the same executable that +// is invoked with different arguments depending on which function is required. var InProcessArgon2KDF = inProcessArgon2KDFImpl{} type nullArgon2KDFImpl struct{} diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index eb0233f4..76681f52 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -28,6 +28,7 @@ import ( "errors" "fmt" "io" + "os" "os/exec" "sync" "sync/atomic" @@ -48,8 +49,8 @@ const ( Argon2OutOfProcessCommandTime Argon2OutOfProcessCommand = "time" // Argon2OutOfProcessCommandWatchdog requests a watchdog ping, when using - // [WaitForAndRunArgon2OutOfProcessRequest]. This does not work with - // [RunArgon2OutOfProcessRequest], which runs the supplied request synchronously + // WaitForAndRunArgon2OutOfProcessRequest. This does not work with + // RunArgon2OutOfProcessRequest, which runs the supplied request synchronously // in the current go routine. Argon2OutOfProcessCommandWatchdog Argon2OutOfProcessCommand = "watchdog" ) @@ -58,6 +59,7 @@ const ( // a remote process. type Argon2OutOfProcessRequest struct { Command Argon2OutOfProcessCommand `json:"command"` // The command to run + Timeout time.Duration `json:"timeout"` // The maximum amount of time to wait for the request to start before aborting it 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 @@ -89,12 +91,20 @@ const ( // of inputs associated with the supplied request. Argon2OutOfProcessErrorUnexpectedInput Argon2OutOfProcessErrorType = "unexpected-input" - // Argon2OutOfProcessErrorRestartProcess means that this process has already performed one - // execution of the KDF, and the process should exit and be replaced by a new one. + // Argon2OutOfProcessErrorTimeout means that the specified command timeout expired before + // the request was given a chance to start. + Argon2OutOfProcessErrorKDFTimeout Argon2OutOfProcessErrorType = "timeout-error" + + // Argon2OutOfProcessErrorRestartProcess means that this process has already processed a + // good KDF request, and the process should exit and be replaced by a new one. Argon2OutOfProcessErrorRestartProcess Argon2OutOfProcessErrorType = "restart-process" - // Argon2OutOfProcessErrorUnexpected means that an unexpected error occurred when - // running the operation. + // Argon2OutOfProcessErrorKDFUnexpected means that an unexpected error occurred when + // running the actual KDF operation. + Argon2OutOfProcessErrorKDFUnexpected Argon2OutOfProcessErrorType = "unexpected-kdf-error" + + // Argon2OutOfProcessErrorUnexpected means that an unexpected error occurred without + // a more specific error type. Argon2OutOfProcessErrorUnexpected Argon2OutOfProcessErrorType = "unexpected-error" ) @@ -111,7 +121,7 @@ type Argon2OutOfProcessResponse struct { // 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 +// implementation created by [NewOutOfProcessArgon2KDF] when the received response indicates // that an error ocurred. type Argon2OutOfProcessError struct { ErrorType Argon2OutOfProcessErrorType @@ -120,17 +130,18 @@ type Argon2OutOfProcessError struct { // Error implements the error interface. func (e *Argon2OutOfProcessError) Error() string { - str := new(bytes.Buffer) - fmt.Fprintf(str, "cannot process KDF request: %v", e.ErrorType) + str := "cannot process request: " + string(e.ErrorType) if e.ErrorString != "" { - fmt.Fprintf(str, " (%s)", e.ErrorString) + str += " (" + str += e.ErrorString + str += ")" } - return str.String() + return str } -// 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]. +// Err returns an error associated with the response if one occurred (if the +// ErrorType field is not empty), 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 @@ -141,128 +152,74 @@ func (o *Argon2OutOfProcessResponse) Err() error { } } -const ( - argon2Unused uint32 = 0 - argon2Expired uint32 = 1 -) - -var errArgon2OutOfProcessHandlerExpired = errors.New("argon2 out-of-process handler has already been used to process a request - a new process should be started to handle another request") +// Argon2OutOfProcessWatchdogError is returned from [Argon2KDF] instances created by +// [NewOutOfProcessArgon2KDF] in the event of a watchdog failure. +type Argon2OutOfProcessWatchdogError struct { + err error +} -// 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 +// Error implements the error interface +func (e *Argon2OutOfProcessWatchdogError) Error() string { + return "watchdog failure: " + e.err.Error() } -// canHandleRequest returns whether this KDF can be used to handle a request. -// It will only ever return true once. If it returns false, the pending KDF -// request must be rejected. On the single occasion that it returns true -// true, then the pending KDF request can be handled, but subsequent calls to -// this function will always return false. -func (k *argon2OutOfProcessHandler) canHandleRequest() bool { - return atomic.CompareAndSwapUint32(&k.Status, argon2Unused, argon2Expired) +func (e *Argon2OutOfProcessWatchdogError) Unwrap() error { + return e.err } -func (k *argon2OutOfProcessHandler) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) ([]byte, error) { - if !k.canHandleRequest() { - return nil, errArgon2OutOfProcessHandlerExpired - } - return k.KDF.Derive(passphrase, salt, mode, params, keyLen) +// Argon2OutOfProcessResponseCommandInvalidError is returned from [Argon2KDF] instances +// created by [NewOutOfProcessArgon2KDF] if the response contains an unexpected command +// field value. +type Argon2OutOfProcessResponseCommandInvalidError struct { + Response Argon2OutOfProcessCommand + Expected Argon2OutOfProcessCommand } -func (k *argon2OutOfProcessHandler) Time(mode Argon2Mode, params *Argon2CostParams) (time.Duration, error) { - if !k.canHandleRequest() { - return 0, errArgon2OutOfProcessHandlerExpired - } - return k.KDF.Time(mode, params) +// Error implements the error interface +func (e *Argon2OutOfProcessResponseCommandInvalidError) Error() string { + return fmt.Sprintf("received a response with an unexpected command value (got %q, expected %q)", e.Response, e.Expected) } const ( - notArgon2HandlerProcess uint32 = 0 - becomingArgon2HandlerProcess uint32 = 1 - readyArgon2HandlerProcess uint32 = 2 + inProcessArgon2KDFAvailable uint32 = 0 + inProcessArgon2KDFUsed uint32 = 1 ) var ( - argon2OutOfProcessStatus uint32 = notArgon2HandlerProcess + // argon2OutOfProcessHandlerStatus indicates whether this process has handled + // a KDF request on behalf of another process. Process's should only handle a + // single request, and then reject further requests. + argon2OutOfProcessHandlerStatus uint32 = inProcessArgon2KDFAvailable + + // ErrArgon2OutOfProcessHandlerUnavailable is returned directly from + // WaitForAndRunArgon2OutOfProcessRequest if this process is not available + // to handle anymore KDF requests. It can also be returned as the error + // string in a Argon2OutOfProcessResponse + ErrArgon2OutOfProcessHandlerUnavailable = errors.New("this process cannot handle any more KDF requests") ) -// 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], which run Argon2 requests in-process. Argon2 -// remoting support (the ability for a consumer of Argon2 to delegate calls to a short-lived utility -// process) is required in go and other garbage collected languages because they intentionally -// allocate significant amounts of memory. Without performing a full GC mark-sweep inbetween -// each call, repeated calls will rapidly trigger the kernel's OOM killer. Go's sweep implementation -// stops the world, which makes interaction with goroutines and the scheduler poor. It's better to -// just let the kernel deal with this instead by using short-lived process's as opposed to calling -// [runtime.GC] in processes's that we want to remain responsive. -func SetIsArgon2HandlerProcess() { - // Mark process as becoming an argon2 handler process. This will ensure that new calls - // to both this function and SetArgon2KDF will 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. The - // implementation we set is a version of InProcessArgon2KDF that will only run once - // before returning an error. - 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. +// function will only handle one argon2 request in a process. Subsequent calls in the same process +// after a previous successful call will result in an error response being returned. // // This is quite a low-level function, suitable for implementations that want to manage their own // transport. In general, implementations will use [WaitForAndRunArgon2OutOfProcessRequest]. // // This function does not service watchdog requests, as the KDF request happens synchronously in the -// current go routine. If this is required, it needs to be implemented in supporting code that makes -// use of other go routines, noting that the watchdog handler shoould test that the input request -// processing continues to function. [WaitForAndRunArgon2OutOfProcessRequest] already does this correctly, -// and most implementations should just use this. +// current goroutine. If this is required, it needs to be implemented in supporting code that makes +// use of other go routines, noting that the watchdog handler should test that the input request and +// output response processing continues to function. [WaitForAndRunArgon2OutOfProcessRequest] already +// does this correctly, and most implementations should just use this. // -// Unfortunately, there is no way to interrupt this function once it has been called. because the -// low-level crypto library does not support this. This feature may be desired in the future, which might -// require replacing the existing library we use for Argon2. +// Unfortunately, there is no way to interrupt this function once the key derivation is in progress, +// because the low-level crypto library does not support this. This feature may be desired in the +// future, which might require replacing the existing library we use for Argon2. func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2OutOfProcessResponse { - if atomic.LoadUint32(&argon2OutOfProcessStatus) < readyArgon2HandlerProcess { - // SetIsArgon2HandlerProcess hasn't been called, or hasn't completed yet. + if !atomic.CompareAndSwapUint32(&argon2OutOfProcessHandlerStatus, inProcessArgon2KDFAvailable, inProcessArgon2KDFUsed) { return &Argon2OutOfProcessResponse{ Command: request.Command, - ErrorType: Argon2OutOfProcessErrorUnexpected, - ErrorString: "cannot handle request in a process that isn't configured as an Argon2 handler process", + ErrorType: Argon2OutOfProcessErrorRestartProcess, + ErrorString: ErrArgon2OutOfProcessHandlerUnavailable.Error(), } } @@ -274,7 +231,7 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out return &Argon2OutOfProcessResponse{ Command: request.Command, ErrorType: Argon2OutOfProcessErrorInvalidMode, - ErrorString: fmt.Sprintf("invalid mode: %q", string(request.Mode)), + ErrorString: fmt.Sprintf("mode cannot be %q", string(request.Mode)), } } @@ -287,14 +244,14 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out return &Argon2OutOfProcessResponse{ Command: request.Command, ErrorType: Argon2OutOfProcessErrorInvalidTimeCost, - ErrorString: "invalid time cost: cannot be zero", + ErrorString: "time cannot be zero", } } if costParams.Threads == 0 { return &Argon2OutOfProcessResponse{ Command: request.Command, ErrorType: Argon2OutOfProcessErrorInvalidThreads, - ErrorString: "invalid threads: cannot be zero", + ErrorString: "threads cannot be zero", } } @@ -309,19 +266,28 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out } } + release, err := acquireArgon2OutOfProcessHandlerSystemLock(request.Timeout) + if err != nil { + errorType := Argon2OutOfProcessErrorUnexpected + if errors.Is(err, errArgon2OutOfProcessHandlerSystemLockTimeout) { + errorType = Argon2OutOfProcessErrorKDFTimeout + } + return &Argon2OutOfProcessResponse{ + Command: request.Command, + ErrorType: errorType, + ErrorString: fmt.Sprintf("cannot acquire argon2 system lock: %v", err), + } + } + defer release() + switch request.Command { case Argon2OutOfProcessCommandDerive: // Perform key derivation - key, err := argon2KDF().Derive(request.Passphrase, request.Salt, request.Mode, costParams, request.Keylen) + key, err := InProcessArgon2KDF.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 = Argon2OutOfProcessErrorRestartProcess - } return &Argon2OutOfProcessResponse{ Command: request.Command, - ErrorType: errorType, + ErrorType: Argon2OutOfProcessErrorKDFUnexpected, ErrorString: fmt.Sprintf("cannot run derive command: %v", err), } } @@ -354,16 +320,11 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out } // Perform timing of the supplied cost parameters. - duration, err := argon2KDF().Time(request.Mode, costParams) + duration, err := InProcessArgon2KDF.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 = Argon2OutOfProcessErrorRestartProcess - } return &Argon2OutOfProcessResponse{ Command: request.Command, - ErrorType: errorType, + ErrorType: Argon2OutOfProcessErrorKDFUnexpected, ErrorString: fmt.Sprintf("cannot run time command: %v", err), } } @@ -377,21 +338,21 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out return &Argon2OutOfProcessResponse{ Command: request.Command, ErrorType: Argon2OutOfProcessErrorInvalidCommand, - ErrorString: fmt.Sprintf("invalid command: %q", string(request.Command)), + ErrorString: fmt.Sprintf("command cannot be %q", string(request.Command)), } } } // Argon2OutOfProcessWatchdogHandler defines the behaviour of a watchdog handler -// for the remote side of an out-of-process [Argon2KDF] implementation created by +// for the remote side of an out-of-process [Argon2KDF] implementation, using // [WaitForAndRunArgon2OutOfProcessRequest]. // -// If is expected to be called periodically on the same go routine that processes -// incoming requests to ensure that this routine is functioning correctly. The response -// should make use of the same code path that the eventual KDF response will be sent -// using so that the watchdog handler tests all of the code associated with this and so -// the parent process can be assured that it will eventually receive a KDF response -// and won't be left waiting indefinitely. +// If is called periodically on the same go routine that processes incoming requests +// to ensure that this routine is functioning correctly. The response makes use of the +// same code path that the eventual KDF response will be sent via, so that the watchdog +// handler tests all of the code associated with this and so the parent process can be +// assured that it will eventually receive a KDF response and won't be left waiting +// indefinitely for one. // // Implementations define their own protocol, with limitations. All requests and // responses use the watchdog command [Argon2OutOfProcessCommandWatchdog]. The @@ -424,7 +385,8 @@ func HMACArgon2OutOfProcessWatchdogHandler(alg crypto.Hash) Argon2OutOfProcessWa h := hmac.New(alg.New, lastResponse) h.Write(challenge) - return h.Sum(nil), nil + lastResponse = h.Sum(nil) + return lastResponse, nil } } @@ -433,61 +395,61 @@ func HMACArgon2OutOfProcessWatchdogHandler(alg crypto.Hash) Argon2OutOfProcessWa // parent side. This implementation will return an error if a watchdog request is received. func NoArgon2OutOfProcessWatchdogHandler() Argon2OutOfProcessWatchdogHandler { return func(_ []byte) ([]byte, error) { - return nil, errors.New("unexpected request: no handler for watchdog") + return nil, errors.New("unexpected watchdog request: no handler") } } -// ErrKDFNotRequested is returned from [WaitForAndRunArgon2OutOfProcessRequest] -// if the supplied io.Reader is closed before a [Argon2OutOfProcessRequest] has been received. -var ErrKDFNotRequested = errors.New("no KDF request was received") - // 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. +// os.Stdout - at least they will need to be when using [NewOutOfProcessArgon2KDF] 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 will only handle one argon2 request from the supplied io.Reader in a process. Subsequent +// requests to the same process after a previous successful call will result in an error response being +// returned via the io.Writer. // // This function will service watchdog requests from the parent process if a watchdog handler is supplied. // If supplied, it must match the corresponding monitor in the parent process. If not supplied, the default // [NoArgon2OutOfProcessWatchdogHandler] will be used. // // Unfortunately, KDF requests cannot be interrupted once they have started because the low-level crypto -// library does not provide this functionality, although watchdog requests can still be serviced. The ability -// to interrupt a KDF request in the future may be desired, although it may require replacing the existing +// library does not provide this functionality, although watchdog requests can still be serviced to provide +// assurance that a response will be received as long as the crypto algorithm completes. The ability to +// interrupt a KDF request in the future may be desired, although it may require replacing the existing // library we use for Argon2. // -// This function requires [SetIsArgon2HandlerProcess] to have already been called in this process, -// else an error response will be returned via the io.Writer. -// // Most errors are sent back to the parent process via the supplied io.Writer. In some cases, errors // returned from go routines that are created during the handling of a request may be returned directly -// from this function. +// from this function to be handled by the current process. // // Note that this function won't return until the supplied io.Reader is closed by the parent. func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdog Argon2OutOfProcessWatchdogHandler) error { + if atomic.LoadUint32(&argon2OutOfProcessHandlerStatus) == inProcessArgon2KDFUsed { + return ErrArgon2OutOfProcessHandlerUnavailable + } + if watchdog == nil { watchdog = NoArgon2OutOfProcessWatchdogHandler() } tmb := new(tomb.Tomb) - // rspChan is the channel from the routine that runs the KDF to the dedicated output routine - // which serializes the response to the supplied io.Writer. + // rspChan is the channel from the routines that process requests and run the KDF to the + // dedicated output routine which serializes the response to the supplied io.Writer. rspChan := make(chan *Argon2OutOfProcessResponse) + // Spin up a routine for receiving requests from the supplied io.Reader. tmb.Go(func() error { // Also spin-up the routine for sending outgoing responses that are generated internally. - // This handles the read end of rspChan. This serializes them to the supplied io.Writer. - // This gets its own goroutine so that all responses are sent via the same code path - // - responses can ultimately come directly from the request processing loop (in the event + // This handles the read end of rspChan, and serializes responses to the supplied io.Writer. + // This gets its own goroutine so that all responses are sent via the same code path - responses + // can ultimately come directly from the request processing loop in this routine (in the event // of a watchdog request), or from a dedicated KDF routine which permits the request processing - // loop to continue executing whilst the KDF is running - something which results in the blocking - // of the current goroutine. + // loop in this routine to continue executing whilst the KDF is running, so we can continue to + // process watchdog requests. tmb.Go(func() error { - for { + // Loop whilst the tomb is alive. + for tmb.Alive() { // Wait for a response from somewhere or wait for the tomb to // begin dying. select { @@ -501,21 +463,14 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo return fmt.Errorf("cannot encode response: %w", err) } case <-tmb.Dying(): - // If the tomb begins dying, end this routine - this is part of - // the normal shutdown. - return nil + // We've begun to die, and this loop will not run again. } } - return nil + return tomb.ErrDying }) - // kdfRequestReceived indicates that a KDF request was received. If one has been received, - // it's not an error for the parent to close its side of the incoming channel. We consider it - // an error for the parent to close its side of the incoming channel before sending a KDF request. - kdfRequestReceived := false - - // Run a loop for receiving incoming requests from the io.Reader as long - // as the tomb remains alive. + // Run a loop for receiving and processing incoming requests from the io.Reader as + // long as the tomb remains alive. for tmb.Alive() { // Wait for a request from the io.Reader. The only way to unblock this is // if the parent sends something or closes its end of the OS pipe. If it's @@ -526,29 +481,29 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo dec := json.NewDecoder(in) dec.DisallowUnknownFields() if err := dec.Decode(&req); err != nil { + // Decoding returned an error. if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { - // The parent has closed stdin without sending us a full request. - if !kdfRequestReceived { - // The parent has closed their end of the connection before - // sending a request, so we return an error here to begin - // the shutdown of the entire tomb and return an appropriate - // error to the caller. - return ErrKDFNotRequested - } - // In any case, if the parent has closed their end of the pipe, there's - // nothing else for us to process so we can break from this loop in order - // to shutdown of this routine. We can't receive any more watchdog requests. - // We have to kill the tomb because else the routine that waits for and - // encodes responses will continue running forever. We don't specify a reason - // here because it's considered a normal shutdown. + // The parent has closed their end of the io.Reader, so kill + // the tomb normally to begin the process of dying. tmb.Kill(nil) - break + break // Break out of the request processing loop + } + + // We failed to decode an incoming request for an unknown reason. We try returning an + // error response to the parent - note that we can't set the command field here + // because we have no idea what it is. + rsp := &Argon2OutOfProcessResponse{ + ErrorType: Argon2OutOfProcessErrorUnexpectedInput, + ErrorString: fmt.Sprintf("cannot decode request: %v", err), + } + select { + case rspChan <- rsp: // Unbuffered channel, but read end is always there unless the tomb is dying. + case <-tmb.Dying(): + // The tomb began dying before the response was sent. } - // We failed to decode an incoming request. Returning the error here will begin the - // shutdown of the tomb, the eventual termination of any goroutines and the eventual - // return of this error to the caller. - return fmt.Errorf("cannot decode request: %w", err) + // Break out of the request processing loop. + break } // We have a request! @@ -558,47 +513,75 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo // Special case to handle watchdog requests. wdRsp, err := watchdog(req.WatchdogChallenge) if err != nil { + // As is documented for Argon2OutOfProcessWatchdogHandler, we don't + // expect the handler to return an error, so begin the shutdown of + // the tomb so that this function eventually returns with an error. return fmt.Errorf("cannot handle watchdog request: %w", err) } // Generate the response structure to send to the same goroutine that // will eventually encode and send the KDF response back to the parent. - rspChan <- &Argon2OutOfProcessResponse{ + rsp := &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandWatchdog, WatchdogResponse: wdRsp, } + select { + case rspChan <- rsp: // Unbuffered channel, but read end is always there unless the tomb is dying. + case <-tmb.Dying(): + // The tomb began dying before the response was sent, so + // the outer loop won't run again. + } default: - // Anything else is considered a KDF request - kdfRequestReceived = true + // Treat everything else as a KDF request. We don't actually check the + // command here - RunArgon2OutOfProcessRequest does this already and will + // return an error response if the command is invalid. // Spin up a new routine to handle the request, as it blocks and is long running, - // and we still want to be able to service watchdog requests whilst it's running. - // Block the current routine until we know the new routine has started so that the - // watchdog handler will fail if the new routine doesn't begin properly. + // and we still want to be able to service watchdog requests on this routine whilst + // it's running. Block the current routine until we know the new routine has started + // so that the watchdog handler will fail if the new routine doesn't begin properly. var startupWg sync.WaitGroup - startupWg.Add(1) + startupWg.Add(1) // Mark the WaitGroup as waiting for a single event. + tmb.Go(func() error { startupWg.Done() // Unblock the main routine. - // Run the KDF request, and send the response structure to the routine that - // is serializing these to the supplied io.Writer. - rspChan <- RunArgon2OutOfProcessRequest(req) + // Run the KDF request. This performs a lot of checking of the supplied + // request, so there's no need to repeat any of that here. + rsp := RunArgon2OutOfProcessRequest(req) + + // Send the response. + select { + case rspChan <- rsp: // Unbuffered channel, but read end is always there unless the tomb is dying. + case <-tmb.Dying(): + // The tomb began dying before the response was sent, + // so exit early. + return tomb.ErrDying + } - // As we only handle a single request, mark the tomb as dying to begin its - // clean shutdown if it isn't shutting down already. + if rsp.Err() != nil { + // We got an error response, which means that the KDF request was + // not handled. The error response has already been sent to the parent, + // so carry on processing requests by returning no error from this + // goroutine. + return nil + } + + // As we only handle a single successful request, mark the tomb as dying to + // begin its clean shutdown. tmb.Kill(nil) - return nil + return tomb.ErrDying }) - // Wait until the routine we spun up to run the KDF request is running before processing - // watchdog requests. If we end up blocked here then the watchdog handler will fail - // to respond. + // Wait here for the KDF handler routine to startup. This should never fail to start-up, + // but doing this blocks the processing of watchdog requests temporarily. startupWg.Wait() } } - return nil + return tomb.ErrDying }) + // Wait here for the tomb to die and return the first error that occurred. return tmb.Wait() } @@ -606,8 +589,8 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo // for out-of-process [Argon2KDF] implementations created by [NewOutOfProcessArgon2KDF], // and is managed on the parent side of an implementation of [Argon2KDF]. // -// It is expected to be called in its own dedicated go routine that is tracked -// by the supplied tomb. +// It will be called in its own dedicated go routine that is tracked by the supplied +// tomb. // // Implementations define their own protocol, with limitations. All requests and // responses use the watchdog command [Argon2OutOfProcessCommandWatchdog]. The @@ -618,18 +601,22 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo // If the watchdog isn't serviced by the remote process correctly or within some // time limit, the implementation is expected to return an error. // -// The [Argon2KDF] implementation that manages this watchdog should kill the remote -// process in the event that the monitor implementation returns an error. It should also -// ensure the termination of the parent tomb and the eventual return of an error to the -// caller. +// The [Argon2KDF] implementation created by [NewOutOfProcessArgon2KDF] will terminate the +// remote process in the event that the monitor implementation returns an error. It also +// kills the supllied tomb, resuling in the eventual return of an error to the caller. // -// The implementation of this should not close reqChan. +// The implementation of this should not close reqChan. The [Argon2KDF] implementation +// created by [NewOutOfProcessArgon2KDF] will not close rspChan. // -// It is expected that the [Argon2KDF] implementation that manages this watchdog -// only sends watchdog requests via the rspChan channel (ie, it's verified that the -// Command field in the [Argon2OutOfProcessResponse] is [Argon2OutOfProcessCommandWatchdog]) +// The [Argon2KDF] implementation created by [NewOutOfProcessArgon2KDF] will only send +// watchdog requests via rspChan. // -// It is expected that the [Argon2KDF] implementation doesn't close the suppled rspChan. +// The supplied reqChan is unbuffered, but the [Argon2KDF] implementation created by +// [NewOutOfProcessArgon2KDF] guarantees there is a reader until the tomb enters a +// dying state. +// +// The supplied rspChan is unbuffered. The monitor implementation should guarantee that +// there is a reader as long as the supplied tomb is alive. type Argon2OutOfProcessWatchdogMonitor = func(tmb *tomb.Tomb, reqChan chan<- *Argon2OutOfProcessRequest, rspChan <-chan *Argon2OutOfProcessResponse) error // HMACArgon2OutOfProcessWatchdogMonitor returns a watchdog monitor that generates a @@ -646,17 +633,16 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period, timeout time } return func(tmb *tomb.Tomb, reqChan chan<- *Argon2OutOfProcessRequest, rspChan <-chan *Argon2OutOfProcessResponse) error { - lastWatchdogResponse := make([]byte, 32) // the last response received from the child. - expectedWatchdogResponse := make([]byte, 32) // the next expected response. + lastWatchdogResponse := make([]byte, 32) // the last response received from the child. - // Only run the watchdog whilst the tomb is alive + // Run the watchdog whilst the tomb is alive. for tmb.Alive() { // Run it every defined period select { - case <-time.After(period): + case <-time.NewTimer(period).C: case <-tmb.Dying(): // Handle the tomb dying before the end of the period. - return nil + return tomb.ErrDying } // Generate a new 32-byte challenge and calculate the expected response @@ -664,11 +650,12 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period, timeout time if _, err := rand.Read(challenge); err != nil { return fmt.Errorf("cannot generate new watchdog challenge: %w", err) } + // The expected response is the HMAC of the challenge, keyed with the // last response. h := hmac.New(alg.New, lastWatchdogResponse) h.Write(challenge) - expectedWatchdogResponse = h.Sum(nil) + expectedWatchdogResponse := h.Sum(nil) req := &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandWatchdog, @@ -677,22 +664,21 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period, timeout time // Send the request. select { - case reqChan <- req: - // Request sent ok + case reqChan <- req: // Unbuffered channel, but read end is always there unless the tomb is dying. case <-tmb.Dying(): // The tomb began dying before we finished sending the request (reqChan is blocking). - return nil + return tomb.ErrDying } // Wait for the response from the remote process. select { - case <-time.Tick(timeout): // Give it up to the time defined by the timeout + case <-time.NewTimer(timeout).C: // Give it up to the time defined by the timeout return errors.New("timeout waiting for watchdog response from remote process") case rsp := <-rspChan: // We got a response from the remote process. if err := rsp.Err(); err != nil { // We got an error response, so just return the error. - return fmt.Errorf("cannot process watchdog response from remote process: %w", rsp.Err()) + return rsp.Err() } if !bytes.Equal(rsp.WatchdogResponse, expectedWatchdogResponse) { // We got an unexpected response, so return an error. @@ -701,13 +687,10 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period, timeout time // The response was good so save the value for the next iteration. lastWatchdogResponse = rsp.WatchdogResponse case <-tmb.Dying(): - // Don't need to wait any more as the tomb has begun dying. - return nil + // The loop won't run again } - return nil } - - return nil + return tomb.ErrDying } } @@ -718,17 +701,16 @@ func NoArgon2OutOfProcessWatchdogMonitor() Argon2OutOfProcessWatchdogMonitor { return func(tmb *tomb.Tomb, reqChan chan<- *Argon2OutOfProcessRequest, rspChan <-chan *Argon2OutOfProcessResponse) error { select { case <-tmb.Dying(): - return nil + return tomb.ErrDying case <-rspChan: return errors.New("unexpected watchdog response") } } } -// outOfProcessArgon2KDFImpl is an Argon2KDF implementation that runs the KDF in a short-lived -// remote process, using the remote JSON protocol defined in this package. type outOfProcessArgon2KDFImpl struct { newHandlerCmd func() (*exec.Cmd, error) + timeout time.Duration watchdog Argon2OutOfProcessWatchdogMonitor } @@ -761,76 +743,97 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out return nil, fmt.Errorf("cannot start handler process: %w", err) } - var actualRsp *Argon2OutOfProcessResponse - tmb := new(tomb.Tomb) + var actualRsp *Argon2OutOfProcessResponse // The response to return to the caller + exitWaitCh := make(chan struct{}) // A channel which signals successful exit of the child process when closed + tmb := new(tomb.Tomb) // To track all goroutines - // Spin up a routine to handle communications with the remote process. + // Spin up a routine to bootstrap the parent side and handle responses from + // the remote process. tmb.Go(func() error { // Spin up a routine for killing the child process if it doesn't // die cleanly tmb.Go(func() error { - <-tmb.Dying() + <-tmb.Dying() // Wait here until the tomb enters a dying state - select {} + select { + case <-exitWaitCh: + // The command closed cleanly, so there's nothing to do. + return tomb.ErrDying + case <-time.NewTimer(5 * time.Second).C: + // We've waited 5 seconds - kill the child process instead. Go 1.20 + // has a new feature (WaitDelay) which might make things a bit better + // here because I don't know how racey things are here - exec.Cmd is + // quite complicated. + if err := cmd.Process.Kill(); err != nil { + if err != os.ErrProcessDone { + return fmt.Errorf("failed to kill blocked remote process: %w", err) + } + } + return errors.New("killed blocked remote process") + } }) - // wdReqChan is sent requests from the watchdog monitor which are then - // received by another goroutine, which serializes them and sends them to - // the remote process via its stdin. - wdReqChan := make(chan *Argon2OutOfProcessRequest) - - // wdRspChan is sent watchdog responses received from the remote process - // via stdout, and they are subsequently received by the watchdog monitor - // for processing. + + // wdRspChan is sent watchdog responses on this goroutine, received from + // the remote process via stdout, and they are subsequently received by + // the watchdog monitor for processing. wdRspChan := make(chan *Argon2OutOfProcessResponse) - // reqChan is sent the main KDF request, which is received by another goroutine, - // which serializes it and sends it to the remote process via its stdin. + // reqChan is sent the initial request from this goroutine and watchdog requests + // from the watchdog routine, which are received by a dedicated goroutine to + // serialize then and sends them to the remote process via its stdin. reqChan := make(chan *Argon2OutOfProcessRequest) // Spin up a routine for sending requests to the remote process via stdinPipe. tmb.Go(func() error { - for { - var jsonReq *Argon2OutOfProcessRequest - - // Handle serializing and sending requests until we begin the process of - // dying. + // Run a loop to send requests as long as the tomb is alive. + for tmb.Alive() { select { case req := <-reqChan: - // We have the main KDF request to send. - jsonReq = req - case req := <-wdReqChan: - // We have a request from the watchdog monitor to send. - jsonReq = req + // Send the request to the remote process via its stdin + enc := json.NewEncoder(stdinPipe) + if err := enc.Encode(req); err != nil { + return fmt.Errorf("cannot encode request: %w", err) + } case <-tmb.Dying(): - // If the tomb begins dying, end this routine - this is a normal - // shutdown of this routine. - return nil - } - - // Send the request to the remote process via its stdin - enc := json.NewEncoder(stdinPipe) - if err := enc.Encode(jsonReq); err != nil { - return fmt.Errorf("cannot encode request: %w", err) + // The tomb is dying, so this loop will stop iterating. } - } - return nil + return tomb.ErrDying }) + // Send the main request before starting the watchdog or running the response loop + select { + case reqChan <- req: // Unbuffered channel, but read end is always there unless the tomb is dying. + case <-tmb.Dying(): + // The tomb has begun dying before we had a chance to send the initial request. + return tomb.ErrDying + } + // Spin up another routine to run the watchdog tmb.Go(func() error { - err := k.watchdog(tmb, wdReqChan, wdRspChan) - if err == nil && tmb.Alive() { - // The watchdog returning an error will terminate the tomb, but if - // it returns no error whilst the tomb is still alive, then consider - // this to be unexpected. In this case, begin the termination of the + err := k.watchdog(tmb, reqChan, wdRspChan) + switch { + case err == tomb.ErrDying: + // Return this error unmodified. + return err + case err != nil: + // Unexpected error. + return &Argon2OutOfProcessWatchdogError{err: err} + case tmb.Alive(): + // The watchdog returned no error whilst the tomb is still alive, + // which is unexpected. In this case, begin the termination of the // tomb. - return errors.New("watchdog monitor terminated unexpectedly") + return &Argon2OutOfProcessWatchdogError{err: errors.New("watchdog monitor terminated unexpectedly without an error")} + case err == nil: + // The tomb is in a dying state, and it's fine to return a nil error + // in this case. We'll return tomb.ErrDying for consistency though. + return tomb.ErrDying + default: + panic("not reached") } - return err }) - // Wait for responses from the remote process whilst the tomb is alive. + // Run a loop to wait for responses from the remote process whilst the tomb is alive. for tmb.Alive() { // Wait for a response from the io.Reader. The only way to unblock this is // if the remote process sends something or closes its end of the OS pipe. @@ -844,49 +847,80 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out return fmt.Errorf("cannot decode response: %w", err) } + if rsp.Err() != nil { + // If we receive an error response, begin the process of the tomb dying. + // Don't wrap the error - this will be returned directly to the caller. + return rsp.Err() + } + switch rsp.Command { case Argon2OutOfProcessCommandWatchdog: // Direct watchdog responses to wdRspChan so they can be picked up by // the watchdog handler. - wdRspChan <- rsp + select { + case wdRspChan <- rsp: // Unbuffered channel, but read end is always there unless the tomb is dying. + case <-tmb.Dying(): + // The loop will no longer iterate + } default: // For any other response, first of all make sure that the command value is // consistent with the sent command. if rsp.Command != req.Command { // Unexpected command. Return an appropriate error to begin the process // of the tomb dying - return fmt.Errorf("received a response with an unexpected command value (got %q, expected %q)", rsp.Command, req.Command) + return &Argon2OutOfProcessResponseCommandInvalidError{ + Response: rsp.Command, + Expected: req.Command, + } } // If it is consistent, save the response to return to the caller and begin a clean // shutdown of the tomb. actualRsp = rsp tmb.Kill(nil) + // This loop will no longer iterate } } - return nil + return tomb.ErrDying }) - // Wait for all go routines to finish. - if err := tmb.Wait(); err != nil { - // Don't wrap this error - this will be the first non-nil error passed - // to Tomb.Kill. There's no benefit to adding additional context here. - return nil, err - } - // Closing the stdin pipe is necessary for WaitForAndRunArgon2OutOfProcessRequest to finish - // on the remote side (so that the child process exits cleanly) + // Wait here until the tomb enters a dying state + <-tmb.Dying() + + // Closing the stdin pipe is necessary to unblock WaitForAndRunArgon2OutOfProcessRequest + // on the remote side, if it is blocked in a read. if err := stdinPipe.Close(); err != nil { return nil, fmt.Errorf("cannot close stdin pipe: %w", err) } + // The go documentation says this should be unnecessary because it will be closed + // by cmd.Wait once the command has exitted. But it's still possible for + // WaitForAndRunArgon2OutOfProcessRequest to be blocked on a write to us, which + // will never complete once that the loop that handles responses has terminated. + // Therefore, we're going to close it explicitly. + if err := stdoutPipe.Close(); err != nil { + return nil, fmt.Errorf("cannot close stdout pipe: %w", err) + } + // We can wait for the remote process to exit now. if err := cmd.Wait(); err != nil { return nil, fmt.Errorf("an error occurred whilst waiting for the remote process to finish: %w", err) } + // Stop the 5 second kill timer + close(exitWaitCh) + + // Wait for all go routines to finish. + if err := tmb.Wait(); err != nil { + // Don't wrap this error - this will be the first non-nil error passed + // to Tomb.Kill. There's no benefit to adding additional context here. + return nil, err + } + return actualRsp, nil } func (k *outOfProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode Argon2Mode, params *Argon2CostParams, keyLen uint32) (key []byte, err error) { req := &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, + Timeout: k.timeout, Passphrase: passphrase, Salt: salt, Keylen: keyLen, @@ -908,6 +942,7 @@ func (k *outOfProcessArgon2KDFImpl) Derive(passphrase string, salt []byte, mode func (k *outOfProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams) (duration time.Duration, err error) { req := &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, + Timeout: k.timeout, Mode: mode, Time: params.Time, MemoryKiB: params.MemoryKiB, @@ -933,6 +968,10 @@ func (k *outOfProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostPara // the request to the process via its stdin and receiving the response from the process // via its stdout. // +// KDF requests are serialized system-wide so that only 1 runs at a time. The supplied +// timeout specifies the maximum amount of time a request will wait to be started before +// giving up with an error. +// // The optional watchdog field makes it possible to send periodic pings to the remote // process to ensure that it is still alive and still processing IPC requests, given that // the KDF may be asked to run for a long time, and the KDF itself is not interruptible. @@ -941,8 +980,11 @@ func (k *outOfProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostPara // supplied, [NoArgon2OutOfProcessWatchdogMonitor] is used, which provides no watchdog monitor // functionality. // -// The watchdog functionality is more suitable for KDF uses that are more than 1 second long. -func NewOutOfProcessArgon2KDF(newHandlerCmd func() (*exec.Cmd, error), watchdog Argon2OutOfProcessWatchdogMonitor) Argon2KDF { +// The watchdog functionality is recommended for KDF uses that are more than 1 second long. +// +// The errors returned from methods of the returned Argon2KDF may be instances of +// *[Argon2OutOfProcessError]. +func NewOutOfProcessArgon2KDF(newHandlerCmd func() (*exec.Cmd, error), timeout time.Duration, watchdog Argon2OutOfProcessWatchdogMonitor) Argon2KDF { if newHandlerCmd == nil { panic("newHandlerCmd cannot be nil") } @@ -951,6 +993,7 @@ func NewOutOfProcessArgon2KDF(newHandlerCmd func() (*exec.Cmd, error), watchdog } return &outOfProcessArgon2KDFImpl{ newHandlerCmd: newHandlerCmd, + timeout: timeout, watchdog: watchdog, } } diff --git a/argon2_out_of_process_support_sync.go b/argon2_out_of_process_support_sync.go new file mode 100644 index 00000000..ff692784 --- /dev/null +++ b/argon2_out_of_process_support_sync.go @@ -0,0 +1,247 @@ +// -*- 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 + +import ( + "errors" + "fmt" + "io" + "os" + "time" + + "github.com/snapcore/secboot/internal/paths" + "golang.org/x/sys/unix" +) + +// Due to the amount of memory a KDF request comsumes, we try to serialize the +// requests system-wide to avoid triggering memory pressure. The system-wide +// lock is represented by a file in /run. A process must open this file and +// hold an exclusive advisory lock on the open file descriptor before processing +// a KDF request. It must maintain this exclusive lock and keep the file descriptor +// open until the KDF request completes, at which point the file can be unlinked +// (still inside the exlusive lock) before closing the file descriptor. +// +// Care must be taken wrt race conditions between other process when removing the +// system-wide lock file. Eg, in between opening the system-wide lock file and +// obtaining an exlusive lock on the opened file descriptor, it's possible that +// a lock holder in another process releases its lock on the same file and +// unlinks the system-wide lock file that we opened. In this case, this process +// doesn't really hold the system-wide lock as nothing prevents another process from +// taking another one by creating a new file. It's also possible that in-between +// opening the system-wide lock file and obtaining an exclusive lock on the opened +// file descriptor, another lock holder released its lock on the same file (unlinking +// the system-wide lock file that we opened), and another process has since created +// a new file in preparation for taking its own lock. Again, we don't really hold the +// system-wide lock in this case because we don't hold a lock on the file that the lock +// file path currently points to, so nothing prevents multiple processes from thinking +// that they have taken the lock. Both of these cases can be tested for by doing the +// following after acquiring an exclusive advisory lock on the open file descriptor for +// the system lock file: +// - Ensure that there is still a file at the system-wide lock file path. +// - Ensure the inode that the system-wide lock file path currently points to matches +// the inode that we acquired an exclusive lock on. +// If either of these checks fail, this process does not own the system-wide lock and +// another attempt must be made to attempt to acquire it. +// +// For this to work reliably and without race conditions, the system-wide lock file must +// only be unlinked by the current lock holder. Unlinking the current file would normally +// happen when a process relinquishes the lock, but it's also ok to leave a stale lock +// file around - in this case, another process will just re-use it rather than creating +// a new one. + +var ( + argon2SysLockStderr io.Writer = os.Stderr + + errArgon2OutOfProcessHandlerSystemLockTimeout = errors.New("request timeout") +) + +// acquireArgon2OutOfProcessHandlerSystemLock acquires the system-wide lock +// for serializing Argon2 execution system-wide via this package. If the +// function returns with an error, then the lock was not acquired. If the +// function returns wthout an error, the returned callback can be used to +// release the lock (note that the lock will be relinquished automatically if +// the process exits too). +// +// The specified timeout determines how long this function will wait before +// aborting its attempt to acquire the lock. If set to 0, the function will +// only perform a single attempt. +func acquireArgon2OutOfProcessHandlerSystemLock(timeout time.Duration) (release func(), err error) { + var lockFile *os.File // The opened lock file + + // Ensure that we close the open lockFile descriptor (if there is one) + // on error paths. Note that this does leave a lock file laying around, + // but this isn't a problem and unlinking it can only happen inside of + // an exclusive lock without breaking the locking contract anyway. + defer func() { + if err == nil || lockFile == nil { + return + } + if err := lockFile.Close(); err != nil { + fmt.Fprintf(argon2SysLockStderr, "Cannot close argon2 lock file descriptor on error: %v", err) + } + }() + + timeoutTimer := time.NewTimer(timeout) // Begin the request timeout timer + triedOnce := false // Handle the case of timeout == 0 + + // Run a loop to try to acquire the lock. + for { + if triedOnce { + // If the loop has executed at least once, make sure that + // the timeout hasn't expired. + select { + case <-timeoutTimer.C: + // The timeout has expired. + return nil, errArgon2OutOfProcessHandlerSystemLockTimeout + default: + // continue trying + } + } + triedOnce = true + + // Make sure that we close the lock file left open from the previous + // attempt, if there is one. Note that this does leave a lock file + // laying around, but this isn't a problem and unlinking it can only + // happen inside of an exclusive lock without breaking the locking + // contract anyway. + if lockFile != nil { + if err := lockFile.Close(); err != nil { + return nil, fmt.Errorf("cannot close lock file from previous attempt before starting new attempt: %w", err) + } + } + + // Attempt to open the lock file for writing. + lockFile, err = os.OpenFile(paths.Argon2OutOfProcessHandlerSystemLockPath, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + // No error is expected here. + return nil, fmt.Errorf("cannot open lock file for writing: %w", err) + } + + // Grab information about the lock file we just opened, via its descriptor. + var lockFileSt unix.Stat_t + if err := unix.Fstat(int(lockFile.Fd()), &lockFileSt); err != nil { + // No error is expected here + return nil, fmt.Errorf("cannot obtain lock file info from open descriptor: %w", err) + } + + // Attempt to acquire an exclusive, non-blocking, advisory lock. + if err := unix.Flock(int(lockFile.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { + // We failed to acquire the lock. + if os.IsTimeout(err) { + // The EWOULDBLOCK case. Someone else already has a lock on the + // file we have opened. Try again with a 10ms backoff time, + // unless the request timeout fires first. + select { + case <-time.NewTimer(10 * time.Millisecond).C: + // We can try again + case <-timeoutTimer.C: + // The timeout has expired + return nil, errArgon2OutOfProcessHandlerSystemLockTimeout + } + continue + } + + // No other error is expected. + return nil, fmt.Errorf("cannot obtain lock on open lock file descriptor: %w", err) + } + + // We have acquired an exclusive advisory lock on the file that we opened, but perform + // some checks to ensure we haven't hit a race condition with another process. + + // Grab information about the inode that the lock file path currently points to. + // It's possible that in the window between opening the lock file and taking + // the exclusive lock on the open descriptor, another process might have released + // its own lock on the file we opened, unlinking the path in the meantime. + var updatedSt unix.Stat_t + if err := unix.Stat(paths.Argon2OutOfProcessHandlerSystemLockPath, &updatedSt); err != nil { + if os.IsNotExist(err) { + // The lock file path no longer exists because it was unlinked by + // another process. Try again immediately. + continue + } + + // No other error is expected. + return nil, fmt.Errorf("cannot obtain lock file info from path: %w", err) + } + + // Make sure that the inode we have an exclusive lock on is the same inode that + // the lock file path currently points to. It's possible that in the window between + // opening the lock file and acquiring the exclusive lock, another process might have + // released its own lock on the same file we opened - unlinking the path in the meantime, + // and another process has since created a new file in order to try to acquire its own + // lock. Note that as part of the lock contract, the system-wide lock file path must + // only be unlinked inside an exclusive lock - a process cannot unlink it it doesn't have + // the lock or relinquishes it momentarily - in which case, it would need to perform the + // same steps to re-acquire it again in a non-racey way. + if lockFileSt.Ino == updatedSt.Ino { + // At this point, we hold the system-wide lock, so break out of the loop. + break + } + + // The inode that we have a lock on is not the same one that the lock file + // path currently points to, so nothing is stopping another process from acquiring + // a lock. We should try again immediately. + } + + if lockFile == nil { + // This shouldn't happen. The loop either returns from the function immediately + // on error or breaks only once we have the lock. + panic("locking loop finished without leaving an open lock file descriptor") + } + + release = func() { + if lockFile == nil { + // Handle being called more than once + return + } + + // We can remove the lock file because we still have an exclusive lock on it + unlinkErr := os.Remove(paths.Argon2OutOfProcessHandlerSystemLockPath) + if unlinkErr != nil { + // Log a message if it fails - it just means that we leave the lock + // file laying around which isn't really a problem in /run. We will + // still carry on to release the lock by closing the descriptor. + fmt.Fprintf(argon2SysLockStderr, "Cannot unlink argon2 lock file: %v\n", unlinkErr) + } + + // If the lock file was successfully unlinked, another process is free to + // acquire the lock now. + + // Closing the open descriptor will release our exclusive advisory lock. If the + // previous unlink succeeded, only proceeses that already have a descriptor open + // to it can acquire a lock on it. They will only do this temporarily though + // because they will detect that the lock file path no longer exists, or exists + // but points to a different inode (if another process recreates it). + closeErr := lockFile.Close() + if closeErr != nil { + fmt.Fprintf(argon2SysLockStderr, "Cannot close argon2 lock file descriptor: %v", closeErr) + } + + switch { + case unlinkErr != nil && closeErr != nil: + fmt.Fprintf(argon2SysLockStderr, "Releasing the Argon2 system lock failed\n") + case unlinkErr == nil || closeErr == nil: + // The lock has been successfully released + lockFile = nil + } + } + + return release, nil +} diff --git a/argon2_out_of_process_support_sync_test.go b/argon2_out_of_process_support_sync_test.go new file mode 100644 index 00000000..095f4298 --- /dev/null +++ b/argon2_out_of_process_support_sync_test.go @@ -0,0 +1,150 @@ +// -*- 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 ( + "errors" + "os" + "path/filepath" + "sync" + "syscall" + "time" + + . "github.com/snapcore/secboot" + "github.com/snapcore/secboot/internal/testutil" + "golang.org/x/sys/unix" + . "gopkg.in/check.v1" +) + +type argon2OutOfProcessSupportSyncSuite struct { + lockPath string + restoreLockPath func() +} + +func (s *argon2OutOfProcessSupportSyncSuite) SetUpSuite(c *C) { + if _, exists := os.LookupEnv("NO_ARGON2_TESTS"); exists { + c.Skip("skipping expensive argon2 tests") + } +} + +func (s *argon2OutOfProcessSupportSyncSuite) SetUpTest(c *C) { + s.lockPath = filepath.Join(c.MkDir(), "argon2.lock") + s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(s.lockPath) +} + +func (s *argon2OutOfProcessSupportSyncSuite) TearDownTest(c *C) { + if s.restoreLockPath != nil { + s.restoreLockPath() + } +} + +var _ = Suite(&argon2OutOfProcessSupportSyncSuite{}) + +func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLock(c *C) { + release, err := AcquireArgon2OutOfProcessHandlerSystemLock(0) + c.Assert(err, IsNil) + defer release() + + f, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) + c.Assert(err, IsNil) + defer f.Close() + + err = unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB) + c.Check(err, ErrorMatches, `resource temporarily unavailable`) + c.Check(errors.Is(err, syscall.Errno(syscall.EWOULDBLOCK)), testutil.IsTrue) + + release() + _, err = os.OpenFile(s.lockPath, os.O_RDWR, 0600) + c.Check(os.IsNotExist(err), testutil.IsTrue) +} + +func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLockTimeout(c *C) { + release, err := AcquireArgon2OutOfProcessHandlerSystemLock(0) + c.Assert(err, IsNil) + defer release() + + _, err = AcquireArgon2OutOfProcessHandlerSystemLock(1 * time.Second) + c.Check(err, Equals, ErrArgon2OutOfProcessHandlerSystemLockTimeout) +} + +func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLockDeletedFile(c *C) { + // Grab an exclusive lock on the file first to block + // AcquireArgon2OutOfProcessHandlerSystemLock + f, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) + c.Assert(err, IsNil) + defer f.Close() + + c.Check(unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB), IsNil) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + release, err := AcquireArgon2OutOfProcessHandlerSystemLock(10 * time.Second) + c.Check(err, IsNil) + release() + wg.Done() + }() + + // Ensure we end up waiting for the exclusive lock + <-time.NewTimer(500 * time.Millisecond).C + + // Delete the file + c.Check(os.Remove(s.lockPath), IsNil) + + // Close our FD to free up the locking - it should loop and try again + c.Check(f.Close(), IsNil) + + wg.Done() +} + +func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLockChangedInode(c *C) { + // Grab an exclusive lock on the file first to block + // AcquireArgon2OutOfProcessHandlerSystemLock + f, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) + c.Assert(err, IsNil) + defer f.Close() + + c.Check(unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB), IsNil) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + release, err := AcquireArgon2OutOfProcessHandlerSystemLock(10 * time.Second) + c.Check(err, IsNil) + release() + wg.Done() + }() + + // Ensure we end up waiting for the exclusive lock + <-time.NewTimer(500 * time.Millisecond).C + + // Delete the file + c.Check(os.Remove(s.lockPath), IsNil) + + // Create a new file + f2, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) + c.Assert(err, IsNil) + defer f2.Close() + + // Close our original FD to free up the locking - it should loop and try again + c.Check(f.Close(), IsNil) + + wg.Done() +} diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index c004537a..38a35b98 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -21,75 +21,310 @@ package secboot_test import ( "crypto" + _ "crypto/sha256" + _ "crypto/sha512" "encoding/json" "errors" "fmt" "io" + "math/rand" "os" "os/exec" "path/filepath" "runtime" "sync" + "time" . "github.com/snapcore/secboot" + "github.com/snapcore/secboot/internal/paths" "github.com/snapcore/secboot/internal/testutil" . "gopkg.in/check.v1" "gopkg.in/tomb.v2" ) -type argon2OutOfProcessSupportSuite struct{} +// argon2OutOfProcessHandlerSupportMixin provides capabilities shared +// between suites that test the remote side of out-of-process Argon2 components. +type argon2OutOfProcessHandlerSupportMixin struct { + restoreLockPath func() +} -func (s *argon2OutOfProcessSupportSuite) TearDownTest(c *C) { - ClearIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportMixin) SetUpTest(c *C) { + s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(filepath.Join(c.MkDir(), "argon2.lock")) } -var _ = Suite(&argon2OutOfProcessSupportSuite{}) +func (s *argon2OutOfProcessHandlerSupportMixin) TearDownTest(c *C) { + if s.restoreLockPath != nil { + s.restoreLockPath() + } + ResetArgon2OutOfProcessHandlerStatus() +} -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)) +type testWaitForAndRunArgon2OutOfProcessRequestParams struct { + req *Argon2OutOfProcessRequest + wdHandler Argon2OutOfProcessWatchdogHandler + wdMonitor Argon2OutOfProcessWatchdogMonitor } -func (s *argon2OutOfProcessSupportSuite) TestCallingSetIsArgon2HandlerMoreThanOncePanics(c *C) { - SetIsArgon2HandlerProcess() - c.Check(func() { SetIsArgon2HandlerProcess() }, PanicMatches, `cannot call SetIsArgon2HandlerProcess more than once`) +func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProcessRequest(c *C, params *testWaitForAndRunArgon2OutOfProcessRequestParams) (*Argon2OutOfProcessResponse, error) { + // Create 2 pipes to communicate with WaitForAndRunArgon2OutOfProcessRequest + reqR, reqW := io.Pipe() + rspR, rspW := io.Pipe() + + var actualRsp *Argon2OutOfProcessResponse // The response from the test function + tmb := new(tomb.Tomb) // The tomb for tracking goroutines + + // Spin up a goroutine to bootstrap the test setup and then process responses from the + // test function. I'm not sure how thread safe the test library is, so we avoid doing + // asserts in any goroutines we create. + tmb.Go(func() error { + // Spin up a dedicated routine for running the test function + // (WaitForAndRunArgon2OutOfProcessRequest), passing it one end + // of each pipe and the supplied watchdog handler. Assuming + // nothing else in the test exits a routine with an error, errors + // returned from the test function will propagate out of the tomb + // and will be checked on the main test goroutine. + tmb.Go(func() (err error) { + return WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW, params.wdHandler) + }) + + // reqChan receives requests and then serializes them to the test function on a + // dedicated goroutine. + reqChan := make(chan *Argon2OutOfProcessRequest) + + // wdRspChan is used by the response processing loop to send watchdog + // responses to the watchdog handler. + wdRspChan := make(chan *Argon2OutOfProcessResponse) + + // Spin up a routine to send requests to the test function. + tmb.Go(func() error { + for tmb.Alive() { + select { + case req := <-reqChan: + enc := json.NewEncoder(reqW) + if err := enc.Encode(req); err != nil { + return fmt.Errorf("cannot encode request: %w", err) + } + case <-tmb.Dying(): + } + } + return tomb.ErrDying + }) + + // Spin up a routine to run the watchdog monitor + tmb.Go(func() error { + watchdog := params.wdMonitor + if watchdog == nil { + // Copy the default behaviour for WaitForAndRunArgon2OutOfProcessRequest + watchdog = NoArgon2OutOfProcessWatchdogMonitor() + } + err := watchdog(tmb, reqChan, wdRspChan) + if err == nil && tmb.Alive() { + // The watchdog is not meant to return a nil error whilst the tomb + // is alive. + return errors.New("watchdog monitor terminated unexpectedly") + } + return err + }) + + // Send the main request + select { + case reqChan <- params.req: + case <-tmb.Dying(): + return tomb.ErrDying + } + + // Process responses + for tmb.Alive() { + dec := json.NewDecoder(rspR) + dec.DisallowUnknownFields() + var rsp *Argon2OutOfProcessResponse + if err := dec.Decode(&rsp); err != nil { + return fmt.Errorf("cannot decode response: %w", err) + } + + if rsp.Err() != nil { + // Just return an error response. + return rsp.Err() + } + + switch rsp.Command { + case Argon2OutOfProcessCommandWatchdog: + // Direct watchdog responses to wdRspChan so they can be picked up by + // the watchdog handler. + select { + case wdRspChan <- rsp: + case <-tmb.Dying(): + // This loop will no longer iterate + } + default: + actualRsp = rsp + tmb.Kill(nil) + // This loop will no longer iterate + } + } + return tomb.ErrDying + }) + + // Wait for the tomb to begin dying + <-tmb.Dying() + + // Close our end of the pipes supplied to the test function, as + // a real parent process would. + c.Check(reqW.Close(), IsNil) + c.Check(rspR.Close(), IsNil) + + // Wait for everything to die, hopefully successfully + if err := tmb.Wait(); err != nil { + return nil, err + } + return actualRsp, nil } -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)) +// argon2OutOfProcessParentSupportMixin provides capabilities shared +// between suites that test the parent side of out-of-process Argon2 components. +type argon2OutOfProcessParentSupportMixin struct { + restoreLockPath func() + runArgon2OutputDir string } -func (s *argon2OutOfProcessSupportSuite) TestCallingSetArgon2KDFAfterSetIsArgon2HandlerPanics(c *C) { - SetIsArgon2HandlerProcess() - c.Check(func() { SetArgon2KDF(InProcessArgon2KDF) }, PanicMatches, `cannot call SetArgon2KDF in a process where SetIsArgon2HandlerProcess has already been called`) +func (s *argon2OutOfProcessParentSupportMixin) SetUpSuite(c *C) { + 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 *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidProcess(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandDerive, - Passphrase: "foo", - Salt: nil, - Keylen: 32, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4, - }) - c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ - Command: Argon2OutOfProcessCommandDerive, - ErrorType: Argon2OutOfProcessErrorUnexpected, - ErrorString: "cannot handle request in a process that isn't configured as an Argon2 handler process, try calling SetIsArgon2HandlerProcess", +func (s *argon2OutOfProcessParentSupportMixin) SetUpTest(c *C) { + s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(filepath.Join(c.MkDir(), "argon2.lock")) +} + +func (s *argon2OutOfProcessParentSupportMixin) TearDownTest(c *C) { + if s.restoreLockPath != nil { + s.restoreLockPath() + } +} + +func (s *argon2OutOfProcessParentSupportMixin) newHandlerCmd(args ...string) func() (*exec.Cmd, error) { + return func() (*exec.Cmd, error) { + return exec.Command(filepath.Join(s.runArgon2OutputDir, "run_argon2"), append([]string{paths.Argon2OutOfProcessHandlerSystemLockPath}, args...)...), nil + } +} + +type testHMACArgon2OutOfProcessWatchdogMonitorParams struct { + monitorAlg crypto.Hash + period time.Duration + timeout time.Duration + handlerAlg crypto.Hash + + minDelay time.Duration + maxDelay time.Duration +} + +func (s *argon2OutOfProcessParentSupportMixin) testHMACArgon2OutOfProcessWatchdogMonitor(c *C, params *testHMACArgon2OutOfProcessWatchdogMonitorParams) error { + c.Assert(params.maxDelay >= params.minDelay, testutil.IsTrue) + + monitor := HMACArgon2OutOfProcessWatchdogMonitor(params.monitorAlg, params.period, params.timeout) + handler := HMACArgon2OutOfProcessWatchdogHandler(params.handlerAlg) + + tmb := new(tomb.Tomb) + reqChan := make(chan *Argon2OutOfProcessRequest) + rspChan := make(chan *Argon2OutOfProcessResponse) + + // Spin up a routine to handle watchdog requests and setup the test + tmb.Go(func() error { + // Spin up a routine to run the monitor + tmb.Go(func() error { + return monitor(tmb, reqChan, rspChan) + }) + + // Spin up a routine that will terminate the test after 2 seconds + tmb.Go(func() error { + <-time.After(2 * time.Second) + tmb.Kill(nil) + return tomb.ErrDying + }) + + // Run a loop whilst the tomb is alive to handle wachdog requests + for tmb.Alive() { + start := time.Now() // Record the start time + select { + case req := <-reqChan: + period := time.Now().Sub(start) + + // Ensure the monitor period is accurate within +/- 10% + min := time.Duration(float64(period) * 0.9) + max := time.Duration(float64(period) * 1.1) + if period < min || period > max { + return fmt.Errorf("unexpected period %v", period) + } + + // Ensure the monitor send the correct command + if req.Command != Argon2OutOfProcessCommandWatchdog { + return fmt.Errorf("unexpected request command %q", req.Command) + } + + // Run the handler + rsp, err := handler(req.WatchdogChallenge) + if err != nil { + return fmt.Errorf("cannot handle challenge: %w", err) + } + + // Insert a delay + delay := params.minDelay + time.Duration((float64(rand.Intn(100))/100)*float64(params.maxDelay-params.minDelay)) + select { + case <-time.After(delay): + case <-tmb.Dying(): + return tomb.ErrDying + } + + // Send the response back to the monmitor + select { + case rspChan <- &Argon2OutOfProcessResponse{Command: Argon2OutOfProcessCommandWatchdog, WatchdogResponse: rsp}: + case <-tmb.Dying(): + } + case <-tmb.Dying(): + } + } + return tomb.ErrDying }) + + return tmb.Wait() +} + +type argon2OutOfProcessHandlerSupportSuite struct { + argon2OutOfProcessHandlerSupportMixin +} + +type argon2OutOfProcessHandlerSupportSuiteExpensive struct { + argon2OutOfProcessHandlerSupportMixin +} + +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) SetUpSuite(c *C) { + if _, exists := os.LookupEnv("NO_ARGON2_TESTS"); exists { + c.Skip("skipping expensive argon2 tests") + } +} + +type argon2OutOfProcessParentSupportSuite struct { + argon2OutOfProcessParentSupportMixin +} + +type argon2OutOfProcessParentSupportSuiteExpensive struct { + argon2OutOfProcessParentSupportMixin +} + +func (s *argon2OutOfProcessParentSupportSuiteExpensive) SetUpSuite(c *C) { + if _, exists := os.LookupEnv("NO_ARGON2_TESTS"); exists { + c.Skip("skipping expensive argon2 tests") + } + s.argon2OutOfProcessParentSupportMixin.SetUpSuite(c) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidMode(c *C) { - SetIsArgon2HandlerProcess() +var _ = Suite(&argon2OutOfProcessHandlerSupportSuite{}) +var _ = Suite(&argon2OutOfProcessHandlerSupportSuiteExpensive{}) +var _ = Suite(&argon2OutOfProcessParentSupportSuite{}) +var _ = Suite(&argon2OutOfProcessParentSupportSuiteExpensive{}) + +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidMode(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -103,12 +338,11 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestI c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, ErrorType: Argon2OutOfProcessErrorInvalidMode, - ErrorString: "invalid mode: \"foo\"", + ErrorString: "mode cannot be \"foo\"", }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidTime(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidTime(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -122,12 +356,11 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestI c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, ErrorType: Argon2OutOfProcessErrorInvalidTimeCost, - ErrorString: "invalid time cost: cannot be zero", + ErrorString: "time cannot be zero", }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidThreads(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidThreads(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -141,12 +374,29 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessDeriveRequestI c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, ErrorType: Argon2OutOfProcessErrorInvalidThreads, - ErrorString: "invalid threads: cannot be zero", + ErrorString: "threads cannot be zero", }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidPassphrase(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidWatchdogChallenge(c *C) { + out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: nil, + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32 * 1024, + Threads: 1, + WatchdogChallenge: []byte{1, 2, 3, 4}, + }) + c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + ErrorType: Argon2OutOfProcessErrorUnexpectedInput, + ErrorString: "invalid watchdog challenge: cannot service a watchdog", + }) +} +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidPassphrase(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, Passphrase: "foo", @@ -162,8 +412,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInv }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidSalt(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidSalt(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, Salt: []byte("0123456789abcdefghijklmnopqrstuv"), @@ -179,8 +428,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInv }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidKeylen(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidKeylen(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, Keylen: 32, @@ -196,8 +444,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTimeRequestInv }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessInvalidCommand(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessInvalidCommand(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommand("foo"), Mode: Argon2id, @@ -208,12 +455,11 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessInvalidCommand c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommand("foo"), ErrorType: Argon2OutOfProcessErrorInvalidCommand, - ErrorString: "invalid command: \"foo\"", + ErrorString: "command cannot be \"foo\"", }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessMinimum(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveMoreThanOnce(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -228,10 +474,35 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessMinimum(c *C) Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "7306196ab24ea3ac9daab7f14345a9dc228dccef07075dbd2e047deac96689ea"), }) + + out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + }) + c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + ErrorType: Argon2OutOfProcessErrorRestartProcess, + ErrorString: "this process cannot handle any more KDF requests", + }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessThreads1(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveMinimum(c *C) { + out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "7306196ab24ea3ac9daab7f14345a9dc228dccef07075dbd2e047deac96689ea"), + }) +} + +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentThreads(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -248,8 +519,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessThreads1(c *C) }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTime5(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentTime(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -266,8 +536,7 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessTime5(c *C) { }) } -func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessMemory64(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentMemory(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -284,7 +553,121 @@ func (s *argon2OutOfProcessSupportSuite) TestRunArgon2OutOfProcessMemory64(c *C) }) } -func (s *argon2OutOfProcessSupportSuite) TestHMACArgon2OutOfProcessWatchdogHandlerSHA256(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentPassphrase(c *C) { + out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "bar", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "43cb7f6d24bb2da9ae04735c7193c7523fe057243f09c1241a99cd4ccd7d17f5"), + }) +} + +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentSalt(c *C) { + out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "97226ac63a73c7dafef57066ee645abe"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "720ff1ce2beecf00c4586d659bd7fa9f018cc4f115f398975eff50b35f3393ff"), + }) +} + +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentKeyLen(c *C) { + out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 64, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "21ab785e199d43575ca11e85e0a1281b4426c973cfad0a899b24bc4b8057355912a20b5f4132d8132ce3aa5bffe0d9a6a7fd05d3ab67898c196d584c98d47e44"), + }) +} + +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentMode(c *C) { + out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2i, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "a02a0203ea0e5e9abe4006fc80d1aca26b0adc1f898214c4c61d31f90bd4d129"), + }) +} + +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcess2GB(c *C) { + out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "bar", + Salt: testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 2 * 1024 * 1024, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd"), + }) +} + +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTime(c *C) { + out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandTime, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, NotNil) + c.Check(out.Command, Equals, Argon2OutOfProcessCommandTime) + c.Check(out.Duration, Not(Equals), time.Duration(0)) + + origDuration := out.Duration + + // Permit calling the function again + ResetArgon2OutOfProcessHandlerStatus() + + out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandTime, + Mode: Argon2id, + Time: 8, + MemoryKiB: 512, + Threads: 4, + }) + c.Check(out, NotNil) + c.Check(out.Command, Equals, Argon2OutOfProcessCommandTime) + c.Check(out.Duration > origDuration, testutil.IsTrue) +} + +func (s *argon2OutOfProcessHandlerSupportSuite) TestHMACArgon2OutOfProcessWatchdogHandlerSHA256(c *C) { handler := HMACArgon2OutOfProcessWatchdogHandler(crypto.SHA256) rsp, err := handler(testutil.DecodeHexString(c, "3674f5b88f2e6b36ae94aa01f1ee16eaf9ab90df0979ae966837bcd37f0fa1fc")) @@ -293,10 +676,10 @@ func (s *argon2OutOfProcessSupportSuite) TestHMACArgon2OutOfProcessWatchdogHandl rsp, err = handler(testutil.DecodeHexString(c, "3c1de58760e53cac4facc2d5409b362fcf9b81f9b611479f5956abdb0227e567")) c.Check(err, IsNil) - c.Check(rsp, DeepEquals, testutil.DecodeHexString(c, "d9938d01b2e93073d4b1524371bd3f7b646d5f06335861c54030ba6a1164f6d0")) + c.Check(rsp, DeepEquals, testutil.DecodeHexString(c, "3d746bc1f5c471ea9983596512ac846910facf966b611dc2c62e08203afc86f0")) } -func (s *argon2OutOfProcessSupportSuite) TestHMACArgon2OutOfProcessWatchdogHandlerSHA384(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuite) TestHMACArgon2OutOfProcessWatchdogHandlerSHA384(c *C) { handler := HMACArgon2OutOfProcessWatchdogHandler(crypto.SHA384) rsp, err := handler(testutil.DecodeHexString(c, "7b70dfe03ac13bf595061f0d454d10a3595b494277306fe3ed6cdc1c711199cf943bed96023dbd07699f1b6fcbe96574")) @@ -305,158 +688,530 @@ func (s *argon2OutOfProcessSupportSuite) TestHMACArgon2OutOfProcessWatchdogHandl rsp, err = handler(testutil.DecodeHexString(c, "dada0215efc0e034f431fce916caf73af7fd84ad24f9215d08959699745957c7e29190d214e8c1cda78c45a2f0bd4059")) c.Check(err, IsNil) - c.Check(rsp, DeepEquals, testutil.DecodeHexString(c, "41c4b9e86cf1c43bb53439a105f7a769b0bbd7669e11d4761ebb4e7b8ec0de8169d427c63443b772655c66846fd2c8dd")) + c.Check(rsp, DeepEquals, testutil.DecodeHexString(c, "a36e2bda200e88620d53d32196bb6c49efa24c152009a7aac7fcb36e1b97ae2fb62ffc359c2247a8c2bb8f2e89f4b8a9")) } -func (s *argon2OutOfProcessSupportSuite) TestNoArgon2OutOfProcessWatchdogHandler(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuite) TestNoArgon2OutOfProcessWatchdogHandler(c *C) { handler := NoArgon2OutOfProcessWatchdogHandler() _, err := handler([]byte{1, 2, 3, 4}) - c.Check(err, ErrorMatches, `unexpected request: no handler for watchdog`) + c.Check(err, ErrorMatches, `unexpected watchdog request: no handler`) } -type argon2OutOfProcessSupportSuiteExpensive struct { - runArgon2OutputDir string +func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestMinimum(c *C) { + rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + req: &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }, + wdHandler: NoArgon2OutOfProcessWatchdogHandler(), + wdMonitor: NoArgon2OutOfProcessWatchdogMonitor(), + }) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "7306196ab24ea3ac9daab7f14345a9dc228dccef07075dbd2e047deac96689ea"), + }) } -func (s *argon2OutOfProcessSupportSuiteExpensive) runArgon2HandlerPath() string { - return filepath.Join(s.runArgon2OutputDir, "run_argon2") +func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentThreads(c *C) { + rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + req: &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 1, + }, + wdHandler: NoArgon2OutOfProcessWatchdogHandler(), + wdMonitor: NoArgon2OutOfProcessWatchdogMonitor(), + }) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "5699b81ee10e189505874d0cbd93d61186b90554c716d309037907b7238113e1"), + }) } -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 *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentTime(c *C) { + rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + req: &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 5, + MemoryKiB: 32, + Threads: 4, + }, + wdHandler: NoArgon2OutOfProcessWatchdogHandler(), + wdMonitor: NoArgon2OutOfProcessWatchdogMonitor(), + }) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "2f2d7dd170cf43aff82737bc1c2fbe685b34190fc8b62378693c3b0685b96912"), + }) } -func (s *argon2OutOfProcessSupportSuiteExpensive) SetUpTest(c *C) { - SetIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentMemory(c *C) { + rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + req: &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 64, + Threads: 4, + }, + wdHandler: NoArgon2OutOfProcessWatchdogHandler(), + wdMonitor: NoArgon2OutOfProcessWatchdogMonitor(), + }) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "6f49db1f7336329c0d5fd652642b144b204d7976c5fcb4c72b6e1d9ea345fa32"), + }) } -func (s *argon2OutOfProcessSupportSuiteExpensive) TearDownTest(c *C) { - ClearIsArgon2HandlerProcess() +func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestPassphrase(c *C) { + rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + req: &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "bar", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }, + wdHandler: NoArgon2OutOfProcessWatchdogHandler(), + wdMonitor: NoArgon2OutOfProcessWatchdogMonitor(), + }) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "43cb7f6d24bb2da9ae04735c7193c7523fe057243f09c1241a99cd4ccd7d17f5"), + }) } -var _ = Suite(&argon2OutOfProcessSupportSuiteExpensive{}) +func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentSalt(c *C) { + rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + req: &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "97226ac63a73c7dafef57066ee645abe"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }, + wdHandler: NoArgon2OutOfProcessWatchdogHandler(), + wdMonitor: NoArgon2OutOfProcessWatchdogMonitor(), + }) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "720ff1ce2beecf00c4586d659bd7fa9f018cc4f115f398975eff50b35f3393ff"), + }) +} -func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcess2GB(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandDerive, - Passphrase: "bar", - Salt: testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), - Keylen: 32, - Mode: Argon2id, - Time: 4, - MemoryKiB: 2 * 1024 * 1024, - Threads: 4, +func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentKeyLen(c *C) { + rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + req: &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 64, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }, + wdHandler: NoArgon2OutOfProcessWatchdogHandler(), + wdMonitor: NoArgon2OutOfProcessWatchdogMonitor(), }) - c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "21ab785e199d43575ca11e85e0a1281b4426c973cfad0a899b24bc4b8057355912a20b5f4132d8132ce3aa5bffe0d9a6a7fd05d3ab67898c196d584c98d47e44"), + }) +} + +func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentMode(c *C) { + rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + req: &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2i, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }, + wdHandler: NoArgon2OutOfProcessWatchdogHandler(), + wdMonitor: NoArgon2OutOfProcessWatchdogMonitor(), + }) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "a02a0203ea0e5e9abe4006fc80d1aca26b0adc1f898214c4c61d31f90bd4d129"), + }) +} + +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequest2GB(c *C) { + rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + req: &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "bar", + Salt: testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 2 * 1024 * 1024, + Threads: 4, + }, + wdHandler: HMACArgon2OutOfProcessWatchdogHandler(crypto.SHA256), + wdMonitor: HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond), + }) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd"), }) } -func (s *argon2OutOfProcessSupportSuiteExpensive) testWaitForAndRunArgon2OutOfProcessRequest(c *C, req *Argon2OutOfProcessRequest, expectedRsp *Argon2OutOfProcessResponse, wdMonitor Argon2OutOfProcessWatchdogMonitor, wdHandler Argon2OutOfProcessWatchdogHandler) { - reqR, reqW := io.Pipe() - rspR, rspW := io.Pipe() +func (s *argon2OutOfProcessParentSupportSuite) TestNoArgon2OutOfProcessWatchdogMonitorUnexpectedResponse(c *C) { + monitor := NoArgon2OutOfProcessWatchdogMonitor() tmb := new(tomb.Tomb) - - var wg sync.WaitGroup - wg.Add(1) + reqChan := make(chan *Argon2OutOfProcessRequest) + rspChan := make(chan *Argon2OutOfProcessResponse) tmb.Go(func() error { - // Spin up a dedicated routine for running the test function - // (WaitForAndRunArgon2OutOfProcessRequest). - tmb.Go(func() (err error) { - defer wg.Done() - return WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW, wdHandler) - }) + return monitor(tmb, reqChan, rspChan) + }) - // wdReqChan is sent requests from the watchdog monitor which are then - // received by another goroutine, which serializes them and sends them to - // the remote process via its stdin. - wdReqChan := make(chan *Argon2OutOfProcessRequest) + select { + case rspChan <- new(Argon2OutOfProcessResponse): + case <-time.After(2 * time.Second): // Give the test 2 seconds to complete + } - // wdRspChan is sent watchdog responses received from the remote process - // via stdout, and they are subsequently received by the watchdog monitor - // for processing. - wdRspChan := make(chan *Argon2OutOfProcessResponse) + c.Check(tmb.Wait(), ErrorMatches, `unexpected watchdog response`) +} - // Spin up the watchdog monitor +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestNoArgon2OutOfProcessWatchdogMonitor(c *C) { + monitor := NoArgon2OutOfProcessWatchdogMonitor() + + tmb := new(tomb.Tomb) + reqChan := make(chan *Argon2OutOfProcessRequest) + rspChan := make(chan *Argon2OutOfProcessResponse) + + tmb.Go(func() error { tmb.Go(func() error { - err := wdMonitor(tmb, wdReqChan, wdRspChan) - if err == nil && tmb.Alive() { - // The watchdog returning an error will terminate the tomb, but if - // it returns no error whilst the tomb is still alive, then consider - // this to be unexpected. In this case, begin the termination of the - // tomb. - return errors.New("watchdog monitor terminated unexpectedly") - } - return err + return monitor(tmb, reqChan, rspChan) }) - // Spin up a tomb for processing requests from the watchdog. - tmb.Go(func() error { - for tmb.Alive() { - dec := json.NewDecoder(rspR) - var rsp *Argon2OutOfProcessResponse - c.Check(dec.Decode(&rsp), IsNil) - - switch rsp.Command { - case Argon2OutOfProcessCommandWatchdog: - wdRspChan <- rsp - default: - // Check the final reponse - c.Check(rsp, DeepEquals, expectedRsp) - - // Trigger the termination of WaitForAndRunArgon2OutOfProcessRequest. - c.Check(reqW.Close(), IsNil) - - // Wait for WaitForAndRunArgon2OutOfProcessRequest and the rest of the tomb to finish cleanly. - wg.Wait() - } - } + // Run the monitor for 2 seconds to make sure we don't see any requests. + select { + case <-time.After(2 * time.Second): + case <-reqChan: + return errors.New("unexpected watchdog request") + } - tmb.Go(func() error { - if req != nil { - enc := json.NewEncoder(reqW) - if err := enc.Encode(req); err != nil { - return fmt.Errorf("cannot encode initial request: %w", err) - } - } - for { - select { - case <-tmb.Dying(): - return nil - case req := <-wdReqChan: - // Send the watchdog request to its io.Reader - enc := json.NewEncoder(reqR) - if err := enc.Encode(req); err != nil { - return fmt.Errorf("cannot encode watchdog request: %w", err) - } - } - } - }) - return nil - }) - return nil + tmb.Kill(nil) + return tomb.ErrDying }) - c.Check(tmb.Wait(), IsNil) } -func (s *argon2OutOfProcessSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequest(c *C) { - s.testWaitForAndRunArgon2OutOfProcessRequest(c, &Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandDerive, - Passphrase: "bar", - Salt: testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), - Keylen: 32, - Mode: Argon2id, - Time: 4, - MemoryKiB: 128 * 1024, - Threads: 4, - }, &Argon2OutOfProcessResponse{}, NoArgon2OutOfProcessWatchdogMonitor(), NoArgon2OutOfProcessWatchdogHandler()) +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestHMACArgon2OutOfProcessWatchdogMonitor(c *C) { + err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ + monitorAlg: crypto.SHA256, + period: 100 * time.Millisecond, + timeout: 20 * time.Millisecond, + handlerAlg: crypto.SHA256, + minDelay: 5 * time.Millisecond, + maxDelay: 15 * time.Millisecond, + }) + c.Check(err, IsNil) +} + +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestHMACArgon2OutOfProcessWatchdogMonitorDifferentAlg(c *C) { + err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ + monitorAlg: crypto.SHA384, + period: 100 * time.Millisecond, + timeout: 20 * time.Millisecond, + handlerAlg: crypto.SHA384, + minDelay: 5 * time.Millisecond, + maxDelay: 15 * time.Millisecond, + }) + c.Check(err, IsNil) +} + +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestHMACArgon2OutOfProcessWatchdogMonitorDifferentPeriod(c *C) { + err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ + monitorAlg: crypto.SHA256, + period: 200 * time.Millisecond, + timeout: 20 * time.Millisecond, + handlerAlg: crypto.SHA256, + minDelay: 5 * time.Millisecond, + maxDelay: 15 * time.Millisecond, + }) + c.Check(err, IsNil) +} + +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestHMACArgon2OutOfProcessWatchdogMonitorDifferentTimeout(c *C) { + err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ + monitorAlg: crypto.SHA256, + period: 100 * time.Millisecond, + timeout: 50 * time.Millisecond, + handlerAlg: crypto.SHA256, + minDelay: 35 * time.Millisecond, + maxDelay: 45 * time.Millisecond, + }) + c.Check(err, IsNil) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestHMACArgon2OutOfProcessWatchdogMonitorResponseTimeout(c *C) { + err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ + monitorAlg: crypto.SHA256, + period: 100 * time.Millisecond, + timeout: 20 * time.Millisecond, + handlerAlg: crypto.SHA256, + minDelay: 25 * time.Millisecond, + maxDelay: 25 * time.Millisecond, + }) + c.Check(err, ErrorMatches, `timeout waiting for watchdog response from remote process`) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestHMACArgon2OutOfProcessWatchdogMonitorInvalidResponse(c *C) { + err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ + monitorAlg: crypto.SHA384, + period: 100 * time.Millisecond, + timeout: 20 * time.Millisecond, + handlerAlg: crypto.SHA256, + minDelay: 5 * time.Millisecond, + maxDelay: 15 * time.Millisecond, + }) + c.Check(err, ErrorMatches, `unexpected watchdog response value from remote process`) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveMinimum(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4, + } + key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "7306196ab24ea3ac9daab7f14345a9dc228dccef07075dbd2e047deac96689ea")) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentThreads(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 1, + } + key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "5699b81ee10e189505874d0cbd93d61186b90554c716d309037907b7238113e1")) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentTime(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) + params := &Argon2CostParams{ + Time: 5, + MemoryKiB: 32, + Threads: 4, + } + key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "2f2d7dd170cf43aff82737bc1c2fbe685b34190fc8b62378693c3b0685b96912")) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMemory(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 64, + Threads: 4, + } + key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "6f49db1f7336329c0d5fd652642b144b204d7976c5fcb4c72b6e1d9ea345fa32")) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentPassphrase(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4, + } + key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "43cb7f6d24bb2da9ae04735c7193c7523fe057243f09c1241a99cd4ccd7d17f5")) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentSalt(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4, + } + key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "97226ac63a73c7dafef57066ee645abe"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "720ff1ce2beecf00c4586d659bd7fa9f018cc4f115f398975eff50b35f3393ff")) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveKeyLen(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4, + } + key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 64) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "21ab785e199d43575ca11e85e0a1281b4426c973cfad0a899b24bc4b8057355912a20b5f4132d8132ce3aa5bffe0d9a6a7fd05d3ab67898c196d584c98d47e44")) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMode(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4, + } + key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2i, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "a02a0203ea0e5e9abe4006fc80d1aca26b0adc1f898214c4c61d31f90bd4d129")) +} + +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GB(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 2 * 1024 * 1024, + Threads: 4, + } + key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd")) +} + +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GBDifferentWatchdogHMAC(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha384"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA384, 100*time.Millisecond, 50*time.Millisecond)) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 2 * 1024 * 1024, + Threads: 4, + } + key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd")) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveErr(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) + params := &Argon2CostParams{ + Time: 0, + MemoryKiB: 32, + Threads: 4, + } + _, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) + c.Check(err, ErrorMatches, `cannot process request: invalid-time-cost \(time cannot be zero\)`) + c.Check(err, testutil.ConvertibleTo, new(Argon2OutOfProcessError)) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFTime(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 32, + Threads: 4, + } + origDuration, err := kdf.Time(Argon2id, params) + c.Check(err, IsNil) + + params = &Argon2CostParams{ + Time: 8, + MemoryKiB: 512, + Threads: 4, + } + duration, err := kdf.Time(Argon2id, params) + c.Check(err, IsNil) + c.Check(duration > origDuration, testutil.IsTrue) +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelSerialized(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 10*time.Second, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 2 * 1024 * 1024, + Threads: 4, + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "d38bda36abedb509c77cbc032d80f8ebc2c1742f12a4dbedfe42a20c77db51f3")) + wg.Done() + }() + go func() { + key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "4f98f746990ccc53520a3096ce201e2935aac92649896c65fb5f947824f0032e")) + wg.Done() + }() + wg.Wait() +} + +func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelTimeout(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 100*time.Millisecond, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 2 * 1024 * 1024, + Threads: 4, + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) + c.Check(err, IsNil) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "d38bda36abedb509c77cbc032d80f8ebc2c1742f12a4dbedfe42a20c77db51f3")) + wg.Done() + }() + go func() { + <-time.NewTimer(20 * time.Millisecond).C + _, err := kdf.Derive("bar", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) + c.Check(err, ErrorMatches, `cannot process request: timeout-error \(cannot acquire argon2 system lock: request timeout\)`) + c.Check(err, testutil.ConvertibleTo, &Argon2OutOfProcessError{}) + wg.Done() + }() + wg.Wait() } diff --git a/argon2_out_of_process_support_test.go.bak b/argon2_out_of_process_support_test.go.bak deleted file mode 100644 index a162ce82..00000000 --- a/argon2_out_of_process_support_test.go.bak +++ /dev/null @@ -1,785 +0,0 @@ -// -*- 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" - "time" - - . "github.com/snapcore/secboot" - "github.com/snapcore/secboot/internal/testutil" - . "gopkg.in/check.v1" - "gopkg.in/tomb.v2" -) - -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 * 1024, - Threads: 4, - }) - c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ - Command: Argon2OutOfProcessCommandDerive, - ErrorType: Argon2OutOfProcessErrorUnexpected, - ErrorString: "cannot handle 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 * 10224, - 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 * 1024, - 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 * 1024, - 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 * 1024, - 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 * 1024, - 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 * 1024, - 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 * 1024, - 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: Argon2OutOfProcessErrorUnexpected, - 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: unexpected-error \(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)) -} - -func (s *argon2OutOfProcessSupportSuite) TestCallingSetArgon2KDFAfterSetIsArgon2HandlerPanics(c *C) { - SetIsArgon2HandlerProcess() - c.Check(func() { SetArgon2KDF(InProcessArgon2KDF) }, PanicMatches, `cannot call SetArgon2KDF in a process where SetIsArgon2HandlerProcess has already been called`) -} - -func (s *argon2OutOfProcessSupportSuite) TestNoArgon2OutOfProcessWatchdogHandler(c *C) { - handler := NoArgon2OutOfProcessWatchdogHandler() - _, err := handler([]byte{1, 2, 3, 4}) - c.Check(err, ErrorMatches, `unexpected request: no handler for watchdog`) -} - -func (s *argon2OutOfProcessSupportSuite) TestDefaultArgon2OutOfProcessWatchdogHandler(c *C) { - handler := DefaultArgon2OutOfProcessWatchdogHandler() - - rsp, err := handler(testutil.DecodeHexString(c, "79e7d47fed15d6eef1e7e5f54cbb69d37169378527d65d2ba809a364930e94e3")) - c.Check(err, IsNil) - c.Logf("%x", rsp) - c.Check(rsp, DeepEquals, testutil.DecodeHexString(c, "1fed49cb3f22b3ddc895a7837833d84b181bda9a1a6f098a297d163729a33c58")) - - rsp, err := handler(testutil.DecodeHexString(c, "3c1de58760e53cac4facc2d5409b362fcf9b81f9b611479f5956abdb0227e567")) - c.Check(err, IsNil) - c.Logf("%x", rsp) - c.Check(rsp, DeepEquals, testutil.DecodeHexString(c, "cb487ce254d115cf91f282c8c82a6c5d01b16db99f71175242d060f455fc4624")) -} - -func (s *argon2OutOfProcessSupportSuite) TestNoArgon2OutOfProcessWatchdogMonitorUnexpectedResponse(c *C) { - monitor := NoArgon2OutOfProcessWatchdogMonitor() - - reqChan := make(chan *Argon2OutOfProcessRequest) - rspChan := make(chan *Argon2OutOfProcessResponse) - - tmb := new(tomb.Tomb) - tmb.Go(func() error { - // Run a routine for running NoArgon2OutOfProcessWatchdogMonitor. - tmb.Go(func() error { - return monitor(tmb, reqChan, rspChan) - }) - - rspChan <- new(Argon2OutOfProcessResponse) - return nil - }) - c.Check(tmb.Wait(), ErrorMatches, `unexpected watchdog response`) -} - -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{}) - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestNoArgon2OutOfProcessWatchdogMonitor(c *C) { - monitor := NoArgon2OutOfProcessWatchdogMonitor() - - reqChan := make(chan *Argon2OutOfProcessRequest) - rspChan := make(chan *Argon2OutOfProcessResponse) - - tmb := new(tomb.Tomb) - tmb.Go(func() error { - // Run a routine for running the monitor. - tmb.Go(func() error { - // Kill the tomb if the monitor returns early so that we can detect this. - err := monitor(tmb, reqChan, rspChan) - tmb.Kill(errors.New("monitor exitted unexpectedly")) - return err - }) - - // Run another routine to make sure we get no watchdog requests. - tmb.Go(func() error { - for tmb.Alive() { - select { - case <-reqChan: - return errors.New("unexpected watchdog request") - case <-tmb.Dying(): - return nil - } - } - return nil - }) - - // Run the test for 1 second. - select { - case <-time.After(1 * time.Second): - // Kill the tomb to finish the test - tmb.Kill(nil) - case <-tmb.Dying(): - // Something else already failed - there's no point in waiting for the timeout here - return nil - } - return nil - }) - c.Check(tmb.Wait(), IsNil) // This will return an error here if the monitor returned with an error -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) testDefaultArgon2OutOfProcessWatchdogMonitor(c *C, period, timeout time.Duration) { - monitor := DefaultArgon2OutOfProcessWatchdogMonitor(period, timeout) - - reqChan := make(chan *Argon2OutOfProcessRequest) - rspChan := make(chan *Argon2OutOfProcessResponse) - - tmb := new(tomb.Tomb) - tmb.Go(func() error { - // Run a routine for running the monitor. - tmb.Go(func() error { - return monitor(tmb, reqChan, rspChan) - }) - - // Run another routine to make sure we get no watchdog requests. - tmb.Go(func() error { - for tmb.Alive() { - select { - case <-reqChan: - return errors.New("unexpected watchdog request") - case <-tmb.Dying(): - return nil - } - } - return nil - }) - - // Run the test for 1 second. - select { - case <-time.After(1 * time.Second): - // Kill the tomb to finish the test - tmb.Kill(nil) - case <-tmb.Dying(): - // Something else already failed - there's no point in waiting for the timeout here - return nil - } - return nil - }) - c.Check(tmb.Wait(), IsNil) // This will return an error here if the monitor returned with an error -} - -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 * 1024, - Threads: 4, - Keylen: 32, - }, - expectedKey: testutil.DecodeHexString(c, "b47ad96075d64cb92cdc7678e6bbb85f496da6e84d7ea05fbc0092dfb0ac3e13"), - }) -} - -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 * 1024, - Threads: 4, - Keylen: 32, - }, - expectedKey: testutil.DecodeHexString(c, "e5081bdbb5dc709ecd789ad6da76ce6c49d2bc3b958dda4a93c6b4140def877e"), - }) -} - -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 * 1024, - Threads: 4, - Keylen: 32, - }, - expectedKey: testutil.DecodeHexString(c, "bd962af1e81debad7966d3c0ca1dd9398dc231a3c25c96de54a1df97233d1a49"), - }) -} - -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 * 1024, - Threads: 4, - Keylen: 32, - }, - expectedKey: testutil.DecodeHexString(c, "0d781d62896d7bb71830251af01be0323f2006770beb917e62a2ea3330693625"), - }) -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestDeriveDifferentParams(c *C) { - s.testRunArgon2OutOfProcessRequestDerive(c, &testRunArgon2OutOfProcessRequestDeriveParams{ - req: &Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandDerive, - Passphrase: "foo", - Salt: []byte("0123456789abcdefghijklmnopqrstuv"), - Mode: Argon2id, - Time: 48, - MemoryKiB: 64 * 1024, - Threads: 4, - Keylen: 32, - }, - expectedKey: testutil.DecodeHexString(c, "ba935d605f3f021c6cad26c8e2c0316fcc23814b2aa580e33e0ddb040692fb77"), - }) -} - -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 * 1024, - Threads: 4, - Keylen: 64, - }, - expectedKey: testutil.DecodeHexString(c, "385251574d5dfa3c25eb5fa2ad99f74cba39c284a16999b2d8e6908ad2304225e1f706dc860867179759ca058c9e0b961f6a4ec88f0eb38ba825d655bf892116"), - }) -} - -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) TestRunArgon2OutOfProcessRequestDeriveRestartProcess(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandDerive, - Passphrase: "foo", - Salt: nil, - Keylen: 32, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4, - }) - c.Check(out, NotNil) - - out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandDerive, - Passphrase: "foo", - Salt: nil, - Keylen: 32, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4, - }) - c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ - Command: Argon2OutOfProcessCommandDerive, - ErrorType: Argon2OutOfProcessErrorRestartProcess, - ErrorString: "cannot run \"derive\" command: argon2 out-of-process handler has already been used to process a request - a new process should be started to handle another request", - }) -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestRunArgon2OutOfProcessRequestTimeRestartProcess(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandTime, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4, - }) - c.Check(out, NotNil) - - out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandTime, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4, - }) - c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ - Command: Argon2OutOfProcessCommandTime, - ErrorType: Argon2OutOfProcessErrorRestartProcess, - ErrorString: "cannot run \"time\" command: argon2 out-of-process handler has already been used to process a request - a new process should be started to handle another 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, nil), 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 * 1024, - 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, "b47ad96075d64cb92cdc7678e6bbb85f496da6e84d7ea05fbc0092dfb0ac3e13"), - }) - - c.Check(reqW.Close(), IsNil) - wg.Wait() -} - -type testOutOfProcessArgon2DeriveParams struct { - watchdogMonitor Argon2OutOfProcessWatchdogMonitor - watchdogHandler string - 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(), params.watchdogHandler), nil - }, params.watchdogMonitor) - key, err := kdf.Derive(params.passphrase, params.salt, params.mode, params.params, params.keyLen) - c.Check(err, IsNil) - c.Check(key, DeepEquals, params.expectedKey) - c.Logf("%x", key) -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2Derive(c *C) { - s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - watchdogMonitor: DefaultArgon2OutOfProcessWatchdogMonitor(50*time.Millisecond, 50*time.Millisecond), - watchdogHandler: "hmac-sha256", - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "b47ad96075d64cb92cdc7678e6bbb85f496da6e84d7ea05fbc0092dfb0ac3e13"), - }) -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentPassphrase(c *C) { - s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - watchdogMonitor: DefaultArgon2OutOfProcessWatchdogMonitor(50*time.Millisecond, 50*time.Millisecond), - watchdogHandler: "hmac-sha256", - passphrase: "bar", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "e5081bdbb5dc709ecd789ad6da76ce6c49d2bc3b958dda4a93c6b4140def877e"), - }) -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentSalt(c *C) { - s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - watchdogMonitor: DefaultArgon2OutOfProcessWatchdogMonitor(50*time.Millisecond, 50*time.Millisecond), - watchdogHandler: "hmac-sha256", - passphrase: "foo", - salt: []byte("zyxwtsrqponmlkjihgfedcba987654"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "bd962af1e81debad7966d3c0ca1dd9398dc231a3c25c96de54a1df97233d1a49"), - }) -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentMode(c *C) { - s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - watchdogMonitor: DefaultArgon2OutOfProcessWatchdogMonitor(50*time.Millisecond, 50*time.Millisecond), - watchdogHandler: "hmac-sha256", - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2i, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "0d781d62896d7bb71830251af01be0323f2006770beb917e62a2ea3330693625"), - }) -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentParams(c *C) { - s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - watchdogMonitor: DefaultArgon2OutOfProcessWatchdogMonitor(50*time.Millisecond, 50*time.Millisecond), - watchdogHandler: "hmac-sha256", - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 48, - MemoryKiB: 64 * 1024, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "ba935d605f3f021c6cad26c8e2c0316fcc23814b2aa580e33e0ddb040692fb77"), - }) -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveLongDuration(c *C) { - s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - watchdogMonitor: DefaultArgon2OutOfProcessWatchdogMonitor(50*time.Millisecond, 50*time.Millisecond), - watchdogHandler: "hmac-sha256", - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 100, - MemoryKiB: 512 * 1024, - Threads: 4}, - keyLen: 32, - expectedKey: testutil.DecodeHexString(c, "f3dc0bc830b9530adc647b136765b4266a41a62d90b9ce6b7a784b91b1566ab5"), - }) -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2DeriveDifferentKeylen(c *C) { - s.testOutOfProcessArgon2Derive(c, &testOutOfProcessArgon2DeriveParams{ - watchdogMonitor: DefaultArgon2OutOfProcessWatchdogMonitor(50*time.Millisecond, 50*time.Millisecond), - watchdogHandler: "hmac-sha256", - passphrase: "foo", - salt: []byte("0123456789abcdefghijklmnopqrstuv"), - mode: Argon2id, - params: &Argon2CostParams{ - Time: 4, - MemoryKiB: 32 * 1024, - Threads: 4}, - keyLen: 64, - expectedKey: testutil.DecodeHexString(c, "385251574d5dfa3c25eb5fa2ad99f74cba39c284a16999b2d8e6908ad2304225e1f706dc860867179759ca058c9e0b961f6a4ec88f0eb38ba825d655bf892116"), - }) -} - -func (s *argon2OutOfProcessSupportSuiteExpensive) TestOutOfProcessArgon2Time(c *C) { - kdf := NewOutOfProcessArgon2KDF(func() (*exec.Cmd, error) { - return exec.Command(s.runArgon2HandlerPath(), "hmac-sha256"), nil - }, DefaultArgon2OutOfProcessWatchdogMonitor(50*time.Millisecond, 50*time.Millisecond)) - - 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 8cfc9d36..7d1f5841 100644 --- a/argon2_test.go +++ b/argon2_test.go @@ -298,12 +298,6 @@ func (s *argon2Suite) TestModeConstants(c *C) { c.Check(Argon2id, Equals, Argon2Mode(argon2.ModeID)) } -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{} var _ = Suite(&argon2Expensive{}) diff --git a/cmd/run_argon2/main.go b/cmd/run_argon2/main.go index c640d5d2..bb9436d4 100644 --- a/cmd/run_argon2/main.go +++ b/cmd/run_argon2/main.go @@ -28,24 +28,26 @@ import ( "os" "github.com/snapcore/secboot" + "github.com/snapcore/secboot/internal/paths" ) func run() error { - if len(os.Args) < 2 { - return errors.New("usage: echo | run_argon2 []") + if len(os.Args) < 3 { + return errors.New("usage: echo | run_argon2 []") } - secboot.SetIsArgon2HandlerProcess() + + paths.Argon2OutOfProcessHandlerSystemLockPath = os.Args[1] var watchdog secboot.Argon2OutOfProcessWatchdogHandler - switch os.Args[1] { + switch os.Args[2] { case "none": watchdog = secboot.NoArgon2OutOfProcessWatchdogHandler() case "hmac": - if len(os.Args) != 3 { + if len(os.Args) != 4 { return errors.New("usage: echo | run_argon2 hmac ") } var alg crypto.Hash - switch os.Args[2] { + switch os.Args[3] { case "sha1": alg = crypto.SHA1 case "sha224": @@ -57,7 +59,7 @@ func run() error { case "sha512": alg = crypto.SHA512 default: - return fmt.Errorf("unrecognized HMAC digest algorithm %q", os.Args[2]) + return fmt.Errorf("unrecognized HMAC digest algorithm %q", os.Args[3]) } watchdog = secboot.HMACArgon2OutOfProcessWatchdogHandler(alg) } diff --git a/export_test.go b/export_test.go index 95924214..c055db9a 100644 --- a/export_test.go +++ b/export_test.go @@ -1,3 +1,4 @@ +// go:build test // -*- Mode: Go; indent-tabs-mode: t -*- /* @@ -28,6 +29,7 @@ import ( "github.com/snapcore/secboot/internal/luks2" "github.com/snapcore/secboot/internal/luksview" + "github.com/snapcore/secboot/internal/paths" ) const ( @@ -35,15 +37,15 @@ const ( ) var ( - GlobalArgon2KDF = argon2KDF - UnmarshalV1KeyPayload = unmarshalV1KeyPayload - UnmarshalProtectedKeys = unmarshalProtectedKeys + AcquireArgon2OutOfProcessHandlerSystemLock = acquireArgon2OutOfProcessHandlerSystemLock + ErrArgon2OutOfProcessHandlerSystemLockTimeout = errArgon2OutOfProcessHandlerSystemLockTimeout + UnmarshalV1KeyPayload = unmarshalV1KeyPayload + UnmarshalProtectedKeys = unmarshalProtectedKeys ) type ( - Argon2OutOfProcessHandler = argon2OutOfProcessHandler - KdfParams = kdfParams - ProtectedKeys = protectedKeys + KdfParams = kdfParams + ProtectedKeys = protectedKeys ) func KDFOptionsKdfParams(o KDFOptions, keyLen uint32) (*KdfParams, error) { @@ -58,6 +60,22 @@ func (o *PBKDF2Options) KdfParams(keyLen uint32) (*KdfParams, error) { return o.kdfParams(keyLen) } +func MockArgon2OutOfProcessHandlerSystemLockPath(path string) (restore func()) { + orig := paths.Argon2OutOfProcessHandlerSystemLockPath + paths.Argon2OutOfProcessHandlerSystemLockPath = path + return func() { + paths.Argon2OutOfProcessHandlerSystemLockPath = orig + } +} + +func MockArgon2SysLockStderr(w io.Writer) (restore func()) { + orig := argon2SysLockStderr + argon2SysLockStderr = w + return func() { + argon2SysLockStderr = orig + } +} + func MockLUKS2Activate(fn func(string, string, []byte, int) error) (restore func()) { origActivate := luks2Activate luks2Activate = fn @@ -174,16 +192,8 @@ 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) +func ResetArgon2OutOfProcessHandlerStatus() { + atomic.StoreUint32(&argon2OutOfProcessHandlerStatus, inProcessArgon2KDFAvailable) runtime.GC() } diff --git a/internal/paths/paths.go b/internal/paths/paths.go index fc2f09f2..6510e847 100644 --- a/internal/paths/paths.go +++ b/internal/paths/paths.go @@ -19,4 +19,13 @@ package paths -var RunDir = "/run" +import "path/filepath" + +var ( + RunDir = "/run" + + // Argon2OutOfProcessHandlerSystemLockPath is the lock file path used to + // serialize KDF requests system-wide. All process's that use the system-wide + // lock participate in the lock/unlock contract described above. + Argon2OutOfProcessHandlerSystemLockPath = filepath.Join(RunDir, "snapd/argon2.lock") +) From a097997aa191878d72a818d310304dfbfad5ff38 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Mon, 2 Dec 2024 10:23:12 +0000 Subject: [PATCH 06/40] Fix a couplke of data races --- argon2_out_of_process_support_sync_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/argon2_out_of_process_support_sync_test.go b/argon2_out_of_process_support_sync_test.go index 095f4298..e1046996 100644 --- a/argon2_out_of_process_support_sync_test.go +++ b/argon2_out_of_process_support_sync_test.go @@ -111,7 +111,7 @@ func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfPro // Close our FD to free up the locking - it should loop and try again c.Check(f.Close(), IsNil) - wg.Done() + wg.Wait() } func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLockChangedInode(c *C) { @@ -146,5 +146,5 @@ func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfPro // Close our original FD to free up the locking - it should loop and try again c.Check(f.Close(), IsNil) - wg.Done() + wg.Wait() } From ef2c8e4898da96d8899da070af68a24abce2df6c Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Mon, 2 Dec 2024 11:38:30 +0000 Subject: [PATCH 07/40] argon2: increase timeout for github --- argon2_out_of_process_support_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 38a35b98..950cc47a 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -1164,7 +1164,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFTime(c *C) { } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelSerialized(c *C) { - kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 10*time.Second, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 10*time.Minute, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) params := &Argon2CostParams{ Time: 4, MemoryKiB: 2 * 1024 * 1024, From dce22007747374a9e9c0d2d373bbb20c11518c73 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Mon, 2 Dec 2024 12:30:49 +0000 Subject: [PATCH 08/40] reduce memory usage of tests that fail in github to see if they will pass --- argon2_out_of_process_support_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 950cc47a..4db95d5b 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -1164,10 +1164,10 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFTime(c *C) { } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelSerialized(c *C) { - kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 10*time.Minute, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 1*time.Minute, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) params := &Argon2CostParams{ Time: 4, - MemoryKiB: 2 * 1024 * 1024, + MemoryKiB: 512 * 1024, Threads: 4, } @@ -1177,13 +1177,13 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelSerial go func() { key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) c.Check(err, IsNil) - c.Check(key, DeepEquals, testutil.DecodeHexString(c, "d38bda36abedb509c77cbc032d80f8ebc2c1742f12a4dbedfe42a20c77db51f3")) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "08f295932cdf618ac5a085f177d621ec0d0a0d2a4a3ed4e471d67133cb875c6a")) wg.Done() }() go func() { key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) c.Check(err, IsNil) - c.Check(key, DeepEquals, testutil.DecodeHexString(c, "4f98f746990ccc53520a3096ce201e2935aac92649896c65fb5f947824f0032e")) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "1e75b6c1809f73f0127fffcf013241fe5476558b3a748e78e02638012bd1cc01")) wg.Done() }() wg.Wait() @@ -1193,7 +1193,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelTimeou kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 100*time.Millisecond, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) params := &Argon2CostParams{ Time: 4, - MemoryKiB: 2 * 1024 * 1024, + MemoryKiB: 512 * 1024, Threads: 4, } @@ -1203,7 +1203,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelTimeou go func() { key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) c.Check(err, IsNil) - c.Check(key, DeepEquals, testutil.DecodeHexString(c, "d38bda36abedb509c77cbc032d80f8ebc2c1742f12a4dbedfe42a20c77db51f3")) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "08f295932cdf618ac5a085f177d621ec0d0a0d2a4a3ed4e471d67133cb875c6a")) wg.Done() }() go func() { From 8f57b45a23971205c0ac7d72c7bbba9947b48e57 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Mon, 2 Dec 2024 21:00:28 +0000 Subject: [PATCH 09/40] argon2: add a missing comment --- argon2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argon2.go b/argon2.go index e2610ebe..40c6c16f 100644 --- a/argon2.go +++ b/argon2.go @@ -130,7 +130,7 @@ func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) { mode := o.Mode if mode == Argon2Default { - // + // Select the hybrid mode by default. mode = Argon2id } From 3c64825b4f87975d4fe64f037e65fc9b4554e359 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Mon, 2 Dec 2024 21:08:56 +0000 Subject: [PATCH 10/40] argon2: replace the use of sync.WaitGroup with a channel --- argon2_out_of_process_support.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 76681f52..d52de70b 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -30,7 +30,6 @@ import ( "io" "os" "os/exec" - "sync" "sync/atomic" "time" @@ -540,11 +539,10 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo // and we still want to be able to service watchdog requests on this routine whilst // it's running. Block the current routine until we know the new routine has started // so that the watchdog handler will fail if the new routine doesn't begin properly. - var startupWg sync.WaitGroup - startupWg.Add(1) // Mark the WaitGroup as waiting for a single event. + startupCh := make(chan struct{}) tmb.Go(func() error { - startupWg.Done() // Unblock the main routine. + startupCh <- struct{}{} // Unblock the main routine. // Run the KDF request. This performs a lot of checking of the supplied // request, so there's no need to repeat any of that here. @@ -575,7 +573,7 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo // Wait here for the KDF handler routine to startup. This should never fail to start-up, // but doing this blocks the processing of watchdog requests temporarily. - startupWg.Wait() + <-startupCh } } return tomb.ErrDying From 35fd6f1d5a8daecf9a4d3c01c8db23b198521a26 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Mon, 2 Dec 2024 22:18:44 +0000 Subject: [PATCH 11/40] argon2: reduce memory usage for github These tests keep passing locally and failing in github, and memory usage is the only thing I can think of that might be causing the failures. --- argon2_out_of_process_support_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 4db95d5b..403206cc 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -621,7 +621,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD }) } -func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcess2GB(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcess512MB(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "bar", @@ -629,12 +629,12 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProce Keylen: 32, Mode: Argon2id, Time: 4, - MemoryKiB: 2 * 1024 * 1024, + MemoryKiB: 512 * 1024, Threads: 4, }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, - Key: testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd"), + Key: testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0"), }) } @@ -873,7 +873,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc }) } -func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequest2GB(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequest512MB(c *C) { rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -882,7 +882,7 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2 Keylen: 32, Mode: Argon2id, Time: 4, - MemoryKiB: 2 * 1024 * 1024, + MemoryKiB: 512 * 1024, Threads: 4, }, wdHandler: HMACArgon2OutOfProcessWatchdogHandler(crypto.SHA256), @@ -891,7 +891,7 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2 c.Check(err, IsNil) c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, - Key: testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd"), + Key: testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0"), }) } From 8b5c9027e7102ccba1d1fa243350a3b1e8af7ea4 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Mon, 2 Dec 2024 22:29:06 +0000 Subject: [PATCH 12/40] reduce memory usage in a couple more argon2 tests Also abort WaitForAndRunArgon2OutOfProcessRequest on an error response, as the underlying RunArgon2OutOfProcessRequest function can't be called again anyway. --- argon2_out_of_process_support.go | 13 ++----- argon2_out_of_process_support_test.go | 56 +++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index d52de70b..c0765a3c 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -557,16 +557,9 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo return tomb.ErrDying } - if rsp.Err() != nil { - // We got an error response, which means that the KDF request was - // not handled. The error response has already been sent to the parent, - // so carry on processing requests by returning no error from this - // goroutine. - return nil - } - - // As we only handle a single successful request, mark the tomb as dying to - // begin its clean shutdown. + // As we only handle a single request, mark the tomb as dying to + // begin its clean shutdown. RunArgon2OutOfProcessRequest will only + // run a single time. tmb.Kill(nil) return tomb.ErrDying }) diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 403206cc..a7828c3b 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -459,6 +459,40 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessInvalid }) } +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveMoreThanOnceAfterErr(c *C) { + out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: nil, + Keylen: 32, + Mode: Argon2Mode("foo"), + Time: 4, + MemoryKiB: 32 * 10224, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + ErrorType: Argon2OutOfProcessErrorInvalidMode, + ErrorString: "mode cannot be \"foo\"", + }) + + out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + }) + c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + ErrorType: Argon2OutOfProcessErrorRestartProcess, + ErrorString: "this process cannot handle any more KDF requests", + }) +} + func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveMoreThanOnce(c *C) { out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -476,7 +510,14 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveM }) out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandDerive, + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, @@ -1107,28 +1148,29 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMode( c.Check(key, DeepEquals, testutil.DecodeHexString(c, "a02a0203ea0e5e9abe4006fc80d1aca26b0adc1f898214c4c61d31f90bd4d129")) } -func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GB(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive512MB(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) params := &Argon2CostParams{ Time: 4, - MemoryKiB: 2 * 1024 * 1024, + MemoryKiB: 512 * 1024, Threads: 4, } key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) c.Check(err, IsNil) - c.Check(key, DeepEquals, testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd")) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0")) + c.Logf("%x", key) } -func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GBDifferentWatchdogHMAC(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive512MBDifferentWatchdogHMAC(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha384"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA384, 100*time.Millisecond, 50*time.Millisecond)) params := &Argon2CostParams{ Time: 4, - MemoryKiB: 2 * 1024 * 1024, + MemoryKiB: 512 * 1024, Threads: 4, } key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) c.Check(err, IsNil) - c.Check(key, DeepEquals, testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd")) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0")) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveErr(c *C) { From 9d6d0e40cfdb9e10c325d5fb214c2fda7d0c0170 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Tue, 3 Dec 2024 12:18:07 +0000 Subject: [PATCH 13/40] Change the backoff time to 100ms and refactor the code to make the tests more reproducible --- argon2_out_of_process_support_sync.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/argon2_out_of_process_support_sync.go b/argon2_out_of_process_support_sync.go index ff692784..26ecfdc4 100644 --- a/argon2_out_of_process_support_sync.go +++ b/argon2_out_of_process_support_sync.go @@ -103,6 +103,7 @@ func acquireArgon2OutOfProcessHandlerSystemLock(timeout time.Duration) (release // Run a loop to try to acquire the lock. for { + skipBackoffCh := make(chan struct{}) // Don't wait 100ms before trying again if triedOnce { // If the loop has executed at least once, make sure that // the timeout hasn't expired. @@ -110,8 +111,10 @@ func acquireArgon2OutOfProcessHandlerSystemLock(timeout time.Duration) (release case <-timeoutTimer.C: // The timeout has expired. return nil, errArgon2OutOfProcessHandlerSystemLockTimeout - default: - // continue trying + case <-skipBackoffCh: + // continue trying without waiting + case <-time.NewTimer(100 * time.Millisecond).C: + // Wait for 100ms before trying again } } triedOnce = true @@ -146,15 +149,7 @@ func acquireArgon2OutOfProcessHandlerSystemLock(timeout time.Duration) (release // We failed to acquire the lock. if os.IsTimeout(err) { // The EWOULDBLOCK case. Someone else already has a lock on the - // file we have opened. Try again with a 10ms backoff time, - // unless the request timeout fires first. - select { - case <-time.NewTimer(10 * time.Millisecond).C: - // We can try again - case <-timeoutTimer.C: - // The timeout has expired - return nil, errArgon2OutOfProcessHandlerSystemLockTimeout - } + // file we have opened. Try again with a 100ms backoff time. continue } @@ -174,6 +169,7 @@ func acquireArgon2OutOfProcessHandlerSystemLock(timeout time.Duration) (release if os.IsNotExist(err) { // The lock file path no longer exists because it was unlinked by // another process. Try again immediately. + skipBackoffCh <- struct{}{} continue } @@ -198,6 +194,7 @@ func acquireArgon2OutOfProcessHandlerSystemLock(timeout time.Duration) (release // The inode that we have a lock on is not the same one that the lock file // path currently points to, so nothing is stopping another process from acquiring // a lock. We should try again immediately. + skipBackoffCh <- struct{}{} } if lockFile == nil { From d1f97b3e2dab5240b0d0e6629c2595b60e285fac Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Wed, 4 Dec 2024 14:58:31 +0000 Subject: [PATCH 14/40] close the channel to unblock the receiving routine --- argon2_out_of_process_support.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index c0765a3c..aa047cf8 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -542,7 +542,7 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo startupCh := make(chan struct{}) tmb.Go(func() error { - startupCh <- struct{}{} // Unblock the main routine. + close(startupCh) // Run the KDF request. This performs a lot of checking of the supplied // request, so there's no need to repeat any of that here. From fabe880acad30ff5357f7d2661910fbef34ce306 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Wed, 4 Dec 2024 14:59:23 +0000 Subject: [PATCH 15/40] complete a doc comment --- argon2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argon2.go b/argon2.go index 40c6c16f..8f4bac09 100644 --- a/argon2.go +++ b/argon2.go @@ -177,7 +177,7 @@ func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) { } if o.MemoryKiB != 0 { - // The memory cost has been spcified expli + // The memory cost has been specified explicitly benchmarkParams.MaxMemoryCostKiB = o.MemoryKiB // this is capped to 4GiB by internal/argon2. } if o.TargetDuration != 0 { From 0c869e813de6fe7da8cedf315c72324adeffe466 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Wed, 4 Dec 2024 20:09:13 +0000 Subject: [PATCH 16/40] Update x/crypto This updates the x/crypto repository to the latest version we possibly can whilst still maintaining compatibility with go 1.18. --- go.mod | 6 +++--- go.sum | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 720f623c..454dc0f6 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,11 @@ require ( github.com/canonical/go-tpm2 v1.7.6 github.com/canonical/tcglog-parser v0.0.0-20240924110432-d15eaf652981 github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85 - golang.org/x/crypto v0.21.0 - golang.org/x/sys v0.19.0 + golang.org/x/crypto v0.23.0 + golang.org/x/sys v0.21.0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c + gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 gopkg.in/yaml.v2 v2.3.0 maze.io/x/crypto v0.0.0-20190131090603-9b94c9afe066 ) @@ -25,5 +26,4 @@ require ( github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 // indirect golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/net v0.21.0 // indirect - gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect ) diff --git a/go.sum b/go.sum index 1ee5d1bc..93facb06 100644 --- a/go.sum +++ b/go.sum @@ -45,8 +45,8 @@ github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85/go.mod h1:Ab4TsNgVa go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -58,8 +58,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= From e18c47be121a36383cb503e6ea91e57d3a94ad6f Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Wed, 4 Dec 2024 20:12:02 +0000 Subject: [PATCH 17/40] Don't use json.Decoder.DisallowUnknownFields This may make it unnecessarily difficult to make changes to the request or response objects in the future without introducing a handshaking mechanism. --- argon2_out_of_process_support.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index aa047cf8..1242dcc5 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -478,7 +478,6 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo // eventually returning an error to the caller. var req *Argon2OutOfProcessRequest dec := json.NewDecoder(in) - dec.DisallowUnknownFields() if err := dec.Decode(&req); err != nil { // Decoding returned an error. if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { @@ -832,7 +831,6 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out // request channel - in general, the last response is the result of the KDF // operation which begins the tomb's dying process anyway. dec := json.NewDecoder(stdoutPipe) - dec.DisallowUnknownFields() var rsp *Argon2OutOfProcessResponse if err := dec.Decode(&rsp); err != nil { return fmt.Errorf("cannot decode response: %w", err) From 513d782f9b401723132e20fdf041045e0d581cc1 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 14:48:42 +0000 Subject: [PATCH 18/40] Fix various race conditions with the Argon2 remoting This makes the following changes to hopefully avoid some race conditions for the Argon2 remoting functionality: - The remote side only begins terminating now when the parent closes its end of the request channel, as opposed to also beginning termination on completion of a KDF request, which should avoid errors caused by an in-flight watchdog request. - None of the functions on the remote side automatically release the system-wide lock. They do provide a callback explicitly release it, but it's advised to let it be implicitly released on process exit. The example child binary used for testing (./cmd/run_argon2) uses implicit release now. --- argon2_out_of_process_support.go | 298 ++++++++++++---------- argon2_out_of_process_support_sync.go | 72 +++--- argon2_out_of_process_support_test.go | 344 +++++++++++++++++++------- cmd/run_argon2/main.go | 3 +- export_test.go | 7 - 5 files changed, 465 insertions(+), 259 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 1242dcc5..6d40ec74 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -30,7 +30,6 @@ import ( "io" "os" "os/exec" - "sync/atomic" "time" "gopkg.in/tomb.v2" @@ -94,16 +93,9 @@ const ( // the request was given a chance to start. Argon2OutOfProcessErrorKDFTimeout Argon2OutOfProcessErrorType = "timeout-error" - // Argon2OutOfProcessErrorRestartProcess means that this process has already processed a - // good KDF request, and the process should exit and be replaced by a new one. - Argon2OutOfProcessErrorRestartProcess Argon2OutOfProcessErrorType = "restart-process" - - // Argon2OutOfProcessErrorKDFUnexpected means that an unexpected error occurred when - // running the actual KDF operation. - Argon2OutOfProcessErrorKDFUnexpected Argon2OutOfProcessErrorType = "unexpected-kdf-error" - // Argon2OutOfProcessErrorUnexpected means that an unexpected error occurred without - // a more specific error type. + // a more specific error type, eg, an unexpected failure to acquire the system-wide + // lock or an unexpected error returned from the underlying Argon2 KDF implementation. Argon2OutOfProcessErrorUnexpected Argon2OutOfProcessErrorType = "unexpected-error" ) @@ -179,27 +171,18 @@ func (e *Argon2OutOfProcessResponseCommandInvalidError) Error() string { return fmt.Sprintf("received a response with an unexpected command value (got %q, expected %q)", e.Response, e.Expected) } -const ( - inProcessArgon2KDFAvailable uint32 = 0 - inProcessArgon2KDFUsed uint32 = 1 -) - -var ( - // argon2OutOfProcessHandlerStatus indicates whether this process has handled - // a KDF request on behalf of another process. Process's should only handle a - // single request, and then reject further requests. - argon2OutOfProcessHandlerStatus uint32 = inProcessArgon2KDFAvailable - - // ErrArgon2OutOfProcessHandlerUnavailable is returned directly from - // WaitForAndRunArgon2OutOfProcessRequest if this process is not available - // to handle anymore KDF requests. It can also be returned as the error - // string in a Argon2OutOfProcessResponse - ErrArgon2OutOfProcessHandlerUnavailable = errors.New("this process cannot handle any more KDF requests") -) - -// RunArgon2OutOfProcessRequest runs the specified argon2 request, and returns a response. This -// function will only handle one argon2 request in a process. Subsequent calls in the same process -// after a previous successful call will result in an error response being returned. +// RunArgon2OutOfProcessRequest runs the specified Argon2 request, and returns a response. +// +// In general, this is intended to be executed once in a process, before the process is discarded. +// +// Note that Argon2 requests are serialized using a system-wide lock, which this function does not +// explicitly release. If the lock is acquired, it returns a callback that the caller may choose +// to execute in order to explicitly release the lock, or the caller can just leave it to be +// implicitly released on process exit. If the lock is explicitly released, the caller must be +// sure that the large amount of memory allocated for the Argon2 operation has been freed and +// returned back to the OS, else this defeats the point of having a system-wide lock (to avoid +// having multiple processes with high physical memory requirements running at the same time). If +// the lock wasn't acquired, no release callback will be returned. // // This is quite a low-level function, suitable for implementations that want to manage their own // transport. In general, implementations will use [WaitForAndRunArgon2OutOfProcessRequest]. @@ -213,15 +196,7 @@ var ( // Unfortunately, there is no way to interrupt this function once the key derivation is in progress, // because the low-level crypto library does not support this. This feature may be desired in the // future, which might require replacing the existing library we use for Argon2. -func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2OutOfProcessResponse { - if !atomic.CompareAndSwapUint32(&argon2OutOfProcessHandlerStatus, inProcessArgon2KDFAvailable, inProcessArgon2KDFUsed) { - return &Argon2OutOfProcessResponse{ - Command: request.Command, - ErrorType: Argon2OutOfProcessErrorRestartProcess, - ErrorString: ErrArgon2OutOfProcessHandlerUnavailable.Error(), - } - } - +func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) (response *Argon2OutOfProcessResponse, lockRelease func()) { // Perform checks of arguments that are common to call requests switch request.Mode { case Argon2id, Argon2i: @@ -231,7 +206,7 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out Command: request.Command, ErrorType: Argon2OutOfProcessErrorInvalidMode, ErrorString: fmt.Sprintf("mode cannot be %q", string(request.Mode)), - } + }, nil } costParams := &Argon2CostParams{ @@ -244,14 +219,14 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out Command: request.Command, ErrorType: Argon2OutOfProcessErrorInvalidTimeCost, ErrorString: "time cannot be zero", - } + }, nil } if costParams.Threads == 0 { return &Argon2OutOfProcessResponse{ Command: request.Command, ErrorType: Argon2OutOfProcessErrorInvalidThreads, ErrorString: "threads cannot be zero", - } + }, nil } if len(request.WatchdogChallenge) > 0 { @@ -262,10 +237,49 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out Command: request.Command, ErrorType: Argon2OutOfProcessErrorUnexpectedInput, ErrorString: "invalid watchdog challenge: cannot service a watchdog", + }, nil + } + + // Do some last minute, command-specific validation + switch request.Command { + case Argon2OutOfProcessCommandDerive: + // ok + case Argon2OutOfProcessCommandTime: + // Make sure that redundant parameters haven't been set. + if len(request.Passphrase) > 0 { + return &Argon2OutOfProcessResponse{ + Command: request.Command, + ErrorType: Argon2OutOfProcessErrorUnexpectedInput, + ErrorString: "cannot supply passphrase for \"time\" command", + }, nil + } + if len(request.Salt) > 0 { + return &Argon2OutOfProcessResponse{ + Command: request.Command, + ErrorType: Argon2OutOfProcessErrorUnexpectedInput, + ErrorString: "cannot supply salt for \"time\" command", + }, nil } + if request.Keylen > 0 { + return &Argon2OutOfProcessResponse{ + Command: request.Command, + ErrorType: Argon2OutOfProcessErrorUnexpectedInput, + ErrorString: "cannot supply keylen for \"time\" command", + }, nil + } + default: + // This is an unrecognized commmand. This includes watchdog requests, which must be handled by + // a higher level function. + return &Argon2OutOfProcessResponse{ + Command: request.Command, + ErrorType: Argon2OutOfProcessErrorInvalidCommand, + ErrorString: fmt.Sprintf("command cannot be %q", string(request.Command)), + }, nil } - release, err := acquireArgon2OutOfProcessHandlerSystemLock(request.Timeout) + // Acquire the system-wide lock. + var err error + lockRelease, err = acquireArgon2OutOfProcessHandlerSystemLock(request.Timeout) if err != nil { errorType := Argon2OutOfProcessErrorUnexpected if errors.Is(err, errArgon2OutOfProcessHandlerSystemLockTimeout) { @@ -275,10 +289,10 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out Command: request.Command, ErrorType: errorType, ErrorString: fmt.Sprintf("cannot acquire argon2 system lock: %v", err), - } + }, nil } - defer release() + // We have the system-wide lock - execute the command switch request.Command { case Argon2OutOfProcessCommandDerive: // Perform key derivation @@ -286,59 +300,30 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) *Argon2Out if err != nil { return &Argon2OutOfProcessResponse{ Command: request.Command, - ErrorType: Argon2OutOfProcessErrorKDFUnexpected, + ErrorType: Argon2OutOfProcessErrorUnexpected, ErrorString: fmt.Sprintf("cannot run derive command: %v", err), - } + }, lockRelease } return &Argon2OutOfProcessResponse{ Command: request.Command, Key: key, - } + }, lockRelease case Argon2OutOfProcessCommandTime: - // Make sure that redundant parameters haven't been set. - 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", - } - } - // Perform timing of the supplied cost parameters. duration, err := InProcessArgon2KDF.Time(request.Mode, costParams) if err != nil { return &Argon2OutOfProcessResponse{ Command: request.Command, - ErrorType: Argon2OutOfProcessErrorKDFUnexpected, + ErrorType: Argon2OutOfProcessErrorUnexpected, ErrorString: fmt.Sprintf("cannot run time command: %v", err), - } + }, lockRelease } return &Argon2OutOfProcessResponse{ Command: request.Command, Duration: duration, - } + }, lockRelease default: - // This is an unrecognized commmand. This includes watchdog requests, which must be handled by - // a higher level function. - return &Argon2OutOfProcessResponse{ - Command: request.Command, - ErrorType: Argon2OutOfProcessErrorInvalidCommand, - ErrorString: fmt.Sprintf("command cannot be %q", string(request.Command)), - } + panic("not reachable") } } @@ -403,30 +388,41 @@ func NoArgon2OutOfProcessWatchdogHandler() Argon2OutOfProcessWatchdogHandler { // 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 [NewOutOfProcessArgon2KDF] on the parent side. // -// This function will only handle one argon2 request from the supplied io.Reader in a process. Subsequent -// requests to the same process after a previous successful call will result in an error response being -// returned via the io.Writer. -// // This function will service watchdog requests from the parent process if a watchdog handler is supplied. // If supplied, it must match the corresponding monitor in the parent process. If not supplied, the default // [NoArgon2OutOfProcessWatchdogHandler] will be used. // +// In general, this is intended to be executed once in a process, before the process is discarded. +// +// Note that Argon2 requests are serialized using a system-wide lock, which this function does not +// explicitly release. If the lock is acquired, it returns a callback that the caller may choose +// to execute in order to explicitly release the lock, or the caller can just leave it to be +// implicitly released on process exit. If the lock is explicitly released, the caller must be +// sure that the large amount of memory allocated for the Argon2 operation has been freed and +// returned back to the OS, else this defeats the point of having a system-wide lock (to avoid +// having multiple processes with high physical memory requirements running at the same time). If +// the lock wasn't acquired, no release callback will be returned. +// +// This function may return a callback to release the system wide lock even if an error is returned, +// which will happen if an error occurs after the lock is acquired. +// // Unfortunately, KDF requests cannot be interrupted once they have started because the low-level crypto // library does not provide this functionality, although watchdog requests can still be serviced to provide // assurance that a response will be received as long as the crypto algorithm completes. The ability to // interrupt a KDF request in the future may be desired, although it may require replacing the existing // library we use for Argon2. // -// Most errors are sent back to the parent process via the supplied io.Writer. In some cases, errors -// returned from go routines that are created during the handling of a request may be returned directly -// from this function to be handled by the current process. +// Most errors are sent back to the parent process via the supplied io.Writer. In some limited cases, +// errors returned from goroutines that are created during the handling of a request may be returned +// directly from this function to be handled by the current process. These limited examples are where +// the function receives input it can't decode, where a response cannot be encoded and sent to the parent, +// if the watchdog handler function returns an error, or if the supplied response channel returns an error +// when closing. // -// Note that this function won't return until the supplied io.Reader is closed by the parent. -func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdog Argon2OutOfProcessWatchdogHandler) error { - if atomic.LoadUint32(&argon2OutOfProcessHandlerStatus) == inProcessArgon2KDFUsed { - return ErrArgon2OutOfProcessHandlerUnavailable - } - +// Note that this function won't return until the supplied io.Reader is closed by the parent, or an internal +// error occurs in one of the goroutines created by this function. It will close the supplied io.WriteCloser +// before returning. +func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.WriteCloser, watchdog Argon2OutOfProcessWatchdogHandler) (lockRelease func(), err error) { if watchdog == nil { watchdog = NoArgon2OutOfProcessWatchdogHandler() } @@ -437,6 +433,10 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo // dedicated output routine which serializes the response to the supplied io.Writer. rspChan := make(chan *Argon2OutOfProcessResponse) + // lockReleaseChan is the channel from which this routine receives a callback to explicitly + // relinquish the system-wide lock. It's buffered because it's read by the current goroutine. + lockReleaseChan := make(chan func(), 1) + // Spin up a routine for receiving requests from the supplied io.Reader. tmb.Go(func() error { // Also spin-up the routine for sending outgoing responses that are generated internally. @@ -459,6 +459,11 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo // being returned to the caller. enc := json.NewEncoder(out) if err := enc.Encode(rsp); err != nil { + if errors.Is(err, os.ErrClosed) && !tmb.Alive() { + // We close our side of the response channel when the + // tomb enters a dying state, so this error is expected. + return nil + } return fmt.Errorf("cannot encode response: %w", err) } case <-tmb.Dying(): @@ -468,6 +473,16 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo return tomb.ErrDying }) + // Spin up a routine which just waits for the tomb to enter a dying state, whether + // requested by the parent by it closing its end of the request channel, or because + // some other error happened, and then close our side of the response channel. The + // netpoller will wake up any pending writers, unblocking any in-progress calls to + // out.Write in the json encoder. + tmb.Go(func() error { + <-tmb.Dying() + return out.Close() + }) + // Run a loop for receiving and processing incoming requests from the io.Reader as // long as the tomb remains alive. for tmb.Alive() { @@ -481,26 +496,17 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo if err := dec.Decode(&req); err != nil { // Decoding returned an error. if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { - // The parent has closed their end of the io.Reader, so kill - // the tomb normally to begin the process of dying. + // The parent has closed their end of the io.Reader, which + // is our signal to return, so kill the tomb normally to + // begin the process of dying. tmb.Kill(nil) break // Break out of the request processing loop } - // We failed to decode an incoming request for an unknown reason. We try returning an - // error response to the parent - note that we can't set the command field here - // because we have no idea what it is. - rsp := &Argon2OutOfProcessResponse{ - ErrorType: Argon2OutOfProcessErrorUnexpectedInput, - ErrorString: fmt.Sprintf("cannot decode request: %v", err), - } - select { - case rspChan <- rsp: // Unbuffered channel, but read end is always there unless the tomb is dying. - case <-tmb.Dying(): - // The tomb began dying before the response was sent. - } - - // Break out of the request processing loop. + // We failed to decode an incoming request for an unknown reason. We'll + // handle this by putting the tomb into a dying state and returning an + // error to the caller. + tmb.Kill(fmt.Errorf("cannot decode request: %w", err)) break } @@ -545,7 +551,20 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo // Run the KDF request. This performs a lot of checking of the supplied // request, so there's no need to repeat any of that here. - rsp := RunArgon2OutOfProcessRequest(req) + rsp, release := RunArgon2OutOfProcessRequest(req) + + if release != nil { + // This channel is buffered with a size of 1 so that it can be obtained by + // the calling goroutine when the tomb has died. Therefore, it should never + // block. Although theoretically this process could be sent more than one + // request (although the parent side implementation in secboot won't do this), + // subsequent calls to RunArgon2OutOfProcessRequest eventually all timeout, + // unable to acquire another system-wide lock (it will be acquired on the first + // request and the release callback buffered in the channel already). In this + // case, there will be no new release callback so the channel should always be + // empty at this point. + lockReleaseChan <- release + } // Send the response. select { @@ -556,11 +575,12 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo return tomb.ErrDying } - // As we only handle a single request, mark the tomb as dying to - // begin its clean shutdown. RunArgon2OutOfProcessRequest will only - // run a single time. - tmb.Kill(nil) - return tomb.ErrDying + // Although we're only meant to handle a single request, and we've + // done that now, don't put the tomb into a dying state yet. Wait + // for the parent process to close its end of the request channel + // first to avoid race conditions with an in-flight watchdog request + // from the parent process. + return nil }) // Wait here for the KDF handler routine to startup. This should never fail to start-up, @@ -572,7 +592,16 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.Writer, watchdo }) // Wait here for the tomb to die and return the first error that occurred. - return tmb.Wait() + err = tmb.Wait() + + select { + case lockRelease = <-lockReleaseChan: + // The system-wide lock was acquired and we have a release callback for it. + default: + // The system-wide lock was never acquired + } + + return lockRelease, err } // Argon2OutOfProcessWatchdogMonitor defines the behaviour of a watchdog monitor @@ -733,6 +762,22 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out return nil, fmt.Errorf("cannot start handler process: %w", err) } + defer func() { + // The remote process may release the system-wide lock implicitly on process + // termination. In this case, we make an attempt to cleanup the lock-file on + // behalf of the remote process. This isn't strictly necessary, which is why + // we set the timeout to 0 - we don't want to wait if someone else has already + // managed to grab the lock and we don't want to delay the return of the response + // from this function. + release, err := acquireArgon2OutOfProcessHandlerSystemLock(0) + if err != nil { + // We didn't acquire the lock with a single attempt, so never mind. + return + } + // We have the lock. Explicitly releasing it again will unlink the lock file. + release() + }() + var actualRsp *Argon2OutOfProcessResponse // The response to return to the caller exitWaitCh := make(chan struct{}) // A channel which signals successful exit of the child process when closed tmb := new(tomb.Tomb) // To track all goroutines @@ -872,22 +917,17 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out return tomb.ErrDying }) - // Wait here until the tomb enters a dying state + // Wait here until the tomb enters a dying state. <-tmb.Dying() - // Closing the stdin pipe is necessary to unblock WaitForAndRunArgon2OutOfProcessRequest - // on the remote side, if it is blocked in a read. + // [exec.Cmd.Wait] will close parent FDs for us once the process has exitted. However, closing + // the stdin pipe is necessary to unblock WaitForAndRunArgon2OutOfProcessRequest on the remote + // side, if it is blocked in a read. We don't do the same for stdoutPipe (the request channel) + // because the remote process is expected to close its end of it, freeing up any goroutines + // that are blocked on writing a response to us. if err := stdinPipe.Close(); err != nil { return nil, fmt.Errorf("cannot close stdin pipe: %w", err) } - // The go documentation says this should be unnecessary because it will be closed - // by cmd.Wait once the command has exitted. But it's still possible for - // WaitForAndRunArgon2OutOfProcessRequest to be blocked on a write to us, which - // will never complete once that the loop that handles responses has terminated. - // Therefore, we're going to close it explicitly. - if err := stdoutPipe.Close(); err != nil { - return nil, fmt.Errorf("cannot close stdout pipe: %w", err) - } // We can wait for the remote process to exit now. if err := cmd.Wait(); err != nil { diff --git a/argon2_out_of_process_support_sync.go b/argon2_out_of_process_support_sync.go index 26ecfdc4..dd04945a 100644 --- a/argon2_out_of_process_support_sync.go +++ b/argon2_out_of_process_support_sync.go @@ -30,41 +30,54 @@ import ( "golang.org/x/sys/unix" ) -// Due to the amount of memory a KDF request comsumes, we try to serialize the -// requests system-wide to avoid triggering memory pressure. The system-wide +// Due to the amount of memory an Argon2 KDF request comsumes, we try to serialize +// the requests system-wide to avoid triggering memory pressure. The system-wide // lock is represented by a file in /run. A process must open this file and // hold an exclusive advisory lock on the open file descriptor before processing -// a KDF request. It must maintain this exclusive lock and keep the file descriptor -// open until the KDF request completes, at which point the file can be unlinked -// (still inside the exlusive lock) before closing the file descriptor. +// an Argon2 KDF request. It must maintain this exclusive lock until the request +// completes, and ideally should maintain hold of the lock until the process has +// handed the memory the operation consumed back to the operating system, which is +// only guaranteed to happen once the program exits or until after a call to +// [runtime.GC]. // -// Care must be taken wrt race conditions between other process when removing the -// system-wide lock file. Eg, in between opening the system-wide lock file and -// obtaining an exlusive lock on the opened file descriptor, it's possible that -// a lock holder in another process releases its lock on the same file and -// unlinks the system-wide lock file that we opened. In this case, this process -// doesn't really hold the system-wide lock as nothing prevents another process from -// taking another one by creating a new file. It's also possible that in-between -// opening the system-wide lock file and obtaining an exclusive lock on the opened -// file descriptor, another lock holder released its lock on the same file (unlinking -// the system-wide lock file that we opened), and another process has since created -// a new file in preparation for taking its own lock. Again, we don't really hold the +// This means that processes that are handling Argon2 KDF requests should generally +// not explicitly release the lock if they actually execute the KDF, and should just +// let it be released implicitly by calling [os.Exit] and the file descriptor being +// closed as part of normal process termination. +// +// Implicit release of the lock does leave a lock file in /run. If so desired, this +// can be removed by the parent process, although the parent process would need to +// temporarily acquire the lock to do this (it could do this with a timeout of 0 to +// avoid any delays). +// +// Note the comments below wrt race conditions when removing the lock file, which +// explains why the file can only be removed by the current lock holder. +// +// Care must be taken wrt race conditions between other process or goroutines when +// removing the system-wide lock file. Eg, in between opening the system-wide lock +// file and obtaining an exlusive lock on the opened file descriptor, it's possible +// that another lock holder in another process or goroutine explicitly releases its +// lock on the same file and unlinks the system-wide lock file that we opened. In this +// case, the calling goroutine doesn't really hold the system-wide lock as nothing +// prevents another process or goroutine from taking another one by creating a new file. +// It's also possible that in-between opening the system-wide lock file and obtaining +// an exclusive lock on the opened file descriptor, another lock holder explicitly +// released its lock on the same file (unlinking the system-wide lock file that we +// opened), and another process or goroutine has since created a new file in preparation +// for taking its own lock. Again, the calling goroutine doesn't really hold the // system-wide lock in this case because we don't hold a lock on the file that the lock -// file path currently points to, so nothing prevents multiple processes from thinking -// that they have taken the lock. Both of these cases can be tested for by doing the -// following after acquiring an exclusive advisory lock on the open file descriptor for -// the system lock file: +// file path currently points to, so nothing prevents multiple processes or goroutines +// from thinking that they have taken the lock. Both of these cases can be tested for by +// doing the following after acquiring an exclusive advisory lock on the open file +// descriptor for the system lock file: // - Ensure that there is still a file at the system-wide lock file path. // - Ensure the inode that the system-wide lock file path currently points to matches // the inode that we acquired an exclusive lock on. -// If either of these checks fail, this process does not own the system-wide lock and -// another attempt must be made to attempt to acquire it. +// If either of these checks fail, the calling gorouitine does not own the system-wide +// lock and another attempt must be made to attempt to acquire it. // // For this to work reliably and without race conditions, the system-wide lock file must -// only be unlinked by the current lock holder. Unlinking the current file would normally -// happen when a process relinquishes the lock, but it's also ok to leave a stale lock -// file around - in this case, another process will just re-use it rather than creating -// a new one. +// only be unlinked by the current lock holder. var ( argon2SysLockStderr io.Writer = os.Stderr @@ -73,11 +86,12 @@ var ( ) // acquireArgon2OutOfProcessHandlerSystemLock acquires the system-wide lock -// for serializing Argon2 execution system-wide via this package. If the +// for serializing Argon2 execution system-wide via this package. If the // function returns with an error, then the lock was not acquired. If the // function returns wthout an error, the returned callback can be used to -// release the lock (note that the lock will be relinquished automatically if -// the process exits too). +// explicitly release the lock (note that the lock will be relinquished +// automatically when the process exits too, although the lock-file won't +// be unlinked). // // The specified timeout determines how long this function will wait before // aborting its attempt to acquire the lock. If set to 0, the function will diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index a7828c3b..09a11105 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -45,18 +45,29 @@ import ( // argon2OutOfProcessHandlerSupportMixin provides capabilities shared // between suites that test the remote side of out-of-process Argon2 components. type argon2OutOfProcessHandlerSupportMixin struct { + lockPath string restoreLockPath func() } func (s *argon2OutOfProcessHandlerSupportMixin) SetUpTest(c *C) { - s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(filepath.Join(c.MkDir(), "argon2.lock")) + s.lockPath = filepath.Join(c.MkDir(), "argon2.lock") + s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(s.lockPath) } func (s *argon2OutOfProcessHandlerSupportMixin) TearDownTest(c *C) { if s.restoreLockPath != nil { s.restoreLockPath() } - ResetArgon2OutOfProcessHandlerStatus() + runtime.GC() +} + +func (s *argon2OutOfProcessHandlerSupportMixin) checkNoLockFile(c *C) { + f, err := os.Open(s.lockPath) + c.Check(os.IsNotExist(err), testutil.IsTrue) + if err != nil { + return + } + f.Close() } type testWaitForAndRunArgon2OutOfProcessRequestParams struct { @@ -65,13 +76,14 @@ type testWaitForAndRunArgon2OutOfProcessRequestParams struct { wdMonitor Argon2OutOfProcessWatchdogMonitor } -func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProcessRequest(c *C, params *testWaitForAndRunArgon2OutOfProcessRequestParams) (*Argon2OutOfProcessResponse, error) { +func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProcessRequest(c *C, params *testWaitForAndRunArgon2OutOfProcessRequestParams) (rsp *Argon2OutOfProcessResponse, release func(), err error) { // Create 2 pipes to communicate with WaitForAndRunArgon2OutOfProcessRequest reqR, reqW := io.Pipe() rspR, rspW := io.Pipe() - var actualRsp *Argon2OutOfProcessResponse // The response from the test function - tmb := new(tomb.Tomb) // The tomb for tracking goroutines + rspChan := make(chan *Argon2OutOfProcessResponse, 1) // A buffered channel to receive the response from the test function + releaseChan := make(chan func(), 1) // A buffered channel to receive the lock release callback from the test function + tmb := new(tomb.Tomb) // The tomb for tracking goroutines // Spin up a goroutine to bootstrap the test setup and then process responses from the // test function. I'm not sure how thread safe the test library is, so we avoid doing @@ -84,11 +96,15 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc // returned from the test function will propagate out of the tomb // and will be checked on the main test goroutine. tmb.Go(func() (err error) { - return WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW, params.wdHandler) + release, err := WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW, params.wdHandler) + releaseChan <- release + return err }) - // reqChan receives requests and then serializes them to the test function on a - // dedicated goroutine. + // The rest of the code in this function mocks the parent side. + + // reqChan receives requests and then serializes them on a dedicated + // goroutine to the request channel which is connected to the test function. reqChan := make(chan *Argon2OutOfProcessRequest) // wdRspChan is used by the response processing loop to send watchdog @@ -114,7 +130,7 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc tmb.Go(func() error { watchdog := params.wdMonitor if watchdog == nil { - // Copy the default behaviour for WaitForAndRunArgon2OutOfProcessRequest + // Copy the default behaviour for NewOutOfProcessArgonKDF. watchdog = NoArgon2OutOfProcessWatchdogMonitor() } err := watchdog(tmb, reqChan, wdRspChan) @@ -136,17 +152,11 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc // Process responses for tmb.Alive() { dec := json.NewDecoder(rspR) - dec.DisallowUnknownFields() var rsp *Argon2OutOfProcessResponse if err := dec.Decode(&rsp); err != nil { return fmt.Errorf("cannot decode response: %w", err) } - if rsp.Err() != nil { - // Just return an error response. - return rsp.Err() - } - switch rsp.Command { case Argon2OutOfProcessCommandWatchdog: // Direct watchdog responses to wdRspChan so they can be picked up by @@ -157,7 +167,8 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc // This loop will no longer iterate } default: - actualRsp = rsp + // We got a response - begin the process of dying. + rspChan <- rsp tmb.Kill(nil) // This loop will no longer iterate } @@ -168,21 +179,95 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc // Wait for the tomb to begin dying <-tmb.Dying() - // Close our end of the pipes supplied to the test function, as - // a real parent process would. + // Closing our end of the request channel supplied to the test function, as + // a real parent process would, should be sufficient to begin termination + // of the test function (WaitForAndRunArgon2OutOfProcessRequest) on the remote + // side. + // + // Note that the test will block indefinitely on waiting for the tomb to fully + // die if this doesn't work properly, which isn't ideal. I don't think there's + // a way to mitigate that c.Check(reqW.Close(), IsNil) - c.Check(rspR.Close(), IsNil) - // Wait for everything to die, hopefully successfully - if err := tmb.Wait(); err != nil { - return nil, err + // Wait for everything to die, hopefully successfully. + err = tmb.Wait() + + // Make sure that the test function closed its side of the response channel, as + // it's expected to do. We do this by attemping to read from the parent side, + // which should immediately return io.EOF. We do this in a way that won't cause + // the test to block indefinitely if the test function misbehaves and doesn't do + // this. + cleanupTmb := new(tomb.Tomb) + + // Spin up a routine that will timeout and unblock the test if the test function + // didn't close its side of the response channel. + cleanupTmb.Go(func() error { + // Spin up a routine to attempt to read from the response channel. + // It should immediately receive io.EOF, putting the tomb into a + // dying state with this error. + cleanupTmb.Go(func() error { + var data [1]byte + _, err := rspR.Read(data[:]) + // This should be io.EOF and should put the tomb into a + // dying state + return err + }) + + // Spin up a routine that will unblock the goroutine that is blocked + // on its attempt to read from the response channel if it wasn't closed + // properly by the test function. We do this in a separate routine as + // opposed to just after the timer fires, because we want the tomb to + // enter a dying state with an error other than io.EOF in this case. + cleanupTmb.Go(func() error { + <-cleanupTmb.Dying() // Wait until we enter a dying state. + + // Check the error - if the test function closed its end of the + // response channel, we expect the dying reason to be io.EOF. If + // it's not, we'll close the write end of the response channel here. + switch cleanupTmb.Err() { + case io.EOF: + // Everything's good - nothing to do here. + default: + rspW.Close() + } + + return tomb.ErrDying + }) + + // Give the tomb 500ms to enter a dying state, which should be long + // enough for the Read attempt to return io.EOF. + select { + case <-time.NewTimer(500 * time.Millisecond).C: + // This will put the tomb into a dying state ane make sure it + // return an appropriate error before we unblock the blocked + // goroutine. + return errors.New("remote side of response channel not closed as expected") + case <-cleanupTmb.Dying(): + } + + return tomb.ErrDying + }) + c.Check(cleanupTmb.Wait(), Equals, io.EOF) + + // Grab the response + select { + case rsp = <-rspChan: + default: + } + + // Grab the lock release callback + select { + case release = <-releaseChan: + default: } - return actualRsp, nil + + return rsp, release, err } // argon2OutOfProcessParentSupportMixin provides capabilities shared // between suites that test the parent side of out-of-process Argon2 components. type argon2OutOfProcessParentSupportMixin struct { + lockPath string restoreLockPath func() runArgon2OutputDir string } @@ -194,7 +279,8 @@ func (s *argon2OutOfProcessParentSupportMixin) SetUpSuite(c *C) { } func (s *argon2OutOfProcessParentSupportMixin) SetUpTest(c *C) { - s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(filepath.Join(c.MkDir(), "argon2.lock")) + s.lockPath = filepath.Join(c.MkDir(), "argon2.lock") + s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(s.lockPath) } func (s *argon2OutOfProcessParentSupportMixin) TearDownTest(c *C) { @@ -209,6 +295,15 @@ func (s *argon2OutOfProcessParentSupportMixin) newHandlerCmd(args ...string) fun } } +func (s *argon2OutOfProcessParentSupportMixin) checkNoLockFile(c *C) { + f, err := os.Open(s.lockPath) + c.Check(os.IsNotExist(err), testutil.IsTrue) + if err != nil { + return + } + f.Close() +} + type testHMACArgon2OutOfProcessWatchdogMonitorParams struct { monitorAlg crypto.Hash period time.Duration @@ -325,7 +420,7 @@ var _ = Suite(&argon2OutOfProcessParentSupportSuite{}) var _ = Suite(&argon2OutOfProcessParentSupportSuiteExpensive{}) func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidMode(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: nil, @@ -340,10 +435,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveR ErrorType: Argon2OutOfProcessErrorInvalidMode, ErrorString: "mode cannot be \"foo\"", }) + c.Check(release, IsNil) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidTime(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: nil, @@ -358,10 +455,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveR ErrorType: Argon2OutOfProcessErrorInvalidTimeCost, ErrorString: "time cannot be zero", }) + c.Check(release, IsNil) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidThreads(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: nil, @@ -376,10 +475,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveR ErrorType: Argon2OutOfProcessErrorInvalidThreads, ErrorString: "threads cannot be zero", }) + c.Check(release, IsNil) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidWatchdogChallenge(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: nil, @@ -395,9 +496,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveR ErrorType: Argon2OutOfProcessErrorUnexpectedInput, ErrorString: "invalid watchdog challenge: cannot service a watchdog", }) + c.Check(release, IsNil) + s.checkNoLockFile(c) } + func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidPassphrase(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, Passphrase: "foo", Mode: Argon2id, @@ -410,10 +514,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeReq ErrorType: Argon2OutOfProcessErrorUnexpectedInput, ErrorString: "cannot supply passphrase for \"time\" command", }) + c.Check(release, IsNil) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidSalt(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, Salt: []byte("0123456789abcdefghijklmnopqrstuv"), Mode: Argon2id, @@ -426,10 +532,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeReq ErrorType: Argon2OutOfProcessErrorUnexpectedInput, ErrorString: "cannot supply salt for \"time\" command", }) + c.Check(release, IsNil) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidKeylen(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, Keylen: 32, Mode: Argon2id, @@ -442,10 +550,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeReq ErrorType: Argon2OutOfProcessErrorUnexpectedInput, ErrorString: "cannot supply keylen for \"time\" command", }) + c.Check(release, IsNil) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessInvalidCommand(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommand("foo"), Mode: Argon2id, Time: 4, @@ -457,44 +567,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessInvalid ErrorType: Argon2OutOfProcessErrorInvalidCommand, ErrorString: "command cannot be \"foo\"", }) + c.Check(release, IsNil) + s.checkNoLockFile(c) } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveMoreThanOnceAfterErr(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandDerive, - Passphrase: "foo", - Salt: nil, - Keylen: 32, - Mode: Argon2Mode("foo"), - Time: 4, - MemoryKiB: 32 * 10224, - Threads: 4, - }) - c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ - Command: Argon2OutOfProcessCommandDerive, - ErrorType: Argon2OutOfProcessErrorInvalidMode, - ErrorString: "mode cannot be \"foo\"", - }) - - out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ - Command: Argon2OutOfProcessCommandDerive, - Passphrase: "foo", - Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), - Keylen: 32, - Mode: Argon2id, - Time: 4, - MemoryKiB: 32, - Threads: 4, - }) - c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ - Command: Argon2OutOfProcessCommandDerive, - ErrorType: Argon2OutOfProcessErrorRestartProcess, - ErrorString: "this process cannot handle any more KDF requests", - }) -} - -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveMoreThanOnce(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ +func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveMoreThanOnceWithRelease(c *C) { + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), @@ -508,9 +586,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveM Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "7306196ab24ea3ac9daab7f14345a9dc228dccef07075dbd2e047deac96689ea"), }) + c.Assert(release, NotNil) + defer release() - out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release2 := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, + Timeout: 0, Passphrase: "foo", Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Keylen: 32, @@ -521,13 +602,14 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveM }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, - ErrorType: Argon2OutOfProcessErrorRestartProcess, - ErrorString: "this process cannot handle any more KDF requests", + ErrorType: Argon2OutOfProcessErrorKDFTimeout, + ErrorString: "cannot acquire argon2 system lock: request timeout", }) + c.Check(release2, IsNil) } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveMinimum(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), @@ -541,10 +623,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveM Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "7306196ab24ea3ac9daab7f14345a9dc228dccef07075dbd2e047deac96689ea"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentThreads(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), @@ -558,10 +642,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "5699b81ee10e189505874d0cbd93d61186b90554c716d309037907b7238113e1"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentTime(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), @@ -575,10 +661,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "2f2d7dd170cf43aff82737bc1c2fbe685b34190fc8b62378693c3b0685b96912"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentMemory(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), @@ -592,10 +680,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "6f49db1f7336329c0d5fd652642b144b204d7976c5fcb4c72b6e1d9ea345fa32"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentPassphrase(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "bar", Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), @@ -609,10 +699,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "43cb7f6d24bb2da9ae04735c7193c7523fe057243f09c1241a99cd4ccd7d17f5"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentSalt(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: testutil.DecodeHexString(c, "97226ac63a73c7dafef57066ee645abe"), @@ -626,10 +718,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "720ff1ce2beecf00c4586d659bd7fa9f018cc4f115f398975eff50b35f3393ff"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentKeyLen(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), @@ -643,10 +737,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "21ab785e199d43575ca11e85e0a1281b4426c973cfad0a899b24bc4b8057355912a20b5f4132d8132ce3aa5bffe0d9a6a7fd05d3ab67898c196d584c98d47e44"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentMode(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), @@ -660,10 +756,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "a02a0203ea0e5e9abe4006fc80d1aca26b0adc1f898214c4c61d31f90bd4d129"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcess512MB(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "bar", Salt: testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), @@ -677,10 +775,12 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProce Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTime(c *C) { - out := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, Mode: Argon2id, Time: 4, @@ -690,13 +790,15 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTime(c c.Check(out, NotNil) c.Check(out.Command, Equals, Argon2OutOfProcessCommandTime) c.Check(out.Duration, Not(Equals), time.Duration(0)) + c.Assert(release, NotNil) + release() origDuration := out.Duration // Permit calling the function again - ResetArgon2OutOfProcessHandlerStatus() + runtime.GC() - out = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ + out, release = RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, Mode: Argon2id, Time: 8, @@ -706,6 +808,8 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTime(c c.Check(out, NotNil) c.Check(out.Command, Equals, Argon2OutOfProcessCommandTime) c.Check(out.Duration > origDuration, testutil.IsTrue) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestHMACArgon2OutOfProcessWatchdogHandlerSHA256(c *C) { @@ -739,7 +843,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestNoArgon2OutOfProcessWatchdog } func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestMinimum(c *C) { - rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -758,10 +862,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "7306196ab24ea3ac9daab7f14345a9dc228dccef07075dbd2e047deac96689ea"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentThreads(c *C) { - rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -780,10 +886,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "5699b81ee10e189505874d0cbd93d61186b90554c716d309037907b7238113e1"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentTime(c *C) { - rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -802,10 +910,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "2f2d7dd170cf43aff82737bc1c2fbe685b34190fc8b62378693c3b0685b96912"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentMemory(c *C) { - rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -824,10 +934,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "6f49db1f7336329c0d5fd652642b144b204d7976c5fcb4c72b6e1d9ea345fa32"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestPassphrase(c *C) { - rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "bar", @@ -846,10 +958,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "43cb7f6d24bb2da9ae04735c7193c7523fe057243f09c1241a99cd4ccd7d17f5"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentSalt(c *C) { - rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -868,10 +982,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "720ff1ce2beecf00c4586d659bd7fa9f018cc4f115f398975eff50b35f3393ff"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentKeyLen(c *C) { - rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -890,10 +1006,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "21ab785e199d43575ca11e85e0a1281b4426c973cfad0a899b24bc4b8057355912a20b5f4132d8132ce3aa5bffe0d9a6a7fd05d3ab67898c196d584c98d47e44"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentMode(c *C) { - rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -912,10 +1030,12 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "a02a0203ea0e5e9abe4006fc80d1aca26b0adc1f898214c4c61d31f90bd4d129"), }) + c.Assert(release, NotNil) + release() } func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequest512MB(c *C) { - rsp, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "bar", @@ -934,6 +1054,33 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2 Command: Argon2OutOfProcessCommandDerive, Key: testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0"), }) + c.Assert(release, NotNil) + release() +} + +func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestInvalidRequest(c *C) { + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ + req: &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 0, + MemoryKiB: 32, + Threads: 4, + }, + wdHandler: NoArgon2OutOfProcessWatchdogHandler(), + wdMonitor: NoArgon2OutOfProcessWatchdogMonitor(), + }) + c.Check(err, IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + ErrorType: Argon2OutOfProcessErrorInvalidTimeCost, + ErrorString: "time cannot be zero", + }) + c.Check(release, IsNil) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestNoArgon2OutOfProcessWatchdogMonitorUnexpectedResponse(c *C) { @@ -1062,6 +1209,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveMinimum(c *C) key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) c.Check(err, IsNil) c.Check(key, DeepEquals, testutil.DecodeHexString(c, "7306196ab24ea3ac9daab7f14345a9dc228dccef07075dbd2e047deac96689ea")) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentThreads(c *C) { @@ -1074,6 +1222,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentThrea key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) c.Check(err, IsNil) c.Check(key, DeepEquals, testutil.DecodeHexString(c, "5699b81ee10e189505874d0cbd93d61186b90554c716d309037907b7238113e1")) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentTime(c *C) { @@ -1086,6 +1235,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentTime( key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) c.Check(err, IsNil) c.Check(key, DeepEquals, testutil.DecodeHexString(c, "2f2d7dd170cf43aff82737bc1c2fbe685b34190fc8b62378693c3b0685b96912")) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMemory(c *C) { @@ -1098,6 +1248,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMemor key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) c.Check(err, IsNil) c.Check(key, DeepEquals, testutil.DecodeHexString(c, "6f49db1f7336329c0d5fd652642b144b204d7976c5fcb4c72b6e1d9ea345fa32")) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentPassphrase(c *C) { @@ -1110,6 +1261,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentPassp key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 32) c.Check(err, IsNil) c.Check(key, DeepEquals, testutil.DecodeHexString(c, "43cb7f6d24bb2da9ae04735c7193c7523fe057243f09c1241a99cd4ccd7d17f5")) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentSalt(c *C) { @@ -1122,6 +1274,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentSalt( key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "97226ac63a73c7dafef57066ee645abe"), Argon2id, params, 32) c.Check(err, IsNil) c.Check(key, DeepEquals, testutil.DecodeHexString(c, "720ff1ce2beecf00c4586d659bd7fa9f018cc4f115f398975eff50b35f3393ff")) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveKeyLen(c *C) { @@ -1134,6 +1287,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveKeyLen(c *C) { key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2id, params, 64) c.Check(err, IsNil) c.Check(key, DeepEquals, testutil.DecodeHexString(c, "21ab785e199d43575ca11e85e0a1281b4426c973cfad0a899b24bc4b8057355912a20b5f4132d8132ce3aa5bffe0d9a6a7fd05d3ab67898c196d584c98d47e44")) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMode(c *C) { @@ -1146,6 +1300,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMode( key, err := kdf.Derive("foo", testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), Argon2i, params, 32) c.Check(err, IsNil) c.Check(key, DeepEquals, testutil.DecodeHexString(c, "a02a0203ea0e5e9abe4006fc80d1aca26b0adc1f898214c4c61d31f90bd4d129")) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive512MB(c *C) { @@ -1158,7 +1313,7 @@ func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive512MB key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) c.Check(err, IsNil) c.Check(key, DeepEquals, testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0")) - c.Logf("%x", key) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive512MBDifferentWatchdogHMAC(c *C) { @@ -1171,6 +1326,7 @@ func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive512MB key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) c.Check(err, IsNil) c.Check(key, DeepEquals, testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0")) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveErr(c *C) { @@ -1183,6 +1339,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveErr(c *C) { _, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) c.Check(err, ErrorMatches, `cannot process request: invalid-time-cost \(time cannot be zero\)`) c.Check(err, testutil.ConvertibleTo, new(Argon2OutOfProcessError)) + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFTime(c *C) { @@ -1229,6 +1386,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelSerial wg.Done() }() wg.Wait() + s.checkNoLockFile(c) } func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelTimeout(c *C) { diff --git a/cmd/run_argon2/main.go b/cmd/run_argon2/main.go index bb9436d4..d3635dad 100644 --- a/cmd/run_argon2/main.go +++ b/cmd/run_argon2/main.go @@ -64,7 +64,8 @@ func run() error { watchdog = secboot.HMACArgon2OutOfProcessWatchdogHandler(alg) } - err := secboot.WaitForAndRunArgon2OutOfProcessRequest(os.Stdin, os.Stdout, watchdog) + // Ignore the lock release callback and use implicit release on process termination. + _, err := secboot.WaitForAndRunArgon2OutOfProcessRequest(os.Stdin, os.Stdout, watchdog) if err != nil { return fmt.Errorf("cannot run request: %w", err) } diff --git a/export_test.go b/export_test.go index c055db9a..f63e1115 100644 --- a/export_test.go +++ b/export_test.go @@ -23,8 +23,6 @@ package secboot import ( "crypto" "io" - "runtime" - "sync/atomic" "time" "github.com/snapcore/secboot/internal/luks2" @@ -192,11 +190,6 @@ func MockHashAlgAvailable() (restore func()) { } } -func ResetArgon2OutOfProcessHandlerStatus() { - atomic.StoreUint32(&argon2OutOfProcessHandlerStatus, inProcessArgon2KDFAvailable) - runtime.GC() -} - func (d *KeyData) DerivePassphraseKeys(passphrase string) (key, iv, auth []byte, err error) { return d.derivePassphraseKeys(passphrase) } From b803ecfe3f2cf506985eb8d4e676844369d6f74c Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 15:03:00 +0000 Subject: [PATCH 19/40] Close both ends of the request channel on error If exec.Cmd.StdoutPipe() returns an error, we close the parent end of the request channel (created by calling exec.Cmd.StdinPipe), but we leak the child end of it, so just make sure that we close both ends. --- argon2_out_of_process_support.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 6d40ec74..0c592d65 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -752,13 +752,17 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out 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 other than closing the stdinPipe - stdinPipe.Close() + // cleanup to do on failure paths other than closing the stdinPipe. + // Note that we need to close both ends of it. + stdinPipe.Close() // The parent end + cmd.Stdin.(*os.File).Close() // The child end return nil, fmt.Errorf("cannot create stdout pipe: %w", err) } // Start the remote process. if err := cmd.Start(); err != nil { + // This takes care of closing both ends of each of the pipes + // we created earlier. return nil, fmt.Errorf("cannot start handler process: %w", err) } @@ -923,8 +927,8 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out // [exec.Cmd.Wait] will close parent FDs for us once the process has exitted. However, closing // the stdin pipe is necessary to unblock WaitForAndRunArgon2OutOfProcessRequest on the remote // side, if it is blocked in a read. We don't do the same for stdoutPipe (the request channel) - // because the remote process is expected to close its end of it, freeing up any goroutines - // that are blocked on writing a response to us. + // because the remote process is expected to close its end of it, freeing up any of its own + // goroutines that are blocked on writing a response to us. if err := stdinPipe.Close(); err != nil { return nil, fmt.Errorf("cannot close stdin pipe: %w", err) } From d38ab1cde7a71532b2832edcd9faaaf17f6a00c7 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 15:08:49 +0000 Subject: [PATCH 20/40] Remove a bit of unnecessary test code --- argon2_out_of_process_support_test.go | 57 --------------------------- 1 file changed, 57 deletions(-) diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 09a11105..e746ada9 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -192,63 +192,6 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc // Wait for everything to die, hopefully successfully. err = tmb.Wait() - // Make sure that the test function closed its side of the response channel, as - // it's expected to do. We do this by attemping to read from the parent side, - // which should immediately return io.EOF. We do this in a way that won't cause - // the test to block indefinitely if the test function misbehaves and doesn't do - // this. - cleanupTmb := new(tomb.Tomb) - - // Spin up a routine that will timeout and unblock the test if the test function - // didn't close its side of the response channel. - cleanupTmb.Go(func() error { - // Spin up a routine to attempt to read from the response channel. - // It should immediately receive io.EOF, putting the tomb into a - // dying state with this error. - cleanupTmb.Go(func() error { - var data [1]byte - _, err := rspR.Read(data[:]) - // This should be io.EOF and should put the tomb into a - // dying state - return err - }) - - // Spin up a routine that will unblock the goroutine that is blocked - // on its attempt to read from the response channel if it wasn't closed - // properly by the test function. We do this in a separate routine as - // opposed to just after the timer fires, because we want the tomb to - // enter a dying state with an error other than io.EOF in this case. - cleanupTmb.Go(func() error { - <-cleanupTmb.Dying() // Wait until we enter a dying state. - - // Check the error - if the test function closed its end of the - // response channel, we expect the dying reason to be io.EOF. If - // it's not, we'll close the write end of the response channel here. - switch cleanupTmb.Err() { - case io.EOF: - // Everything's good - nothing to do here. - default: - rspW.Close() - } - - return tomb.ErrDying - }) - - // Give the tomb 500ms to enter a dying state, which should be long - // enough for the Read attempt to return io.EOF. - select { - case <-time.NewTimer(500 * time.Millisecond).C: - // This will put the tomb into a dying state ane make sure it - // return an appropriate error before we unblock the blocked - // goroutine. - return errors.New("remote side of response channel not closed as expected") - case <-cleanupTmb.Dying(): - } - - return tomb.ErrDying - }) - c.Check(cleanupTmb.Wait(), Equals, io.EOF) - // Grab the response select { case rsp = <-rspChan: From 093b6ee51a9ae21b5bf1c854b9c1630fcbe049ce Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 15:41:22 +0000 Subject: [PATCH 21/40] Test WaitForAndRunArgon2OutOfProcessRequest closes its end of the response channel --- argon2_out_of_process_support_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index e746ada9..10a3a232 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -192,6 +192,30 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc // Wait for everything to die, hopefully successfully. err = tmb.Wait() + // Make sure that WaitForAndRunArgon2OutOfProcessRequest closed its end of the + // response channel + cleanupTmb := new(tomb.Tomb) + cleanupTmb.Go(func() error { + cleanupTmb.Go(func() error { + var data [1]byte + _, err := rspW.Write(data[:]) + return err + }) + + select { + case <-time.NewTimer(500 * time.Millisecond).C: + return errors.New("write end of response channel was not closed by WaitForAndRunArgon2OutOfProcessRequest") + case <-cleanupTmb.Dying(): + } + return tomb.ErrDying + }) + <-cleanupTmb.Dying() + c.Check(cleanupTmb.Err(), Equals, io.ErrClosedPipe) + if cleanupTmb.Err() != io.ErrClosedPipe { + c.Check(rspW.Close(), IsNil) + } + c.Check(cleanupTmb.Wait(), Equals, io.ErrClosedPipe) + // Grab the response select { case rsp = <-rspChan: From c8d71ba7855996e9b689e81290e08e7b9636d44b Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 16:13:46 +0000 Subject: [PATCH 22/40] Improve the argon2 remoting system-lock tests This ended up catching a bug that resulted in a deadlock if the locking function had to perform a retry. --- argon2_out_of_process_support_sync.go | 8 +- argon2_out_of_process_support_sync_test.go | 124 +++++++++++++-------- argon2_out_of_process_support_test.go | 23 ++-- export_test.go | 8 ++ 4 files changed, 103 insertions(+), 60 deletions(-) diff --git a/argon2_out_of_process_support_sync.go b/argon2_out_of_process_support_sync.go index dd04945a..f0b1c95a 100644 --- a/argon2_out_of_process_support_sync.go +++ b/argon2_out_of_process_support_sync.go @@ -80,7 +80,8 @@ import ( // only be unlinked by the current lock holder. var ( - argon2SysLockStderr io.Writer = os.Stderr + argon2SysLockStderr io.Writer = os.Stderr + acquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint = func() {} errArgon2OutOfProcessHandlerSystemLockTimeout = errors.New("request timeout") ) @@ -117,7 +118,7 @@ func acquireArgon2OutOfProcessHandlerSystemLock(timeout time.Duration) (release // Run a loop to try to acquire the lock. for { - skipBackoffCh := make(chan struct{}) // Don't wait 100ms before trying again + skipBackoffCh := make(chan struct{}, 1) // Don't wait 100ms before trying again if triedOnce { // If the loop has executed at least once, make sure that // the timeout hasn't expired. @@ -171,6 +172,9 @@ func acquireArgon2OutOfProcessHandlerSystemLock(timeout time.Duration) (release return nil, fmt.Errorf("cannot obtain lock on open lock file descriptor: %w", err) } + // This is useful for blocking the function here in unit tests + acquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint() + // We have acquired an exclusive advisory lock on the file that we opened, but perform // some checks to ensure we haven't hit a race condition with another process. diff --git a/argon2_out_of_process_support_sync_test.go b/argon2_out_of_process_support_sync_test.go index e1046996..8ec94120 100644 --- a/argon2_out_of_process_support_sync_test.go +++ b/argon2_out_of_process_support_sync_test.go @@ -23,7 +23,6 @@ import ( "errors" "os" "path/filepath" - "sync" "syscall" "time" @@ -71,11 +70,13 @@ func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfPro c.Check(errors.Is(err, syscall.Errno(syscall.EWOULDBLOCK)), testutil.IsTrue) release() - _, err = os.OpenFile(s.lockPath, os.O_RDWR, 0600) + _, err = os.Stat(s.lockPath) c.Check(os.IsNotExist(err), testutil.IsTrue) } func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLockTimeout(c *C) { + // Note that this test could potentially deadlock if the timeout + // functionality is broken. release, err := AcquireArgon2OutOfProcessHandlerSystemLock(0) c.Assert(err, IsNil) defer release() @@ -85,66 +86,97 @@ func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfPro } func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLockDeletedFile(c *C) { - // Grab an exclusive lock on the file first to block - // AcquireArgon2OutOfProcessHandlerSystemLock - f, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) + loopedOnce := false + restore := MockAcquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint(func() { + if loopedOnce { + return + } + loopedOnce = true + c.Check(os.Remove(s.lockPath), IsNil) + }) + defer restore() + + release, err := AcquireArgon2OutOfProcessHandlerSystemLock(10 * time.Second) c.Assert(err, IsNil) - defer f.Close() - - c.Check(unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB), IsNil) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - release, err := AcquireArgon2OutOfProcessHandlerSystemLock(10 * time.Second) - c.Check(err, IsNil) - release() - wg.Done() - }() + defer release() - // Ensure we end up waiting for the exclusive lock - <-time.NewTimer(500 * time.Millisecond).C + c.Check(loopedOnce, testutil.IsTrue) - // Delete the file - c.Check(os.Remove(s.lockPath), IsNil) + f, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) + c.Assert(err, IsNil) + defer f.Close() - // Close our FD to free up the locking - it should loop and try again - c.Check(f.Close(), IsNil) + err = unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB) + c.Check(err, ErrorMatches, `resource temporarily unavailable`) + c.Check(errors.Is(err, syscall.Errno(syscall.EWOULDBLOCK)), testutil.IsTrue) - wg.Wait() + release() + _, err = os.Stat(s.lockPath) + c.Check(os.IsNotExist(err), testutil.IsTrue) } func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLockChangedInode(c *C) { - // Grab an exclusive lock on the file first to block - // AcquireArgon2OutOfProcessHandlerSystemLock + loopedOnce := false + restore := MockAcquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint(func() { + if loopedOnce { + return + } + loopedOnce = true + c.Check(os.Remove(s.lockPath), IsNil) + f, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) + c.Assert(err, IsNil) + c.Check(f.Close(), IsNil) + }) + defer restore() + + release, err := AcquireArgon2OutOfProcessHandlerSystemLock(10 * time.Second) + c.Assert(err, IsNil) + defer release() + + c.Check(loopedOnce, testutil.IsTrue) + f, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) c.Assert(err, IsNil) defer f.Close() - c.Check(unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB), IsNil) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - release, err := AcquireArgon2OutOfProcessHandlerSystemLock(10 * time.Second) - c.Check(err, IsNil) - release() - wg.Done() - }() + err = unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB) + c.Check(err, ErrorMatches, `resource temporarily unavailable`) + c.Check(errors.Is(err, syscall.Errno(syscall.EWOULDBLOCK)), testutil.IsTrue) - // Ensure we end up waiting for the exclusive lock - <-time.NewTimer(500 * time.Millisecond).C + release() + _, err = os.Stat(s.lockPath) + c.Check(os.IsNotExist(err), testutil.IsTrue) +} - // Delete the file - c.Check(os.Remove(s.lockPath), IsNil) +func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLockMultipleLoops(c *C) { + loopCount := 0 + restore := MockAcquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint(func() { + loopCount += 1 + switch loopCount { + case 1, 3: + c.Check(os.Remove(s.lockPath), IsNil) + case 2, 4: + c.Check(os.Remove(s.lockPath), IsNil) + f, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) + c.Assert(err, IsNil) + c.Check(f.Close(), IsNil) + } + }) + defer restore() + + release, err := AcquireArgon2OutOfProcessHandlerSystemLock(10 * time.Second) + c.Assert(err, IsNil) + defer release() - // Create a new file - f2, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) + f, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0600) c.Assert(err, IsNil) - defer f2.Close() + defer f.Close() - // Close our original FD to free up the locking - it should loop and try again - c.Check(f.Close(), IsNil) + err = unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB) + c.Check(err, ErrorMatches, `resource temporarily unavailable`) + c.Check(errors.Is(err, syscall.Errno(syscall.EWOULDBLOCK)), testutil.IsTrue) - wg.Wait() + release() + _, err = os.Stat(s.lockPath) + c.Check(os.IsNotExist(err), testutil.IsTrue) } diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 10a3a232..f89957ca 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -62,12 +62,8 @@ func (s *argon2OutOfProcessHandlerSupportMixin) TearDownTest(c *C) { } func (s *argon2OutOfProcessHandlerSupportMixin) checkNoLockFile(c *C) { - f, err := os.Open(s.lockPath) + _, err := os.Stat(s.lockPath) c.Check(os.IsNotExist(err), testutil.IsTrue) - if err != nil { - return - } - f.Close() } type testWaitForAndRunArgon2OutOfProcessRequestParams struct { @@ -176,7 +172,8 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc return tomb.ErrDying }) - // Wait for the tomb to begin dying + // Wait for the tomb to begin dying. The test could block here indefinitely if + // we never get a response. <-tmb.Dying() // Closing our end of the request channel supplied to the test function, as @@ -189,7 +186,10 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc // a way to mitigate that c.Check(reqW.Close(), IsNil) - // Wait for everything to die, hopefully successfully. + // Wait for everything to die, hopefully successfully. The test could block + // indefinitely here if WaitForAndRunArgon2OutOfProcessRequest doesn't return + // when we closed our end of the request channel above, or if the supplied + // watchdog monitor misbehaves and doesn't return when it is supposed to. err = tmb.Wait() // Make sure that WaitForAndRunArgon2OutOfProcessRequest closed its end of the @@ -263,12 +263,8 @@ func (s *argon2OutOfProcessParentSupportMixin) newHandlerCmd(args ...string) fun } func (s *argon2OutOfProcessParentSupportMixin) checkNoLockFile(c *C) { - f, err := os.Open(s.lockPath) + _, err := os.Stat(s.lockPath) c.Check(os.IsNotExist(err), testutil.IsTrue) - if err != nil { - return - } - f.Close() } type testHMACArgon2OutOfProcessWatchdogMonitorParams struct { @@ -349,6 +345,9 @@ func (s *argon2OutOfProcessParentSupportMixin) testHMACArgon2OutOfProcessWatchdo return tomb.ErrDying }) + // This test could block indefinitely here if HMACArgon2OutOfProcessWatchdogMonitor + // doesn't behave as it's expected and return when the supplied tomb enters a + // dying state. return tmb.Wait() } diff --git a/export_test.go b/export_test.go index f63e1115..b1328fbd 100644 --- a/export_test.go +++ b/export_test.go @@ -66,6 +66,14 @@ func MockArgon2OutOfProcessHandlerSystemLockPath(path string) (restore func()) { } } +func MockAcquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint(fn func()) (restore func()) { + orig := acquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint + acquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint = fn + return func() { + acquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint = orig + } +} + func MockArgon2SysLockStderr(w io.Writer) (restore func()) { orig := argon2SysLockStderr argon2SysLockStderr = w From a1e15a7095c497f64a13bb928960c4651f205e31 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 18:33:44 +0000 Subject: [PATCH 23/40] Make use of strings.Builder --- argon2_out_of_process_support.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 0c592d65..a7db8cb0 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -30,6 +30,7 @@ import ( "io" "os" "os/exec" + "strings" "time" "gopkg.in/tomb.v2" @@ -121,13 +122,12 @@ type Argon2OutOfProcessError struct { // Error implements the error interface. func (e *Argon2OutOfProcessError) Error() string { - str := "cannot process request: " + string(e.ErrorType) + var b strings.Builder + b.WriteString("cannot process request: " + string(e.ErrorType)) if e.ErrorString != "" { - str += " (" - str += e.ErrorString - str += ")" + b.WriteString(" (" + e.ErrorString + ")") } - return str + return b.String() } // Err returns an error associated with the response if one occurred (if the From 9c9d5ca000173f67a19dfeabdaff9564c109f442 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 18:41:29 +0000 Subject: [PATCH 24/40] remove some unnecessary channel usage --- argon2_out_of_process_support.go | 29 +++++++-------------------- argon2_out_of_process_support_test.go | 27 +++++++------------------ 2 files changed, 14 insertions(+), 42 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index a7db8cb0..24a4328b 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -433,10 +433,6 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.WriteCloser, wa // dedicated output routine which serializes the response to the supplied io.Writer. rspChan := make(chan *Argon2OutOfProcessResponse) - // lockReleaseChan is the channel from which this routine receives a callback to explicitly - // relinquish the system-wide lock. It's buffered because it's read by the current goroutine. - lockReleaseChan := make(chan func(), 1) - // Spin up a routine for receiving requests from the supplied io.Reader. tmb.Go(func() error { // Also spin-up the routine for sending outgoing responses that are generated internally. @@ -554,16 +550,14 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.WriteCloser, wa rsp, release := RunArgon2OutOfProcessRequest(req) if release != nil { - // This channel is buffered with a size of 1 so that it can be obtained by - // the calling goroutine when the tomb has died. Therefore, it should never - // block. Although theoretically this process could be sent more than one - // request (although the parent side implementation in secboot won't do this), + // Although theoretically this process could be sent more than one request + // (although the parent side implementation in secboot won't do this), // subsequent calls to RunArgon2OutOfProcessRequest eventually all timeout, // unable to acquire another system-wide lock (it will be acquired on the first - // request and the release callback buffered in the channel already). In this - // case, there will be no new release callback so the channel should always be - // empty at this point. - lockReleaseChan <- release + // request and the return value will contain the release callback already). In + // this case, there will be no new release callback to overwrite the one that + // the return value was set to on the first request. + lockRelease = release } // Send the response. @@ -592,16 +586,7 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.WriteCloser, wa }) // Wait here for the tomb to die and return the first error that occurred. - err = tmb.Wait() - - select { - case lockRelease = <-lockReleaseChan: - // The system-wide lock was acquired and we have a release callback for it. - default: - // The system-wide lock was never acquired - } - - return lockRelease, err + return lockRelease, tmb.Wait() } // Argon2OutOfProcessWatchdogMonitor defines the behaviour of a watchdog monitor diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index f89957ca..d8042348 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -77,9 +77,8 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc reqR, reqW := io.Pipe() rspR, rspW := io.Pipe() - rspChan := make(chan *Argon2OutOfProcessResponse, 1) // A buffered channel to receive the response from the test function - releaseChan := make(chan func(), 1) // A buffered channel to receive the lock release callback from the test function - tmb := new(tomb.Tomb) // The tomb for tracking goroutines + var actualRsp *Argon2OutOfProcessResponse + tmb := new(tomb.Tomb) // The tomb for tracking goroutines // Spin up a goroutine to bootstrap the test setup and then process responses from the // test function. I'm not sure how thread safe the test library is, so we avoid doing @@ -91,9 +90,9 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc // nothing else in the test exits a routine with an error, errors // returned from the test function will propagate out of the tomb // and will be checked on the main test goroutine. - tmb.Go(func() (err error) { - release, err := WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW, params.wdHandler) - releaseChan <- release + tmb.Go(func() error { + var err error + release, err = WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW, params.wdHandler) return err }) @@ -164,7 +163,7 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc } default: // We got a response - begin the process of dying. - rspChan <- rsp + actualRsp = rsp tmb.Kill(nil) // This loop will no longer iterate } @@ -216,19 +215,7 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc } c.Check(cleanupTmb.Wait(), Equals, io.ErrClosedPipe) - // Grab the response - select { - case rsp = <-rspChan: - default: - } - - // Grab the lock release callback - select { - case release = <-releaseChan: - default: - } - - return rsp, release, err + return actualRsp, release, err } // argon2OutOfProcessParentSupportMixin provides capabilities shared From 327f2355245ee7a7a7c8af02488c24ee1a7b8407 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 18:49:58 +0000 Subject: [PATCH 25/40] Try running some tests with a memory consumption of 2GiB again --- argon2_out_of_process_support_test.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index d8042348..f1322727 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -713,7 +713,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD release() } -func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcess512MB(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcess2GB(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "bar", @@ -721,12 +721,12 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProce Keylen: 32, Mode: Argon2id, Time: 4, - MemoryKiB: 512 * 1024, + MemoryKiB: 2 * 1024 * 1024, Threads: 4, }) c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, - Key: testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0"), + Key: testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd"), }) c.Assert(release, NotNil) release() @@ -987,7 +987,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc release() } -func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequest512MB(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequest2GB(c *C) { rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -996,7 +996,7 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2 Keylen: 32, Mode: Argon2id, Time: 4, - MemoryKiB: 512 * 1024, + MemoryKiB: 2 * 1024 * 1024, Threads: 4, }, wdHandler: HMACArgon2OutOfProcessWatchdogHandler(crypto.SHA256), @@ -1005,7 +1005,7 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2 c.Check(err, IsNil) c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ Command: Argon2OutOfProcessCommandDerive, - Key: testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0"), + Key: testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd"), }) c.Assert(release, NotNil) release() @@ -1256,29 +1256,29 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMode( s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive512MB(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GB(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) params := &Argon2CostParams{ Time: 4, - MemoryKiB: 512 * 1024, + MemoryKiB: 2 * 1024 * 1024, Threads: 4, } key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) c.Check(err, IsNil) - c.Check(key, DeepEquals, testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0")) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd")) s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive512MBDifferentWatchdogHMAC(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GBDifferentWatchdogHMAC(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha384"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA384, 100*time.Millisecond, 50*time.Millisecond)) params := &Argon2CostParams{ Time: 4, - MemoryKiB: 512 * 1024, + MemoryKiB: 2 * 1024 * 1024, Threads: 4, } key, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) c.Check(err, IsNil) - c.Check(key, DeepEquals, testutil.DecodeHexString(c, "80dec1e34e9ea2da382852e4d935672ed4ed0c56aa9d109a14829a3f161903c0")) + c.Check(key, DeepEquals, testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd")) s.checkNoLockFile(c) } From afca4f4d572067173ffc0cfe3126e13e18df7d52 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 21:06:23 +0000 Subject: [PATCH 26/40] Simplify the HMACArgon2OutOfProcessWatchdogMonitor function --- argon2_out_of_process_support.go | 19 +++++++------- argon2_out_of_process_support_test.go | 38 +++++++-------------------- 2 files changed, 19 insertions(+), 38 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 24a4328b..8ff7298e 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -625,25 +625,23 @@ type Argon2OutOfProcessWatchdogMonitor = func(tmb *tomb.Tomb, reqChan chan<- *Ar // HMACArgon2OutOfProcessWatchdogMonitor returns a watchdog monitor that generates a // challenge every period, computes a HMAC of this challenge, keyed with previously received -// watchdog response. It stops and returns an error if it doen't receive a valid respose -// in the specified timeout. This is intended be paired with [HMACArgon2OutOfProcessWatchdogHandler] -// on the remote side. -func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period, timeout time.Duration) Argon2OutOfProcessWatchdogMonitor { - if timeout > period { - panic("watchdog timeout can't be larger than period") - } +// watchdog response. It stops and returns an error if it doen't receive a valid response +// before the next cycle is meant to run. This is intended be paired with +// [HMACArgon2OutOfProcessWatchdogHandler] on the remote side. +func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period time.Duration) Argon2OutOfProcessWatchdogMonitor { if !alg.Available() { panic("specified digest algorithm not available") } return func(tmb *tomb.Tomb, reqChan chan<- *Argon2OutOfProcessRequest, rspChan <-chan *Argon2OutOfProcessResponse) error { lastWatchdogResponse := make([]byte, 32) // the last response received from the child. + ticker := time.NewTicker(period) // Run the watchdog whilst the tomb is alive. for tmb.Alive() { - // Run it every defined period + // Wait for the next tick select { - case <-time.NewTimer(period).C: + case <-ticker.C: case <-tmb.Dying(): // Handle the tomb dying before the end of the period. return tomb.ErrDying @@ -676,7 +674,8 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period, timeout time // Wait for the response from the remote process. select { - case <-time.NewTimer(timeout).C: // Give it up to the time defined by the timeout + case <-ticker.C: + // We didn't receive a response before the next tick. return errors.New("timeout waiting for watchdog response from remote process") case rsp := <-rspChan: // We got a response from the remote process. diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index f1322727..85afeef5 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -257,7 +257,6 @@ func (s *argon2OutOfProcessParentSupportMixin) checkNoLockFile(c *C) { type testHMACArgon2OutOfProcessWatchdogMonitorParams struct { monitorAlg crypto.Hash period time.Duration - timeout time.Duration handlerAlg crypto.Hash minDelay time.Duration @@ -267,7 +266,7 @@ type testHMACArgon2OutOfProcessWatchdogMonitorParams struct { func (s *argon2OutOfProcessParentSupportMixin) testHMACArgon2OutOfProcessWatchdogMonitor(c *C, params *testHMACArgon2OutOfProcessWatchdogMonitorParams) error { c.Assert(params.maxDelay >= params.minDelay, testutil.IsTrue) - monitor := HMACArgon2OutOfProcessWatchdogMonitor(params.monitorAlg, params.period, params.timeout) + monitor := HMACArgon2OutOfProcessWatchdogMonitor(params.monitorAlg, params.period) handler := HMACArgon2OutOfProcessWatchdogHandler(params.handlerAlg) tmb := new(tomb.Tomb) @@ -1000,7 +999,7 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2 Threads: 4, }, wdHandler: HMACArgon2OutOfProcessWatchdogHandler(crypto.SHA256), - wdMonitor: HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond), + wdMonitor: HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond), }) c.Check(err, IsNil) c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ @@ -1084,7 +1083,6 @@ func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestHMACArgon2OutOfProce err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ monitorAlg: crypto.SHA256, period: 100 * time.Millisecond, - timeout: 20 * time.Millisecond, handlerAlg: crypto.SHA256, minDelay: 5 * time.Millisecond, maxDelay: 15 * time.Millisecond, @@ -1096,7 +1094,6 @@ func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestHMACArgon2OutOfProce err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ monitorAlg: crypto.SHA384, period: 100 * time.Millisecond, - timeout: 20 * time.Millisecond, handlerAlg: crypto.SHA384, minDelay: 5 * time.Millisecond, maxDelay: 15 * time.Millisecond, @@ -1108,7 +1105,6 @@ func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestHMACArgon2OutOfProce err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ monitorAlg: crypto.SHA256, period: 200 * time.Millisecond, - timeout: 20 * time.Millisecond, handlerAlg: crypto.SHA256, minDelay: 5 * time.Millisecond, maxDelay: 15 * time.Millisecond, @@ -1116,26 +1112,13 @@ func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestHMACArgon2OutOfProce c.Check(err, IsNil) } -func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestHMACArgon2OutOfProcessWatchdogMonitorDifferentTimeout(c *C) { - err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ - monitorAlg: crypto.SHA256, - period: 100 * time.Millisecond, - timeout: 50 * time.Millisecond, - handlerAlg: crypto.SHA256, - minDelay: 35 * time.Millisecond, - maxDelay: 45 * time.Millisecond, - }) - c.Check(err, IsNil) -} - func (s *argon2OutOfProcessParentSupportSuite) TestHMACArgon2OutOfProcessWatchdogMonitorResponseTimeout(c *C) { err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ monitorAlg: crypto.SHA256, period: 100 * time.Millisecond, - timeout: 20 * time.Millisecond, handlerAlg: crypto.SHA256, - minDelay: 25 * time.Millisecond, - maxDelay: 25 * time.Millisecond, + minDelay: 200 * time.Millisecond, + maxDelay: 200 * time.Millisecond, }) c.Check(err, ErrorMatches, `timeout waiting for watchdog response from remote process`) } @@ -1144,7 +1127,6 @@ func (s *argon2OutOfProcessParentSupportSuite) TestHMACArgon2OutOfProcessWatchdo err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{ monitorAlg: crypto.SHA384, period: 100 * time.Millisecond, - timeout: 20 * time.Millisecond, handlerAlg: crypto.SHA256, minDelay: 5 * time.Millisecond, maxDelay: 15 * time.Millisecond, @@ -1257,7 +1239,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMode( } func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GB(c *C) { - kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond)) params := &Argon2CostParams{ Time: 4, MemoryKiB: 2 * 1024 * 1024, @@ -1270,7 +1252,7 @@ func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GB(c } func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GBDifferentWatchdogHMAC(c *C) { - kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha384"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA384, 100*time.Millisecond, 50*time.Millisecond)) + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha384"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA384, 100*time.Millisecond)) params := &Argon2CostParams{ Time: 4, MemoryKiB: 2 * 1024 * 1024, @@ -1315,8 +1297,8 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFTime(c *C) { c.Check(duration > origDuration, testutil.IsTrue) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelSerialized(c *C) { - kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 1*time.Minute, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveParallelSerialized(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 1*time.Minute, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond)) params := &Argon2CostParams{ Time: 4, MemoryKiB: 512 * 1024, @@ -1342,8 +1324,8 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelSerial s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveParallelTimeout(c *C) { - kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 100*time.Millisecond, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond, 50*time.Millisecond)) +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveParallelTimeout(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 100*time.Millisecond, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond)) params := &Argon2CostParams{ Time: 4, MemoryKiB: 512 * 1024, From ba7c52f577755ea32cf10fe441039e3cdeb51755 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 21:57:05 +0000 Subject: [PATCH 27/40] Move all tests that run Argon2 to the expensive suites --- argon2_out_of_process_support_test.go | 62 ++++++++++++++------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 85afeef5..0fb28f46 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -58,7 +58,6 @@ func (s *argon2OutOfProcessHandlerSupportMixin) TearDownTest(c *C) { if s.restoreLockPath != nil { s.restoreLockPath() } - runtime.GC() } func (s *argon2OutOfProcessHandlerSupportMixin) checkNoLockFile(c *C) { @@ -351,6 +350,11 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) SetUpSuite(c *C) { } } +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TearDownTest(c *C) { + s.argon2OutOfProcessHandlerSupportMixin.TearDownTest(c) + runtime.GC() // Because we are running Argon2 in this process. +} + type argon2OutOfProcessParentSupportSuite struct { argon2OutOfProcessParentSupportMixin } @@ -523,7 +527,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessInvalid s.checkNoLockFile(c) } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveMoreThanOnceWithRelease(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveMoreThanOnceWithRelease(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -560,7 +564,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveM c.Check(release2, IsNil) } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveMinimum(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveMinimum(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -579,7 +583,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveM release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentThreads(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentThreads(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -598,7 +602,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentTime(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentTime(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -617,7 +621,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentMemory(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentMemory(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -636,7 +640,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentPassphrase(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentPassphrase(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "bar", @@ -655,7 +659,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentSalt(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentSalt(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -674,7 +678,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentKeyLen(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentKeyLen(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -693,7 +697,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveD release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveDifferentMode(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentMode(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, Passphrase: "foo", @@ -731,7 +735,7 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProce release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTime(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessTime(c *C) { out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandTime, Mode: Argon2id, @@ -794,7 +798,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestNoArgon2OutOfProcessWatchdog c.Check(err, ErrorMatches, `unexpected watchdog request: no handler`) } -func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestMinimum(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestMinimum(c *C) { rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -818,7 +822,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentThreads(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentThreads(c *C) { rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -842,7 +846,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentTime(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentTime(c *C) { rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -866,7 +870,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentMemory(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentMemory(c *C) { rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -890,7 +894,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestPassphrase(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestPassphrase(c *C) { rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -914,7 +918,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentSalt(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentSalt(c *C) { rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -938,7 +942,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentKeyLen(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentKeyLen(c *C) { rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -962,7 +966,7 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc release() } -func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProcessRequestDifferentMode(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentMode(c *C) { rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -999,7 +1003,7 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2 Threads: 4, }, wdHandler: HMACArgon2OutOfProcessWatchdogHandler(crypto.SHA256), - wdMonitor: HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond), + wdMonitor: HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 200*time.Millisecond), }) c.Check(err, IsNil) c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ @@ -1134,7 +1138,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestHMACArgon2OutOfProcessWatchdo c.Check(err, ErrorMatches, `unexpected watchdog response value from remote process`) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveMinimum(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveMinimum(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) params := &Argon2CostParams{ Time: 4, @@ -1147,7 +1151,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveMinimum(c *C) s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentThreads(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveDifferentThreads(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) params := &Argon2CostParams{ Time: 4, @@ -1160,7 +1164,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentThrea s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentTime(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveDifferentTime(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) params := &Argon2CostParams{ Time: 5, @@ -1173,7 +1177,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentTime( s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMemory(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveDifferentMemory(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) params := &Argon2CostParams{ Time: 4, @@ -1186,7 +1190,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMemor s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentPassphrase(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveDifferentPassphrase(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) params := &Argon2CostParams{ Time: 4, @@ -1199,7 +1203,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentPassp s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentSalt(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveDifferentSalt(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) params := &Argon2CostParams{ Time: 4, @@ -1212,7 +1216,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentSalt( s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveKeyLen(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveKeyLen(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) params := &Argon2CostParams{ Time: 4, @@ -1225,7 +1229,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveKeyLen(c *C) { s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveDifferentMode(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveDifferentMode(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) params := &Argon2CostParams{ Time: 4, @@ -1277,7 +1281,7 @@ func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveErr(c *C) { s.checkNoLockFile(c) } -func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFTime(c *C) { +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFTime(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) params := &Argon2CostParams{ Time: 4, From 6270d98d71c719cd2dc0ad93533f23fa3eda0d1a Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 22:36:33 +0000 Subject: [PATCH 28/40] Add some debugging fmt.Printf statements --- argon2_out_of_process_support.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 8ff7298e..a4ac8db9 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -647,12 +647,17 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period time.Duration return tomb.ErrDying } + fmt.Printf("* New watchdog ping\n") + start := time.Now() + // Generate a new 32-byte challenge and calculate the expected response challenge := make([]byte, 32) if _, err := rand.Read(challenge); err != nil { return fmt.Errorf("cannot generate new watchdog challenge: %w", err) } + fmt.Printf(" - time to read entropy: %v\n", time.Now().Sub(start)) + // The expected response is the HMAC of the challenge, keyed with the // last response. h := hmac.New(alg.New, lastWatchdogResponse) @@ -664,6 +669,8 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period time.Duration WatchdogChallenge: challenge, } + fmt.Printf(" - time to prepare request: %v\n", time.Now().Sub(start)) + // Send the request. select { case reqChan <- req: // Unbuffered channel, but read end is always there unless the tomb is dying. @@ -672,13 +679,16 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period time.Duration return tomb.ErrDying } + fmt.Printf(" - time to send request: %v\n", time.Now().Sub(start)) + // Wait for the response from the remote process. select { - case <-ticker.C: - // We didn't receive a response before the next tick. - return errors.New("timeout waiting for watchdog response from remote process") + //case <-ticker.C: + // // We didn't receive a response before the next tick. + // return errors.New("timeout waiting for watchdog response from remote process") case rsp := <-rspChan: // We got a response from the remote process. + fmt.Printf(" - time to receive response: %v\n", time.Now().Sub(start)) if err := rsp.Err(); err != nil { // We got an error response, so just return the error. return rsp.Err() From 88d8658ae6ec9500469828db28c89926cbfc4247 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 22:50:12 +0000 Subject: [PATCH 29/40] Reenable the watchdog timeout again --- argon2_out_of_process_support.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index a4ac8db9..626826b8 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -681,11 +681,15 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period time.Duration fmt.Printf(" - time to send request: %v\n", time.Now().Sub(start)) + // Reset the ticker to remove the cost of gathering entropy, calculating the + // challenge and sending it. + ticker.Reset(period) + // Wait for the response from the remote process. select { - //case <-ticker.C: - // // We didn't receive a response before the next tick. - // return errors.New("timeout waiting for watchdog response from remote process") + case <-ticker.C: + // We didn't receive a response before the next tick. + return errors.New("timeout waiting for watchdog response from remote process") case rsp := <-rspChan: // We got a response from the remote process. fmt.Printf(" - time to receive response: %v\n", time.Now().Sub(start)) From d6cabb31cb5dcd688c6088345e785892f6863ae0 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 23:12:08 +0000 Subject: [PATCH 30/40] try disabling the GC during the failing test --- argon2_out_of_process_support_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 0fb28f46..042de319 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -32,6 +32,7 @@ import ( "os/exec" "path/filepath" "runtime" + "runtime/debug" "sync" "time" @@ -991,6 +992,17 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2 } func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequest2GB(c *C) { + // We're running the Argon2 KDF with high memory requirements in-process + // and testing the watchdog functionality at the same time. This case + // seems particularly hard in the Github runner environment, with tests + // failing due to missed watchdog responses, despite debugging showing that + // we're normally getting responses within ~10ms when running the KDF out + // of process. Try temporarily disabling GC during this test to see if it + // helps. + runtime.GC() + origGCPercent := debug.SetGCPercent(-1) + defer debug.SetGCPercent(origGCPercent) + rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, From 4cdec5fee30a0399b8be0902903e509d546ecd48 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 23:49:23 +0000 Subject: [PATCH 31/40] remove debug fmt.Printfs --- argon2_out_of_process_support.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 626826b8..416c75e4 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -647,17 +647,12 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period time.Duration return tomb.ErrDying } - fmt.Printf("* New watchdog ping\n") - start := time.Now() - // Generate a new 32-byte challenge and calculate the expected response challenge := make([]byte, 32) if _, err := rand.Read(challenge); err != nil { return fmt.Errorf("cannot generate new watchdog challenge: %w", err) } - fmt.Printf(" - time to read entropy: %v\n", time.Now().Sub(start)) - // The expected response is the HMAC of the challenge, keyed with the // last response. h := hmac.New(alg.New, lastWatchdogResponse) @@ -669,8 +664,6 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period time.Duration WatchdogChallenge: challenge, } - fmt.Printf(" - time to prepare request: %v\n", time.Now().Sub(start)) - // Send the request. select { case reqChan <- req: // Unbuffered channel, but read end is always there unless the tomb is dying. @@ -679,8 +672,6 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period time.Duration return tomb.ErrDying } - fmt.Printf(" - time to send request: %v\n", time.Now().Sub(start)) - // Reset the ticker to remove the cost of gathering entropy, calculating the // challenge and sending it. ticker.Reset(period) @@ -692,7 +683,6 @@ func HMACArgon2OutOfProcessWatchdogMonitor(alg crypto.Hash, period time.Duration return errors.New("timeout waiting for watchdog response from remote process") case rsp := <-rspChan: // We got a response from the remote process. - fmt.Printf(" - time to receive response: %v\n", time.Now().Sub(start)) if err := rsp.Err(); err != nil { // We got an error response, so just return the error. return rsp.Err() From 57a56942f9e5d029be241d4446701e2574ac75d6 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Sat, 7 Dec 2024 16:11:15 +0000 Subject: [PATCH 32/40] Ensure WaitForAndRunArgon2OutOfProcessRequest will only process a single request --- argon2_out_of_process_support.go | 156 +++++++++++++++----------- argon2_out_of_process_support_test.go | 78 ++++++++++++- 2 files changed, 164 insertions(+), 70 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 416c75e4..9c618640 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -173,19 +173,25 @@ func (e *Argon2OutOfProcessResponseCommandInvalidError) Error() string { // RunArgon2OutOfProcessRequest runs the specified Argon2 request, and returns a response. // -// In general, this is intended to be executed once in a process, before the process is discarded. +// In general, this is intended to be executed once in a short-lived process, before the process +// is discarded. It could be executed more than once in the same process, as long as the caller +// takes steps to ensure that memory consumed by previous calls has been reclaimed by the GC +// before calling this function again, but this isn't advised. // // Note that Argon2 requests are serialized using a system-wide lock, which this function does not // explicitly release. If the lock is acquired, it returns a callback that the caller may choose // to execute in order to explicitly release the lock, or the caller can just leave it to be // implicitly released on process exit. If the lock is explicitly released, the caller must be -// sure that the large amount of memory allocated for the Argon2 operation has been freed and -// returned back to the OS, else this defeats the point of having a system-wide lock (to avoid -// having multiple processes with high physical memory requirements running at the same time). If -// the lock wasn't acquired, no release callback will be returned. +// sure that the large amount of memory allocated for the Argon2 operation has been reclaimed by +// the GC, else this defeats the point of having a system-wide lock (to avoid multiple operations +// consuming too much memory). If the process is re-used by calling this function more than once, +// the lock will have to be explcitly released. If the lock wasn't acquired, no release callback +// will be returned. // // This is quite a low-level function, suitable for implementations that want to manage their own -// transport. In general, implementations will use [WaitForAndRunArgon2OutOfProcessRequest]. +// transport and their own remote process management. In general, implementations will use +// [WaitForAndRunArgon2OutOfProcessRequest] in the remote process and [NewOutOfProcessArgonKDF] +// for process management in the parent process. // // This function does not service watchdog requests, as the KDF request happens synchronously in the // current goroutine. If this is required, it needs to be implemented in supporting code that makes @@ -385,14 +391,16 @@ func NoArgon2OutOfProcessWatchdogHandler() Argon2OutOfProcessWatchdogHandler { // 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 [NewOutOfProcessArgon2KDF] on the parent side. +// the supplied io.WriteCloser. These will generally be connected to the process's os.Stdin and +// os.Stdout - at least they will need to be when using [NewOutOfProcessArgon2KDF] on the parent side, +// which this function is intended to be compatible with. // // This function will service watchdog requests from the parent process if a watchdog handler is supplied. // If supplied, it must match the corresponding monitor in the parent process. If not supplied, the default // [NoArgon2OutOfProcessWatchdogHandler] will be used. // -// In general, this is intended to be executed once in a process, before the process is discarded. +// This won't process more than one request, and in general is intended to be executed once in a process, +// before the process is discarded. This is how the function is used with [NewOutOfProcessArgon2KDF]. // // Note that Argon2 requests are serialized using a system-wide lock, which this function does not // explicitly release. If the lock is acquired, it returns a callback that the caller may choose @@ -429,14 +437,18 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.WriteCloser, wa tmb := new(tomb.Tomb) - // rspChan is the channel from the routines that process requests and run the KDF to the - // dedicated output routine which serializes the response to the supplied io.Writer. - rspChan := make(chan *Argon2OutOfProcessResponse) - // Spin up a routine for receiving requests from the supplied io.Reader. tmb.Go(func() error { - // Also spin-up the routine for sending outgoing responses that are generated internally. - // This handles the read end of rspChan, and serializes responses to the supplied io.Writer. + // reqChan is sent requests from this routine which are received by the dedicated + // KDF routine. + reqChan := make(chan *Argon2OutOfProcessRequest) + + // rspChan is sent responses from the KDF routine or watchdog, which are then received + // by a dedicated output routine which serializes the response to the supplied io.Writer. + rspChan := make(chan *Argon2OutOfProcessResponse) + + // Spin-up the routine for sending outgoing responses that are generated internally. + // This handles the read end of rspChan, and serializes responses to the supplied io.WriteCloser. // This gets its own goroutine so that all responses are sent via the same code path - responses // can ultimately come directly from the request processing loop in this routine (in the event // of a watchdog request), or from a dedicated KDF routine which permits the request processing @@ -479,6 +491,55 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.WriteCloser, wa return out.Close() }) + // Spin up a goroutine for running the KDF without blocking the request handling + // loop on this routine. This reads from reqChan. + tmb.Go(func() error { + select { + case req := <-reqChan: + // Run the KDF request. This performs a lot of checking of the supplied + // request, so there's no need to repeat any of that here. + rsp, release := RunArgon2OutOfProcessRequest(req) + + // Ensure the release callback for the system lock gets returned + // to the caller. + lockRelease = release + + // Send the response. + select { + case rspChan <- rsp: // Unbuffered channel, but read end is always there unless the tomb is dying. + case <-tmb.Dying(): + // The tomb began dying before the response was sent, + // so exit early. + return tomb.ErrDying + } + case <-tmb.Dying(): + return tomb.ErrDying + } + + // We don't handle any more requests. Run a loop for processing additional + // requests in order to return errors, until the tomb enters a dying state. + for tmb.Alive() { + select { + case req := <-reqChan: + rsp := &Argon2OutOfProcessResponse{ + Command: req.Command, + ErrorType: Argon2OutOfProcessErrorInvalidCommand, + ErrorString: "a command has already been executed", + } + // Send the response. + select { + case rspChan <- rsp: // Unbuffered channel, but read end is always there unless the tomb is dying. + case <-tmb.Dying(): + // The tomb began dying before the response was sent. The + // loop won't run again. + } + case <-tmb.Dying(): + // The loop won't run again. + } + } + return tomb.ErrDying + }) + // Run a loop for receiving and processing incoming requests from the io.Reader as // long as the tomb remains alive. for tmb.Alive() { @@ -533,53 +594,14 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.WriteCloser, wa } default: // Treat everything else as a KDF request. We don't actually check the - // command here - RunArgon2OutOfProcessRequest does this already and will - // return an error response if the command is invalid. - - // Spin up a new routine to handle the request, as it blocks and is long running, - // and we still want to be able to service watchdog requests on this routine whilst - // it's running. Block the current routine until we know the new routine has started - // so that the watchdog handler will fail if the new routine doesn't begin properly. - startupCh := make(chan struct{}) - - tmb.Go(func() error { - close(startupCh) - - // Run the KDF request. This performs a lot of checking of the supplied - // request, so there's no need to repeat any of that here. - rsp, release := RunArgon2OutOfProcessRequest(req) - - if release != nil { - // Although theoretically this process could be sent more than one request - // (although the parent side implementation in secboot won't do this), - // subsequent calls to RunArgon2OutOfProcessRequest eventually all timeout, - // unable to acquire another system-wide lock (it will be acquired on the first - // request and the return value will contain the release callback already). In - // this case, there will be no new release callback to overwrite the one that - // the return value was set to on the first request. - lockRelease = release - } - - // Send the response. - select { - case rspChan <- rsp: // Unbuffered channel, but read end is always there unless the tomb is dying. - case <-tmb.Dying(): - // The tomb began dying before the response was sent, - // so exit early. - return tomb.ErrDying - } - - // Although we're only meant to handle a single request, and we've - // done that now, don't put the tomb into a dying state yet. Wait - // for the parent process to close its end of the request channel - // first to avoid race conditions with an in-flight watchdog request - // from the parent process. - return nil - }) - - // Wait here for the KDF handler routine to startup. This should never fail to start-up, - // but doing this blocks the processing of watchdog requests temporarily. - <-startupCh + // request here - this is done by RunArgon2OutOfProcessRequest which runs + // on a dedicated routine. + select { + case reqChan <- req: // Unbuffered channel, but read end is always there unless the tomb is dying. + case <-tmb.Dying(): + // The tomb began dying before the rquest was sent, so + // the outer loop won't run again. + } } } return tomb.ErrDying @@ -758,9 +780,9 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out // The remote process may release the system-wide lock implicitly on process // termination. In this case, we make an attempt to cleanup the lock-file on // behalf of the remote process. This isn't strictly necessary, which is why - // we set the timeout to 0 - we don't want to wait if someone else has already - // managed to grab the lock and we don't want to delay the return of the response - // from this function. + // we set the timeout to 0, which makes it completely non-blocking - we don't + // want to wait if someone else has already managed to grab the lock and we + // don't want to delay the return of the response from this function. release, err := acquireArgon2OutOfProcessHandlerSystemLock(0) if err != nil { // We didn't acquire the lock with a single attempt, so never mind. @@ -810,7 +832,7 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out // serialize then and sends them to the remote process via its stdin. reqChan := make(chan *Argon2OutOfProcessRequest) - // Spin up a routine for sending requests to the remote process via stdinPipe. + // Spin up a routine for encoding and sending requests to the remote process via stdinPipe. tmb.Go(func() error { // Run a loop to send requests as long as the tomb is alive. for tmb.Alive() { @@ -828,7 +850,7 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out return tomb.ErrDying }) - // Send the main request before starting the watchdog or running the response loop + // Send the main request before starting the watchdog or running the response loop. select { case reqChan <- req: // Unbuffered channel, but read end is always there unless the tomb is dying. case <-tmb.Dying(): diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 042de319..4dd72a85 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -171,8 +171,8 @@ func (s *argon2OutOfProcessHandlerSupportMixin) testWaitForAndRunArgon2OutOfProc return tomb.ErrDying }) - // Wait for the tomb to begin dying. The test could block here indefinitely if - // we never get a response. + // Wait for the tomb to begin dying. Bugs in WaitForAndRunArgon2OutOfProcessRequest + // could cause the test to block here indefinitely. <-tmb.Dying() // Closing our end of the request channel supplied to the test function, as @@ -895,7 +895,7 @@ func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2 release() } -func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestPassphrase(c *C) { +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentPassphrase(c *C) { rsp, release, err := s.testWaitForAndRunArgon2OutOfProcessRequest(c, &testWaitForAndRunArgon2OutOfProcessRequestParams{ req: &Argon2OutOfProcessRequest{ Command: Argon2OutOfProcessCommandDerive, @@ -1051,6 +1051,78 @@ func (s *argon2OutOfProcessHandlerSupportSuite) TestWaitForAndRunArgon2OutOfProc s.checkNoLockFile(c) } +func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestMultiple(c *C) { + // Create 2 pipes to communicate with WaitForAndRunArgon2OutOfProcessRequest + reqR, reqW := io.Pipe() + rspR, rspW := io.Pipe() + + var rsp *Argon2OutOfProcessResponse + tmb := new(tomb.Tomb) // The tomb for tracking goroutines + + // Spin up a dedicated routine for running the test function + // (WaitForAndRunArgon2OutOfProcessRequest), passing it one end + // of each pipe. + tmb.Go(func() error { + release, err := WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW, nil) + if release != nil { + defer release() + } + return err + }) + + req := &Argon2OutOfProcessRequest{ + Command: Argon2OutOfProcessCommandDerive, + Passphrase: "foo", + Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"), + Keylen: 32, + Mode: Argon2id, + Time: 4, + MemoryKiB: 32, + Threads: 4, + } + + // Note that bugs in WaitForAndRunArgon2OutOfProcessRequest could cause the test to + // block here indefinitely on any request / response. + enc := json.NewEncoder(reqW) + c.Check(enc.Encode(req), IsNil) + + // Wait for the first response + dec := json.NewDecoder(rspR) + c.Check(dec.Decode(&rsp), IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + Key: testutil.DecodeHexString(c, "7306196ab24ea3ac9daab7f14345a9dc228dccef07075dbd2e047deac96689ea"), + }) + + rsp = nil + + // Send the second request + c.Check(enc.Encode(req), IsNil) + + // Wait for the second response + c.Check(dec.Decode(&rsp), IsNil) + c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{ + Command: Argon2OutOfProcessCommandDerive, + ErrorType: Argon2OutOfProcessErrorInvalidCommand, + ErrorString: "a command has already been executed", + }) + + // Closing our end of the request channel supplied to the test function, as + // a real parent process would, should be sufficient to begin termination + // of the test function (WaitForAndRunArgon2OutOfProcessRequest) on the remote + // side. + // + // Note that the test will block indefinitely on waiting for the tomb to fully + // die if this doesn't work properly, which isn't ideal. + c.Check(reqW.Close(), IsNil) + + // Wait for everything to die, hopefully successfully. The test could block + // indefinitely here if WaitForAndRunArgon2OutOfProcessRequest doesn't return + // when we closed our end of the request channel above, or if the supplied + // watchdog monitor misbehaves and doesn't return when it is supposed to. + c.Check(tmb.Wait(), IsNil) +} + func (s *argon2OutOfProcessParentSupportSuite) TestNoArgon2OutOfProcessWatchdogMonitorUnexpectedResponse(c *C) { monitor := NoArgon2OutOfProcessWatchdogMonitor() From 586bc6b016141b0d5690f15572496e96f59d38b7 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 17 Jan 2025 10:42:31 +0000 Subject: [PATCH 33/40] argon2: Add a comment about not launching sub-processes --- argon2_out_of_process_support.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 9c618640..d752c8ca 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -813,6 +813,11 @@ func (k *outOfProcessArgon2KDFImpl) sendRequestAndWaitForResponse(req *Argon2Out // has a new feature (WaitDelay) which might make things a bit better // here because I don't know how racey things are here - exec.Cmd is // quite complicated. + // + // Note that this only kills the process launched by us - it's not expected + // that processes launched to handle KDF requests fork or clone any other + // processes, as these will continue running, being reparented to the nearest + // reaper. if err := cmd.Process.Kill(); err != nil { if err != os.ErrProcessDone { return fmt.Errorf("failed to kill blocked remote process: %w", err) From 7a87d2b7f9606144ce64ee7645b6344ebe31ccee Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 17 Jan 2025 13:15:06 +0000 Subject: [PATCH 34/40] argon2: Ensure a panic in the KDF is handled correctly The gopkg.in/tomb.v2 package does not handle routines that panic very well - the active routine count will not decrement and it won't put the tomb into a dying state. In fact, it looks like it makes it impossible for the tomb to ever reach a dead state. If the Argon2 crypto package panics, the KDF routine will disappear but the child process will carry on processing watchdog requests despite the fact that the parent is never going to get an answer. We recover panics on the KDF routine and turn them into errors that put the tomb into a dying state. This begins the teardown of the child process and halts the processing of watchdog requests. For this reason, the parent may see 1 of 2 errors - either an error indicating that the child process exited with a non-zero exit code, or an error indicating a watchdog timeout. The test added for this handles both cases. This also introduces a small package - internal/testenv - with a single function (testenv.IsTestBinary) that can be used in production code and requires some special linker flags to enable. This permits us to export a function from the core secboot library to mock an internal function that should only be accessible in unit tests (and should panic otherwise). --- argon2_out_of_process_support.go | 32 +++++++++++++++++++++++++-- argon2_out_of_process_support_test.go | 22 ++++++++++++++++-- cmd/run_argon2/main.go | 15 +++++++++++-- internal/testenv/testenv.go | 30 +++++++++++++++++++++++++ run-tests | 2 +- 5 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 internal/testenv/testenv.go diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index d752c8ca..24eaa183 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -33,6 +33,7 @@ import ( "strings" "time" + "github.com/snapcore/secboot/internal/testenv" "gopkg.in/tomb.v2" ) @@ -389,6 +390,22 @@ func NoArgon2OutOfProcessWatchdogHandler() Argon2OutOfProcessWatchdogHandler { } } +var runArgon2OutOfProcessRequest = RunArgon2OutOfProcessRequest + +// MockRunArgon2OutOfProcessRequestForTest mocks the call to [RunArgon2OutOfProcessRequest] +// from [WaitForAndRunArgon2OutOfProcessRequest]. This can only be used in test binaries, and +// will panic otherwise. +func MockRunArgon2OutOfProcessRequestForTest(fn func(*Argon2OutOfProcessRequest) (*Argon2OutOfProcessResponse, func())) (restore func()) { + if !testenv.IsTestBinary() { + panic("not a test binary") + } + orig := runArgon2OutOfProcessRequest + runArgon2OutOfProcessRequest = fn + return func() { + runArgon2OutOfProcessRequest = orig + } +} + // WaitForAndRunArgon2OutOfProcessRequest waits for a [Argon2OutOfProcessRequest] request on the // supplied io.Reader before running it and sending a [Argon2OutOfProcessResponse] response back via // the supplied io.WriteCloser. These will generally be connected to the process's os.Stdin and @@ -493,12 +510,23 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.WriteCloser, wa // Spin up a goroutine for running the KDF without blocking the request handling // loop on this routine. This reads from reqChan. - tmb.Go(func() error { + tmb.Go(func() (err error) { + defer func() { + // The tomb package doesn't handle panics very well - it won't result + // in the routine count being decremented and nor will it put it into + // a dying state. Ensure we put it into a dying state if we encounter + // a panic, else this routine will disappear and we'll continue serving + // watchdog requests forever. + if r := recover(); r != nil { + err = fmt.Errorf("goroutine for KDF encountered a panic: %v", r) + } + }() + select { case req := <-reqChan: // Run the KDF request. This performs a lot of checking of the supplied // request, so there's no need to repeat any of that here. - rsp, release := RunArgon2OutOfProcessRequest(req) + rsp, release := runArgon2OutOfProcessRequest(req) // Ensure the release callback for the system lock gets returned // to the caller. diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 4dd72a85..9ed42b57 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -228,8 +228,8 @@ type argon2OutOfProcessParentSupportMixin struct { func (s *argon2OutOfProcessParentSupportMixin) SetUpSuite(c *C) { 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) + cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), "build", "-ldflags", "-X github.com/snapcore/secboot/internal/testenv.testBinary=enabled", "-o", s.runArgon2OutputDir, "./cmd/run_argon2") + c.Check(cmd.Run(), IsNil) } func (s *argon2OutOfProcessParentSupportMixin) SetUpTest(c *C) { @@ -1352,6 +1352,24 @@ func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GBDi s.checkNoLockFile(c) } +func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDeriveKDFPanic(c *C) { + kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256", "kdf_panic"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*time.Millisecond)) + params := &Argon2CostParams{ + Time: 4, + MemoryKiB: 2 * 1024 * 1024, + Threads: 4, + } + _, err := kdf.Derive("bar", testutil.DecodeHexString(c, "5d53157092d5f97034c0d3fd078b8f5c"), Argon2id, params, 32) + // We could get 1 of 2 errors here - either we notice that the process has terminated early, or we get a watchdog failure + var wdErr *Argon2OutOfProcessWatchdogError + if errors.As(err, &wdErr) { + c.Check(err, ErrorMatches, `watchdog failure: timeout waiting for watchdog response from remote process`) + } else { + c.Check(err, ErrorMatches, `an error occurred whilst waiting for the remote process to finish: exit status 1`) + } + s.checkNoLockFile(c) +} + func (s *argon2OutOfProcessParentSupportSuite) TestArgon2KDFDeriveErr(c *C) { kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("none"), 0, nil) params := &Argon2CostParams{ diff --git a/cmd/run_argon2/main.go b/cmd/run_argon2/main.go index d3635dad..f26a63e6 100644 --- a/cmd/run_argon2/main.go +++ b/cmd/run_argon2/main.go @@ -26,6 +26,7 @@ import ( "errors" "fmt" "os" + "time" "github.com/snapcore/secboot" "github.com/snapcore/secboot/internal/paths" @@ -33,17 +34,19 @@ import ( func run() error { if len(os.Args) < 3 { - return errors.New("usage: echo | run_argon2 []") + return errors.New("usage: echo | run_argon2 [] [kdf_panic]") } paths.Argon2OutOfProcessHandlerSystemLockPath = os.Args[1] var watchdog secboot.Argon2OutOfProcessWatchdogHandler + var remaining []string switch os.Args[2] { case "none": watchdog = secboot.NoArgon2OutOfProcessWatchdogHandler() + remaining = os.Args[3:] case "hmac": - if len(os.Args) != 4 { + if len(os.Args) < 4 { return errors.New("usage: echo | run_argon2 hmac ") } var alg crypto.Hash @@ -62,6 +65,14 @@ func run() error { return fmt.Errorf("unrecognized HMAC digest algorithm %q", os.Args[3]) } watchdog = secboot.HMACArgon2OutOfProcessWatchdogHandler(alg) + remaining = os.Args[4:] + } + + if len(remaining) > 0 && remaining[0] == "kdf_panic" { + secboot.MockRunArgon2OutOfProcessRequestForTest(func(*secboot.Argon2OutOfProcessRequest) (*secboot.Argon2OutOfProcessResponse, func()) { + <-time.NewTimer(500 * time.Millisecond).C + panic("KDF panic") + }) } // Ignore the lock release callback and use implicit release on process termination. diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go new file mode 100644 index 00000000..01ebe4e8 --- /dev/null +++ b/internal/testenv/testenv.go @@ -0,0 +1,30 @@ +// -*- 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 testenv + +var testBinary string = "" + +// IsTestBinary returns whether the current binary is a test binary. To +// define something as a test binary and make this return true, pass +// "-ldflags '-X github.com/snapcore/secboot/internal/testenv.testBinary=enabled'" +// to "go build" or "go test". +func IsTestBinary() bool { + return testBinary == "enabled" +} diff --git a/run-tests b/run-tests index 00ac8251..866cf498 100755 --- a/run-tests +++ b/run-tests @@ -23,4 +23,4 @@ while [ $# -gt 0 ]; do esac done -env $ENV go test -v -race -p 1 -timeout 20m ./... -args -check.v $@ +env $ENV go test -ldflags '-X github.com/snapcore/secboot/internal/testenv.testBinary=enabled' -v -race -p 1 -timeout 20m ./... -args -check.v $@ From 625db2688d38f0935ab0e071a22e047966daee7e Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 17 Jan 2025 13:25:28 +0000 Subject: [PATCH 35/40] internal/paths: Use a snapd agnostic path for the Argon2 lock file --- argon2_out_of_process_support_sync_test.go | 2 +- argon2_out_of_process_support_test.go | 2 +- internal/paths/paths.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/argon2_out_of_process_support_sync_test.go b/argon2_out_of_process_support_sync_test.go index 8ec94120..8181e6da 100644 --- a/argon2_out_of_process_support_sync_test.go +++ b/argon2_out_of_process_support_sync_test.go @@ -44,7 +44,7 @@ func (s *argon2OutOfProcessSupportSyncSuite) SetUpSuite(c *C) { } func (s *argon2OutOfProcessSupportSyncSuite) SetUpTest(c *C) { - s.lockPath = filepath.Join(c.MkDir(), "argon2.lock") + s.lockPath = filepath.Join(c.MkDir(), "secboot_argon2.lock") s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(s.lockPath) } diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 9ed42b57..0bdf3743 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -51,7 +51,7 @@ type argon2OutOfProcessHandlerSupportMixin struct { } func (s *argon2OutOfProcessHandlerSupportMixin) SetUpTest(c *C) { - s.lockPath = filepath.Join(c.MkDir(), "argon2.lock") + s.lockPath = filepath.Join(c.MkDir(), "secboot_argon2.lock") s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(s.lockPath) } diff --git a/internal/paths/paths.go b/internal/paths/paths.go index 6510e847..6d80ba59 100644 --- a/internal/paths/paths.go +++ b/internal/paths/paths.go @@ -27,5 +27,5 @@ var ( // Argon2OutOfProcessHandlerSystemLockPath is the lock file path used to // serialize KDF requests system-wide. All process's that use the system-wide // lock participate in the lock/unlock contract described above. - Argon2OutOfProcessHandlerSystemLockPath = filepath.Join(RunDir, "snapd/argon2.lock") + Argon2OutOfProcessHandlerSystemLockPath = filepath.Join(RunDir, "secboot_argon2.lock") ) From 8883d0406c86265dd5f37186dccb6521c1da7088 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 17 Jan 2025 13:28:20 +0000 Subject: [PATCH 36/40] argon2: Add a note explaining why we don't validate the MemoryKiB cost parameter --- argon2_out_of_process_support.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 24eaa183..8bda6363 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -236,6 +236,11 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) (response }, nil } + // We don't validate the MemoryKiB parameter here. The Argon2 crypto package we use + // will round up this value to the minimum required, which is 8KiB per thread (so if + // we pass MemoryKiB==0 and Threads==4, then MemoryKiB will automatically be increased + // to 32KiB). + if len(request.WatchdogChallenge) > 0 { // This function does everything in the same go routine, and therefore // has no ability to service a watchdog. It's an error if we get here From e094dfa49944952a72febcc11a670ce49befb230 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 17 Jan 2025 13:33:57 +0000 Subject: [PATCH 37/40] argon2: Don't use type aliases for the watchdog function templates --- argon2_out_of_process_support.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/argon2_out_of_process_support.go b/argon2_out_of_process_support.go index 8bda6363..ff7cc725 100644 --- a/argon2_out_of_process_support.go +++ b/argon2_out_of_process_support.go @@ -362,7 +362,7 @@ func RunArgon2OutOfProcessRequest(request *Argon2OutOfProcessRequest) (response // // The implementation is expected to be paired with an equivalent implementation of // [Argon2OutOfProcessWatchdogMonitor] in the parent process. -type Argon2OutOfProcessWatchdogHandler = func(challenge []byte) (response []byte, err error) +type Argon2OutOfProcessWatchdogHandler func(challenge []byte) (response []byte, err error) // HMACArgon2OutOfProcessWatchdogHandler returns the remote process counterpart to // [HMACArgon2OutOfProcessWatchdogMonitor]. It receives a challenge from the monitor, @@ -676,7 +676,7 @@ func WaitForAndRunArgon2OutOfProcessRequest(in io.Reader, out io.WriteCloser, wa // // The supplied rspChan is unbuffered. The monitor implementation should guarantee that // there is a reader as long as the supplied tomb is alive. -type Argon2OutOfProcessWatchdogMonitor = func(tmb *tomb.Tomb, reqChan chan<- *Argon2OutOfProcessRequest, rspChan <-chan *Argon2OutOfProcessResponse) error +type Argon2OutOfProcessWatchdogMonitor func(tmb *tomb.Tomb, reqChan chan<- *Argon2OutOfProcessRequest, rspChan <-chan *Argon2OutOfProcessResponse) error // HMACArgon2OutOfProcessWatchdogMonitor returns a watchdog monitor that generates a // challenge every period, computes a HMAC of this challenge, keyed with previously received From a264de8b8c086d67472d2ea2ae020ffd1a047092 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 17 Jan 2025 13:39:54 +0000 Subject: [PATCH 38/40] argon2: Pass O_NOFOLLOW when opening the lock file I was going to also pass O_CLOEXEC, but the go runtime already does this. --- argon2_out_of_process_support_sync.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/argon2_out_of_process_support_sync.go b/argon2_out_of_process_support_sync.go index f0b1c95a..e24369b8 100644 --- a/argon2_out_of_process_support_sync.go +++ b/argon2_out_of_process_support_sync.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "os" + "syscall" "time" "github.com/snapcore/secboot/internal/paths" @@ -146,7 +147,7 @@ func acquireArgon2OutOfProcessHandlerSystemLock(timeout time.Duration) (release } // Attempt to open the lock file for writing. - lockFile, err = os.OpenFile(paths.Argon2OutOfProcessHandlerSystemLockPath, os.O_RDWR|os.O_CREATE, 0600) + lockFile, err = os.OpenFile(paths.Argon2OutOfProcessHandlerSystemLockPath, os.O_RDWR|os.O_CREATE|syscall.O_NOFOLLOW, 0600) if err != nil { // No error is expected here. return nil, fmt.Errorf("cannot open lock file for writing: %w", err) From 0e6e3865f248ed3f583ff700d784f45ed438bd0a Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 17 Jan 2025 14:10:16 +0000 Subject: [PATCH 39/40] argon2: make sure the lock file is a regular file --- argon2_out_of_process_support_sync.go | 5 +++++ argon2_out_of_process_support_sync_test.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/argon2_out_of_process_support_sync.go b/argon2_out_of_process_support_sync.go index e24369b8..b6348cf1 100644 --- a/argon2_out_of_process_support_sync.go +++ b/argon2_out_of_process_support_sync.go @@ -160,6 +160,11 @@ func acquireArgon2OutOfProcessHandlerSystemLock(timeout time.Duration) (release return nil, fmt.Errorf("cannot obtain lock file info from open descriptor: %w", err) } + // Make sure we have opened a regular file + if lockFileSt.Mode&syscall.S_IFMT != syscall.S_IFREG { + return nil, errors.New("opened lock file is not a regular file") + } + // Attempt to acquire an exclusive, non-blocking, advisory lock. if err := unix.Flock(int(lockFile.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { // We failed to acquire the lock. diff --git a/argon2_out_of_process_support_sync_test.go b/argon2_out_of_process_support_sync_test.go index 8181e6da..4d299ae6 100644 --- a/argon2_out_of_process_support_sync_test.go +++ b/argon2_out_of_process_support_sync_test.go @@ -21,6 +21,7 @@ package secboot_test import ( "errors" + "fmt" "os" "path/filepath" "syscall" @@ -180,3 +181,10 @@ func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfPro _, err = os.Stat(s.lockPath) c.Check(os.IsNotExist(err), testutil.IsTrue) } + +func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireArgon2OutOfProcessHandlerSystemLockErrorDir(c *C) { + os.Mkdir(s.lockPath, 0755) + + _, err := AcquireArgon2OutOfProcessHandlerSystemLock(0) + c.Assert(err, ErrorMatches, fmt.Sprintf("cannot open lock file for writing: open %s: is a directory", s.lockPath)) +} From 77dcb36aa5dfe8cadbeda00e4f27cedd7803e93c Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Thu, 23 Jan 2025 16:16:16 +0000 Subject: [PATCH 40/40] internal/paths: update the Argon2 lock filename --- argon2_out_of_process_support_sync_test.go | 2 +- argon2_out_of_process_support_test.go | 2 +- internal/paths/paths.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/argon2_out_of_process_support_sync_test.go b/argon2_out_of_process_support_sync_test.go index 4d299ae6..9843581e 100644 --- a/argon2_out_of_process_support_sync_test.go +++ b/argon2_out_of_process_support_sync_test.go @@ -45,7 +45,7 @@ func (s *argon2OutOfProcessSupportSyncSuite) SetUpSuite(c *C) { } func (s *argon2OutOfProcessSupportSyncSuite) SetUpTest(c *C) { - s.lockPath = filepath.Join(c.MkDir(), "secboot_argon2.lock") + s.lockPath = filepath.Join(c.MkDir(), "secboot-argon2.lock") s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(s.lockPath) } diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go index 0bdf3743..355fa7db 100644 --- a/argon2_out_of_process_support_test.go +++ b/argon2_out_of_process_support_test.go @@ -51,7 +51,7 @@ type argon2OutOfProcessHandlerSupportMixin struct { } func (s *argon2OutOfProcessHandlerSupportMixin) SetUpTest(c *C) { - s.lockPath = filepath.Join(c.MkDir(), "secboot_argon2.lock") + s.lockPath = filepath.Join(c.MkDir(), "secboot-argon2.lock") s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(s.lockPath) } diff --git a/internal/paths/paths.go b/internal/paths/paths.go index 6d80ba59..504e5235 100644 --- a/internal/paths/paths.go +++ b/internal/paths/paths.go @@ -27,5 +27,5 @@ var ( // Argon2OutOfProcessHandlerSystemLockPath is the lock file path used to // serialize KDF requests system-wide. All process's that use the system-wide // lock participate in the lock/unlock contract described above. - Argon2OutOfProcessHandlerSystemLockPath = filepath.Join(RunDir, "secboot_argon2.lock") + Argon2OutOfProcessHandlerSystemLockPath = filepath.Join(RunDir, "secboot-argon2.lock") )