Initial QSfera import
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright 2024 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package tpm
|
||||
|
||||
import "fmt"
|
||||
|
||||
// LoadJetStreamEncryptionKeyFromTPM here is a stub for unsupported platforms.
|
||||
func LoadJetStreamEncryptionKeyFromTPM(srkPassword, jsKeyFile, jsKeyPassword string, pcr int) (string, error) {
|
||||
return "", fmt.Errorf("TPM functionality is not supported on this platform")
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
// Copyright 2024 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package tpm
|
||||
|
||||
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 (
|
||||
// Version of the NATS TPM JS implmentation
|
||||
JsKeyTPMVersion = 1
|
||||
)
|
||||
|
||||
// How this works:
|
||||
// Create a Storage Root Key (SRK) in the TPM.
|
||||
// If existing JS Encryption 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 encryption 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.
|
||||
//
|
||||
// Note: a SRK password for the SRK is supported but not tested here.
|
||||
|
||||
// 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:"private_key"`
|
||||
PublicKey []byte `json:"public_key"`
|
||||
}
|
||||
|
||||
// Writes the private and public blobs to disk in a single file. If the directory does
|
||||
// not exist, it will be created. If the file already exists it will be overwritten.
|
||||
func writeTPMKeysToFile(filename string, privateBlob []byte, publicBlob []byte) error {
|
||||
keyDir := filepath.Dir(filename)
|
||||
if err := os.MkdirAll(keyDir, 0750); 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 necessarily
|
||||
// need to be protected as the TPM password is required to use unseal, although it's
|
||||
// a good idea to put this in a secure location accessible to the server.
|
||||
tpmKeys := natsTPMPersistedKeys{
|
||||
Version: JsKeyTPMVersion,
|
||||
PrivateKey: make([]byte, base64.StdEncoding.EncodedLen(len(privateBlob))),
|
||||
PublicKey: make([]byte, base64.StdEncoding.EncodedLen(len(publicBlob))),
|
||||
}
|
||||
base64.StdEncoding.Encode(tpmKeys.PrivateKey, privateBlob)
|
||||
base64.StdEncoding.Encode(tpmKeys.PublicKey, 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, 0640); 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) {
|
||||
keysJSON, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, nil, 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.
|
||||
privateBlob := make([]byte, base64.StdEncoding.DecodedLen(len(tpmKeys.PrivateKey)))
|
||||
publicBlob := make([]byte, base64.StdEncoding.DecodedLen(len(tpmKeys.PublicKey)))
|
||||
prn, err := base64.StdEncoding.Decode(privateBlob, tpmKeys.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to decode privateBlob from base64: %v", err)
|
||||
}
|
||||
pun, err := base64.StdEncoding.Decode(publicBlob, tpmKeys.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to decode publicBlob from base64: %v", err)
|
||||
}
|
||||
return publicBlob[:pun], privateBlob[:prn], 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)
|
||||
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 encryption 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 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 tpm2.FlushContext(rwc, objectHandle)
|
||||
|
||||
// Create the authorization session with TPM.
|
||||
sessHandle, _, err := policyPCRPasswordSession(rwc, pcr)
|
||||
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.
|
||||
func policyPCRPasswordSession(rwc io.ReadWriteCloser, pcr int) (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 keyfile does not exist, a key 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) {
|
||||
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 encryption 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
|
||||
}
|
||||
Reference in New Issue
Block a user