Skip to content

Commit

Permalink
Seal info w EK pub or cert on systems without TPM
Browse files Browse the repository at this point in the history
This patch introduces support for sealing information to an
endorsement key without needing to be on a system with a TPM. This
feature enables support for clients that want to encrypt small
amounts of data in order to bootstrap a virtual machine that may
only have an endorsement key prior to first boot.
  • Loading branch information
akutz committed Sep 14, 2023
1 parent c49efc4 commit 05ab0eb
Show file tree
Hide file tree
Showing 21 changed files with 1,798 additions and 168 deletions.
156 changes: 156 additions & 0 deletions examples/tpm2-ekseal/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Binary tpm2-ekseal seals plain-text using a provided EK public area or
// certificate and emits a duplicated object that can only be unsealed on the
// system that has the provided EK.
package main

import (
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"io"
"os"

"github.com/google/go-tpm/tpm2"
)

var (
flagEKPub = flag.String(
"ek-pub", "",
"Path to the endorsement key's public area saved as a binary file.\n"+
"May not be used with -ek-pem")

flagEKPem = flag.String(
"ek-pem", "",
"Path to the endorsement key's public certificate.\n"+
"May not be used with -ek-pub")

flagPlainText = flag.String(
"plain-text", "-",
`Plain-text data. Defaults to STDIN via "-".`)

flagOutFormat = flag.String(
"f", "json",
"Output format. Options are 'json', 'cmds', and '0'.\n"+
"The format 'json' emits the encrypted data as a JSON object.\n"+
"The format 'cmds' emits three shell commands that can be copied \n"+
"and pasted into another system to easily create the encrypted\n"+
"data's three parts as files.\n"+
"The format '0' emits the encrypted data as base64-encoded\n"+
"data structures delimited by @@NULL@@.\n"+
"Both the 'cmds' and '0' formats may be used with unseal.sh on a\n"+
"Linux system with tpm2-tools to unseal the data.",
)
)

func main() {
flag.Parse()

var ek tpm2.TPMTPublic

switch {
case *flagEKPem != "" && *flagEKPub != "":
fmt.Fprintln(os.Stderr, "-ek-pem and -ek-pub are mutually exclusive")
os.Exit(1)
case *flagEKPem != "":
pemData, err := os.ReadFile(*flagEKPem)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to read ek pem data: %s", err)
os.Exit(1)
}
pemBlock, _ := pem.Decode([]byte(pemData))
if pemBlock == nil {
fmt.Fprintln(os.Stderr, "failed to decode ek pem data")
os.Exit(1)
}
cert, err := x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load ek cert %s", err)
os.Exit(1)
}
if ek, err = tpm2.EKCertToTPMTPublic(*cert); err != nil {
fmt.Fprintf(os.Stderr, "failed to load ek from cert %s", err)
os.Exit(1)
}
case *flagEKPub != "":
ekData, err := os.ReadFile(*flagEKPub)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to read ek binary data: %s", err)
os.Exit(1)
}
ekTPM2BPublic, err := tpm2.Unmarshal[tpm2.TPM2BPublic](ekData)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load ek: %s", err)
os.Exit(1)
}
ekPtr, err := ekTPM2BPublic.Contents()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to unbox ek: %s", err)
os.Exit(1)
}
ek = *ekPtr
}

// Read the plain-text.
var plainTextFile *os.File
if *flagPlainText == "-" {
plainTextFile = os.Stdin
} else {
f, err := os.Open(*flagPlainText)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to open input file: %s", err)
os.Exit(1)
}
plainTextFile = f
defer f.Close()
}
plainText, err := io.ReadAll(plainTextFile)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to read input data: %s", err)
os.Exit(1)
}

pub, priv, seed, err := tpm2.EKSeal(ek, plainText)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to seal plain-text: %s", err)
os.Exit(1)
}

pubData, privData, seedData :=
tpm2.Marshal(pub),
tpm2.Marshal(priv),
tpm2.Marshal(seed)

