File: secureboot.go

package info (click to toggle)
golang-github-canonical-go-efilib 1.6.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 6,836 kB
  • sloc: makefile: 3
file content (267 lines) | stat: -rw-r--r-- 9,830 bytes parent folder | download
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
// Copyright 2024 Canonical Ltd.
// Licensed under the LGPLv3 with static-linking exception.
// See LICENCE file for details.

package efi

import (
	"bytes"
	"context"
	"crypto/x509"
	"errors"
	"fmt"
)

func readBinaryVariable(ctx context.Context, name string, guid GUID) (bool, error) {
	data, _, err := ReadVariable(ctx, name, guid)
	if err != nil {
		return false, err
	}
	if len(data) != 1 {
		return false, fmt.Errorf("variable contents has unexpected number of bytes (got %d bytes)", len(data))
	}
	switch data[0] {
	case 0:
		return false, nil
	case 1:
		return true, nil
	default:
		return false, fmt.Errorf("unexpected variable contents: %d", uint8(data[0]))
	}
}

// ReadSecureBootVariable reads the SecureBoot global variable which provides
// an indication of whether secure boot is enabled. If it returns false, then
// secure boot is disabled. If it returns true, then it is an indication that
// secure boot is enabled. In general, [DefaultVarContext] should be supplied
// to this.
//
// Note that this function cannot prove that secure boot is enabled. If a platform
// provides a way to disable secure boot and execute arbitrary code, then the
// platform or kernel may not tell the truth about this. Obtaining proof that
// secure boot is enabled would involve the use of attestations and a third
// party verifier.
func ReadSecureBootVariable(ctx context.Context) (bool, error) {
	return readBinaryVariable(ctx, "SecureBoot", GlobalVariable)
}

// ReadPlatformKeyCertificate reads the PK global variable and returns the corresponding
// certificate, if a platform key is enrolled. If no platform key is enrolled, this
// will return nil. In general, [DefaultVarContext] should be supplied to this.
func ReadPlatformKeyCertificate(ctx context.Context) (*x509.Certificate, error) {
	db, err := ReadSignatureDatabaseVariable(ctx, PKVariable)
	if err != nil {
		return nil, err
	}

	switch len(db) {
	case 0:
		return nil, nil
	case 1:
		esl := db[0]
		if esl.Type != CertX509Guid {
			return nil, fmt.Errorf("invalid PK contents: unexpected signature list type: %v", esl.Type)
		}
		if len(esl.Signatures) != 1 {
			return nil, fmt.Errorf("invalid PK contents: unexpected number of signature data entries (%d)", len(esl.Signatures))
		}
		return x509.ParseCertificate(esl.Signatures[0].Data)
	default:
		return nil, errors.New("invalid PK contents: more than one signature list")
	}
}

var (
	// PKVariable corresponds to the PK global variable
	PKVariable VariableDescriptor = VariableDescriptor{Name: "PK", GUID: GlobalVariable}

	// KEKVariable corresponds to the KEK global variable
	KEKVariable VariableDescriptor = VariableDescriptor{Name: "KEK", GUID: GlobalVariable}

	// DbVariable corresponds to the authorized signature database variable
	DbVariable VariableDescriptor = VariableDescriptor{Name: "db", GUID: ImageSecurityDatabaseGuid}

	// DbxVariable corresponds to the forbidden signature database variable
	DbxVariable VariableDescriptor = VariableDescriptor{Name: "dbx", GUID: ImageSecurityDatabaseGuid}
)

// ReadSignatureDatabaseVariable reads the signature database from the supplied
// variable. In general, [DefaultVarContext] should be supplied to this.
func ReadSignatureDatabaseVariable(ctx context.Context, desc VariableDescriptor) (SignatureDatabase, error) {
	data, _, err := ReadVariable(ctx, desc.Name, desc.GUID)
	if err != nil {
		return nil, err
	}
	return ReadSignatureDatabase(bytes.NewReader(data))
}

// InconsistentSecureBootModeError is returned from [ComputeSecureBootMode] if
// some of the variables are in an inconsistent state.
type InconsistentSecureBootModeError struct {
	err error
}

func (e *InconsistentSecureBootModeError) Error() string {
	return "inconsistent secure boot mode: " + e.err.Error()
}

func (e *InconsistentSecureBootModeError) Unwrap() error {
	return e.err
}

// SecureBootMode describes the secure boot mode of a platform.
type SecureBootMode int

