diff options
| author | Paweł Stołowski <stolowski@gmail.com> | 2020-10-29 09:59:48 +0100 |
|---|---|---|
| committer | Paweł Stołowski <stolowski@gmail.com> | 2020-10-29 09:59:48 +0100 |
| commit | 8a082408fddb2e791aca737347b13d72ba6a1e8c (patch) | |
| tree | 7db3083e5ae4c685cc62d80147995c6da531281a | |
| parent | eee9569170b4a5d5075a19a64220bc77cdb8cb41 (diff) | |
| parent | afab8fb88b821a54f73c3a349720fe21b9c9e488 (diff) | |
Merge branch 'master' into preseeding-interface-hookspreseeding-interface-hooks
32 files changed, 1552 insertions, 129 deletions
diff --git a/boot/seal.go b/boot/seal.go index 363bdd762d..80bc2115f6 100644 --- a/boot/seal.go +++ b/boot/seal.go @@ -107,10 +107,10 @@ func sealKeyToModeenv(key secboot.EncryptionKey, model *asserts.Model, modeenv * } } sealKeyParams := &secboot.SealKeyParams{ - ModelParams: modelParams, - KeyFile: filepath.Join(InitramfsEncryptionKeyDir, "ubuntu-data.sealed-key"), - TPMPolicyUpdateDataFile: filepath.Join(InstallHostFDEDataDir, "policy-update-data"), - TPMLockoutAuthFile: filepath.Join(InstallHostFDEDataDir, "tpm-lockout-auth"), + ModelParams: modelParams, + KeyFile: filepath.Join(InitramfsEncryptionKeyDir, "ubuntu-data.sealed-key"), + TPMPolicyAuthKeyFile: filepath.Join(InstallHostFDEDataDir, "tpm-policy-auth-key"), + TPMLockoutAuthFile: filepath.Join(InstallHostFDEDataDir, "tpm-lockout-auth"), } // finally, seal the key if err := secbootSealKey(key, sealKeyParams); err != nil { @@ -217,9 +217,9 @@ func resealKeyToModeenv(rootdir string, model *asserts.Model, modeenv *Modeenv, return fmt.Errorf("cannot prepare for key resealing: %v", err) } resealKeyParams := &secboot.ResealKeyParams{ - ModelParams: modelParams, - KeyFile: filepath.Join(InitramfsEncryptionKeyDir, "ubuntu-data.sealed-key"), - TPMPolicyUpdateDataFile: filepath.Join(dirs.SnapFDEDirUnder(rootdir), "policy-update-data"), + ModelParams: modelParams, + KeyFile: filepath.Join(InitramfsEncryptionKeyDir, "ubuntu-data.sealed-key"), + TPMPolicyAuthKeyFile: filepath.Join(dirs.SnapFDEDirUnder(rootdir), "tpm-policy-auth-key"), } if err := secbootResealKey(resealKeyParams); err != nil { return fmt.Errorf("cannot reseal the encryption key: %v", err) diff --git a/client/client.go b/client/client.go index c859be76dd..d4336eeaa2 100644 --- a/client/client.go +++ b/client/client.go @@ -317,7 +317,9 @@ func (client *Client) Hijack(f func(*http.Request) (*http.Response, error)) { type doOptions struct { // Timeout is the overall request timeout Timeout time.Duration - // Retry interval + // Retry interval. + // Note for a request with a Timeout but without a retry, Retry should just + // be set to something larger than the Timeout. Retry time.Duration } @@ -344,6 +346,10 @@ var doNoTimeoutAndRetry = &doOptions{ func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, opts *doOptions) (statusCode int, err error) { opts = ensureDoOpts(opts) + if err := client.checkMaintenanceJSON(); err != nil { + return 500, err + } + var rsp *http.Response var ctx context.Context = context.Background() if opts.Timeout <= 0 { @@ -413,7 +419,57 @@ func (client *Client) doSync(method, path string, query url.Values, headers map[ return client.doSyncWithOpts(method, path, query, headers, body, v, nil) } +// checkMaintenanceJSON checks if there is a maintenance.json file written by +// snapd the daemon that positively identifies snapd as being unavailable due to +// maintenance, either for snapd restarting itself to update, or rebooting the +// system to update the kernel or base snap, etc. If there is ongoing +// maintenance, then the maintenance object on the client is set appropriately. +// note that currently checkMaintenanceJSON does not return non-nil errors, so +// if the file is missing or corrupt or empty, nothing will happen +func (client *Client) checkMaintenanceJSON() error { + f, err := os.Open(dirs.SnapdMaintenanceFile) + // just continue if we can't read the maintenance file + if err != nil { + return nil + } + + // we have a maintenance file, try to read it + maintenance := &Error{} + + if err := json.NewDecoder(f).Decode(&maintenance); err != nil { + // if the json is malformed, just ignore it for now, we only use it for + // positive identification of snapd down for maintenance + return nil + } + + if maintenance != nil { + switch maintenance.Kind { + case ErrorKindDaemonRestart: + client.maintenance = maintenance + case ErrorKindSystemRestart: + client.maintenance = maintenance + } + // don't set maintenance for other kinds, as we don't know what it + // is yet + + // this also means an empty json object in maintenance.json doesn't get + // treated as a real maintenance downtime for example + } + + return nil +} + func (client *Client) doSyncWithOpts(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, opts *doOptions) (*ResultInfo, error) { + // first check maintenance.json to see if snapd is down for a restart, and + // set cli.maintenance as appropriate, then perform the request + // TODO: it would be a nice thing to skip the request if we know that snapd + // won't respond and return a specific error, but that's a big behavior + // change we probably shouldn't make right now, not to mention it probably + // requires adjustments in other areas too + if err := client.checkMaintenanceJSON(); err != nil { + return nil, err + } + var rsp response statusCode, err := client.do(method, path, query, headers, body, &rsp, opts) if err != nil { diff --git a/client/client_test.go b/client/client_test.go index 8b312cf169..20e2cfca4c 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -20,6 +20,7 @@ package client_test import ( + "encoding/json" "errors" "fmt" "io" @@ -145,6 +146,63 @@ func (cs *clientSuite) TestClientWorks(c *C) { c.Check(cs.req.URL.Path, Equals, "/this") } +func (cs *clientSuite) TestClientSetMaintenanceForMaintenanceJSON(c *C) { + // write a maintenance.json that says snapd is down for a restart + maintErr := &client.Error{ + Kind: client.ErrorKindSystemRestart, + Message: "system is restarting", + } + + b, err := json.Marshal(maintErr) + c.Assert(err, IsNil) + + c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755), IsNil) + c.Assert(ioutil.WriteFile(dirs.SnapdMaintenanceFile, b, 0644), IsNil) + var v []int + cs.rsp = `[1,2]` + reqBody := ioutil.NopCloser(strings.NewReader("")) + statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, nil) + c.Check(err, IsNil) + c.Check(statusCode, Equals, 200) + c.Check(v, DeepEquals, []int{1, 2}) + c.Assert(cs.req, NotNil) + c.Assert(cs.req.URL, NotNil) + c.Check(cs.req.Method, Equals, "GET") + c.Check(cs.req.Body, Equals, reqBody) + c.Check(cs.req.URL.Path, Equals, "/this") + + returnedErr := cs.cli.Maintenance() + c.Assert(returnedErr, Not(IsNil)) + + returnedMaintErr, ok := returnedErr.(*client.Error) + c.Check(ok, Equals, true) + + c.Assert(returnedMaintErr, DeepEquals, maintErr) +} + +func (cs *clientSuite) TestClientIgnoresGarbageMaintenanceJSON(c *C) { + // write a garbage maintenance.json that can't be unmarshalled + maintGarbage := []byte("blah blah blah not json") + + c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755), IsNil) + c.Assert(ioutil.WriteFile(dirs.SnapdMaintenanceFile, maintGarbage, 0644), IsNil) + var v []int + cs.rsp = `[1,2]` + reqBody := ioutil.NopCloser(strings.NewReader("")) + statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, nil) + c.Check(err, IsNil) + c.Check(statusCode, Equals, 200) + c.Check(v, DeepEquals, []int{1, 2}) + c.Assert(cs.req, NotNil) + c.Assert(cs.req.URL, NotNil) + c.Check(cs.req.Method, Equals, "GET") + c.Check(cs.req.Body, Equals, reqBody) + c.Check(cs.req.URL.Path, Equals, "/this") + + returnedErr := cs.cli.Maintenance() + c.Assert(returnedErr, IsNil) +} + func (cs *clientSuite) TestClientDoNoTimeoutIgnoresRetry(c *C) { var v []int cs.rsp = `[1,2]` diff --git a/client/console_conf.go b/client/console_conf.go new file mode 100644 index 0000000000..d07899ce03 --- /dev/null +++ b/client/console_conf.go @@ -0,0 +1,44 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 <http://www.gnu.org/licenses/>. + * + */ + +package client + +import "time" + +// InternalConsoleConfStartResponse is the response from console-conf start +// support +type InternalConsoleConfStartResponse struct { + ActiveAutoRefreshChanges []string `json:"active-auto-refreshes,omitempty"` + ActiveAutoRefreshSnaps []string `json:"active-auto-refresh-snaps,omitempty"` +} + +// InternalConsoleConfStart invokes the dedicated console-conf start support +// to handle intervening auto-refreshes. +// Not for general use. +func (client *Client) InternalConsoleConfStart() ([]string, []string, error) { + resp := &InternalConsoleConfStartResponse{} + // do the post with a short timeout so that if snapd is not available due to + // maintenance we will return very quickly so the caller can handle that + opts := &doOptions{ + Timeout: 2 * time.Second, + Retry: 1 * time.Hour, + } + _, err := client.doSyncWithOpts("POST", "/v2/internal/console-conf-start", nil, nil, nil, resp, opts) + return resp.ActiveAutoRefreshChanges, resp.ActiveAutoRefreshSnaps, err +} diff --git a/client/console_conf_test.go b/client/console_conf_test.go new file mode 100644 index 0000000000..09aab5fff2 --- /dev/null +++ b/client/console_conf_test.go @@ -0,0 +1,63 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 <http://www.gnu.org/licenses/>. + * + */ + +package client_test + +import ( + . "gopkg.in/check.v1" +) + +func (cs *clientSuite) TestClientInternalConsoleConfEndpointEmpty(c *C) { + // no changes and no snaps + cs.status = 200 + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {} + }` + + chgs, snaps, err := cs.cli.InternalConsoleConfStart() + c.Assert(chgs, HasLen, 0) + c.Assert(snaps, HasLen, 0) + c.Assert(err, IsNil) + c.Check(cs.req.Method, Equals, "POST") + c.Check(cs.req.URL.Path, Equals, "/v2/internal/console-conf-start") + c.Check(cs.doCalls, Equals, 1) +} + +func (cs *clientSuite) TestClientInternalConsoleConfEndpoint(c *C) { + // some changes and snaps + cs.status = 200 + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel"] + } + }` + + chgs, snaps, err := cs.cli.InternalConsoleConfStart() + c.Assert(err, IsNil) + c.Assert(chgs, DeepEquals, []string{"1"}) + c.Assert(snaps, DeepEquals, []string{"pc-kernel"}) + c.Check(cs.req.Method, Equals, "POST") + c.Check(cs.req.URL.Path, Equals, "/v2/internal/console-conf-start") + c.Check(cs.doCalls, Equals, 1) +} diff --git a/cmd/snap/cmd_routine_console_conf.go b/cmd/snap/cmd_routine_console_conf.go new file mode 100644 index 0000000000..cb412c1e1d --- /dev/null +++ b/cmd/snap/cmd_routine_console_conf.go @@ -0,0 +1,144 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 <http://www.gnu.org/licenses/>. + * + */ + +package main + +import ( + "fmt" + "sort" + "strings" + "sync" + "time" + + "github.com/snapcore/snapd/client" + + "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/i18n" +) + +type cmdRoutineConsoleConfStart struct { + clientMixin +} + +var shortRoutineConsoleConfStartHelp = i18n.G("Start console-conf snapd routine") +var longRoutineConsoleConfStartHelp = i18n.G(` +The console-conf-start command starts synchronization with console-conf + +This command is used by console-conf when it starts up. It delays refreshes if +there are none currently ongoing, and exits with a specific error code if there +are ongoing refreshes which console-conf should wait for before prompting the +user to begin configuring the device. +`) + +// TODO: move these to their own package for unified time constants for how +// often or long we do things like waiting for a reboot, etc. ? +var snapdAPIInterval = 2 * time.Second +var snapdWaitForFullSystemReboot = 10 * time.Minute + +func init() { + c := addRoutineCommand("console-conf-start", shortRoutineConsoleConfStartHelp, longRoutineConsoleConfStartHelp, func() flags.Commander { + return &cmdRoutineConsoleConfStart{} + }, nil, nil) + c.hidden = true +} + +func printfFunc(msg string, format ...interface{}) func() { + return func() { + fmt.Fprintf(Stderr, msg, format...) + } +} + +func (x *cmdRoutineConsoleConfStart) Execute(args []string) error { + snapdReloadMsgOnce := sync.Once{} + systemReloadMsgOnce := sync.Once{} + snapRefreshMsgOnce := sync.Once{} + + for { + chgs, snaps, err := x.client.InternalConsoleConfStart() + if err != nil { + // snapd may be under maintenance right now, either for base/kernel + // snap refreshes which result in a reboot, or for snapd itself + // which just results in a restart of the daemon + maybeMaintErr := x.client.Maintenance() + if maybeMaintErr == nil { + // not a maintenance error, give up + return err + } + + maintErr, ok := maybeMaintErr.(*client.Error) + if !ok { + // if cli.Maintenance() didn't return a client.Error we have very weird + // problems + return fmt.Errorf("internal error: client.Maintenance() didn't return a client.Error") + } + + if maintErr.Kind == client.ErrorKindDaemonRestart { + // then we need to wait for snapd to restart, so keep trying + // the console-conf-start endpoint until it works + snapdReloadMsgOnce.Do(printfFunc("Snapd is reloading, please wait...\n")) + + // we know that snapd isn't available because it is in + // maintenance so we don't gain anything by hitting it + // more frequently except for perhaps a quicker latency + // for the user when it comes back, but it will be busy + // doing things when it starts up anyways so it won't be + // able to respond immediately + time.Sleep(snapdAPIInterval) + continue + } else if maintErr.Kind == client.ErrorKindSystemRestart { + // system is rebooting, just wait for the reboot + systemReloadMsgOnce.Do(printfFunc("System is rebooting, please wait for reboot...\n")) + time.Sleep(snapdWaitForFullSystemReboot) + // if we didn't reboot after 10 minutes something's probably broken + return fmt.Errorf("system didn't reboot after 10 minutes even though snapd daemon is in maintenance") + } + } + + if len(chgs) == 0 { + break + } + + if len(snaps) == 0 { + // internal error if we have chg id's, but no snaps + return fmt.Errorf("internal error: returned changes (%v) but no snap names", chgs) + } + + snapRefreshMsgOnce.Do(func() { + sort.Strings(snaps) + + var snapNameList string + switch len(snaps) { + case 1: + snapNameList = snaps[0] + case 2: + snapNameList = fmt.Sprintf("%s and %s", snaps[0], snaps[1]) + default: + // don't forget the oxford comma! + snapNameList = fmt.Sprintf("%s, and %s", strings.Join(snaps[:len(snaps)-1], ", "), snaps[len(snaps)-1]) + } + + fmt.Fprintf(Stderr, "Snaps (%s) are refreshing, please wait...\n", snapNameList) + }) + + // don't DDOS snapd by hitting it's API too often + time.Sleep(snapdAPIInterval) + } + + return nil +} diff --git a/cmd/snap/cmd_routine_console_conf_test.go b/cmd/snap/cmd_routine_console_conf_test.go new file mode 100644 index 0000000000..50c529d487 --- /dev/null +++ b/cmd/snap/cmd_routine_console_conf_test.go @@ -0,0 +1,355 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 <http://www.gnu.org/licenses/>. + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/testutil" +) + +func (s *SnapSuite) TestRoutineConsoleConfStartTrivialCase(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestRoutineConsoleConfStartInconsistentAPIResponseError(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // return just refresh changes but no snap ids + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"] + } + }`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, ErrorMatches, `internal error: returned changes .* but no snap names`) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestRoutineConsoleConfStartNonMaintenanceErrorReturned(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // return internal server error + fmt.Fprintf(w, `{ + "type":"error", + "status-code": 500, + "result": { + "message": "broken server" + } + }`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, ErrorMatches, "broken server") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestRoutineConsoleConfStartSingleSnap(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + // first 4 times we hit the API there is a snap refresh ongoing + case 1, 2, 3, 4: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // return just refresh changes but no snap ids + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel"] + } + }`) + // 5th time we return nothing as we are done + case 5: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "Snaps (pc-kernel) are refreshing, please wait...\n") +} + +func (s *SnapSuite) TestRoutineConsoleConfStartTwoSnaps(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + // first 4 times we hit the API there is a snap refresh ongoing + case 1, 2, 3, 4: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // return just refresh changes but no snap ids + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel","core20"] + } + }`) + // 5th time we return nothing as we are done + case 5: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "Snaps (core20 and pc-kernel) are refreshing, please wait...\n") +} + +func (s *SnapSuite) TestRoutineConsoleConfStartMultipleSnaps(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + // first 4 times we hit the API there are snap refreshes ongoing + case 1, 2, 3, 4: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel","snapd","core20","pc"] + } + }`) + // 5th time we return nothing as we are done + case 5: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "Snaps (core20, pc, pc-kernel, and snapd) are refreshing, please wait...\n") +} + +// TODO:UC20: when maintenance.json is a thing, then add a similar test as this +// one, but using the maintenance.json file instead of the maintenance response +// as that is closer to the real desired behavior +func (s *SnapSuite) TestRoutineConsoleConfStartSnapdRefreshRestart(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + + // 1st time we hit the API there is a snapd snap refresh ongoing + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["snapd"] + } + }`) + + // 2nd time we hit the API, set maintenance in the response + case 2: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["snapd"] + }, + "maintenance": { + "kind": "daemon-restart", + "message": "daemon is restarting" + } + }`) + + // 3rd time we return nothing as if we are down for maintenance + case 3: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // 4th time we resume responding, but still in progress + case 4: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["snapd"] + } + }`) + + // 5th time we are actually done + case 5: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), testutil.Contains, "Snapd is reloading, please wait...\n") + c.Check(s.Stderr(), testutil.Contains, "Snaps (snapd) are refreshing, please wait...\n") +} + +func (s *SnapSuite) TestRoutineConsoleConfStartKernelRefreshReboot(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + r = snap.MockSnapdWaitForFullSystemReboot(0) + defer r() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + + // 1st time we hit the API there is a snapd snap refresh ongoing + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel"] + } + }`) + + // 2nd time we hit the API, set maintenance in the response, but still + // give a valid response (so that it reads the maintenance) + case 2: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel"] + }, + "maintenance": { + "kind": "system-restart", + "message": "system is restarting" + } + }`) + + // 3rd time we hit the API, we need to not return anything so that the + // client will inspect the error and see there is a maintenance error + case 3: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + default: + c.Errorf("unexpected %s request (number %d) to %s", r.Method, n, r.URL.Path) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + // this is the internal error, which we will hit immediately for testing, + // in a real scenario a reboot would happen OOTB from the snap client + c.Assert(err, ErrorMatches, "system didn't reboot after 10 minutes even though snapd daemon is in maintenance") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), testutil.Contains, "System is rebooting, please wait for reboot...\n") + c.Check(s.Stderr(), testutil.Contains, "Snaps (pc-kernel) are refreshing, please wait...\n") +} diff --git a/cmd/snap/export_test.go b/cmd/snap/export_test.go index 23f0418770..733b065488 100644 --- a/cmd/snap/export_test.go +++ b/cmd/snap/export_test.go @@ -384,3 +384,19 @@ func MockDownloadDirect(f func(snapName string, revision snap.Revision, dlOpts i downloadDirect = old } } + +func MockSnapdAPIInterval(t time.Duration) (restore func()) { + old := snapdAPIInterval + snapdAPIInterval = t + return func() { + snapdAPIInterval = old + } +} + +func MockSnapdWaitForFullSystemReboot(t time.Duration) (restore func()) { + old := snapdWaitForFullSystemReboot + snapdWaitForFullSystemReboot = t + return func() { + snapdWaitForFullSystemReboot = old + } +} diff --git a/daemon/api.go b/daemon/api.go index 541c01b5a9..4465cc0f42 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -113,6 +113,7 @@ var api = []*Command{ serialModelCmd, systemsCmd, systemsActionCmd, + routineConsoleConfStartCmd, } var servicestateControl = servicestate.Control diff --git a/daemon/api_console_conf.go b/daemon/api_console_conf.go new file mode 100644 index 0000000000..dda28eca95 --- /dev/null +++ b/daemon/api_console_conf.go @@ -0,0 +1,96 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 <http://www.gnu.org/licenses/>. + * + */ + +package daemon + +import ( + "encoding/json" + "io" + "net/http" + "time" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/overlord/auth" +) + +var ( + routineConsoleConfStartCmd = &Command{ + Path: "/v2/internal/console-conf-start", + POST: consoleConfStartRoutine, + } +) + +var delayTime = 20 * time.Minute + +type consoleConfRoutine struct{} + +// ConsoleConfStartRoutineResult is the result of running the console-conf start +// routine.. +type ConsoleConfStartRoutineResult struct { + ActiveAutoRefreshChanges []string `json:"active-auto-refreshes,omitempty"` + ActiveAutoRefreshSnaps []string `json:"active-auto-refresh-snaps,omitempty"` +} + +func consoleConfStartRoutine(c *Command, r *http.Request, _ *auth.UserState) Response { + // no body expected, error if we were provided anything + defer r.Body.Close() + var routineBody struct{} + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&routineBody); err != nil && err != io.EOF { + return BadRequest("cannot decode request body into console-conf operation: %v", err) + } + + // now run the start routine first by trying to grab a lock on the refreshes + // for all snaps, which fails if there are any active changes refreshing + // snaps + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + snapAutoRefreshChanges, err := c.d.overlord.SnapManager().EnsureAutoRefreshesAreDelayed(delayTime) + if err != nil { + return InternalError(err.Error()) + } + + logger.Debugf("Ensured that new auto refreshes are delayed by %s to allow console-conf to run", delayTime) + + if len(snapAutoRefreshChanges) == 0 { + // no changes yet, and we delayed the refresh successfully so + // console-conf is okay to run normally + return SyncResponse(&ConsoleConfStartRoutineResult{}, nil) + } + + chgIds := make([]string, 0, len(snapAutoRefreshChanges)) + snapNames := make([]string, 0) + for _, chg := range snapAutoRefreshChanges { + chgIds = append(chgIds, chg.ID()) + var updatedSnaps []string + err := chg.Get("snap-names", &updatedSnaps) + if err != nil { + return InternalError(err.Error()) + } + snapNames = append(snapNames, updatedSnaps...) + } + + // we have changes that the client should wait for before being ready + return SyncResponse(&ConsoleConfStartRoutineResult{ + ActiveAutoRefreshChanges: chgIds, + ActiveAutoRefreshSnaps: snapNames, + }, nil) +} diff --git a/daemon/api_console_conf_test.go b/daemon/api_console_conf_test.go new file mode 100644 index 0000000000..19380fa09d --- /dev/null +++ b/daemon/api_console_conf_test.go @@ -0,0 +1,116 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 <http://www.gnu.org/licenses/>. + * + */ + +package daemon + +import ( + "bytes" + "net/http" + "sort" + "time" + + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "gopkg.in/check.v1" + . "gopkg.in/check.v1" +) + +var _ = Suite(&consoleConfSuite{}) + +type consoleConfSuite struct { + apiBaseSuite +} + +func (s *consoleConfSuite) TestPostConsoleConfStartRoutine(c *C) { + t0 := time.Now() + d := s.daemonWithOverlordMock(c) + snapMgr, err := snapstate.Manager(d.overlord.State(), d.overlord.TaskRunner()) + c.Assert(err, check.IsNil) + d.overlord.AddManager(snapMgr) + + st := d.overlord.State() + + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("POST", "/v2/internal/console-conf-start", body) + c.Assert(err, IsNil) + + // no changes in state, no changes in response + rsp := consoleConfStartRoutine(routineConsoleConfStartCmd, req, nil).(*resp) + c.Check(rsp.Type, Equals, ResponseTypeSync) + c.Assert(rsp.Result, DeepEquals, &ConsoleConfStartRoutineResult{}) + + // we did set the refresh.hold time back 20 minutes though + st.Lock() + defer st.Unlock() + + tr := config.NewTransaction(st) + var t1 time.Time + err = tr.Get("core", "refresh.hold", &t1) + c.Assert(err, IsNil) + + c.Assert(t0.Add(20*time.Minute).After(t1), Equals, false) + + // if we add some changes to state that are in progress, then they are + // returned + + // now make some auto-refresh changes to make sure we get those figured out + chg0 := st.NewChange("auto-refresh", "auto-refresh-the-things") + chg0.AddTask(st.NewTask("nop", "do nothing")) + + // make it in doing state + chg0.SetStatus(state.DoingStatus) + chg0.Set("snap-names", []string{"doing-snap"}) + + // this one will be picked up too + chg1 := st.NewChange("auto-refresh", "auto-refresh-the-things") + chg1.AddTask(st.NewTask("nop", "do nothing")) + chg1.SetStatus(state.DoStatus) + chg1.Set("snap-names", []string{"do-snap"}) + + // this one won't, it's Done + chg2 := st.NewChange("auto-refresh", "auto-refresh-the-things") + chg2.AddTask(st.NewTask("nop", "do nothing")) + chg2.SetStatus(state.DoneStatus) + chg2.Set("snap-names", []string{"done-snap"}) + + // nor this one, it's Undone + chg3 := st.NewChange("auto-refresh", "auto-refresh-the-things") + chg3.AddTask(st.NewTask("nop", "do nothing")) + chg3.SetStatus(state.UndoneStatus) + chg3.Set("snap-names", []string{"undone-snap"}) + + st.Unlock() + defer st.Lock() + + req2, err := http.NewRequest("POST", "/v2/internal/console-conf-start", body) + c.Assert(err, IsNil) + rsp2 := consoleConfStartRoutine(routineConsoleConfStartCmd, req2, nil).(*resp) + c.Check(rsp2.Type, Equals, ResponseTypeSync) + c.Assert(rsp2.Result, FitsTypeOf, &ConsoleConfStartRoutineResult{}) + res := rsp2.Result.(*ConsoleConfStartRoutineResult) + sort.Strings(res.ActiveAutoRefreshChanges) + sort.Strings(res.ActiveAutoRefreshSnaps) + expChgs := []string{chg0.ID(), chg1.ID()} + sort.Strings(expChgs) + c.Assert(res, DeepEquals, &ConsoleConfStartRoutineResult{ + ActiveAutoRefreshChanges: expChgs, + ActiveAutoRefreshSnaps: []string{"do-snap", "doing-snap"}, + }) +} diff --git a/daemon/api_systems_test.go b/daemon/api_systems_test.go index 8cd0f1ffc4..b98cb62bc1 100644 --- a/daemon/api_systems_test.go +++ b/daemon/api_systems_test.go @@ -487,7 +487,7 @@ func (s *apiSuite) TestSystemActionRequestWithSeeded(c *check.C) { // daemon is not started, only check whether reboot was scheduled as expected // reboot flag - c.Check(d.restartSystem, check.Equals, state.RestartSystemNow, check.Commentf(tc.comment)) + c.Check(d.requestedRestart, check.Equals, state.RestartSystemNow, check.Commentf(tc.comment)) // slow reboot schedule c.Check(cmd.Calls(), check.DeepEquals, [][]string{ {"shutdown", "-r", "+10", "reboot scheduled to update the system"}, diff --git a/daemon/daemon.go b/daemon/daemon.go index 640010b938..4ad3798ae9 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -20,7 +20,9 @@ package daemon import ( + "bytes" "context" + "encoding/json" "fmt" "net" "net/http" @@ -55,6 +57,12 @@ var ErrRestartSocket = fmt.Errorf("daemon stop requested to wait for socket acti var systemdSdNotify = systemd.SdNotify +const ( + daemonRestartMsg = "system is restarting" + systemRestartMsg = "daemon is restarting" + socketRestartMsg = "daemon is stopping to wait for socket activation" +) + // A Daemon listens for requests and routes them to the right command type Daemon struct { Version string @@ -68,8 +76,8 @@ type Daemon struct { router *mux.Router standbyOpinions *standby.StandbyOpinions - // set to remember we need to restart the system - restartSystem state.RestartType + // set to what kind of restart was requested if any + requestedRestart state.RestartType // set to remember that we need to exit the daemon in a way that // prevents systemd from restarting it restartSocket bool @@ -259,14 +267,10 @@ func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) { if rsp, ok := rsp.(*resp); ok { _, rst := st.Restarting() - switch rst { - case state.RestartSystem, state.RestartSystemNow: - rsp.transmitMaintenance(client.ErrorKindSystemRestart, "system is restarting") - case state.RestartDaemon: - rsp.transmitMaintenance(client.ErrorKindDaemonRestart, "daemon is restarting") - case state.RestartSocket: - rsp.transmitMaintenance(client.ErrorKindDaemonRestart, "daemon is stopping to wait for socket activation") + if rst != state.RestartUnset { + rsp.Maintenance = maintenanceForRestartType(rst) } + if rsp.Type != ResponseTypeError { st.Lock() count, stamp := st.WarningsSummary() @@ -450,6 +454,14 @@ func (d *Daemon) Start() error { // enable standby handling d.initStandbyHandling() + // before serving actual connections remove the maintenance.json file as we + // are no longer down for maintenance, this state most closely corresponds + // to state.RestartUnset + err = d.updateMaintenanceFile(state.RestartUnset) + if err != nil { + return err + } + // the loop runs in its own goroutine d.overlord.Loop() @@ -481,6 +493,10 @@ func (d *Daemon) HandleRestart(t state.RestartType) { // die when asked to restart (systemd should get us back up!) etc switch t { case state.RestartDaemon: + d.mu.Lock() + defer d.mu.Unlock() + // save the restart kind to write out a maintenance.json in a bit + d.requestedRestart = t case state.RestartSystem, state.RestartSystemNow: // try to schedule a fallback slow reboot already here // in case we get stuck shutting down @@ -490,17 +506,20 @@ func (d *Daemon) HandleRestart(t state.RestartType) { d.mu.Lock() defer d.mu.Unlock() - // remember we need to restart the system - d.restartSystem = t + // save the restart kind to write out a maintenance.json in a bit + d.requestedRestart = t case state.RestartSocket: d.mu.Lock() defer d.mu.Unlock() + // save the restart kind to write out a maintenance.json in a bit + d.requestedRestart = t d.restartSocket = true case state.StopDaemon: logger.Noticef("stopping snapd as requested") default: logger.Noticef("internal error: restart handler called with unknown restart type: %v", t) } + d.tomb.Kill(nil) } @@ -511,6 +530,32 @@ var ( rebootMaxTentatives = 3 ) +func (d *Daemon) updateMaintenanceFile(rst state.RestartType) error { + // for unset restart, just remove the maintenance.json file + if rst == state.RestartUnset { + err := os.Remove(dirs.SnapdMaintenanceFile) + // only return err if the error was something other than the file not + // existing + if err != nil && !os.IsNotExist(err) { + return err + } + return nil + } + + // otherwise marshal and write it out appropriately + b, err := json.Marshal(maintenanceForRestartType(rst)) + if err != nil { + return err + } + + err = osutil.AtomicWrite(dirs.SnapdMaintenanceFile, bytes.NewBuffer(b), 0644, 0) + if err != nil { + return err + } + + return nil +} + // Stop shuts down the Daemon func (d *Daemon) Stop(sigCh chan<- os.Signal) error { // we need to schedule/wait for a system restart again @@ -525,12 +570,24 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error { d.tomb.Kill(nil) + // check the state associated with a potential restart with the lock to + // prevent races d.mu.Lock() - restartSystem := d.restartSystem != state.RestartUnset - immediateReboot := d.restartSystem == state.RestartSystemNow + // needsFullReboot is whether the entire system will be rebooted or not as + // a consequence of this restart + needsFullReboot := (d.requestedRestart == state.RestartSystemNow || d.requestedRestart == state.RestartSystem) + immediateReboot := d.requestedRestart == state.RestartSystemNow restartSocket := d.restartSocket d.mu.Unlock() + // before not accepting any new client connections we need to write the + // maintenance.json file for potential clients to see after the daemon stops + // responding so they can read it correctly and handle the maintenance + err := d.updateMaintenanceFile(d.requestedRestart) + if err != nil { + logger.Noticef("error writing maintenance file: %v", err) + } + d.snapdListener.Close() d.standbyOpinions.Stop() @@ -547,7 +604,7 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error { d.snapListener.Close() } - if restartSystem { + if needsFullReboot { // give time to polling clients to notice restart time.Sleep(rebootNoticeWait) } @@ -559,10 +616,9 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error { d.tomb.Kill(d.serve.Shutdown(ctx)) cancel() - if !restartSystem { + if !needsFullReboot { // tell systemd that we are stopping systemdSdNotify("STOPPING=1") - } if restartSocket { @@ -580,7 +636,7 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error { } d.overlord.Stop() - err := d.tomb.Wait() + err = d.tomb.Wait() if err != nil { if err == context.DeadlineExceeded { logger.Noticef("WARNING: cannot gracefully shut down in-flight snapd API activity within: %v", shutdownTimeout) @@ -592,7 +648,7 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error { // because we already scheduled a slow shutdown and // exiting here will just restart snapd (via systemd) // which will lead to confusing results. - if restartSystem { + if needsFullReboot { logger.Noticef("WARNING: cannot stop daemon: %v", err) } else { return err @@ -600,7 +656,7 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error { } } - if restartSystem { + if needsFullReboot { return d.doReboot(sigCh, immediateReboot, rebootWaitTimeout) } diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go index bf6344edb2..b2bde1db8d 100644 --- a/daemon/daemon_test.go +++ b/daemon/daemon_test.go @@ -209,6 +209,80 @@ func (s *daemonSuite) TestCommandRestartingState(c *check.C) { }) } +func (s *daemonSuite) TestMaintenanceJsonDeletedOnStart(c *check.C) { + // write a maintenance.json file that has that the system is restarting + maintErr := &errorResult{ + Kind: client.ErrorKindDaemonRestart, + Message: systemRestartMsg, + } + + b, err := json.Marshal(maintErr) + c.Assert(err, check.IsNil) + + c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755), check.IsNil) + + c.Assert(ioutil.WriteFile(dirs.SnapdMaintenanceFile, b, 0644), check.IsNil) + + d := newTestDaemon(c) + + // mark as already seeded + s.markSeeded(d) + // and pretend we have snaps + st := d.overlord.State() + st.Lock() + snapstate.Set(st, "core", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "core", Revision: snap.R(1), SnapID: "core-snap-id"}, + }, + Current: snap.R(1), + }) + st.Unlock() + // 1 snap => extended timeout 30s + 5s + const extendedTimeoutUSec = "EXTEND_TIMEOUT_USEC=35000000" + + l1, err := net.Listen("tcp", "127.0.0.1:0") + c.Assert(err, check.IsNil) + l2, err := net.Listen("tcp", "127.0.0.1:0") + c.Assert(err, check.IsNil) + + snapdAccept := make(chan struct{}) + d.snapdListener = &witnessAcceptListener{Listener: l1, accept: snapdAccept} + + snapAccept := make(chan struct{}) + d.snapListener = &witnessAcceptListener{Listener: l2, accept: snapAccept} + + d.Start() + + snapdDone := make(chan struct{}) + go func() { + select { + case <-snapdAccept: + case <-time.After(2 * time.Second): + c.Fatal("snapd accept was not called") + } + close(snapdDone) + }() + + snapDone := make(chan struct{}) + go func() { + select { + case <-snapAccept: + case <-time.After(2 * time.Second): + c.Fatal("snapd accept was not called") + } + close(snapDone) + }() + + <-snapdDone + <-snapDone + + // maintenance.json should be removed + c.Assert(dirs.SnapdMaintenanceFile, testutil.FileAbsent) + + d.Stop(nil) +} + func (s *daemonSuite) TestFillsWarnings(c *check.C) { d := newTestDaemon(c) @@ -590,7 +664,12 @@ func (s *daemonSuite) TestRestartWiring(c *check.C) { d.snapListener = &witnessAcceptListener{Listener: l, accept: snapAccept} c.Assert(d.Start(), check.IsNil) - defer d.Stop(nil) + stoppedYet := false + defer func() { + if !stoppedYet { + d.Stop(nil) + } + }() snapdDone := make(chan struct{}) go func() { @@ -622,6 +701,11 @@ func (s *daemonSuite) TestRestartWiring(c *check.C) { case <-time.After(2 * time.Second): c.Fatal("RequestRestart -> overlord -> Kill chain didn't work") } + + d.Stop(nil) + stoppedYet = true + + c.Assert(s.notified, check.DeepEquals, []string{"EXTEND_TIMEOUT_USEC=30000000", "READY=1", "STOPPING=1"}) } func (s *daemonSuite) TestGracefulStop(c *check.C) { @@ -877,7 +961,7 @@ func (s *daemonSuite) testRestartSystemWiring(c *check.C, restartKind state.Rest defer func() { d.mu.Lock() - d.restartSystem = state.RestartUnset + d.requestedRestart = state.RestartUnset d.mu.Unlock() }() @@ -888,7 +972,7 @@ func (s *daemonSuite) testRestartSystemWiring(c *check.C, restartKind state.Rest } d.mu.Lock() - rs := d.restartSystem + rs := d.requestedRestart d.mu.Unlock() c.Check(rs, check.Equals, restartKind) @@ -924,6 +1008,17 @@ func (s *daemonSuite) testRestartSystemWiring(c *check.C, restartKind state.Rest // should be good enough c.Check(rebootAt.Before(now.Add(10*time.Second)), check.Equals, true) } + + // finally check that maintenance.json was written appropriate for this + // restart reason + b, err := ioutil.ReadFile(dirs.SnapdMaintenanceFile) + c.Assert(err, check.IsNil) + + maintErr := &errorResult{} + c.Assert(json.Unmarshal(b, maintErr), check.IsNil) + + exp := maintenanceForRestartType(restartKind) + c.Assert(maintErr, check.DeepEquals, exp) } func (s *daemonSuite) TestRestartSystemGracefulWiring(c *check.C) { diff --git a/daemon/response.go b/daemon/response.go index d2b0091b2d..680874994c 100644 --- a/daemon/response.go +++ b/daemon/response.go @@ -37,6 +37,7 @@ import ( "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/snapshotstate" "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/store" "github.com/snapcore/snapd/systemd" @@ -67,11 +68,23 @@ type resp struct { Maintenance *errorResult `json:"maintenance,omitempty"` } -func (r *resp) transmitMaintenance(kind client.ErrorKind, message string) { - r.Maintenance = &errorResult{ - Kind: kind, - Message: message, - } +func maintenanceForRestartType(rst state.RestartType) *errorResult { + e := &errorResult{} + switch rst { + case state.RestartSystem, state.RestartSystemNow: + e.Kind = client.ErrorKindSystemRestart + e.Message = daemonRestartMsg + case state.RestartDaemon: + e.Kind = client.ErrorKindDaemonRestart + e.Message = systemRestartMsg + case state.RestartSocket: + e.Kind = client.ErrorKindDaemonRestart + e.Message = socketRestartMsg + case state.RestartUnset: + // shouldn't happen, maintenance for unset type should just be nil + panic("internal error: cannot marshal maintenance for RestartUnset") + } + return e } func (r *resp) addWarningsToMeta(count int, stamp time.Time) { @@ -92,7 +105,7 @@ func (r *resp) addWarningsToMeta(count int, stamp time.Time) { // JSON representation in the API in time for the release. // The right code style takes a bit more work and unifies // these fields inside resp. -// Increment the counter if you read this: 42 +// Increment the counter if you read this: 43 type Meta struct { Sources []string `json:"sources,omitempty"` SuggestedCurrency string `json:"suggested-currency,omitempty"` diff --git a/dirs/dirs.go b/dirs/dirs.go index 30bc7b4503..20dc5cac43 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -58,6 +58,8 @@ var ( SnapRunLockDir string SnapBootstrapRunDir string + SnapdMaintenanceFile string + SnapdStoreSSLCertsDir string SnapSeedDir string @@ -314,6 +316,7 @@ func SetRootDir(rootdir string) { SnapSeccompDir = filepath.Join(SnapSeccompBase, "bpf") SnapMountPolicyDir = filepath.Join(rootdir, snappyDir, "mount") SnapMetaDir = filepath.Join(rootdir, snappyDir, "meta") + SnapdMaintenanceFile = filepath.Join(rootdir, snappyDir, "maintenance.json") SnapBlobDir = SnapBlobDirUnder(rootdir) // ${snappyDir}/desktop is added to $XDG_DATA_DIRS. // Subdirectories are interpreted according to the relevant diff --git a/interfaces/apparmor/template.go b/interfaces/apparmor/template.go index 19b44b4789..187423e4d0 100644 --- a/interfaces/apparmor/template.go +++ b/interfaces/apparmor/template.go @@ -314,6 +314,9 @@ var templateCommon = ` # Read-only of this snap /var/lib/snapd/snaps/@{SNAP_NAME}_*.snap r, + # Read-only of snapd restart state for snapctl specifically + /var/lib/snapd/maintenance.json r, + # Read-only for the install directory # bind mount used here (see 'parallel installs', above) @{INSTALL_DIR}/{@{SNAP_NAME},@{SNAP_INSTANCE_NAME}}/ r, diff --git a/interfaces/builtin/x11.go b/interfaces/builtin/x11.go index 9d03374e64..9ff7013a21 100644 --- a/interfaces/builtin/x11.go +++ b/interfaces/builtin/x11.go @@ -102,6 +102,7 @@ unix (connect, receive, send, accept) type=stream addr="@/tmp/.X11-unix/X[0-9]*" peer=(label=###PLUG_SECURITY_TAGS###), +# TODO: deprecate and remove this if it doesn't break X11 server snaps. unix (connect, receive, send, accept) type=stream addr="@/tmp/.ICE-unix/[0-9]*" @@ -138,6 +139,14 @@ owner /run/user/[0-9]*/mutter/Xauthority r, network netlink raw, /run/udev/data/c13:[0-9]* r, /run/udev/data/+input:* r, + +# Deny access to ICE granted by abstractions/X +# See: https://bugs.launchpad.net/snapd/+bug/1901489 +deny owner @{HOME}/.ICEauthority r, +deny owner /run/user/*/ICEauthority r, +deny unix (connect, receive, send) + type=stream + peer=(addr="@/tmp/.ICE-unix/[0-9]*"), ` const x11ConnectedPlugSecComp = ` diff --git a/osutil/disks/mockdisk.go b/osutil/disks/mockdisk.go index 609a417c39..e90514831f 100644 --- a/osutil/disks/mockdisk.go +++ b/osutil/disks/mockdisk.go @@ -96,9 +96,23 @@ type Mountpoint struct { func MockMountPointDisksToPartitionMapping(mockedMountPoints map[Mountpoint]*MockDiskMapping) (restore func()) { osutil.MustBeTestBinary("mock disks only to be used in tests") - // verify that all unique MockDiskMapping's have unique DevNum's + // verify that all unique MockDiskMapping's have unique DevNum's and that + // the srcMntPt's are all consistent + // we can't have the same mountpoint exist both as a decrypted device and + // not as a decrypted device, this is an impossible mapping, but we need to + // expose functionality to mock the same mountpoint as a decrypted device + // and as an unencrypyted device for different tests, but never at the same + // time with the same mapping alreadySeen := make(map[string]*MockDiskMapping, len(mockedMountPoints)) - for _, mockDisk := range mockedMountPoints { + seenSrcMntPts := make(map[string]bool, len(mockedMountPoints)) + for srcMntPt, mockDisk := range mockedMountPoints { + if decryptedVal, ok := seenSrcMntPts[srcMntPt.Mountpoint]; ok { + if decryptedVal != srcMntPt.IsDecryptedDevice { + msg := fmt.Sprintf("mocked source mountpoint %s is duplicated with different options - previous option for IsDecryptedDevice was %t, current option is %t", srcMntPt.Mountpoint, decryptedVal, srcMntPt.IsDecryptedDevice) + panic(msg) + } + } + seenSrcMntPts[srcMntPt.Mountpoint] = srcMntPt.IsDecryptedDevice if old, ok := alreadySeen[mockDisk.DevNum]; ok { if mockDisk != old { // we already saw a disk with this DevNum as a different pointer diff --git a/osutil/disks/mockdisk_test.go b/osutil/disks/mockdisk_test.go index 7acb13e05a..caded22186 100644 --- a/osutil/disks/mockdisk_test.go +++ b/osutil/disks/mockdisk_test.go @@ -88,6 +88,31 @@ func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMappingVerifiesUniquen defer r() } +func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMappingVerifiesConsistency(c *C) { + d1 := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "label1": "part1", + }, + DiskHasPartitions: true, + DevNum: "d1", + } + + // a mountpoint mapping where the same mountpoint has different options for + // the source mountpoint + m := map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: "mount1", IsDecryptedDevice: false}: d1, + {Mountpoint: "mount1", IsDecryptedDevice: true}: d1, + } + + // mocking shouldn't work + c.Assert( + func() { disks.MockMountPointDisksToPartitionMapping(m) }, + PanicMatches, + // use .* for true/false since iterating over map order is not defined + `mocked source mountpoint mount1 is duplicated with different options - previous option for IsDecryptedDevice was .*, current option is .*`, + ) +} + func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMapping(c *C) { d1 := &disks.MockDiskMapping{ FilesystemLabelToPartUUID: map[string]string{ diff --git a/overlord/snapstate/autorefresh.go b/overlord/snapstate/autorefresh.go index a52fe3c26b..16c72cce7c 100644 --- a/overlord/snapstate/autorefresh.go +++ b/overlord/snapstate/autorefresh.go @@ -130,6 +130,31 @@ func (m *autoRefresh) EffectiveRefreshHold() (time.Time, error) { return holdTime, nil } +func (m *autoRefresh) ensureRefreshHoldAtLeast(duration time.Duration) error { + now := time.Now() + + // get the effective refresh hold and check if it is sooner than the + // specified duration in the future + effective, err := m.EffectiveRefreshHold() + if err != nil { + return err + } + + if effective.IsZero() || effective.Sub(now) < duration { + // the effective refresh hold is sooner than the desired delay, so + // move it out to the specified duration + holdTime := now.Add(duration) + tr := config.NewTransaction(m.state) + err := tr.Set("core", "refresh.hold", &holdTime) + if err != nil && !config.IsNoOption(err) { + return err + } + tr.Commit() + } + + return nil +} + // clearRefreshHold clears refresh.hold configuration. func (m *autoRefresh) clearRefreshHold() { tr := config.NewTransaction(m.state) diff --git a/overlord/snapstate/autorefresh_test.go b/overlord/snapstate/autorefresh_test.go index f4dca3f371..78ee66b014 100644 --- a/overlord/snapstate/autorefresh_test.go +++ b/overlord/snapstate/autorefresh_test.go @@ -569,6 +569,98 @@ func (s *autoRefreshTestSuite) TestLastRefreshRefreshHoldExpiredReschedule(c *C) c.Check(nextRefresh1.Before(nextRefresh), Equals, false) } +func (s *autoRefreshTestSuite) TestEnsureRefreshHoldAtLeastZeroTimes(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // setup hold-time as time.Time{} and next-refresh as now to simulate real + // console-conf-start situations + t0 := time.Now() + + tr := config.NewTransaction(s.state) + tr.Set("core", "refresh.hold", time.Time{}) + tr.Commit() + + af := snapstate.NewAutoRefresh(s.state) + snapstate.MockNextRefresh(af, t0) + + err := af.EnsureRefreshHoldAtLeast(time.Hour) + c.Assert(err, IsNil) + + s.state.Unlock() + err = af.Ensure() + s.state.Lock() + c.Check(err, IsNil) + + // refresh did not happen + c.Check(s.store.ops, HasLen, 0) + + // hold is now more than an hour later than when the test started + tr = config.NewTransaction(s.state) + var t1 time.Time + err = tr.Get("core", "refresh.hold", &t1) + c.Assert(err, IsNil) + + // use After() == false here in case somehow the t0 + 1hr is exactly t1, + // Before() and After() are false for the same time instants + c.Assert(t0.Add(time.Hour).After(t1), Equals, false) +} + +func (s *autoRefreshTestSuite) TestEnsureRefreshHoldAtLeast(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // setup last-refresh as happening a long time ago, and refresh-hold as + // having been expired + t0 := time.Now() + s.state.Set("last-refresh", t0.Add(-12*time.Hour)) + + holdTime := t0.Add(-1 * time.Minute) + tr := config.NewTransaction(s.state) + tr.Set("core", "refresh.hold", holdTime) + + tr.Commit() + + af := snapstate.NewAutoRefresh(s.state) + snapstate.MockNextRefresh(af, holdTime.Add(-2*time.Minute)) + + err := af.EnsureRefreshHoldAtLeast(time.Hour) + c.Assert(err, IsNil) + + s.state.Unlock() + err = af.Ensure() + s.state.Lock() + c.Check(err, IsNil) + + // refresh did not happen + c.Check(s.store.ops, HasLen, 0) + + // hold is now more than an hour later than when the test started + tr = config.NewTransaction(s.state) + var t1 time.Time + err = tr.Get("core", "refresh.hold", &t1) + c.Assert(err, IsNil) + + // use After() == false here in case somehow the t0 + 1hr is exactly t1, + // Before() and After() are false for the same time instants + c.Assert(t0.Add(time.Hour).After(t1), Equals, false) + + // setting it to a shorter time will not change it + err = af.EnsureRefreshHoldAtLeast(30 * time.Minute) + c.Assert(err, IsNil) + + // time is still equal to t1 + tr = config.NewTransaction(s.state) + var t2 time.Time + err = tr.Get("core", "refresh.hold", &t2) + c.Assert(err, IsNil) + + // when traversing json through the core config transaction, there will be + // different wall/monotonic clock times, we remove this ambiguity by + // formatting as rfc3339 which will strip this negligible difference in time + c.Assert(t1.Format(time.RFC3339), Equals, t2.Format(time.RFC3339)) +} + func (s *autoRefreshTestSuite) TestEffectiveRefreshHold(c *C) { s.state.Lock() defer s.state.Unlock() diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go index e35262efbe..c61d53cb59 100644 --- a/overlord/snapstate/export_test.go +++ b/overlord/snapstate/export_test.go @@ -290,3 +290,7 @@ func MockGenericRefreshCheck(fn func(info *snap.Info, canAppRunDuringRefresh fun genericRefreshCheck = fn return func() { genericRefreshCheck = old } } + +func (m *autoRefresh) EnsureRefreshHoldAtLeast(d time.Duration) error { + return m.ensureRefreshHoldAtLeast(d) +} diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go index 7054b28ddd..90edb0a6aa 100644 --- a/overlord/snapstate/snapmgr.go +++ b/overlord/snapstate/snapmgr.go @@ -556,6 +556,29 @@ func (m *SnapManager) RefreshSchedule() (string, bool, error) { return m.autoRefresh.RefreshSchedule() } +// EnsureAutoRefreshesAreDelayed will delay refreshes for the specified amount +// of time, as well as return any active auto-refresh changes that are currently +// not ready so that the client can wait for those. +func (m *SnapManager) EnsureAutoRefreshesAreDelayed(delay time.Duration) ([]*state.Change, error) { + // always delay for at least the specified time, this ensures that even if + // there are active refreshes right now, there won't be more auto-refreshes + // that happen after the current set finish + err := m.autoRefresh.ensureRefreshHoldAtLeast(delay) + if err != nil { + return nil, err + } + + // look for auto refresh changes in progress + autoRefreshChgs := []*state.Change{} + for _, chg := range m.state.Changes() { + if chg.Kind() == "auto-refresh" && !chg.Status().Ready() { + autoRefreshChgs = append(autoRefreshChgs, chg) + } + } + + return autoRefreshChgs, nil +} + // ensureForceDevmodeDropsDevmodeFromState undoes the forced devmode // in snapstate for forced devmode distros. func (m *SnapManager) ensureForceDevmodeDropsDevmodeFromState() error { diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index 14255717a9..3ad1ec2660 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -6261,3 +6261,62 @@ func (s *snapmgrTestSuite) TestForSnapSetupResetsFlags(c *C) { RequireTypeBase: false, }) } + +func (s *snapmgrTestSuite) TestEnsureAutoRefreshesAreDelayed(c *C) { + s.state.Lock() + defer s.state.Unlock() + + t0 := time.Now() + // with no changes in flight still works and we set the auto-refresh time as + // at least one minute past the start of the test + chgs, err := s.snapmgr.EnsureAutoRefreshesAreDelayed(time.Minute) + c.Assert(err, IsNil) + c.Assert(chgs, HasLen, 0) + + var holdTime time.Time + tr := config.NewTransaction(s.state) + err = tr.Get("core", "refresh.hold", &holdTime) + c.Assert(err, IsNil) + // use After() == false in case holdTime is _exactly_ one minute later than + // t0, in which case both After() and Before() will be false + c.Assert(t0.Add(time.Minute).After(holdTime), Equals, false) + + // now make some auto-refresh changes to make sure we get those figured out + chg0 := s.state.NewChange("auto-refresh", "auto-refresh-the-things") + chg0.AddTask(s.state.NewTask("nop", "do nothing")) + + // make it in doing state + chg0.SetStatus(state.DoingStatus) + + // this one will be picked up too + chg1 := s.state.NewChange("auto-refresh", "auto-refresh-the-things") + chg1.AddTask(s.state.NewTask("nop", "do nothing")) + chg1.SetStatus(state.DoStatus) + + // this one won't, it's Done + chg2 := s.state.NewChange("auto-refresh", "auto-refresh-the-things") + chg2.AddTask(s.state.NewTask("nop", "do nothing")) + chg2.SetStatus(state.DoneStatus) + + // nor this one, it's Undone + chg3 := s.state.NewChange("auto-refresh", "auto-refresh-the-things") + chg3.AddTask(s.state.NewTask("nop", "do nothing")) + chg3.SetStatus(state.UndoneStatus) + + // now we get our change ID returned when calling EnsureAutoRefreshesAreDelayed + chgs, err = s.snapmgr.EnsureAutoRefreshesAreDelayed(time.Minute) + c.Assert(err, IsNil) + // more helpful error message if we first compare the change ID's + expids := []string{chg0.ID(), chg1.ID()} + sort.Strings(expids) + c.Assert(chgs, HasLen, len(expids)) + gotids := []string{chgs[0].ID(), chgs[1].ID()} + sort.Strings(gotids) + c.Assert(expids, DeepEquals, gotids) + + sort.SliceStable(chgs, func(i, j int) bool { + return chgs[i].ID() < chgs[j].ID() + }) + + c.Assert(chgs, DeepEquals, []*state.Change{chg0, chg1}) +} diff --git a/secboot/export_test.go b/secboot/export_test.go index ac55ee524b..3cbb99c4ba 100644 --- a/secboot/export_test.go +++ b/secboot/export_test.go @@ -38,11 +38,11 @@ func MockSbConnectToDefaultTPM(f func() (*sb.TPMConnection, error)) (restore fun } } -func MockSbProvisionTPM(f func(tpm *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error) (restore func()) { - old := sbProvisionTPM - sbProvisionTPM = f +func MockProvisionTPM(f func(tpm *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error) (restore func()) { + old := provisionTPM + provisionTPM = f return func() { - sbProvisionTPM = old + provisionTPM = old } } @@ -78,7 +78,7 @@ func MockSbAddSnapModelProfile(f func(profile *sb.PCRProtectionProfile, params * } } -func MockSbSealKeyToTPM(f func(tpm *sb.TPMConnection, key []byte, keyPath, policyUpdatePath string, params *sb.KeyCreationParams) error) (restore func()) { +func MockSbSealKeyToTPM(f func(tpm *sb.TPMConnection, key []byte, keyPath string, params *sb.KeyCreationParams) (sb.TPMPolicyAuthKey, error)) (restore func()) { old := sbSealKeyToTPM sbSealKeyToTPM = f return func() { @@ -86,7 +86,7 @@ func MockSbSealKeyToTPM(f func(tpm *sb.TPMConnection, key []byte, keyPath, polic } } -func MockSbUpdateKeyPCRProtectionPolicy(f func(tpm *sb.TPMConnection, keyPath, policyUpdatePath string, pcrProfile *sb.PCRProtectionProfile) error) (restore func()) { +func MockSbUpdateKeyPCRProtectionPolicy(f func(tpm *sb.TPMConnection, keyPath string, authKey sb.TPMPolicyAuthKey, pcrProfile *sb.PCRProtectionProfile) error) (restore func()) { old := sbUpdateKeyPCRProtectionPolicy sbUpdateKeyPCRProtectionPolicy = f return func() { @@ -94,16 +94,16 @@ func MockSbUpdateKeyPCRProtectionPolicy(f func(tpm *sb.TPMConnection, keyPath, p } } -func MockSbLockAccessToSealedKeys(f func(tpm *sb.TPMConnection) error) (restore func()) { - old := sbLockAccessToSealedKeys - sbLockAccessToSealedKeys = f +func MockSbBlockPCRProtectionPolicies(f func(tpm *sb.TPMConnection, pcrs []int) error) (restore func()) { + old := sbBlockPCRProtectionPolicies + sbBlockPCRProtectionPolicies = f return func() { - sbLockAccessToSealedKeys = old + sbBlockPCRProtectionPolicies = old } } func MockSbActivateVolumeWithRecoveryKey(f func(volumeName, sourceDevicePath string, - keyReader io.Reader, options *sb.ActivateWithRecoveryKeyOptions) error) (restore func()) { + keyReader io.Reader, options *sb.ActivateVolumeOptions) error) (restore func()) { old := sbActivateVolumeWithRecoveryKey sbActivateVolumeWithRecoveryKey = f return func() { @@ -112,7 +112,7 @@ func MockSbActivateVolumeWithRecoveryKey(f func(volumeName, sourceDevicePath str } func MockSbActivateVolumeWithTPMSealedKey(f func(tpm *sb.TPMConnection, volumeName, sourceDevicePath, keyPath string, - pinReader io.Reader, options *sb.ActivateWithTPMSealedKeyOptions) (bool, error)) (restore func()) { + pinReader io.Reader, options *sb.ActivateVolumeOptions) (bool, error)) (restore func()) { old := sbActivateVolumeWithTPMSealedKey sbActivateVolumeWithTPMSealedKey = f return func() { diff --git a/secboot/secboot.go b/secboot/secboot.go index e36edcbd03..2ced4046ba 100644 --- a/secboot/secboot.go +++ b/secboot/secboot.go @@ -60,8 +60,8 @@ type SealKeyParams struct { ModelParams []*SealKeyModelParams // The path to store the sealed key file KeyFile string - // The path to the authorization policy update data file (only relevant for TPM) - TPMPolicyUpdateDataFile string + // The path to the authorization policy update key file (only relevant for TPM) + TPMPolicyAuthKeyFile string // The path to the lockout authorization file (only relevant for TPM) TPMLockoutAuthFile string } @@ -71,6 +71,6 @@ type ResealKeyParams struct { ModelParams []*SealKeyModelParams // The path to the sealed key file KeyFile string - // The path to the authorization policy update data file (only relevant for TPM) - TPMPolicyUpdateDataFile string + // The path to the authorization policy update key file (only relevant for TPM) + TPMPolicyAuthKeyFile string } diff --git a/secboot/secboot_tpm.go b/secboot/secboot_tpm.go index 2fbe768884..60124a2804 100644 --- a/secboot/secboot_tpm.go +++ b/secboot/secboot_tpm.go @@ -24,6 +24,7 @@ import ( "crypto/rand" "errors" "fmt" + "io/ioutil" "os" "path/filepath" @@ -43,27 +44,29 @@ import ( const ( // Handles are in the block reserved for owner objects (0x01800000 - 0x01bfffff) - pinHandle = 0x01880000 + policyCounterHandle = 0x01880001 + + keyringPrefix = "snapd" ) var ( sbConnectToDefaultTPM = sb.ConnectToDefaultTPM sbMeasureSnapSystemEpochToTPM = sb.MeasureSnapSystemEpochToTPM sbMeasureSnapModelToTPM = sb.MeasureSnapModelToTPM - sbLockAccessToSealedKeys = sb.LockAccessToSealedKeys + sbBlockPCRProtectionPolicies = sb.BlockPCRProtectionPolicies sbActivateVolumeWithTPMSealedKey = sb.ActivateVolumeWithTPMSealedKey sbActivateVolumeWithRecoveryKey = sb.ActivateVolumeWithRecoveryKey sbAddEFISecureBootPolicyProfile = sb.AddEFISecureBootPolicyProfile sbAddEFIBootManagerProfile = sb.AddEFIBootManagerProfile sbAddSystemdEFIStubProfile = sb.AddSystemdEFIStubProfile sbAddSnapModelProfile = sb.AddSnapModelProfile - sbProvisionTPM = sb.ProvisionTPM sbSealKeyToTPM = sb.SealKeyToTPM sbUpdateKeyPCRProtectionPolicy = sb.UpdateKeyPCRProtectionPolicy randutilRandomKernelUUID = randutil.RandomKernelUUID isTPMEnabled = isTPMEnabledImpl + provisionTPM = provisionTPMImpl ) func isTPMEnabledImpl(tpm *sb.TPMConnection) bool { @@ -116,7 +119,9 @@ func checkSecureBootEnabled() error { return nil } -const tpmPCR = 12 +// initramfsPCR is the TPM PCR that we reserve for the EFI image and use +// for measurement from the initramfs. +const initramfsPCR = 12 func secureConnectToTPM(ekcfile string) (*sb.TPMConnection, error) { ekCertReader, err := os.Open(ekcfile) @@ -154,7 +159,7 @@ func measureWhenPossible(whatHow func(tpm *sb.TPMConnection) error) error { // TPM device is available. If there's no TPM device success is returned. func MeasureSnapSystemEpochWhenPossible() error { measure := func(tpm *sb.TPMConnection) error { - return sbMeasureSnapSystemEpochToTPM(tpm, tpmPCR) + return sbMeasureSnapSystemEpochToTPM(tpm, initramfsPCR) } if err := measureWhenPossible(measure); err != nil { @@ -172,7 +177,7 @@ func MeasureSnapModelWhenPossible(findModel func() (*asserts.Model, error)) erro if err != nil { return err } - return sbMeasureSnapModelToTPM(tpm, tpmPCR, model) + return sbMeasureSnapModelToTPM(tpm, initramfsPCR, model) } if err := measureWhenPossible(measure); err != nil { @@ -219,7 +224,11 @@ func UnlockVolumeIfEncrypted(disk disks.Disk, name string, encryptionKeyDir stri // volumes to unlock we should lock access to the sealed keys only after // the last encrypted volume is unlocked, in which case lockKeysOnFinish // should be set to true. - lockErr = sbLockAccessToSealedKeys(tpm) + // + // We should only touch the PCR that we've currently reserved for the kernel + // EFI image. Touching others will break the ability to perform any kind of + // attestation using the TPM because it will make the log inconsistent. + lockErr = sbBlockPCRProtectionPolicies(tpm, []int{initramfsPCR}) } }() @@ -246,7 +255,7 @@ func UnlockVolumeIfEncrypted(disk disks.Disk, name string, encryptionKeyDir stri } sealedKeyPath := filepath.Join(encryptionKeyDir, name+".sealed-key") - return unlockEncryptedPartitionWithSealedKey(tpm, mapperName, encdev, sealedKeyPath, "", lockKeysOnFinish), true + return unlockEncryptedPartitionWithSealedKey(tpm, mapperName, encdev, sealedKeyPath, ""), true }() if err != nil { return "", false, err @@ -272,8 +281,9 @@ func UnlockVolumeIfEncrypted(disk disks.Disk, name string, encryptionKeyDir stri // unlockEncryptedPartitionWithRecoveryKey prompts for the recovery key and use // it to open an encrypted device. func unlockEncryptedPartitionWithRecoveryKey(name, device string) error { - options := sb.ActivateWithRecoveryKeyOptions{ - Tries: 3, + options := sb.ActivateVolumeOptions{ + RecoveryKeyTries: 3, + KeyringPrefix: keyringPrefix, } if err := sbActivateVolumeWithRecoveryKey(name, device, nil, &options); err != nil { @@ -286,11 +296,11 @@ func unlockEncryptedPartitionWithRecoveryKey(name, device string) error { // unlockEncryptedPartitionWithSealedKey unseals the keyfile and opens an encrypted // device. If activation with the sealed key fails, this function will attempt to // activate it with the fallback recovery key instead. -func unlockEncryptedPartitionWithSealedKey(tpm *sb.TPMConnection, name, device, keyfile, pinfile string, lock bool) error { - options := sb.ActivateWithTPMSealedKeyOptions{ - PINTries: 1, - RecoveryKeyTries: 3, - LockSealedKeyAccess: lock, +func unlockEncryptedPartitionWithSealedKey(tpm *sb.TPMConnection, name, device, keyfile, pinfile string) error { + options := sb.ActivateVolumeOptions{ + PassphraseTries: 1, + RecoveryKeyTries: 3, + KeyringPrefix: keyringPrefix, } // XXX: pinfile is currently not used @@ -338,10 +348,19 @@ func SealKey(key EncryptionKey, params *SealKeyParams) error { // Seal key to the TPM creationParams := sb.KeyCreationParams{ - PCRProfile: pcrProfile, - PINHandle: pinHandle, + PCRProfile: pcrProfile, + PCRPolicyCounterHandle: policyCounterHandle, } - return sbSealKeyToTPM(tpm, key[:], params.KeyFile, params.TPMPolicyUpdateDataFile, &creationParams) + + authKey, err := sbSealKeyToTPM(tpm, key[:], params.KeyFile, &creationParams) + if err != nil { + return err + } + if err := osutil.AtomicWriteFile(params.TPMPolicyAuthKeyFile, authKey, 0600, 0); err != nil { + return fmt.Errorf("cannot write the policy auth key file: %v", err) + } + + return nil } // ResealKey updates the PCR protection policy for the sealed encryption key according to @@ -366,7 +385,12 @@ func ResealKey(params *ResealKeyParams) error { return err } - return sbUpdateKeyPCRProtectionPolicy(tpm, params.KeyFile, params.TPMPolicyUpdateDataFile, pcrProfile) + authKey, err := ioutil.ReadFile(params.TPMPolicyAuthKeyFile) + if err != nil { + return fmt.Errorf("cannot read the policy auth key file: %v", err) + } + + return sbUpdateKeyPCRProtectionPolicy(tpm, params.KeyFile, authKey, pcrProfile) } func buildPCRProtectionProfile(modelParams []*SealKeyModelParams) (*sb.PCRProtectionProfile, error) { @@ -408,7 +432,7 @@ func buildPCRProtectionProfile(modelParams []*SealKeyModelParams) (*sb.PCRProtec if len(mp.KernelCmdlines) != 0 { systemdStubParams := sb.SystemdEFIStubProfileParams{ PCRAlgorithm: tpm2.HashAlgorithmSHA256, - PCRIndex: tpmPCR, + PCRIndex: initramfsPCR, KernelCmdlines: mp.KernelCmdlines, } if err := sbAddSystemdEFIStubProfile(modelProfile, &systemdStubParams); err != nil { @@ -420,7 +444,7 @@ func buildPCRProtectionProfile(modelParams []*SealKeyModelParams) (*sb.PCRProtec if mp.Model != nil { snapModelParams := sb.SnapModelProfileParams{ PCRAlgorithm: tpm2.HashAlgorithmSHA256, - PCRIndex: tpmPCR, + PCRIndex: initramfsPCR, Models: []sb.SnapModel{mp.Model}, } if err := sbAddSnapModelProfile(modelProfile, &snapModelParams); err != nil { @@ -458,13 +482,17 @@ func tpmProvision(tpm *sb.TPMConnection, lockoutAuthFile string) error { // TODO:UC20: ideally we should ask the firmware to clear the TPM and then reboot // if the device has previously been provisioned, see // https://godoc.org/github.com/snapcore/secboot#RequestTPMClearUsingPPI - if err := sbProvisionTPM(tpm, sb.ProvisionModeFull, lockoutAuth); err != nil { + if err := provisionTPM(tpm, sb.ProvisionModeFull, lockoutAuth); err != nil { logger.Noticef("TPM provisioning error: %v", err) return fmt.Errorf("cannot provision TPM: %v", err) } return nil } +func provisionTPMImpl(tpm *sb.TPMConnection, mode sb.ProvisionMode, lockoutAuth []byte) error { + return tpm.EnsureProvisioned(mode, lockoutAuth) +} + // buildLoadSequences builds EFI load image event trees from this package LoadChains func buildLoadSequences(chains []*LoadChain) (loadseqs []*sb.EFIImageLoadEvent, err error) { // this will build load event trees for the current diff --git a/secboot/secboot_tpm_test.go b/secboot/secboot_tpm_test.go index b3d6542b12..26263c8a13 100644 --- a/secboot/secboot_tpm_test.go +++ b/secboot/secboot_tpm_test.go @@ -37,6 +37,7 @@ import ( "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/efi" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/secboot" "github.com/snapcore/snapd/snap" @@ -280,9 +281,9 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) { device: "name", disk: mockDiskWithEncDev, }, { - // activation works but lock fails (lock requested) + // activation works but PCR policy block fails (lock requested) tpmEnabled: true, hasEncdev: true, lockRequest: true, activated: true, - err: "cannot lock access to sealed keys: lock failed", + err: "cannot lock access to sealed keys: block failed", device: "name", disk: mockDiskWithEncDev, }, { @@ -308,7 +309,7 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) { }, { // activation works but lock fails, without encrypted device (lock requested) tpmEnabled: true, lockRequest: true, activated: true, - err: "cannot lock access to sealed keys: lock failed", + err: "cannot lock access to sealed keys: block failed", disk: mockDiskWithUnencDev, }, { // happy case without encrypted device @@ -393,14 +394,15 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) { }) defer restore() - n := 0 - restore = secboot.MockSbLockAccessToSealedKeys(func(tpm *sb.TPMConnection) error { - n++ + sbBlockPCRProtectionPolicesCalls := 0 + restore = secboot.MockSbBlockPCRProtectionPolicies(func(tpm *sb.TPMConnection, pcrs []int) error { + sbBlockPCRProtectionPolicesCalls++ c.Assert(tpm, Equals, mockSbTPM) + c.Assert(pcrs, DeepEquals, []int{12}) if tc.lockOk { return nil } - return errors.New("lock failed") + return errors.New("block failed") }) defer restore() @@ -412,14 +414,14 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) { devicePath := filepath.Join("/dev/disk/by-partuuid", partuuid) restore = secboot.MockSbActivateVolumeWithTPMSealedKey(func(tpm *sb.TPMConnection, volumeName, sourceDevicePath, - keyPath string, pinReader io.Reader, options *sb.ActivateWithTPMSealedKeyOptions) (bool, error) { + keyPath string, pinReader io.Reader, options *sb.ActivateVolumeOptions) (bool, error) { c.Assert(volumeName, Equals, "name-"+randomUUID) c.Assert(sourceDevicePath, Equals, devicePath) c.Assert(keyPath, Equals, filepath.Join("encrypt-key-dir", "name.sealed-key")) - c.Assert(*options, DeepEquals, sb.ActivateWithTPMSealedKeyOptions{ - PINTries: 1, - RecoveryKeyTries: 3, - LockSealedKeyAccess: tc.lockRequest, + c.Assert(*options, DeepEquals, sb.ActivateVolumeOptions{ + PassphraseTries: 1, + RecoveryKeyTries: 3, + KeyringPrefix: "snapd", }) if !tc.activated { return false, errors.New("activation error") @@ -429,7 +431,7 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) { defer restore() restore = secboot.MockSbActivateVolumeWithRecoveryKey(func(name, device string, keyReader io.Reader, - options *sb.ActivateWithRecoveryKeyOptions) error { + options *sb.ActivateVolumeOptions) error { return tc.rkErr }) defer restore() @@ -446,14 +448,14 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) { } else { c.Assert(err, ErrorMatches, tc.err) } - // LockAccessToSealedKeys should be called whenever there is a TPM device + // BlockPCRProtectionPolicies should be called whenever there is a TPM device // detected, regardless of whether secure boot is enabled or there is an // encrypted volume to unlock. If we have multiple encrypted volumes, we // should call it after the last one is unlocked. if tc.tpmErr == nil && tc.lockRequest { - c.Assert(n, Equals, 1) + c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 1) } else { - c.Assert(n, Equals, 0) + c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 0) } } } @@ -593,9 +595,9 @@ func (s *secbootSuite) TestSealKey(c *C) { Model: &asserts.Model{}, }, }, - KeyFile: "keyfile", - TPMPolicyUpdateDataFile: "policy-update-data-file", - TPMLockoutAuthFile: filepath.Join(tmpDir, "lockout-auth-file"), + KeyFile: "keyfile", + TPMPolicyAuthKeyFile: filepath.Join(tmpDir, "policy-auth-key-file"), + TPMLockoutAuthFile: filepath.Join(tmpDir, "lockout-auth-file"), } myKey := secboot.EncryptionKey{} @@ -752,7 +754,7 @@ func (s *secbootSuite) TestSealKey(c *C) { // mock provisioning provisioningCalls := 0 - restore = secboot.MockSbProvisionTPM(func(t *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error { + restore = secboot.MockProvisionTPM(func(t *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error { provisioningCalls++ c.Assert(t, Equals, tpm) c.Assert(mode, Equals, sb.ProvisionModeFull) @@ -763,14 +765,13 @@ func (s *secbootSuite) TestSealKey(c *C) { // mock sealing sealCalls := 0 - restore = secboot.MockSbSealKeyToTPM(func(t *sb.TPMConnection, key []byte, keyPath, policyUpdatePath string, params *sb.KeyCreationParams) error { + restore = secboot.MockSbSealKeyToTPM(func(t *sb.TPMConnection, key []byte, keyPath string, params *sb.KeyCreationParams) (sb.TPMPolicyAuthKey, error) { sealCalls++ c.Assert(t, Equals, tpm) c.Assert(key, DeepEquals, myKey[:]) c.Assert(keyPath, Equals, myParams.KeyFile) - c.Assert(policyUpdatePath, Equals, myParams.TPMPolicyUpdateDataFile) - c.Assert(params.PINHandle, Equals, tpm2.Handle(0x01880000)) - return tc.sealErr + c.Assert(params.PCRPolicyCounterHandle, Equals, tpm2.Handle(0x01880001)) + return sb.TPMPolicyAuthKey{}, tc.sealErr }) defer restore() @@ -786,6 +787,7 @@ func (s *secbootSuite) TestSealKey(c *C) { c.Assert(addEFISbPolicyCalls, Equals, 2) c.Assert(addSystemdEfiStubCalls, Equals, 2) c.Assert(addSnapModelCalls, Equals, 2) + c.Assert(osutil.FileExists(myParams.TPMPolicyAuthKeyFile), Equals, true) } else { c.Assert(err, ErrorMatches, tc.expectedErr) } @@ -821,6 +823,11 @@ func (s *secbootSuite) TestResealKey(c *C) { {tpmEnabled: true, resealErr: mockErr, resealCalls: 1, expectedErr: "some error"}, {tpmEnabled: true, resealCalls: 1, expectedErr: ""}, } { + mockTPMPolicyAuthKey := []byte{1, 3, 3, 7} + mockTPMPolicyAuthKeyFile := filepath.Join(c.MkDir(), "policy-auth-key-file") + err := ioutil.WriteFile(mockTPMPolicyAuthKeyFile, mockTPMPolicyAuthKey, 0600) + c.Assert(err, IsNil) + mockEFI := bootloader.NewBootFile("", filepath.Join(c.MkDir(), "file.efi"), bootloader.RoleRecovery) if !tc.missingFile { err := ioutil.WriteFile(mockEFI.Path, nil, 0644) @@ -835,8 +842,8 @@ func (s *secbootSuite) TestResealKey(c *C) { Model: &asserts.Model{}, }, }, - KeyFile: "keyfile", - TPMPolicyUpdateDataFile: "policy-update-data-file", + KeyFile: "keyfile", + TPMPolicyAuthKeyFile: mockTPMPolicyAuthKeyFile, } sequences := []*sb.EFIImageLoadEvent{ @@ -905,17 +912,17 @@ func (s *secbootSuite) TestResealKey(c *C) { // mock PCR protection policy update resealCalls := 0 - restore = secboot.MockSbUpdateKeyPCRProtectionPolicy(func(t *sb.TPMConnection, keyPath, polUpdatePath string, profile *sb.PCRProtectionProfile) error { + restore = secboot.MockSbUpdateKeyPCRProtectionPolicy(func(t *sb.TPMConnection, keyPath string, authKey sb.TPMPolicyAuthKey, profile *sb.PCRProtectionProfile) error { resealCalls++ c.Assert(t, Equals, tpm) c.Assert(keyPath, Equals, myParams.KeyFile) - c.Assert(polUpdatePath, Equals, myParams.TPMPolicyUpdateDataFile) + c.Assert(authKey, DeepEquals, sb.TPMPolicyAuthKey(mockTPMPolicyAuthKey)) c.Assert(profile, Equals, pcrProfile) return tc.resealErr }) defer restore() - err := secboot.ResealKey(myParams) + err = secboot.ResealKey(myParams) if tc.expectedErr == "" { c.Assert(err, IsNil) c.Assert(addEFISbPolicyCalls, Equals, 1) @@ -931,9 +938,9 @@ func (s *secbootSuite) TestResealKey(c *C) { func (s *secbootSuite) TestSealKeyNoModelParams(c *C) { myKey := secboot.EncryptionKey{} myParams := secboot.SealKeyParams{ - KeyFile: "keyfile", - TPMPolicyUpdateDataFile: "policy-update-data-file", - TPMLockoutAuthFile: "lockout-auth-file", + KeyFile: "keyfile", + TPMPolicyAuthKeyFile: "policy-auth-key-file", + TPMLockoutAuthFile: "lockout-auth-file", } err := secboot.SealKey(myKey, &myParams) diff --git a/tests/nested/core20/gadget-reseal/task.yaml b/tests/nested/core20/gadget-reseal/task.yaml index 727a25f1a3..41259d960c 100644 --- a/tests/nested/core20/gadget-reseal/task.yaml +++ b/tests/nested/core20/gadget-reseal/task.yaml @@ -46,6 +46,9 @@ execute: | nested_wait_for_reboot "${boot_id}" nested_exec sudo snap watch "${REMOTE_CHG_ID}" + # sanity check that the gadget asset was changed + nested_exec sudo grep -q -a "This program cannot be run in XXX mode" /run/mnt/ubuntu-boot/EFI/boot/grubx64.efi + # the gadget has changed, we should see resealing SEALED_KEY_MTIME_3="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-seed/device/fde/ubuntu-data.sealed-key)" test "$SEALED_KEY_MTIME_3" -gt "$SEALED_KEY_MTIME_2" diff --git a/tests/nested/core20/kernel-reseal/task.yaml b/tests/nested/core20/kernel-reseal/task.yaml index fbc722cefd..883a7cbee7 100644 --- a/tests/nested/core20/kernel-reseal/task.yaml +++ b/tests/nested/core20/kernel-reseal/task.yaml @@ -6,12 +6,17 @@ prepare: | # shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - snap download pc-kernel --channel=20/edge --basename=pc-kernel - unsquashfs -d pc-kernel pc-kernel.snap + # we cannot use the kernel from store as it may have a version of + # snap-bootstrap that will not be able to unseal the keys and unlock the + # encrypted volumes, instead use the kernel we repacked when building the UC20 + # image + KERNEL_SNAP="$(ls "$NESTED_ASSETS_DIR"/pc-kernel_*.snap)" + unsquashfs -d pc-kernel "$KERNEL_SNAP" # ensure we really have the header we expect grep -q -a "This program cannot be run in DOS mode" pc-kernel/kernel.efi # modify the kernel so that the hash changes - sed -i 's/This program cannot be run in DOS mode/This program cannot be run in D0S mode/' pc-kernel/kernel.efi + sed -i 's/This program cannot be run in DOS mode/This program cannot be run in XXX mode/' pc-kernel/kernel.efi + grep -q -a "This program cannot be run in XXX mode" pc-kernel/kernel.efi KEY_NAME=$(nested_get_snakeoil_key) SNAKEOIL_KEY="$PWD/$KEY_NAME.key" @@ -36,6 +41,9 @@ execute: | nested_wait_for_reboot "${boot_id}" nested_exec sudo snap watch "${REMOTE_CHG_ID}" + # sanity check that we are using the right kernel + nested_exec sudo grep -q -a "This program cannot be run in XXX mode" /boot/grub/kernel.efi + # ensure ubuntu-data.sealed-key mtime is newer SEALED_KEY_MTIME_2="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-seed/device/fde/ubuntu-data.sealed-key)" test "$SEALED_KEY_MTIME_2" -gt "$SEALED_KEY_MTIME_1" diff --git a/vendor/vendor.json b/vendor/vendor.json index 5c4c82d231..4644dd521c 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -25,10 +25,10 @@ "revisionTime": "2020-08-24T11:54:14Z" }, { - "checksumSHA1": "eDjzake0GpHm9kfTH7FMUWX8zVA=", - "path": "github.com/chrisccoulson/tcglog-parser", - "revision": "7b0f085a85398d368e10382a21a44ec2226c35b3", - "revisionTime": "2020-02-28T14:36:39Z" + "checksumSHA1": "QGwWyf2f/5+FacNj2iKpjp8N5hg=", + "path": "github.com/canonical/tcglog-parser", + "revision": "12a3a7bcf5a14486e838fea211e8d4aa280e9671", + "revisionTime": "2020-09-08T16:50:21Z" }, { "checksumSHA1": "zg16zjZTQ9R89+UOLmEZxHgxDtM=", @@ -116,40 +116,40 @@ "revisionTime": "2017-09-28T14:21:59Z" }, { - "checksumSHA1": "bNQROczU0gF+BeQHApftqWNUMe8=", + "checksumSHA1": "sKSczCVVhI1zTfY4zQBPYs/34vQ=", "path": "github.com/snapcore/secboot", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb", + "revisionTime": "2020-10-15T16:53:38Z" }, { "checksumSHA1": "c7jHLQSWFWbymTcFWZMQH0C5Wik=", "path": "github.com/snapcore/secboot/internal/efi", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb", + "revisionTime": "2020-10-15T16:53:38Z" }, { "checksumSHA1": "loFEiH6evGaDnDSlQgk3ugemkcU=", "path": "github.com/snapcore/secboot/internal/pe1.14", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb", + "revisionTime": "2020-10-15T16:53:38Z" }, { "checksumSHA1": "kDay47kq9OgDplpkrYw0/a8Z+YY=", "path": "github.com/snapcore/secboot/internal/tcg", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb", + "revisionTime": "2020-10-15T16:53:38Z" }, { "checksumSHA1": "PRS8ACUu14shrvAgb747Izc25ns=", "path": "github.com/snapcore/secboot/internal/tcti", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb", + "revisionTime": "2020-10-15T16:53:38Z" }, { "checksumSHA1": "TnfofdyojXYWOwdWCKMY5RCeI7s=", "path": "github.com/snapcore/secboot/internal/truststore", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb", + "revisionTime": "2020-10-15T16:53:38Z" }, { "checksumSHA1": "3AmEm18mKj8XxBuru/ix4OOpRkE=", @@ -301,6 +301,13 @@ "path": "gopkg.in/yaml.v2", "revision": "86f5ed62f8a0ee96bd888d2efdfd6d4fb100a4eb", "revisionTime": "2018-03-26T05:07:29Z" + }, + { + "checksumSHA1": "tZ9GNzrjTZEPDhoJEkouGPKRmZk=", + "origin": "github.com/pedronis/maze.io-x-crypto/afis", + "path": "maze.io/x/crypto/afis", + "revision": "9b94c9afe06676a7da39a608a48ed4e31f5d125f", + "revisionTime": "2019-01-31T09:06:03Z" } ], "rootPath": "github.com/snapcore/snapd" |