switch *flagOutFormat {
case "json":
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(struct {
Public []byte `json:"public"`
Private []byte `json:"private"`
Seed []byte `json:"seed"`
}{
Public: pubData,
Private: privData,
Seed: seedData,
}); err != nil {
fmt.Fprintf(os.Stderr, "failed to encode duped object: %s", err)
os.Exit(1)
}
case "cmds":
fmt.Printf("echo '%s' | base64 -d >enc.bin.pub;",
base64.StdEncoding.EncodeToString(pubData))
fmt.Printf("echo '%s' | base64 -d >enc.bin.priv;",
base64.StdEncoding.EncodeToString(privData))
fmt.Printf("echo '%s' | base64 -d >enc.bin.seed\n",
base64.StdEncoding.EncodeToString(seedData))
case "0":
fmt.Printf("%s@@NULL@@%s@@NULL@@%s",
base64.StdEncoding.EncodeToString(pubData),
base64.StdEncoding.EncodeToString(privData),
base64.StdEncoding.EncodeToString(seedData))
}

}
236 changes: 236 additions & 0 deletions examples/tpm2-ekseal/tpm2-ekunseal.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
#!/bin/sh
#
# A POSIX-compliant shell script
#

set -o errexit # Exits immediately on unexpected errors (does not bypass traps)
set -o nounset # Errors if variables are used without first being defined

################################################################################
## USAGE
################################################################################

USAGE="USAGE: ${0} [FLAGS]
unseals information sealed tog this system's endorsement key
FLAGS
-h prints this help
-0 read the encrypted data via stdin as the TPM2B_PUBLIC, TPM2B_PRIVATE,
and TPM2B_ENCRYPTED_SECRET data structures serialized as binary data,
encoded with base64, and delimited with '@@NULL@@'
-u PATH a binary file with the encrypted data's TPM2B_PUBLIC struct
-i PATH a binary file with the encrypted data's TPM2B_PRIVATE struct
-s PATH a binary file with the encrypted data's TPM2B_ENCRYPTED_SECRET struct
-G ALG ek algorithm. valid choices are rsa and ecc. defaults to rsa
"

################################################################################
## TPM_HANDLES
################################################################################

TPK_EK_CRT='0x01C00002' # reserved handle for endorsement key cert
# - https://via.vmw.com/tpm2-provisioning-guide

TPM_RH_ENDORSEMENT='0x4000000B' # reserved handle for endorsement primary seed
# - https://via.vmw.com/TPM_RH_ENDORSEMENT
# - https://via.vmw.com/tpm2-structs


################################################################################
## temp files
################################################################################

# Create some temporary files used in decrypting the data.
EK_CTX="$(mktemp)"; rm -f "${EK_CTX}"
SESSION_DAT="$(mktemp)"; rm -f "${SESSION_DAT}"
ENC_OBJ_CTX="$(mktemp)"; rm -f "${ENC_OBJ_CTX}"
ENC_OBJ_KEY="$(mktemp)"; rm -f "${ENC_OBJ_KEY}"
ENC_OBJ_PUB="$(mktemp)"; rm -f "${ENC_OBJ_PUB}"
ENC_OBJ_PRIV="$(mktemp)"; rm -f "${ENC_OBJ_PRIV}"
ENC_OBJ_SEED="$(mktemp)"; rm -f "${ENC_OBJ_SEED}"


################################################################################
## flag values
################################################################################

READ_STDIN=""
EK_ALG="rsa"


################################################################################
## funcs
################################################################################

# error stores exit code, writes arguments to STDERR, and returns stored exit
# code fatal is like error except it will exit program if exit code >0
error() {
exit_code="${?}"
echo "${@}" 1>&2
return "${exit_code}"
}
fatal() {
error "${@}"
exit_code="${?}"
[ "${exit_code}" -gt "0" ] || exit_code=1
exit "${exit_code}"
}
check_command() {
command -v "${1}" >/dev/null 2>&1 || fatal "${1} is required"
}
check_dependencies() {
check_command tpm2_createek # https://via.vmw.com/tpm2_createek.md
check_command tpm2_flushcontext # https://via.vmw.com/tpm2_flushcontext.md
check_command tpm2_import # https://via.vmw.com/tpm2_import.md
check_command tpm2_load # https://via.vmw.com/tpm2_load.md
check_command tpm2_nvread # https://via.vmw.com/tpm2_nvread.md
check_command tpm2_policysecret # https://via.vmw.com/tpm2_policysecret.md
check_command tpm2_readpublic # https://via.vmw.com/tpm2_readpublic.md
check_command tpm2_startauthsession # https://via.vmw.com/tpm2_startauthsession.md
check_command tpm2_unseal # https://via.vmw.com/tpm2_unseal.md
}
cleanup() {
# remove any temp files that might exist
rm -f "${EK_CTX}" \
"${SESSION_DAT}" \
"${ENC_OBJ_CTX}" \
"${ENC_OBJ_KEY}" \

# only delete the encrypted object's public, private, and seed files if they
# were created as temporary files due to the input coming from stdin
if [ "${READ_STDIN}" = "1" ]; then
rm -f "${ENC_OBJ_PUB}" \
"${ENC_OBJ_PRIV}" \
"${ENC_OBJ_SEED}"
fi
}