const (
	// SetupMode indicates that a platform is in setup mode. In this mode, no platform
	// key is enrolled and secure boot cannot be enabled. Writes to secure boot
	// variables other than PK can be performed without authentication.
	//
	// SetupMode can transition to UserMode by enrolling a platform key, which can be
	// done from the OS by performing a self-signed authenticated write to the PK
	// global variable.
	//
	// Since UEFI 2.5, SetupMode can transition to AuditMode by writing 1 to the
	// AuditMode global variable before ExitBootServices.
	SetupMode SecureBootMode = iota + 1

	// AuditMode indicates that a platform is in audit mode. This mode implies setup
	// mode - no platform key is enrolled and secure boot cannot be enabled. Writes to
	// secure boot variables other than PK can be performed without authentication.
	//
	// AuditMode provides a way of ensuring that the current signature database
	// configuration is able to authenticate an OS without preventing it from booting
	// if authentication fails.
	//
	// AuditMode can transition to DeployedMode by enrolling a platform key, which can be
	// done from the OS by performing a self-signed authenticated write to the PK
	// global variable.
	//
	// AuditMode only exists since UEFI 2.5.
	AuditMode

	// UserMode indicates that a platform is in user mode. In this mode, a platform
	// key is enrolled and secure boot can be enabled (but may be disabled using some
	// platform specific mechanism). Writes to secure boot variables require authentication.
	//
	// UserMode can transition to SetupMode by erasing the platform key, either via
	// some platform specific mechanism or by an authenticated write of an empty payload
	// to the PK global variable.
	//
	// Since UEFI 2.5, UserMode can transition to AuditMode by writing 1 to the AuditMode
	// global variable before ExitBootServices.
	//
	// Since UEFI 2.5, UserMode can transition to DeployedMode by writing 1 to the
	// DeployedMode global variable before ExitBootServices.
	UserMode

	// DeployedMode indicates that a platform is in deployed mode. In this mode, a
	// platform key is enrolled and secure boot can be enabled (but may be disabled using
	// some platform specific mechanism. Writes to secure boot variables require
	// authentication. This is the most secure mode.
	//
	// DeployedMode may transition back to UserMode by some optional platform specific
	// mechanism which clears the DeployedMode variable.
	//
	// DeployedMode exists since UEFI 2.5.
	DeployedMode
)

type secureBootModeFeatures int

const (
	secureBootModeFeaturesUndetermined secureBootModeFeatures = iota
	secureBootModeFeaturesAtLeastUefi2_5
	secureBootModeFeaturesBeforeUefi2_5
)

// ComputeSecureBootMode determines the secure boot mode of a platform. In general,
// [DefaultVarContext] should be supplied to this.
func ComputeSecureBootMode(ctx context.Context) (SecureBootMode, error) {
	setupMode, err := readBinaryVariable(ctx, "SetupMode", GlobalVariable)
	if err != nil {
		return 0, fmt.Errorf("cannot read SetupMode variable: %w", err)
	}

	var features secureBootModeFeatures

	auditMode, err := readBinaryVariable(ctx, "AuditMode", GlobalVariable)
	switch {
	case errors.Is(err, ErrVarNotExist):
		features = secureBootModeFeaturesBeforeUefi2_5
	case err != nil:
		return 0, fmt.Errorf("cannot read AuditMode variable: %w", err)
	default:
		features = secureBootModeFeaturesAtLeastUefi2_5
	}

	var deployedMode bool
	if features == secureBootModeFeaturesAtLeastUefi2_5 {
		var err error
		deployedMode, err = readBinaryVariable(ctx, "DeployedMode", GlobalVariable)
		if err != nil {
			return 0, fmt.Errorf("cannot read DeployedMode variable: %w", err)
		}
	}

	secureBoot, err := ReadSecureBootVariable(ctx)
	if err != nil {
		return 0, fmt.Errorf("cannot read SecureBoot variable: %w", err)
	}
	pk, err := ReadPlatformKeyCertificate(ctx)
	if err != nil {
		return 0, fmt.Errorf("cannot read PK variable: %w", err)
	}

	switch setupMode {
	case true:
		if secureBoot {
			// Secure boot cannot be enabled in setup mode
			return 0, &InconsistentSecureBootModeError{errors.New("firmware indicates secure boot is enabled in setup mode")}
		}
		if pk != nil {
			// There should be no platform key in setup mode. If one is enrolled from the OS,
			// the firmware should update the value of SetupMode.
			return 0, &InconsistentSecureBootModeError{errors.New("firmware indicates setup mode is enabled with a platform key enrolled")}
		}
		if features == secureBootModeFeaturesBeforeUefi2_5 {
			return SetupMode, nil
		}
		if deployedMode {
			// Deployed mode cannot be enabled in setup mode.
			return 0, &InconsistentSecureBootModeError{errors.New("firmware indicates deployed mode is enabled in setup mode")}
		}
		if auditMode {
			return AuditMode, nil
		}
		return SetupMode, nil
	case false:
		if pk == nil {
			// There should be a platform key when not in setup mode. If it is deleted with
			// an authenticated write from the OS, then the firmware should update the value
			// of SetupMode.
			return 0, &InconsistentSecureBootModeError{errors.New("firmware indicates it isn't in setup mode when no platform key is enrolled")}
		}
		if features == secureBootModeFeaturesBeforeUefi2_5 {
			return UserMode, nil
		}
		if auditMode {
			// Audit mode implies setup mode.
			return 0, &InconsistentSecureBootModeError{errors.New("firmware indicates audit mode is enabled when not in setup mode")}
		}
		if deployedMode {
			return DeployedMode, nil
		}
		return UserMode, nil
	}

	panic("not reached")
}

// IsDeployedModeSupported indicates whether the firmware is new enough (ie based on
// at least UEFI 2.5) to support deployed mode. In general, [DefaultVarContext] should
// be supplied to this.
func IsDeployedModeSupported(ctx context.Context) bool {
	_, _, err := ReadVariable(ctx, "DeployedMode", GlobalVariable)
	return err == nil
}