Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

seed: ReadSystemEssentialAndBetterEarliestTime #10005

Merged
merged 3 commits into from
Mar 18, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions asserts/batch.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2016-2019 Canonical Ltd
* Copyright (C) 2016-2021 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
Expand Down Expand Up @@ -117,7 +117,7 @@ func (b *Batch) Fetch(trustedDB RODatabase, retrieve func(*Ref) (Assertion, erro

func (b *Batch) precheck(db *Database) error {
db = db.WithStackedBackstore(NewMemoryBackstore())
return b.commitTo(db)
return b.commitTo(db, nil)
}

type CommitOptions struct {
Expand All @@ -139,12 +139,31 @@ func (b *Batch) CommitTo(db *Database, opts *CommitOptions) error {
}
}

return b.commitTo(db)
return b.commitTo(db, nil)
}

// CommitToAndObserve adds the batch of assertions to the given
// assertion database while invoking observe for each one after they
// are added.
// Nothing will be committed if there are missing prerequisites, for a
// full consistency check beforehand there is the Precheck option.
// For convenience observe can be nil in which case is ignored.
func (b *Batch) CommitToAndObserve(db *Database, observe func(Assertion), opts *CommitOptions) error {
if opts == nil {
opts = &CommitOptions{}
}
if opts.Precheck {
if err := b.precheck(db); err != nil {
return err
}
}

return b.commitTo(db, observe)
}

// commitTo does a best effort of adding all the batch assertions to
// the target database.
func (b *Batch) commitTo(db *Database) error {
func (b *Batch) commitTo(db *Database, observe func(Assertion)) error {
if err := b.prereqSort(db); err != nil {
return err
}
Expand All @@ -164,6 +183,8 @@ func (b *Batch) commitTo(db *Database) error {
}
if err != nil {
errs = append(errs, err)
} else if observe != nil {
observe(a)
}
}
if len(errs) != 0 {
Expand Down
44 changes: 43 additions & 1 deletion asserts/batch_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2016-2019 Canonical Ltd
* Copyright (C) 2016-2021 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
Expand Down Expand Up @@ -103,6 +103,48 @@ func (s *batchSuite) TestAddStream(c *C) {
c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
}

func (s *batchSuite) TestCommitToAndObserve(c *C) {
b := &bytes.Buffer{}
enc := asserts.NewEncoder(b)
// wrong order is ok
err := enc.Encode(s.dev1Acct)
c.Assert(err, IsNil)
enc.Encode(s.storeSigning.StoreAccountKey(""))
c.Assert(err, IsNil)

batch := asserts.NewBatch(nil)
refs, err := batch.AddStream(b)
c.Assert(err, IsNil)
c.Check(refs, DeepEquals, []*asserts.Ref{
{Type: asserts.AccountType, PrimaryKey: []string{s.dev1Acct.AccountID()}},
{Type: asserts.AccountKeyType, PrimaryKey: []string{s.storeSigning.StoreAccountKey("").PublicKeyID()}},
})

// noop
err = batch.Add(s.storeSigning.StoreAccountKey(""))
c.Assert(err, IsNil)

var seen []*asserts.Ref
obs := func(verified asserts.Assertion) {
seen = append(seen, verified.Ref())
}
err = batch.CommitToAndObserve(s.db, obs, nil)
c.Assert(err, IsNil)

devAcct, err := s.db.Find(asserts.AccountType, map[string]string{
"account-id": s.dev1Acct.AccountID(),
})
c.Assert(err, IsNil)
c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")

// this is the order they needed to be added
c.Check(seen, DeepEquals, []*asserts.Ref{

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

{Type: asserts.AccountKeyType, PrimaryKey: []string{s.storeSigning.StoreAccountKey("").PublicKeyID()}},
{Type: asserts.AccountType, PrimaryKey: []string{s.dev1Acct.AccountID()}},
})
}

func (s *batchSuite) TestAddEmptyStream(c *C) {
b := &bytes.Buffer{}

Expand Down
6 changes: 3 additions & 3 deletions seed/helpers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2016-2020 Canonical Ltd
* Copyright (C) 2016-2021 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
Expand Down Expand Up @@ -41,7 +41,7 @@ func MockTrusted(mockTrusted []asserts.Assertion) (restore func()) {
}
}

func newMemAssertionsDB() (db asserts.RODatabase, commitTo func(*asserts.Batch) error, err error) {
func newMemAssertionsDB(commitObserve func(verified asserts.Assertion)) (db *asserts.Database, commitTo func(*asserts.Batch) error, err error) {
memDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
Backstore: asserts.NewMemoryBackstore(),
Trusted: trusted,
Expand All @@ -51,7 +51,7 @@ func newMemAssertionsDB() (db asserts.RODatabase, commitTo func(*asserts.Batch)
}

commitTo = func(b *asserts.Batch) error {
return b.CommitTo(memDB, nil)
return b.CommitToAndObserve(memDB, commitObserve, nil)
}

return memDB, commitTo, nil
Expand Down
62 changes: 61 additions & 1 deletion seed/seed.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2019-2020 Canonical Ltd
* Copyright (C) 2019-2021 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
Expand All @@ -24,6 +24,7 @@ import (
"errors"
"fmt"
"path/filepath"
"time"

"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/seed/internal"
Expand Down Expand Up @@ -153,3 +154,62 @@ func ReadSystemEssential(seedDir, label string, essentialTypes []snap.Type, tm t

return seed20.Model(), seed20.EssentialSnaps(), nil
}

// ReadSystemEssentialAndBetterEarliestTime retrieves in one go
// information about the model and essential snaps of the given types
// for the Core 20 recovery system seed specified by seedDir and label
// (which cannot be empty).
// It can operate even if current system time is unreliable by taking
// a earliestTime lower bound for current time.
// It returns as well an improved lower bound by considering
// appropriate assertions in the seed.
func ReadSystemEssentialAndBetterEarliestTime(seedDir, label string, essentialTypes []snap.Type, earliestTime time.Time, tm timings.Measurer) (*asserts.Model, []*Snap, time.Time, error) {
if label == "" {
return nil, nil, time.Time{}, fmt.Errorf("system label cannot be empty")
}
seed20, err := Open(seedDir, label)
if err != nil {
return nil, nil, time.Time{}, err

}

improve := func(a asserts.Assertion) {
// we consider only snap-revisions as they are stored-signed
// and more recent than snap-declarations anyway,
// other assertions are ignored as they might be added
// with unreliable times
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// we consider only snap-revisions as they are stored-signed
// and more recent than snap-declarations anyway,
// other assertions are ignored as they might be added
// with unreliable times
// we consider only snap-revision and snap-declaration
// assertions here as they must be store-signed, see
// checkConsistency for each type
// other assertions are ignored as they might have a root
// of trust that does not include the brand key that signed
// the model assertion; thereby allowing an attacker with an
// account-key that was signed by canonical to inject new
// assertions that are not rooted in trust with the store directly
// or with the brand key directly

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used a slightly different explanation in the end

if a.Type() == asserts.SnapRevisionType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as mentioned on IRC, we could also use the snap-declaration timestamp here, since the checkConsistency method also verified that the snap-declarations are store-signed too, the case is unlikely where the snap-declaration is newer than the snap-revision, but it could happen and I think it's okay to be slightly more accepting of assertion types here

sr := a.(*asserts.SnapRevision)
if sr.Timestamp().After(earliestTime) {
earliestTime = sr.Timestamp()
}
}
}

// create a temporary database, commitTo will invoke improve
db, commitTo, err := newMemAssertionsDB(improve)
if err != nil {
return nil, nil, time.Time{}, err
}
// set up the database to check for key expiry only assuming
// earliestTime (if not zero)
db.SetEarliestTime(earliestTime)

// load assertions into the temporary database
if err := seed20.LoadAssertions(db, commitTo); err != nil {
return nil, nil, time.Time{}, err
}

// load and verify info about essential snaps
if err := seed20.LoadEssentialMeta(essentialTypes, tm); err != nil {
return nil, nil, time.Time{}, err
}

// consider model own timestamp as well
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// consider model own timestamp as well
// consider the model's timestamp as well - it must be signed
// by the brand so is safe from the attack detailed above

mod := seed20.Model()
if mod.Timestamp().After(earliestTime) {
earliestTime = mod.Timestamp()
}

return mod, seed20.EssentialSnaps(), earliestTime, nil
}
2 changes: 1 addition & 1 deletion seed/seed16.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (s *seed16) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Ba
if db == nil {
// a db was not provided, create an internal temporary one
var err error
db, commitTo, err = newMemAssertionsDB()
db, commitTo, err = newMemAssertionsDB(nil)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion seed/seed20.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (s *seed20) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Ba
if db == nil {
// a db was not provided, create an internal temporary one
var err error
db, commitTo, err = newMemAssertionsDB()
db, commitTo, err = newMemAssertionsDB(nil)
if err != nil {
return err
}
Expand Down
125 changes: 125 additions & 0 deletions seed/seed20_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,131 @@ func (s *seed20Suite) TestLoadEssentialMetaCore20(c *C) {
}
}

func (s *seed20Suite) TestReadSystemEssentialAndBetterEarliestTime(c *C) {
r := seed.MockTrusted(s.StoreSigning.Trusted)
defer r()

s.makeSnap(c, "snapd", "")
s.makeSnap(c, "core20", "")
s.makeSnap(c, "pc-kernel=20", "")
s.makeSnap(c, "pc=20", "")
s.makeSnap(c, "core18", "")
t0 := time.Now().UTC().Truncate(time.Second)
s.SetSnapAssertionNow(t0.Add(2 * time.Second))
s.makeSnap(c, "required18", "developerid")
s.SetSnapAssertionNow(time.Time{})

snapdSnap := &seed.Snap{
Path: s.expectedPath("snapd"),
SideInfo: &s.AssertedSnapInfo("snapd").SideInfo,
EssentialType: snap.TypeSnapd,
Essential: true,
Required: true,
Channel: "latest/stable",
}
pcKernelSnap := &seed.Snap{
Path: s.expectedPath("pc-kernel"),
SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo,
EssentialType: snap.TypeKernel,
Essential: true,
Required: true,
Channel: "20",
}
core20Snap := &seed.Snap{Path: s.expectedPath("core20"),
SideInfo: &s.AssertedSnapInfo("core20").SideInfo,
EssentialType: snap.TypeBase,
Essential: true,
Required: true,
Channel: "latest/stable",
}
pcSnap := &seed.Snap{
Path: s.expectedPath("pc"),
SideInfo: &s.AssertedSnapInfo("pc").SideInfo,
EssentialType: snap.TypeGadget,
Essential: true,
Required: true,
Channel: "20",
}

tests := []struct {
onlyTypes []snap.Type
expected []*seed.Snap
}{
{[]snap.Type{snap.TypeSnapd}, []*seed.Snap{snapdSnap}},
{[]snap.Type{snap.TypeKernel}, []*seed.Snap{pcKernelSnap}},
{[]snap.Type{snap.TypeBase}, []*seed.Snap{core20Snap}},
{[]snap.Type{snap.TypeGadget}, []*seed.Snap{pcSnap}},
{[]snap.Type{snap.TypeSnapd, snap.TypeKernel, snap.TypeBase}, []*seed.Snap{snapdSnap, pcKernelSnap, core20Snap}},
// the order in essentialTypes is not relevant
{[]snap.Type{snap.TypeGadget, snap.TypeKernel}, []*seed.Snap{pcKernelSnap, pcSnap}},
// degenerate case
{[]snap.Type{}, []*seed.Snap(nil)},
}

baseLabel := "20210315"

testReadSystemEssentialAndBetterEarliestTime := func(sysLabel string, earliestTime, modelTime, improvedTime time.Time) {
s.MakeSeed(c, sysLabel, "my-brand", "my-model", map[string]interface{}{
"display-name": "my model",
"timestamp": modelTime.Format(time.RFC3339),
"architecture": "amd64",
"base": "core20",
"snaps": []interface{}{
map[string]interface{}{
"name": "pc-kernel",
"id": s.AssertedSnapID("pc-kernel"),
"type": "kernel",
"default-channel": "20",
},
map[string]interface{}{
"name": "pc",
"id": s.AssertedSnapID("pc"),
"type": "gadget",
"default-channel": "20",
},
map[string]interface{}{
"name": "core18",
"id": s.AssertedSnapID("core18"),
"type": "base",
},
map[string]interface{}{
"name": "required18",
"id": s.AssertedSnapID("required18"),
}},
}, nil)

for _, t := range tests {
// test short-cut helper as well
mod, essSnaps, betterTime, err := seed.ReadSystemEssentialAndBetterEarliestTime(s.SeedDir, sysLabel, t.onlyTypes, earliestTime, s.perfTimings)
c.Assert(err, IsNil)
c.Check(mod.BrandID(), Equals, "my-brand")
c.Check(mod.Model(), Equals, "my-model")
c.Check(mod.Timestamp().Equal(modelTime), Equals, true)
c.Check(essSnaps, HasLen, len(t.expected))
c.Check(essSnaps, DeepEquals, t.expected)
c.Check(betterTime.Equal(improvedTime), Equals, true, Commentf("%v expected: %v", betterTime, improvedTime))
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

}

revsTime := s.AssertedSnapRevision("required18").Timestamp()
t2 := revsTime.Add(1 * time.Second)

timeCombos := []struct {
earliestTime, modelTime, improvedTime time.Time
}{
{time.Time{}, t0, revsTime},
{t2.AddDate(-1, 0, 0), t0, revsTime},
{t2.AddDate(-1, 0, 0), t2, t2},
{t2.AddDate(0, 1, 0), t2, t2.AddDate(0, 1, 0)},
}

for i, c := range timeCombos {
label := fmt.Sprintf("%s%d", baseLabel, i)
testReadSystemEssentialAndBetterEarliestTime(label, c.earliestTime, c.modelTime, c.improvedTime)
}
}

func (s *seed20Suite) TestLoadEssentialAndMetaCore20(c *C) {
r := seed.MockTrusted(s.StoreSigning.Trusted)
defer r()
Expand Down
Loading