From 05ab0eba850b3e03ba9489e2942f8fa6d019f035 Mon Sep 17 00:00:00 2001 From: akutz Date: Thu, 14 Sep 2023 10:45:53 -0500 Subject: [PATCH] Seal info w EK pub or cert on systems without TPM 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. --- examples/tpm2-ekseal/main.go | 156 +++++++++ examples/tpm2-ekseal/tpm2-ekunseal.sh | 236 ++++++++++++++ tpm2/constants.go | 8 + tpm2/crypto.go | 438 ++++++++++++++++++++++++++ tpm2/crypto_test.go | 149 +++++++++ tpm2/marshalling_test.go | 94 +++--- tpm2/sessions.go | 124 +------- tpm2/structures.go | 17 + tpm2/structures_test.go | 67 ++++ tpm2/templates.go | 26 ++ tpm2/testdata/ek-ecc-crt.pem | 26 ++ tpm2/testdata/ek-ecc-crt.txt | 63 ++++ tpm2/testdata/ek-ecc.bin | Bin 0 -> 124 bytes tpm2/testdata/ek-ecc.yaml | 36 +++ tpm2/testdata/ek-rsa-crt.pem | 30 ++ tpm2/testdata/ek-rsa-crt.txt | 75 +++++ tpm2/testdata/ek-rsa.bin | Bin 0 -> 316 bytes tpm2/testdata/ek-rsa.yaml | 28 ++ tpm2/tpm2_test.go | 25 ++ tpm2/x509.go | 57 ++++ tpm2/x509_test.go | 311 ++++++++++++++++++ 21 files changed, 1798 insertions(+), 168 deletions(-) create mode 100644 examples/tpm2-ekseal/main.go create mode 100644 examples/tpm2-ekseal/tpm2-ekunseal.sh create mode 100644 tpm2/crypto_test.go create mode 100644 tpm2/structures_test.go create mode 100644 tpm2/testdata/ek-ecc-crt.pem create mode 100644 tpm2/testdata/ek-ecc-crt.txt create mode 100644 tpm2/testdata/ek-ecc.bin create mode 100644 tpm2/testdata/ek-ecc.yaml create mode 100644 tpm2/testdata/ek-rsa-crt.pem create mode 100644 tpm2/testdata/ek-rsa-crt.txt create mode 100644 tpm2/testdata/ek-rsa.bin create mode 100644 tpm2/testdata/ek-rsa.yaml create mode 100644 tpm2/tpm2_test.go create mode 100644 tpm2/x509.go create mode 100644 tpm2/x509_test.go diff --git a/examples/tpm2-ekseal/main.go b/examples/tpm2-ekseal/main.go new file mode 100644 index 00000000..0588f045 --- /dev/null +++ b/examples/tpm2-ekseal/main.go @@ -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)) + } + +} diff --git a/examples/tpm2-ekseal/tpm2-ekunseal.sh b/examples/tpm2-ekseal/tpm2-ekunseal.sh new file mode 100644 index 00000000..c6344fc9 --- /dev/null +++ b/examples/tpm2-ekseal/tpm2-ekunseal.sh @@ -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 diff --git a/tpm2/constants.go b/tpm2/constants.go index bb517c36..6f4e965b 100644 --- a/tpm2/constants.go +++ b/tpm2/constants.go @@ -675,3 +675,11 @@ const ( // a pinLimit TPMNTPinPass TPMNT = 0x9 ) + +// MaxSymData is the maximum number of octets that may be in a sealed blob. +// +// See definition in Part 4: Supporting routines, Table 7 as well as Part 2: +// Structures, section 11.1.13, which states "For interoperability, MAX_SYM_DATA +// should be 128." So while it *is* possible to seal data with a length that +// exceeds MaxSymData, it is not recommended to do so. +const MaxSymData = 0x00000080 diff --git a/tpm2/crypto.go b/tpm2/crypto.go index c8eb2bc7..cd98d94a 100644 --- a/tpm2/crypto.go +++ b/tpm2/crypto.go @@ -1,8 +1,17 @@ package tpm2 import ( + "bytes" + "crypto" + "crypto/aes" + "crypto/cipher" "crypto/elliptic" + "crypto/hmac" + "crypto/rand" "crypto/rsa" + "crypto/x509" + "encoding/binary" + "fmt" "math/big" ) @@ -41,3 +50,432 @@ func ECCPub(parms *TPMSECCParms, pub *TPMSECCPoint) (*ECDHPub, error) { Y: big.NewInt(0).SetBytes(pub.Y.Buffer), }, nil } + +const ( + duplicateLabel = "DUPLICATE" + integrityLabel = "INTEGRITY" + storageLabel = "STORAGE" +) + +// EKSealOptions are options for influencing the EKSeal function. +type EKSealOptions struct { + // IgnoreMaxSymData, if true, causes EKSeal to not return an error if the + // length of the provided plain-text data exceeds MaxSymData. + IgnoreMaxSymData bool +} + +// EKSealOption is some configuration that modifies options for an EKSeal +// operation. +type EKSealOption interface { + // ApplyToEKSeal applies this configuration to the given options. + ApplyToEKSeal(*EKSealOptions) +} + +// EKSealIgnoreMaxSymData instructs EKSeal to skip validating whether or not +// the length of the provided plain-text data exceeds MaxSymData. +var EKSealIgnoreMaxSymData = ignoreMaxSymData{} + +type ignoreMaxSymData struct{} + +// ApplyToEKSeal applies this configuration to the given options. +func (ignoreMaxSymData) ApplyToEKSeal(opts *EKSealOptions) { + opts.IgnoreMaxSymData = true +} + +// EKSeal encrypts the provided plain-text data so only the TPM with the +// specified endorsement key will be able to decrypt the data. +func EKSeal( + ek TPMTPublic, + plainText []byte, + options ...EKSealOption) (TPM2BPublic, TPM2BPrivate, TPM2BEncryptedSecret, error) { + + // Apply the options. + var opts EKSealOptions + for i := range options { + options[i].ApplyToEKSeal(&opts) + } + + // If the length of the plain-text data exceeds MaxSymData and the + // IgnoreMaxSymData option was not set, return an error. + if lpt := len(plainText); lpt > MaxSymData && !opts.IgnoreMaxSymData { + return TPM2BPublic{}, TPM2BPrivate{}, TPM2BEncryptedSecret{}, + fmt.Errorf("len(plainText)=%d > MaxSymData=%d", lpt, MaxSymData) + } + + public, private, err := keyedHashFromSecret(plainText, ek.NameAlg) + if err != nil { + return TPM2BPublic{}, TPM2BPrivate{}, TPM2BEncryptedSecret{}, + fmt.Errorf("failed to get keyed hash: %w", err) + } + + return wrap(ek, public, private) +} + +// EKCertSeal encrypts the provided plain-text data so only the TPM with the +// endorsement key matching the specified certificate will be able to decrypt +// the data. +func EKCertSeal( + ekCert x509.Certificate, + plainText []byte, + options ...EKSealOption) (TPM2BPublic, TPM2BPrivate, TPM2BEncryptedSecret, error) { + + ek, err := EKCertToTPMTPublic(ekCert) + if err != nil { + return TPM2BPublic{}, TPM2BPrivate{}, TPM2BEncryptedSecret{}, + fmt.Errorf("failed to parse ek cert: %w", err) + } + + return EKSeal(ek, plainText, options...) +} + +// keyedHashFromSecret is based on the keyedhash_from_secret functions from +// TPM2B_SENSITIVE -- https://github.com/tpm2-software/tpm2-pytss/blob/1411ebd916467f3ad4032e4fa02b321a4c1528a1/src/tpm2_pytss/types.py#L1423-L1462 +// and +// TPMT_SENSITIVE -- https://github.com/tpm2-software/tpm2-pytss/blob/1411ebd916467f3ad4032e4fa02b321a4c1528a1/src/tpm2_pytss/types.py#L2113-L2154 +func keyedHashFromSecret( + plainText []byte, + nameAlg TPMIAlgHash) (TPMTPublic, TPMTSensitive, error) { + + hashID, err := nameAlg.Hash() + if err != nil { + return TPMTPublic{}, TPMTSensitive{}, fmt.Errorf( + "failed to get hash algorithm: %w", err) + } + + seed, err := newSeed(hashID.Size()) + if err != nil { + return TPMTPublic{}, TPMTSensitive{}, fmt.Errorf( + "failed to generate seed: %w", err) + } + + symmetricData, err := calculateSymUnique(hashID, plainText, seed) + if err != nil { + return TPMTPublic{}, TPMTSensitive{}, fmt.Errorf( + "failed to calculate symmetric data: %w", err) + } + + private := TPMTSensitive{ + SensitiveType: TPMAlgKeyedHash, + SeedValue: TPM2BDigest{ + Buffer: seed, + }, + Sensitive: NewTPMUSensitiveComposite( + TPMAlgKeyedHash, + &TPM2BSensitiveData{ + Buffer: plainText, + }, + ), + } + + public := TPMTPublic{ + Type: TPMAlgKeyedHash, + NameAlg: TPMAlgSHA256, + AuthPolicy: TPM2BDigest{ + Buffer: bytes.Repeat([]byte{'\x00'}, 32), + }, + ObjectAttributes: TPMAObject{ + NoDA: true, + }, + Parameters: NewTPMUPublicParms( + TPMAlgKeyedHash, + &TPMSKeyedHashParms{ + Scheme: TPMTKeyedHashScheme{ + Scheme: TPMAlgNull, + }, + }, + ), + Unique: NewTPMUPublicID( + TPMAlgKeyedHash, + &TPM2BDigest{ + Buffer: symmetricData, + }, + ), + } + + return public, private, nil +} + +// calculateSymUnique is based on the _calculate_sym_unique function from +// https://github.com/tpm2-software/tpm2-pytss/blob/1411ebd916467f3ad4032e4fa02b321a4c1528a1/src/tpm2_pytss/internal/crypto.py#L387-L394 +func calculateSymUnique( + hashID crypto.Hash, plainText, seed []byte) ([]byte, error) { + + hash := hashID.New() + + // Write the seed to the hash. + if n, err := hash.Write(seed); err != nil { + return nil, fmt.Errorf("failed to write seed to hash: %w", err) + } else if a, e := n, len(seed); a != e { + return nil, fmt.Errorf( + "failed to write seed to hash; act=%d, exp=%d", a, e) + } + + // Write the plain-text to the hash. + if n, err := hash.Write(plainText); err != nil { + return nil, fmt.Errorf("failed to write plain-text to hash: %w", err) + } else if a, e := n, len(plainText); a != e { + return nil, fmt.Errorf( + "failed to write plain-text to hash; act=%d, exp=%d", a, e) + } + + return hash.Sum(nil), nil +} + +// wrap is based on the wrap function from +// https://github.com/tpm2-software/tpm2-pytss/blob/1411ebd916467f3ad4032e4fa02b321a4c1528a1/src/tpm2_pytss/utils.py#L134-L209 +func wrap( + parent TPMTPublic, + public TPMTPublic, + sensitive TPMTSensitive) ( + + TPM2BPublic, + TPM2BPrivate, + TPM2BEncryptedSecret, + error) { + + var ( + dupePub TPM2BPublic + dupePriv TPM2BPrivate + dupeSeed TPM2BEncryptedSecret + ) + + parentHashID, err := parent.NameAlg.Hash() + if err != nil { + return dupePub, dupePriv, dupeSeed, + fmt.Errorf("failed to get parent hash id: %w", err) + } + + pubName, err := ObjectName(&public) + if err != nil { + return dupePub, dupePriv, dupeSeed, + fmt.Errorf("failed to get public name: %w", err) + } + + bits, err := symmetricDefinitionToCrypto(parent) + if err != nil { + return dupePub, dupePriv, dupeSeed, + fmt.Errorf("failed to symdef to crypto: %w", err) + } + + encSalt, salt, err := getEncryptedSalt(parent, duplicateLabel) + if err != nil { + return dupePub, dupePriv, dupeSeed, + fmt.Errorf("failed to get encrypted salt: %w", err) + } + + outerKey := KDFa( + parentHashID, + salt, + storageLabel, + pubName.Buffer, nil, + bits) + dupeSensitive, err := encryptAESCFB(outerKey, Marshal(New2B(sensitive))) + if err != nil { + return dupePub, dupePriv, dupeSeed, + fmt.Errorf("failed to enc sensitive data: %w", err) + } + + hmacKey := KDFa( + parentHashID, + salt, + integrityLabel, + nil, nil, + parentHashID.Size()*8) + hmacHash := hmac.New(parentHashID.New, hmacKey) + hmacHash.Write(dupeSensitive) + hmacHash.Write(pubName.Buffer) + hmacData := Marshal(TPM2BDigest{Buffer: hmacHash.Sum(nil)}) + + dupePub = New2B(public) + dupePriv.Buffer = append(hmacData, dupeSensitive...) + dupeSeed.Buffer = encSalt.Buffer + + return dupePub, dupePriv, dupeSeed, nil +} + +// symmetricDefinitionToCrypto is based on the _symdef_to_crypt function from +// https://github.com/tpm2-software/tpm2-pytss/blob/1411ebd916467f3ad4032e4fa02b321a4c1528a1/src/tpm2_pytss/internal/crypto.py#L376-L384 +func symmetricDefinitionToCrypto( + public TPMTPublic) (int, error) { + + var symDef TPMTSymDefObject + + switch public.Type { + case TPMAlgRSA: + asymDetail, err := public.Parameters.RSADetail() + if err != nil { + return 0, fmt.Errorf("failed to get asymmetric rsa detail: %w", err) + } + symDef = asymDetail.Symmetric + case TPMAlgECC: + asymDetail, err := public.Parameters.ECCDetail() + if err != nil { + return 0, fmt.Errorf("failed to get asymmetric ecc detail: %w", err) + } + symDef = asymDetail.Symmetric + default: + return 0, fmt.Errorf("unsupported type: %v", public.Type) + } + + if symAlg := symDef.Algorithm; symAlg != TPMAlgAES { + return 0, fmt.Errorf( + "invalid sym alg id: exp=%v, act=%v", TPMAlgAES, symAlg) + } + + // For whatever reason, symDef.Mode is set to AlgID=6, AES, even though + // the EK's public area should set it to CFB. Thus it cannot be verified + // here or else it will return an error. + + keyBits, err := symDef.KeyBits.AES() + if err != nil { + return 0, fmt.Errorf("failed to get sym key bits: %w", err) + } + if keyBits == nil { + return 0, fmt.Errorf("sym key bits are nil") + } + + return int(*keyBits), nil +} + +// newSeed returns a byte slice of the specified size filled with random data +// from the crypto.Rand reader. +func newSeed(size int) ([]byte, error) { + seed := make([]byte, size) + if n, err := rand.Read(seed); err != nil { + return nil, err + } else if n != len(seed) { + return nil, fmt.Errorf( + "invalid seed length; exp=%d, act=%d", size, n) + } + return seed, nil +} + +func encryptAESCFB(key, plainText []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("failed to get aes cipher: %w", err) + } + iv := bytes.Repeat([]byte{'\x00'}, len(key)) + cipherText := make([]byte, len(plainText)) + cfb := cipher.NewCFBEncrypter(block, iv) + cfb.XORKeyStream(cipherText, plainText) + return cipherText, nil +} + +// Part 1, B.10.2 +func getEncryptedSaltRSA(nameAlg TPMIAlgHash, parms *TPMSRSAParms, pub *TPM2BPublicKeyRSA, label string) (*TPM2BEncryptedSecret, []byte, error) { + rsaPub, err := RSAPub(parms, pub) + if err != nil { + return nil, nil, fmt.Errorf("could not encrypt salt to RSA key: %w", err) + } + // Odd special case: the size of the salt depends on the RSA scheme's + // hash alg. + var hAlg TPMIAlgHash + switch parms.Scheme.Scheme { + case TPMAlgRSASSA: + rsassa, err := parms.Scheme.Details.RSASSA() + if err != nil { + return nil, nil, err + } + hAlg = rsassa.HashAlg + case TPMAlgRSAES: + hAlg = nameAlg + case TPMAlgRSAPSS: + rsapss, err := parms.Scheme.Details.RSAPSS() + if err != nil { + return nil, nil, err + } + hAlg = rsapss.HashAlg + case TPMAlgOAEP: + oaep, err := parms.Scheme.Details.OAEP() + if err != nil { + return nil, nil, err + } + hAlg = oaep.HashAlg + case TPMAlgNull: + hAlg = nameAlg + default: + return nil, nil, fmt.Errorf("unsupported RSA salt key scheme: %v", parms.Scheme.Scheme) + } + ha, err := hAlg.Hash() + if err != nil { + return nil, nil, err + } + salt, err := newSeed(ha.Size()) + if err != nil { + return nil, nil, fmt.Errorf("generating random salt: %w", err) + } + // Part 1, section 4.6 specifies the trailing NULL byte for the label. + encSalt, err := rsa.EncryptOAEP(ha.New(), rand.Reader, rsaPub, salt, []byte(label+"\x00")) + if err != nil { + return nil, nil, fmt.Errorf("encrypting salt: %w", err) + } + return &TPM2BEncryptedSecret{ + Buffer: encSalt, + }, salt, nil +} + +// Part 1, 19.6.13 +func getEncryptedSaltECC(nameAlg TPMIAlgHash, parms *TPMSECCParms, pub *TPMSECCPoint, label string) (*TPM2BEncryptedSecret, []byte, error) { + curve, err := parms.CurveID.Curve() + if err != nil { + return nil, nil, fmt.Errorf("could not encrypt salt to ECC key: %w", err) + } + eccPub, err := ECCPub(parms, pub) + if err != nil { + return nil, nil, fmt.Errorf("could not encrypt salt to ECC key: %w", err) + } + ephPriv, ephPubX, ephPubY, err := elliptic.GenerateKey(curve, rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("could not encrypt salt to ECC key: %w", err) + } + zx, _ := curve.Params().ScalarMult(eccPub.X, eccPub.Y, ephPriv) + // ScalarMult returns a big.Int, whose Bytes() function may return the + // compacted form. In our case, we want to left-pad zx to the size of + // the curve. + z := make([]byte, (curve.Params().BitSize+7)/8) + zx.FillBytes(z) + ha, err := nameAlg.Hash() + if err != nil { + return nil, nil, err + } + salt := KDFe(ha, z, label, ephPubX.Bytes(), pub.X.Buffer, ha.Size()*8) + + var encSalt bytes.Buffer + binary.Write(&encSalt, binary.BigEndian, uint16(len(ephPubX.Bytes()))) + encSalt.Write(ephPubX.Bytes()) + binary.Write(&encSalt, binary.BigEndian, uint16(len(ephPubY.Bytes()))) + encSalt.Write(ephPubY.Bytes()) + return &TPM2BEncryptedSecret{ + Buffer: encSalt.Bytes(), + }, salt, nil +} + +// getEncryptedSalt creates a salt value for salted sessions. +// Returns the encrypted salt and plaintext salt, or an error value. +func getEncryptedSalt(pub TPMTPublic, label string) (*TPM2BEncryptedSecret, []byte, error) { + switch pub.Type { + case TPMAlgRSA: + rsaParms, err := pub.Parameters.RSADetail() + if err != nil { + return nil, nil, err + } + rsaPub, err := pub.Unique.RSA() + if err != nil { + return nil, nil, err + } + return getEncryptedSaltRSA(pub.NameAlg, rsaParms, rsaPub, label) + case TPMAlgECC: + eccParms, err := pub.Parameters.ECCDetail() + if err != nil { + return nil, nil, err + } + eccPub, err := pub.Unique.ECC() + if err != nil { + return nil, nil, err + } + return getEncryptedSaltECC(pub.NameAlg, eccParms, eccPub, label) + default: + return nil, nil, fmt.Errorf("salt encryption alg '%v' not supported", pub.Type) + } +} diff --git a/tpm2/crypto_test.go b/tpm2/crypto_test.go new file mode 100644 index 00000000..26e6e133 --- /dev/null +++ b/tpm2/crypto_test.go @@ -0,0 +1,149 @@ +package tpm2_test + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "os" + "strings" + "testing" + + "github.com/google/go-tpm/tpm2" +) + +func TestEKSeal(t *testing.T) { + testCases := []struct { + name string + ekFile string + plainText string + expErr error + options []tpm2.EKSealOption + }{ + { + name: "RSA", + ekFile: "./testdata/ek-rsa.bin", + plainText: "Hello, world.", + }, + { + name: "ECC", + ekFile: "./testdata/ek-ecc.bin", + plainText: "Hello, world.", + }, + { + name: "Exceeds MaxSymData", + ekFile: "./testdata/ek-rsa.bin", + plainText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Is129.", + expErr: fmt.Errorf("len(plainText)=129 > MaxSymData=128"), + }, + { + name: "Exceeds MaxSymData and IgnoreMaxSymData", + ekFile: "./testdata/ek-rsa.bin", + plainText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Is129.", + options: []tpm2.EKSealOption{tpm2.EKSealIgnoreMaxSymData}, + }, + } + + for i := range testCases { + tc := testCases[i] + t.Run(tc.name, func(t *testing.T) { + ekData, err := os.ReadFile(tc.ekFile) + if err != nil { + t.Fatalf("failed to read ek binary data") + } + ekTPM2BPublic, err := tpm2.Unmarshal[tpm2.TPM2BPublic](ekData) + if err != nil { + t.Fatalf("failed to load ek: %s", err) + } + ek, err := ekTPM2BPublic.Contents() + if err != nil { + t.Fatalf("failed to unbox ek: %s", err) + } + + pub, priv, seed, err := tpm2.EKSeal(*ek, []byte(tc.plainText), tc.options...) + if !assertExpectedError(t, true, err, tc.expErr) { + return + } + ekSealTestPrintTrailingCommands(t, tc.name, pub, priv, seed) + }) + } +} + +func TestEKCertSeal(t *testing.T) { + testCases := []struct { + name string + pemFile string + plainText string + expErr error + options []tpm2.EKSealOption + }{ + { + name: "RSA", + pemFile: "./testdata/ek-rsa-crt.pem", + plainText: "Hello, world.", + }, + { + name: "ECC", + pemFile: "./testdata/ek-ecc-crt.pem", + plainText: "Hello, world.", + }, + { + name: "Exceeds MaxSymData", + pemFile: "./testdata/ek-rsa-crt.pem", + plainText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Is129.", + expErr: fmt.Errorf("len(plainText)=129 > MaxSymData=128"), + }, + { + name: "Exceeds MaxSymData and IgnoreMaxSymData", + pemFile: "./testdata/ek-rsa-crt.pem", + plainText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Is129.", + options: []tpm2.EKSealOption{tpm2.EKSealIgnoreMaxSymData}, + }, + } + + for i := range testCases { + tc := testCases[i] + t.Run(tc.name, func(t *testing.T) { + pemData, err := os.ReadFile(tc.pemFile) + if err != nil { + t.Fatalf("failed to read ek pem data") + } + pemBlock, _ := pem.Decode([]byte(pemData)) + if pemBlock == nil { + t.Fatalf("failed to decode ek pem data") + } + cert, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + t.Fatalf("failed to load ek cert: %s", err) + } + + pub, priv, seed, err := tpm2.EKCertSeal( + *cert, + []byte(tc.plainText), + tc.options..., + ) + if !assertExpectedError(t, true, err, tc.expErr) { + return + } + ekSealTestPrintTrailingCommands(t, tc.name, pub, priv, seed) + }) + } +} + +func ekSealTestPrintTrailingCommands( + t *testing.T, + testCaseName string, + pub tpm2.TPM2BPublic, + priv tpm2.TPM2BPrivate, + seed tpm2.TPM2BEncryptedSecret) { + + t.Logf("\n\n"+ + "# Copy the following line onto the system with the EK\n"+ + "# and use ../examples/tpm2-ekseal/tpm2-ekunseal.sh along with\n"+ + "# tpm2-tools to unseal the data.\n\n"+ + " echo '%s@@NULL@@%s@@NULL@@%s' | unseal.sh -0 -G %s\n\n", + base64.StdEncoding.EncodeToString(tpm2.Marshal(pub)), + base64.StdEncoding.EncodeToString(tpm2.Marshal(priv)), + base64.StdEncoding.EncodeToString(tpm2.Marshal(seed)), + strings.ToLower(testCaseName)) +} diff --git a/tpm2/marshalling_test.go b/tpm2/marshalling_test.go index bb4c45dc..8d933ba5 100644 --- a/tpm2/marshalling_test.go +++ b/tpm2/marshalling_test.go @@ -1,16 +1,18 @@ -package tpm2 +package tpm2_test import ( "bytes" "testing" + + "github.com/google/go-tpm/tpm2" ) func TestMarshal2B(t *testing.T) { // Define some TPMT_Public - pub := TPMTPublic{ - Type: TPMAlgKeyedHash, - NameAlg: TPMAlgSHA256, - ObjectAttributes: TPMAObject{ + pub := tpm2.TPMTPublic{ + Type: tpm2.TPMAlgKeyedHash, + NameAlg: tpm2.TPMAlgSHA256, + ObjectAttributes: tpm2.TPMAObject{ FixedTPM: true, FixedParent: true, UserWithAuth: true, @@ -19,36 +21,36 @@ func TestMarshal2B(t *testing.T) { } // Get the wire-format version - pubBytes := Marshal(pub) + pubBytes := tpm2.Marshal(pub) // Create two versions of the same 2B: // one instantiated by the actual TPMTPublic // one instantiated by the contents - var boxed1 TPM2BPublic - var boxed2 TPM2BPublic - boxed1 = New2B(pub) - boxed2 = BytesAs2B[TPMTPublic](pubBytes) + var boxed1 tpm2.TPM2BPublic + var boxed2 tpm2.TPM2BPublic + boxed1 = tpm2.New2B(pub) + boxed2 = tpm2.BytesAs2B[tpm2.TPMTPublic](pubBytes) - boxed1Bytes := Marshal(boxed1) - boxed2Bytes := Marshal(boxed2) + boxed1Bytes := tpm2.Marshal(boxed1) + boxed2Bytes := tpm2.Marshal(boxed2) if !bytes.Equal(boxed1Bytes, boxed2Bytes) { t.Errorf("got %x want %x", boxed2Bytes, boxed1Bytes) } - z, err := Unmarshal[TPM2BPublic](boxed1Bytes) + z, err := tpm2.Unmarshal[tpm2.TPM2BPublic](boxed1Bytes) if err != nil { t.Fatalf("could not unmarshal TPM2BPublic: %v", err) } t.Logf("%v", z) - boxed3Bytes := Marshal(z) + boxed3Bytes := tpm2.Marshal(z) if !bytes.Equal(boxed1Bytes, boxed3Bytes) { t.Errorf("got %x want %x", boxed3Bytes, boxed1Bytes) } // Make a nonsense 2B_Public, demonstrating that the library doesn't have to understand the serialization - BytesAs2B[TPMTPublic]([]byte{0xff}) + tpm2.BytesAs2B[tpm2.TPMTPublic]([]byte{0xff}) } func unwrap[T any](f func() (*T, error)) *T { @@ -61,94 +63,94 @@ func unwrap[T any](f func() (*T, error)) *T { func TestMarshalT(t *testing.T) { // Define some TPMT_Public - pub := TPMTPublic{ - Type: TPMAlgECC, - NameAlg: TPMAlgSHA256, - ObjectAttributes: TPMAObject{ + pub := tpm2.TPMTPublic{ + Type: tpm2.TPMAlgECC, + NameAlg: tpm2.TPMAlgSHA256, + ObjectAttributes: tpm2.TPMAObject{ SignEncrypt: true, }, - Parameters: NewTPMUPublicParms( - TPMAlgECC, - &TPMSECCParms{ - CurveID: TPMECCNistP256, + Parameters: tpm2.NewTPMUPublicParms( + tpm2.TPMAlgECC, + &tpm2.TPMSECCParms{ + CurveID: tpm2.TPMECCNistP256, }, ), - Unique: NewTPMUPublicID( + Unique: tpm2.NewTPMUPublicID( // This happens to be a P256 EKpub from the simulator - TPMAlgECC, - &TPMSECCPoint{ - X: TPM2BECCParameter{}, - Y: TPM2BECCParameter{}, + tpm2.TPMAlgECC, + &tpm2.TPMSECCPoint{ + X: tpm2.TPM2BECCParameter{}, + Y: tpm2.TPM2BECCParameter{}, }, ), } // Marshal each component of the parameters - symBytes := Marshal(&unwrap(pub.Parameters.ECCDetail).Symmetric) + symBytes := tpm2.Marshal(&unwrap(pub.Parameters.ECCDetail).Symmetric) t.Logf("Symmetric: %x\n", symBytes) - sym, err := Unmarshal[TPMTSymDefObject](symBytes) + sym, err := tpm2.Unmarshal[tpm2.TPMTSymDefObject](symBytes) if err != nil { t.Fatalf("could not unmarshal TPMTSymDefObject: %v", err) } - symBytes2 := Marshal(sym) + symBytes2 := tpm2.Marshal(sym) if !bytes.Equal(symBytes, symBytes2) { t.Errorf("want %x\ngot %x", symBytes, symBytes2) } - schemeBytes := Marshal(&unwrap(pub.Parameters.ECCDetail).Scheme) + schemeBytes := tpm2.Marshal(&unwrap(pub.Parameters.ECCDetail).Scheme) t.Logf("Scheme: %x\n", symBytes) - scheme, err := Unmarshal[TPMTECCScheme](schemeBytes) + scheme, err := tpm2.Unmarshal[tpm2.TPMTECCScheme](schemeBytes) if err != nil { t.Fatalf("could not unmarshal TPMTECCScheme: %v", err) } - schemeBytes2 := Marshal(scheme) + schemeBytes2 := tpm2.Marshal(scheme) if !bytes.Equal(schemeBytes, schemeBytes2) { t.Errorf("want %x\ngot %x", schemeBytes, schemeBytes2) } - kdfBytes := Marshal(&unwrap(pub.Parameters.ECCDetail).KDF) + kdfBytes := tpm2.Marshal(&unwrap(pub.Parameters.ECCDetail).KDF) t.Logf("KDF: %x\n", kdfBytes) - kdf, err := Unmarshal[TPMTKDFScheme](kdfBytes) + kdf, err := tpm2.Unmarshal[tpm2.TPMTKDFScheme](kdfBytes) if err != nil { t.Fatalf("could not unmarshal TPMTKDFScheme: %v", err) } - kdfBytes2 := Marshal(kdf) + kdfBytes2 := tpm2.Marshal(kdf) if !bytes.Equal(kdfBytes, kdfBytes2) { t.Errorf("want %x\ngot %x", kdfBytes, kdfBytes2) } // Marshal the parameters - parmsBytes := Marshal(unwrap(pub.Parameters.ECCDetail)) + parmsBytes := tpm2.Marshal(unwrap(pub.Parameters.ECCDetail)) t.Logf("Parms: %x\n", parmsBytes) - parms, err := Unmarshal[TPMSECCParms](parmsBytes) + parms, err := tpm2.Unmarshal[tpm2.TPMSECCParms](parmsBytes) if err != nil { t.Fatalf("could not unmarshal TPMSECCParms: %v", err) } - parmsBytes2 := Marshal(parms) + parmsBytes2 := tpm2.Marshal(parms) if !bytes.Equal(parmsBytes, parmsBytes2) { t.Errorf("want %x\ngot %x", parmsBytes, parmsBytes2) } // Marshal the unique area - uniqueBytes := Marshal(unwrap(pub.Unique.ECC)) + uniqueBytes := tpm2.Marshal(unwrap(pub.Unique.ECC)) t.Logf("Unique: %x\n", uniqueBytes) - unique, err := Unmarshal[TPMSECCPoint](uniqueBytes) + unique, err := tpm2.Unmarshal[tpm2.TPMSECCPoint](uniqueBytes) if err != nil { t.Fatalf("could not unmarshal TPMSECCPoint: %v", err) } - uniqueBytes2 := Marshal(unique) + uniqueBytes2 := tpm2.Marshal(unique) if !bytes.Equal(uniqueBytes, uniqueBytes2) { t.Errorf("want %x\ngot %x", uniqueBytes, uniqueBytes2) } // Get the wire-format version of the whole thing - pubBytes := Marshal(&pub) + pubBytes := tpm2.Marshal(&pub) - pub2, err := Unmarshal[TPMTPublic](pubBytes) + pub2, err := tpm2.Unmarshal[tpm2.TPMTPublic](pubBytes) if err != nil { t.Fatalf("could not unmarshal TPMTPublic: %v", err) } // Some default fields might have been populated in the round-trip. Get the wire-format again and compare. - pub2Bytes := Marshal(pub2) + pub2Bytes := tpm2.Marshal(pub2) if !bytes.Equal(pubBytes, pub2Bytes) { t.Errorf("want %x\ngot %x", pubBytes, pub2Bytes) diff --git a/tpm2/sessions.go b/tpm2/sessions.go index 171adf8a..6ae6ff9c 100644 --- a/tpm2/sessions.go +++ b/tpm2/sessions.go @@ -4,10 +4,8 @@ import ( "bytes" "crypto/aes" "crypto/cipher" - "crypto/elliptic" "crypto/hmac" "crypto/rand" - "crypto/rsa" "encoding/binary" "fmt" @@ -370,124 +368,6 @@ func HMACSession(t transport.TPM, hash TPMIAlgHash, nonceSize int, opts ...AuthO return &sess, closer, nil } -// Part 1, B.10.2 -func getEncryptedSaltRSA(nameAlg TPMIAlgHash, parms *TPMSRSAParms, pub *TPM2BPublicKeyRSA) (*TPM2BEncryptedSecret, []byte, error) { - rsaPub, err := RSAPub(parms, pub) - if err != nil { - return nil, nil, fmt.Errorf("could not encrypt salt to RSA key: %w", err) - } - // Odd special case: the size of the salt depends on the RSA scheme's - // hash alg. - var hAlg TPMIAlgHash - switch parms.Scheme.Scheme { - case TPMAlgRSASSA: - rsassa, err := parms.Scheme.Details.RSASSA() - if err != nil { - return nil, nil, err - } - hAlg = rsassa.HashAlg - case TPMAlgRSAES: - hAlg = nameAlg - case TPMAlgRSAPSS: - rsapss, err := parms.Scheme.Details.RSAPSS() - if err != nil { - return nil, nil, err - } - hAlg = rsapss.HashAlg - case TPMAlgOAEP: - oaep, err := parms.Scheme.Details.OAEP() - if err != nil { - return nil, nil, err - } - hAlg = oaep.HashAlg - case TPMAlgNull: - hAlg = nameAlg - default: - return nil, nil, fmt.Errorf("unsupported RSA salt key scheme: %v", parms.Scheme.Scheme) - } - ha, err := hAlg.Hash() - if err != nil { - return nil, nil, err - } - salt := make([]byte, ha.Size()) - if _, err := rand.Read(salt); err != nil { - return nil, nil, fmt.Errorf("generating random salt: %w", err) - } - // Part 1, section 4.6 specifies the trailing NULL byte for the label. - encSalt, err := rsa.EncryptOAEP(ha.New(), rand.Reader, rsaPub, salt, []byte("SECRET\x00")) - if err != nil { - return nil, nil, fmt.Errorf("encrypting salt: %w", err) - } - return &TPM2BEncryptedSecret{ - Buffer: encSalt, - }, salt, nil -} - -// Part 1, 19.6.13 -func getEncryptedSaltECC(nameAlg TPMIAlgHash, parms *TPMSECCParms, pub *TPMSECCPoint) (*TPM2BEncryptedSecret, []byte, error) { - curve, err := parms.CurveID.Curve() - if err != nil { - return nil, nil, fmt.Errorf("could not encrypt salt to ECC key: %w", err) - } - eccPub, err := ECCPub(parms, pub) - if err != nil { - return nil, nil, fmt.Errorf("could not encrypt salt to ECC key: %w", err) - } - ephPriv, ephPubX, ephPubY, err := elliptic.GenerateKey(curve, rand.Reader) - if err != nil { - return nil, nil, fmt.Errorf("could not encrypt salt to ECC key: %w", err) - } - zx, _ := curve.Params().ScalarMult(eccPub.X, eccPub.Y, ephPriv) - // ScalarMult returns a big.Int, whose Bytes() function may return the - // compacted form. In our case, we want to left-pad zx to the size of - // the curve. - z := make([]byte, (curve.Params().BitSize+7)/8) - zx.FillBytes(z) - ha, err := nameAlg.Hash() - if err != nil { - return nil, nil, err - } - salt := KDFe(ha, z, "SECRET", ephPubX.Bytes(), pub.X.Buffer, ha.Size()*8) - - var encSalt bytes.Buffer - binary.Write(&encSalt, binary.BigEndian, uint16(len(ephPubX.Bytes()))) - encSalt.Write(ephPubX.Bytes()) - binary.Write(&encSalt, binary.BigEndian, uint16(len(ephPubY.Bytes()))) - encSalt.Write(ephPubY.Bytes()) - return &TPM2BEncryptedSecret{ - Buffer: encSalt.Bytes(), - }, salt, nil -} - -// getEncryptedSalt creates a salt value for salted sessions. -// Returns the encrypted salt and plaintext salt, or an error value. -func getEncryptedSalt(pub TPMTPublic) (*TPM2BEncryptedSecret, []byte, error) { - switch pub.Type { - case TPMAlgRSA: - rsaParms, err := pub.Parameters.RSADetail() - if err != nil { - return nil, nil, err - } - rsaPub, err := pub.Unique.RSA() - if err != nil { - return nil, nil, err - } - return getEncryptedSaltRSA(pub.NameAlg, rsaParms, rsaPub) - case TPMAlgECC: - eccParms, err := pub.Parameters.ECCDetail() - if err != nil { - return nil, nil, err - } - eccPub, err := pub.Unique.ECC() - if err != nil { - return nil, nil, err - } - return getEncryptedSaltECC(pub.NameAlg, eccParms, eccPub) - default: - return nil, nil, fmt.Errorf("salt encryption alg '%v' not supported", pub.Type) - } -} - // Init initializes the session, just in time, if needed. func (s *hmacSession) Init(t transport.TPM) error { if s.handle != TPMRHNull { @@ -517,7 +397,7 @@ func (s *hmacSession) Init(t transport.TPM) error { if s.saltHandle != TPMRHNull { var err error var encSalt *TPM2BEncryptedSecret - encSalt, salt, err = getEncryptedSalt(s.saltPub) + encSalt, salt, err = getEncryptedSalt(s.saltPub, "SECRET") if err != nil { return err } @@ -883,7 +763,7 @@ func (s *policySession) Init(t transport.TPM) error { if s.saltHandle != TPMRHNull { var err error var encSalt *TPM2BEncryptedSecret - encSalt, salt, err = getEncryptedSalt(s.saltPub) + encSalt, salt, err = getEncryptedSalt(s.saltPub, "SECRET") if err != nil { return err } diff --git a/tpm2/structures.go b/tpm2/structures.go index ac531a31..604b825a 100644 --- a/tpm2/structures.go +++ b/tpm2/structures.go @@ -2781,6 +2781,23 @@ type TPMTPublic struct { Unique TPMUPublicID `gotpm:"tag=Type"` } +// Copy returns a deep-copy of this object. +// +// Please note, due to the marshalling logic's dependency on valid union tags, +// this function panics if the source of the copy is an emptyTPMTPublic object. +func Copy[T Marshallable, P interface { + *T + Unmarshallable +}](t T) (T, error) { + + cpy, err := Unmarshal[T, P](Marshal(t)) + if err != nil { + var empty T + return empty, err + } + return *cpy, nil +} + // TPM2BPublic represents a TPM2B_PUBLIC. // See definition in Part 2: Structures, section 12.2.5. type TPM2BPublic = TPM2B[TPMTPublic, *TPMTPublic] diff --git a/tpm2/structures_test.go b/tpm2/structures_test.go new file mode 100644 index 00000000..79600c31 --- /dev/null +++ b/tpm2/structures_test.go @@ -0,0 +1,67 @@ +package tpm2_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/google/go-tpm/tpm2" +) + +func TestTPMTPublicCopy(t *testing.T) { + testCases := []struct { + name string + src tpm2.TPMTPublic + expErr error + expPanicOnCopy bool + }{ + { + name: "Panic on empty TPMTPublic object", + expPanicOnCopy: true, + expErr: fmt.Errorf("unexpected error marshalling TPMTPublic: no union member for tag 0"), + }, + { + name: "RSA EK template", + src: tpm2.RSAEKTemplate, + }, + { + name: "ECC EK template", + src: tpm2.ECCEKTemplate, + }, + { + name: "RSA EK", + src: rsaEKWithPubKey(ekRSAPubKey), + }, + { + name: "ECC EK", + src: eccEKWithPoint(ekECCPointX, ekECCPointY), + }, + } + + for i := range testCases { + tc := testCases[i] + t.Run(tc.name, func(t *testing.T) { + if tc.expPanicOnCopy { + defer func() { + if r := recover(); r != nil { + actErr := fmt.Errorf(r.(string)) + assertExpectedError(t, true, actErr, tc.expErr) + } + }() + } + dst, err := tpm2.Copy(tc.src) + if !tc.expPanicOnCopy { + if !assertExpectedError(t, true, err, tc.expErr) { + return + } + } + + srcData := tpm2.Marshal(tc.src) + dstData := tpm2.Marshal(dst) + + if !bytes.Equal(srcData, dstData) { + t.Fatal("src and dst are not equal") + } + }) + } +} diff --git a/tpm2/templates.go b/tpm2/templates.go index a0cfa811..9217372c 100644 --- a/tpm2/templates.go +++ b/tpm2/templates.go @@ -1,5 +1,9 @@ package tpm2 +import ( + "fmt" +) + var ( // RSASRKTemplate contains the TCG reference RSA-2048 SRK template. // https://trustedcomputinggroup.org/wp-content/uploads/TCG-TPM-v2.0-Provisioning-Guidance-Published-v1r1.pdf @@ -198,3 +202,25 @@ var ( ), } ) + +// RSAEKTemplateWithPublicKey returns a new TPMT_PUBLIC using the template for +// an RSA EK and the specified RSA public key. +func RSAEKTemplateWithPublicKey(pubKey TPM2BPublicKeyRSA) (TPMTPublic, error) { + ek, err := Copy(RSAEKTemplate) + if err != nil { + return TPMTPublic{}, fmt.Errorf("failed to copy rsa ek tpl: %w", err) + } + ek.Unique = NewTPMUPublicID(TPMAlgRSA, &pubKey) + return ek, err +} + +// ECCEKTemplateWithPoint returns a new TPMT_PUBLIC using the template for an +// ECC EK and the specified ECC point. +func ECCEKTemplateWithPoint(point TPMSECCPoint) (TPMTPublic, error) { + ek, err := Copy(ECCEKTemplate) + if err != nil { + return TPMTPublic{}, fmt.Errorf("failed to copy ecc ek tpl: %w", err) + } + ek.Unique = NewTPMUPublicID(TPMAlgECC, &point) + return ek, err +} diff --git a/tpm2/testdata/ek-ecc-crt.pem b/tpm2/testdata/ek-ecc-crt.pem new file mode 100644 index 00000000..095f96cb --- /dev/null +++ b/tpm2/testdata/ek-ecc-crt.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEbzCCAtegAwIBAgIJAPyDMhSaeSEEMA0GCSqGSIb3DQEBCwUAMIGoMQswCQYD +VQQDDAJDQTEXMBUGCgmSJomT8ixkARkWB3ZzcGhlcmUxFTATBgoJkiaJk/IsZAEZ +FgVsb2NhbDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExKjAoBgNV +BAoMIXNjMi0xMC0xODQtMTAzLTEyNi5lbmcudm13YXJlLmNvbTEbMBkGA1UECwwS +Vk13YXJlIEVuZ2luZWVyaW5nMB4XDTIzMDgyOTIwMzg1NloXDTMzMDgxNzA4NDU0 +MVowSDEWMBQGBWeBBQIBDAtpZDo1NjRENTcwMDEWMBQGBWeBBQICDAtWTXdhcmUg +VFBNMjEWMBQGBWeBBQIDDAtpZDowMDAyMDA2NTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABD4/Q4n5Ju60174JndC/GOJmMwtK1jXbaGv8jJsJiC1ojL3bxR/agDSn +zSejK/vMUtElZSXRyMi3oZ60sq6xOhyjggFEMIIBQDAOBgNVHQ8BAf8EBAMCAwgw +WAYDVR0RAQH/BE4wTKRKMEgxFjAUBgVngQUCAQwLaWQ6NTY0RDU3MDAxFjAUBgVn +gQUCAgwLVk13YXJlIFRQTTIxFjAUBgVngQUCAwwLaWQ6MDAwMjAwNjUwDAYDVR0T +AQH/BAIwADAQBgNVHSUECTAHBgVngQUIATAhBgNVHQkEGjAYMBYGBWeBBQIQMQ0w +CwwDMi4wAgEAAgF0MB0GA1UdDgQWBBSWtRzqfDcPeatHhgkhowb85EhyCDAfBgNV +HSMEGDAWgBSmwq+iw85ovFyxi4MRmnn6yJbBizBRBggrBgEFBQcBAQRFMEMwQQYI +KwYBBQUHMAKGNWh0dHBzOi8vc2MyLTEwLTE4NC0xMDMtMTI2LmVuZy52bXdhcmUu +Y29tL2FmZC92ZWNzL2NhMA0GCSqGSIb3DQEBCwUAA4IBgQA5gMNkE+upkJFB6FBy +aHbH2fraJ6tOgxMsbQvj2G1dqf7r8W9XJj2oHG+Q6wY42PKSDRVYNYBBkl739neP +frS67zGbOOkmVp3CUzJjBaA7thFa6ZqS1h5NHokQ81gGFX7wzHrNIqokYIRlLaK3 +Be7XVHXJrEM6R+xi/s2ZmtR1I5JZLcKSZ5qFYvlKJoRX09pNyEE+DHNKm9MPU0ci +cX10igdGhzeTTm7nvcblp8NLl9gEbcb+ej0E1doN9OYKz4vIcjfT5C5zwM0QnjSr +QxTPbXMCVG78lB6UjsFS7hNp0qmU4EsS+qZyZIolArDRcjmyQK8X/JUukrPqVNze +FOWVf13coOo5siUYBW5IfMoJ0BrLSyGrNuGgB5R0m/lxGeZQb5y/3YhlPn5af9FJ +uutfgkJTREUMR/ue0PpOSPVAqccmpreFrBgU6iUsWYdBwjVU/59RUxdLRm8OfsnH +ntuWJPjo3QV6kA7U4z53nPO6gULpn4sU9db5HnzRvUGePsQ= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tpm2/testdata/ek-ecc-crt.txt b/tpm2/testdata/ek-ecc-crt.txt new file mode 100644 index 00000000..32352252 --- /dev/null +++ b/tpm2/testdata/ek-ecc-crt.txt @@ -0,0 +1,63 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + fc:83:32:14:9a:79:21:04 + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN = CA, DC = vsphere, DC = local, C = US, ST = California, O = sc2-10-184-103-126.eng.vmware.com, OU = VMware Engineering + Validity + Not Before: Aug 29 20:38:56 2023 GMT + Not After : Aug 17 08:45:41 2033 GMT + Subject: 2.23.133.2.1 = id:564D5700, 2.23.133.2.2 = VMware TPM2, 2.23.133.2.3 = id:00020065 + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:3e:3f:43:89:f9:26:ee:b4:d7:be:09:9d:d0:bf: + 18:e2:66:33:0b:4a:d6:35:db:68:6b:fc:8c:9b:09: + 88:2d:68:8c:bd:db:c5:1f:da:80:34:a7:cd:27:a3: + 2b:fb:cc:52:d1:25:65:25:d1:c8:c8:b7:a1:9e:b4: + b2:ae:b1:3a:1c + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Key Usage: critical + Key Agreement + X509v3 Subject Alternative Name: critical + DirName:/2.23.133.2.1=id:564D5700/2.23.133.2.2=VMware TPM2/2.23.133.2.3=id:00020065 + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Extended Key Usage: + 2.23.133.8.1 + X509v3 Subject Directory Attributes: +0...2.0.....t 0.0...g....1 + X509v3 Subject Key Identifier: + 96:B5:1C:EA:7C:37:0F:79:AB:47:86:09:21:A3:06:FC:E4:48:72:08 + X509v3 Authority Key Identifier: + A6:C2:AF:A2:C3:CE:68:BC:5C:B1:8B:83:11:9A:79:FA:C8:96:C1:8B + Authority Information Access: + CA Issuers - URI:https://sc2-10-184-103-126.eng.vmware.com/afd/vecs/ca + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 39:80:c3:64:13:eb:a9:90:91:41:e8:50:72:68:76:c7:d9:fa: + da:27:ab:4e:83:13:2c:6d:0b:e3:d8:6d:5d:a9:fe:eb:f1:6f: + 57:26:3d:a8:1c:6f:90:eb:06:38:d8:f2:92:0d:15:58:35:80: + 41:92:5e:f7:f6:77:8f:7e:b4:ba:ef:31:9b:38:e9:26:56:9d: + c2:53:32:63:05:a0:3b:b6:11:5a:e9:9a:92:d6:1e:4d:1e:89: + 10:f3:58:06:15:7e:f0:cc:7a:cd:22:aa:24:60:84:65:2d:a2: + b7:05:ee:d7:54:75:c9:ac:43:3a:47:ec:62:fe:cd:99:9a:d4: + 75:23:92:59:2d:c2:92:67:9a:85:62:f9:4a:26:84:57:d3:da: + 4d:c8:41:3e:0c:73:4a:9b:d3:0f:53:47:22:71:7d:74:8a:07: + 46:87:37:93:4e:6e:e7:bd:c6:e5:a7:c3:4b:97:d8:04:6d:c6: + fe:7a:3d:04:d5:da:0d:f4:e6:0a:cf:8b:c8:72:37:d3:e4:2e: + 73:c0:cd:10:9e:34:ab:43:14:cf:6d:73:02:54:6e:fc:94:1e: + 94:8e:c1:52:ee:13:69:d2:a9:94:e0:4b:12:fa:a6:72:64:8a: + 25:02:b0:d1:72:39:b2:40:af:17:fc:95:2e:92:b3:ea:54:dc: + de:14:e5:95:7f:5d:dc:a0:ea:39:b2:25:18:05:6e:48:7c:ca: + 09:d0:1a:cb:4b:21:ab:36:e1:a0:07:94:74:9b:f9:71:19:e6: + 50:6f:9c:bf:dd:88:65:3e:7e:5a:7f:d1:49:ba:eb:5f:82:42: + 53:44:45:0c:47:fb:9e:d0:fa:4e:48:f5:40:a9:c7:26:a6:b7: + 85:ac:18:14:ea:25:2c:59:87:41:c2:35:54:ff:9f:51:53:17: + 4b:46:6f:0e:7e:c9:c7:9e:db:96:24:f8:e8:dd:05:7a:90:0e: + d4:e3:3e:77:9c:f3:ba:81:42:e9:9f:8b:14:f5:d6:f9:1e:7c: + d1:bd:41:9e:3e:c4 diff --git a/tpm2/testdata/ek-ecc.bin b/tpm2/testdata/ek-ecc.bin new file mode 100644 index 0000000000000000000000000000000000000000..7bcd4be626e833407f81452a8bed4b1223255599 GIT binary patch literal 124 zcmV-?0E7PkdH^E;3jhNEvH&21ahGRAgtPb>kj#xnrPn0=QrB(JAU;1siTNh(wAa1~ozTA+;$|}oO4c>oXlwk8n+b?5XaFFLz1zhf e+JH2t%_pNP`^-|&C1oYi$jG;$p0u*Au{s<)gfsmB literal 0 HcmV?d00001 diff --git a/tpm2/testdata/ek-ecc.yaml b/tpm2/testdata/ek-ecc.yaml new file mode 100644 index 00000000..f46f9a14 --- /dev/null +++ b/tpm2/testdata/ek-ecc.yaml @@ -0,0 +1,36 @@ +name: 000b385ecb6c63be2a16c41b6439d568d48dde9fa9f8032e5c81dfb4c92ecac53c58 +qualified name: 000b62328dc70254afdf573a36af8096c6a6afc56a5bbb8edb3673dbc22884037591 +name-alg: + value: sha256 + raw: 0xb +attributes: + value: fixedtpm|fixedparent|sensitivedataorigin|adminwithpolicy|restricted|decrypt + raw: 0x300b2 +type: + value: ecc + raw: 0x23 +curve-id: + value: NIST p256 + raw: 0x3 +kdfa-alg: + value: null + raw: 0x10 +kdfa-halg: + value: (null) + raw: 0x0 +scheme: + value: null + raw: 0x10 +scheme-halg: + value: (null) + raw: 0x0 +sym-alg: + value: aes + raw: 0x6 +sym-mode: + value: cfb + raw: 0x43 +sym-keybits: 128 +x: 3e3f4389f926eeb4d7be099dd0bf18e266330b4ad635db686bfc8c9b09882d68 +y: 8cbddbc51fda8034a7cd27a32bfbcc52d1256525d1c8c8b7a19eb4b2aeb13a1c +authorization policy: 837197674484b3f81a90cc8d46a5d724fd52d76e06520b64f2a1da1b331469aa \ No newline at end of file diff --git a/tpm2/testdata/ek-rsa-crt.pem b/tpm2/testdata/ek-rsa-crt.pem new file mode 100644 index 00000000..5723b82b --- /dev/null +++ b/tpm2/testdata/ek-rsa-crt.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFOjCCA6KgAwIBAgIJAMIy0YbGeaalMA0GCSqGSIb3DQEBCwUAMIGoMQswCQYD +VQQDDAJDQTEXMBUGCgmSJomT8ixkARkWB3ZzcGhlcmUxFTATBgoJkiaJk/IsZAEZ +FgVsb2NhbDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExKjAoBgNV +BAoMIXNjMi0xMC0xODQtMTAzLTEyNi5lbmcudm13YXJlLmNvbTEbMBkGA1UECwwS +Vk13YXJlIEVuZ2luZWVyaW5nMB4XDTIzMDgyOTIwMzg1NloXDTMzMDgxNzA4NDU0 +MVowSDEWMBQGBWeBBQIBDAtpZDo1NjRENTcwMDEWMBQGBWeBBQICDAtWTXdhcmUg +VFBNMjEWMBQGBWeBBQIDDAtpZDowMDAyMDA2NTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAM+G7c5eAqIEvT0Ykt6y1asNdd+P3ly+8Flgg6gWcIaRrrq7 +7Pbdxdo0BJkednLExNyB4KsRLxZ84e/ayz7R74wzwjNdMiWESRO+p01wEKM3qCBb +OYyYk0yNNCYASP9HHjScogZdiV/GTXSNsaLkpUjHEQv9ofu4FAEUcUBsXf3opWr8 +8KBaTIdodiES/vQSDT0DqIK6HecsC1j1h2mVodK+NTkvhf+hudVKQgYDAc9m8y7z +VUJVZ+kK8Uun/KFPCAfWsqIjXRKw4ziC2gcVaB4N7pV3BgGKvZ5ynvBgx31lScqy +t4qdHfFPisyWSboNs+o6wR5WXh96BR6YNhrKZasCAwEAAaOCAUQwggFAMA4GA1Ud +DwEB/wQEAwIFIDBYBgNVHREBAf8ETjBMpEowSDEWMBQGBWeBBQIBDAtpZDo1NjRE +NTcwMDEWMBQGBWeBBQICDAtWTXdhcmUgVFBNMjEWMBQGBWeBBQIDDAtpZDowMDAy +MDA2NTAMBgNVHRMBAf8EAjAAMBAGA1UdJQQJMAcGBWeBBQgBMCEGA1UdCQQaMBgw +FgYFZ4EFAhAxDTALDAMyLjACAQACAXQwHQYDVR0OBBYEFPLoKLq+GpN1u9MDVQ3n +s+u/zTm3MB8GA1UdIwQYMBaAFKbCr6LDzmi8XLGLgxGaefrIlsGLMFEGCCsGAQUF +BwEBBEUwQzBBBggrBgEFBQcwAoY1aHR0cHM6Ly9zYzItMTAtMTg0LTEwMy0xMjYu +ZW5nLnZtd2FyZS5jb20vYWZkL3ZlY3MvY2EwDQYJKoZIhvcNAQELBQADggGBAKtE +EU4p71p3zXOTcnlqEjW55sIM8+8BZ+BuaSx4yiHrIZMnBuxN83HDh9wlaNaRn53X +35kq4lh1u3Llq85W5JvXydOZooeAYg0KZ3IU7SD2hI8bd7ZIsOql8NveFB8446u3 +g04+ZD54wR1wscXftkSS8nsSe/Ze/XxelzsOl/XCICusqI6j24cBXbNd8JyZWpqx +e6hMg+aZiWAugaccMClMVmefp8cKpz+FWiWKZzNGGTbNOJp6ZS+J/SIx1chzC5UH +u7Ds3LkEDayKtEbC2EYRs5GirIxeW5S1OOPSjoe6TrJ6jw4zXdUAnfIMTodd4cli +rLaQmDyCWTqzihslTN1jSQuNLF9nKvAzCTCLXAhiei9iqfHCcFeozXNIUR8gUKHm +sXhl/qdpDcxcYiijVZF82OArMRDUm2kDkbbw6l0aBOwOJfqWj/0yfTUQol5Ly6Eg +JtAVl7h8HlQQ8+Rk4gmMbG3nwVFkkVPsW9NbnLISG0xeHpyXAMxm83XuQgRKVQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tpm2/testdata/ek-rsa-crt.txt b/tpm2/testdata/ek-rsa-crt.txt new file mode 100644 index 00000000..853e8633 --- /dev/null +++ b/tpm2/testdata/ek-rsa-crt.txt @@ -0,0 +1,75 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + c2:32:d1:86:c6:79:a6:a5 + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN = CA, DC = vsphere, DC = local, C = US, ST = California, O = sc2-10-184-103-126.eng.vmware.com, OU = VMware Engineering + Validity + Not Before: Aug 29 20:38:56 2023 GMT + Not After : Aug 17 08:45:41 2033 GMT + Subject: 2.23.133.2.1 = id:564D5700, 2.23.133.2.2 = VMware TPM2, 2.23.133.2.3 = id:00020065 + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:cf:86:ed:ce:5e:02:a2:04:bd:3d:18:92:de:b2: + d5:ab:0d:75:df:8f:de:5c:be:f0:59:60:83:a8:16: + 70:86:91:ae:ba:bb:ec:f6:dd:c5:da:34:04:99:1e: + 76:72:c4:c4:dc:81:e0:ab:11:2f:16:7c:e1:ef:da: + cb:3e:d1:ef:8c:33:c2:33:5d:32:25:84:49:13:be: + a7:4d:70:10:a3:37:a8:20:5b:39:8c:98:93:4c:8d: + 34:26:00:48:ff:47:1e:34:9c:a2:06:5d:89:5f:c6: + 4d:74:8d:b1:a2:e4:a5:48:c7:11:0b:fd:a1:fb:b8: + 14:01:14:71:40:6c:5d:fd:e8:a5:6a:fc:f0:a0:5a: + 4c:87:68:76:21:12:fe:f4:12:0d:3d:03:a8:82:ba: + 1d:e7:2c:0b:58:f5:87:69:95:a1:d2:be:35:39:2f: + 85:ff:a1:b9:d5:4a:42:06:03:01:cf:66:f3:2e:f3: + 55:42:55:67:e9:0a:f1:4b:a7:fc:a1:4f:08:07:d6: + b2:a2:23:5d:12:b0:e3:38:82:da:07:15:68:1e:0d: + ee:95:77:06:01:8a:bd:9e:72:9e:f0:60:c7:7d:65: + 49:ca:b2:b7:8a:9d:1d:f1:4f:8a:cc:96:49:ba:0d: + b3:ea:3a:c1:1e:56:5e:1f:7a:05:1e:98:36:1a:ca: + 65:ab + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Key Encipherment + X509v3 Subject Alternative Name: critical + DirName:/2.23.133.2.1=id:564D5700/2.23.133.2.2=VMware TPM2/2.23.133.2.3=id:00020065 + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Extended Key Usage: + 2.23.133.8.1 + X509v3 Subject Directory Attributes: +0...2.0.....t 0.0...g....1 + X509v3 Subject Key Identifier: + F2:E8:28:BA:BE:1A:93:75:BB:D3:03:55:0D:E7:B3:EB:BF:CD:39:B7 + X509v3 Authority Key Identifier: + A6:C2:AF:A2:C3:CE:68:BC:5C:B1:8B:83:11:9A:79:FA:C8:96:C1:8B + Authority Information Access: + CA Issuers - URI:https://sc2-10-184-103-126.eng.vmware.com/afd/vecs/ca + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + ab:44:11:4e:29:ef:5a:77:cd:73:93:72:79:6a:12:35:b9:e6: + c2:0c:f3:ef:01:67:e0:6e:69:2c:78:ca:21:eb:21:93:27:06: + ec:4d:f3:71:c3:87:dc:25:68:d6:91:9f:9d:d7:df:99:2a:e2: + 58:75:bb:72:e5:ab:ce:56:e4:9b:d7:c9:d3:99:a2:87:80:62: + 0d:0a:67:72:14:ed:20:f6:84:8f:1b:77:b6:48:b0:ea:a5:f0: + db:de:14:1f:38:e3:ab:b7:83:4e:3e:64:3e:78:c1:1d:70:b1: + c5:df:b6:44:92:f2:7b:12:7b:f6:5e:fd:7c:5e:97:3b:0e:97: + f5:c2:20:2b:ac:a8:8e:a3:db:87:01:5d:b3:5d:f0:9c:99:5a: + 9a:b1:7b:a8:4c:83:e6:99:89:60:2e:81:a7:1c:30:29:4c:56: + 67:9f:a7:c7:0a:a7:3f:85:5a:25:8a:67:33:46:19:36:cd:38: + 9a:7a:65:2f:89:fd:22:31:d5:c8:73:0b:95:07:bb:b0:ec:dc: + b9:04:0d:ac:8a:b4:46:c2:d8:46:11:b3:91:a2:ac:8c:5e:5b: + 94:b5:38:e3:d2:8e:87:ba:4e:b2:7a:8f:0e:33:5d:d5:00:9d: + f2:0c:4e:87:5d:e1:c9:62:ac:b6:90:98:3c:82:59:3a:b3:8a: + 1b:25:4c:dd:63:49:0b:8d:2c:5f:67:2a:f0:33:09:30:8b:5c: + 08:62:7a:2f:62:a9:f1:c2:70:57:a8:cd:73:48:51:1f:20:50: + a1:e6:b1:78:65:fe:a7:69:0d:cc:5c:62:28:a3:55:91:7c:d8: + e0:2b:31:10:d4:9b:69:03:91:b6:f0:ea:5d:1a:04:ec:0e:25: + fa:96:8f:fd:32:7d:35:10:a2:5e:4b:cb:a1:20:26:d0:15:97: + b8:7c:1e:54:10:f3:e4:64:e2:09:8c:6c:6d:e7:c1:51:64:91: + 53:ec:5b:d3:5b:9c:b2:12:1b:4c:5e:1e:9c:97:00:cc:66:f3: + 75:ee:42:04:4a:55 diff --git a/tpm2/testdata/ek-rsa.bin b/tpm2/testdata/ek-rsa.bin new file mode 100644 index 0000000000000000000000000000000000000000..948e469f3a94e3663b282ad34728cd0774a92257 GIT binary patch literal 316 zcmV-C0mJ?QIsgFx3jhNEvH&21ahGRAgtPb>kj#xnrPn0=QrB(ENPT)yyGV1uX@aE6hty1VT5 z-No881eqRoa>T^kf#9nVFBW{^@7l{g(eI2i!ZTemC4@;6zNbxa5TiG!AX_<%n3GJ6 zG$sH@|3@A)oT3I@iC@M|bd9m18=R OULSe|9+);7%4MrCRE@O& literal 0 HcmV?d00001 diff --git a/tpm2/testdata/ek-rsa.yaml b/tpm2/testdata/ek-rsa.yaml new file mode 100644 index 00000000..f908c2f7 --- /dev/null +++ b/tpm2/testdata/ek-rsa.yaml @@ -0,0 +1,28 @@ +name: 000b5897f009da23436d916f53f13e5ae362b6e587300b6af91066f9d6547b3e98b4 +qualified name: 000ba933395c7cfbab970c168a9ef1a58a61da417402d323d60545f37d469f8132fd +name-alg: + value: sha256 + raw: 0xb +attributes: + value: fixedtpm|fixedparent|sensitivedataorigin|adminwithpolicy|restricted|decrypt + raw: 0x300b2 +type: + value: rsa + raw: 0x1 +exponent: 65537 +bits: 2048 +scheme: + value: null + raw: 0x10 +scheme-halg: + value: (null) + raw: 0x0 +sym-alg: + value: aes + raw: 0x6 +sym-mode: + value: cfb + raw: 0x43 +sym-keybits: 128 +rsa: cf86edce5e02a204bd3d1892deb2d5ab0d75df8fde5cbef0596083a816708691aebabbecf6ddc5da3404991e7672c4c4dc81e0ab112f167ce1efdacb3ed1ef8c33c2335d3225844913bea74d7010a337a8205b398c98934c8d34260048ff471e349ca2065d895fc64d748db1a2e4a548c7110bfda1fbb814011471406c5dfde8a56afcf0a05a4c8768762112fef4120d3d03a882ba1de72c0b58f5876995a1d2be35392f85ffa1b9d54a42060301cf66f32ef355425567e90af14ba7fca14f0807d6b2a2235d12b0e33882da0715681e0dee957706018abd9e729ef060c77d6549cab2b78a9d1df14f8acc9649ba0db3ea3ac11e565e1f7a051e98361aca65ab +authorization policy: 837197674484b3f81a90cc8d46a5d724fd52d76e06520b64f2a1da1b331469aa \ No newline at end of file diff --git a/tpm2/tpm2_test.go b/tpm2/tpm2_test.go new file mode 100644 index 00000000..922f9c4d --- /dev/null +++ b/tpm2/tpm2_test.go @@ -0,0 +1,25 @@ +package tpm2_test + +import "testing" + +// assertExpectedError asserts an expected error either occurred or not. +// If fatal is true, then errors are indicated with t.Fatalf, otherwise +// t.Errorf is used. +func assertExpectedError(t *testing.T, fatal bool, a, e error) bool { + failFn := t.Errorf + if fatal { + failFn = t.Fatalf + } + switch { + case a != nil && e != nil && a.Error() != e.Error(): + failFn("unexpected error occurred: act=%q, exp=%q", a, e) + case a != nil && e != nil && a.Error() == e.Error(): + t.Logf("expected error occurred: exp=%s", e) + return false + case a != nil && e == nil: + failFn("unexpected error occurred: act=%s", a) + case a == nil && e != nil: + failFn("expected error did not occur: exp=%s", e) + } + return true +} diff --git a/tpm2/x509.go b/tpm2/x509.go new file mode 100644 index 00000000..c4b8f0dc --- /dev/null +++ b/tpm2/x509.go @@ -0,0 +1,57 @@ +package tpm2 + +import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "fmt" +) + +// EKCertToTPMTPublic returns the TPMT_PUBLIC data structure for the provided +// EK certificate. +func EKCertToTPMTPublic(cert x509.Certificate) (TPMTPublic, error) { + switch cert.PublicKeyAlgorithm { + case x509.RSA: + ek, err := Copy(RSAEKTemplate) + if err != nil { + return TPMTPublic{}, fmt.Errorf("failed to copy rsa ek tpl: %w", err) + } + pk := cert.PublicKey.(*rsa.PublicKey) + if pk.N == nil { + return TPMTPublic{}, fmt.Errorf("rsa pub key modulus is nil") + } + ek.Unique = NewTPMUPublicID( + TPMAlgRSA, + &TPM2BPublicKeyRSA{ + Buffer: pk.N.Bytes(), + }, + ) + return ek, nil + case x509.ECDSA: + ek, err := Copy(ECCEKTemplate) + if err != nil { + return TPMTPublic{}, fmt.Errorf("failed to copy ecc ek tpl: %w", err) + } + pk := cert.PublicKey.(*ecdsa.PublicKey) + if pk.X == nil { + return TPMTPublic{}, fmt.Errorf("ecc point.X is nil") + } + if pk.Y == nil { + return TPMTPublic{}, fmt.Errorf("ecc point.Y is nil") + } + ek.Unique = NewTPMUPublicID( + TPMAlgECC, + &TPMSECCPoint{ + X: TPM2BECCParameter{ + Buffer: pk.X.Bytes(), + }, + Y: TPM2BECCParameter{ + Buffer: pk.Y.Bytes(), + }, + }, + ) + return ek, nil + default: + return TPMTPublic{}, fmt.Errorf("unsupported pub key alg: %v", cert.PublicKeyAlgorithm) + } +} diff --git a/tpm2/x509_test.go b/tpm2/x509_test.go new file mode 100644 index 00000000..9b2fb8ae --- /dev/null +++ b/tpm2/x509_test.go @@ -0,0 +1,311 @@ +package tpm2_test + +import ( + "bytes" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "os" + "testing" + + "github.com/google/go-tpm/tpm2" +) + +func TestEKCertToTPMTPublic(t *testing.T) { + testCases := []struct { + name string + pemFile string + expObj tpm2.TPMTPublic + expErr error + }{ + { + name: "RSA", + pemFile: "./testdata/ek-rsa-crt.pem", + expObj: rsaEKWithPubKey(ekRSAPubKey), + }, + { + name: "ECC", + pemFile: "./testdata/ek-ecc-crt.pem", + expObj: eccEKWithPoint(ekECCPointX, ekECCPointY), + }, + } + + for i := range testCases { + tc := testCases[i] + t.Run(tc.name, func(t *testing.T) { + pemData, err := os.ReadFile(tc.pemFile) + if err != nil { + t.Fatalf("failed to read ek pem data") + } + pemBlock, _ := pem.Decode([]byte(pemData)) + if pemBlock == nil { + t.Fatalf("failed to decode ek pem data") + } + cert, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + t.Fatalf("failed to load ek cert: %s", err) + } + actObj, actErr := tpm2.EKCertToTPMTPublic(*cert) + if !assertExpectedError(t, true, actErr, tc.expErr) { + return + } + assertEK(t, actObj, tc.expObj) + }) + } +} + +const ( + // the ECC point for ./testdata/ek-ecc* + ekECCPointX = "3e3f4389f926eeb4d7be099dd0bf18e266330b4ad635db686bfc8c9b09882d68" + ekECCPointY = "8cbddbc51fda8034a7cd27a32bfbcc52d1256525d1c8c8b7a19eb4b2aeb13a1c" + + // the RSA pub key for ./testdata/ek-rsa* + ekRSAPubKey = "cf86edce5e02a204bd3d1892deb2d5ab0d75df8fde5cbef0596083a816708691aebabbecf6ddc5da3404991e7672c4c4dc81e0ab112f167ce1efdacb3ed1ef8c33c2335d3225844913bea74d7010a337a8205b398c98934c8d34260048ff471e349ca2065d895fc64d748db1a2e4a548c7110bfda1fbb814011471406c5dfde8a56afcf0a05a4c8768762112fef4120d3d03a882ba1de72c0b58f5876995a1d2be35392f85ffa1b9d54a42060301cf66f32ef355425567e90af14ba7fca14f0807d6b2a2235d12b0e33882da0715681e0dee957706018abd9e729ef060c77d6549cab2b78a9d1df14f8acc9649ba0db3ea3ac11e565e1f7a051e98361aca65ab" +) + +func rsaEKWithPubKey(pubKey string) tpm2.TPMTPublic { + pk := make([]byte, 256) + if n, err := hex.Decode(pk, []byte(pubKey)); err != nil { + panic(fmt.Sprintf("failed to decode rsa modulus: err=%s", err)) + } else if n != 256 { + panic(fmt.Sprintf("failed to read entire rsa modulus: n=%d", n)) + } + ek, err := tpm2.RSAEKTemplateWithPublicKey(tpm2.TPM2BPublicKeyRSA{ + Buffer: pk, + }) + if err != nil { + panic(fmt.Sprintf("failed to get rsa ek with pub key: %s", err)) + } + return ek +} + +func eccEKWithPoint(x, y string) tpm2.TPMTPublic { + xp := make([]byte, 32) + yp := make([]byte, 32) + if n, err := hex.Decode(xp, []byte(x)); err != nil { + panic(fmt.Sprintf("failed to decode ecc point.X: err=%s", err)) + } else if n != 32 { + panic(fmt.Sprintf("failed to read entire ecc point.X: n=%d", n)) + } + if n, err := hex.Decode(yp, []byte(y)); err != nil { + panic(fmt.Sprintf("failed to decode ecc point.Y: err=%s", err)) + } else if n != 32 { + panic(fmt.Sprintf("failed to read entire ecc point.Y: n=%d", n)) + } + ek, err := tpm2.ECCEKTemplateWithPoint(tpm2.TPMSECCPoint{ + X: tpm2.TPM2BECCParameter{ + Buffer: xp, + }, + Y: tpm2.TPM2BECCParameter{ + Buffer: yp, + }, + }) + if err != nil { + panic(fmt.Sprintf("failed to get ecc ek with point: %s", err)) + } + return ek +} + +func assertEK(t *testing.T, ap, ep tpm2.TPMTPublic) { + assertEKType(t, ap, ep) + assertEKNameAlg(t, ap, ep) + assertEKAuthPolicy(t, ap, ep) + assertEKObjectAttributes(t, ap, ep) + assertEKParameters(t, ap, ep) + assertEKUnique(t, ap, ep) +} + +func assertEKType(t *testing.T, ap, ep tpm2.TPMTPublic) { + a, e := ap.Type, ep.Type + switch { + case a != tpm2.TPMAlgRSA && a != tpm2.TPMAlgECC: + t.Errorf("unexpected pub key type: act=%v", a) + case a != e: + t.Errorf("unexpected pub key type: act=%v, exp=%v", a, e) + } +} + +func assertEKNameAlg(t *testing.T, ap, ep tpm2.TPMTPublic) { + a, e := ap.NameAlg, ep.NameAlg + switch { + case a != tpm2.TPMAlgSHA256: + t.Errorf("unexpected pub key alg: act=%v", a) + case a != e: + t.Errorf("unexpected pub key alg: act=%v, exp=%v", a, e) + } +} + +func assertEKAuthPolicy(t *testing.T, ap, ep tpm2.TPMTPublic) { + a, e := ap.AuthPolicy.Buffer, ep.AuthPolicy.Buffer + if !bytes.Equal(a, e) { + t.Errorf("unexpected auth policy: act=%x, exp=%x", a, e) + } +} + +func assertEKObjectAttributes(t *testing.T, ap, ep tpm2.TPMTPublic) { + a, e := ap.ObjectAttributes, ep.ObjectAttributes + if a, e := a.AdminWithPolicy, e.AdminWithPolicy; a != e { + t.Errorf("unexpected admin w policy: act=%v, exp=%v", a, e) + } + if a, e := a.Decrypt, e.Decrypt; a != e { + t.Errorf("unexpected decrypt: act=%v, exp=%v", a, e) + } + if a, e := a.EncryptedDuplication, e.EncryptedDuplication; a != e { + t.Errorf("unexpected encrypted dupe: act=%v, exp=%v", a, e) + } + if a, e := a.FixedParent, e.FixedParent; a != e { + t.Errorf("unexpected fixed parent: act=%v, exp=%v", a, e) + } + if a, e := a.FixedTPM, e.FixedTPM; a != e { + t.Errorf("unexpected fixed tpm: act=%v, exp=%v", a, e) + } + if a, e := a.NoDA, e.NoDA; a != e { + t.Errorf("unexpected noda: act=%v, exp=%v", a, e) + } + if a, e := a.Restricted, e.Restricted; a != e { + t.Errorf("unexpected restricted: act=%v, exp=%v", a, e) + } + if a, e := a.STClear, e.STClear; a != e { + t.Errorf("unexpected stclear: act=%v, exp=%v", a, e) + } + if a, e := a.SensitiveDataOrigin, e.SensitiveDataOrigin; a != e { + t.Errorf("unexpected sens data origin: act=%v, exp=%v", a, e) + } + if a, e := a.SignEncrypt, e.SignEncrypt; a != e { + t.Errorf("unexpected sign encrypt: act=%v, exp=%v", a, e) + } + if a, e := a.UserWithAuth, e.UserWithAuth; a != e { + t.Errorf("unexpected user w auth: act=%v, exp=%v", a, e) + } + if a, e := a.X509Sign, e.X509Sign; a != e { + t.Errorf("unexpected x509 sign: act=%v, exp=%v", a, e) + } +} + +func assertEKParameters(t *testing.T, ap, ep tpm2.TPMTPublic) { + apm, epm := ap.Parameters, ep.Parameters + + switch ap.Type { + case tpm2.TPMAlgRSA: + assertFnErr(t, "unexpected ecc detail", apm.ECCDetail) + assertFnErr(t, "unexpected keyed hash detail", apm.KeyedHashDetail) + assertFnErr(t, "unexpected sym detail", apm.SymDetail) + + a := assertFnObj(t, "missing act rsa detail", apm.RSADetail) + e := assertFnObj(t, "missing exp rsa detail", epm.RSADetail) + + assertEKRSAParms(t, a, e) + + case tpm2.TPMAlgECC: + assertFnErr(t, "unexpected keyed hash detail", apm.KeyedHashDetail) + assertFnErr(t, "unexpected rsa detail", apm.RSADetail) + assertFnErr(t, "unexpected sym detail", apm.SymDetail) + + a := assertFnObj(t, "missing act ecc detail", apm.ECCDetail) + e := assertFnObj(t, "missing exp ecc detail", epm.ECCDetail) + + assertEKECCParms(t, a, e) + } +} + +func assertEKUnique(t *testing.T, ap, ep tpm2.TPMTPublic) { + apu, epu := ap.Unique, ep.Unique + + switch ap.Type { + case tpm2.TPMAlgRSA: + assertFnErr(t, "unexpected ecc", apu.ECC) + assertFnErr(t, "unexpected keyed hash", apu.KeyedHash) + assertFnErr(t, "unexpected sym cipher", apu.SymCipher) + + a := assertFnObj(t, "missing act rsa key", apu.RSA) + e := assertFnObj(t, "missing exp rsa key", epu.RSA) + + assertEKPublicKeyRSA(t, a, e) + + case tpm2.TPMAlgECC: + assertFnErr(t, "unexpected keyed hash", apu.KeyedHash) + assertFnErr(t, "unexpected rsa", apu.RSA) + assertFnErr(t, "unexpected sym cipher", apu.SymCipher) + + a := assertFnObj(t, "missing act ecc point", apu.ECC) + e := assertFnObj(t, "missing exp ecc point", epu.ECC) + + assertEKECCPoint(t, a, e) + } +} + +func assertEKPublicKeyRSA(t *testing.T, a, e tpm2.TPM2BPublicKeyRSA) { + if a, e := a.Buffer, e.Buffer; !bytes.Equal(a, e) { + t.Errorf("unexpected rsa pub key: act=%x, exp=%x", a, e) + } +} + +func assertEKECCPoint(t *testing.T, a, e tpm2.TPMSECCPoint) { + if a, e := a.X.Buffer, e.X.Buffer; !bytes.Equal(a, e) { + t.Errorf("unexpected ecc point.X: act=%x, exp=%x", a, e) + } + if a, e := a.Y.Buffer, e.Y.Buffer; !bytes.Equal(a, e) { + t.Errorf("unexpected ecc point.Y: act=%x, exp=%x", a, e) + } +} + +func assertEKRSAParms(t *testing.T, a, e tpm2.TPMSRSAParms) { + if a, e := a.Exponent, e.Exponent; a != e { + t.Errorf("unexpected rsa exponent: act=%v, exp=%v", a, e) + } + if a, e := a.KeyBits, e.KeyBits; a != e { + t.Errorf("unexpected rsa key bits: act=%d, exp=%d", a, e) + } + assertEKSymDef(t, a.Symmetric, e.Symmetric) +} + +func assertEKECCParms(t *testing.T, a, e tpm2.TPMSECCParms) { + if a, e := a.CurveID, e.CurveID; a != e { + t.Errorf("unexpected ecc curve id: act=%v, exp=%v", a, e) + } + assertEKSymDef(t, a.Symmetric, e.Symmetric) +} + +func assertEKSymDef(t *testing.T, a, e tpm2.TPMTSymDefObject) { + if a, e := a.Algorithm, e.Algorithm; a != e { + t.Errorf("unexpected sym def alg: act=%d, exp=%d", a, e) + } + assertFnErr(t, "unexpected sym def key bits xor", a.KeyBits.XOR) + + akb := assertFnObj(t, "missing act aes key bits", a.KeyBits.AES) + ekb := assertFnObj(t, "missing exp aes key bits", e.KeyBits.AES) + + if a, e := akb, ekb; a != e { + t.Errorf("unexpected sym def key bits: act=%d, exp=%d", a, e) + } + + // The mode's contents reflect CFB, but it's not possible to retrieve that + // information here. + assertFnObj(t, "unexpected sym def mode", a.Mode.AES) +} + +func assertFnErr[T any](t *testing.T, msg string, fn func() (T, error)) { + if _, err := fn(); err == nil { + t.Error(msg) + } +} + +type rsaEccDetailKeyPointTypes interface { + tpm2.TPMSRSAParms | tpm2.TPMSECCParms | + tpm2.TPM2BPublicKeyRSA | tpm2.TPMSECCPoint | + tpm2.TPMKeyBits | tpm2.TPMAlgID +} + +func assertFnObj[T rsaEccDetailKeyPointTypes]( + t *testing.T, msg string, fn func() (*T, error)) T { + + obj, err := fn() + if err != nil { + t.Errorf(msg+": %v", err) + } + if obj == nil { + t.Fatalf(msg + ": nil details") + } + return *obj +}