diff --git a/daemon/api.go b/daemon/api.go
index 0d860e28314..6b25a12f760 100644
--- a/daemon/api.go
+++ b/daemon/api.go
@@ -85,6 +85,7 @@ var api = []*Command{
registryCmd,
noticesCmd,
noticeCmd,
+ systemSecurebootCmd,
}
const (
diff --git a/daemon/api_system_secureboot.go b/daemon/api_system_secureboot.go
new file mode 100644
index 00000000000..98ea45fc58a
--- /dev/null
+++ b/daemon/api_system_secureboot.go
@@ -0,0 +1,166 @@
+// -*- 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 daemon
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/fdestate"
+)
+
+var systemSecurebootCmd = &Command{
+ // TODO GET returning whether secure boot is relevant for the system?
+
+ Path: "/v2/system-secureboot",
+ POST: postSystemSecurebootAction,
+ WriteAccess: interfaceProviderRootAccess{
+ // TODO find a specialized interface for this, but for now assume that
+ // requests will come only from snaps plugging fwupd interface on the
+ // slot side, which also allows manipulation of EFI variables
+ Interfaces: []string{"fwupd"},
+ },
+}
+
+func postSystemSecurebootAction(c *Command, r *http.Request, user *auth.UserState) Response {
+ contentType := r.Header.Get("Content-Type")
+
+ switch contentType {
+ case "application/json":
+ return postSystemSecurebootActionJSON(c, r)
+ default:
+ return BadRequest("unexpected content type: %q", contentType)
+ }
+}
+
+type securebootRequest struct {
+ Action string `json:"action,omitempty"`
+
+ // Payload is a base64 encoded binary blob, is used in
+ // efi-secureboot-db-prepare action, and carries the DBX update content. The
+ // blob is in the range from few kB to tens of kBs
+ Payload string `json:"payload,omitempty"`
+
+ // DB is used with efi-secureboot-db-prepare action, and indicates the
+ // secureboot keys DB which is a target of the action, possible values are
+ // PK, KEK, DB, DBX
+ DB string `json:"db,omitempty"`
+}
+
+func (r *securebootRequest) Validate() error {
+ switch r.Action {
+ case "efi-secureboot-update-startup", "efi-secureboot-update-db-cleanup":
+ if r.DB != "" {
+ return fmt.Errorf("unexpected key DB for action %q", r.Action)
+ }
+
+ if len(r.Payload) > 0 {
+ return fmt.Errorf("unexpected payload for action %q", r.Action)
+ }
+ case "efi-secureboot-update-db-prepare":
+ switch r.DB {
+ case "PK", "KEK", "DB", "DBX":
+ default:
+ return fmt.Errorf("invalid key DB %q", r.DB)
+ }
+
+ if len(r.Payload) == 0 {
+ return errors.New("update payload not provided")
+ }
+ default:
+ return fmt.Errorf("unsupported EFI secure boot action %q", r.Action)
+ }
+ return nil
+}
+
+func postSystemSecurebootActionJSON(c *Command, r *http.Request) Response {
+ var req securebootRequest
+
+ decoder := json.NewDecoder(r.Body)
+
+ if err := decoder.Decode(&req); err != nil {
+ return BadRequest("cannot decode request body: %v", err)
+ }
+
+ if decoder.More() {
+ return BadRequest("extra content found in request body")
+ }
+
+ if err := req.Validate(); err != nil {
+ return BadRequest(err.Error())
+ }
+
+ switch req.Action {
+ case "efi-secureboot-update-startup":
+ return postSystemActionEFISecurebootUpdateStartup(c)
+ case "efi-secureboot-update-db-cleanup":
+ return postSystemActionEFISecurebootUpdateDBCleanup(c)
+ case "efi-secureboot-update-db-prepare":
+ return postSystemActionEFISecurebootUpdateDBPrepare(c, &req)
+ default:
+ return InternalError("support for EFI secure boot action %q is not implemented", req.Action)
+ }
+}
+
+var fdestateEFISecureBootDBUpdatePrepare = fdestate.EFISecureBootDBUpdatePrepare
+
+func postSystemActionEFISecurebootUpdateDBPrepare(c *Command, req *securebootRequest) Response {
+ if req.DB != "DBX" {
+ return InternalError("support for key DB %q is not implemented", req.DB)
+ }
+
+ payload, err := base64.StdEncoding.DecodeString(req.Payload)
+ if err != nil {
+ return BadRequest("cannot decode payload: %v", err)
+ }
+
+ err = fdestateEFISecureBootDBUpdatePrepare(c.d.state,
+ fdestate.EFISecurebootDBX, // only DBX updates are supported
+ payload)
+ if err != nil {
+ return BadRequest("cannot notify of update prepare: %v", err)
+ }
+
+ return SyncResponse(nil)
+}
+
+var fdestateEFISecureBootDBUpdateCleanup = fdestate.EFISecureBootDBUpdateCleanup
+
+func postSystemActionEFISecurebootUpdateDBCleanup(c *Command) Response {
+ if err := fdestateEFISecureBootDBUpdateCleanup(c.d.state); err != nil {
+ return BadRequest("cannot notify of update cleanup: %v", err)
+ }
+
+ return SyncResponse(nil)
+}
+
+var fdestateEFISecureBootDBManagerStartup = fdestate.EFISecureBootDBManagerStartup
+
+func postSystemActionEFISecurebootUpdateStartup(c *Command) Response {
+ if err := fdestateEFISecureBootDBManagerStartup(c.d.state); err != nil {
+ return BadRequest("cannot notify of manager startup: %v", err)
+ }
+
+ return SyncResponse(nil)
+}
diff --git a/daemon/api_system_secureboot_test.go b/daemon/api_system_secureboot_test.go
new file mode 100644
index 00000000000..4e4a017143c
--- /dev/null
+++ b/daemon/api_system_secureboot_test.go
@@ -0,0 +1,251 @@
+// -*- 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 daemon_test
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+ "strings"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/daemon"
+ "github.com/snapcore/snapd/overlord/fdestate"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+var _ = Suite(&systemSecurebootSuite{})
+
+type systemSecurebootSuite struct {
+ apiBaseSuite
+}
+
+func (s *systemSecurebootSuite) SetUpTest(c *C) {
+ s.apiBaseSuite.SetUpTest(c)
+
+ s.expectRootAccess()
+ s.expectWriteAccess(daemon.InterfaceProviderRootAccess{
+ Interfaces: []string{"fwupd"},
+ })
+
+ s.AddCleanup(daemon.MockFdestateEFISecureBootDBUpdatePrepare(func(st *state.State, db fdestate.EFISecurebooKeystDB, payload []byte) error {
+ panic("unexpected call")
+ }))
+ s.AddCleanup(daemon.MockFdestateEFISecureBootDBUpdateCleanup(func(st *state.State) error {
+ panic("unexpected call")
+ }))
+ s.AddCleanup(daemon.MockFdestateEFISecureBootDBManagerStartup(func(st *state.State) error {
+ panic("unexpected call")
+ }))
+}
+
+func (s *systemSecurebootSuite) TestEFISecurebootContentType(c *C) {
+ s.daemon(c)
+
+ body := strings.NewReader(`{"action": "blah"}`)
+ req, err := http.NewRequest("POST", "/v2/system-secureboot", body)
+ c.Assert(err, IsNil)
+
+ rsp := s.errorReq(c, req, nil)
+ c.Assert(rsp.Status, Equals, 400)
+ c.Assert(rsp.Message, Equals, `unexpected content type: ""`)
+}
+
+func (s *systemSecurebootSuite) TestEFISecurebootBogusAction(c *C) {
+ s.daemon(c)
+
+ body := strings.NewReader(`{"action": "blah"}`)
+ req, err := http.NewRequest("POST", "/v2/system-secureboot", body)
+ c.Assert(err, IsNil)
+ req.Header.Add("Content-Type", "application/json")
+
+ rsp := s.errorReq(c, req, nil)
+ c.Assert(rsp.Status, Equals, 400)
+ c.Assert(rsp.Message, Equals, `unsupported EFI secure boot action "blah"`)
+}
+
+func (s *systemSecurebootSuite) TestEFISecurebootUpdateStartup(c *C) {
+ s.daemon(c)
+
+ startupCalls := 0
+ s.AddCleanup(daemon.MockFdestateEFISecureBootDBManagerStartup(func(st *state.State) error {
+ startupCalls++
+ return nil
+ }))
+
+ body := strings.NewReader(`{"action": "efi-secureboot-update-startup"}`)
+ req, err := http.NewRequest("POST", "/v2/system-secureboot", body)
+ c.Assert(err, IsNil)
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
+ req.Header.Add("Content-Type", "application/json")
+
+ rsp := s.syncReq(c, req, nil)
+ c.Assert(rsp.Status, Equals, 200)
+
+ c.Check(startupCalls, Equals, 1)
+}
+
+func (s *systemSecurebootSuite) TestEFISecurebootUpdateDBCleanup(c *C) {
+ s.daemon(c)
+
+ cleanupCalls := 0
+ s.AddCleanup(daemon.MockFdestateEFISecureBootDBUpdateCleanup(func(st *state.State) error {
+ cleanupCalls++
+ return nil
+ }))
+
+ body := strings.NewReader(`{"action": "efi-secureboot-update-db-cleanup"}`)
+ req, err := http.NewRequest("POST", "/v2/system-secureboot", body)
+ c.Assert(err, IsNil)
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
+ req.Header.Add("Content-Type", "application/json")
+
+ rsp := s.syncReq(c, req, nil)
+ c.Assert(rsp.Status, Equals, 200)
+
+ c.Check(cleanupCalls, Equals, 1)
+}
+
+func (s *systemSecurebootSuite) TestEFISecurebootUpdateDBPrepareNoData(c *C) {
+ s.daemon(c)
+
+ body := strings.NewReader(`{
+ "action": "efi-secureboot-update-db-prepare",
+ "db": "DBX"
+}`)
+ req, err := http.NewRequest("POST", "/v2/system-secureboot", body)
+ c.Assert(err, IsNil)
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
+ req.Header.Add("Content-Type", "application/json")
+
+ rsp := s.errorReq(c, req, nil)
+ c.Assert(rsp.Status, Equals, 400)
+ c.Check(rsp.Message, Matches, "update payload not provided")
+}
+
+func (s *systemSecurebootSuite) TestEFISecurebootUpdateDBPrepareBogusDB(c *C) {
+ s.daemon(c)
+
+ body := strings.NewReader(`{
+ "action": "efi-secureboot-update-db-prepare",
+ "db": "FOO"
+}`)
+ req, err := http.NewRequest("POST", "/v2/system-secureboot", body)
+ c.Assert(err, IsNil)
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
+ req.Header.Add("Content-Type", "application/json")
+
+ rsp := s.errorReq(c, req, nil)
+ c.Assert(rsp.Status, Equals, 400)
+ c.Check(rsp.Message, Equals, `invalid key DB "FOO"`)
+}
+
+func (s *systemSecurebootSuite) TestEFISecurebootUpdateDBPrepareBadPayload(c *C) {
+ s.daemon(c)
+
+ body := strings.NewReader(`{
+ "action": "efi-secureboot-update-db-prepare",
+ "db": "DBX",
+ "payload": "123"
+}`)
+ req, err := http.NewRequest("POST", "/v2/system-secureboot", body)
+ c.Assert(err, IsNil)
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
+ req.Header.Add("Content-Type", "application/json")
+
+ rsp := s.errorReq(c, req, nil)
+ c.Assert(rsp.Status, Equals, 400)
+ c.Check(rsp.Message, Matches, `cannot decode payload: illegal base64 .*`)
+}
+
+func (s *systemSecurebootSuite) TestEFISecurebootUpdateDBPrepareHappy(c *C) {
+ s.daemon(c)
+
+ updatePrepareCalls := 0
+ s.AddCleanup(daemon.MockFdestateEFISecureBootDBUpdatePrepare(func(st *state.State, db fdestate.EFISecurebooKeystDB, payload []byte) error {
+ c.Check(db, Equals, fdestate.EFISecurebootDBX)
+ c.Check(payload, DeepEquals, []byte("payload"))
+ updatePrepareCalls++
+ return nil
+ }))
+
+ body, err := json.Marshal(map[string]any{
+ "action": "efi-secureboot-update-db-prepare",
+ "db": "DBX",
+ "payload": base64.StdEncoding.EncodeToString([]byte("payload")),
+ })
+ c.Assert(err, IsNil)
+ req, err := http.NewRequest("POST", "/v2/system-secureboot", bytes.NewReader(body))
+ c.Assert(err, IsNil)
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
+ req.Header.Add("Content-Type", "application/json")
+
+ rsp := s.syncReq(c, req, nil)
+ c.Assert(rsp.Status, Equals, 200)
+
+ c.Check(updatePrepareCalls, Equals, 1)
+}
+
+func (s *systemSecurebootSuite) TestSecurebootRequestValidate(c *C) {
+ r := daemon.SecurebootRequest{
+ Action: "foo",
+ }
+ c.Check(r.Validate(), ErrorMatches, `unsupported EFI secure boot action "foo"`)
+
+ r = daemon.SecurebootRequest{
+ Action: "efi-secureboot-update-startup",
+ DB: "DBX",
+ }
+ c.Check(r.Validate(), ErrorMatches, `unexpected key DB for action "efi-secureboot-update-startup"`)
+
+ r = daemon.SecurebootRequest{
+ Action: "efi-secureboot-update-db-cleanup",
+ Payload: "123",
+ }
+ c.Check(r.Validate(), ErrorMatches, `unexpected payload for action "efi-secureboot-update-db-cleanup"`)
+
+ r = daemon.SecurebootRequest{
+ Action: "efi-secureboot-update-db-prepare",
+ DB: "FOO",
+ }
+ c.Check(r.Validate(), ErrorMatches, `invalid key DB "FOO"`)
+
+ r = daemon.SecurebootRequest{
+ Action: "efi-secureboot-update-db-prepare",
+ DB: "DBX",
+ }
+ c.Check(r.Validate(), ErrorMatches, `update payload not provided`)
+
+ // valid
+ for _, r := range []daemon.SecurebootRequest{{
+ Action: "efi-secureboot-update-db-prepare",
+ DB: "DBX",
+ Payload: "123",
+ }, {
+ Action: "efi-secureboot-update-db-cleanup",
+ }, {
+ Action: "efi-secureboot-update-startup",
+ }} {
+ c.Logf("testing valid request %+v", r)
+ c.Check(r.Validate(), IsNil)
+ }
+}
diff --git a/daemon/export_api_system_secureboot_test.go b/daemon/export_api_system_secureboot_test.go
new file mode 100644
index 00000000000..d489c93e1db
--- /dev/null
+++ b/daemon/export_api_system_secureboot_test.go
@@ -0,0 +1,52 @@
+// -*- 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 daemon
+
+import (
+ "github.com/snapcore/snapd/overlord/fdestate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type SecurebootRequest = securebootRequest
+
+func MockFdestateEFISecureBootDBUpdatePrepare(
+ f func(st *state.State, db fdestate.EFISecurebooKeystDB, payload []byte) error,
+) (restore func()) {
+ restore = testutil.Backup(&fdestateEFISecureBootDBUpdatePrepare)
+ fdestateEFISecureBootDBUpdatePrepare = f
+ return restore
+}
+
+func MockFdestateEFISecureBootDBUpdateCleanup(
+ f func(st *state.State) error,
+) (restore func()) {
+ restore = testutil.Backup(&fdestateEFISecureBootDBUpdateCleanup)
+ fdestateEFISecureBootDBUpdateCleanup = f
+ return restore
+}
+
+func MockFdestateEFISecureBootDBManagerStartup(
+ f func(st *state.State) error,
+) (restore func()) {
+ restore = testutil.Backup(&fdestateEFISecureBootDBManagerStartup)
+ fdestateEFISecureBootDBManagerStartup = f
+ return restore
+}