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 }