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,