github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/asserts/model_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package asserts_test
    21  
    22  import (
    23  	"fmt"
    24  	"strings"
    25  	"time"
    26  
    27  	. "gopkg.in/check.v1"
    28  
    29  	"github.com/snapcore/snapd/asserts"
    30  	"github.com/snapcore/snapd/snap/naming"
    31  )
    32  
    33  type modelSuite struct {
    34  	ts     time.Time
    35  	tsLine string
    36  }
    37  
    38  var (
    39  	_ = Suite(&modelSuite{})
    40  )
    41  
    42  func (mods *modelSuite) SetUpSuite(c *C) {
    43  	mods.ts = time.Now().Truncate(time.Second).UTC()
    44  	mods.tsLine = "timestamp: " + mods.ts.Format(time.RFC3339) + "\n"
    45  }
    46  
    47  const (
    48  	reqSnaps     = "required-snaps:\n  - foo\n  - bar\n"
    49  	sysUserAuths = "system-user-authority: *\n"
    50  	serialAuths  = "serial-authority:\n  - generic\n"
    51  )
    52  
    53  const (
    54  	modelExample = "type: model\n" +
    55  		"authority-id: brand-id1\n" +
    56  		"series: 16\n" +
    57  		"brand-id: brand-id1\n" +
    58  		"model: baz-3000\n" +
    59  		"display-name: Baz 3000\n" +
    60  		"architecture: amd64\n" +
    61  		"gadget: brand-gadget\n" +
    62  		"base: core18\n" +
    63  		"kernel: baz-linux\n" +
    64  		"store: brand-store\n" +
    65  		serialAuths +
    66  		sysUserAuths +
    67  		reqSnaps +
    68  		"TSLINE" +
    69  		"body-length: 0\n" +
    70  		"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
    71  		"\n\n" +
    72  		"AXNpZw=="
    73  
    74  	classicModelExample = "type: model\n" +
    75  		"authority-id: brand-id1\n" +
    76  		"series: 16\n" +
    77  		"brand-id: brand-id1\n" +
    78  		"model: baz-3000\n" +
    79  		"display-name: Baz 3000\n" +
    80  		"classic: true\n" +
    81  		"architecture: amd64\n" +
    82  		"gadget: brand-gadget\n" +
    83  		"store: brand-store\n" +
    84  		reqSnaps +
    85  		"TSLINE" +
    86  		"body-length: 0\n" +
    87  		"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
    88  		"\n\n" +
    89  		"AXNpZw=="
    90  
    91  	core20ModelExample = `type: model
    92  authority-id: brand-id1
    93  series: 16
    94  brand-id: brand-id1
    95  model: baz-3000
    96  display-name: Baz 3000
    97  architecture: amd64
    98  system-user-authority: *
    99  base: core20
   100  store: brand-store
   101  snaps:
   102    -
   103      name: baz-linux
   104      id: bazlinuxidididididididididididid
   105      type: kernel
   106      default-channel: 20
   107    -
   108      name: brand-gadget
   109      id: brandgadgetdidididididididididid
   110      type: gadget
   111    -
   112      name: other-base
   113      id: otherbasedididididididididididid
   114      type: base
   115      modes:
   116        - run
   117      presence: required
   118    -
   119      name: nm
   120      id: nmididididididididididididididid
   121      modes:
   122        - ephemeral
   123        - run
   124      default-channel: 1.0
   125    -
   126      name: myapp
   127      id: myappdididididididididididididid
   128      type: app
   129      default-channel: 2.0
   130    -
   131      name: myappopt
   132      id: myappoptidididididididididididid
   133      type: app
   134      presence: optional
   135  OTHERgrade: secured
   136  storage-safety: encrypted
   137  ` + "TSLINE" +
   138  		"body-length: 0\n" +
   139  		"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
   140  		"\n\n" +
   141  		"AXNpZw=="
   142  )
   143  
   144  func (mods *modelSuite) TestDecodeOK(c *C) {
   145  	encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   146  	a, err := asserts.Decode([]byte(encoded))
   147  	c.Assert(err, IsNil)
   148  	c.Check(a.Type(), Equals, asserts.ModelType)
   149  	model := a.(*asserts.Model)
   150  	c.Check(model.AuthorityID(), Equals, "brand-id1")
   151  	c.Check(model.Timestamp(), Equals, mods.ts)
   152  	c.Check(model.Series(), Equals, "16")
   153  	c.Check(model.BrandID(), Equals, "brand-id1")
   154  	c.Check(model.Model(), Equals, "baz-3000")
   155  	c.Check(model.DisplayName(), Equals, "Baz 3000")
   156  	c.Check(model.Architecture(), Equals, "amd64")
   157  	c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
   158  		Name:     "brand-gadget",
   159  		SnapType: "gadget",
   160  		Modes:    []string{"run"},
   161  		Presence: "required",
   162  	})
   163  	c.Check(model.Gadget(), Equals, "brand-gadget")
   164  	c.Check(model.GadgetTrack(), Equals, "")
   165  	c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{
   166  		Name:     "baz-linux",
   167  		SnapType: "kernel",
   168  		Modes:    []string{"run"},
   169  		Presence: "required",
   170  	})
   171  	c.Check(model.Kernel(), Equals, "baz-linux")
   172  	c.Check(model.KernelTrack(), Equals, "")
   173  	c.Check(model.Base(), Equals, "core18")
   174  	c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{
   175  		Name:     "core18",
   176  		SnapType: "base",
   177  		Modes:    []string{"run"},
   178  		Presence: "required",
   179  	})
   180  	c.Check(model.Store(), Equals, "brand-store")
   181  	c.Check(model.Grade(), Equals, asserts.ModelGradeUnset)
   182  	c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyUnset)
   183  	essentialSnaps := model.EssentialSnaps()
   184  	c.Check(essentialSnaps, DeepEquals, []*asserts.ModelSnap{
   185  		model.KernelSnap(),
   186  		model.BaseSnap(),
   187  		model.GadgetSnap(),
   188  	})
   189  	snaps := model.SnapsWithoutEssential()
   190  	c.Check(snaps, DeepEquals, []*asserts.ModelSnap{
   191  		{
   192  			Name:     "foo",
   193  			Modes:    []string{"run"},
   194  			Presence: "required",
   195  		},
   196  		{
   197  			Name:     "bar",
   198  			Modes:    []string{"run"},
   199  			Presence: "required",
   200  		},
   201  	})
   202  	// essential snaps included
   203  	reqSnaps := naming.NewSnapSet(model.RequiredWithEssentialSnaps())
   204  	for _, e := range essentialSnaps {
   205  		c.Check(reqSnaps.Contains(e), Equals, true)
   206  	}
   207  	for _, s := range snaps {
   208  		c.Check(reqSnaps.Contains(s), Equals, true)
   209  	}
   210  	c.Check(reqSnaps.Size(), Equals, len(essentialSnaps)+len(snaps))
   211  	// essential snaps excluded
   212  	noEssential := naming.NewSnapSet(model.RequiredNoEssentialSnaps())
   213  	for _, e := range essentialSnaps {
   214  		c.Check(noEssential.Contains(e), Equals, false)
   215  	}
   216  	for _, s := range snaps {
   217  		c.Check(noEssential.Contains(s), Equals, true)
   218  	}
   219  	c.Check(noEssential.Size(), Equals, len(snaps))
   220  
   221  	c.Check(model.SystemUserAuthority(), HasLen, 0)
   222  	c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1", "generic"})
   223  }
   224  
   225  func (mods *modelSuite) TestDecodeStoreIsOptional(c *C) {
   226  	withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   227  	encoded := strings.Replace(withTimestamp, "store: brand-store\n", "store: \n", 1)
   228  	a, err := asserts.Decode([]byte(encoded))
   229  	c.Assert(err, IsNil)
   230  	model := a.(*asserts.Model)
   231  	c.Check(model.Store(), Equals, "")
   232  
   233  	encoded = strings.Replace(withTimestamp, "store: brand-store\n", "", 1)
   234  	a, err = asserts.Decode([]byte(encoded))
   235  	c.Assert(err, IsNil)
   236  	model = a.(*asserts.Model)
   237  	c.Check(model.Store(), Equals, "")
   238  }
   239  
   240  func (mods *modelSuite) TestDecodeBaseIsOptional(c *C) {
   241  	withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   242  	encoded := strings.Replace(withTimestamp, "base: core18\n", "base: \n", 1)
   243  	a, err := asserts.Decode([]byte(encoded))
   244  	c.Assert(err, IsNil)
   245  	model := a.(*asserts.Model)
   246  	c.Check(model.Base(), Equals, "")
   247  
   248  	encoded = strings.Replace(withTimestamp, "base: core18\n", "", 1)
   249  	a, err = asserts.Decode([]byte(encoded))
   250  	c.Assert(err, IsNil)
   251  	model = a.(*asserts.Model)
   252  	c.Check(model.Base(), Equals, "")
   253  	c.Check(model.BaseSnap(), IsNil)
   254  }
   255  
   256  func (mods *modelSuite) TestDecodeDisplayNameIsOptional(c *C) {
   257  	withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   258  	encoded := strings.Replace(withTimestamp, "display-name: Baz 3000\n", "display-name: \n", 1)
   259  	a, err := asserts.Decode([]byte(encoded))
   260  	c.Assert(err, IsNil)
   261  	model := a.(*asserts.Model)
   262  	// optional but we fallback to Model
   263  	c.Check(model.DisplayName(), Equals, "baz-3000")
   264  
   265  	encoded = strings.Replace(withTimestamp, "display-name: Baz 3000\n", "", 1)
   266  	a, err = asserts.Decode([]byte(encoded))
   267  	c.Assert(err, IsNil)
   268  	model = a.(*asserts.Model)
   269  	// optional but we fallback to Model
   270  	c.Check(model.DisplayName(), Equals, "baz-3000")
   271  }
   272  
   273  func (mods *modelSuite) TestDecodeRequiredSnapsAreOptional(c *C) {
   274  	withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   275  	encoded := strings.Replace(withTimestamp, reqSnaps, "", 1)
   276  	a, err := asserts.Decode([]byte(encoded))
   277  	c.Assert(err, IsNil)
   278  	model := a.(*asserts.Model)
   279  	c.Check(model.RequiredNoEssentialSnaps(), HasLen, 0)
   280  }
   281  
   282  func (mods *modelSuite) TestDecodeValidatesSnapNames(c *C) {
   283  	withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   284  	encoded := strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n  - foo_bar\n  - bar\n", 1)
   285  	a, err := asserts.Decode([]byte(encoded))
   286  	c.Assert(a, IsNil)
   287  	c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: foo_bar`)
   288  
   289  	encoded = strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n  - foo\n  - bar-;;''\n", 1)
   290  	a, err = asserts.Decode([]byte(encoded))
   291  	c.Assert(a, IsNil)
   292  	c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: bar-;;''`)
   293  
   294  	encoded = strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux_instance\n", 1)
   295  	a, err = asserts.Decode([]byte(encoded))
   296  	c.Assert(a, IsNil)
   297  	c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: baz-linux_instance`)
   298  
   299  	encoded = strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget_instance\n", 1)
   300  	a, err = asserts.Decode([]byte(encoded))
   301  	c.Assert(a, IsNil)
   302  	c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "gadget" header: brand-gadget_instance`)
   303  
   304  	encoded = strings.Replace(withTimestamp, "base: core18\n", "base: core18_instance\n", 1)
   305  	a, err = asserts.Decode([]byte(encoded))
   306  	c.Assert(a, IsNil)
   307  	c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "base" header: core18_instance`)
   308  }
   309  
   310  func (mods modelSuite) TestDecodeValidSnapNames(c *C) {
   311  	// reuse test cases for snap.ValidateName()
   312  
   313  	withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   314  
   315  	validNames := []string{
   316  		"aa", "aaa", "aaaa",
   317  		"a-a", "aa-a", "a-aa", "a-b-c",
   318  		"a0", "a-0", "a-0a",
   319  		"01game", "1-or-2",
   320  		// a regexp stresser
   321  		"u-94903713687486543234157734673284536758",
   322  	}
   323  	for _, name := range validNames {
   324  		encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1)
   325  		a, err := asserts.Decode([]byte(encoded))
   326  		c.Assert(err, IsNil)
   327  		model := a.(*asserts.Model)
   328  		c.Check(model.Kernel(), Equals, name)
   329  	}
   330  	invalidNames := []string{
   331  		// name cannot be empty, never reaches snap name validation
   332  		"",
   333  		// too short (min 2 chars)
   334  		"a",
   335  		// names cannot be too long
   336  		"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
   337  		"xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx",
   338  		"1111111111111111111111111111111111111111x",
   339  		"x1111111111111111111111111111111111111111",
   340  		"x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x",
   341  		// a regexp stresser
   342  		"u-9490371368748654323415773467328453675-",
   343  		// dashes alone are not a name
   344  		"-", "--",
   345  		// double dashes in a name are not allowed
   346  		"a--a",
   347  		// name should not end with a dash
   348  		"a-",
   349  		// name cannot have any spaces in it
   350  		"a ", " a", "a a",
   351  		// a number alone is not a name
   352  		"0", "123",
   353  		// identifier must be plain ASCII
   354  		"日本語", "한글", "ру́сский язы́к",
   355  		// instance names are invalid too
   356  		"foo_bar", "x_1",
   357  	}
   358  	for _, name := range invalidNames {
   359  		encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1)
   360  		a, err := asserts.Decode([]byte(encoded))
   361  		c.Assert(a, IsNil)
   362  		if name != "" {
   363  			c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: .*`)
   364  		} else {
   365  			c.Assert(err, ErrorMatches, `assertion model: "kernel" header should not be empty`)
   366  		}
   367  	}
   368  }
   369  
   370  func (mods *modelSuite) TestDecodeSerialAuthorityIsOptional(c *C) {
   371  	withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   372  	encoded := strings.Replace(withTimestamp, serialAuths, "", 1)
   373  	a, err := asserts.Decode([]byte(encoded))
   374  	c.Assert(err, IsNil)
   375  	model := a.(*asserts.Model)
   376  	// the default is just to accept the brand itself
   377  	c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1"})
   378  
   379  	encoded = strings.Replace(withTimestamp, serialAuths, "serial-authority:\n  - foo\n  - bar\n", 1)
   380  	a, err = asserts.Decode([]byte(encoded))
   381  	c.Assert(err, IsNil)
   382  	model = a.(*asserts.Model)
   383  	// the brand is always added implicitly
   384  	c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1", "foo", "bar"})
   385  }
   386  
   387  func (mods *modelSuite) TestDecodeSystemUserAuthorityIsOptional(c *C) {
   388  	withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   389  	encoded := strings.Replace(withTimestamp, sysUserAuths, "", 1)
   390  	a, err := asserts.Decode([]byte(encoded))
   391  	c.Assert(err, IsNil)
   392  	model := a.(*asserts.Model)
   393  	// the default is just to accept the brand itself
   394  	c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1"})
   395  
   396  	encoded = strings.Replace(withTimestamp, sysUserAuths, "system-user-authority:\n  - foo\n  - bar\n", 1)
   397  	a, err = asserts.Decode([]byte(encoded))
   398  	c.Assert(err, IsNil)
   399  	model = a.(*asserts.Model)
   400  	// the brand is always added implicitly, it can always sign
   401  	// a new revision of the model anyway
   402  	c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1", "foo", "bar"})
   403  }
   404  
   405  func (mods *modelSuite) TestDecodeKernelTrack(c *C) {
   406  	withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   407  	encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux=18\n", 1)
   408  	a, err := asserts.Decode([]byte(encoded))
   409  	c.Assert(err, IsNil)
   410  	model := a.(*asserts.Model)
   411  	c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{
   412  		Name:        "baz-linux",
   413  		SnapType:    "kernel",
   414  		Modes:       []string{"run"},
   415  		PinnedTrack: "18",
   416  		Presence:    "required",
   417  	})
   418  	c.Check(model.Kernel(), Equals, "baz-linux")
   419  	c.Check(model.KernelTrack(), Equals, "18")
   420  }
   421  
   422  func (mods *modelSuite) TestDecodeGadgetTrack(c *C) {
   423  	withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   424  	encoded := strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget=18\n", 1)
   425  	a, err := asserts.Decode([]byte(encoded))
   426  	c.Assert(err, IsNil)
   427  	model := a.(*asserts.Model)
   428  	c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
   429  		Name:        "brand-gadget",
   430  		SnapType:    "gadget",
   431  		Modes:       []string{"run"},
   432  		PinnedTrack: "18",
   433  		Presence:    "required",
   434  	})
   435  	c.Check(model.Gadget(), Equals, "brand-gadget")
   436  	c.Check(model.GadgetTrack(), Equals, "18")
   437  }
   438  
   439  const (
   440  	modelErrPrefix = "assertion model: "
   441  )
   442  
   443  func (mods *modelSuite) TestDecodeInvalid(c *C) {
   444  	encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
   445  
   446  	invalidTests := []struct{ original, invalid, expectedErr string }{
   447  		{"series: 16\n", "", `"series" header is mandatory`},
   448  		{"series: 16\n", "series: \n", `"series" header should not be empty`},
   449  		{"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
   450  		{"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
   451  		{"brand-id: brand-id1\n", "brand-id: random\n", `authority-id and brand-id must match, model assertions are expected to be signed by the brand: "brand-id1" != "random"`},
   452  		{"model: baz-3000\n", "", `"model" header is mandatory`},
   453  		{"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
   454  		{"model: baz-3000\n", "model: baz/3000\n", `"model" primary key header cannot contain '/'`},
   455  		// lift this restriction at a later point
   456  		{"model: baz-3000\n", "model: BAZ-3000\n", `"model" header cannot contain uppercase letters`},
   457  		{"display-name: Baz 3000\n", "display-name:\n  - xyz\n", `"display-name" header must be a string`},
   458  		{"architecture: amd64\n", "", `"architecture" header is mandatory`},
   459  		{"architecture: amd64\n", "architecture: \n", `"architecture" header should not be empty`},
   460  		{"gadget: brand-gadget\n", "", `"gadget" header is mandatory`},
   461  		{"gadget: brand-gadget\n", "gadget: \n", `"gadget" header should not be empty`},
   462  		{"gadget: brand-gadget\n", "gadget: brand-gadget=x/x/x\n", `"gadget" channel selector must be a track name only`},
   463  		{"gadget: brand-gadget\n", "gadget: brand-gadget=stable\n", `"gadget" channel selector must be a track name`},
   464  		{"gadget: brand-gadget\n", "gadget: brand-gadget=18/beta\n", `"gadget" channel selector must be a track name only`},
   465  		{"gadget: brand-gadget\n", "gadget:\n  - xyz \n", `"gadget" header must be a string`},
   466  		{"kernel: baz-linux\n", "", `"kernel" header is mandatory`},
   467  		{"kernel: baz-linux\n", "kernel: \n", `"kernel" header should not be empty`},
   468  		{"kernel: baz-linux\n", "kernel: baz-linux=x/x/x\n", `"kernel" channel selector must be a track name only`},
   469  		{"kernel: baz-linux\n", "kernel: baz-linux=stable\n", `"kernel" channel selector must be a track name`},
   470  		{"kernel: baz-linux\n", "kernel: baz-linux=18/beta\n", `"kernel" channel selector must be a track name only`},
   471  		{"kernel: baz-linux\n", "kernel:\n  - xyz \n", `"kernel" header must be a string`},
   472  		{"base: core18\n", "base:\n  - xyz \n", `"base" header must be a string`},
   473  		{"store: brand-store\n", "store:\n  - xyz\n", `"store" header must be a string`},
   474  		{mods.tsLine, "", `"timestamp" header is mandatory`},
   475  		{mods.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
   476  		{mods.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
   477  		{reqSnaps, "required-snaps: foo\n", `"required-snaps" header must be a list of strings`},
   478  		{reqSnaps, "required-snaps:\n  -\n    - nested\n", `"required-snaps" header must be a list of strings`},
   479  		{serialAuths, "serial-authority:\n  a: 1\n", `"serial-authority" header must be a list of account ids`},
   480  		{serialAuths, "serial-authority:\n  - 5_6\n", `"serial-authority" header must be a list of account ids`},
   481  		{sysUserAuths, "system-user-authority:\n  a: 1\n", `"system-user-authority" header must be '\*' or a list of account ids`},
   482  		{sysUserAuths, "system-user-authority:\n  - 5_6\n", `"system-user-authority" header must be '\*' or a list of account ids`},
   483  		{reqSnaps, "grade: dangerous\n", `cannot specify a grade for model without the extended snaps header`},
   484  	}
   485  
   486  	for _, test := range invalidTests {
   487  		invalid := strings.Replace(encoded, test.original, test.invalid, 1)
   488  		_, err := asserts.Decode([]byte(invalid))
   489  		c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
   490  	}
   491  }
   492  
   493  func (mods *modelSuite) TestModelCheck(c *C) {
   494  	ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)))
   495  	c.Assert(err, IsNil)
   496  
   497  	storeDB, db := makeStoreAndCheckDB(c)
   498  	brandDB := setup3rdPartySigning(c, "brand-id1", storeDB, db)
   499  
   500  	headers := ex.Headers()
   501  	headers["brand-id"] = brandDB.AuthorityID
   502  	headers["timestamp"] = time.Now().Format(time.RFC3339)
   503  	model, err := brandDB.Sign(asserts.ModelType, headers, nil, "")
   504  	c.Assert(err, IsNil)
   505  
   506  	err = db.Check(model)
   507  	c.Assert(err, IsNil)
   508  }
   509  
   510  func (mods *modelSuite) TestModelCheckInconsistentTimestamp(c *C) {
   511  	ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)))
   512  	c.Assert(err, IsNil)
   513  
   514  	storeDB, db := makeStoreAndCheckDB(c)
   515  	brandDB := setup3rdPartySigning(c, "brand-id1", storeDB, db)
   516  
   517  	headers := ex.Headers()
   518  	headers["brand-id"] = brandDB.AuthorityID
   519  	headers["timestamp"] = "2011-01-01T14:00:00Z"
   520  	model, err := brandDB.Sign(asserts.ModelType, headers, nil, "")
   521  	c.Assert(err, IsNil)
   522  
   523  	err = db.Check(model)
   524  	c.Assert(err, ErrorMatches, `model assertion timestamp "2011-01-01 14:00:00 \+0000 UTC" outside of signing key validity \(key valid since.*\)`)
   525  }
   526  
   527  func (mods *modelSuite) TestClassicDecodeOK(c *C) {
   528  	encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
   529  	a, err := asserts.Decode([]byte(encoded))
   530  	c.Assert(err, IsNil)
   531  	c.Check(a.Type(), Equals, asserts.ModelType)
   532  	model := a.(*asserts.Model)
   533  	c.Check(model.AuthorityID(), Equals, "brand-id1")
   534  	c.Check(model.Timestamp(), Equals, mods.ts)
   535  	c.Check(model.Series(), Equals, "16")
   536  	c.Check(model.BrandID(), Equals, "brand-id1")
   537  	c.Check(model.Model(), Equals, "baz-3000")
   538  	c.Check(model.DisplayName(), Equals, "Baz 3000")
   539  	c.Check(model.Classic(), Equals, true)
   540  	c.Check(model.Architecture(), Equals, "amd64")
   541  	c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
   542  		Name:     "brand-gadget",
   543  		SnapType: "gadget",
   544  		Modes:    []string{"run"},
   545  		Presence: "required",
   546  	})
   547  	c.Check(model.Gadget(), Equals, "brand-gadget")
   548  	c.Check(model.KernelSnap(), IsNil)
   549  	c.Check(model.Kernel(), Equals, "")
   550  	c.Check(model.KernelTrack(), Equals, "")
   551  	c.Check(model.Base(), Equals, "")
   552  	c.Check(model.BaseSnap(), IsNil)
   553  	c.Check(model.Store(), Equals, "brand-store")
   554  	essentialSnaps := model.EssentialSnaps()
   555  	c.Check(essentialSnaps, DeepEquals, []*asserts.ModelSnap{
   556  		model.GadgetSnap(),
   557  	})
   558  	snaps := model.SnapsWithoutEssential()
   559  	c.Check(snaps, DeepEquals, []*asserts.ModelSnap{
   560  		{
   561  			Name:     "foo",
   562  			Modes:    []string{"run"},
   563  			Presence: "required",
   564  		},
   565  		{
   566  			Name:     "bar",
   567  			Modes:    []string{"run"},
   568  			Presence: "required",
   569  		},
   570  	})
   571  	// gadget included
   572  	reqSnaps := naming.NewSnapSet(model.RequiredWithEssentialSnaps())
   573  	for _, e := range essentialSnaps {
   574  		c.Check(reqSnaps.Contains(e), Equals, true)
   575  	}
   576  	for _, s := range snaps {
   577  		c.Check(reqSnaps.Contains(s), Equals, true)
   578  	}
   579  	c.Check(reqSnaps.Size(), Equals, len(essentialSnaps)+len(snaps))
   580  	// gadget excluded
   581  	noEssential := naming.NewSnapSet(model.RequiredNoEssentialSnaps())
   582  	for _, e := range essentialSnaps {
   583  		c.Check(noEssential.Contains(e), Equals, false)
   584  	}
   585  	for _, s := range snaps {
   586  		c.Check(noEssential.Contains(s), Equals, true)
   587  	}
   588  	c.Check(noEssential.Size(), Equals, len(snaps))
   589  }
   590  
   591  func (mods *modelSuite) TestClassicDecodeInvalid(c *C) {
   592  	encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
   593  
   594  	invalidTests := []struct{ original, invalid, expectedErr string }{
   595  		{"classic: true\n", "classic: foo\n", `"classic" header must be 'true' or 'false'`},
   596  		{"architecture: amd64\n", "architecture:\n  - foo\n", `"architecture" header must be a string`},
   597  		{"gadget: brand-gadget\n", "gadget:\n  - foo\n", `"gadget" header must be a string`},
   598  		{"gadget: brand-gadget\n", "kernel: brand-kernel\n", `cannot specify a kernel with a classic model`},
   599  		{"gadget: brand-gadget\n", "base: some-base\n", `cannot specify a base with a classic model`},
   600  		{"gadget: brand-gadget\n", "gadget:\n  - xyz\n", `"gadget" header must be a string`},
   601  	}
   602  
   603  	for _, test := range invalidTests {
   604  		invalid := strings.Replace(encoded, test.original, test.invalid, 1)
   605  		_, err := asserts.Decode([]byte(invalid))
   606  		c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
   607  	}
   608  }
   609  
   610  func (mods *modelSuite) TestClassicDecodeGadgetAndArchOptional(c *C) {
   611  	encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
   612  	encoded = strings.Replace(encoded, "gadget: brand-gadget\n", "", 1)
   613  	encoded = strings.Replace(encoded, "architecture: amd64\n", "", 1)
   614  	a, err := asserts.Decode([]byte(encoded))
   615  	c.Assert(err, IsNil)
   616  	c.Check(a.Type(), Equals, asserts.ModelType)
   617  	model := a.(*asserts.Model)
   618  	c.Check(model.Classic(), Equals, true)
   619  	c.Check(model.Architecture(), Equals, "")
   620  	c.Check(model.GadgetSnap(), IsNil)
   621  	c.Check(model.Gadget(), Equals, "")
   622  	c.Check(model.GadgetTrack(), Equals, "")
   623  }
   624  
   625  func (mods *modelSuite) TestCore20DecodeOK(c *C) {
   626  	encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
   627  	encoded = strings.Replace(encoded, "OTHER", "", 1)
   628  	a, err := asserts.Decode([]byte(encoded))
   629  	c.Assert(err, IsNil)
   630  	c.Check(a.Type(), Equals, asserts.ModelType)
   631  	model := a.(*asserts.Model)
   632  	c.Check(model.AuthorityID(), Equals, "brand-id1")
   633  	c.Check(model.Timestamp(), Equals, mods.ts)
   634  	c.Check(model.Series(), Equals, "16")
   635  	c.Check(model.BrandID(), Equals, "brand-id1")
   636  	c.Check(model.Model(), Equals, "baz-3000")
   637  	c.Check(model.DisplayName(), Equals, "Baz 3000")
   638  	c.Check(model.Architecture(), Equals, "amd64")
   639  	c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
   640  		Name:           "brand-gadget",
   641  		SnapID:         "brandgadgetdidididididididididid",
   642  		SnapType:       "gadget",
   643  		Modes:          []string{"run", "ephemeral"},
   644  		DefaultChannel: "latest/stable",
   645  		Presence:       "required",
   646  	})
   647  	c.Check(model.Gadget(), Equals, "brand-gadget")
   648  	c.Check(model.GadgetTrack(), Equals, "")
   649  	c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{
   650  		Name:           "baz-linux",
   651  		SnapID:         "bazlinuxidididididididididididid",
   652  		SnapType:       "kernel",
   653  		Modes:          []string{"run", "ephemeral"},
   654  		DefaultChannel: "20",
   655  		Presence:       "required",
   656  	})
   657  	c.Check(model.Kernel(), Equals, "baz-linux")
   658  	c.Check(model.KernelTrack(), Equals, "")
   659  	c.Check(model.Base(), Equals, "core20")
   660  	c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{
   661  		Name:           "core20",
   662  		SnapID:         naming.WellKnownSnapID("core20"),
   663  		SnapType:       "base",
   664  		Modes:          []string{"run", "ephemeral"},
   665  		DefaultChannel: "latest/stable",
   666  		Presence:       "required",
   667  	})
   668  	c.Check(model.Store(), Equals, "brand-store")
   669  	c.Check(model.Grade(), Equals, asserts.ModelSecured)
   670  	c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyEncrypted)
   671  	essentialSnaps := model.EssentialSnaps()
   672  	c.Check(essentialSnaps, DeepEquals, []*asserts.ModelSnap{
   673  		model.KernelSnap(),
   674  		model.BaseSnap(),
   675  		model.GadgetSnap(),
   676  	})
   677  	snaps := model.SnapsWithoutEssential()
   678  	c.Check(snaps, DeepEquals, []*asserts.ModelSnap{
   679  		{
   680  			Name:           "other-base",
   681  			SnapID:         "otherbasedididididididididididid",
   682  			SnapType:       "base",
   683  			Modes:          []string{"run"},
   684  			DefaultChannel: "latest/stable",
   685  			Presence:       "required",
   686  		},
   687  		{
   688  			Name:           "nm",
   689  			SnapID:         "nmididididididididididididididid",
   690  			SnapType:       "app",
   691  			Modes:          []string{"ephemeral", "run"},
   692  			DefaultChannel: "1.0",
   693  			Presence:       "required",
   694  		},
   695  		{
   696  			Name:           "myapp",
   697  			SnapID:         "myappdididididididididididididid",
   698  			SnapType:       "app",
   699  			Modes:          []string{"run"},
   700  			DefaultChannel: "2.0",
   701  			Presence:       "required",
   702  		},
   703  		{
   704  			Name:           "myappopt",
   705  			SnapID:         "myappoptidididididididididididid",
   706  			SnapType:       "app",
   707  			Modes:          []string{"run"},
   708  			DefaultChannel: "latest/stable",
   709  			Presence:       "optional",
   710  		},
   711  	})
   712  	// essential snaps included
   713  	reqSnaps := naming.NewSnapSet(model.RequiredWithEssentialSnaps())
   714  	for _, e := range essentialSnaps {
   715  		c.Check(reqSnaps.Contains(e), Equals, true)
   716  	}
   717  	for _, s := range snaps {
   718  		c.Check(reqSnaps.Contains(s), Equals, s.Presence == "required")
   719  	}
   720  	c.Check(reqSnaps.Size(), Equals, len(essentialSnaps)+len(snaps)-1)
   721  	// essential snaps excluded
   722  	noEssential := naming.NewSnapSet(model.RequiredNoEssentialSnaps())
   723  	for _, e := range essentialSnaps {
   724  		c.Check(noEssential.Contains(e), Equals, false)
   725  	}
   726  	for _, s := range snaps {
   727  		c.Check(noEssential.Contains(s), Equals, s.Presence == "required")
   728  	}
   729  	c.Check(noEssential.Size(), Equals, len(snaps)-1)
   730  
   731  	c.Check(model.SystemUserAuthority(), HasLen, 0)
   732  	c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1"})
   733  }
   734  
   735  func (mods *modelSuite) TestCore20ExplictBootBase(c *C) {
   736  	encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
   737  	encoded = strings.Replace(encoded, "OTHER", `  -
   738      name: core20
   739      id: core20ididididididididididididid
   740      type: base
   741      default-channel: latest/candidate
   742  `, 1)
   743  	a, err := asserts.Decode([]byte(encoded))
   744  	c.Assert(err, IsNil)
   745  	c.Check(a.Type(), Equals, asserts.ModelType)
   746  	model := a.(*asserts.Model)
   747  	c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{
   748  		Name:           "core20",
   749  		SnapID:         "core20ididididididididididididid",
   750  		SnapType:       "base",
   751  		Modes:          []string{"run", "ephemeral"},
   752  		DefaultChannel: "latest/candidate",
   753  		Presence:       "required",
   754  	})
   755  }
   756  
   757  func (mods *modelSuite) TestCore20ExplictSnapd(c *C) {
   758  	encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
   759  	encoded = strings.Replace(encoded, "OTHER", `  -
   760      name: snapd
   761      id: snapdidididididididididididididd
   762      type: snapd
   763      default-channel: latest/edge
   764  `, 1)
   765  	a, err := asserts.Decode([]byte(encoded))
   766  	c.Assert(err, IsNil)
   767  	c.Check(a.Type(), Equals, asserts.ModelType)
   768  	model := a.(*asserts.Model)
   769  	snapdSnap := model.EssentialSnaps()[0]
   770  	c.Check(snapdSnap, DeepEquals, &asserts.ModelSnap{
   771  		Name:           "snapd",
   772  		SnapID:         "snapdidididididididididididididd",
   773  		SnapType:       "snapd",
   774  		Modes:          []string{"run", "ephemeral"},
   775  		DefaultChannel: "latest/edge",
   776  		Presence:       "required",
   777  	})
   778  }
   779  
   780  func (mods *modelSuite) TestCore20GradeOptionalDefaultSigned(c *C) {
   781  	encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
   782  	encoded = strings.Replace(encoded, "OTHER", "", 1)
   783  	encoded = strings.Replace(encoded, "grade: secured\n", "", 1)
   784  
   785  	a, err := asserts.Decode([]byte(encoded))
   786  	c.Assert(err, IsNil)
   787  	c.Check(a.Type(), Equals, asserts.ModelType)
   788  	model := a.(*asserts.Model)
   789  	c.Check(model.Grade(), Equals, asserts.ModelSigned)
   790  }
   791  
   792  func (mods *modelSuite) TestCore20ValidGrades(c *C) {
   793  	encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
   794  	encoded = strings.Replace(encoded, "OTHER", "", 1)
   795  
   796  	for _, grade := range []string{"signed", "secured", "dangerous"} {
   797  		ex := strings.Replace(encoded, "grade: secured\n", fmt.Sprintf("grade: %s\n", grade), 1)
   798  		a, err := asserts.Decode([]byte(ex))
   799  		c.Assert(err, IsNil)
   800  		c.Check(a.Type(), Equals, asserts.ModelType)
   801  		model := a.(*asserts.Model)
   802  		c.Check(string(model.Grade()), Equals, grade)
   803  	}
   804  }
   805  
   806  func (mods *modelSuite) TestModelGradeCode(c *C) {
   807  	for i, grade := range []asserts.ModelGrade{asserts.ModelGradeUnset, asserts.ModelDangerous, asserts.ModelSigned, asserts.ModelSecured} {
   808  		// unset is represented as zero
   809  		code := 0
   810  		if i > 0 {
   811  			// have some space between grades to add new ones
   812  			n := (i - 1) * 8
   813  			if n == 0 {
   814  				n = 1 // dangerous
   815  			}
   816  			// lower 16 bits are reserved
   817  			code = n << 16
   818  		}
   819  		c.Check(grade.Code(), Equals, uint32(code))
   820  	}
   821  }
   822  
   823  func (mods *modelSuite) TestCore20GradeDangerous(c *C) {
   824  	encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
   825  	encoded = strings.Replace(encoded, "OTHER", "", 1)
   826  	encoded = strings.Replace(encoded, "grade: secured\n", "grade: dangerous\n", 1)
   827  	// snap ids are optional with grade dangerous to allow working
   828  	// with local/not pushed yet to the store snaps
   829  	encoded = strings.Replace(encoded, "    id: myappdididididididididididididid\n", "", 1)
   830  	encoded = strings.Replace(encoded, "    id: brandgadgetdidididididididididid\n", "", 1)
   831  	a, err := asserts.Decode([]byte(encoded))
   832  	c.Assert(err, IsNil)
   833  	c.Check(a.Type(), Equals, asserts.ModelType)
   834  	model := a.(*asserts.Model)
   835  	c.Check(model.Grade(), Equals, asserts.ModelDangerous)
   836  	snaps := model.SnapsWithoutEssential()
   837  	c.Check(snaps[len(snaps)-2], DeepEquals, &asserts.ModelSnap{
   838  		Name:           "myapp",
   839  		SnapType:       "app",
   840  		Modes:          []string{"run"},
   841  		DefaultChannel: "2.0",
   842  		Presence:       "required",
   843  	})
   844  }
   845  
   846  func (mods *modelSuite) TestCore20ValidStorageSafety(c *C) {
   847  	encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
   848  	encoded = strings.Replace(encoded, "OTHER", "", 1)
   849  	encoded = strings.Replace(encoded, "grade: secured\n", "grade: signed\n", 1)
   850  
   851  	for _, tc := range []struct {
   852  		ss  asserts.StorageSafety
   853  		sss string
   854  	}{
   855  		{asserts.StorageSafetyPreferEncrypted, "prefer-encrypted"},
   856  		{asserts.StorageSafetyPreferUnencrypted, "prefer-unencrypted"},
   857  		{asserts.StorageSafetyEncrypted, "encrypted"},
   858  	} {
   859  		ex := strings.Replace(encoded, "storage-safety: encrypted\n", fmt.Sprintf("storage-safety: %s\n", tc.sss), 1)
   860  		a, err := asserts.Decode([]byte(ex))
   861  		c.Assert(err, IsNil)
   862  		c.Check(a.Type(), Equals, asserts.ModelType)
   863  		model := a.(*asserts.Model)
   864  		c.Check(model.StorageSafety(), Equals, tc.ss)
   865  	}
   866  }
   867  
   868  func (mods *modelSuite) TestCore20DefaultStorageSafetySecured(c *C) {
   869  	encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
   870  	encoded = strings.Replace(encoded, "OTHER", "", 1)
   871  	ex := strings.Replace(encoded, "storage-safety: encrypted\n", "", 1)
   872  
   873  	a, err := asserts.Decode([]byte(ex))
   874  	c.Assert(err, IsNil)
   875  	c.Check(a.Type(), Equals, asserts.ModelType)
   876  	model := a.(*asserts.Model)
   877  	c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyEncrypted)
   878  }
   879  
   880  func (mods *modelSuite) TestCore20DefaultStorageSafetySignedDangerous(c *C) {
   881  	encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
   882  	encoded = strings.Replace(encoded, "OTHER", "", 1)
   883  	encoded = strings.Replace(encoded, "storage-safety: encrypted\n", "", 1)
   884  
   885  	for _, grade := range []string{"dangerous", "signed"} {
   886  		ex := strings.Replace(encoded, "grade: secured\n", fmt.Sprintf("grade: %s\n", grade), 1)
   887  		a, err := asserts.Decode([]byte(ex))
   888  		c.Assert(err, IsNil)
   889  		c.Check(a.Type(), Equals, asserts.ModelType)
   890  		model := a.(*asserts.Model)
   891  		c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyPreferEncrypted)
   892  	}
   893  }
   894  
   895  func (mods *modelSuite) TestCore20DecodeInvalid(c *C) {
   896  	encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
   897  
   898  	snapsStanza := encoded[strings.Index(encoded, "snaps:"):strings.Index(encoded, "grade:")]
   899  
   900  	invalidTests := []struct{ original, invalid, expectedErr string }{
   901  		{"base: core20\n", "", `"base" header is mandatory`},
   902  		{"base: core20\n", "base: alt-base\n", `cannot specify not well-known base "alt-base" without a corresponding "snaps" header entry`},
   903  		{"OTHER", "classic: true\n", `cannot use extended snaps header for a classic model \(yet\)`},
   904  		{snapsStanza, "snaps: snap\n", `"snaps" header must be a list of maps`},
   905  		{snapsStanza, "snaps:\n  - snap\n", `"snaps" header must be a list of maps`},
   906  		{"name: myapp\n", "other: 1\n", `"name" of snap is mandatory`},
   907  		{"name: myapp\n", "name: myapp_2\n", `invalid snap name "myapp_2"`},
   908  		{"id: myappdididididididididididididid\n", "id: 2\n", `"id" of snap "myapp" contains invalid characters: "2"`},
   909  		{"    id: myappdididididididididididididid\n", "", `"id" of snap "myapp" is mandatory for secured grade model`},
   910  		{"type: gadget\n", "type:\n      - g\n", `"type" of snap "brand-gadget" must be a string`},
   911  		{"type: app\n", "type: thing\n", `"type" of snap "myappopt" must be one of must be one of app|base|gadget|kernel|core|snapd`},
   912  		{"modes:\n      - run\n", "modes: run\n", `"modes" of snap "other-base" must be a list of strings`},
   913  		{"default-channel: 20\n", "default-channel: edge\n", `default channel for snap "baz-linux" must specify a track`},
   914  		{"default-channel: 2.0\n", "default-channel:\n      - x\n", `"default-channel" of snap "myapp" must be a string`},
   915  		{"default-channel: 2.0\n", "default-channel: 2.0/xyz/z\n", `invalid default channel for snap "myapp": invalid risk in channel name: 2.0/xyz/z`},
   916  		{"presence: optional\n", "presence:\n      - opt\n", `"presence" of snap "myappopt" must be a string`},
   917  		{"presence: optional\n", "presence: no\n", `"presence" of snap "myappopt" must be one of must be one of required|optional`},
   918  		{"OTHER", "  -\n    name: myapp\n    id: myappdididididididididididididid\n", `cannot list the same snap "myapp" multiple times`},
   919  		{"OTHER", "  -\n    name: myapp2\n    id: myappdididididididididididididid\n", `cannot specify the same snap id "myappdididididididididididididid" multiple times, specified for snaps "myapp" and "myapp2"`},
   920  		{"OTHER", "  -\n    name: kernel2\n    id: kernel2didididididididididididid\n    type: kernel\n", `cannot specify multiple kernel snaps: "baz-linux" and "kernel2"`},
   921  		{"OTHER", "  -\n    name: gadget2\n    id: gadget2didididididididididididid\n    type: gadget\n", `cannot specify multiple gadget snaps: "brand-gadget" and "gadget2"`},
   922  		{"type: gadget\n", "type: gadget\n    presence: required\n", `essential snaps are always available, cannot specify modes or presence for snap "brand-gadget"`},
   923  		{"type: gadget\n", "type: gadget\n    modes:\n      - run\n", `essential snaps are always available, cannot specify modes or presence for snap "brand-gadget"`},
   924  		{"type: kernel\n", "type: kernel\n    presence: required\n", `essential snaps are always available, cannot specify modes or presence for snap "baz-linux"`},
   925  		{"OTHER", "  -\n    name: core20\n    id: core20ididididididididididididid\n    type: base\n    presence: optional\n", `essential snaps are always available, cannot specify modes or presence for snap "core20"`},
   926  		{"type: gadget\n", "type: app\n", `one "snaps" header entry must specify the model gadget`},
   927  		{"type: kernel\n", "type: app\n", `one "snaps" header entry must specify the model kernel`},
   928  		{"OTHER", "  -\n    name: core20\n    id: core20ididididididididididididid\n    type: app\n", `boot base "core20" must specify type "base", not "app"`},
   929  		{"OTHER", "kernel: foo\n", `cannot specify separate "kernel" header once using the extended snaps header`},
   930  		{"OTHER", "gadget: foo\n", `cannot specify separate "gadget" header once using the extended snaps header`},
   931  		{"OTHER", "required-snaps:\n  - foo\n", `cannot specify separate "required-snaps" header once using the extended snaps header`},
   932  		{"grade: secured\n", "grade: foo\n", `grade for model must be secured|signed|dangerous`},
   933  		{"storage-safety: encrypted\n", "storage-safety: foo\n", `storage-safety for model must be encrypted\|prefer-encrypted\|prefer-unencrypted, not "foo"`},
   934  		{"storage-safety: encrypted\n", "storage-safety: prefer-unencrypted\n", `secured grade model must not have storage-safety overridden, only "encrypted" is valid`},
   935  	}
   936  	for _, test := range invalidTests {
   937  		invalid := strings.Replace(encoded, test.original, test.invalid, 1)
   938  		invalid = strings.Replace(invalid, "OTHER", "", 1)
   939  		_, err := asserts.Decode([]byte(invalid))
   940  		c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
   941  	}
   942  }