Skip to content

Instantly share code, notes, and snippets.

@salrashid123
Last active April 24, 2025 10:54
Show Gist options
  • Save salrashid123/d525f960a7c342164f69ec1431869cfd to your computer and use it in GitHub Desktop.
Save salrashid123/d525f960a7c342164f69ec1431869cfd to your computer and use it in GitHub Desktop.
KDF using TPM based HMAC

Snippet which uses NIST SP 800-108 KDF with Countermode basically, this is an adaptation of github.com/hashicorp/vault/sdk/helper/kdf#CounterMode.

but with the HMAC operation using the TPM.


the sample below uses a swtpm where the hmac key is saved as a PEM encoded file.

First embed the key:

sudo swtpm_setup --tpmstate myvtpm --tpm2 --create-ek-cert
sudo swtpm socket --tpmstate dir=myvtpm --tpm2 --server type=tcp,port=2321 --ctrl type=tcp,port=2322 --flags not-need-init,startup-clear --log level=5

export TPM2TOOLS_TCTI="swtpm:port=2321"
export TPM2OPENSSL_TCTI="swtpm:port=2321"

export secret="my_api_key"
echo -n $secret > hmac.key
hexkey=$(xxd -p -c 256 < hmac.key)
echo $hexkey

printf '\x00\x00' > unique.dat
tpm2_createprimary -C o -G ecc  -g sha256  -c primary.ctx -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|noda|restricted|decrypt" -u unique.dat

tpm2 import -C primary.ctx -G hmac -i hmac.key -u hmac.pub -r hmac.priv
tpm2_flushcontext -t && tpm2_flushcontext -s && tpm2_flushcontext -l
tpm2 load -C primary.ctx -u hmac.pub -r hmac.priv -c hmac.ctx
tpm2_encodeobject -C primary.ctx -u hmac.pub -r hmac.priv -o tpm-key.pem

then if you run, a new key is derived by both the TPM and the default vault library of the same.

the expected result is the same

$ go run main.go
2025/04/23 15:52:39 TPM    KDF 8ee68b83a24249fd9dcd162921c3f5486f591620a871bedf9efce044d5e74734
2025/04/23 15:52:39 Vault  KDF 8ee68b83a24249fd9dcd162921c3f5486f591620a871bedf9efce044d5e74734
package main

import (
	"encoding/hex"
	"flag"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"slices"

	keyfile "github.com/foxboron/go-tpm-keyfiles"
	"github.com/google/go-tpm-tools/simulator"
	"github.com/google/go-tpm/tpm2"
	"github.com/google/go-tpm/tpm2/transport"
	"github.com/google/go-tpm/tpmutil"
	"github.com/hashicorp/vault/sdk/helper/kdf"
)

const ()

var (
	tpmPath = flag.String("tpm-path", "127.0.0.1:2321", "Path to the TPM device (character device or a Unix socket).")
	in      = flag.String("in", "tpm-key.pem", "privateKey File")
)

func main() {

	flag.Parse()
	b := []byte("foo")

	key := []byte("my_api_key")
	prf := kdf.HMACSHA256PRF
	prfLen := kdf.HMACSHA256PRFLen

	/// Vault
	out, err := kdf.CounterMode(prf, prfLen, key, b, 256)
	if err != nil {
		panic(err)
	}

	log.Printf("Vault  KDF %s\n", hex.EncodeToString(out))

	//// TPM

	r, err := kdf.CounterMode(TPMHMACSHA256PRF, prfLen, nil, b, 256)
	if err != nil {
		panic(err)
	}

	log.Printf("TPM    KDF %s\n", hex.EncodeToString(r))
}

func TPMHMACSHA256PRF(key []byte, data []byte) ([]byte, error) {
	return TPMHMAC(*tpmPath, *in, data)
}

var TPMDEVICES = []string{"/dev/tpm0", "/dev/tpmrm0"}

func openTPM(path string) (io.ReadWriteCloser, error) {
	if slices.Contains(TPMDEVICES, path) {
		return tpmutil.OpenTPM(path)
	} else if path == "simulator" {
		return simulator.Get()
	} else {
		return net.Dial("tcp", path)
	}
}

const (
	maxInputBuffer = 1024
)

func TPMHMAC(tpmPath string, pemkey string, data []byte) ([]byte, error) {

	rwc, err := openTPM(tpmPath)
	if err != nil {
		return nil, err
	}
	defer func() {
		rwc.Close()
	}()

	rwr := transport.FromReadWriter(rwc)

	c, err := os.ReadFile(pemkey)
	if err != nil {
		return nil, err
	}
	key, err := keyfile.Decode(c)
	if err != nil {
		return nil, err
	}

	// specify its parent directly
	primaryKey, err := tpm2.CreatePrimary{
		PrimaryHandle: key.Parent,
		InPublic:      tpm2.New2B(keyfile.ECCSRK_H2_Template),
	}.Execute(rwr)
	if err != nil {
		return nil, err
	}

	defer func() {
		flushContextCmd := tpm2.FlushContext{
			FlushHandle: primaryKey.ObjectHandle,
		}
		_, _ = flushContextCmd.Execute(rwr)
	}()

	hKey, err := tpm2.Load{
		ParentHandle: tpm2.AuthHandle{
			Handle: primaryKey.ObjectHandle,
			Name:   tpm2.TPM2BName(primaryKey.Name),
			Auth:   tpm2.PasswordAuth([]byte("")),
		},
		InPublic:  key.Pubkey,
		InPrivate: key.Privkey,
	}.Execute(rwr)

	if err != nil {
		return nil, err
	}

	defer func() {
		flushContextCmd := tpm2.FlushContext{
			FlushHandle: hKey.ObjectHandle,
		}
		_, _ = flushContextCmd.Execute(rwr)
	}()

	objAuth := &tpm2.TPM2BAuth{
		Buffer: nil,
	}

	sas, sasCloser, err := tpm2.HMACSession(rwr, tpm2.TPMAlgSHA256, 16, tpm2.Auth(objAuth.Buffer))
	if err != nil {
		return nil, err
	}
	defer func() {
		_ = sasCloser()
	}()

	hmacStart := tpm2.HmacStart{
		Handle: tpm2.AuthHandle{
			Handle: hKey.ObjectHandle,
			Name:   hKey.Name,
			Auth:   sas,
		},
		Auth:    *objAuth,
		HashAlg: tpm2.TPMAlgNull,
	}

	rspHS, err := hmacStart.Execute(rwr)
	if err != nil {
		return nil, err
	}

	authHandle := tpm2.AuthHandle{
		Name:   hKey.Name,
		Handle: rspHS.SequenceHandle,
		Auth:   tpm2.PasswordAuth(objAuth.Buffer),
	}
	for len(data) > maxInputBuffer {
		sequenceUpdate := tpm2.SequenceUpdate{
			SequenceHandle: authHandle,
			Buffer: tpm2.TPM2BMaxBuffer{
				Buffer: data[:maxInputBuffer],
			},
		}
		_, err = sequenceUpdate.Execute(rwr)
		if err != nil {
			return nil, err
		}

		data = data[maxInputBuffer:]
	}

	sequenceComplete := tpm2.SequenceComplete{
		SequenceHandle: authHandle,
		Buffer: tpm2.TPM2BMaxBuffer{
			Buffer: data,
		},
		Hierarchy: tpm2.TPMRHOwner,
	}

	rspSC, err := sequenceComplete.Execute(rwr)
	if err != nil {
		return nil, err
	}

	return rspSC.Result.Buffer, nil

}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment