diff --git a/.gitignore b/.gitignore
index 5a46d357..75bf717e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
+./run_argon2
vendor/*/
diff --git a/argon2.go b/argon2.go
index 20479ae5..8f4bac09 100644
--- a/argon2.go
+++ b/argon2.go
@@ -33,19 +33,26 @@ 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
)
-// 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 returns the previously configured Argon2KDF instance.
+//
+// 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 {
argon2Mu.Lock()
defer argon2Mu.Unlock()
@@ -59,13 +66,17 @@ func SetArgon2KDF(kdf Argon2KDF) Argon2KDF {
return orig
}
+// argon2KDF returns the global [Argon2KDF] implementation set for this process. This
+// 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 (
@@ -119,6 +130,7 @@ func (o *Argon2Options) kdfParams(keyLen uint32) (*kdfParams, error) {
mode := o.Mode
if mode == Argon2Default {
+ // Select the hybrid mode by default.
mode = Argon2id
}
@@ -126,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)
@@ -157,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 specified explicitly
benchmarkParams.MaxMemoryCostKiB = o.MemoryKiB // this is capped to 4GiB by internal/argon2.
}
if o.TargetDuration != 0 {
@@ -172,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,
@@ -217,7 +233,9 @@ 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. See
+// [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.
@@ -246,10 +264,27 @@ func (_ inProcessArgon2KDFImpl) Time(mode Argon2Mode, params *Argon2CostParams)
return argon2.KeyDuration(argon2.Mode(mode), params.internalParams())
}
-// InProcessArgon2KDF is the in-process implementation of the Argon2 KDF. This
-// shouldn't be used in long-lived system processes - these processes should
-// instead provide their own KDF implementation which delegates to a short-lived
-// utility process which will use the in-process implementation.
+// InProcessArgon2KDF is the in-process implementation of the Argon2 KDF.
+//
+// This shouldn't be used in long-lived system processes. As Argon2 intentionally
+// allocates a lot of memory and go is garbage collected, it may be some time before
+// the large amounts of memory it allocates are freed and made available to other code
+// or other processes on the system. Consecutive calls can rapidly result in the
+// application being unable to allocate more memory, and even worse, may trigger the
+// kernel's OOM killer. Whilst implementations can call [runtime.GC] 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 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
new file mode 100644
index 00000000..ff7cc725
--- /dev/null
+++ b/argon2_out_of_process_support.go
@@ -0,0 +1,1080 @@
+// -*- 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"
+ "crypto"
+ "crypto/hmac"
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "github.com/snapcore/secboot/internal/testenv"
+ "gopkg.in/tomb.v2"
+)
+
+// 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"
+
+ // 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
+ 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
+ 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]
+// 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"
+
+ // Argon2OutOfProcessErrorUnexpectedInput means that there was an error with the combination
+ // of inputs associated with the supplied request.
+ Argon2OutOfProcessErrorUnexpectedInput Argon2OutOfProcessErrorType = "unexpected-input"
+
+ // Argon2OutOfProcessErrorTimeout means that the specified command timeout expired before
+ // the request was given a chance to start.
+ Argon2OutOfProcessErrorKDFTimeout Argon2OutOfProcessErrorType = "timeout-error"
+
+ // Argon2OutOfProcessErrorUnexpected means that an unexpected error occurred without
+ // 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"
+)
+
+// 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"
+ 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]
+// if the response indicates an error, or directly from methods of the [Argon2KDF]
+// implementation created by [NewOutOfProcessArgon2KDF] 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 {
+ var b strings.Builder
+ b.WriteString("cannot process request: " + string(e.ErrorType))
+ if e.ErrorString != "" {
+ b.WriteString(" (" + e.ErrorString + ")")
+ }
+ return b.String()
+}
+
+// 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
+ }
+ return &Argon2OutOfProcessError{
+ ErrorType: o.ErrorType,
+ ErrorString: o.ErrorString,
+ }
+}
+
+// Argon2OutOfProcessWatchdogError is returned from [Argon2KDF] instances created by
+// [NewOutOfProcessArgon2KDF] in the event of a watchdog failure.
+type Argon2OutOfProcessWatchdogError struct {
+ err error
+}
+
+// Error implements the error interface
+func (e *Argon2OutOfProcessWatchdogError) Error() string {
+ return "watchdog failure: " + e.err.Error()
+}
+
+func (e *Argon2OutOfProcessWatchdogError) Unwrap() error {
+ return e.err
+}
+
+// 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
+}
+
+// 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)
+}
+
+// RunArgon2OutOfProcessRequest runs the specified Argon2 request, and returns a response.
+//
+// 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 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 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
+// 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 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) (response *Argon2OutOfProcessResponse, lockRelease func()) {
+ // Perform checks of arguments that are common to call requests
+ switch request.Mode {
+ case Argon2id, Argon2i:
+ // ok
+ default:
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorInvalidMode,
+ ErrorString: fmt.Sprintf("mode cannot be %q", string(request.Mode)),
+ }, nil
+ }
+
+ costParams := &Argon2CostParams{
+ Time: request.Time,
+ MemoryKiB: request.MemoryKiB,
+ Threads: request.Threads,
+ }
+ if costParams.Time == 0 {
+ return &Argon2OutOfProcessResponse{
+ 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
+ }
+
+ // 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
+ // with a watchdog request.
+ return &Argon2OutOfProcessResponse{
+ 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
+ }
+
+ // Acquire the system-wide lock.
+ var err error
+ lockRelease, 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),
+ }, nil
+ }
+
+ // We have the system-wide lock - execute the command
+ switch request.Command {
+ case Argon2OutOfProcessCommandDerive:
+ // Perform key derivation
+ key, err := InProcessArgon2KDF.Derive(request.Passphrase, request.Salt, request.Mode, costParams, request.Keylen)
+ if err != nil {
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorUnexpected,
+ ErrorString: fmt.Sprintf("cannot run derive command: %v", err),
+ }, lockRelease
+ }
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ Key: key,
+ }, lockRelease
+ case Argon2OutOfProcessCommandTime:
+ // Perform timing of the supplied cost parameters.
+ duration, err := InProcessArgon2KDF.Time(request.Mode, costParams)
+ if err != nil {
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ ErrorType: Argon2OutOfProcessErrorUnexpected,
+ ErrorString: fmt.Sprintf("cannot run time command: %v", err),
+ }, lockRelease
+ }
+ return &Argon2OutOfProcessResponse{
+ Command: request.Command,
+ Duration: duration,
+ }, lockRelease
+ default:
+ panic("not reachable")
+ }
+}
+
+// Argon2OutOfProcessWatchdogHandler defines the behaviour of a watchdog handler
+// for the remote side of an out-of-process [Argon2KDF] implementation, using
+// [WaitForAndRunArgon2OutOfProcessRequest].
+//
+// 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
+// [Argon2OutOfProcessRequest] type has a WatchdogChallenge field (which is supplied
+// as an argument to this function. The [Argon2OutOfProcessResponse] type has a
+// WatchdogResponse field (which the response of this function is used for). It's up
+// to the implementation how they choose to use these fields.
+//
+// If the implementation returns an error, it begins the shutdown of the processing
+// of commands and the eventual return of [WaitForAndRunArgon2OutOfProcessRequest].
+//
+// 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)
+
+// HMACArgon2OutOfProcessWatchdogHandler returns the remote process counterpart to
+// [HMACArgon2OutOfProcessWatchdogMonitor]. It receives a challenge from the monitor,
+// computes a HMAC of this challenge, keyed with previously sent response. Both
+// implementations must use the same algorithm.
+//
+// This implementation of [Argon2OutOfProcessWatchdogHandler] never returns an error.
+func HMACArgon2OutOfProcessWatchdogHandler(alg crypto.Hash) Argon2OutOfProcessWatchdogHandler {
+ if !alg.Available() {
+ panic("digest algorithm unavailable")
+ }
+
+ lastResponse := make([]byte, 32)
+
+ return func(challenge []byte) (response []byte, err error) {
+ h := hmac.New(alg.New, lastResponse)
+ h.Write(challenge)
+
+ lastResponse = h.Sum(nil)
+ return lastResponse, nil
+ }
+}
+
+// NoArgon2OutOfProcessWatchdogHandler is an implmenentation of [Argon2OutOfProcessWatchdogHandler] that
+// provides no watchdog functionality. It is paired with [NoArgon2OutOfProcessWatchdogMonitor] on the
+// 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 watchdog request: no handler")
+ }
+}
+
+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
+// 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.
+//
+// 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
+// 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 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, 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()
+ }
+
+ tmb := new(tomb.Tomb)
+
+ // Spin up a routine for receiving requests from the supplied io.Reader.
+ tmb.Go(func() error {
+ // 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
+ // loop in this routine to continue executing whilst the KDF is running, so we can continue to
+ // process watchdog requests.
+ tmb.Go(func() error {
+ // Loop whilst the tomb is alive.
+ for tmb.Alive() {
+ // Wait for a response from somewhere or wait for the tomb to
+ // begin dying.
+ select {
+ case rsp := <-rspChan:
+ // 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(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():
+ // We've begun to die, and this loop will not run again.
+ }
+ }
+ 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()
+ })
+
+ // Spin up a goroutine for running the KDF without blocking the request handling
+ // loop on this routine. This reads from reqChan.
+ 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)
+
+ // 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() {
+ // 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 to the caller.
+ var req *Argon2OutOfProcessRequest
+ dec := json.NewDecoder(in)
+ 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, 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'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
+ }
+
+ // We have a request!
+
+ switch req.Command {
+ case Argon2OutOfProcessCommandWatchdog:
+ // 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.
+ 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:
+ // Treat everything else as a KDF request. We don't actually check the
+ // 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
+ })
+
+ // Wait here for the tomb to die and return the first error that occurred.
+ return lockRelease, tmb.Wait()
+}
+
+// Argon2OutOfProcessWatchdogMonitor defines the behaviour of a watchdog monitor
+// for out-of-process [Argon2KDF] implementations created by [NewOutOfProcessArgon2KDF],
+// and is managed on the parent side of an implementation of [Argon2KDF].
+//
+// 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
+// [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 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 [Argon2KDF] implementation
+// created by [NewOutOfProcessArgon2KDF] will not close rspChan.
+//
+// The [Argon2KDF] implementation created by [NewOutOfProcessArgon2KDF] will only send
+// watchdog requests via 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
+// 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 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() {
+ // Wait for the next tick
+ select {
+ case <-ticker.C:
+ case <-tmb.Dying():
+ // Handle the tomb dying before the end of the period.
+ return tomb.ErrDying
+ }
+
+ // 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 of the challenge, keyed with the
+ // last response.
+ h := hmac.New(alg.New, lastWatchdogResponse)
+ h.Write(challenge)
+ expectedWatchdogResponse := h.Sum(nil)
+
+ req := &Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandWatchdog,
+ WatchdogChallenge: challenge,
+ }
+
+ // Send the request.
+ 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 we finished sending the request (reqChan is blocking).
+ return tomb.ErrDying
+ }
+
+ // 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 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 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():
+ // The loop won't run again
+ }
+ }
+ return tomb.ErrDying
+ }
+}
+
+// NoArgon2OutOfProcessWatchdogMonitor is an implmenentation of Argon2OutOfProcessWatchdogMonitor that
+// provides no watchdog functionality. It is paired with [NoArgon2OutOfProcessWatchdogHandler] on the
+// remote side. It holds the watchdog goroutine in a parked state for the lifetime of the tomb.
+func NoArgon2OutOfProcessWatchdogMonitor() Argon2OutOfProcessWatchdogMonitor {
+ return func(tmb *tomb.Tomb, reqChan chan<- *Argon2OutOfProcessRequest, rspChan <-chan *Argon2OutOfProcessResponse) error {
+ select {
+ case <-tmb.Dying():
+ return tomb.ErrDying
+ case <-rspChan:
+ return errors.New("unexpected watchdog response")
+ }
+ }
+}
+
+type outOfProcessArgon2KDFImpl struct {
+ newHandlerCmd func() (*exec.Cmd, error)
+ timeout time.Duration
+ watchdog Argon2OutOfProcessWatchdogMonitor
+}
+
+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 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)
+ }
+
+ 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, 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.
+ 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
+
+ // 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() // Wait here until the tomb enters a dying state
+
+ 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.
+ //
+ // 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)
+ }
+ }
+ return errors.New("killed blocked remote process")
+ }
+ })
+
+ // 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 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 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() {
+ select {
+ case req := <-reqChan:
+ // 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():
+ // The tomb is dying, so this loop will stop iterating.
+ }
+ }
+ 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, 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 &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")
+ }
+ })
+
+ // 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.
+ // We do no special error handling here like we do on the remote side for the
+ // 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)
+ var rsp *Argon2OutOfProcessResponse
+ if err := dec.Decode(&rsp); err != nil {
+ 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.
+ 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 &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 tomb.ErrDying
+ })
+
+ // Wait here until the tomb enters a dying state.
+ <-tmb.Dying()
+
+ // [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 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)
+ }
+
+ // 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,
+ 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,
+ Timeout: k.timeout,
+ 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], which
+// will be expected to run in the remote 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.
+//
+// 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.
+// The supplied monitor must be paired with a matching handler on the remote side (passed to
+// [WaitForAndRunArgon2OutOfProcessRequest] for this to work properly. If no monitor is
+// supplied, [NoArgon2OutOfProcessWatchdogMonitor] is used, which provides no watchdog monitor
+// functionality.
+//
+// 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")
+ }
+ if watchdog == nil {
+ watchdog = NoArgon2OutOfProcessWatchdogMonitor()
+ }
+ 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..b6348cf1
--- /dev/null
+++ b/argon2_out_of_process_support_sync.go
@@ -0,0 +1,268 @@
+// -*- 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"
+ "syscall"
+ "time"
+
+ "github.com/snapcore/secboot/internal/paths"
+ "golang.org/x/sys/unix"
+)
+
+// 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
+// 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].
+//
+// 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 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, 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.
+
+var (
+ argon2SysLockStderr io.Writer = os.Stderr
+ acquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint = func() {}
+
+ 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
+// 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
+// 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 {
+ 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.
+ select {
+ case <-timeoutTimer.C:
+ // The timeout has expired.
+ return nil, errArgon2OutOfProcessHandlerSystemLockTimeout
+ case <-skipBackoffCh:
+ // continue trying without waiting
+ case <-time.NewTimer(100 * time.Millisecond).C:
+ // Wait for 100ms before trying again
+ }
+ }
+ 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|syscall.O_NOFOLLOW, 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)
+ }
+
+ // 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.
+ if os.IsTimeout(err) {
+ // The EWOULDBLOCK case. Someone else already has a lock on the
+ // file we have opened. Try again with a 100ms backoff time.
+ continue
+ }
+
+ // No other error is expected.
+ 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.
+
+ // 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.
+ skipBackoffCh <- struct{}{}
+ 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.
+ skipBackoffCh <- struct{}{}
+ }
+
+ 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..9843581e
--- /dev/null
+++ b/argon2_out_of_process_support_sync_test.go
@@ -0,0 +1,190 @@
+// -*- 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"
+ "fmt"
+ "os"
+ "path/filepath"
+ "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(), "secboot-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.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()
+
+ _, err = AcquireArgon2OutOfProcessHandlerSystemLock(1 * time.Second)
+ c.Check(err, Equals, ErrArgon2OutOfProcessHandlerSystemLockTimeout)
+}
+
+func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLockDeletedFile(c *C) {
+ 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 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()
+
+ 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.Stat(s.lockPath)
+ c.Check(os.IsNotExist(err), testutil.IsTrue)
+}
+
+func (s *argon2OutOfProcessSupportSyncSuite) TestAcquireAndReleaseArgon2OutOfProcessHandlerSystemLockChangedInode(c *C) {
+ 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()
+
+ 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.Stat(s.lockPath)
+ c.Check(os.IsNotExist(err), testutil.IsTrue)
+}
+
+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()
+
+ 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.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))
+}
diff --git a/argon2_out_of_process_support_test.go b/argon2_out_of_process_support_test.go
new file mode 100644
index 00000000..355fa7db
--- /dev/null
+++ b/argon2_out_of_process_support_test.go
@@ -0,0 +1,1458 @@
+// -*- 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 (
+ "crypto"
+ _ "crypto/sha256"
+ _ "crypto/sha512"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "runtime/debug"
+ "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"
+)
+
+// 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.lockPath = filepath.Join(c.MkDir(), "secboot-argon2.lock")
+ s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(s.lockPath)
+}
+
+func (s *argon2OutOfProcessHandlerSupportMixin) TearDownTest(c *C) {
+ if s.restoreLockPath != nil {
+ s.restoreLockPath()
+ }
+}
+
+func (s *argon2OutOfProcessHandlerSupportMixin) checkNoLockFile(c *C) {
+ _, err := os.Stat(s.lockPath)
+ c.Check(os.IsNotExist(err), testutil.IsTrue)
+}
+
+type testWaitForAndRunArgon2OutOfProcessRequestParams struct {
+ req *Argon2OutOfProcessRequest
+ wdHandler Argon2OutOfProcessWatchdogHandler
+ wdMonitor Argon2OutOfProcessWatchdogMonitor
+}
+
+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
+ 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() error {
+ var err error
+ release, err = WaitForAndRunArgon2OutOfProcessRequest(reqR, rspW, params.wdHandler)
+ return err
+ })
+
+ // 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
+ // 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 NewOutOfProcessArgonKDF.
+ 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)
+ 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 picked up by
+ // the watchdog handler.
+ select {
+ case wdRspChan <- rsp:
+ case <-tmb.Dying():
+ // This loop will no longer iterate
+ }
+ default:
+ // We got a response - begin the process of dying.
+ actualRsp = rsp
+ tmb.Kill(nil)
+ // This loop will no longer iterate
+ }
+ }
+ return tomb.ErrDying
+ })
+
+ // 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
+ // 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)
+
+ // 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
+ // 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)
+
+ return actualRsp, 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
+}
+
+func (s *argon2OutOfProcessParentSupportMixin) SetUpSuite(c *C) {
+ s.runArgon2OutputDir = c.MkDir()
+ 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) {
+ s.lockPath = filepath.Join(c.MkDir(), "argon2.lock")
+ s.restoreLockPath = MockArgon2OutOfProcessHandlerSystemLockPath(s.lockPath)
+}
+
+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
+ }
+}
+
+func (s *argon2OutOfProcessParentSupportMixin) checkNoLockFile(c *C) {
+ _, err := os.Stat(s.lockPath)
+ c.Check(os.IsNotExist(err), testutil.IsTrue)
+}
+
+type testHMACArgon2OutOfProcessWatchdogMonitorParams struct {
+ monitorAlg crypto.Hash
+ period 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)
+ 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
+ })
+
+ // 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()
+}
+
+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")
+ }
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TearDownTest(c *C) {
+ s.argon2OutOfProcessHandlerSupportMixin.TearDownTest(c)
+ runtime.GC() // Because we are running Argon2 in this process.
+}
+
+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)
+}
+
+var _ = Suite(&argon2OutOfProcessHandlerSupportSuite{})
+var _ = Suite(&argon2OutOfProcessHandlerSupportSuiteExpensive{})
+var _ = Suite(&argon2OutOfProcessParentSupportSuite{})
+var _ = Suite(&argon2OutOfProcessParentSupportSuiteExpensive{})
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidMode(c *C) {
+ out, release := 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\"",
+ })
+ c.Check(release, IsNil)
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidTime(c *C) {
+ out, release := 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: "time cannot be zero",
+ })
+ c.Check(release, IsNil)
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidThreads(c *C) {
+ out, release := 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: "threads cannot be zero",
+ })
+ c.Check(release, IsNil)
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessDeriveRequestInvalidWatchdogChallenge(c *C) {
+ out, release := 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",
+ })
+ c.Check(release, IsNil)
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidPassphrase(c *C) {
+ out, release := 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",
+ })
+ c.Check(release, IsNil)
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidSalt(c *C) {
+ out, release := 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",
+ })
+ c.Check(release, IsNil)
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessTimeRequestInvalidKeylen(c *C) {
+ out, release := 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",
+ })
+ c.Check(release, IsNil)
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestRunArgon2OutOfProcessInvalidCommand(c *C) {
+ out, release := 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: "command cannot be \"foo\"",
+ })
+ c.Check(release, IsNil)
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveMoreThanOnceWithRelease(c *C) {
+ out, release := 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"),
+ })
+ c.Assert(release, NotNil)
+ defer release()
+
+ out, release2 := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Timeout: 0,
+ 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: Argon2OutOfProcessErrorKDFTimeout,
+ ErrorString: "cannot acquire argon2 system lock: request timeout",
+ })
+ c.Check(release2, IsNil)
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveMinimum(c *C) {
+ out, release := 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentThreads(c *C) {
+ out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"),
+ Keylen: 32,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 32,
+ Threads: 1,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ Key: testutil.DecodeHexString(c, "5699b81ee10e189505874d0cbd93d61186b90554c716d309037907b7238113e1"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentTime(c *C) {
+ out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"),
+ Keylen: 32,
+ Mode: Argon2id,
+ Time: 5,
+ MemoryKiB: 32,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ Key: testutil.DecodeHexString(c, "2f2d7dd170cf43aff82737bc1c2fbe685b34190fc8b62378693c3b0685b96912"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentMemory(c *C) {
+ out, release := RunArgon2OutOfProcessRequest(&Argon2OutOfProcessRequest{
+ Command: Argon2OutOfProcessCommandDerive,
+ Passphrase: "foo",
+ Salt: testutil.DecodeHexString(c, "7ed928d8153e3084393d73f938ad3e03"),
+ Keylen: 32,
+ Mode: Argon2id,
+ Time: 4,
+ MemoryKiB: 64,
+ Threads: 4,
+ })
+ c.Check(out, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ Key: testutil.DecodeHexString(c, "6f49db1f7336329c0d5fd652642b144b204d7976c5fcb4c72b6e1d9ea345fa32"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentPassphrase(c *C) {
+ out, release := 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentSalt(c *C) {
+ out, release := 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentKeyLen(c *C) {
+ out, release := 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessDeriveDifferentMode(c *C) {
+ out, release := 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcess2GB(c *C) {
+ out, release := 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestRunArgon2OutOfProcessTime(c *C) {
+ out, release := 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))
+ c.Assert(release, NotNil)
+ release()
+
+ origDuration := out.Duration
+
+ // Permit calling the function again
+ runtime.GC()
+
+ out, release = 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)
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestHMACArgon2OutOfProcessWatchdogHandlerSHA256(c *C) {
+ handler := HMACArgon2OutOfProcessWatchdogHandler(crypto.SHA256)
+
+ rsp, err := handler(testutil.DecodeHexString(c, "3674f5b88f2e6b36ae94aa01f1ee16eaf9ab90df0979ae966837bcd37f0fa1fc"))
+ c.Check(err, IsNil)
+ c.Check(rsp, DeepEquals, testutil.DecodeHexString(c, "9086bd5b0208ac012a345839d7dd5e442db9597a882e2d328ebf35a2f27ce919"))
+
+ rsp, err = handler(testutil.DecodeHexString(c, "3c1de58760e53cac4facc2d5409b362fcf9b81f9b611479f5956abdb0227e567"))
+ c.Check(err, IsNil)
+ c.Check(rsp, DeepEquals, testutil.DecodeHexString(c, "3d746bc1f5c471ea9983596512ac846910facf966b611dc2c62e08203afc86f0"))
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestHMACArgon2OutOfProcessWatchdogHandlerSHA384(c *C) {
+ handler := HMACArgon2OutOfProcessWatchdogHandler(crypto.SHA384)
+
+ rsp, err := handler(testutil.DecodeHexString(c, "7b70dfe03ac13bf595061f0d454d10a3595b494277306fe3ed6cdc1c711199cf943bed96023dbd07699f1b6fcbe96574"))
+ c.Check(err, IsNil)
+ c.Check(rsp, DeepEquals, testutil.DecodeHexString(c, "5e1c3249bcf2e8c93dad1368ef2204fc0d497336ac0e4f260f8c39fa300dc9b9c7f9e19156b4f87c08c1b34537d7d2e1"))
+
+ rsp, err = handler(testutil.DecodeHexString(c, "dada0215efc0e034f431fce916caf73af7fd84ad24f9215d08959699745957c7e29190d214e8c1cda78c45a2f0bd4059"))
+ c.Check(err, IsNil)
+ c.Check(rsp, DeepEquals, testutil.DecodeHexString(c, "a36e2bda200e88620d53d32196bb6c49efa24c152009a7aac7fcb36e1b97ae2fb62ffc359c2247a8c2bb8f2e89f4b8a9"))
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuite) TestNoArgon2OutOfProcessWatchdogHandler(c *C) {
+ handler := NoArgon2OutOfProcessWatchdogHandler()
+ _, err := handler([]byte{1, 2, 3, 4})
+ c.Check(err, ErrorMatches, `unexpected watchdog request: no handler`)
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestMinimum(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: 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentThreads(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: 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentTime(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: 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentMemory(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: 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentPassphrase(c *C) {
+ rsp, release, 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentSalt(c *C) {
+ rsp, release, 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentKeyLen(c *C) {
+ rsp, release, 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(err, IsNil)
+ c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ Key: testutil.DecodeHexString(c, "21ab785e199d43575ca11e85e0a1281b4426c973cfad0a899b24bc4b8057355912a20b5f4132d8132ce3aa5bffe0d9a6a7fd05d3ab67898c196d584c98d47e44"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+func (s *argon2OutOfProcessHandlerSupportSuiteExpensive) TestWaitForAndRunArgon2OutOfProcessRequestDifferentMode(c *C) {
+ rsp, release, 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"),
+ })
+ c.Assert(release, NotNil)
+ release()
+}
+
+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,
+ 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, 200*time.Millisecond),
+ })
+ c.Check(err, IsNil)
+ c.Check(rsp, DeepEquals, &Argon2OutOfProcessResponse{
+ Command: Argon2OutOfProcessCommandDerive,
+ Key: testutil.DecodeHexString(c, "9b5add3d66b041c49c63ba1244bb1cd8cbc7dcf1e4b0918dc13b4fd6131ae5fd"),
+ })
+ 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 *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()
+
+ tmb := new(tomb.Tomb)
+ reqChan := make(chan *Argon2OutOfProcessRequest)
+ rspChan := make(chan *Argon2OutOfProcessResponse)
+
+ tmb.Go(func() error {
+ return monitor(tmb, reqChan, rspChan)
+ })
+
+ select {
+ case rspChan <- new(Argon2OutOfProcessResponse):
+ case <-time.After(2 * time.Second): // Give the test 2 seconds to complete
+ }
+
+ c.Check(tmb.Wait(), ErrorMatches, `unexpected watchdog response`)
+}
+
+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 {
+ return monitor(tmb, reqChan, rspChan)
+ })
+
+ // 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.Kill(nil)
+ return tomb.ErrDying
+ })
+ c.Check(tmb.Wait(), IsNil)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestHMACArgon2OutOfProcessWatchdogMonitor(c *C) {
+ err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{
+ monitorAlg: crypto.SHA256,
+ period: 100 * 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,
+ 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,
+ handlerAlg: crypto.SHA256,
+ minDelay: 5 * time.Millisecond,
+ maxDelay: 15 * time.Millisecond,
+ })
+ c.Check(err, IsNil)
+}
+
+func (s *argon2OutOfProcessParentSupportSuite) TestHMACArgon2OutOfProcessWatchdogMonitorResponseTimeout(c *C) {
+ err := s.testHMACArgon2OutOfProcessWatchdogMonitor(c, &testHMACArgon2OutOfProcessWatchdogMonitorParams{
+ monitorAlg: crypto.SHA256,
+ period: 100 * time.Millisecond,
+ handlerAlg: crypto.SHA256,
+ minDelay: 200 * time.Millisecond,
+ maxDelay: 200 * 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,
+ handlerAlg: crypto.SHA256,
+ minDelay: 5 * time.Millisecond,
+ maxDelay: 15 * time.Millisecond,
+ })
+ c.Check(err, ErrorMatches, `unexpected watchdog response value from remote process`)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) 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"))
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) 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"))
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) 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"))
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) 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"))
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) 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"))
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) 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"))
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) 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"))
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) 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"))
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GB(c *C) {
+ kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha256"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA256, 100*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"))
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) TestArgon2KDFDerive2GBDifferentWatchdogHMAC(c *C) {
+ kdf := NewOutOfProcessArgon2KDF(s.newHandlerCmd("hmac", "sha384"), 0, HMACArgon2OutOfProcessWatchdogMonitor(crypto.SHA384, 100*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"))
+ 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{
+ 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))
+ s.checkNoLockFile(c)
+}
+
+func (s *argon2OutOfProcessParentSupportSuiteExpensive) 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 *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,
+ 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, "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, "1e75b6c1809f73f0127fffcf013241fe5476558b3a748e78e02638012bd1cc01"))
+ wg.Done()
+ }()
+ wg.Wait()
+ s.checkNoLockFile(c)
+}
+
+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,
+ 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, "08f295932cdf618ac5a085f177d621ec0d0a0d2a4a3ed4e471d67133cb875c6a"))
+ 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_test.go b/argon2_test.go
index ae8269b9..7d1f5841 100644
--- a/argon2_test.go
+++ b/argon2_test.go
@@ -253,16 +253,16 @@ func (s *argon2Suite) TestKDFParamsInvalidMemoryKiB(c *C) {
c.Check(err, ErrorMatches, `invalid memory cost 4294967295KiB`)
}
+func (s *argon2Suite) TestInProcessKDFDeriveNoParams(c *C) {
+ _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, nil, 32)
+ c.Check(err, ErrorMatches, `nil params`)
+}
+
func (s *argon2Suite) TestInProcessKDFDeriveInvalidMode(c *C) {
_, err := InProcessArgon2KDF.Derive("foo", nil, Argon2Default, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1}, 32)
c.Check(err, ErrorMatches, `invalid mode`)
}
-func (s *argon2Suite) TestInProcessKDFDeriveInvalidParams(c *C) {
- _, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, nil, 32)
- c.Check(err, ErrorMatches, `nil params`)
-}
-
func (s *argon2Suite) TestInProcessKDFDeriveInvalidTime(c *C) {
_, err := InProcessArgon2KDF.Derive("foo", nil, Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1}, 32)
c.Check(err, ErrorMatches, `invalid time cost`)
@@ -273,16 +273,16 @@ func (s *argon2Suite) TestInProcessKDFDeriveInvalidThreads(c *C) {
c.Check(err, ErrorMatches, `invalid number of threads`)
}
+func (s *argon2Suite) TestInProcessKDFTimeNoParams(c *C) {
+ _, err := InProcessArgon2KDF.Time(Argon2id, nil)
+ c.Check(err, ErrorMatches, `nil params`)
+}
+
func (s *argon2Suite) TestInProcessKDFTimeInvalidMode(c *C) {
_, err := InProcessArgon2KDF.Time(Argon2Default, &Argon2CostParams{Time: 4, MemoryKiB: 32, Threads: 1})
c.Check(err, ErrorMatches, `invalid mode`)
}
-func (s *argon2Suite) TestInProcessKDFTimeInvalidParams(c *C) {
- _, err := InProcessArgon2KDF.Time(Argon2id, nil)
- c.Check(err, ErrorMatches, `nil params`)
-}
-
func (s *argon2Suite) TestInProcessKDFTimeInvalidTime(c *C) {
_, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 0, MemoryKiB: 32, Threads: 1})
c.Check(err, ErrorMatches, `invalid time cost`)
@@ -298,40 +298,33 @@ func (s *argon2Suite) TestModeConstants(c *C) {
c.Check(Argon2id, Equals, Argon2Mode(argon2.ModeID))
}
-type argon2SuiteExpensive struct{}
+type argon2Expensive struct{}
+
+var _ = Suite(&argon2Expensive{})
-func (s *argon2SuiteExpensive) SetUpSuite(c *C) {
+func (s *argon2Expensive) SetUpSuite(c *C) {
if _, exists := os.LookupEnv("NO_ARGON2_TESTS"); exists {
c.Skip("skipping expensive argon2 tests")
}
}
-var _ = Suite(&argon2SuiteExpensive{})
-
type testInProcessArgon2KDFDeriveData struct {
passphrase string
salt []byte
mode Argon2Mode
params *Argon2CostParams
keyLen uint32
+
+ expectedKey []byte
}
-func (s *argon2SuiteExpensive) testInProcessKDFDerive(c *C, data *testInProcessArgon2KDFDeriveData) {
+func (s *argon2Expensive) testInProcessKDFDerive(c *C, data *testInProcessArgon2KDFDeriveData) {
key, err := InProcessArgon2KDF.Derive(data.passphrase, data.salt, data.mode, data.params, data.keyLen)
c.Check(err, IsNil)
- runtime.GC()
-
- expected, err := 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)
- c.Check(err, IsNil)
- runtime.GC()
-
- c.Check(key, DeepEquals, expected)
+ c.Check(key, DeepEquals, data.expectedKey)
}
-func (s *argon2SuiteExpensive) TestInProcessKDFDerive(c *C) {
+func (s *argon2Expensive) TestInProcessKDFDerive(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "foo",
salt: []byte("0123456789abcdefghijklmnopqrstuv"),
@@ -340,10 +333,12 @@ func (s *argon2SuiteExpensive) TestInProcessKDFDerive(c *C) {
Time: 4,
MemoryKiB: 32,
Threads: 4},
- keyLen: 32})
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "cbd85bef66eae997ed1f8f7f3b1d5bec09425f72789f5113d0215bb8bdc6891f"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) {
+func (s *argon2Expensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "bar",
salt: []byte("0123456789abcdefghijklmnopqrstuv"),
@@ -352,22 +347,26 @@ func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentPassphrase(c *C) {
Time: 4,
MemoryKiB: 32,
Threads: 4},
- keyLen: 32})
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "19b17adfb811233811b9e5872165803d01e81d3951e73b996a40c49b15c6e532"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFiDeriveDifferentSalt(c *C) {
+func (s *argon2Expensive) TestInProcessKDFiDeriveDifferentSalt(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "foo",
- salt: []byte("zyxwvutsrqponmlkjihgfedcba987654"),
+ salt: []byte("zyxwtsrqponmlkjihgfedcba987654"),
mode: Argon2id,
params: &Argon2CostParams{
Time: 4,
MemoryKiB: 32,
Threads: 4},
- keyLen: 32})
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "b5cf92c57c00f2a1d0de9d46ba0acef0e37ad1d4807b45b2dad1a50e797cc96d"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentMode(c *C) {
+func (s *argon2Expensive) TestInProcessKDFDeriveDifferentMode(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "foo",
salt: []byte("0123456789abcdefghijklmnopqrstuv"),
@@ -376,10 +375,12 @@ func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentMode(c *C) {
Time: 4,
MemoryKiB: 32,
Threads: 4},
- keyLen: 32})
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "60b6d0ab8d4c39b4f17a7c05486c714097d2bf1f1d85c6d5fad4fe24171003fe"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentParams(c *C) {
+func (s *argon2Expensive) TestInProcessKDFDeriveDifferentParams(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "foo",
salt: []byte("0123456789abcdefghijklmnopqrstuv"),
@@ -388,10 +389,12 @@ func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentParams(c *C) {
Time: 48,
MemoryKiB: 32 * 1024,
Threads: 4},
- keyLen: 32})
+ keyLen: 32,
+ expectedKey: testutil.DecodeHexString(c, "f83001f90fbbc24823773e56f65eeace261285ab7e1394efeb8348d2184c240c"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) {
+func (s *argon2Expensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) {
s.testInProcessKDFDerive(c, &testInProcessArgon2KDFDeriveData{
passphrase: "foo",
salt: []byte("0123456789abcdefghijklmnopqrstuv"),
@@ -400,30 +403,31 @@ func (s *argon2SuiteExpensive) TestInProcessKDFDeriveDifferentKeyLen(c *C) {
Time: 4,
MemoryKiB: 32,
Threads: 4},
- keyLen: 64})
+ keyLen: 64,
+ expectedKey: testutil.DecodeHexString(c, "dc8b7ed604470a49d983f86b1574b8619631ccd0282f591b227c153ce200f395615e7ddb5b01026edbf9bf7105ca2de294d67f69d9678e65417d59e51566e746"),
+ })
}
-func (s *argon2SuiteExpensive) TestInProcessKDFTime(c *C) {
+func (s *argon2Expensive) TestInProcessKDFTime(c *C) {
time1, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 4})
- runtime.GC()
c.Check(err, IsNil)
- time2, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 16, MemoryKiB: 32 * 1024, Threads: 4})
runtime.GC()
+ time2, err := InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 16, MemoryKiB: 32 * 1024, Threads: 4})
c.Check(err, IsNil)
// XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
// types of int64 kind
c.Check(time2 > time1, testutil.IsTrue)
- time2, err = InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 128 * 1024, Threads: 4})
runtime.GC()
+ time2, err = InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 128 * 1024, Threads: 4})
c.Check(err, IsNil)
// XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
// types of int64 kind
c.Check(time2 > time1, testutil.IsTrue)
- time2, err = InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 1})
runtime.GC()
+ time2, err = InProcessArgon2KDF.Time(Argon2id, &Argon2CostParams{Time: 4, MemoryKiB: 32 * 1024, Threads: 1})
c.Check(err, IsNil)
// XXX: this needs a checker like go-tpm2/testutil's IntGreater, which copes with
// types of int64 kind
diff --git a/cmd/run_argon2/main.go b/cmd/run_argon2/main.go
new file mode 100644
index 00000000..f26a63e6
--- /dev/null
+++ b/cmd/run_argon2/main.go
@@ -0,0 +1,93 @@
+// -*- 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 (
+ "crypto"
+ _ "crypto/sha1"
+ _ "crypto/sha256"
+ _ "crypto/sha512"
+ "errors"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/snapcore/secboot"
+ "github.com/snapcore/secboot/internal/paths"
+)
+
+func run() error {
+ if len(os.Args) < 3 {
+ 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 {
+ return errors.New("usage: echo | run_argon2 hmac ")
+ }
+ var alg crypto.Hash
+ switch os.Args[3] {
+ case "sha1":
+ alg = crypto.SHA1
+ case "sha224":
+ alg = crypto.SHA224
+ case "sha256":
+ alg = crypto.SHA256
+ case "sha384":
+ alg = crypto.SHA384
+ case "sha512":
+ alg = crypto.SHA512
+ default:
+ 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.
+ _, err := secboot.WaitForAndRunArgon2OutOfProcessRequest(os.Stdin, os.Stdout, watchdog)
+ 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 3847409a..826e748d 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,8 +37,10 @@ const (
)
var (
- UnmarshalV1KeyPayload = unmarshalV1KeyPayload
- UnmarshalProtectedKeys = unmarshalProtectedKeys
+ AcquireArgon2OutOfProcessHandlerSystemLock = acquireArgon2OutOfProcessHandlerSystemLock
+ ErrArgon2OutOfProcessHandlerSystemLockTimeout = errArgon2OutOfProcessHandlerSystemLockTimeout
+ UnmarshalV1KeyPayload = unmarshalV1KeyPayload
+ UnmarshalProtectedKeys = unmarshalProtectedKeys
)
type (
@@ -56,6 +60,30 @@ 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 MockAcquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint(fn func()) (restore func()) {
+ orig := acquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint
+ acquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint = fn
+ return func() {
+ acquireArgon2OutOfProcessHandlerSystemLockAcquiredCheckpoint = 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
diff --git a/go.mod b/go.mod
index fa1647a7..d7082004 100644
--- a/go.mod
+++ b/go.mod
@@ -10,10 +10,11 @@ require (
github.com/canonical/go-tpm2 v1.11.1
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 2035c508..f59ce8dc 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=
diff --git a/internal/paths/paths.go b/internal/paths/paths.go
index fc2f09f2..504e5235 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, "secboot-argon2.lock")
+)
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 $@