diff --git a/cmd/snap-bootstrap/main.go b/cmd/snap-bootstrap/main.go index 82a649cd95c..42a7b0407bc 100644 --- a/cmd/snap-bootstrap/main.go +++ b/cmd/snap-bootstrap/main.go @@ -26,6 +26,7 @@ import ( "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/secboot" ) var ( @@ -40,6 +41,8 @@ such as initramfs. ) func main() { + secboot.HijackAndRunArgon2OutOfProcessHandlerOnArg([]string{"argon2-proc"}) + err := run(os.Args[1:]) if err != nil { fmt.Fprintf(os.Stderr, "error: %s\n", err) diff --git a/cmd/snapd/main.go b/cmd/snapd/main.go index bf5b6e7b1c2..693d7b7ddad 100644 --- a/cmd/snapd/main.go +++ b/cmd/snapd/main.go @@ -32,6 +32,7 @@ import ( "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/sandbox" + "github.com/snapcore/snapd/secboot" "github.com/snapcore/snapd/snapdenv" "github.com/snapcore/snapd/snapdtool" "github.com/snapcore/snapd/syscheck" @@ -57,6 +58,8 @@ func main() { snapdtool.ExecInSnapdOrCoreSnap() } + secboot.HijackAndRunArgon2OutOfProcessHandlerOnArg([]string{"argon2-proc"}) + if err := snapdtool.MaybeSetupFIPS(); err != nil { fmt.Fprintf(os.Stderr, "cannot check or enable FIPS mode: %v", err) os.Exit(1) diff --git a/secboot/argon2_out_of_process.go b/secboot/argon2_out_of_process.go new file mode 100644 index 00000000000..7dbc1e254ae --- /dev/null +++ b/secboot/argon2_out_of_process.go @@ -0,0 +1,36 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2025 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 "os" + +func isOutOfProcessArgon2KDFMode(args []string) bool { + if len(os.Args) != len(args)+1 { + return false + } + + for i := 0; i < len(args); i++ { + if os.Args[i+1] != args[i] { + return false + } + } + + return true +} diff --git a/secboot/argon2_out_of_process_dummy.go b/secboot/argon2_out_of_process_dummy.go new file mode 100644 index 00000000000..4dae4db8e09 --- /dev/null +++ b/secboot/argon2_out_of_process_dummy.go @@ -0,0 +1,27 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +//go:build nosecboot + +/* + * Copyright (C) 2025 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 + +func HijackAndRunArgon2OutOfProcessHandlerOnArg(args []string) { + if isOutOfProcessArgon2KDFMode(args) { + panic("internal error: unexpected call to execute as argon2 runner in non-secboot binary") + } +} diff --git a/secboot/argon2_out_of_process_sb.go b/secboot/argon2_out_of_process_sb.go new file mode 100644 index 00000000000..da289b4b18a --- /dev/null +++ b/secboot/argon2_out_of_process_sb.go @@ -0,0 +1,92 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot + +/* + * Copyright (C) 2025 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 ( + "fmt" + "os" + "os/exec" + "time" + + sb "github.com/snapcore/secboot" + + "github.com/snapcore/snapd/logger" +) + +var ( + osExit = os.Exit + osReadlink = os.Readlink + + sbWaitForAndRunArgon2OutOfProcessRequest = sb.WaitForAndRunArgon2OutOfProcessRequest + sbNewOutOfProcessArgon2KDF = sb.NewOutOfProcessArgon2KDF + sbSetArgon2KDF = sb.SetArgon2KDF +) + +const ( + outOfProcessArgon2KDFTimeout = 100 * time.Millisecond +) + +// HijackAndRunArgon2OutOfProcessHandlerOnArg is supposed to be called from the +// main() of binaries involved with sealing/unsealing of keys (i.e. snapd and +// snap-bootstrap). +// +// This switches the binary to a special argon2 mode when the matching args are +// detected where it hijacks the process and acts as an argon2 out-of-process +// helper command and exits when its work is done, otherwise (in normal mode) +// it sets the default argon2 kdf implementation to be self-invoking into the +// special argon2 mode of the calling binary. +// +// For more context, check docs for sb.WaitForAndRunArgon2OutOfProcessRequest +// and sb.NewOutOfProcessArgon2KDF for details on how the flow works +// in secboot. +func HijackAndRunArgon2OutOfProcessHandlerOnArg(args []string) { + if !isOutOfProcessArgon2KDFMode(args) { + // Binary was invoked in normal mode, let's setup default argon2 kdf implementation + // to point to this binary when invoked using special args. + exe, err := osReadlink("/proc/self/exe") + if err != nil { + logger.Noticef("internal error: failed to read symlink of /proc/self/exe: %v", err) + return + } + + handlerCmd := func() (*exec.Cmd, error) { + cmd := exec.Command(exe, args...) + return cmd, nil + } + argon2KDF := sbNewOutOfProcessArgon2KDF(handlerCmd, outOfProcessArgon2KDFTimeout, nil) + sbSetArgon2KDF(argon2KDF) + + return + } + + logger.Noticef("running argon2 out-of-process helper") + // Ignore the lock release callback and use implicit release on process termination. + _, err := sbWaitForAndRunArgon2OutOfProcessRequest(os.Stdin, os.Stdout, sb.NoArgon2OutOfProcessWatchdogHandler()) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot run argon2 out-of-process request: %v", err) + osExit(1) + } + + // Argon2 request was processed successfully + osExit(0) + + panic("internal error: not reachable") +} diff --git a/secboot/argon2_out_of_process_sb_test.go b/secboot/argon2_out_of_process_sb_test.go new file mode 100644 index 00000000000..d1db0884d90 --- /dev/null +++ b/secboot/argon2_out_of_process_sb_test.go @@ -0,0 +1,180 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot + +/* + * Copyright (C) 2025 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" + "io" + "os/exec" + "time" + + sb "github.com/snapcore/secboot" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/secboot" +) + +type argon2Suite struct { +} + +var _ = Suite(&argon2Suite{}) + +func (*argon2Suite) TestHijackAndRunArgon2OutOfProcessHandlerOnArgArgon2Mode(c *C) { + runArgon2Called := 0 + restore := secboot.MockSbWaitForAndRunArgon2OutOfProcessRequest(func(_ io.Reader, _ io.WriteCloser, _ sb.Argon2OutOfProcessWatchdogHandler) (lockRelease func(), err error) { + runArgon2Called++ + return nil, nil + }) + defer restore() + + setArgon2Called := 0 + restore = secboot.MockSbSetArgon2KDF(func(kdf sb.Argon2KDF) sb.Argon2KDF { + setArgon2Called++ + return nil + }) + defer restore() + + exitCalled := 0 + restore = secboot.MockOsExit(func(code int) { + exitCalled++ + c.Assert(code, Equals, 0) + panic("os.Exit(0)") + }) + defer restore() + + restore = secboot.MockOsArgs([]string{"/path/to/cmd", "--test-special-argon2-mode"}) + defer restore() + + // Since we override os.Exit(0), we expect to panic (injected above) + f := func() { secboot.HijackAndRunArgon2OutOfProcessHandlerOnArg([]string{"--test-special-argon2-mode"}) } + c.Assert(f, Panics, "os.Exit(0)") + + c.Check(setArgon2Called, Equals, 0) + c.Check(runArgon2Called, Equals, 1) + c.Check(exitCalled, Equals, 1) +} + +func (*argon2Suite) TestHijackAndRunArgon2OutOfProcessHandlerOnArgArgon2ModeError(c *C) { + runArgon2Called := 0 + restore := secboot.MockSbWaitForAndRunArgon2OutOfProcessRequest(func(_ io.Reader, _ io.WriteCloser, _ sb.Argon2OutOfProcessWatchdogHandler) (lockRelease func(), err error) { + runArgon2Called++ + return nil, errors.New("boom!") + }) + defer restore() + + setArgon2Called := 0 + restore = secboot.MockSbSetArgon2KDF(func(kdf sb.Argon2KDF) sb.Argon2KDF { + setArgon2Called++ + return nil + }) + defer restore() + + exitCalled := 0 + restore = secboot.MockOsExit(func(code int) { + exitCalled++ + c.Assert(code, Equals, 1) + panic("os.Exit(1)") + }) + defer restore() + + restore = secboot.MockOsArgs([]string{"/path/to/cmd", "--test-special-argon2-mode"}) + defer restore() + + f := func() { secboot.HijackAndRunArgon2OutOfProcessHandlerOnArg([]string{"--test-special-argon2-mode"}) } + c.Assert(f, Panics, "os.Exit(1)") + + c.Check(setArgon2Called, Equals, 0) + c.Check(runArgon2Called, Equals, 1) + c.Check(exitCalled, Equals, 1) +} + +type mockArgon2KDF struct{} + +func (*mockArgon2KDF) Derive(passphrase string, salt []byte, mode sb.Argon2Mode, params *sb.Argon2CostParams, keyLen uint32) ([]byte, error) { + return nil, nil +} + +func (*mockArgon2KDF) Time(mode sb.Argon2Mode, params *sb.Argon2CostParams) (time.Duration, error) { + return 0, nil +} + +func (*argon2Suite) TestHijackAndRunArgon2OutOfProcessHandlerOnArgNormalMode(c *C) { + runArgon2Called := 0 + restore := secboot.MockSbWaitForAndRunArgon2OutOfProcessRequest(func(_ io.Reader, _ io.WriteCloser, _ sb.Argon2OutOfProcessWatchdogHandler) (lockRelease func(), err error) { + runArgon2Called++ + return nil, nil + }) + defer restore() + + exitCalled := 0 + restore = secboot.MockOsExit(func(code int) { + exitCalled++ + c.Assert(code, Equals, 0) + panic("injected panic") + }) + defer restore() + + restore = secboot.MockOsReadlink(func(name string) (string, error) { + c.Assert(name, Equals, "/proc/self/exe") + return "/path/to/cmd", nil + }) + defer restore() + + argon2KDF := &mockArgon2KDF{} + + restore = secboot.MockSbNewOutOfProcessArgon2KDF(func(newHandlerCmd func() (*exec.Cmd, error), timeout time.Duration, watchdog sb.Argon2OutOfProcessWatchdogMonitor) sb.Argon2KDF { + c.Check(timeout, Equals, 100*time.Millisecond) + c.Check(watchdog, IsNil) + + cmd, err := newHandlerCmd() + c.Assert(err, IsNil) + c.Check(cmd.Path, Equals, "/path/to/cmd") + c.Check(cmd.Args, DeepEquals, []string{"/path/to/cmd", "--test-special-argon2-mode"}) + + return argon2KDF + }) + defer restore() + + setArgon2Called := 0 + restore = secboot.MockSbSetArgon2KDF(func(kdf sb.Argon2KDF) sb.Argon2KDF { + setArgon2Called++ + // Check pointer points to mock implementation + c.Assert(kdf, Equals, argon2KDF) + return nil + }) + defer restore() + + for _, args := range [][]string{ + {}, + {"/path/to/cmd"}, + {"/path/to/cmd", "not-run-argon2"}, + {"/path/to/cmd", "not-run-argon2", "--argon2-proc"}, + } { + restore := secboot.MockOsArgs(args) + defer restore() + // This should exit early before running the argon2 helper and calling os.Exit (and no injected panic) + secboot.HijackAndRunArgon2OutOfProcessHandlerOnArg([]string{"--test-special-argon2-mode"}) + } + + c.Check(setArgon2Called, Equals, 4) + c.Check(runArgon2Called, Equals, 0) + c.Check(exitCalled, Equals, 0) +} diff --git a/secboot/export_sb_test.go b/secboot/export_sb_test.go index 1dad283556f..0b8a554b173 100644 --- a/secboot/export_sb_test.go +++ b/secboot/export_sb_test.go @@ -22,6 +22,9 @@ package secboot import ( "io" + "os" + "os/exec" + "time" "github.com/canonical/go-tpm2" sb "github.com/snapcore/secboot" @@ -449,3 +452,27 @@ func MockDisksDevlinks(f func(node string) ([]string, error)) (restore func()) { disksDevlinks = old } } + +func MockOsArgs(args []string) (restore func()) { + return testutil.Mock(&os.Args, args) +} + +func MockOsExit(f func(code int)) (restore func()) { + return testutil.Mock(&osExit, f) +} + +func MockOsReadlink(f func(name string) (string, error)) (restore func()) { + return testutil.Mock(&osReadlink, f) +} + +func MockSbWaitForAndRunArgon2OutOfProcessRequest(f func(in io.Reader, out io.WriteCloser, watchdog sb.Argon2OutOfProcessWatchdogHandler) (lockRelease func(), err error)) (restore func()) { + return testutil.Mock(&sbWaitForAndRunArgon2OutOfProcessRequest, f) +} + +func MockSbNewOutOfProcessArgon2KDF(f func(newHandlerCmd func() (*exec.Cmd, error), timeout time.Duration, watchdog sb.Argon2OutOfProcessWatchdogMonitor) sb.Argon2KDF) (restore func()) { + return testutil.Mock(&sbNewOutOfProcessArgon2KDF, f) +} + +func MockSbSetArgon2KDF(f func(kdf sb.Argon2KDF) sb.Argon2KDF) (restore func()) { + return testutil.Mock(&sbSetArgon2KDF, f) +} diff --git a/secboot/secboot_sb_test.go b/secboot/secboot_sb_test.go index 64cc3d48dee..d2e32a44fff 100644 --- a/secboot/secboot_sb_test.go +++ b/secboot/secboot_sb_test.go @@ -1161,7 +1161,7 @@ func (s *secbootSuite) TestSealKey(c *C) { expectedKDFOptions = &sb.Argon2Options{Mode: sb.Argon2id, TargetDuration: tc.volumesAuth.KDFTime} case "argon2i": expectedKDFOptions = &sb.Argon2Options{Mode: sb.Argon2i, TargetDuration: tc.volumesAuth.KDFTime} - case "pbkdf2", "": + case "pbkdf2": expectedKDFOptions = &sb.PBKDF2Options{TargetDuration: tc.volumesAuth.KDFTime} } c.Assert(params.KDFOptions, DeepEquals, expectedKDFOptions) diff --git a/secboot/secboot_tpm.go b/secboot/secboot_tpm.go index 9e46b979320..798efec35d4 100644 --- a/secboot/secboot_tpm.go +++ b/secboot/secboot_tpm.go @@ -487,20 +487,13 @@ func ProvisionForCVM(initramfsUbuntuSeedDir string) error { func kdfOptions(volumesAuth *device.VolumesAuthOptions) (sb.KDFOptions, error) { switch volumesAuth.KDFType { case "": - // TODO:FDEM:FIX: default to out-of-process argon2id implementation - return &sb.PBKDF2Options{ - TargetDuration: volumesAuth.KDFTime, - }, nil + return nil, nil case "argon2id": - // TODO:FDEM:FIX: sb.SetArgon2KDF(sb.InProcessArgon2KDF) or intentionally fail - // until out-of-process variant is implemented? return &sb.Argon2Options{ Mode: sb.Argon2id, TargetDuration: volumesAuth.KDFTime, }, nil case "argon2i": - // TODO:FDEM:FIX: sb.SetArgon2KDF(sb.InProcessArgon2KDF) or intentionally fail - // until out-of-process variant is implemented? return &sb.Argon2Options{ Mode: sb.Argon2i, TargetDuration: volumesAuth.KDFTime,