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
}