github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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  }