github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/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 }