################################################################################
## main
################################################################################

# Clean up any lingering files on exit.
trap cleanup EXIT

# Verify the required dependencies are met.
check_dependencies

# Parse the command line arguments
while getopts ":h0u:i:s:G:" opt; do
case ${opt} in
h)
fatal "${USAGE}"
;;
0)
READ_STDIN="1"
;;
u)
ENC_OBJ_PUB="${OPTARG}"
;;
i)
ENC_OBJ_PRIV="${OPTARG}"
;;
s)
ENC_OBJ_SEED="${OPTARG}"
;;
G)
EK_ALG="${OPTARG}"
;;
*)
# Ignore other flags
;;
esac
done
shift $((OPTIND - 1))

if [ "${READ_STDIN}" = "1" ]; then
read -r stdin
echo "${stdin}" | awk -F'@@NULL@@' '{print $1}' | base64 -d >"${ENC_OBJ_PUB}"
echo "${stdin}" | awk -F'@@NULL@@' '{print $2}' | base64 -d >"${ENC_OBJ_PRIV}"
echo "${stdin}" | awk -F'@@NULL@@' '{print $3}' | base64 -d >"${ENC_OBJ_SEED}"
fi

if [ ! "${EK_ALG:-}" = "rsa" ] && [ ! "${EK_ALG:-}" = "ecc" ]; then
fatal "-G ${EK_ALG:-} is invalid. valid choices are rsa or ecc"
fi


#
# VALIDATE EK CERT
#

# vSphere VMs with a vTPM have an EK certificate in their NVRAM at the address
# TPK_EK_CRT. Exit with an error if this certificate does not exist.
tpm2_nvread "${TPK_EK_CRT}" >/dev/null 2>&1 || fatal "missing ek cert"


#
# CREATE EK
#

# While vSphere VMs with a vTPM have an EK certificate in their NVRAM, the
# actual EK is not created by default.
tpm2_createek -c "${EK_CTX}" -G "${EK_ALG}" 1>&2


#
# IMPORT
#

# Create an authentication session with the TPM in order to use it to unseal
# the provided data.
tpm2_startauthsession --policy-session -S "${SESSION_DAT}" 1>&2

# Couple the authentication session with the EK's primary seed.
tpm2_policysecret -c "${TPM_RH_ENDORSEMENT}" -S "${SESSION_DAT}" 1>&2

# Import the encrypted object's cryptographic information as a child of the EK
# and persist to disk the encrypted, private portion of the object to be
# decrypted.
tpm2_import \
-C "${EK_CTX}" \
-P "session:${SESSION_DAT}" \
-u "${ENC_OBJ_PUB}" \
-i "${ENC_OBJ_PRIV}" \
-s "${ENC_OBJ_SEED}" \
-r "${ENC_OBJ_KEY}" 1>&2

# Flush the session context.
tpm2_flushcontext "${SESSION_DAT}" 1>&2

#
# LOAD
#

# Create an authentication session with the TPM in order to use it to unseal
# the provided data.
tpm2_startauthsession --policy-session -S "${SESSION_DAT}" 1>&2

# Couple the authentication session with the EK's primary seed.
tpm2_policysecret -c "${TPM_RH_ENDORSEMENT}" -S "${SESSION_DAT}" 1>&2

# Load the encrypted object into the specified context file so it can be
# unsealed. This step requires the key from the previous step.
tpm2_load \
-C "${EK_CTX}" \
-P "session:${SESSION_DAT}" \
-u "${ENC_OBJ_PUB}" \
-r "${ENC_OBJ_KEY}" \
-c "${ENC_OBJ_CTX}" 1>&2

# Flush the session context.
tpm2_flushcontext "${SESSION_DAT}" 1>&2

#
# UNSEAL
#

# Create an authentication session with the TPM in order to use it to unseal
# the provided data.
tpm2_startauthsession --policy-session -S "${SESSION_DAT}" 1>&2

tpm2_unseal -p "session:${SESSION_DAT}" -c "${ENC_OBJ_CTX}"

# Flush the session context.
tpm2_flushcontext "${SESSION_DAT}" 1>&2
Loading

0 comments on commit 05ab0eb

Please sign in to comment.