-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnats_js_tpm.go
273 lines (251 loc) · 10.5 KB
/
nats_js_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
//go:build windows
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"github.com/google/go-tpm/legacy/tpm2"
"github.com/google/go-tpm/tpmutil"
"github.com/nats-io/nkeys"
)
var (
JsKeyTPMVersion = 1 // Version of the TPM JS implmentation
)
// How this works:
// Create a Storage Root Key (SRK) in the TPM.
// If existing JS Encrpytion keys do not exist on disk.
// - Create a JetStream encryption key (js key) and seal it to the SRK
// using a provided js encryption key password.
// - Save the public and private blobs to a file on disk.
// - Return the new js encrpytion key (the private portion of the nkey)
// Otherwise (keys exist on disk)
// - Read the public and private blobs from disk
// - Load them into the TPM
// - Unseal the js key using the TPM, and the provided js encryption keys password.
//
// Notes: an owner password for the SRK is supported but not tested here.
// TODO:
// Add HMAC (or something) to the TPM session to make it more secure.
// Gets/Regenerates the Storage Root Key (SRK) from the TPM. Caller MUST flush this handle when done.
func regenerateSRK(rwc io.ReadWriteCloser, srkPassword string) (tpmutil.Handle, error) {
// Default EK template defined in:
// https://trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf
// Shared SRK template based off of EK template and specified in:
// https://trustedcomputinggroup.org/wp-content/uploads/TCG-TPM-v2.0-Provisioning-Guidance-Published-v1r1.pdf
srkTemplate := tpm2.Public{
Type: tpm2.AlgRSA,
NameAlg: tpm2.AlgSHA256,
Attributes: tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagRestricted | tpm2.FlagDecrypt | tpm2.FlagNoDA,
AuthPolicy: nil,
// We must use RSA 2048 for the intel TSS2 stack
RSAParameters: &tpm2.RSAParams{
Symmetric: &tpm2.SymScheme{
Alg: tpm2.AlgAES,
KeyBits: 128,
Mode: tpm2.AlgCFB,
},
KeyBits: 2048,
ModulusRaw: make([]byte, 256),
},
}
// Create the parent key against which to seal the data
srkHandle, _, err := tpm2.CreatePrimary(rwc, tpm2.HandleOwner, tpm2.PCRSelection{}, "", srkPassword, srkTemplate)
return srkHandle, err
}
type natsTPMPersistedKeys struct {
Version int // json: "version"
PrivateKey []byte // json: "privatekey"
PublicKey []byte // json: "publickey"
}
// Writes the private and public blobs to disk in a single file. If the directory does
// not exist, it will be created. If the files already exists it will be overwritten.
func writeTPMKeysToFile(filename string, privateBlob []byte, publicBlob []byte) error {
keyDir := filepath.Dir(filename)
if err := os.MkdirAll(keyDir, 0755); err != nil {
return fmt.Errorf("unable to create/access directory %q: %v", keyDir, err)
}
// Create a new set of persisted keys. Note that the private key doesn't necessary
// need to be protected as the TPM password is required to use unseal, but it's
// a good idea to put this in a secure location accessible to the server.
tpmKeys := natsTPMPersistedKeys{
Version: JsKeyTPMVersion,
PrivateKey: []byte(base64.StdEncoding.EncodeToString(privateBlob)),
PublicKey: []byte(base64.StdEncoding.EncodeToString(publicBlob)),
}
// Convert to JSON
keysJSON, err := json.Marshal(tpmKeys)
if err != nil {
return fmt.Errorf("unable to marshal keys to JSON: %v", err)
}
// Write the JSON to a file
if err := os.WriteFile(filename, keysJSON, 0644); err != nil {
return fmt.Errorf("unable to write keys file to %q: %v", filename, err)
}
return nil
}
// Reads the private and public blobs from a single file. If the file does not exist,
// or the file cannot be read and the keys decoded, an error is returned.
func readTPMKeysFromFile(filename string) ([]byte, []byte, error) {
if _, err := os.Stat(filename); os.IsNotExist(err) {
return nil, nil, err
}
keysJSON, err := os.ReadFile(filename)
if err != nil {
return nil, nil, fmt.Errorf("unable to read persisted keys from %q: %v", filename, err)
}
var tpmKeys natsTPMPersistedKeys
if err := json.Unmarshal(keysJSON, &tpmKeys); err != nil {
return nil, nil, fmt.Errorf("unable to unmarshal TPM file keys JSON from %s: %v", filename, err)
}
// Placeholder for future-proofing. Here is where we would
// check the current version against tpmKeys.Version and
// handle any changes.
// Base64 decode the private and public blobs.
publicBlob, err := base64.StdEncoding.DecodeString(string(tpmKeys.PublicKey))
if err != nil {
return nil, nil, fmt.Errorf("unable to decode publicBlob from base64: %v", err)
}
privateBlob, err := base64.StdEncoding.DecodeString(string(tpmKeys.PrivateKey))
if err != nil {
return nil, nil, fmt.Errorf("unable to decode privateBlob from base64: %v", err)
}
return publicBlob, privateBlob, nil
}
// Creates a new JetStream encryption key, seals it to the TPM, and saves the public and
// private blobs to disk in a JSON encoded file. The key is returned as a string.
func createAndSealJsEncryptionKey(rwc io.ReadWriteCloser, srkHandle tpmutil.Handle, srkPassword, jsKeyFile, jsKeyPassword string, pcr int) (string, error) {
// Get the authorization policy that will protect the data to be sealed
sessHandle, policy, err := policyPCRPasswordSession(rwc, pcr, jsKeyPassword)
if err != nil {
return "", fmt.Errorf("unable to get policy: %v", err)
}
if err := tpm2.FlushContext(rwc, sessHandle); err != nil {
return "", fmt.Errorf("unable to flush session: %v", err)
}
// Seal the data to the parent key and the policy
user, err := nkeys.CreateUser()
if err != nil {
return "", fmt.Errorf("unable to create seed: %v", err)
}
// We'll use the seed to represent the encrpytion key.
jsStoreKey, err := user.Seed()
if err != nil {
return "", fmt.Errorf("unable to get seed: %v", err)
}
privateArea, publicArea, err := tpm2.Seal(rwc, srkHandle, srkPassword, jsKeyPassword, policy, jsStoreKey)
if err != nil {
return "", fmt.Errorf("unable to seal data: %v", err)
}
err = writeTPMKeysToFile(jsKeyFile, privateArea, publicArea)
if err != nil {
return "", fmt.Errorf("unable to write key file: %v", err)
}
return string(jsStoreKey), nil
}
// Unseals the JetStream encryption key from the TPM with the provided public/private keys.
// The key is returned as a string.
func unsealJsEncrpytionKey(rwc io.ReadWriteCloser, pcr int, srkHandle tpmutil.Handle, srkPassword, objectPassword string, publicBlob, privateBlob []byte) (string, error) {
// Load the public/private blobs into the TPM for decryption.
objectHandle, _, err := tpm2.Load(rwc, srkHandle, srkPassword, publicBlob, privateBlob)
if err != nil {
return "", fmt.Errorf("unable to load data: %v", err)
}
defer func() {
tpm2.FlushContext(rwc, objectHandle)
}()
// Create the authorization session with TPM.
sessHandle, _, err := policyPCRPasswordSession(rwc, pcr, objectPassword)
if err != nil {
return "", fmt.Errorf("unable to get auth session: %v", err)
}
defer func() {
tpm2.FlushContext(rwc, sessHandle)
}()
// Unseal the data we've loaded into the TPM with the object (js key) password.
unsealedData, err := tpm2.UnsealWithSession(rwc, sessHandle, objectHandle, objectPassword)
if err != nil {
return "", fmt.Errorf("unable to unseal data: %v", err)
}
return string(unsealedData), nil
}
// Returns session handle and policy digest.
// TODO - this is not a secure session - need to add HMAC or something.
func policyPCRPasswordSession(rwc io.ReadWriteCloser, pcr int, password string) (sessHandle tpmutil.Handle, policy []byte, retErr error) {
sessHandle, _, err := tpm2.StartAuthSession(
rwc,
tpm2.HandleNull, /*tpmKey*/
tpm2.HandleNull, /*bindKey*/
make([]byte, 16), /*nonceCaller*/
nil, /*secret*/
tpm2.SessionPolicy,
tpm2.AlgNull,
tpm2.AlgSHA256)
if err != nil {
return tpm2.HandleNull, nil, fmt.Errorf("unable to start session: %v", err)
}
defer func() {
if sessHandle != tpm2.HandleNull && err != nil {
if err := tpm2.FlushContext(rwc, sessHandle); err != nil {
retErr = fmt.Errorf("%v\nunable to flush session: %v", retErr, err)
}
}
}()
pcrSelection := tpm2.PCRSelection{
Hash: tpm2.AlgSHA256,
PCRs: []int{pcr},
}
if err := tpm2.PolicyPCR(rwc, sessHandle, nil, pcrSelection); err != nil {
return sessHandle, nil, fmt.Errorf("unable to bind PCRs to auth policy: %v", err)
}
if err := tpm2.PolicyPassword(rwc, sessHandle); err != nil {
return sessHandle, nil, fmt.Errorf("unable to require password for auth policy: %v", err)
}
policy, err = tpm2.PolicyGetDigest(rwc, sessHandle)
if err != nil {
return sessHandle, nil, fmt.Errorf("unable to get policy digest: %v", err)
}
return sessHandle, policy, nil
}
// LoadJetStreamEncryptionKeyFromTPM loads the JetStream encryption key from the TPM.
// If the key does not exist, it will be created and sealed. Public and private blobs
// used to decrypt the key in future sessions will be saved to disk in the file provided.
// The key will be unsealed and returned only with the correct password and PCR value.
func LoadJetStreamEncryptionKeyFromTPM(srkPassword, jsKeyFile, jsKeyPassword string, pcr int) (string, error) {
var err error
rwc, err := tpm2.OpenTPM()
if err != nil {
return "", fmt.Errorf("could not open the TPM: %v", err)
}
defer rwc.Close()
// Load the key from the TPM
srkHandle, err := regenerateSRK(rwc, srkPassword)
defer func() {
tpm2.FlushContext(rwc, srkHandle)
}()
if err != nil {
return "", fmt.Errorf("unable to regenerate SRK from the TPM: %v", err)
}
// Read the keys from the key file. If the filed doesn't exist it means we need to create
// a new js encrytpion key.
publicBlob, privateBlob, err := readTPMKeysFromFile(jsKeyFile)
if err != nil {
if os.IsNotExist(err) {
jsek, err := createAndSealJsEncryptionKey(rwc, srkHandle, srkPassword, jsKeyFile, jsKeyPassword, pcr)
if err != nil {
return "", fmt.Errorf("unable to generate new key from the TPM: %v", err)
}
// we've created and sealed the JS Encryption key, now we just return it.
return jsek, nil
}
return "", fmt.Errorf("unable to load key from TPM: %v", err)
}
// Unseal the JetStream encrpytion key using the TPM.
jsek, err := unsealJsEncrpytionKey(rwc, pcr, srkHandle, srkPassword, jsKeyPassword, publicBlob, privateBlob)
if err != nil {
return "", fmt.Errorf("unable to unseal key from the TPM: %v", err)
}
return jsek, nil
}