diff --git a/asserts/snapasserts/snapasserts.go b/asserts/snapasserts/snapasserts.go index e7a77795a00d..247ad8bd3d54 100644 --- a/asserts/snapasserts/snapasserts.go +++ b/asserts/snapasserts/snapasserts.go @@ -379,6 +379,36 @@ func FetchSnapAssertions(f asserts.Fetcher, snapSHA3_384, provenance string) err return f.Fetch(ref) } +// FetchComponentAssertions fetches the assertions matching the information +// described in the given SideInfo and ComponentSideInfo using the given +// fetcher. +func FetchComponentAssertions(f asserts.Fetcher, si *snap.SideInfo, csi *snap.ComponentSideInfo, hash, provenance string) error { + // for now starting from the snap-resource-revision will get us all other relevant assertions + ref := &asserts.Ref{ + Type: asserts.SnapResourceRevisionType, + PrimaryKey: []string{si.SnapID, csi.Component.ComponentName, hash}, + } + if provenance != "" { + ref.PrimaryKey = append(ref.PrimaryKey, provenance) + } + + if err := f.Fetch(ref); err != nil { + return err + } + + // fetch the snap-resource-pair as well + ref = &asserts.Ref{ + Type: asserts.SnapResourcePairType, + PrimaryKey: []string{si.SnapID, csi.Component.ComponentName, csi.Revision.String(), si.Revision.String()}, + } + + if provenance != "" { + ref.PrimaryKey = append(ref.PrimaryKey, provenance) + } + + return f.Fetch(ref) +} + // FetchSnapDeclaration fetches the snap declaration and its prerequisites for the given snap id using the given fetcher. func FetchSnapDeclaration(f asserts.Fetcher, snapID string) error { ref := &asserts.Ref{ diff --git a/overlord/assertstate/assertmgr.go b/overlord/assertstate/assertmgr.go index b81bb67bcc0d..7bf0a195cab5 100644 --- a/overlord/assertstate/assertmgr.go +++ b/overlord/assertstate/assertmgr.go @@ -20,6 +20,7 @@ package assertstate import ( + "errors" "fmt" "gopkg.in/tomb.v2" @@ -42,6 +43,7 @@ func Manager(s *state.State, runner *state.TaskRunner) (*AssertManager, error) { delayedCrossMgrInit() runner.AddHandler("validate-snap", doValidateSnap, nil) + runner.AddHandler("validate-component", doValidateComponent, nil) db, err := sysdb.Open() if err != nil { @@ -153,3 +155,67 @@ func doValidateSnap(t *state.Task, _ *tomb.Tomb) error { // TODO: set DeveloperID from assertions return nil } + +func doValidateComponent(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + + compsup, snapsup, err := snapstate.TaskComponentSetup(t) + if err != nil { + return fmt.Errorf("internal error: cannot obtain snap setup: %s", err) + } + + sha3_384, compSize, err := asserts.SnapFileSHA3_384(compsup.CompPath) + if err != nil { + return err + } + + deviceCtx, err := snapstate.DeviceCtx(st, t, nil) + if err != nil { + return err + } + + modelAs := deviceCtx.Model() + + // TODO: i think that the provenance of the component should be the same as + // the snap's provenance + expectedProv := snapsup.ExpectedProvenance + + err = doFetch(st, snapsup.UserID, deviceCtx, nil, func(f asserts.Fetcher) error { + if err := snapasserts.FetchComponentAssertions(f, snapsup.SideInfo, compsup.CompSideInfo, sha3_384, expectedProv); err != nil { + return err + } + + // TODO: do we want this part here? it happens in doValidateSnap + // fetch store assertion if available + if modelAs.Store() != "" { + err := snapasserts.FetchStore(f, modelAs.Store()) + if notFound, ok := err.(*asserts.NotFoundError); ok { + if notFound.Type != asserts.StoreType { + return err + } + } else if err != nil { + return err + } + } + + return nil + }) + if errors.Is(err, &asserts.NotFoundError{}) { + return fmt.Errorf("cannot find supported signatures to verify component %q and its hash (%v)", compsup.ComponentName(), err) + } + if err != nil { + return err + } + + db := DB(st) + err = snapasserts.CrossCheckResource(compsup.ComponentName(), sha3_384, expectedProv, compSize, compsup.CompSideInfo, snapsup.SideInfo, modelAs, db) + if err != nil { + return err + } + + // TODO: do we need to do something like snapasserts.CheckProvenanceWithVerifiedRevision for components? + + return nil +} diff --git a/overlord/assertstate/assertstate_test.go b/overlord/assertstate/assertstate_test.go index 095258760166..dc8173616057 100644 --- a/overlord/assertstate/assertstate_test.go +++ b/overlord/assertstate/assertstate_test.go @@ -26,6 +26,7 @@ import ( "errors" "fmt" "sort" + "strconv" "strings" "testing" "time" @@ -48,6 +49,7 @@ import ( "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/store" "github.com/snapcore/snapd/store/storetest" @@ -566,6 +568,53 @@ func (s *assertMgrSuite) prereqSnapAssertions(c *C, revisions ...int) (paths map return paths, digests } +func (s *assertMgrSuite) prereqComponentAssertions(c *C, snapRev, compRev snap.Revision) (compPath string, digest string) { + const ( + resourceName = "test-component" + snapID = "snap-id-1" + componentYaml = `component: snap+test-component +type: test +version: 1.0.2 +` + ) + + compPath = snaptest.MakeTestComponentWithFiles(c, resourceName+".comp", componentYaml, nil) + + digest, size, err := asserts.SnapFileSHA3_384(compPath) + c.Assert(err, IsNil) + + revHeaders := map[string]interface{}{ + "snap-id": snapID, + "resource-name": resourceName, + "resource-sha3-384": digest, + "resource-revision": compRev.String(), + "resource-size": strconv.Itoa(int(size)), + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + + resourceRev, err := s.storeSigning.Sign(asserts.SnapResourceRevisionType, revHeaders, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(resourceRev) + c.Assert(err, IsNil) + + pairHeaders := map[string]interface{}{ + "snap-id": snapID, + "resource-name": resourceName, + "resource-revision": compRev.String(), + "snap-revision": snapRev.String(), + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + + resourcePair, err := s.storeSigning.Sign(asserts.SnapResourcePairType, pairHeaders, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(resourcePair) + c.Assert(err, IsNil) + + return compPath, digest +} + func (s *assertMgrSuite) TestDoFetch(c *C) { _, digests := s.prereqSnapAssertions(c, 10) @@ -5216,3 +5265,66 @@ func (s *assertMgrSuite) TestAspectBundle(c *C) { c.Check(bundle.Name, Equals, "foo") c.Check(bundle.Schema, NotNil) } + +func (s *assertMgrSuite) TestValidateComponent(c *C) { + snapRev, compRev := snap.R(10), snap.R(20) + + paths, _ := s.prereqSnapAssertions(c, 10) + snapPath := paths[10] + + compPath, compDigest := s.prereqComponentAssertions(c, snapRev, compRev) + + s.state.Lock() + defer s.state.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + err := s.storeSigning.Add(storeAs) + c.Assert(err, IsNil) + + chg := s.state.NewChange("install", "...") + t := s.state.NewTask("validate-component", "Fetch and check snap assertions") + snapsup := snapstate.SnapSetup{ + SnapPath: snapPath, + UserID: 0, + SideInfo: &snap.SideInfo{ + RealName: "foo", + SnapID: "snap-id-1", + Revision: snapRev, + }, + } + compsup := snapstate.ComponentSetup{ + CompPath: compPath, + CompSideInfo: &snap.ComponentSideInfo{ + Component: naming.NewComponentRef("foo", "test-component"), + Revision: compRev, + }, + } + t.Set("snap-setup", snapsup) + t.Set("component-setup", compsup) + chg.AddTask(t) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + + db := assertstate.DB(s.state) + + a, err := db.Find(asserts.SnapResourceRevisionType, map[string]string{ + "resource-sha3-384": compDigest, + "resource-name": "test-component", + "snap-id": "snap-id-1", + }) + c.Assert(err, IsNil) + c.Check(a.(*asserts.SnapResourceRevision).ResourceRevision(), Equals, 20) + + // TODO: remove this check if we decide we don't need it + // store assertion was also fetched + _, err = db.Find(asserts.StoreType, map[string]string{ + "store": "my-brand-store", + }) + c.Assert(err, IsNil) +}