github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/asserts/repair_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 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  	"io/ioutil"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/snapcore/snapd/asserts"
    31  	. "gopkg.in/check.v1"
    32  )
    33  
    34  var (
    35  	_ = Suite(&repairSuite{})
    36  )
    37  
    38  type repairSuite struct {
    39  	modelsLine string
    40  	ts         time.Time
    41  	tsLine     string
    42  	basesLine  string
    43  	modesLine  string
    44  
    45  	repairStr string
    46  }
    47  
    48  const script = `#!/bin/sh
    49  set -e
    50  echo "Unpack embedded payload"
    51  match=$(grep --text --line-number '^PAYLOAD:$' $0 | cut -d ':' -f 1)
    52  payload_start=$((match + 1))
    53  # Using "base64" as its part of coreutils which should be available
    54  # everywhere
    55  tail -n +$payload_start $0 | base64 --decode - | tar -xzf -
    56  # run embedded content
    57  ./hello
    58  exit 0
    59  # payload generated with, may contain binary data
    60  #   printf '#!/bin/sh\necho hello from the inside\n' > hello
    61  #   chmod +x hello
    62  #   tar czf - hello | base64 -
    63  PAYLOAD:
    64  H4sIAJJt+FgAA+3STQrCMBDF8ax7ihEP0CkxyXn8iCZQE2jr/W11Iwi6KiL8f5u3mLd4i0mx76tZ
    65  l86Cc0t2welrPu2c6awGr95bG4x26rw1oivveriN034QMfFSy6fet/uf2m7aQy7tmJp4TFXS8g5y
    66  HupVphQllzGfYvPrkQAAAAAAAAAAAAAAAACAN3dTp9TNACgAAA==
    67  `
    68  
    69  var repairExample = fmt.Sprintf("type: repair\n"+
    70  	"authority-id: acme\n"+
    71  	"brand-id: acme\n"+
    72  	"summary: example repair\n"+
    73  	"architectures:\n"+
    74  	"  - amd64\n"+
    75  	"  - arm64\n"+
    76  	"repair-id: 42\n"+
    77  	"series:\n"+
    78  	"  - 16\n"+
    79  	"MODELSLINE"+
    80  	"TSLINE"+
    81  	"BASESLINE"+
    82  	"MODESLINE"+
    83  	"body-length: %v\n"+
    84  	"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij"+
    85  	"\n\n"+
    86  	script+"\n\n"+
    87  	"AXNpZw==", len(script))
    88  
    89  func (s *repairSuite) SetUpTest(c *C) {
    90  	s.modelsLine = "models:\n  - acme/frobinator\n"
    91  	s.ts = time.Now().Truncate(time.Second).UTC()
    92  	s.tsLine = "timestamp: " + s.ts.Format(time.RFC3339) + "\n"
    93  	s.basesLine = "bases:\n  - core20\n"
    94  	s.modesLine = "modes:\n  - run\n"
    95  
    96  	s.repairStr = strings.Replace(repairExample, "MODELSLINE", s.modelsLine, 1)
    97  	s.repairStr = strings.Replace(s.repairStr, "TSLINE", s.tsLine, 1)
    98  	s.repairStr = strings.Replace(s.repairStr, "BASESLINE", s.basesLine, 1)
    99  	s.repairStr = strings.Replace(s.repairStr, "MODESLINE", s.modesLine, 1)
   100  }
   101  
   102  func (s *repairSuite) TestDecodeOK(c *C) {
   103  	a, err := asserts.Decode([]byte(s.repairStr))
   104  	c.Assert(err, IsNil)
   105  	c.Check(a.Type(), Equals, asserts.RepairType)
   106  	_, ok := a.(asserts.SequenceMember)
   107  	c.Assert(ok, Equals, true)
   108  	repair := a.(*asserts.Repair)
   109  	c.Check(repair.Timestamp(), Equals, s.ts)
   110  	c.Check(repair.BrandID(), Equals, "acme")
   111  	c.Check(repair.RepairID(), Equals, 42)
   112  	c.Check(repair.Sequence(), Equals, 42)
   113  	c.Check(repair.Summary(), Equals, "example repair")
   114  	c.Check(repair.Series(), DeepEquals, []string{"16"})
   115  	c.Check(repair.Bases(), DeepEquals, []string{"core20"})
   116  	c.Check(repair.Modes(), DeepEquals, []string{"run"})
   117  	c.Check(repair.Architectures(), DeepEquals, []string{"amd64", "arm64"})
   118  	c.Check(repair.Models(), DeepEquals, []string{"acme/frobinator"})
   119  	c.Check(string(repair.Body()), Equals, script)
   120  }
   121  
   122  const (
   123  	repairErrPrefix = "assertion repair: "
   124  )
   125  
   126  func (s *repairSuite) TestDisabled(c *C) {
   127  	disabledTests := []struct {
   128  		disabled, expectedErr string
   129  		dis                   bool
   130  	}{
   131  		{"true", "", true},
   132  		{"false", "", false},
   133  		{"foo", `"disabled" header must be 'true' or 'false'`, false},
   134  	}
   135  
   136  	for _, test := range disabledTests {
   137  		repairStr := strings.Replace(repairExample, "MODELSLINE", fmt.Sprintf("disabled: %s\n", test.disabled), 1)
   138  		repairStr = strings.Replace(repairStr, "TSLINE", s.tsLine, 1)
   139  		repairStr = strings.Replace(repairStr, "BASESLINE", "", 1)
   140  		repairStr = strings.Replace(repairStr, "MODESLINE", "", 1)
   141  
   142  		a, err := asserts.Decode([]byte(repairStr))
   143  		if test.expectedErr != "" {
   144  			c.Check(err, ErrorMatches, repairErrPrefix+test.expectedErr)
   145  		} else {
   146  			c.Assert(err, IsNil)
   147  			repair := a.(*asserts.Repair)
   148  			c.Check(repair.Disabled(), Equals, test.dis)
   149  		}
   150  	}
   151  }
   152  
   153  func (s *repairSuite) TestDecodeModesAndBases(c *C) {
   154  	tt := []struct {
   155  		comment  string
   156  		bases    []string
   157  		modes    []string
   158  		expbases []string
   159  		expmodes []string
   160  		err      string
   161  	}{
   162  		// happy uc20+ cases
   163  		{
   164  			comment:  "core20 base with run mode",
   165  			bases:    []string{"core20"},
   166  			modes:    []string{"run"},
   167  			expbases: []string{"core20"},
   168  			expmodes: []string{"run"},
   169  		},
   170  		{
   171  			comment:  "core20 base with recover mode",
   172  			bases:    []string{"core20"},
   173  			modes:    []string{"recover"},
   174  			expbases: []string{"core20"},
   175  			expmodes: []string{"recover"},
   176  		},
   177  		{
   178  			comment:  "core20 base with recover and run modes",
   179  			bases:    []string{"core20"},
   180  			modes:    []string{"recover", "run"},
   181  			expbases: []string{"core20"},
   182  			expmodes: []string{"recover", "run"},
   183  		},
   184  		{
   185  			comment:  "core22 base with run mode",
   186  			bases:    []string{"core22"},
   187  			modes:    []string{"run"},
   188  			expbases: []string{"core22"},
   189  			expmodes: []string{"run"},
   190  		},
   191  		{
   192  			comment:  "core20 and core22 bases with run mode",
   193  			bases:    []string{"core20", "core22"},
   194  			modes:    []string{"run"},
   195  			expbases: []string{"core20", "core22"},
   196  			expmodes: []string{"run"},
   197  		},
   198  		{
   199  			comment:  "core20 and core22 bases with run and recover modes",
   200  			bases:    []string{"core20", "core22"},
   201  			modes:    []string{"run", "recover"},
   202  			expbases: []string{"core20", "core22"},
   203  			expmodes: []string{"run", "recover"},
   204  		},
   205  		{
   206  			comment:  "all bases with run mode (effectively all uc20 bases)",
   207  			modes:    []string{"run"},
   208  			expmodes: []string{"run"},
   209  		},
   210  		{
   211  			comment:  "all bases with recover mode (effectively all uc20 bases)",
   212  			modes:    []string{"recover"},
   213  			expmodes: []string{"recover"},
   214  		},
   215  		{
   216  			comment:  "core20 base with empty modes",
   217  			bases:    []string{"core20"},
   218  			expbases: []string{"core20"},
   219  		},
   220  		{
   221  			comment:  "core22 base with empty modes",
   222  			bases:    []string{"core22"},
   223  			expbases: []string{"core22"},
   224  		},
   225  
   226  		// unhappy uc20 cases
   227  		{
   228  			comment: "core20 base with single invalid mode",
   229  			bases:   []string{"core20"},
   230  			modes:   []string{"not-a-real-uc20-mode"},
   231  			err:     `assertion repair: header \"modes\" contains an invalid element: \"not-a-real-uc20-mode\" \(valid values are run and recover\)`,
   232  		},
   233  		{
   234  			comment: "core20 base with invalid modes",
   235  			bases:   []string{"core20"},
   236  			modes:   []string{"run", "not-a-real-uc20-mode"},
   237  			err:     `assertion repair: header \"modes\" contains an invalid element: \"not-a-real-uc20-mode\" \(valid values are run and recover\)`,
   238  		},
   239  		{
   240  			comment: "core20 base with install mode",
   241  			bases:   []string{"core20"},
   242  			modes:   []string{"install"},
   243  			err:     `assertion repair: header \"modes\" contains an invalid element: \"install\" \(valid values are run and recover\)`,
   244  		},
   245  
   246  		// happy uc18/uc16 cases
   247  		{
   248  			comment:  "core18 base with empty modes",
   249  			bases:    []string{"core18"},
   250  			expbases: []string{"core18"},
   251  		},
   252  		{
   253  			comment:  "core base with empty modes",
   254  			bases:    []string{"core"},
   255  			expbases: []string{"core"},
   256  		},
   257  
   258  		// unhappy uc18/uc16 cases
   259  		{
   260  			comment: "core18 base with non-empty modes",
   261  			bases:   []string{"core18"},
   262  			modes:   []string{"run"},
   263  			err:     "assertion repair: in the presence of a non-empty \"modes\" header, \"bases\" must only contain base snaps supporting recovery modes",
   264  		},
   265  		{
   266  			comment: "core base with non-empty modes",
   267  			bases:   []string{"core"},
   268  			modes:   []string{"run"},
   269  			err:     "assertion repair: in the presence of a non-empty \"modes\" header, \"bases\" must only contain base snaps supporting recovery modes",
   270  		},
   271  		{
   272  			comment: "core16 base with non-empty modes",
   273  			bases:   []string{"core16"},
   274  			modes:   []string{"run"},
   275  			err:     "assertion repair: in the presence of a non-empty \"modes\" header, \"bases\" must only contain base snaps supporting recovery modes",
   276  		},
   277  
   278  		// unhappy non-core specific cases
   279  		{
   280  			comment: "invalid snap name as base",
   281  			bases:   []string{"foo....bar"},
   282  			err:     "assertion repair: invalid snap name \"foo....bar\" in \"bases\"",
   283  		},
   284  	}
   285  
   286  	for _, t := range tt {
   287  		comment := Commentf(t.comment)
   288  		repairStr := strings.Replace(repairExample, "MODELSLINE", s.modelsLine, 1)
   289  		repairStr = strings.Replace(repairStr, "TSLINE", s.tsLine, 1)
   290  
   291  		var basesStr, modesStr string
   292  		if len(t.bases) != 0 {
   293  			basesStr = "bases:\n"
   294  			for _, b := range t.bases {
   295  				basesStr += "  - " + b + "\n"
   296  			}
   297  		}
   298  		if len(t.modes) != 0 {
   299  			modesStr = "modes:\n"
   300  			for _, m := range t.modes {
   301  				modesStr += "  - " + m + "\n"
   302  			}
   303  		}
   304  
   305  		repairStr = strings.Replace(repairStr, "BASESLINE", basesStr, 1)
   306  		repairStr = strings.Replace(repairStr, "MODESLINE", modesStr, 1)
   307  
   308  		assert, err := asserts.Decode([]byte(repairStr))
   309  		if t.err != "" {
   310  			c.Assert(err, ErrorMatches, t.err, comment)
   311  		} else {
   312  			c.Assert(err, IsNil, comment)
   313  			repair, ok := assert.(*asserts.Repair)
   314  			c.Assert(ok, Equals, true, comment)
   315  
   316  			c.Assert(repair.Bases(), DeepEquals, t.expbases, comment)
   317  			c.Assert(repair.Modes(), DeepEquals, t.expmodes, comment)
   318  		}
   319  	}
   320  }
   321  
   322  func (s *repairSuite) TestDecodeInvalid(c *C) {
   323  	invalidTests := []struct{ original, invalid, expectedErr string }{
   324  		{"series:\n  - 16\n", "series: \n", `"series" header must be a list of strings`},
   325  		{"series:\n  - 16\n", "series: something\n", `"series" header must be a list of strings`},
   326  		{"architectures:\n  - amd64\n  - arm64\n", "architectures: foo\n", `"architectures" header must be a list of strings`},
   327  		{"models:\n  - acme/frobinator\n", "models: \n", `"models" header must be a list of strings`},
   328  		{"models:\n  - acme/frobinator\n", "models: something\n", `"models" header must be a list of strings`},
   329  		{"repair-id: 42\n", "repair-id: no-number\n", `"repair-id" header is not an integer: no-number`},
   330  		{"repair-id: 42\n", "repair-id: 0\n", `"repair-id" must be >=1: 0`},
   331  		{"repair-id: 42\n", "repair-id: 01\n", `"repair-id" header has invalid prefix zeros: 01`},
   332  		{"repair-id: 42\n", "repair-id: 99999999999999999999\n", `"repair-id" header is out of range: 99999999999999999999`},
   333  		{"brand-id: acme\n", "brand-id: brand-id-not-eq-authority-id\n", `authority-id and brand-id must match, repair assertions are expected to be signed by the brand: "acme" != "brand-id-not-eq-authority-id"`},
   334  		{"summary: example repair\n", "", `"summary" header is mandatory`},
   335  		{"summary: example repair\n", "summary: \n", `"summary" header should not be empty`},
   336  		{"summary: example repair\n", "summary:\n    multi\n    line\n", `"summary" header cannot have newlines`},
   337  		{s.tsLine, "", `"timestamp" header is mandatory`},
   338  		{s.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
   339  		{s.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
   340  	}
   341  
   342  	for _, test := range invalidTests {
   343  		invalid := strings.Replace(s.repairStr, test.original, test.invalid, 1)
   344  		_, err := asserts.Decode([]byte(invalid))
   345  		c.Check(err, ErrorMatches, repairErrPrefix+test.expectedErr)
   346  	}
   347  }
   348  
   349  // FIXME: move to a different layer later
   350  func (s *repairSuite) TestRepairCanEmbedScripts(c *C) {
   351  	a, err := asserts.Decode([]byte(s.repairStr))
   352  	c.Assert(err, IsNil)
   353  	c.Check(a.Type(), Equals, asserts.RepairType)
   354  	repair := a.(*asserts.Repair)
   355  
   356  	tmpdir := c.MkDir()
   357  	repairScript := filepath.Join(tmpdir, "repair")
   358  	err = ioutil.WriteFile(repairScript, []byte(repair.Body()), 0755)
   359  	c.Assert(err, IsNil)
   360  	cmd := exec.Command(repairScript)
   361  	cmd.Dir = tmpdir
   362  	output, err := cmd.CombinedOutput()
   363  	c.Check(err, IsNil)
   364  	c.Check(string(output), Equals, `Unpack embedded payload
   365  hello from the inside
   366  `)
   367  }