github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/kernel/fde/reveal_key.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
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"os"
    28  	"os/exec"
    29  	"path/filepath"
    30  	"time"
    31  
    32  	"github.com/snapcore/snapd/dirs"
    33  	"github.com/snapcore/snapd/logger"
    34  	"github.com/snapcore/snapd/osutil"
    35  	"github.com/snapcore/snapd/randutil"
    36  )
    37  
    38  // RevealKeyRequest carries the operation parameters to the fde-reavel-key
    39  // helper that receives them serialized over stdin.
    40  type RevealKeyRequest struct {
    41  	Op string `json:"op"`
    42  
    43  	SealedKey []byte           `json:"sealed-key,omitempty"`
    44  	Handle    *json.RawMessage `json:"handle,omitempty"`
    45  	// deprecated for v1
    46  	KeyName string `json:"key-name,omitempty"`
    47  
    48  	// TODO: add VolumeName,SourceDevicePath later
    49  }
    50  
    51  // fdeRevealKeyRuntimeMax is the maximum runtime a fde-reveal-key can execute
    52  // XXX: what is a reasonable default here?
    53  var fdeRevealKeyRuntimeMax = 2 * time.Minute
    54  
    55  // 50 ms means we check at a frequency 20 Hz, fast enough to not hold
    56  // up boot, but not too fast that we are hogging the CPU from the
    57  // thing we are waiting to finish running
    58  var fdeRevealKeyPollWait = 50 * time.Millisecond
    59  
    60  // fdeRevealKeyPollWaitParanoiaFactor controls much longer we wait
    61  // then fdeRevealKeyRuntimeMax before stopping to poll for results
    62  var fdeRevealKeyPollWaitParanoiaFactor = 2
    63  
    64  // overridden in tests
    65  var fdeRevealKeyCommandExtra []string
    66  
    67  // runFDERevealKeyCommand returns the output of fde-reveal-key run
    68  // with systemd.
    69  //
    70  // Note that systemd-run in the initrd can only talk to the private
    71  // systemd bus so this cannot use "--pipe" or "--wait", see
    72  // https://github.com/snapcore/core-initrd/issues/13
    73  func runFDERevealKeyCommand(req *RevealKeyRequest) (output []byte, err error) {
    74  	stdin, err := json.Marshal(req)
    75  	if err != nil {
    76  		return nil, fmt.Errorf(`cannot build request for fde-reveal-key %q: %v`, req.Op, err)
    77  	}
    78  
    79  	runDir := filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key")
    80  	if err := os.MkdirAll(runDir, 0700); err != nil {
    81  		return nil, fmt.Errorf("cannot create tmp dir for fde-reveal-key: %v", err)
    82  	}
    83  
    84  	// delete and re-create the std{in,out,err} stream files that we use for the
    85  	// hook to be robust against bugs where the files are created with too
    86  	// permissive permissions or not properly deleted afterwards since the hook
    87  	// will be invoked multiple times during the initrd and we want to be really
    88  	// careful since the stdout file will contain the unsealed encryption key
    89  	for _, stream := range []string{"stdin", "stdout", "stderr"} {
    90  		streamFile := filepath.Join(runDir, "fde-reveal-key."+stream)
    91  		// we want to make sure that the file permissions for stdout are always
    92  		// 0600, so to ensure this is the case and be robust against bugs, we
    93  		// always delete the file and re-create it with 0600
    94  
    95  		// note that if the file already exists, WriteFile will not change the
    96  		// permissions, so deleting first is the right thing to do
    97  		os.Remove(streamFile)
    98  		if stream == "stdin" {
    99  			err = ioutil.WriteFile(streamFile, stdin, 0600)
   100  		} else {
   101  			err = ioutil.WriteFile(streamFile, nil, 0600)
   102  		}
   103  		if err != nil {
   104  			return nil, fmt.Errorf("cannot create %s for fde-reveal-key: %v", stream, err)
   105  		}
   106  	}
   107  
   108  	// TODO: put this into a new "systemd/run" package
   109  	cmd := exec.Command(
   110  		"systemd-run",
   111  		"--collect",
   112  		"--service-type=exec",
   113  		"--quiet",
   114  		// ensure we get some result from the hook within a
   115  		// reasonable timeout and output from systemd if
   116  		// things go wrong
   117  		fmt.Sprintf("--property=RuntimeMaxSec=%s", fdeRevealKeyRuntimeMax),
   118  		// Do not allow mounting, this ensures hooks in initrd
   119  		// can not mess around with ubuntu-data.
   120  		//
   121  		// Note that this is not about perfect confinement, more about
   122  		// making sure that people using the hook know that we do not
   123  		// want them to mess around outside of just providing unseal.
   124  		"--property=SystemCallFilter=~@mount",
   125  		// WORKAROUNDS
   126  		// workaround the lack of "--pipe"
   127  		fmt.Sprintf("--property=StandardInput=file:%s/fde-reveal-key.stdin", runDir),
   128  		// NOTE: these files are manually created above with 0600 because by
   129  		// default systemd will create them 0644 and we want to be paranoid here
   130  		fmt.Sprintf("--property=StandardOutput=file:%s/fde-reveal-key.stdout", runDir),
   131  		fmt.Sprintf("--property=StandardError=file:%s/fde-reveal-key.stderr", runDir),
   132  		// this ensures we get useful output for e.g. segfaults
   133  		fmt.Sprintf(`--property=ExecStopPost=/bin/sh -c 'if [ "$EXIT_STATUS" = 0 ]; then touch %[1]s/fde-reveal-key.success; else echo "service result: $SERVICE_RESULT" >%[1]s/fde-reveal-key.failed; fi'`, runDir),
   134  	)
   135  	if fdeRevealKeyCommandExtra != nil {
   136  		cmd.Args = append(cmd.Args, fdeRevealKeyCommandExtra...)
   137  	}
   138  	// fde-reveal-key is what we actually need to run
   139  	cmd.Args = append(cmd.Args, "fde-reveal-key")
   140  
   141  	// ensure we cleanup our tmp files
   142  	defer func() {
   143  		if err := os.RemoveAll(runDir); err != nil {
   144  			logger.Noticef("cannot remove tmp dir: %v", err)
   145  		}
   146  	}()
   147  
   148  	// run the command
   149  	output, err = cmd.CombinedOutput()
   150  	if err != nil {
   151  		return output, err
   152  	}
   153  
   154  	// This loop will be terminate by systemd-run, either because
   155  	// fde-reveal-key exists or it gets killed when it reaches the
   156  	// fdeRevealKeyRuntimeMax defined above.
   157  	//
   158  	// However we are paranoid and exit this loop if systemd
   159  	// did not terminate the process after twice the allocated
   160  	// runtime
   161  	maxLoops := int(fdeRevealKeyRuntimeMax/fdeRevealKeyPollWait) * fdeRevealKeyPollWaitParanoiaFactor
   162  	for i := 0; i < maxLoops; i++ {
   163  		switch {
   164  		case osutil.FileExists(filepath.Join(runDir, "fde-reveal-key.failed")):
   165  			stderr, _ := ioutil.ReadFile(filepath.Join(runDir, "fde-reveal-key.stderr"))
   166  			systemdErr, _ := ioutil.ReadFile(filepath.Join(runDir, "fde-reveal-key.failed"))
   167  			buf := bytes.NewBuffer(stderr)
   168  			buf.Write(systemdErr)
   169  			return buf.Bytes(), fmt.Errorf("fde-reveal-key failed")
   170  		case osutil.FileExists(filepath.Join(runDir, "fde-reveal-key.success")):
   171  			return ioutil.ReadFile(filepath.Join(runDir, "fde-reveal-key.stdout"))
   172  		default:
   173  			time.Sleep(fdeRevealKeyPollWait)
   174  		}
   175  	}
   176  
   177  	// this should never happen, the loop above should be terminated
   178  	// via systemd
   179  	return nil, fmt.Errorf("internal error: systemd-run did not honor RuntimeMax=%s setting", fdeRevealKeyRuntimeMax)
   180  }
   181  
   182  var runFDERevealKey = runFDERevealKeyCommand
   183  
   184  func MockRunFDERevealKey(mock func(*RevealKeyRequest) ([]byte, error)) (restore func()) {
   185  	oldRunFDERevealKey := runFDERevealKey
   186  	runFDERevealKey = mock
   187  	return func() {
   188  		runFDERevealKey = oldRunFDERevealKey
   189  	}
   190  }
   191  
   192  func LockSealedKeys() error {
   193  	req := &RevealKeyRequest{
   194  		Op: "lock",
   195  	}
   196  	if output, err := runFDERevealKey(req); err != nil {
   197  		return fmt.Errorf(`cannot run fde-reveal-key "lock": %v`, osutil.OutputErr(output, err))
   198  	}
   199  
   200  	return nil
   201  }
   202  
   203  // RevealParams contains the parameters for fde-reveal-key reveal operation.
   204  type RevealParams struct {
   205  	SealedKey []byte
   206  	Handle    *json.RawMessage
   207  	// V2Payload is set true if SealedKey is expected to contain a v2 payload
   208  	// (disk key + aux key)
   209  	V2Payload bool
   210  }
   211  
   212  type revealKeyResult struct {
   213  	Key []byte `json:"key"`
   214  }
   215  
   216  const (
   217  	v1keySize  = 64
   218  	v1NoHandle = `{"v1-no-handle":true}`
   219  )
   220  
   221  // Reveal invokes the fde-reveal-key reveal operation.
   222  func Reveal(params *RevealParams) (payload []byte, err error) {
   223  	handle := params.Handle
   224  	if params.V2Payload && handle != nil && bytes.Equal([]byte(*handle), []byte(v1NoHandle)) {
   225  		handle = nil
   226  	}
   227  	req := &RevealKeyRequest{
   228  		Op:        "reveal",
   229  		SealedKey: params.SealedKey,
   230  		Handle:    handle,
   231  		// deprecated but needed for v1 hooks
   232  		KeyName: "deprecated-" + randutil.RandomString(12),
   233  	}
   234  	output, err := runFDERevealKey(req)
   235  	if err != nil {
   236  		return nil, fmt.Errorf(`cannot run fde-reveal-key "reveal": %v`, osutil.OutputErr(output, err))
   237  	}
   238  	// We expect json output that fits the revealKeyResult json at
   239  	// this point. However the "denver" project uses the old and
   240  	// deprecated v1 API that returns raw bytes and we still need
   241  	// to support this.
   242  	var res revealKeyResult
   243  	if err := json.Unmarshal(output, &res); err != nil {
   244  		if params.V2Payload {
   245  			// We expect a v2 payload but not having json
   246  			// output from the hook means that either the
   247  			// hook is buggy or we have a v1 based hook
   248  			// (e.g. "denver" project) with v2 based json
   249  			// data on disk. This is supported but we let
   250  			// the higher levels unmarshaling of the
   251  			// payload deal with the buggy case.
   252  			return output, nil
   253  		}
   254  		// If the payload is not expected to be v2 and, the
   255  		// output is not json but matches the size of the
   256  		// "denver" project encrypton key (64 bytes) we assume
   257  		// we deal with a v1 API.
   258  		if len(output) != v1keySize {
   259  			return nil, fmt.Errorf(`cannot decode fde-reveal-key "reveal" result: %v`, err)
   260  		}
   261  		return output, nil
   262  	}
   263  	return res.Key, nil
   264  }