diff --git a/asserts/snapasserts/snapasserts.go b/asserts/snapasserts/snapasserts.go index 70b7eea76f8..33129190138 100644 --- a/asserts/snapasserts/snapasserts.go +++ b/asserts/snapasserts/snapasserts.go @@ -23,6 +23,7 @@ package snapasserts import ( "errors" "fmt" + "strconv" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/release" @@ -58,6 +59,25 @@ func findSnapDeclaration(snapID, name string, db Finder) (*asserts.SnapDeclarati return snapDecl, nil } +func findResourcePair(name, snapID string, resourceRev, snapRev int, provenance string, db Finder) (*asserts.SnapResourcePair, error) { + headers := map[string]string{ + "resource-name": name, + "snap-id": snapID, + "resource-revision": strconv.Itoa(resourceRev), + "snap-revision": strconv.Itoa(snapRev), + } + if provenance != "" { + headers["provenance"] = provenance + } + + a, err := db.Find(asserts.SnapResourcePairType, headers) + if err != nil { + return nil, fmt.Errorf("cannot find snap-resource-pair for %s: %w", name, err) + } + + return a.(*asserts.SnapResourcePair), nil +} + // CrossCheck tries to cross check the instance name, hash digest, provenance // and size of a snap plus its metadata in a SideInfo with the relevant // snap assertions in a database that should have been populated with @@ -113,6 +133,110 @@ func CrossCheck(instanceName, snapSHA3_384, provenance string, snapSize uint64, return snapRev, nil } +// CrossCheckResource tries to cross check the name, hash digest, size, +// provenance, and metadata of a snap resource with the relevant assertions +// (snap-resource-revision and snap-resource-pair) in a database that should be +// pre-populated with them. +func CrossCheckResource(name, hash, provenance string, size uint64, csi *snap.ComponentSideInfo, si *snap.SideInfo, model *asserts.Model, db Finder) error { + headers := map[string]string{ + "resource-sha3-384": hash, + "resource-name": name, + "snap-id": si.SnapID, + } + if provenance != "" { + headers["provenance"] = provenance + } + + a, err := db.Find(asserts.SnapResourceRevisionType, headers) + if err != nil { + provInf := "" + if provenance != "" { + provInf = fmt.Sprintf(" provenance: %s", provenance) + } + return fmt.Errorf("internal error: cannot find pre-populated snap-resource-revision assertion for %q: %s%s", name, hash, provInf) + } + + resrev := a.(*asserts.SnapResourceRevision) + + if resrev.ResourceSize() != size { + return fmt.Errorf( + "resource %q file does not have expected size according to signatures (download is broken or tampered): %d != %d", + name, size, resrev.ResourceSize(), + ) + } + + if resrev.ResourceRevision() != csi.Revision.N { + return fmt.Errorf( + "resource %q does not have expected revision according to assertions (metadata is broken or tampered): %s != %d", + name, csi.Revision, resrev.ResourceRevision(), + ) + } + + // we don't actually need to use the resource pair, since all of the values + // that we need to validate are primary keys, but we do need to check that + // it exists + _, err = findResourcePair(name, si.SnapID, csi.Revision.N, si.Revision.N, provenance, db) + if err != nil { + return err + } + + if provenance != "" { + snapDecl, err := findSnapDeclaration(si.SnapID, si.RealName, db) + if err != nil { + return err + } + + if err := crossCheckResourceProvenance(resrev, snapDecl, model, db); err != nil { + return err + } + } + + return nil +} + +// crossCheckResourceProvenance tries to cross check the given +// snap-resource-revision's provenance with the snap-declaration's revision +// authority. +func crossCheckResourceProvenance(resrev *asserts.SnapResourceRevision, snapDecl *asserts.SnapDeclaration, model *asserts.Model, db Finder) error { + // nothing to check when using the default provenance + if resrev.Provenance() == "global-upload" { + return nil + } + + store, err := maybeFindStore(model, db) + if err != nil { + return err + } + + ras := snapDecl.RevisionAuthority(resrev.Provenance()) + for _, ra := range ras { + if err := ra.CheckResourceRevision(resrev, model, store); err == nil { + return nil + } + } + + return fmt.Errorf( + "snap resource %q revision assertion with provenance %q is not signed by an authority authorized on this device: %s", + resrev.ResourceName(), resrev.Provenance(), resrev.AuthorityID(), + ) +} + +func maybeFindStore(model *asserts.Model, db Finder) (*asserts.Store, error) { + if model != nil && model.Store() != "" { + a, err := db.Find(asserts.StoreType, map[string]string{ + "store": model.Store(), + }) + if err != nil && !errors.Is(err, &asserts.NotFoundError{}) { + return nil, err + } + if a != nil { + return a.(*asserts.Store), nil + } + } + + return nil, nil +} + // CrossCheckProvenance tries to cross check the given snap-revision // if it has a non default provenance with the revision-authority // constraints of the given snap-declaration including any device @@ -125,18 +249,12 @@ func CrossCheckProvenance(instanceName string, snapRev *asserts.SnapRevision, sn // nothing to check return "", nil } - var store *asserts.Store - if model != nil && model.Store() != "" { - a, err := db.Find(asserts.StoreType, map[string]string{ - "store": model.Store(), - }) - if err != nil && !errors.Is(err, &asserts.NotFoundError{}) { - return "", err - } - if a != nil { - store = a.(*asserts.Store) - } + + store, err := maybeFindStore(model, db) + if err != nil { + return "", err } + ras := snapDecl.RevisionAuthority(snapRev.Provenance()) matchingRevAuthority := false for _, ra := range ras { @@ -261,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/asserts/snapasserts/snapasserts_test.go b/asserts/snapasserts/snapasserts_test.go index 11f243b028d..921f82a9a69 100644 --- a/asserts/snapasserts/snapasserts_test.go +++ b/asserts/snapasserts/snapasserts_test.go @@ -25,6 +25,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "testing" "time" @@ -35,6 +36,7 @@ import ( "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" ) @@ -789,3 +791,320 @@ provenance: prov`, nil) _, err = snapasserts.DeriveSideInfoFromDigestAndSize(withProv, digest, size, nil, s.localDB) c.Check(err, ErrorMatches, `safely handling snaps with different provenance but same hash not yet supported`) } + +func (s *snapassertsSuite) TestCrossCheckResource(c *C) { + digest := makeDigest(12) + componentRev := snap.R(24) + snapRev := snap.R(12) + const size = uint64(1024) + const resourceName = "test-component" + const snapID = "snap-id-1" + + revHeaders := map[string]interface{}{ + "snap-id": snapID, + "resource-name": resourceName, + "resource-sha3-384": digest, + "resource-revision": componentRev.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.localDB.Add(resourceRev) + c.Assert(err, IsNil) + + pairHeaders := map[string]interface{}{ + "snap-id": snapID, + "resource-name": resourceName, + "resource-revision": componentRev.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.localDB.Add(resourcePair) + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + csi := &snap.ComponentSideInfo{ + Component: naming.NewComponentRef("snap", "test-component"), + Revision: snap.R(24), + } + + // everything cross checks, with the regular snap name + err = snapasserts.CrossCheckResource(resourceName, digest, "", size, csi, si, nil, s.localDB) + c.Assert(err, IsNil) +} + +func (s *snapassertsSuite) TestCrossCheckResourceMissingRevisionAssertion(c *C) { + s.testCrossCheckResourceMissingRevisionAssertion(c, "") +} + +func (s *snapassertsSuite) TestCrossCheckResourceProvenanceMissingRevisionAssertion(c *C) { + s.testCrossCheckResourceMissingRevisionAssertion(c, "prov") +} + +func (s *snapassertsSuite) testCrossCheckResourceMissingRevisionAssertion(c *C, provenance string) { + digest := makeDigest(12) + const resourceName = "test-component" + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + csi := &snap.ComponentSideInfo{ + Component: naming.NewComponentRef("snap", "test-component"), + Revision: snap.R(24), + } + + err := snapasserts.CrossCheckResource(resourceName, digest, provenance, uint64(1024), csi, si, nil, s.localDB) + c.Assert(err, NotNil) + + var suffix string + if provenance != "" { + suffix = fmt.Sprintf(" provenance: %s", provenance) + } + c.Assert(err, ErrorMatches, fmt.Sprintf("internal error: cannot find pre-populated snap-resource-revision assertion for %q: %s%s", resourceName, digest, suffix)) +} + +func (s *snapassertsSuite) TestCrossCheckResourceProvenance(c *C) { + snapRev := snap.R(12) + const ( + snapID = "snap-id-1" + provenance = "prov" + ) + + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": snapID, + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "1", + "revision-authority": []interface{}{ + map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{ + provenance, + }, + }, + }, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + digest := makeDigest(12) + componentRev := snap.R(24) + const ( + size = uint64(1024) + resourceName = "test-component" + ) + + revHeaders := map[string]interface{}{ + "authority-id": s.dev1Acct.AccountID(), + "snap-id": snapID, + "resource-name": resourceName, + "resource-sha3-384": digest, + "resource-revision": componentRev.String(), + "resource-size": strconv.Itoa(int(size)), + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + "provenance": provenance, + } + + resourceRev, err := s.dev1Signing.Sign(asserts.SnapResourceRevisionType, revHeaders, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(resourceRev) + c.Assert(err, IsNil) + + pairHeaders := map[string]interface{}{ + "authority-id": s.dev1Acct.AccountID(), + "snap-id": snapID, + "resource-name": resourceName, + "resource-revision": componentRev.String(), + "snap-revision": snapRev.String(), + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + "provenance": provenance, + } + + resourcePair, err := s.dev1Signing.Sign(asserts.SnapResourcePairType, pairHeaders, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(resourcePair) + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + csi := &snap.ComponentSideInfo{ + Component: naming.NewComponentRef("snap", "test-component"), + Revision: snap.R(24), + } + + err = snapasserts.CrossCheckResource(resourceName, digest, provenance, size, csi, si, nil, s.localDB) + c.Assert(err, IsNil) + + // update the snap-decl with a mismatch provenance and check that the cross + // check fails + snapDecl, err = s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": snapID, + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "2", + "revision-authority": []interface{}{ + map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{ + "new-prov", + }, + }, + }, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + err = snapasserts.CrossCheckResource(resourceName, digest, provenance, size, csi, si, nil, s.localDB) + c.Assert(err, ErrorMatches, `snap resource \"test-component\" revision assertion with provenance \"prov\" is not signed by an authority authorized on this device:.*`) +} + +func (s *snapassertsSuite) TestCrossCheckResourceErrors(c *C) { + digest := makeDigest(12) + componentRev := snap.R(24) + snapRev := snap.R(12) + const ( + size = uint64(1024) + resourceName = "test-component" + snapID = "snap-id-1" + ) + + originalRevHeaders := map[string]interface{}{ + "snap-id": snapID, + "resource-name": resourceName, + "resource-sha3-384": digest, + "resource-revision": componentRev.String(), + "resource-size": strconv.Itoa(int(size)), + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + + originalPairHeaders := map[string]interface{}{ + "snap-id": snapID, + "resource-name": resourceName, + "resource-revision": componentRev.String(), + "snap-revision": snapRev.String(), + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + + type test struct { + revisionOverrides map[string]interface{} + pairOverrides map[string]interface{} + err string + } + + tests := []test{ + { + revisionOverrides: map[string]interface{}{ + "resource-size": "1023", + }, + err: `resource \"test-component\" file does not have expected size according to signatures \(download is broken or tampered\): 1024 != 1023`, + }, + { + revisionOverrides: map[string]interface{}{ + "resource-revision": "25", + }, + err: `resource \"test-component\" does not have expected revision according to assertions \(metadata is broken or tampered\): 24 != 25`, + }, + { + pairOverrides: map[string]interface{}{ + "resource-revision": "25", + }, + err: `cannot find snap-resource-pair for test-component: snap-resource-pair assertion not found`, + }, + { + revisionOverrides: map[string]interface{}{ + "resource-name": "nope", + }, + err: `internal error: cannot find pre-populated snap-resource-revision assertion for \"test-component\": .*`, + }, + } + + for _, t := range tests { + localDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: s.storeSigning.Trusted, + }) + c.Assert(err, IsNil) + + err = localDB.Add(s.storeSigning.StoreAccountKey("")) + c.Assert(err, IsNil) + err = localDB.Add(s.dev1Acct) + c.Assert(err, IsNil) + + revHeaders := copyMapWithOverrides(originalRevHeaders, t.revisionOverrides) + pairHeaders := copyMapWithOverrides(originalPairHeaders, t.pairOverrides) + + err = localDB.Add(snapDecl) + c.Assert(err, IsNil) + + resourceRev, err := s.storeSigning.Sign(asserts.SnapResourceRevisionType, revHeaders, nil, "") + c.Assert(err, IsNil) + err = localDB.Add(resourceRev) + c.Assert(err, IsNil) + + resourcePair, err := s.storeSigning.Sign(asserts.SnapResourcePairType, pairHeaders, nil, "") + c.Assert(err, IsNil) + err = localDB.Add(resourcePair) + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + csi := &snap.ComponentSideInfo{ + Component: naming.NewComponentRef("snap", "test-component"), + Revision: snap.R(24), + } + + // everything cross checks, with the regular snap name + err = snapasserts.CrossCheckResource(resourceName, digest, "", size, csi, si, nil, localDB) + c.Assert(err, ErrorMatches, t.err) + } +} + +func copyMapWithOverrides(m map[string]interface{}, overrides map[string]interface{}) map[string]interface{} { + c := make(map[string]interface{}, len(m)) + for k, v := range m { + c[k] = v + } + for k, v := range overrides { + c[k] = v + } + return c +} diff --git a/overlord/assertstate/assertmgr.go b/overlord/assertstate/assertmgr.go index b81bb67bcc0..7a079ab304f 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,68 @@ 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() + + // the provenance of the snap is the expected provenance for the component + 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: check the provenance stored inside the component blob against what + // we expect from the assertion (similar to + // snapasserts.CheckProvenanceWithVerifiedRevision) + + return nil +} diff --git a/overlord/assertstate/assertstate_test.go b/overlord/assertstate/assertstate_test.go index 09525876016..ed67ef7618a 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" @@ -526,7 +528,7 @@ version: %d return snaptest.MakeTestSnapWithFiles(c, yaml, nil) } -func (s *assertMgrSuite) prereqSnapAssertions(c *C, revisions ...int) (paths map[int]string, digests map[int]string) { +func (s *assertMgrSuite) prereqSnapAssertions(c *C, provenance string, revisions ...int) (paths map[int]string, digests map[int]string) { headers := map[string]interface{}{ "series": "16", "snap-id": "snap-id-1", @@ -534,6 +536,17 @@ func (s *assertMgrSuite) prereqSnapAssertions(c *C, revisions ...int) (paths map "publisher-id": s.dev1Acct.AccountID(), "timestamp": time.Now().Format(time.RFC3339), } + if provenance != "" { + headers["revision-authority"] = []interface{}{ + map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{ + provenance, + }, + }, + } + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") c.Assert(err, IsNil) err = s.storeSigning.Add(snapDecl) @@ -557,7 +570,13 @@ func (s *assertMgrSuite) prereqSnapAssertions(c *C, revisions ...int) (paths map "developer-id": s.dev1Acct.AccountID(), "timestamp": time.Now().Format(time.RFC3339), } - snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + signer := assertstest.SignerDB(s.storeSigning) + if provenance != "" { + headers["provenance"] = provenance + signer = s.dev1Signing + } + + snapRev, err := signer.Sign(asserts.SnapRevisionType, headers, nil, "") c.Assert(err, IsNil) err = s.storeSigning.Add(snapRev) c.Assert(err, IsNil) @@ -566,8 +585,64 @@ func (s *assertMgrSuite) prereqSnapAssertions(c *C, revisions ...int) (paths map return paths, digests } +func (s *assertMgrSuite) prereqComponentAssertions(c *C, provenance string, 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), + } + + signer := assertstest.SignerDB(s.storeSigning) + if provenance != "" { + revHeaders["provenance"] = provenance + signer = s.dev1Signing + } + + resourceRev, err := signer.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), + } + if provenance != "" { + pairHeaders["provenance"] = provenance + } + + resourcePair, err := signer.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) + _, digests := s.prereqSnapAssertions(c, "", 10) s.state.Lock() defer s.state.Unlock() @@ -588,7 +663,7 @@ func (s *assertMgrSuite) TestDoFetch(c *C) { } func (s *assertMgrSuite) TestFetchIdempotent(c *C) { - _, digests := s.prereqSnapAssertions(c, 10, 11) + _, digests := s.prereqSnapAssertions(c, "", 10, 11) s.state.Lock() defer s.state.Unlock() @@ -744,7 +819,7 @@ func (s *assertMgrSuite) setupModelAndStore(c *C) *asserts.Store { } func (s *assertMgrSuite) TestValidateSnap(c *C) { - paths, digests := s.prereqSnapAssertions(c, 10) + paths, digests := s.prereqSnapAssertions(c, "", 10) snapPath := paths[10] s.state.Lock() @@ -791,7 +866,7 @@ func (s *assertMgrSuite) TestValidateSnap(c *C) { } func (s *assertMgrSuite) TestValidateSnapStoreNotFound(c *C) { - paths, digests := s.prereqSnapAssertions(c, 10) + paths, digests := s.prereqSnapAssertions(c, "", 10) snapPath := paths[10] @@ -883,7 +958,7 @@ func (s *assertMgrSuite) TestValidateSnapNotFound(c *C) { } func (s *assertMgrSuite) TestValidateSnapCrossCheckFail(c *C) { - paths, _ := s.prereqSnapAssertions(c, 10) + paths, _ := s.prereqSnapAssertions(c, "", 10) snapPath := paths[10] @@ -5216,3 +5291,82 @@ func (s *assertMgrSuite) TestAspectBundle(c *C) { c.Check(bundle.Name, Equals, "foo") c.Check(bundle.Schema, NotNil) } + +func (s *assertMgrSuite) TestValidateComponent(c *C) { + const provenance = "" + s.testValidateComponent(c, provenance) +} + +func (s *assertMgrSuite) TestValidateComponentProvenance(c *C) { + const provenance = "provenance" + s.testValidateComponent(c, provenance) +} + +func (s *assertMgrSuite) testValidateComponent(c *C, provenance string) { + snapRev, compRev := snap.R(10), snap.R(20) + + paths, _ := s.prereqSnapAssertions(c, provenance, 10) + snapPath := paths[10] + + compPath, compDigest := s.prereqComponentAssertions(c, provenance, 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, + ExpectedProvenance: provenance, + 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) + + headers := map[string]string{ + "resource-sha3-384": compDigest, + "resource-name": "test-component", + "snap-id": "snap-id-1", + } + if provenance != "" { + headers["provenance"] = provenance + } + + a, err := db.Find(asserts.SnapResourceRevisionType, headers) + 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) +}