Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# tpm

This package is an abstraction on top of the go-tpm libraries to use a local
TPM to create and use RSA, ECC, and AES keys that are bound to that TPM. The
keys can never be used without the TPM that was used to create them.
TPM to create and use RSA, ECC, AES, and HMAC keys that are bound to that TPM.
The keys can never be used without the TPM that was used to create them.

Any number of keys can be created and used concurrently. The library takes
care loading the right key in the TPM, as needed.

By default, 2048-bit RSA keys are created. AES keys, ECC keys, and RSA keys of
different sizes can also be created if the TPM supports them.
By default, 2048-bit RSA keys are created. AES keys, ECC keys, HMAC keys, and
RSA keys of different sizes can also be created if the TPM supports them.

## Example:

Expand Down
154 changes: 146 additions & 8 deletions tpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
// SOFTWARE.

// Package tpm is an abstraction on top of the go-tpm libraries to use a local
// TPM to create and use RSA, ECC, and AES keys that are bound to that TPM. The
// keys can never be used without the TPM that was used to create them.
// TPM to create and use RSA, ECC, AES, and HMAC keys that are bound to that TPM.
// The keys can never be used without the TPM that was used to create them.
//
// Any number of keys can be created and used concurrently. The library takes
// care loading the right key in the TPM, as needed.
//
// By default, 2048-bit RSA keys are created. AES keys, ECC keys, and RSA keys
// of different sizes can also be created if the TPM supports them.
// By default, 2048-bit RSA keys are created. AES keys, ECC keys, HMAC keys, and
// RSA keys of different sizes can also be created if the TPM supports them.
package tpm

import (
Expand All @@ -56,9 +56,10 @@ import (
)

const (
TypeRSA KeyType = 1
TypeECC KeyType = 2
TypeAES KeyType = 3
TypeRSA KeyType = 1
TypeECC KeyType = 2
TypeAES KeyType = 3
TypeHMAC KeyType = 4
)

var (
Expand All @@ -77,6 +78,8 @@ func (t KeyType) String() string {
return "ECC"
case TypeAES:
return "AES"
case TypeHMAC:
return "HMAC"
default:
return ""
}
Expand Down Expand Up @@ -192,6 +195,15 @@ func WithAES(bits int) KeyOption {
}
}

// WithHMAC indicates that an HMAC key should be created.
func WithHMAC(bits int) KeyOption {
return func(opts *keyOptions) {
opts.keyType = TypeHMAC
opts.bits = bits
opts.curve = nil
}
}

// CreateKey creates a new key that's ready to use. Keys can be serialized and
// stored offline with [Key.Marshal], and restored with [TPM.UnmarshalKey]. The
// serialized keys can only be restored using the same TPM.
Expand Down Expand Up @@ -350,6 +362,60 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) {
&tpm2.TPM2BDigest{Buffer: unique},
),
}

case TypeHMAC:
var hashAlg tpm2.TPMAlgID
switch opt.bits {
case 384:
hashAlg = tpm2.TPMAlgSHA384
case 512:
hashAlg = tpm2.TPMAlgSHA512
case 0, 256:
hashAlg = tpm2.TPMAlgSHA256
default:
return nil, fmt.Errorf("HMAC key size %d not supported", opt.bits)
}

unique := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, unique); err != nil {
return nil, fmt.Errorf("rand: %w", err)
}
public = tpm2.TPMTPublic{
Type: tpm2.TPMAlgKeyedHash,
NameAlg: hashAlg,
ObjectAttributes: tpm2.TPMAObject{
FixedTPM: true,
STClear: false,
FixedParent: true,
SensitiveDataOrigin: true,
UserWithAuth: true,
AdminWithPolicy: false,
NoDA: false,
EncryptedDuplication: false,
Restricted: false,
Decrypt: false,
SignEncrypt: true,
},
Parameters: tpm2.NewTPMUPublicParms(
tpm2.TPMAlgKeyedHash,
&tpm2.TPMSKeyedHashParms{
Scheme: tpm2.TPMTKeyedHashScheme{
Scheme: tpm2.TPMAlgHMAC,
Details: tpm2.NewTPMUSchemeKeyedHash(
tpm2.TPMAlgHMAC,
&tpm2.TPMSSchemeHMAC{
HashAlg: hashAlg,
},
),
},
},
),
Unique: tpm2.NewTPMUPublicID(
tpm2.TPMAlgKeyedHash,
&tpm2.TPM2BDigest{Buffer: unique},
),
}

default:
return nil, ErrWrongKeyType
}
Expand Down Expand Up @@ -553,6 +619,29 @@ func (k *Key) getPublicLocked() error {
k.keyType = TypeAES
k.bits = int(*bits)
}

case tpm2.TPMAlgKeyedHash:
keyedHashParms, err := outPublic.Parameters.KeyedHashDetail()
if err != nil {
return fmt.Errorf("TPM2_ReadPublic: %w", err)
}
if keyedHashParms.Scheme.Scheme == tpm2.TPMAlgHMAC {
k.keyType = TypeHMAC
details, err := keyedHashParms.Scheme.Details.HMAC()
if err != nil {
return fmt.Errorf("TPM2_ReadPublic: %w", err)
}
switch details.HashAlg {
case tpm2.TPMAlgSHA384:
k.bits = 384
case tpm2.TPMAlgSHA512:
k.bits = 512
case tpm2.TPMAlgSHA256:
k.bits = 256
default:
k.bits = -1
}
}
}
return nil
}
Expand Down Expand Up @@ -603,7 +692,53 @@ func (k *Key) Curve() elliptic.Curve {
return k.curve
}

