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