From 938e6b61d0ad0032794486ba0b72bf5d20c6ab23 Mon Sep 17 00:00:00 2001 From: Samuele Pedroni Date: Fri, 30 Sep 2016 23:26:36 +0200 Subject: [PATCH] asserts,interfaces/builtin,overlord/assertstate: introduce base-declaration (#2037) This introduces a base-declaration assertion with the default policies (for now about interfaces) that govern all snaps. This has the assertion definition itself, the first pass at plumbing to have the content defined in interfaces/builtin, while the forward compatible (with the idea to revision/distribute this through the store) interface to it is asserstate.BaseDeclaration. The assertion is not used yet in any way. --- asserts/asserts.go | 2 + asserts/asserts_test.go | 1 + asserts/assertstest/assertstest.go | 21 ++ asserts/snap_asserts.go | 141 ++++++++++++ asserts/snap_asserts_test.go | 240 +++++++++++++++++++++ interfaces/builtin/basedeclaration.go | 49 +++++ interfaces/builtin/basedeclaration_test.go | 36 ++++ overlord/assertstate/assertmgr.go | 11 + overlord/assertstate/assertmgr_test.go | 26 +++ 9 files changed, 527 insertions(+) create mode 100644 interfaces/builtin/basedeclaration.go create mode 100644 interfaces/builtin/basedeclaration_test.go diff --git a/asserts/asserts.go b/asserts/asserts.go index 04e6594850c..26cb2932e35 100644 --- a/asserts/asserts.go +++ b/asserts/asserts.go @@ -55,6 +55,7 @@ var ( AccountKeyType = &AssertionType{"account-key", []string{"public-key-sha3-384"}, assembleAccountKey, 0} ModelType = &AssertionType{"model", []string{"series", "brand-id", "model"}, assembleModel, 0} SerialType = &AssertionType{"serial", []string{"brand-id", "model", "serial"}, assembleSerial, 0} + BaseDeclarationType = &AssertionType{"base-declaration", []string{"series"}, assembleBaseDeclaration, 0} SnapDeclarationType = &AssertionType{"snap-declaration", []string{"series", "snap-id"}, assembleSnapDeclaration, 0} SnapBuildType = &AssertionType{"snap-build", []string{"snap-sha3-384"}, assembleSnapBuild, 0} SnapRevisionType = &AssertionType{"snap-revision", []string{"snap-sha3-384"}, assembleSnapRevision, 0} @@ -77,6 +78,7 @@ var typeRegistry = map[string]*AssertionType{ AccountKeyType.Name: AccountKeyType, ModelType.Name: ModelType, SerialType.Name: SerialType, + BaseDeclarationType.Name: BaseDeclarationType, SnapDeclarationType.Name: SnapDeclarationType, SnapBuildType.Name: SnapBuildType, SnapRevisionType.Name: SnapRevisionType, diff --git a/asserts/asserts_test.go b/asserts/asserts_test.go index 7504d114e2c..2de9235d93a 100644 --- a/asserts/asserts_test.go +++ b/asserts/asserts_test.go @@ -701,6 +701,7 @@ func (as *assertsSuite) TestWithAuthority(c *C) { withAuthority := []string{ "account", "account-key", + "base-declaration", "snap-declaration", "snap-build", "snap-revision", diff --git a/asserts/assertstest/assertstest.go b/asserts/assertstest/assertstest.go index 45d605200cf..3ce89e4a97c 100644 --- a/asserts/assertstest/assertstest.go +++ b/asserts/assertstest/assertstest.go @@ -332,3 +332,24 @@ func (ss *StoreStack) StoreAccountKey(keyID string) *asserts.AccountKey { } return key.(*asserts.AccountKey) } + +// MockBuiltinBaseDeclaration mocks the builtin base-declaration exposed by asserts.BuiltinBaseDeclaration. +func MockBuiltinBaseDeclaration(headers []byte) (restore func()) { + var prevHeaders []byte + decl := asserts.BuiltinBaseDeclaration() + if decl != nil { + prevHeaders, _ = decl.Signature() + } + + err := asserts.InitBuiltinBaseDeclaration(headers) + if err != nil { + panic(err) + } + + return func() { + err := asserts.InitBuiltinBaseDeclaration(prevHeaders) + if err != nil { + panic(err) + } + } +} diff --git a/asserts/snap_asserts.go b/asserts/snap_asserts.go index 04a6e7767cb..1241e5e897c 100644 --- a/asserts/snap_asserts.go +++ b/asserts/snap_asserts.go @@ -20,6 +20,7 @@ package asserts import ( + "bytes" "crypto" "fmt" "time" @@ -485,3 +486,143 @@ func assembleValidation(assert assertionBase) (Assertion, error) { approvedSnapRevision: approvedSnapRevision, }, nil } + +// BaseDeclaration holds a base-declaration assertion, declaring the +// policies (to start with interface ones) applying to all snaps of +// a series. +type BaseDeclaration struct { + assertionBase + plugRules map[string]*PlugRule + slotRules map[string]*SlotRule + timestamp time.Time +} + +// Series returns the series whose snaps are governed by the declaration. +func (basedcl *BaseDeclaration) Series() string { + return basedcl.HeaderString("series") +} + +// Timestamp returns the time when the base-declaration was issued. +func (basedcl *BaseDeclaration) Timestamp() time.Time { + return basedcl.timestamp +} + +// PlugRule returns the plug-side rule about the given interface if one was included in the plugs stanza of the declaration, otherwise it returns nil. +func (basedcl *BaseDeclaration) PlugRule(interfaceName string) *PlugRule { + return basedcl.plugRules[interfaceName] +} + +// SlotRule returns the slot-side rule about the given interface if one was included in the slots stanza of the declaration, otherwise it returns nil. +func (basedcl *BaseDeclaration) SlotRule(interfaceName string) *SlotRule { + return basedcl.slotRules[interfaceName] +} + +// Implement further consistency checks. +func (basedcl *BaseDeclaration) checkConsistency(db RODatabase, acck *AccountKey) error { + // XXX: not signed or stored yet in a db, but being ready for that + if !db.IsTrustedAccount(basedcl.AuthorityID()) { + return fmt.Errorf("base-declaration assertion for series %s is not signed by a directly trusted authority: %s", basedcl.Series(), basedcl.AuthorityID()) + } + return nil +} + +// sanity +var _ consistencyChecker = (*BaseDeclaration)(nil) + +func assembleBaseDeclaration(assert assertionBase) (Assertion, error) { + var plugRules map[string]*PlugRule + plugs, err := checkMap(assert.headers, "plugs") + if err != nil { + return nil, err + } + if plugs != nil { + plugRules = make(map[string]*PlugRule, len(plugs)) + for iface, rule := range plugs { + plugRule, err := compilePlugRule(iface, rule) + if err != nil { + return nil, err + } + plugRules[iface] = plugRule + } + } + + var slotRules map[string]*SlotRule + slots, err := checkMap(assert.headers, "slots") + if err != nil { + return nil, err + } + if slots != nil { + slotRules = make(map[string]*SlotRule, len(slots)) + for iface, rule := range slots { + slotRule, err := compileSlotRule(iface, rule) + if err != nil { + return nil, err + } + slotRules[iface] = slotRule + } + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &BaseDeclaration{ + assertionBase: assert, + plugRules: plugRules, + slotRules: slotRules, + timestamp: timestamp, + }, nil +} + +var builtinBaseDeclaration *BaseDeclaration + +// BuiltinBaseDeclaration exposes the initialized builtin base-declaration assertion. This is used by overlord/assertstate, other code should use assertstate.BaseDeclaration. +func BuiltinBaseDeclaration() *BaseDeclaration { + return builtinBaseDeclaration +} + +var ( + builtinBaseDeclarationCheckOrder = []string{"type", "authority-id", "series"} + builtinBaseDeclarationExpectedHeaders = map[string]interface{}{ + "type": "base-declaration", + "authority-id": "canonical", + "series": release.Series, + } +) + +// InitBuiltinBaseDeclaration initializes the builtin base-declaration based on headers (or resets it if headers is nil). +func InitBuiltinBaseDeclaration(headers []byte) error { + if headers == nil { + builtinBaseDeclaration = nil + return nil + } + trimmed := bytes.TrimSpace(headers) + h, err := parseHeaders(trimmed) + if err != nil { + return err + } + for _, name := range builtinBaseDeclarationCheckOrder { + expected := builtinBaseDeclarationExpectedHeaders[name] + if h[name] != expected { + return fmt.Errorf("the builtin base-declaration %q header is not set to expected value %q", name, expected) + } + } + revision, err := checkRevision(h) + if err != nil { + return fmt.Errorf("cannot assemble the builtin-base declaration: %v", err) + } + h["timestamp"] = time.Now().UTC().Format(time.RFC3339) + a, err := assembleBaseDeclaration(assertionBase{ + headers: h, + body: nil, + revision: revision, + content: trimmed, + signature: []byte("$builtin"), + }) + if err != nil { + return fmt.Errorf("cannot assemble the builtin base-declaration: %v", err) + } + builtinBaseDeclaration = a.(*BaseDeclaration) + return nil +} diff --git a/asserts/snap_asserts_test.go b/asserts/snap_asserts_test.go index 89c5c969ba4..ab88f831c6f 100644 --- a/asserts/snap_asserts_test.go +++ b/asserts/snap_asserts_test.go @@ -39,6 +39,7 @@ var ( _ = Suite(&snapBuildSuite{}) _ = Suite(&snapRevSuite{}) _ = Suite(&validationSuite{}) + _ = Suite(&baseDeclSuite{}) ) type snapDeclSuite struct { @@ -981,5 +982,244 @@ func (vs *validationSuite) TestPrerequisites(c *C) { Type: asserts.SnapDeclarationType, PrimaryKey: []string{"16", "snap-id-2"}, }) +} + +type baseDeclSuite struct{} +func (s *baseDeclSuite) TestDecodeOK(c *C) { + encoded := `type: base-declaration +authority-id: canonical +series: 16 +plugs: + interface1: + deny-installation: false + allow-auto-connection: + slot-snap-type: + - app + slot-publisher-id: + - acme + slot-attributes: + a1: /foo/.* + plug-attributes: + b1: B1 + deny-auto-connection: + slot-attributes: + a1: !A1 + plug-attributes: + b1: !B1 + interface2: + allow-installation: true + allow-connection: + plug-attributes: + a2: A2 + slot-attributes: + b2: B2 + deny-connection: + slot-snap-id: + - snapidsnapidsnapidsnapidsnapid01 + - snapidsnapidsnapidsnapidsnapid02 + plug-attributes: + a2: !A2 + slot-attributes: + b2: !B2 +slots: + interface3: + deny-installation: false + allow-auto-connection: + plug-snap-type: + - app + plug-publisher-id: + - acme + slot-attributes: + c1: /foo/.* + plug-attributes: + d1: C1 + deny-auto-connection: + slot-attributes: + c1: !C1 + plug-attributes: + d1: !D1 + interface4: + allow-connection: + plug-attributes: + c2: C2 + slot-attributes: + d2: D2 + deny-connection: + plug-snap-id: + - snapidsnapidsnapidsnapidsnapid01 + - snapidsnapidsnapidsnapidsnapid02 + plug-attributes: + c2: !D2 + slot-attributes: + d2: !D2 + allow-installation: + slot-snap-type: + - app + slot-attributes: + e1: E1 +timestamp: 2016-09-29T19:50:49Z +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +$builtin` + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + baseDecl := a.(*asserts.BaseDeclaration) + c.Check(baseDecl.Series(), Equals, "16") + ts, err := time.Parse(time.RFC3339, "2016-09-29T19:50:49Z") + c.Check(baseDecl.Timestamp().Equal(ts), Equals, true) + + c.Check(baseDecl.PlugRule("interfaceX"), IsNil) + c.Check(baseDecl.SlotRule("interfaceX"), IsNil) + + plugRule1 := baseDecl.PlugRule("interface1") + c.Assert(plugRule1, NotNil) + c.Check(plugRule1.DenyInstallation.PlugAttributes, Equals, asserts.NeverMatchAttributes) + c.Check(plugRule1.AllowAutoConnection.SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection.PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.AllowAutoConnection.SlotSnapTypes, DeepEquals, []string{"app"}) + c.Check(plugRule1.AllowAutoConnection.SlotPublisherIDs, DeepEquals, []string{"acme"}) + c.Check(plugRule1.DenyAutoConnection.SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection.PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`) + plugRule2 := baseDecl.PlugRule("interface2") + c.Assert(plugRule2, NotNil) + c.Check(plugRule2.AllowInstallation.PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Check(plugRule2.AllowConnection.PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection.SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection.PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection.SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection.SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + + slotRule3 := baseDecl.SlotRule("interface3") + c.Assert(slotRule3, NotNil) + c.Check(slotRule3.DenyInstallation.SlotAttributes, Equals, asserts.NeverMatchAttributes) + c.Check(slotRule3.AllowAutoConnection.SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection.PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection.PlugSnapTypes, DeepEquals, []string{"app"}) + c.Check(slotRule3.AllowAutoConnection.PlugPublisherIDs, DeepEquals, []string{"acme"}) + c.Check(slotRule3.DenyAutoConnection.SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection.PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`) + slotRule4 := baseDecl.SlotRule("interface4") + c.Assert(slotRule4, NotNil) + c.Check(slotRule4.AllowConnection.PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection.SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection.PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection.SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection.PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + c.Check(slotRule4.AllowInstallation.SlotAttributes.Check(nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation.SlotSnapTypes, DeepEquals, []string{"app"}) + +} + +func (s *baseDeclSuite) TestBaseDeclarationCheckUntrustedAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := map[string]interface{}{ + "series": "16", + "timestamp": time.Now().Format(time.RFC3339), + } + baseDecl, err := otherDB.Sign(asserts.BaseDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(baseDecl) + c.Assert(err, ErrorMatches, `base-declaration assertion for series 16 is not signed by a directly trusted authority: other`) +} + +const ( + baseDeclErrPrefix = "assertion base-declaration: " +) + +func (s *baseDeclSuite) TestDecodeInvalid(c *C) { + tsLine := "timestamp: 2016-09-29T19:50:49Z\n" + + encoded := "type: base-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "plugs:\n interface1: true\n" + + "slots:\n interface2: true\n" + + tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "$builtin" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series: 16\n", "", `"series" header is mandatory`}, + {"series: 16\n", "series: \n", `"series" header should not be empty`}, + {"plugs:\n interface1: true\n", "plugs: \n", `"plugs" header must be a map`}, + {"plugs:\n interface1: true\n", "plugs:\n intf1:\n foo: bar\n", `plug rule for interface "intf1" must specify at least one of.*`}, + {"slots:\n interface2: true\n", "slots: \n", `"slots" header must be a map`}, + {"slots:\n interface2: true\n", "slots:\n intf1:\n foo: bar\n", `slot rule for interface "intf1" must specify at least one of.*`}, + {tsLine, "", `"timestamp" header is mandatory`}, + {tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, baseDeclErrPrefix+test.expectedErr) + } + +} + +func (s *baseDeclSuite) TestBuiltin(c *C) { + baseDecl := asserts.BuiltinBaseDeclaration() + c.Check(baseDecl, IsNil) + + defer asserts.InitBuiltinBaseDeclaration(nil) + + const headers = ` +type: base-declaration +authority-id: canonical +series: 16 +revision: 0 +plugs: + network: true +slots: + network: + allow-installation: + slot-snap-type: + - core +` + + err := asserts.InitBuiltinBaseDeclaration([]byte(headers)) + c.Assert(err, IsNil) + + baseDecl = asserts.BuiltinBaseDeclaration() + c.Assert(baseDecl, NotNil) + + cont, _ := baseDecl.Signature() + c.Check(string(cont), Equals, strings.TrimSpace(headers)) + + c.Check(baseDecl.AuthorityID(), Equals, "canonical") + c.Check(baseDecl.Series(), Equals, "16") + c.Check(baseDecl.PlugRule("network").AllowAutoConnection.SlotAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Check(baseDecl.SlotRule("network").AllowInstallation.SlotSnapTypes, DeepEquals, []string{"core"}) + + enc := asserts.Encode(baseDecl) + // it's expected that it cannot be decoded + _, err = asserts.Decode(enc) + c.Check(err, NotNil) +} + +func (s *baseDeclSuite) TestBuiltinInitErrors(c *C) { + defer asserts.InitBuiltinBaseDeclaration(nil) + + tests := []struct { + headers string + err string + }{ + {"", `header entry missing ':' separator: ""`}, + {"type: foo\n", `the builtin base-declaration "type" header is not set to expected value "base-declaration"`}, + {"type: base-declaration", `the builtin base-declaration "authority-id" header is not set to expected value "canonical"`}, + {"type: base-declaration\nauthority-id: canonical", `the builtin base-declaration "series" header is not set to expected value "16"`}, + {"type: base-declaration\nauthority-id: canonical\nseries: 16\nrevision: zzz", `cannot assemble the builtin-base declaration: "revision" header is not an integer: zzz`}, + {"type: base-declaration\nauthority-id: canonical\nseries: 16\nplugs: foo", `cannot assemble the builtin base-declaration: "plugs" header must be a map`}, + } + + for _, t := range tests { + err := asserts.InitBuiltinBaseDeclaration([]byte(t.headers)) + c.Check(err, ErrorMatches, t.err, Commentf(t.headers)) + } } diff --git a/interfaces/builtin/basedeclaration.go b/interfaces/builtin/basedeclaration.go new file mode 100644 index 00000000000..c5bd8902522 --- /dev/null +++ b/interfaces/builtin/basedeclaration.go @@ -0,0 +1,49 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" +) + +// the headers of the builtin base-declaration describing the default +// interface policies for all snaps. +const baseDeclarationHeaders = ` +type: base-declaration +authority-id: canonical +series: 16 +revision: 0 +plugs: + network: true +slots: + network: + allow-installation: + slot-snap-type: + - core +` + +func init() { + err := asserts.InitBuiltinBaseDeclaration([]byte(baseDeclarationHeaders)) + if err != nil { + panic(fmt.Sprintf("cannot initialize the builtin base-declaration: %v", err)) + } +} diff --git a/interfaces/builtin/basedeclaration_test.go b/interfaces/builtin/basedeclaration_test.go new file mode 100644 index 00000000000..9f85310df15 --- /dev/null +++ b/interfaces/builtin/basedeclaration_test.go @@ -0,0 +1,36 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + "github.com/snapcore/snapd/asserts" + + . "gopkg.in/check.v1" +) + +type baseDeclSuite struct{} + +var _ = Suite(&baseDeclSuite{}) + +func (s *baseDeclSuite) TestSanity(c *C) { + baseDecl := asserts.BuiltinBaseDeclaration() + c.Check(baseDecl.PlugRule("network"), NotNil) + c.Check(baseDecl.SlotRule("network"), NotNil) +} diff --git a/overlord/assertstate/assertmgr.go b/overlord/assertstate/assertmgr.go index d49a5e1586a..38c03c10e1f 100644 --- a/overlord/assertstate/assertmgr.go +++ b/overlord/assertstate/assertmgr.go @@ -470,3 +470,14 @@ func init() { // hook validation of refreshes into snapstate logic snapstate.ValidateRefreshes = ValidateRefreshes } + +// BaseDeclaration returns the base-declaration assertion with policies governing all snaps. +func BaseDeclaration(s *state.State) (*asserts.BaseDeclaration, error) { + // TODO: switch keeping this in the DB and have it revisioned/updated + // via the store + baseDecl := asserts.BuiltinBaseDeclaration() + if baseDecl == nil { + return nil, asserts.ErrNotFound + } + return baseDecl, nil +} diff --git a/overlord/assertstate/assertmgr_test.go b/overlord/assertstate/assertmgr_test.go index dfc48b523e1..ffe8f71cc18 100644 --- a/overlord/assertstate/assertmgr_test.go +++ b/overlord/assertstate/assertmgr_test.go @@ -768,3 +768,29 @@ func (s *assertMgrSuite) TestValidateRefreshesRevokedValidation(c *C) { c.Assert(err, ErrorMatches, `(?s).*cannot refresh "foo" to revision 9: validation by "baz" \(id "baz-id"\) revoked.*`) c.Check(validated, HasLen, 0) } + +func (s *assertMgrSuite) TestBaseSnapDeclaration(c *C) { + s.state.Lock() + defer s.state.Unlock() + + r1 := assertstest.MockBuiltinBaseDeclaration(nil) + defer r1() + + baseDecl, err := assertstate.BaseDeclaration(s.state) + c.Assert(err, Equals, asserts.ErrNotFound) + c.Check(baseDecl, IsNil) + + r2 := assertstest.MockBuiltinBaseDeclaration([]byte(` +type: base-declaration +authority-id: canonical +series: 16 +plugs: + iface: true +`)) + defer r2() + + baseDecl, err = assertstate.BaseDeclaration(s.state) + c.Assert(err, IsNil) + c.Check(baseDecl, NotNil) + c.Check(baseDecl.PlugRule("iface"), NotNil) +}