// Sign signs a digest with the key (RSA only).
// HMAC returns the HMAC signature of the message.
func (k *Key) HMAC(message []byte) ([]byte, error) {
k.t.mu.Lock()
defer k.t.mu.Unlock()
if err := k.loadLocked(); err != nil {
return nil, err
}
if k.keyType != TypeHMAC {
return nil, ErrWrongKeyType
}
return k.hmacLocked(message)
}

func (k *Key) hmacLocked(message []byte) ([]byte, error) {
var hashAlg tpm2.TPMAlgID
switch k.bits {
case 384:
hashAlg = tpm2.TPMAlgSHA384
case 512:
hashAlg = tpm2.TPMAlgSHA512
case 256:
hashAlg = tpm2.TPMAlgSHA256
default:
return nil, ErrWrongKeyType
}
Comment thread
rthellend marked this conversation as resolved.

resp, err := tpm2.Hmac{
Handle: tpm2.AuthHandle{
Handle: k.t.loadedHandle,
Name: tpm2.TPM2BName{
Buffer: []byte(k.id),
},
Auth: tpm2.PasswordAuth(k.t.objectAuth),
},
Buffer: tpm2.TPM2BMaxBuffer{
Buffer: message,
},
HashAlg: hashAlg,
}.Execute(k.t.tpm)
if err != nil {
return nil, fmt.Errorf("TPM2_HMAC: %w", err)
}
return resp.OutHMAC.Buffer, nil
}

// Sign signs a digest with the key (RSA and ECC) or computes the HMAC
// (HMAC keys).
func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
k.t.mu.Lock()
defer k.t.mu.Unlock()
Expand Down Expand Up @@ -704,6 +839,9 @@ func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signatur
})
return b.Bytes()

case TypeHMAC:
Comment thread
rthellend marked this conversation as resolved.
return k.hmacLocked(digest)

default:
return nil, ErrWrongKeyType
}
Expand Down
112 changes: 112 additions & 0 deletions tpm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,118 @@ func TestAES(t *testing.T) {
}
}

func TestHMAC(t *testing.T) {
const (
keyPassphrase = "blah"
payload = "Hello World!"
)

rwc, err := simulator.Get()
if err != nil {
t.Fatalf("simulator.Get: %v", err)
}

tpm, err := New(WithTPM(rwc), WithObjectAuth([]byte(keyPassphrase)))
if err != nil {
t.Fatalf("New: %v", err)
}
defer tpm.Close()

// Test HMAC SHA256, SHA384, SHA512
tests := []struct {
bits int
hash crypto.Hash
}{
{256, crypto.SHA256},
{384, crypto.SHA384},
{512, crypto.SHA512},
}

// Default size (SHA256)
key, err := tpm.CreateKey(WithHMAC(0))
if err != nil {
t.Fatalf("tpm.CreateKey: %v", err)
}
if got, want := key.Bits(), 256; got != want {
t.Fatalf("key.Bits() = %d, want %d", got, want)
}

for _, tc := range tests {
key, err := tpm.CreateKey(WithHMAC(tc.bits))
if err != nil {
t.Fatalf("tpm.CreateKey(%d): %v", tc.bits, err)
}

if got, want := key.Type(), TypeHMAC; got != want {
t.Fatalf("key.Type() = %d, want %d", got, want)
}
if got, want := key.Bits(), tc.bits; got != want {
t.Fatalf("key.Bits() = %d, want %d", got, want)
}

var hashed []byte
if tc.hash == crypto.SHA256 {
h := sha256.Sum256([]byte(payload))
hashed = h[:]
} else {
// For simplicity in test, just use empty or simple data,
// but we should match the hash size to be realistic if needed.
// Actually Sign() takes a digest, so we should provide a digest.
h := tc.hash.New()
h.Write([]byte(payload))
hashed = h.Sum(nil)
}
Comment thread
rthellend marked this conversation as resolved.

sig, err := key.Sign(nil, hashed, tc.hash)
if err != nil {
t.Fatalf("Sign(): %v", err)
}

// HMAC is deterministic. Verify that signing the same data twice produces the same signature.
sig2, err := key.Sign(nil, hashed, tc.hash)
if err != nil {
t.Fatalf("Sign() 2: %v", err)
}

if !bytes.Equal(sig, sig2) {
t.Fatal("HMAC signatures should be deterministic, but they do not match")
}

// Test HMAC method on the original payload.
mac, err := key.HMAC([]byte(payload))
if err != nil {
t.Fatalf("key.HMAC: %v", err)
}

// HMAC is deterministic. Verify that HMACing the same data twice produces the same MAC.
mac2, err := key.HMAC([]byte(payload))
if err != nil {
t.Fatalf("key.HMAC 2: %v", err)
}
if !bytes.Equal(mac, mac2) {
t.Fatal("HMAC results should be deterministic, but they do not match")
}

// The result of HMAC(message) should be different from Sign(hash(message)).
if bytes.Equal(sig, mac) {
t.Fatal("key.Sign(hash) and key.HMAC(message) should not produce the same result")
}

// Verify encryption/decryption fails
if _, err := key.Encrypt([]byte(payload)); err == nil {
t.Fatal("Encrypt should have failed")
}
if _, err := key.Decrypt(nil, []byte(payload), nil); err == nil {
t.Fatal("Decrypt should have failed")
}
}

// Test invalid size
if _, err := tpm.CreateKey(WithHMAC(511)); err == nil {
t.Fatal("tpm.CreateKey(511) should have failed")
}
}

func TestMarshal(t *testing.T) {
const (
keyPassphrase = "blah"
Expand Down
Loading