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 $@