diff --git a/README.md b/README.md index 547d4ca..79b9e73 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/tpm.go b/tpm.go index 7d9ff34..a711254 100644 --- a/tpm.go +++ b/tpm.go @@ -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 ( @@ -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 ( @@ -77,6 +78,8 @@ func (t KeyType) String() string { return "ECC" case TypeAES: return "AES" + case TypeHMAC: + return "HMAC" default: return "" } @@ -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. @@ -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 } @@ -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 } @@ -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 + } + + 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() @@ -704,6 +839,9 @@ func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signatur }) return b.Bytes() + case TypeHMAC: + return k.hmacLocked(digest) + default: return nil, ErrWrongKeyType } diff --git a/tpm_test.go b/tpm_test.go index db9b1e5..4b0a336 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -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) + } + + 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"