-
Notifications
You must be signed in to change notification settings - Fork 103
/
Copy pathtpmrunner.go
279 lines (226 loc) · 6.54 KB
/
tpmrunner.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
//go:build !darwin
// +build !darwin
package tpmrunner
import (
"context"
"crypto"
"errors"
"fmt"
"io"
"log/slog"
"sync"
"sync/atomic"
"time"
"github.com/kolide/krypto/pkg/tpm"
"github.com/kolide/launcher/ee/agent/types"
"github.com/kolide/launcher/pkg/backoff"
"github.com/kolide/launcher/pkg/traces"
)
type (
tpmRunner struct {
signer crypto.Signer
mux sync.Mutex
signerCreator tpmSignerCreator
store types.GetterSetterDeleter
slogger *slog.Logger
interrupt chan struct{}
interrupted atomic.Bool
machineHasTpm atomic.Bool
}
// tpmSignerCreator is an interface for creating and loading TPM signers
// useful for mocking in tests
tpmSignerCreator interface {
CreateKey(opts ...tpm.TpmSignerOption) (private []byte, public []byte, err error)
New(private, public []byte) (crypto.Signer, error)
}
// defaultTpmSignerCreator is the default implementation of tpmSignerCreator
// using the tpm package
defaultTpmSignerCreator struct{}
// tpmRunnerOption is a functional option for tpmRunner
// useful for setting dependencies in tests
tpmRunnerOption func(*tpmRunner)
)
// CreateKey creates a new TPM key
func (d defaultTpmSignerCreator) CreateKey(opts ...tpm.TpmSignerOption) (private []byte, public []byte, err error) {
return tpm.CreateKey()
}
// New creates a new TPM signer
func (d defaultTpmSignerCreator) New(private, public []byte) (crypto.Signer, error) {
return tpm.New(private, public)
}
func New(ctx context.Context, slogger *slog.Logger, store types.GetterSetterDeleter, opts ...tpmRunnerOption) (*tpmRunner, error) {
tpmRunner := &tpmRunner{
store: store,
slogger: slogger.With("component", "tpmrunner"),
interrupt: make(chan struct{}),
signerCreator: defaultTpmSignerCreator{},
}
// assume we have a tpm until we know otherwise
tpmRunner.machineHasTpm.Store(true)
for _, opt := range opts {
opt(tpmRunner)
}
return tpmRunner, nil
}
// Public returns the public key of the current console user
// creating and peristing a new one if needed
func (tr *tpmRunner) Execute() error {
durationCounter := backoff.NewMultiplicativeDurationCounter(time.Second, time.Minute)
retryTicker := time.NewTicker(durationCounter.Next())
defer retryTicker.Stop()
for {
// try to create signer if we don't have one
if tr.signer == nil && tr.machineHasTpm.Load() {
ctx := context.Background()
if err := tr.loadOrCreateKeys(ctx); err != nil {
tr.slogger.Log(ctx, slog.LevelInfo,
"loading or creating keys in execute loop",
"err", err,
)
}
}
if tr.signer != nil || !tr.machineHasTpm.Load() {
retryTicker.Stop()
}
select {
case <-retryTicker.C:
retryTicker.Reset(durationCounter.Next())
continue
case <-tr.interrupt:
tr.slogger.Log(context.TODO(), slog.LevelDebug,
"interrupt received, exiting tpm signer execute loop",
)
return nil
}
}
}
func (tr *tpmRunner) Interrupt(_ error) {
// Only perform shutdown tasks on first call to interrupt -- no need to repeat on potential extra calls.
if tr.interrupted.Load() {
return
}
tr.interrupted.Store(true)
// Tell the execute loop to stop checking, and exit
tr.interrupt <- struct{}{}
}
// Public returns the public hardware key
func (tr *tpmRunner) Public() crypto.PublicKey {
if !tr.machineHasTpm.Load() {
return nil
}
if tr.signer != nil {
return tr.signer.Public()
}
if err := tr.loadOrCreateKeys(context.Background()); err != nil {
tr.slogger.Log(context.Background(), slog.LevelInfo,
"loading or creating keys in public call",
"err", err,
)
return nil
}
return tr.signer.Public()
}
func (tr *tpmRunner) Type() string {
return "tpm"
}
func (tr *tpmRunner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
if tr.signer == nil {
return nil, errors.New("no signer available")
}
return tr.signer.Sign(rand, digest, opts)
}
// This duplicates some of pkg/osquery/extension.go but that feels like the wrong place.
// Really, we should have a simpler interface over a storage layer.
const (
privateEccData = "privateEccData"
publicEccData = "publicEccData"
)
func fetchKeyData(store types.Getter) ([]byte, []byte, error) {
pri, err := store.Get([]byte(privateEccData))
if err != nil {
return nil, nil, err
}
pub, err := store.Get([]byte(publicEccData))
if err != nil {
return nil, nil, err
}
return pri, pub, nil
}
func storeKeyData(store types.Setter, pri, pub []byte) error {
if pri != nil {
if err := store.Set([]byte(privateEccData), pri); err != nil {
return err
}
}
if pub != nil {
if err := store.Set([]byte(publicEccData), pub); err != nil {
return err
}
}
return nil
}
// clearKeyData is used to clear the keys as part of error handling around new keys. It is not intended to be called
// regularly, and since the path that calls it is around DB errors, it has no error handling.
func clearKeyData(slogger *slog.Logger, deleter types.Deleter) {
slogger.Log(context.TODO(), slog.LevelInfo,
"clearing keys",
)
_ = deleter.Delete([]byte(privateEccData), []byte(publicEccData))
}
func (tr *tpmRunner) loadOrCreateKeys(ctx context.Context) error {
ctx, span := traces.StartSpan(ctx)
defer span.End()
tr.mux.Lock()
defer tr.mux.Unlock()
// tpm signer was already created
if tr.signer != nil {
return nil
}
priData, pubData, err := fetchKeyData(tr.store)
if err != nil {
thisErr := fmt.Errorf("fetching key data for data store: %w", err)
traces.SetError(span, thisErr)
return thisErr
}
if pubData == nil || priData == nil {
var err error
priData, pubData, err = tr.signerCreator.CreateKey()
if err != nil {
if isTPMNotFoundErr(err) {
tr.machineHasTpm.Store(false)
tr.slogger.Log(ctx, slog.LevelInfo,
"tpm not found",
"err", err,
)
span.AddEvent("tpm_not_found")
return err
}
thisErr := fmt.Errorf("creating key: %w", err)
traces.SetError(span, thisErr)
clearKeyData(tr.slogger, tr.store)
return thisErr
}
if err := storeKeyData(tr.store, priData, pubData); err != nil {
thisErr := fmt.Errorf("storing key data: %w", err)
traces.SetError(span, thisErr)
clearKeyData(tr.slogger, tr.store)
return thisErr
}
tr.slogger.Log(ctx, slog.LevelInfo,
"new tpm keys generated",
)
span.AddEvent("generated_new_tpm_keys")
}
k, err := tr.signerCreator.New(priData, pubData)
if err != nil {
thisErr := fmt.Errorf("creating tpm signer: %w", err)
traces.SetError(span, thisErr)
return thisErr
}
tr.signer = k
tr.slogger.Log(ctx, slog.LevelDebug,
"tpm signer created",
)
span.AddEvent("created_tpm_signer")
return nil
}