From 7fa15c8589d9d5d53227456a121a088ad3be9abb Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Tue, 13 Jan 2026 21:20:39 +0000 Subject: [PATCH] Add initial Plymouth implementation of AuthRequestor This adds an implementation of AuthRequestor that communicates directly with Plymouth, alongside the existing systemd version that uses systemd-ask-password. Future PRs will update the AuthRequestor interface so that ActivateContext can send user credential error messages directly to Plymouth, and so that the most appropriate implementation of AuthRequestor is selected automatically. Fixes: FR-12403 --- auth_requestor_plymouth.go | 82 ++++++++++ auth_requestor_plymouth_test.go | 258 ++++++++++++++++++++++++++++++++ auth_requestor_systemd.go | 6 +- auth_requestor_systemd_test.go | 24 +-- 4 files changed, 355 insertions(+), 15 deletions(-) create mode 100644 auth_requestor_plymouth.go create mode 100644 auth_requestor_plymouth_test.go diff --git a/auth_requestor_plymouth.go b/auth_requestor_plymouth.go new file mode 100644 index 00000000..187f4dd8 --- /dev/null +++ b/auth_requestor_plymouth.go @@ -0,0 +1,82 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2026 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" + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" +) + +// PlymouthAuthRequestorStringer is used by the Plymouth implementation +// of [AuthRequestor] to obtain translated strings. +type PlymouthAuthRequestorStringer interface { + // RequestUserCredentialFormatString returns a format string used by + // RequestUserCredential to construct a message that is used to request + // credentials with the supplied auth types. The returned format string + // is interpreted with the following parameters: + // - %[1]s: A human readable name for the storage container. + // - %[2]s: The path of the encrypted storage container. + RequestUserCredentialFormatString(authTypes UserAuthType) (string, error) +} + +type plymouthAuthRequestor struct { + stringer PlymouthAuthRequestorStringer +} + +func (r *plymouthAuthRequestor) RequestUserCredential(ctx context.Context, name, path string, authTypes UserAuthType) (string, error) { + fmtString, err := r.stringer.RequestUserCredentialFormatString(authTypes) + if err != nil { + return "", fmt.Errorf("cannot request format string for requested auth types: %w", err) + } + msg := fmt.Sprintf(fmtString, name, path) + + cmd := exec.CommandContext( + ctx, "plymouth", "ask-for-password", + "--prompt", msg) + out := new(bytes.Buffer) + cmd.Stdout = out + cmd.Stdin = os.Stdin + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("cannot execute plymouth ask-for-password: %w", err) + } + result, err := io.ReadAll(out) + if err != nil { + // The only error returned from bytes.Buffer.Read should be io.EOF, + // which io.ReadAll filters out. + return "", fmt.Errorf("unexpected error: %w", err) + } + return string(result), nil +} + +// NewPlymouthAuthRequestor creates an implementation of AuthRequestor that +// communicates directly with Plymouth. +func NewPlymouthAuthRequestor(stringer PlymouthAuthRequestorStringer) (AuthRequestor, error) { + if stringer == nil { + return nil, errors.New("must supply an implementation of PlymouthAuthRequestorStringer") + } + return &plymouthAuthRequestor{ + stringer: stringer, + }, nil +} diff --git a/auth_requestor_plymouth_test.go b/auth_requestor_plymouth_test.go new file mode 100644 index 00000000..436fc927 --- /dev/null +++ b/auth_requestor_plymouth_test.go @@ -0,0 +1,258 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2026 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 ( + "context" + "errors" + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/snapcore/secboot/internal/testutil" + snapd_testutil "github.com/snapcore/snapd/testutil" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/secboot" +) + +type authRequestorPlymouthSuite struct { + snapd_testutil.BaseTest + + passwordFile string + mockPlymouth *snapd_testutil.MockCmd +} + +func (s *authRequestorPlymouthSuite) SetUpTest(c *C) { + dir := c.MkDir() + s.passwordFile = filepath.Join(dir, "password") // password to be returned by the mock plymouth + + plymouthBottom := `cat %[1]s` + s.mockPlymouth = snapd_testutil.MockCommand(c, "plymouth", fmt.Sprintf(plymouthBottom, s.passwordFile)) + s.AddCleanup(s.mockPlymouth.Restore) +} + +func (s *authRequestorPlymouthSuite) setPassphrase(c *C, passphrase string) { + c.Assert(ioutil.WriteFile(s.passwordFile, []byte(passphrase), 0600), IsNil) +} + +var _ = Suite(&authRequestorPlymouthSuite{}) + +type mockPlymouthAuthRequestorStringer struct { + rucErr error +} + +func (s *mockPlymouthAuthRequestorStringer) RequestUserCredentialFormatString(authType UserAuthType) (string, error) { + if s.rucErr != nil { + return "", s.rucErr + } + + switch authType { + case UserAuthTypePassphrase: + return "Enter passphrase for %[1]s (%[2]s):", nil + case UserAuthTypePIN: + return "Enter PIN for %[1]s (%[2]s):", nil + case UserAuthTypeRecoveryKey: + return "Enter recovery key for %[1]s (%[2]s):", nil + case UserAuthTypePassphrase | UserAuthTypePIN: + return "Enter passphrase or PIN for %[1]s (%[2]s):", nil + case UserAuthTypePassphrase | UserAuthTypeRecoveryKey: + return "Enter passphrase or recovery key for %[1]s (%[2]s):", nil + case UserAuthTypePIN | UserAuthTypeRecoveryKey: + return "Enter PIN or recovery key for %[1]s (%[2]s):", nil + case UserAuthTypePassphrase | UserAuthTypePIN | UserAuthTypeRecoveryKey: + return "Enter passphrase, PIN or recovery key for %[1]s (%[2]s):", nil + default: + return "", errors.New("unexpected UserAuthType") + } +} + +type testPlymouthRequestUserCredentialsParams struct { + passphrase string + + ctx context.Context + name string + path string + authTypes UserAuthType + + expectedMsg string +} + +func (s *authRequestorPlymouthSuite) testRequestUserCredential(c *C, params *testPlymouthRequestUserCredentialsParams) { + s.setPassphrase(c, params.passphrase) + + requestor, err := NewPlymouthAuthRequestor(new(mockPlymouthAuthRequestorStringer)) + c.Assert(err, IsNil) + + passphrase, err := requestor.RequestUserCredential(params.ctx, params.name, params.path, params.authTypes) + c.Check(err, IsNil) + c.Check(passphrase, Equals, params.passphrase) + + c.Check(s.mockPlymouth.Calls(), HasLen, 1) + c.Check(s.mockPlymouth.Calls()[0], DeepEquals, []string{"plymouth", "ask-for-password", "--prompt", params.expectedMsg}) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphrase(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "password", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase, + expectedMsg: "Enter passphrase for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphraseDifferentPassphrase(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "1234", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase, + expectedMsg: "Enter passphrase for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialDifferentPath(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "password", + ctx: context.Background(), + name: "data", + path: "/dev/nvme0n1p1", + authTypes: UserAuthTypePassphrase, + expectedMsg: "Enter passphrase for data (/dev/nvme0n1p1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphraseDifferentName(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "password", + ctx: context.Background(), + name: "foo", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase, + expectedMsg: "Enter passphrase for foo (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPIN(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "1234", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePIN, + expectedMsg: "Enter PIN for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialRecoveryKey(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "00000-11111-22222-33333-44444-55555-00000-11111", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypeRecoveryKey, + expectedMsg: "Enter recovery key for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphraseOrPIN(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "1234", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase | UserAuthTypePIN, + expectedMsg: "Enter passphrase or PIN for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphraseOrRecoveryKey(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "password", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase | UserAuthTypeRecoveryKey, + expectedMsg: "Enter passphrase or recovery key for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPINOrRecoveryKey(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "00000-11111-22222-33333-44444-55555-00000-11111", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePIN | UserAuthTypeRecoveryKey, + expectedMsg: "Enter PIN or recovery key for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphraseOrPINOrRecoveryKey(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "password", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase | UserAuthTypePIN | UserAuthTypeRecoveryKey, + expectedMsg: "Enter passphrase, PIN or recovery key for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestNewRequestorNoStringer(c *C) { + _, err := NewPlymouthAuthRequestor(nil) + c.Check(err, ErrorMatches, `must supply an implementation of PlymouthAuthRequestorStringer`) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialObtainFormatStringError(c *C) { + requestor, err := NewPlymouthAuthRequestor(&mockPlymouthAuthRequestorStringer{ + rucErr: errors.New("some error"), + }) + c.Assert(err, IsNil) + + _, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase) + c.Check(err, ErrorMatches, `cannot request format string for requested auth types: some error`) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialFailure(c *C) { + requestor, err := NewPlymouthAuthRequestor(new(mockPlymouthAuthRequestorStringer)) + c.Assert(err, IsNil) + + _, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase) + c.Check(err, ErrorMatches, "cannot execute plymouth ask-for-password: exit status 1") +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialCanceledContext(c *C) { + s.setPassphrase(c, "foo") + + requestor, err := NewPlymouthAuthRequestor(new(mockPlymouthAuthRequestorStringer)) + c.Assert(err, IsNil) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err = requestor.RequestUserCredential(ctx, "data", "/dev/sda1", UserAuthTypePassphrase) + c.Check(err, ErrorMatches, "cannot execute plymouth ask-for-password: context canceled") + c.Check(errors.Is(err, context.Canceled), testutil.IsTrue) +} diff --git a/auth_requestor_systemd.go b/auth_requestor_systemd.go index 39cac1cf..8078262b 100644 --- a/auth_requestor_systemd.go +++ b/auth_requestor_systemd.go @@ -61,9 +61,9 @@ func (r *systemdAuthRequestor) RequestUserCredential(ctx context.Context, name, } // NewSystemdAuthRequestor creates an implementation of AuthRequestor that -// delegates to the systemd-ask-password binary. The caller supplies a map -// of user auth type combinations to format strings that are used to construct -// messages. The format strings are interpreted with the following parameters: +// delegates to the systemd-ask-password binary. The caller supplies a callback +// to map user auth type combinations to format strings that are used to +// messages.The format strings are interpreted with the following parameters: // - %[1]s: A human readable name for the storage container. // - %[2]s: The path of the encrypted storage container. func NewSystemdAuthRequestor(formatStringFn func(UserAuthType) (string, error)) (AuthRequestor, error) { diff --git a/auth_requestor_systemd_test.go b/auth_requestor_systemd_test.go index 5f4d9e17..7ab5149d 100644 --- a/auth_requestor_systemd_test.go +++ b/auth_requestor_systemd_test.go @@ -57,7 +57,7 @@ func (s *authRequestorSystemdSuite) setPassphrase(c *C, passphrase string) { var _ = Suite(&authRequestorSystemdSuite{}) -type testRequestUserCredentialParams struct { +type testSystemdRequestUserCredentialsParams struct { passphrase string ctx context.Context @@ -68,7 +68,7 @@ type testRequestUserCredentialParams struct { expectedMsg string } -func (s *authRequestorSystemdSuite) testRequestUserCredential(c *C, params *testRequestUserCredentialParams) { +func (s *authRequestorSystemdSuite) testRequestUserCredential(c *C, params *testSystemdRequestUserCredentialsParams) { s.setPassphrase(c, params.passphrase) requestor, err := NewSystemdAuthRequestor(func(authType UserAuthType) (string, error) { @@ -103,7 +103,7 @@ func (s *authRequestorSystemdSuite) testRequestUserCredential(c *C, params *test } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphrase(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "password", ctx: context.Background(), name: "data", @@ -114,7 +114,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphrase(c *C) { } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseDifferentPassphrase(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "1234", ctx: context.Background(), name: "data", @@ -125,7 +125,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseDifferent } func (s *authRequestorSystemdSuite) TestRequestUserCredentialDifferentPath(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "password", ctx: context.Background(), name: "data", @@ -136,7 +136,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialDifferentPath(c *C) } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseDifferentName(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "password", ctx: context.Background(), name: "foo", @@ -147,7 +147,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseDifferent } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPIN(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "1234", ctx: context.Background(), name: "data", @@ -158,7 +158,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPIN(c *C) { } func (s *authRequestorSystemdSuite) TestRequestUserCredentialRecoveryKey(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "00000-11111-22222-33333-44444-55555-00000-11111", ctx: context.Background(), name: "data", @@ -169,7 +169,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialRecoveryKey(c *C) { } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseOrPIN(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "1234", ctx: context.Background(), name: "data", @@ -180,7 +180,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseOrPIN(c * } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseOrRecoveryKey(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "password", ctx: context.Background(), name: "data", @@ -191,7 +191,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseOrRecover } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPINOrRecoveryKey(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "00000-11111-22222-33333-44444-55555-00000-11111", ctx: context.Background(), name: "data", @@ -202,7 +202,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPINOrRecoveryKey(c } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseOrPINOrRecoveryKey(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "password", ctx: context.Background(), name: "data",