/
secboot_tpm.go
545 lines (472 loc) · 17.6 KB
/
secboot_tpm.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
// -*- Mode: Go; indent-tabs-mode: t -*-
// +build !nosecboot
/*
* Copyright (C) 2020 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package secboot
import (
"crypto/rand"
"errors"
"fmt"
"io/ioutil"
"github.com/canonical/go-tpm2"
sb "github.com/snapcore/secboot"
"golang.org/x/xerrors"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/bootloader/efi"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/randutil"
"github.com/snapcore/snapd/snap/snapfile"
)
const (
keyringPrefix = "ubuntu-fde"
)
var (
sbConnectToDefaultTPM = sb.ConnectToDefaultTPM
sbMeasureSnapSystemEpochToTPM = sb.MeasureSnapSystemEpochToTPM
sbMeasureSnapModelToTPM = sb.MeasureSnapModelToTPM
sbBlockPCRProtectionPolicies = sb.BlockPCRProtectionPolicies
sbActivateVolumeWithTPMSealedKey = sb.ActivateVolumeWithTPMSealedKey
sbAddEFISecureBootPolicyProfile = sb.AddEFISecureBootPolicyProfile
sbAddEFIBootManagerProfile = sb.AddEFIBootManagerProfile
sbAddSystemdEFIStubProfile = sb.AddSystemdEFIStubProfile
sbAddSnapModelProfile = sb.AddSnapModelProfile
sbSealKeyToTPMMultiple = sb.SealKeyToTPMMultiple
sbUpdateKeyPCRProtectionPolicyMultiple = sb.UpdateKeyPCRProtectionPolicyMultiple
randutilRandomKernelUUID = randutil.RandomKernelUUID
isTPMEnabled = isTPMEnabledImpl
provisionTPM = provisionTPMImpl
// dummy to check whether the interfaces match
_ (sb.SnapModel) = ModelForSealing(nil)
)
func isTPMEnabledImpl(tpm *sb.TPMConnection) bool {
return tpm.IsEnabled()
}
func CheckTPMKeySealingSupported() error {
logger.Noticef("checking if secure boot is enabled...")
if err := checkSecureBootEnabled(); err != nil {
logger.Noticef("secure boot not enabled: %v", err)
return err
}
logger.Noticef("secure boot is enabled")
logger.Noticef("checking if TPM device is available...")
tpm, err := sbConnectToDefaultTPM()
if err != nil {
err = fmt.Errorf("cannot connect to TPM device: %v", err)
logger.Noticef("%v", err)
return err
}
defer tpm.Close()
if !isTPMEnabled(tpm) {
logger.Noticef("TPM device detected but not enabled")
return fmt.Errorf("TPM device is not enabled")
}
logger.Noticef("TPM device detected and enabled")
return nil
}
func checkSecureBootEnabled() error {
// 8be4df61-93ca-11d2-aa0d-00e098032b8c is the EFI Global Variable vendor GUID
b, _, err := efi.ReadVarBytes("SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c")
if err != nil {
if err == efi.ErrNoEFISystem {
return err
}
return fmt.Errorf("cannot read secure boot variable: %v", err)
}
if len(b) < 1 {
return errors.New("secure boot variable does not exist")
}
if b[0] != 1 {
return errors.New("secure boot is disabled")
}
return nil
}
// initramfsPCR is the TPM PCR that we reserve for the EFI image and use
// for measurement from the initramfs.
const initramfsPCR = 12
func insecureConnectToTPM() (*sb.TPMConnection, error) {
return sbConnectToDefaultTPM()
}
func measureWhenPossible(whatHow func(tpm *sb.TPMConnection) error) error {
// the model is ready, we're good to try measuring it now
tpm, err := insecureConnectToTPM()
if err != nil {
if xerrors.Is(err, sb.ErrNoTPM2Device) {
return nil
}
return fmt.Errorf("cannot open TPM connection: %v", err)
}
defer tpm.Close()
if !isTPMEnabled(tpm) {
return nil
}
return whatHow(tpm)
}
// MeasureSnapSystemEpochWhenPossible measures the snap system epoch only if the
// TPM device is available. If there's no TPM device success is returned.
func MeasureSnapSystemEpochWhenPossible() error {
measure := func(tpm *sb.TPMConnection) error {
return sbMeasureSnapSystemEpochToTPM(tpm, initramfsPCR)
}
if err := measureWhenPossible(measure); err != nil {
return fmt.Errorf("cannot measure snap system epoch: %v", err)
}
return nil
}
// MeasureSnapModelWhenPossible measures the snap model only if the TPM device is
// available. If there's no TPM device success is returned.
func MeasureSnapModelWhenPossible(findModel func() (*asserts.Model, error)) error {
measure := func(tpm *sb.TPMConnection) error {
model, err := findModel()
if err != nil {
return err
}
return sbMeasureSnapModelToTPM(tpm, initramfsPCR, model)
}
if err := measureWhenPossible(measure); err != nil {
return fmt.Errorf("cannot measure snap model: %v", err)
}
return nil
}
func lockTPMSealedKeys() error {
tpm, tpmErr := sbConnectToDefaultTPM()
if tpmErr != nil {
if xerrors.Is(tpmErr, sb.ErrNoTPM2Device) {
logger.Noticef("cannot open TPM connection: %v", tpmErr)
return nil
}
return fmt.Errorf("cannot lock TPM: %v", tpmErr)
}
defer tpm.Close()
// Lock access to the sealed keys. This should be called whenever there
// is a TPM device detected, regardless of whether secure boot is enabled
// or there is an encrypted volume to unlock. Note that snap-bootstrap can
// be called several times during initialization, and if there are multiple
// volumes to unlock we should lock access to the sealed keys only after
// the last encrypted volume is unlocked, in which case lockKeysOnFinish
// should be set to true.
//
// We should only touch the PCR that we've currently reserved for the kernel
// EFI image. Touching others will break the ability to perform any kind of
// attestation using the TPM because it will make the log inconsistent.
return sbBlockPCRProtectionPolicies(tpm, []int{initramfsPCR})
}
func unlockVolumeUsingSealedKeyTPM(name, sealedEncryptionKeyFile, sourceDevice, targetDevice, mapperName string, opts *UnlockVolumeUsingSealedKeyOptions) (UnlockResult, error) {
// TODO:UC20: use sb.SecureConnectToDefaultTPM() if we decide there's benefit in doing that or
// we have a hard requirement for a valid EK cert chain for every boot (ie, panic
// if there isn't one). But we can't do that as long as we need to download
// intermediate certs from the manufacturer.
res := UnlockResult{IsEncrypted: true, PartDevice: sourceDevice}
// Obtain a TPM connection.
tpm, tpmErr := sbConnectToDefaultTPM()
if tpmErr != nil {
if !xerrors.Is(tpmErr, sb.ErrNoTPM2Device) {
return res, fmt.Errorf("cannot unlock encrypted device %q: %v", name, tpmErr)
}
logger.Noticef("cannot open TPM connection: %v", tpmErr)
} else {
defer tpm.Close()
}
// Also check if the TPM device is enabled. The platform firmware may disable the storage
// and endorsement hierarchies, but the device will remain visible to the operating system.
tpmDeviceAvailable := tpmErr == nil && isTPMEnabled(tpm)
// if we don't have a tpm, and we allow using a recovery key, do that
// directly
if !tpmDeviceAvailable && opts.AllowRecoveryKey {
if err := UnlockEncryptedVolumeWithRecoveryKey(mapperName, sourceDevice); err != nil {
return res, err
}
res.FsDevice = targetDevice
res.UnlockMethod = UnlockedWithRecoveryKey
return res, nil
}
// otherwise we have a tpm and we should use the sealed key first, but
// this method will fallback to using the recovery key if enabled
method, err := unlockEncryptedPartitionWithSealedKey(tpm, mapperName, sourceDevice, sealedEncryptionKeyFile, "", opts.AllowRecoveryKey)
res.UnlockMethod = method
if err == nil {
res.FsDevice = targetDevice
}
return res, err
}
func isActivatedWithRecoveryKey(err error) bool {
if err == nil {
return false
}
// with non-nil err, we should check for err being ActivateWithTPMSealedKeyError
// and RecoveryKeyUsageErr inside that being nil - this indicates that the
// recovery key was used to unlock it
activateErr, ok := err.(*sb.ActivateWithTPMSealedKeyError)
if !ok {
return false
}
return activateErr.RecoveryKeyUsageErr == nil
}
func activateVolOpts(allowRecoveryKey bool) *sb.ActivateVolumeOptions {
options := sb.ActivateVolumeOptions{
PassphraseTries: 1,
// disable recovery key by default
RecoveryKeyTries: 0,
KeyringPrefix: keyringPrefix,
}
if allowRecoveryKey {
// enable recovery key only when explicitly allowed
options.RecoveryKeyTries = 3
}
return &options
}
// unlockEncryptedPartitionWithSealedKey unseals the keyfile and opens an encrypted
// device. If activation with the sealed key fails, this function will attempt to
// activate it with the fallback recovery key instead.
func unlockEncryptedPartitionWithSealedKey(tpm *sb.TPMConnection, name, device, keyfile, pinfile string, allowRecovery bool) (UnlockMethod, error) {
options := activateVolOpts(allowRecovery)
// XXX: pinfile is currently not used
activated, err := sbActivateVolumeWithTPMSealedKey(tpm, name, device, keyfile, nil, options)
if activated {
// non nil error may indicate the volume was unlocked using the
// recovery key
if err == nil {
logger.Noticef("successfully activated encrypted device %q with TPM", device)
return UnlockedWithSealedKey, nil
} else if isActivatedWithRecoveryKey(err) {
logger.Noticef("successfully activated encrypted device %q using a fallback activation method", device)
return UnlockedWithRecoveryKey, nil
}
// no other error is possible when activation succeeded
return UnlockStatusUnknown, fmt.Errorf("internal error: volume activated with unexpected error: %v", err)
}
// ActivateVolumeWithTPMSealedKey should always return an error if activated == false
return NotUnlocked, fmt.Errorf("cannot activate encrypted device %q: %v", device, err)
}
// SealKeys provisions the TPM and seals the encryption keys according to the
// specified parameters. If the TPM is already provisioned, or a sealed key already
// exists, SealKeys will fail and return an error.
func SealKeys(keys []SealKeyRequest, params *SealKeysParams) error {
numModels := len(params.ModelParams)
if numModels < 1 {
return fmt.Errorf("at least one set of model-specific parameters is required")
}
tpm, err := sbConnectToDefaultTPM()
if err != nil {
return fmt.Errorf("cannot connect to TPM: %v", err)
}
defer tpm.Close()
if !isTPMEnabled(tpm) {
return fmt.Errorf("TPM device is not enabled")
}
pcrProfile, err := buildPCRProtectionProfile(params.ModelParams)
if err != nil {
return err
}
if params.TPMProvision {
// Provision the TPM as late as possible
if err := tpmProvision(tpm, params.TPMLockoutAuthFile); err != nil {
return err
}
}
// Seal the provided keys to the TPM
creationParams := sb.KeyCreationParams{
PCRProfile: pcrProfile,
PCRPolicyCounterHandle: tpm2.Handle(params.PCRPolicyCounterHandle),
AuthKey: params.TPMPolicyAuthKey,
}
sbKeys := make([]*sb.SealKeyRequest, 0, len(keys))
for i := range keys {
sbKeys = append(sbKeys, &sb.SealKeyRequest{
Key: keys[i].Key,
Path: keys[i].KeyFile,
})
}
authKey, err := sbSealKeyToTPMMultiple(tpm, sbKeys, &creationParams)
if err != nil {
return err
}
if params.TPMPolicyAuthKeyFile != "" {
if err := osutil.AtomicWriteFile(params.TPMPolicyAuthKeyFile, authKey, 0600, 0); err != nil {
return fmt.Errorf("cannot write the policy auth key file: %v", err)
}
}
return nil
}
// ResealKeys updates the PCR protection policy for the sealed encryption keys
// according to the specified parameters.
func ResealKeys(params *ResealKeysParams) error {
numModels := len(params.ModelParams)
if numModels < 1 {
return fmt.Errorf("at least one set of model-specific parameters is required")
}
tpm, err := sbConnectToDefaultTPM()
if err != nil {
return fmt.Errorf("cannot connect to TPM: %v", err)
}
defer tpm.Close()
if !isTPMEnabled(tpm) {
return fmt.Errorf("TPM device is not enabled")
}
pcrProfile, err := buildPCRProtectionProfile(params.ModelParams)
if err != nil {
return err
}
authKey, err := ioutil.ReadFile(params.TPMPolicyAuthKeyFile)
if err != nil {
return fmt.Errorf("cannot read the policy auth key file: %v", err)
}
return sbUpdateKeyPCRProtectionPolicyMultiple(tpm, params.KeyFiles, authKey, pcrProfile)
}
func buildPCRProtectionProfile(modelParams []*SealKeyModelParams) (*sb.PCRProtectionProfile, error) {
numModels := len(modelParams)
modelPCRProfiles := make([]*sb.PCRProtectionProfile, 0, numModels)
for _, mp := range modelParams {
modelProfile := sb.NewPCRProtectionProfile()
loadSequences, err := buildLoadSequences(mp.EFILoadChains)
if err != nil {
return nil, fmt.Errorf("cannot build EFI image load sequences: %v", err)
}
// Add EFI secure boot policy profile
policyParams := sb.EFISecureBootPolicyProfileParams{
PCRAlgorithm: tpm2.HashAlgorithmSHA256,
LoadSequences: loadSequences,
// TODO:UC20: set SignatureDbUpdateKeystore to support applying forbidden
// signature updates to blacklist signing keys (after rotating them).
// This also requires integration of sbkeysync, and some work to
// ensure that the PCR profile is updated before/after sbkeysync executes.
}
if err := sbAddEFISecureBootPolicyProfile(modelProfile, &policyParams); err != nil {
return nil, fmt.Errorf("cannot add EFI secure boot policy profile: %v", err)
}
// Add EFI boot manager profile
bootManagerParams := sb.EFIBootManagerProfileParams{
PCRAlgorithm: tpm2.HashAlgorithmSHA256,
LoadSequences: loadSequences,
}
if err := sbAddEFIBootManagerProfile(modelProfile, &bootManagerParams); err != nil {
return nil, fmt.Errorf("cannot add EFI boot manager profile: %v", err)
}
// Add systemd EFI stub profile
if len(mp.KernelCmdlines) != 0 {
systemdStubParams := sb.SystemdEFIStubProfileParams{
PCRAlgorithm: tpm2.HashAlgorithmSHA256,
PCRIndex: initramfsPCR,
KernelCmdlines: mp.KernelCmdlines,
}
if err := sbAddSystemdEFIStubProfile(modelProfile, &systemdStubParams); err != nil {
return nil, fmt.Errorf("cannot add systemd EFI stub profile: %v", err)
}
}
// Add snap model profile
if mp.Model != nil {
snapModelParams := sb.SnapModelProfileParams{
PCRAlgorithm: tpm2.HashAlgorithmSHA256,
PCRIndex: initramfsPCR,
Models: []sb.SnapModel{mp.Model},
}
if err := sbAddSnapModelProfile(modelProfile, &snapModelParams); err != nil {
return nil, fmt.Errorf("cannot add snap model profile: %v", err)
}
}
modelPCRProfiles = append(modelPCRProfiles, modelProfile)
}
var pcrProfile *sb.PCRProtectionProfile
if numModels > 1 {
pcrProfile = sb.NewPCRProtectionProfile().AddProfileOR(modelPCRProfiles...)
} else {
pcrProfile = modelPCRProfiles[0]
}
logger.Debugf("PCR protection profile:\n%s", pcrProfile.String())
return pcrProfile, nil
}
func tpmProvision(tpm *sb.TPMConnection, lockoutAuthFile string) error {
// Create and save the lockout authorization file
lockoutAuth := make([]byte, 16)
// crypto rand is protected against short reads
_, err := rand.Read(lockoutAuth)
if err != nil {
return fmt.Errorf("cannot create lockout authorization: %v", err)
}
if err := osutil.AtomicWriteFile(lockoutAuthFile, lockoutAuth, 0600, 0); err != nil {
return fmt.Errorf("cannot write the lockout authorization file: %v", err)
}
// TODO:UC20: ideally we should ask the firmware to clear the TPM and then reboot
// if the device has previously been provisioned, see
// https://godoc.org/github.com/snapcore/secboot#RequestTPMClearUsingPPI
if err := provisionTPM(tpm, sb.ProvisionModeFull, lockoutAuth); err != nil {
logger.Noticef("TPM provisioning error: %v", err)
return fmt.Errorf("cannot provision TPM: %v", err)
}
return nil
}
func provisionTPMImpl(tpm *sb.TPMConnection, mode sb.ProvisionMode, lockoutAuth []byte) error {
return tpm.EnsureProvisioned(mode, lockoutAuth)
}
// buildLoadSequences builds EFI load image event trees from this package LoadChains
func buildLoadSequences(chains []*LoadChain) (loadseqs []*sb.EFIImageLoadEvent, err error) {
// this will build load event trees for the current
// device configuration, e.g. something like:
//
// shim -> recovery grub -> recovery kernel 1
// |-> recovery kernel 2
// |-> recovery kernel ...
// |-> normal grub -> run kernel good
// |-> run kernel try
for _, chain := range chains {
// root of load events has source Firmware
loadseq, err := chain.loadEvent(sb.Firmware)
if err != nil {
return nil, err
}
loadseqs = append(loadseqs, loadseq)
}
return loadseqs, nil
}
// loadEvent builds the corresponding load event and its tree
func (lc *LoadChain) loadEvent(source sb.EFIImageLoadEventSource) (*sb.EFIImageLoadEvent, error) {
var next []*sb.EFIImageLoadEvent
for _, nextChain := range lc.Next {
// everything that is not the root has source shim
ev, err := nextChain.loadEvent(sb.Shim)
if err != nil {
return nil, err
}
next = append(next, ev)
}
image, err := efiImageFromBootFile(lc.BootFile)
if err != nil {
return nil, err
}
return &sb.EFIImageLoadEvent{
Source: source,
Image: image,
Next: next,
}, nil
}
func efiImageFromBootFile(b *bootloader.BootFile) (sb.EFIImage, error) {
if b.Snap == "" {
if !osutil.FileExists(b.Path) {
return nil, fmt.Errorf("file %s does not exist", b.Path)
}
return sb.FileEFIImage(b.Path), nil
}
snapf, err := snapfile.Open(b.Snap)
if err != nil {
return nil, err
}
return sb.SnapFileEFIImage{
Container: snapf,
FileName: b.Path,
}, nil
}