This repo demonstrates an authentication flow where a the bearer token sent between the client and server is unique to the TLS session used.
In other words, each client->server TLS connetion will derive a unique, non-resuable bearer token on both the client and server. The server will only
accept application layer data if the bearer tokens match.
This conecept is similar to HOTP: An HMAC-Based One-Time Password Algorithm except that in place of the "counter" to HMAC, the TLS Exported Key Material (EKM) is used.
One major advanatage of this flow is that even if the derivedKey is exfiltrated, it is useless since it can only be used on that specific TLS session.
In this flow, both the client and server needs to have an initial shared secret. This can be an HMAC key loaded into the client's Trusted Platform Module(TPM) which helps ensure the security of the shared key.
As shown:
- client loads the shared hmac key
- client connects to server over TLS
- client extracts the EKM
- client derives the sessionKey though calling KDF with HMAC(EKM)
- client sends the sessionKey and its own client identifier to the server
- server derives the EKM
- sever extracts the client_identifer
- server acquires the deriveKey using KDF with hmac(client_identifier, EKM)
- if derivedKeys match, proceed.
Also see:
- Per-Session TLS1.2-PSK using Trusted Platform Module (TPM)
- Exported Key Material (EKM) in golang and openssl
- Hybrid Public Key Encryption (HPKE) with per-message TPM-based PSK Authentication
The default demo here will use a software tpm (swtpm) and HMAC keys already embedded into the client-side TPM.
To run, just start the server and client. Only the client will use a TPM to derive the key and note that the derivedKey is unique to each connection
$ go run server/main.go
Starting Server..
deriving API key for client: client1
derived APIKey: ntbOfmI/pojI+/SgzQDTL8xdpGwuQho4qnerCoqNwQY=
deriving API key for client: client1
derived APIKey: AVMdDD8EpdkM+oEmjf/6Pz8A9ARe/wXEHNShR1XR8Ic=## start the softwareTPM witht the configuration in this repo.
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
$ go run client/main.go
derived APIKey: ntbOfmI/pojI+/SgzQDTL8xdpGwuQho4qnerCoqNwQY=
200 OK
ok
$ go run client/main.go
derived APIKey: AVMdDD8EpdkM+oEmjf/6Pz8A9ARe/wXEHNShR1XR8Ic=
200 OK
okNote that since the samples uses go1.24, you can use a key-exchange suite that is quantum resistent by setting the following flag:
export GODEBUG=tlsmlkem=1
$ go version
go version go1.24.0 linux/amd64For more information see, X25519MLKEM768 client server in go
This repo also contains a sample where instead of just a header, the derivedKey is used to encrypt and or sign a JSON structure.
See the example in the http_jwt_*/ folder. The default example shows a signed JWT using the derivedKey.
signed
## start the softwareTPM
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
$ go run http_jwt_server/main.go
Starting Server..
derived SigningKey: XwZghLVmPF/U5I+9yK1ZwxJNptLP6Se74Y1CSMQfJTY=
$ go run http_jwt_client/main.go
derived SigningKey: XwZghLVmPF/U5I+9yK1ZwxJNptLP6Se74Y1CSMQfJTY=
Signed: eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0In0.fLSIRp4So0H2Pe9HAyqAgprpE7m5Mdb3JEvkM6SOPhc
200 OK
okwhere the jwt header is
{
"alg": "HS256"
}encrypted then signed
$ go run http_jwt_server/main.go
Starting Server..
derived SigningKey: jq4CJ/H08l+m/dfJi96a1OHeId80ZtZvz8xiJpuW+i8=
derived EncryptionKey: E1NrJbfb/n4DJvXvngemQLBya4HdO+DkHLblV3cGYFE=
$ go run http_jwt_client/main.go
derived SigningKey: jq4CJ/H08l+m/dfJi96a1OHeId80ZtZvz8xiJpuW+i8=
derived EncryptionKey: E1NrJbfb/n4DJvXvngemQLBya4HdO+DkHLblV3cGYFE=
Encrypted JWE: eyJhbGciOiJkaXIiLCJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiY2xpZW50XzEiLCJ0eXAiOiJKV1QifQ..UufNES78x_xBEyb1.SU3IvwAJdRP868vdiWvQTuWomnv-1lSpQn0VvIDkp3bQhTIRgGNIHs3AmeKiGPM6NkHaYa0l_d3KvY766Tk-Tc5BjkZ2akliC8qbOxvkZqU3pAS9f99vy0XZe4PfvvF_tHaxUcijTFSXScaR.I0drMfegG3lkwV19eD7USw
200 OK
okwhere the JWE header is:
{
"alg": "dir",
"cty": "JWT",
"enc": "A256GCM",
"kid": "client_1",
"typ": "JWT"
}If you wanted the client to initialize a new swtpm and your own hmac key (vs the defualt swtpm in this library which uses my_api_key as root key), the following will create a software TPM and load/import an HMAC 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