github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/kernel/fde/fde_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 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 fde_test 21 22 import ( 23 "encoding/base64" 24 "encoding/json" 25 "errors" 26 "fmt" 27 "io/ioutil" 28 "math/rand" 29 "os" 30 "os/exec" 31 "path/filepath" 32 "testing" 33 "time" 34 35 . "gopkg.in/check.v1" 36 37 "github.com/snapcore/snapd/dirs" 38 "github.com/snapcore/snapd/kernel/fde" 39 "github.com/snapcore/snapd/osutil" 40 "github.com/snapcore/snapd/testutil" 41 ) 42 43 func TestFde(t *testing.T) { TestingT(t) } 44 45 type fdeSuite struct { 46 testutil.BaseTest 47 } 48 49 var _ = Suite(&fdeSuite{}) 50 51 func (s *fdeSuite) SetUpTest(c *C) { 52 dirs.SetRootDir(c.MkDir()) 53 s.AddCleanup(func() { dirs.SetRootDir("") }) 54 } 55 56 func (s *fdeSuite) TestHasRevealKey(c *C) { 57 oldPath := os.Getenv("PATH") 58 defer func() { os.Setenv("PATH", oldPath) }() 59 60 mockRoot := c.MkDir() 61 os.Setenv("PATH", mockRoot+"/bin") 62 mockBin := mockRoot + "/bin/" 63 err := os.Mkdir(mockBin, 0755) 64 c.Assert(err, IsNil) 65 66 // no fde-reveal-key binary 67 c.Check(fde.HasRevealKey(), Equals, false) 68 69 // fde-reveal-key without +x 70 err = ioutil.WriteFile(mockBin+"fde-reveal-key", nil, 0644) 71 c.Assert(err, IsNil) 72 c.Check(fde.HasRevealKey(), Equals, false) 73 74 // correct fde-reveal-key, no logging 75 err = os.Chmod(mockBin+"fde-reveal-key", 0755) 76 c.Assert(err, IsNil) 77 } 78 79 func (s *fdeSuite) TestInitialSetupV2(c *C) { 80 mockKey := []byte{1, 2, 3, 4} 81 82 runSetupHook := func(req *fde.SetupRequest) ([]byte, error) { 83 c.Check(req, DeepEquals, &fde.SetupRequest{ 84 Op: "initial-setup", 85 Key: mockKey, 86 KeyName: "some-key-name", 87 }) 88 // sealed-key/handle 89 mockJSON := fmt.Sprintf(`{"sealed-key":"%s", "handle":{"some":"handle"}}`, base64.StdEncoding.EncodeToString([]byte("the-encrypted-key"))) 90 return []byte(mockJSON), nil 91 } 92 93 params := &fde.InitialSetupParams{ 94 Key: mockKey, 95 KeyName: "some-key-name", 96 } 97 res, err := fde.InitialSetup(runSetupHook, params) 98 c.Assert(err, IsNil) 99 expectedHandle := json.RawMessage([]byte(`{"some":"handle"}`)) 100 c.Check(res, DeepEquals, &fde.InitialSetupResult{ 101 EncryptedKey: []byte("the-encrypted-key"), 102 Handle: &expectedHandle, 103 }) 104 } 105 106 func (s *fdeSuite) TestInitialSetupError(c *C) { 107 mockKey := []byte{1, 2, 3, 4} 108 109 errHook := errors.New("hook running error") 110 runSetupHook := func(req *fde.SetupRequest) ([]byte, error) { 111 c.Check(req, DeepEquals, &fde.SetupRequest{ 112 Op: "initial-setup", 113 Key: mockKey, 114 KeyName: "some-key-name", 115 }) 116 return nil, errHook 117 } 118 119 params := &fde.InitialSetupParams{ 120 Key: mockKey, 121 KeyName: "some-key-name", 122 } 123 _, err := fde.InitialSetup(runSetupHook, params) 124 c.Check(err, Equals, errHook) 125 } 126 127 func (s *fdeSuite) TestInitialSetupV1(c *C) { 128 mockKey := []byte{1, 2, 3, 4} 129 130 runSetupHook := func(req *fde.SetupRequest) ([]byte, error) { 131 c.Check(req, DeepEquals, &fde.SetupRequest{ 132 Op: "initial-setup", 133 Key: mockKey, 134 KeyName: "some-key-name", 135 }) 136 // needs the USK$ prefix to simulate v1 key 137 return []byte("USK$sealed-key"), nil 138 } 139 140 params := &fde.InitialSetupParams{ 141 Key: mockKey, 142 KeyName: "some-key-name", 143 } 144 res, err := fde.InitialSetup(runSetupHook, params) 145 c.Assert(err, IsNil) 146 expectedHandle := json.RawMessage(`{"v1-no-handle":true}`) 147 c.Assert(json.Valid(expectedHandle), Equals, true) 148 c.Check(res, DeepEquals, &fde.InitialSetupResult{ 149 EncryptedKey: []byte("USK$sealed-key"), 150 Handle: &expectedHandle, 151 }) 152 } 153 154 func (s *fdeSuite) TestInitialSetupBadJSON(c *C) { 155 mockKey := []byte{1, 2, 3, 4} 156 157 runSetupHook := func(req *fde.SetupRequest) ([]byte, error) { 158 return []byte("bad json"), nil 159 } 160 161 params := &fde.InitialSetupParams{ 162 Key: mockKey, 163 KeyName: "some-key-name", 164 } 165 _, err := fde.InitialSetup(runSetupHook, params) 166 c.Check(err, ErrorMatches, `cannot decode hook output "bad json": invalid char.*`) 167 } 168 169 func checkSystemdRunOrSkip(c *C) { 170 // this test uses a real systemd-run --user so check here if that 171 // actually works 172 if output, err := exec.Command("systemd-run", "--user", "--wait", "--collect", "--service-type=exec", "/bin/true").CombinedOutput(); err != nil { 173 c.Skip(fmt.Sprintf("systemd-run not working: %v", osutil.OutputErr(output, err))) 174 } 175 176 } 177 178 func (s *fdeSuite) TestLockSealedKeysCallsFdeReveal(c *C) { 179 checkSystemdRunOrSkip(c) 180 181 restore := fde.MockFdeRevealKeyCommandExtra([]string{"--user"}) 182 defer restore() 183 fdeRevealKeyStdin := filepath.Join(c.MkDir(), "stdin") 184 mockSystemdRun := testutil.MockCommand(c, "fde-reveal-key", fmt.Sprintf(` 185 cat - > %s 186 `, fdeRevealKeyStdin)) 187 defer mockSystemdRun.Restore() 188 189 err := fde.LockSealedKeys() 190 c.Assert(err, IsNil) 191 c.Check(mockSystemdRun.Calls(), DeepEquals, [][]string{ 192 {"fde-reveal-key"}, 193 }) 194 c.Check(fdeRevealKeyStdin, testutil.FileEquals, `{"op":"lock"}`) 195 196 // ensure no tmp files are left behind 197 c.Check(osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key")), Equals, false) 198 } 199 200 func (s *fdeSuite) TestLockSealedKeysHonorsRuntimeMax(c *C) { 201 checkSystemdRunOrSkip(c) 202 203 restore := fde.MockFdeRevealKeyCommandExtra([]string{"--user"}) 204 defer restore() 205 mockSystemdRun := testutil.MockCommand(c, "fde-reveal-key", "sleep 60") 206 defer mockSystemdRun.Restore() 207 208 restore = fde.MockFdeRevealKeyPollWaitParanoiaFactor(100) 209 defer restore() 210 211 restore = fde.MockFdeRevealKeyRuntimeMax(100 * time.Millisecond) 212 defer restore() 213 214 err := fde.LockSealedKeys() 215 c.Assert(err, ErrorMatches, `cannot run fde-reveal-key "lock": service result: timeout`) 216 } 217 218 func (s *fdeSuite) TestLockSealedKeysHonorsParanoia(c *C) { 219 checkSystemdRunOrSkip(c) 220 221 restore := fde.MockFdeRevealKeyCommandExtra([]string{"--user"}) 222 defer restore() 223 mockSystemdRun := testutil.MockCommand(c, "fde-reveal-key", "sleep 60") 224 defer mockSystemdRun.Restore() 225 226 restore = fde.MockFdeRevealKeyPollWaitParanoiaFactor(1) 227 defer restore() 228 229 // shorter than the fdeRevealKeyPollWait time 230 restore = fde.MockFdeRevealKeyRuntimeMax(1 * time.Millisecond) 231 defer restore() 232 233 err := fde.LockSealedKeys() 234 c.Assert(err, ErrorMatches, `cannot run fde-reveal-key "lock": internal error: systemd-run did not honor RuntimeMax=1ms setting`) 235 } 236 237 func (s *fdeSuite) TestReveal(c *C) { 238 checkSystemdRunOrSkip(c) 239 240 // fix randutil outcome 241 rand.Seed(1) 242 243 sealedKey := []byte("sealed-v2-payload") 244 v2payload := []byte("unsealed-v2-payload") 245 246 restore := fde.MockFdeRevealKeyCommandExtra([]string{"--user"}) 247 defer restore() 248 fdeRevealKeyStdin := filepath.Join(c.MkDir(), "stdin") 249 mockSystemdRun := testutil.MockCommand(c, "fde-reveal-key", fmt.Sprintf(` 250 cat - > %s 251 printf '{"key": "%s"}' 252 `, fdeRevealKeyStdin, base64.StdEncoding.EncodeToString(v2payload))) 253 defer mockSystemdRun.Restore() 254 255 handle := json.RawMessage(`{"some": "handle"}`) 256 p := fde.RevealParams{ 257 SealedKey: sealedKey, 258 Handle: &handle, 259 V2Payload: true, 260 } 261 res, err := fde.Reveal(&p) 262 c.Assert(err, IsNil) 263 c.Check(res, DeepEquals, v2payload) 264 c.Check(mockSystemdRun.Calls(), DeepEquals, [][]string{ 265 {"fde-reveal-key"}, 266 }) 267 c.Check(fdeRevealKeyStdin, testutil.FileEquals, fmt.Sprintf(`{"op":"reveal","sealed-key":%q,"handle":{"some":"handle"},"key-name":"deprecated-pw7MpXh0JB4P"}`, base64.StdEncoding.EncodeToString(sealedKey))) 268 269 // ensure no tmp files are left behind 270 c.Check(osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key")), Equals, false) 271 } 272 273 func (s *fdeSuite) TestRevealV1(c *C) { 274 // this test that v1 hooks and raw binary v1 created sealedKey files still work 275 checkSystemdRunOrSkip(c) 276 277 // fix randutil outcome 278 rand.Seed(1) 279 280 restore := fde.MockFdeRevealKeyCommandExtra([]string{"--user"}) 281 defer restore() 282 fdeRevealKeyStdin := filepath.Join(c.MkDir(), "stdin") 283 mockSystemdRun := testutil.MockCommand(c, "fde-reveal-key", fmt.Sprintf(` 284 cat - > %s 285 printf "unsealed-key-64-chars-long-when-not-json-to-match-denver-project" 286 `, fdeRevealKeyStdin)) 287 defer mockSystemdRun.Restore() 288 289 sealedKey := []byte("sealed-key") 290 p := fde.RevealParams{ 291 SealedKey: sealedKey, 292 } 293 res, err := fde.Reveal(&p) 294 c.Assert(err, IsNil) 295 c.Check(res, DeepEquals, []byte("unsealed-key-64-chars-long-when-not-json-to-match-denver-project")) 296 c.Check(mockSystemdRun.Calls(), DeepEquals, [][]string{ 297 {"fde-reveal-key"}, 298 }) 299 c.Check(fdeRevealKeyStdin, testutil.FileEquals, fmt.Sprintf(`{"op":"reveal","sealed-key":%q,"key-name":"deprecated-pw7MpXh0JB4P"}`, base64.StdEncoding.EncodeToString([]byte("sealed-key")))) 300 301 // ensure no tmp files are left behind 302 c.Check(osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key")), Equals, false) 303 } 304 305 func (s *fdeSuite) TestRevealV2PayloadV1Hook(c *C) { 306 checkSystemdRunOrSkip(c) 307 308 // fix randutil outcome 309 rand.Seed(1) 310 311 sealedKey := []byte("sealed-v2-payload") 312 v2payload := []byte("unsealed-v2-payload") 313 314 restore := fde.MockFdeRevealKeyCommandExtra([]string{"--user"}) 315 defer restore() 316 fdeRevealKeyStdin := filepath.Join(c.MkDir(), "stdin") 317 mockSystemdRun := testutil.MockCommand(c, "fde-reveal-key", fmt.Sprintf(` 318 cat - > %s 319 printf %q 320 `, fdeRevealKeyStdin, v2payload)) 321 defer mockSystemdRun.Restore() 322 323 handle := json.RawMessage(`{"v1-no-handle":true}`) 324 p := fde.RevealParams{ 325 SealedKey: sealedKey, 326 Handle: &handle, 327 V2Payload: true, 328 } 329 res, err := fde.Reveal(&p) 330 c.Assert(err, IsNil) 331 c.Check(res, DeepEquals, v2payload) 332 c.Check(mockSystemdRun.Calls(), DeepEquals, [][]string{ 333 {"fde-reveal-key"}, 334 }) 335 c.Check(fdeRevealKeyStdin, testutil.FileEquals, fmt.Sprintf(`{"op":"reveal","sealed-key":%q,"key-name":"deprecated-pw7MpXh0JB4P"}`, base64.StdEncoding.EncodeToString(sealedKey))) 336 337 // ensure no tmp files are left behind 338 c.Check(osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key")), Equals, false) 339 } 340 341 func (s *fdeSuite) TestRevealV2BadJSON(c *C) { 342 // we need let higher level deal with this 343 checkSystemdRunOrSkip(c) 344 345 // fix randutil outcome 346 rand.Seed(1) 347 348 sealedKey := []byte("sealed-v2-payload") 349 350 restore := fde.MockFdeRevealKeyCommandExtra([]string{"--user"}) 351 defer restore() 352 fdeRevealKeyStdin := filepath.Join(c.MkDir(), "stdin") 353 mockSystemdRun := testutil.MockCommand(c, "fde-reveal-key", fmt.Sprintf(` 354 cat - > %s 355 printf 'invalid-json' 356 `, fdeRevealKeyStdin)) 357 defer mockSystemdRun.Restore() 358 359 handle := json.RawMessage(`{"some": "handle"}`) 360 p := fde.RevealParams{ 361 SealedKey: sealedKey, 362 Handle: &handle, 363 V2Payload: true, 364 } 365 res, err := fde.Reveal(&p) 366 c.Assert(err, IsNil) 367 // we just get the bad json out 368 c.Check(res, DeepEquals, []byte("invalid-json")) 369 c.Check(mockSystemdRun.Calls(), DeepEquals, [][]string{ 370 {"fde-reveal-key"}, 371 }) 372 c.Check(fdeRevealKeyStdin, testutil.FileEquals, fmt.Sprintf(`{"op":"reveal","sealed-key":%q,"handle":{"some":"handle"},"key-name":"deprecated-pw7MpXh0JB4P"}`, base64.StdEncoding.EncodeToString(sealedKey))) 373 374 // ensure no tmp files are left behind 375 c.Check(osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key")), Equals, false) 376 } 377 378 func (s *fdeSuite) TestRevealV1BadOutputSize(c *C) { 379 checkSystemdRunOrSkip(c) 380 381 // fix randutil outcome 382 rand.Seed(1) 383 384 restore := fde.MockFdeRevealKeyCommandExtra([]string{"--user"}) 385 defer restore() 386 fdeRevealKeyStdin := filepath.Join(c.MkDir(), "stdin") 387 mockSystemdRun := testutil.MockCommand(c, "fde-reveal-key", fmt.Sprintf(` 388 cat - > %s 389 printf "bad-size" 390 `, fdeRevealKeyStdin)) 391 defer mockSystemdRun.Restore() 392 393 sealedKey := []byte("sealed-key") 394 p := fde.RevealParams{ 395 SealedKey: sealedKey, 396 } 397 _, err := fde.Reveal(&p) 398 c.Assert(err, ErrorMatches, `cannot decode fde-reveal-key \"reveal\" result: .*`) 399 400 c.Check(osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key")), Equals, false) 401 } 402 403 func (s *fdeSuite) TestedRevealTruncatesStreamFiles(c *C) { 404 checkSystemdRunOrSkip(c) 405 406 // fix randutil outcome 407 rand.Seed(1) 408 409 // create the temporary output file streams with garbage data to ensure that 410 // by the time the hook runs the files are emptied and recreated with the 411 // right permissions 412 streamFiles := []string{} 413 for _, stream := range []string{"stdin", "stdout", "stderr"} { 414 streamFile := filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key/fde-reveal-key."+stream) 415 streamFiles = append(streamFiles, streamFile) 416 // make the dir 0700 417 err := os.MkdirAll(filepath.Dir(streamFile), 0700) 418 c.Assert(err, IsNil) 419 // but make the file world-readable as it should be reset to 0600 before 420 // the hook is run 421 err = ioutil.WriteFile(streamFile, []byte("blah blah blah blah blah blah blah blah blah blah"), 0755) 422 c.Assert(err, IsNil) 423 } 424 425 // the hook script only verifies that the stdout file is empty since we 426 // need to write to the stderr file for performing the test, but we still 427 // check the stderr file for correct permissions 428 mockSystemdRun := testutil.MockCommand(c, "fde-reveal-key", fmt.Sprintf(` 429 # check that stdin has the right sealed key content 430 if [ "$(cat %[1]s)" != "{\"op\":\"reveal\",\"sealed-key\":\"AQIDBA==\",\"key-name\":\"deprecated-pw7MpXh0JB4P\"}" ]; then 431 echo "test failed: stdin file has wrong content: $(cat %[1]s)" 1>&2 432 else 433 echo "stdin file has correct content" 1>&2 434 fi 435 436 # check that stdout is empty 437 if [ -n "$(cat %[2]s)" ]; then 438 echo "test failed: stdout file is not empty: $(cat %[2]s)" 1>&2 439 else 440 echo "stdout file is correctly empty" 1>&2 441 fi 442 443 # check that stdin has the right 600 perms 444 if [ "$(stat --format=%%a %[1]s)" != "600" ]; then 445 echo "test failed: stdin file has wrong permissions: $(stat --format=%%a %[1]s)" 1>&2 446 else 447 echo "stdin file has correct 600 permissions" 1>&2 448 fi 449 450 # check that stdout has the right 600 perms 451 if [ "$(stat --format=%%a %[2]s)" != "600" ]; then 452 echo "test failed: stdout file has wrong permissions: $(stat --format=%%a %[2]s)" 1>&2 453 else 454 echo "stdout file has correct 600 permissions" 1>&2 455 fi 456 457 # check that stderr has the right 600 perms 458 if [ "$(stat --format=%%a %[3]s)" != "600" ]; then 459 echo "test failed: stderr file has wrong permissions: $(stat --format=%%a %[3]s)" 1>&2 460 else 461 echo "stderr file has correct 600 permissions" 1>&2 462 fi 463 464 echo "making the hook always fail for simpler test code" 1>&2 465 466 # always make the hook exit 1 for simpler test code 467 exit 1 468 `, streamFiles[0], streamFiles[1], streamFiles[2])) 469 defer mockSystemdRun.Restore() 470 restore := fde.MockFdeRevealKeyCommandExtra([]string{"--user"}) 471 defer restore() 472 473 sealedKey := []byte{1, 2, 3, 4} 474 p := fde.RevealParams{ 475 SealedKey: sealedKey, 476 } 477 _, err := fde.Reveal(&p) 478 c.Assert(err, ErrorMatches, `(?s)cannot run fde-reveal-key "reveal": 479 ----- 480 stdin file has correct content 481 stdout file is correctly empty 482 stdin file has correct 600 permissions 483 stdout file has correct 600 permissions 484 stderr file has correct 600 permissions 485 making the hook always fail for simpler test code 486 service result: exit-code 487 -----`) 488 // ensure no tmp files are left behind 489 c.Check(osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key")), Equals, false) 490 } 491 492 func (s *fdeSuite) TestRevealErr(c *C) { 493 checkSystemdRunOrSkip(c) 494 495 // fix randutil outcome 496 rand.Seed(1) 497 498 mockSystemdRun := testutil.MockCommand(c, "fde-reveal-key", `echo failed 1>&2; false`) 499 defer mockSystemdRun.Restore() 500 restore := fde.MockFdeRevealKeyCommandExtra([]string{"--user"}) 501 defer restore() 502 503 sealedKey := []byte{1, 2, 3, 4} 504 p := fde.RevealParams{ 505 SealedKey: sealedKey, 506 } 507 _, err := fde.Reveal(&p) 508 c.Assert(err, ErrorMatches, `(?s)cannot run fde-reveal-key "reveal": 509 ----- 510 failed 511 service result: exit-code 512 -----`) 513 // ensure no tmp files are left behind 514 c.Check(osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key")), Equals, false) 515 }