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