From 1e4670de1f785a34fe798c87c8aa35a28d6a8de4 Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 10:55:20 -0700 Subject: [PATCH 01/12] Use authenticated encryption with AES keys AES Encrypt/Decrypt used AES-CFB in the TPM with no integrity protection, so ciphertexts were malleable: flipping bits in the ciphertext flipped the same bits in the decrypted plaintext. Each message now gets a random single-use data key, wrapped by the TPM AES key. The message is sealed with AES-GCM in software, using a key derived from the data key with HKDF. The version, IV, and wrapped key are authenticated as additional data. This also removes the TPM's max-buffer size limit on messages. This is a breaking change to the AES ciphertext format. Co-Authored-By: Claude Opus 5.5 --- tpm.go | 163 ++++++++++++++++++++++++++++++++++++---------------- tpm_test.go | 27 +++++++++ 2 files changed, 139 insertions(+), 51 deletions(-) diff --git a/tpm.go b/tpm.go index a711254..99a9c9f 100644 --- a/tpm.go +++ b/tpm.go @@ -34,8 +34,11 @@ package tpm import ( "crypto" + "crypto/aes" + "crypto/cipher" "crypto/ecdsa" "crypto/elliptic" + "crypto/hkdf" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -868,6 +871,13 @@ func addASN1IntBytes(b *cryptobyte.Builder, bytes []byte) { } // Encrypt encrypts cleartext with the key. +// +// With RSA keys, the cleartext is encrypted with RSA-OAEP (SHA-256) and its +// size is limited by the key size. +// +// With AES keys, the cleartext is encrypted with authenticated encryption +// (AES-GCM) using a single-use data key that is wrapped by the TPM key. There +// is no practical size limit. func (k *Key) Encrypt(cleartext []byte) (ciphertext []byte, err error) { k.t.mu.Lock() defer k.t.mu.Unlock() @@ -892,34 +902,7 @@ func (k *Key) Encrypt(cleartext []byte) (ciphertext []byte, err error) { return resp.OutData.Buffer, nil case TypeAES: - iv := make([]byte, 16) - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return nil, fmt.Errorf("rand: %w", err) - } - resp, err := tpm2.EncryptDecrypt2{ - KeyHandle: tpm2.AuthHandle{ - Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), - }, - Message: tpm2.TPM2BMaxBuffer{ - Buffer: cleartext, - }, - Decrypt: false, - IV: tpm2.TPM2BIV{ - Buffer: iv, - }, - }.Execute(k.t.tpm) - if err != nil { - return nil, fmt.Errorf("TPM2_EncryptDecrypt2: %w", err) - } - enc := resp.OutData.Buffer - out := make([]byte, len(iv)+len(enc)) - copy(out, iv) - copy(out[len(iv):], enc) - return out, nil + return k.aesEncryptLocked(cleartext) default: return nil, ErrWrongKeyType @@ -957,35 +940,113 @@ func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, _ crypto.DecrypterOpts) (p return resp.Message.Buffer, nil case TypeAES: - if len(ciphertext) < 16 { - return nil, ErrDecrypt - } - resp, err := tpm2.EncryptDecrypt2{ - KeyHandle: tpm2.AuthHandle{ - Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), - }, - Message: tpm2.TPM2BMaxBuffer{ - Buffer: ciphertext[16:], - }, - Decrypt: true, - IV: tpm2.TPM2BIV{ - Buffer: ciphertext[:16], - }, - }.Execute(k.t.tpm) - if err != nil { - return nil, fmt.Errorf("TPM2_EncryptDecrypt2: %w", err) - } - return resp.OutData.Buffer, nil + return k.aesDecryptLocked(ciphertext) default: return nil, ErrWrongKeyType } } +// AES ciphertext format: +// +// version (1) || iv (16) || wrapped data key (32) || AES-GCM ciphertext+tag +// +// A random data key is generated for each message and wrapped (AES-CFB) by the +// TPM key. The message itself is sealed with AES-GCM in software, using a key +// derived from the data key. The data key is never reused, so a fixed nonce is +// safe. The header is authenticated as additional data. +const ( + aesVersion = 1 + aesIVSize = 16 + aesDataKeyLen = 32 + aesHeaderSize = 1 + aesIVSize + aesDataKeyLen +) + +func (k *Key) aesEncryptLocked(cleartext []byte) ([]byte, error) { + header := make([]byte, aesHeaderSize) + header[0] = aesVersion + iv := header[1 : 1+aesIVSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, fmt.Errorf("rand: %w", err) + } + dataKey := make([]byte, aesDataKeyLen) + if _, err := io.ReadFull(rand.Reader, dataKey); err != nil { + return nil, fmt.Errorf("rand: %w", err) + } + wrapped, err := k.tpmAESLocked(dataKey, iv, false) + if err != nil { + return nil, err + } + if len(wrapped) != aesDataKeyLen { + return nil, errors.New("TPM2_EncryptDecrypt2: unexpected output size") + } + copy(header[1+aesIVSize:], wrapped) + + aead, err := newDataKeyAEAD(dataKey) + if err != nil { + return nil, err + } + nonce := make([]byte, aead.NonceSize()) + return aead.Seal(header, nonce, cleartext, header), nil +} + +func (k *Key) aesDecryptLocked(ciphertext []byte) ([]byte, error) { + if len(ciphertext) < aesHeaderSize || ciphertext[0] != aesVersion { + return nil, ErrDecrypt + } + header := ciphertext[:aesHeaderSize] + iv := header[1 : 1+aesIVSize] + dataKey, err := k.tpmAESLocked(header[1+aesIVSize:], iv, true) + if err != nil { + return nil, err + } + aead, err := newDataKeyAEAD(dataKey) + if err != nil { + return nil, err + } + nonce := make([]byte, aead.NonceSize()) + out, err := aead.Open(nil, nonce, ciphertext[aesHeaderSize:], header) + if err != nil { + return nil, ErrDecrypt + } + return out, nil +} + +func newDataKeyAEAD(dataKey []byte) (cipher.AEAD, error) { + key, err := hkdf.Key(sha256.New, dataKey, nil, "github.com/c2FmZQ/tpm AES-GCM", 32) + if err != nil { + return nil, err + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return cipher.NewGCM(block) +} + +func (k *Key) tpmAESLocked(in, iv []byte, decrypt bool) ([]byte, error) { + resp, err := tpm2.EncryptDecrypt2{ + KeyHandle: tpm2.AuthHandle{ + Handle: k.t.loadedHandle, + Name: tpm2.TPM2BName{ + Buffer: []byte(k.id), + }, + Auth: tpm2.PasswordAuth(k.t.objectAuth), + }, + Message: tpm2.TPM2BMaxBuffer{ + Buffer: in, + }, + Decrypt: decrypt, + IV: tpm2.TPM2BIV{ + Buffer: iv, + }, + }.Execute(k.t.tpm) + if err != nil { + return nil, fmt.Errorf("TPM2_EncryptDecrypt2: %w", err) + } + return resp.OutData.Buffer, nil +} + func (t *TPM) srk() (tpm2.NamedHandle, func(), error) { createPrimaryResp, err := tpm2.CreatePrimary{ PrimaryHandle: tpm2.AuthHandle{ diff --git a/tpm_test.go b/tpm_test.go index 4b0a336..799c6b5 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -31,6 +31,7 @@ import ( "crypto/rsa" "crypto/sha256" "fmt" + "slices" "testing" "github.com/google/go-tpm-tools/simulator" @@ -194,6 +195,32 @@ func TestAES(t *testing.T) { t.Fatalf("Decrypt() = %q, want %q", got, want) } + // Large messages are supported. + large := bytes.Repeat([]byte("x"), 100000) + encLarge, err := key.Encrypt(large) + if err != nil { + t.Fatalf("tpm.Encrypt(large): %v", err) + } + decLarge, err := key.Decrypt(nil, encLarge, nil) + if err != nil { + t.Fatalf("tpm.Decrypt(large): %v", err) + } + if !bytes.Equal(decLarge, large) { + t.Fatal("Decrypt(large) mismatch") + } + + // Any modification of the ciphertext must be detected. + for i := range enc { + tampered := slices.Clone(enc) + tampered[i] ^= 0x01 + if _, err := key.Decrypt(nil, tampered, nil); err == nil { + t.Fatalf("tpm.Decrypt should have failed with byte %d modified", i) + } + } + if _, err := key.Decrypt(nil, enc[:len(enc)-1], nil); err == nil { + t.Fatal("tpm.Decrypt should have failed with truncated ciphertext") + } + tpm.objectAuth = []byte("wrong") if _, err := key.Decrypt(nil, enc, nil); err == nil { t.Fatal("tpm.Decrypt should have failed") From d54ff81af4d2ff08d0ee8b31918012e7184d0b39 Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 10:55:58 -0700 Subject: [PATCH 02/12] Honor DecrypterOpts in RSA Decrypt Decrypt ignored its opts and always used OAEP-SHA256 with an empty label. Callers relying on OAEP labels for domain separation got none, and callers asking for PKCS#1 v1.5 silently got OAEP. The OAEP hash and label from *rsa.OAEPOptions are now passed to the TPM. Other option types are rejected. The TPM requires labels to be null-terminated, so labels without a trailing 0x00 are rejected rather than silently changed. Co-Authored-By: Claude Opus 5.5 --- tpm.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- tpm_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/tpm.go b/tpm.go index 99a9c9f..3609698 100644 --- a/tpm.go +++ b/tpm.go @@ -910,7 +910,11 @@ func (k *Key) Encrypt(cleartext []byte) (ciphertext []byte, err error) { } // Decrypt decrypts ciphertext with the key. -func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, _ crypto.DecrypterOpts) (plaintext []byte, err error) { +// +// With RSA keys, only RSA-OAEP is supported. opts may be nil (SHA-256, no +// label) or a [*rsa.OAEPOptions]. The TPM requires OAEP labels to be +// null-terminated, so a non-empty Label must end with a 0x00 byte. +func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) { k.t.mu.Lock() defer k.t.mu.Unlock() if err := k.loadLocked(); err != nil { @@ -918,6 +922,10 @@ func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, _ crypto.DecrypterOpts) (p } switch k.keyType { case TypeRSA: + hashAlg, label, err := oaepParams(opts) + if err != nil { + return nil, err + } resp, err := tpm2.RSADecrypt{ KeyHandle: tpm2.AuthHandle{ Handle: k.t.loadedHandle, @@ -931,7 +939,10 @@ func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, _ crypto.DecrypterOpts) (p }, InScheme: tpm2.TPMTRSADecrypt{ Scheme: tpm2.TPMAlgOAEP, - Details: tpm2.NewTPMUAsymScheme(tpm2.TPMAlgOAEP, &tpm2.TPMSEncSchemeOAEP{HashAlg: tpm2.TPMAlgSHA256}), + Details: tpm2.NewTPMUAsymScheme(tpm2.TPMAlgOAEP, &tpm2.TPMSEncSchemeOAEP{HashAlg: hashAlg}), + }, + Label: tpm2.TPM2BData{ + Buffer: label, }, }.Execute(k.t.tpm) if err != nil { @@ -947,6 +958,44 @@ func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, _ crypto.DecrypterOpts) (p } } +// oaepParams returns the TPM hash algorithm and label to use for RSA-OAEP +// decryption with the given options. +func oaepParams(opts crypto.DecrypterOpts) (tpm2.TPMIAlgHash, []byte, error) { + if opts == nil { + return tpm2.TPMAlgSHA256, nil, nil + } + o, ok := opts.(*rsa.OAEPOptions) + if !ok { + return 0, nil, fmt.Errorf("unsupported decrypter options %T", opts) + } + if o.MGFHash != 0 && o.MGFHash != o.Hash { + return 0, nil, errors.New("OAEP MGF hash must be the same as the label hash") + } + hashAlg, err := tpmHashAlg(o.Hash) + if err != nil { + return 0, nil, err + } + if len(o.Label) > 0 && o.Label[len(o.Label)-1] != 0 { + return 0, nil, errors.New("OAEP label must be null-terminated") + } + return hashAlg, o.Label, nil +} + +func tpmHashAlg(h crypto.Hash) (tpm2.TPMIAlgHash, error) { + switch h { + case crypto.SHA1: + return tpm2.TPMAlgSHA1, nil + case crypto.SHA256: + return tpm2.TPMAlgSHA256, nil + case crypto.SHA384: + return tpm2.TPMAlgSHA384, nil + case crypto.SHA512: + return tpm2.TPMAlgSHA512, nil + default: + return 0, fmt.Errorf("unexpected hash %v", h) + } +} + // AES ciphertext format: // // version (1) || iv (16) || wrapped data key (32) || AES-GCM ciphertext+tag diff --git a/tpm_test.go b/tpm_test.go index 799c6b5..b77406d 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -28,6 +28,7 @@ import ( "crypto" "crypto/ecdsa" "crypto/elliptic" + "crypto/rand" "crypto/rsa" "crypto/sha256" "fmt" @@ -79,6 +80,35 @@ func TestRSA(t *testing.T) { } pub := key.Public() + + // OAEP options are honored. + label := []byte("label\x00") + for _, h := range []crypto.Hash{crypto.SHA1, crypto.SHA256} { + enc, err := rsa.EncryptOAEP(h.New(), rand.Reader, pub.(*rsa.PublicKey), []byte(payload), label) + if err != nil { + t.Fatalf("rsa.EncryptOAEP: %v", err) + } + dec, err := key.Decrypt(nil, enc, &rsa.OAEPOptions{Hash: h, Label: label}) + if err != nil { + t.Fatalf("tpm.Decrypt(%v, label): %v", h, err) + } + if got, want := string(dec), payload; got != want { + t.Fatalf("Decrypt() = %q, want %q", got, want) + } + if _, err := key.Decrypt(nil, enc, &rsa.OAEPOptions{Hash: h, Label: []byte("other\x00")}); err == nil { + t.Fatal("tpm.Decrypt with wrong label should have failed") + } + if _, err := key.Decrypt(nil, enc, &rsa.OAEPOptions{Hash: h}); err == nil { + t.Fatal("tpm.Decrypt without label should have failed") + } + } + if _, err := key.Decrypt(nil, enc, &rsa.OAEPOptions{Hash: crypto.SHA256, Label: []byte("label")}); err == nil { + t.Fatal("tpm.Decrypt with non-null-terminated label should have failed") + } + if _, err := key.Decrypt(nil, enc, &rsa.PKCS1v15DecryptOptions{}); err == nil { + t.Fatal("tpm.Decrypt with PKCS1v15 options should have failed") + } + hashed := sha256.Sum256([]byte(payload)) sig, err := key.Sign(nil, hashed[:], crypto.SHA256) if err != nil { From f4c139e056d21e031187a6b34dfda0901c7d9084 Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 10:57:32 -0700 Subject: [PATCH 03/12] Create keys under the owner (storage) hierarchy The SRK was created under the endorsement hierarchy, which is meant for identity keys and not for general storage. Keys now use the standard SRK location in the owner hierarchy, and the owner hierarchy's seed is replaced by TPM2_Clear. Clearing the TPM is therefore guaranteed to invalidate all keys created by this library. WithEndorsementAuth is replaced by WithOwnerAuth. This is a breaking change: keys created by earlier versions can no longer be loaded. Co-Authored-By: Claude Opus 5.5 --- tpm.go | 24 +++++++++++++----------- tpm_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/tpm.go b/tpm.go index 3609698..831c947 100644 --- a/tpm.go +++ b/tpm.go @@ -98,10 +98,12 @@ func WithTPM(rwc io.ReadWriteCloser) Option { } } -// WithEndorsementAuth specifies the endorsement passphrase. -func WithEndorsementAuth(pp []byte) Option { +// WithOwnerAuth specifies the owner (storage hierarchy) passphrase. Keys are +// created under a primary key in the owner hierarchy, so clearing the TPM +// (TPM2_Clear) invalidates them. +func WithOwnerAuth(pp []byte) Option { return func(t *TPM) { - t.endorsementAuth = slices.Clone(pp) + t.ownerAuth = slices.Clone(pp) } } @@ -154,12 +156,12 @@ var _ io.Closer = (*TPM)(nil) // cryptographic keys that are bound to that TPM. The keys can never be used // without the TPM created them. type TPM struct { - mu sync.Mutex - tpm transport.TPMCloser - objectAuth []byte - endorsementAuth []byte - loadedKey string - loadedHandle tpm2.TPMHandle + mu sync.Mutex + tpm transport.TPMCloser + objectAuth []byte + ownerAuth []byte + loadedKey string + loadedHandle tpm2.TPMHandle } type keyOptions struct { @@ -1099,8 +1101,8 @@ func (k *Key) tpmAESLocked(in, iv []byte, decrypt bool) ([]byte, error) { func (t *TPM) srk() (tpm2.NamedHandle, func(), error) { createPrimaryResp, err := tpm2.CreatePrimary{ PrimaryHandle: tpm2.AuthHandle{ - Handle: tpm2.TPMRHEndorsement, - Auth: tpm2.PasswordAuth(t.endorsementAuth), + Handle: tpm2.TPMRHOwner, + Auth: tpm2.PasswordAuth(t.ownerAuth), }, InPublic: tpm2.New2B(tpm2.RSASRKTemplate), }.Execute(t.tpm) diff --git a/tpm_test.go b/tpm_test.go index b77406d..47d8329 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -36,6 +36,7 @@ import ( "testing" "github.com/google/go-tpm-tools/simulator" + "github.com/google/go-tpm/tpm2" ) func TestRSA(t *testing.T) { @@ -431,3 +432,45 @@ func TestMarshal(t *testing.T) { } } } + +func TestClearInvalidatesKeys(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + key, err := tpm.CreateKey() + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + b, err := key.Marshal() + if err != nil { + t.Fatalf("key.Marshal: %v", err) + } + if _, err := tpm.UnmarshalKey(b); err != nil { + t.Fatalf("tpm.UnmarshalKey: %v", err) + } + + tpm.mu.Lock() + tpm.flushLocked() + _, err = tpm2.Clear{ + AuthHandle: tpm2.AuthHandle{ + Handle: tpm2.TPMRHLockout, + Auth: tpm2.PasswordAuth(nil), + }, + }.Execute(tpm.tpm) + tpm.mu.Unlock() + if err != nil { + t.Fatalf("TPM2_Clear: %v", err) + } + + if _, err := tpm.UnmarshalKey(b); err == nil { + t.Fatal("tpm.UnmarshalKey should fail after TPM2_Clear") + } +} From a25fb6229c725155985be003711768b6b6310ab5 Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 11:01:07 -0700 Subject: [PATCH 04/12] Protect TPM commands with salted HMAC sessions All commands used plain password sessions. The object and owner auth values were sent in cleartext on every command, decrypted plaintexts and HMAC values came back in cleartext, and nothing checked that the TPM on the other end was the one the keys were created with. Anyone who could observe the TPM bus (or interpose on it) could recover secrets. - The SRK is created once per TPM instance and stays loaded until Close. Its name is checked against its public area. - Key commands use one-time HMAC sessions salted with the SRK. Auth values never cross the bus, responses are authenticated, and secret parameters are encrypted with AES-128-CFB parameter encryption. - Creating the SRK uses an unsalted HMAC session, so the owner auth is not sent in cleartext either. - The serialized key format now includes the SRK name, and UnmarshalKey returns ErrWrongTPM if the TPM's SRK doesn't match. Together with salted sessions, this stops a different device from impersonating the TPM. - Object names are computed from the public area and checked after TPM2_Load, and AuthHandle.Name is now the real object name (it was the hash of the serialized key). - The key's public parameters come from its verified public area, not from an unauthenticated TPM2_ReadPublic. - RSA encryption is done in software with the public key. This is a breaking change to the serialized key format. Co-Authored-By: Claude Opus 5.5 --- tpm.go | 262 ++++++++++++++++++++++++++++++---------------------- tpm_test.go | 123 +++++++++++++++++++++++- 2 files changed, 271 insertions(+), 114 deletions(-) diff --git a/tpm.go b/tpm.go index 831c947..ff7016b 100644 --- a/tpm.go +++ b/tpm.go @@ -33,6 +33,7 @@ package tpm import ( + "bytes" "crypto" "crypto/aes" "crypto/cipher" @@ -69,6 +70,7 @@ var ( ErrWrongKeyType = errors.New("operation not implemented with this key type") ErrInvalidCurve = errors.New("invalid curve id") ErrDecrypt = errors.New("decryption error") + ErrWrongTPM = errors.New("key was created with a different TPM or storage root key") ) type KeyType int @@ -162,6 +164,8 @@ type TPM struct { ownerAuth []byte loadedKey string loadedHandle tpm2.TPMHandle + srk tpm2.NamedHandle + srkPublic tpm2.TPMTPublic } type keyOptions struct { @@ -232,11 +236,10 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { } t.flushLocked() - srk, flush, err := t.srk() + srk, err := t.srkLocked() if err != nil { return nil, err } - defer flush() var public tpm2.TPMTPublic @@ -426,7 +429,11 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { } createResp, err := tpm2.Create{ - ParentHandle: srk, + ParentHandle: tpm2.AuthHandle{ + Handle: srk.Handle, + Name: srk.Name, + Auth: t.sessionLocked(nil, tpm2.AESEncryption(128, tpm2.EncryptIn)), + }, InSensitive: tpm2.TPM2BSensitiveCreate{ Sensitive: &tpm2.TPMSSensitiveCreate{ UserAuth: tpm2.TPM2BAuth{ @@ -444,19 +451,39 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { pub := tpm2.Marshal(createResp.OutPublic) buf := cryptobyte.NewBuilder(nil) - buf.AddUint16(uint16(len(priv))) - buf.AddBytes(priv) - buf.AddUint16(uint16(len(pub))) - buf.AddBytes(pub) - + buf.AddUint8(keyFormatVersion) + buf.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(srk.Name.Buffer) + }) + buf.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(priv) + }) + buf.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(pub) + }) return buf.Bytes() } +// Serialized key format: +// +// version (1) || uint16-prefixed SRK name || uint16-prefixed TPM2B_PRIVATE || +// uint16-prefixed TPM2B_PUBLIC +// +// The SRK name pins the key to the storage root key that it was created +// under. When the key is loaded, the TPM's SRK must have the same name, and +// all commands are protected by sessions salted with that SRK. A different +// device pretending to be the TPM cannot complete these sessions. +const keyFormatVersion = 1 + // Close closes the connections to the TPM. func (t *TPM) Close() error { t.mu.Lock() defer t.mu.Unlock() t.flushLocked() + if t.srk.Handle != 0 { + tpm2.FlushContext{FlushHandle: t.srk.Handle}.Execute(t.tpm) + t.srk = tpm2.NamedHandle{} + } return t.tpm.Close() } @@ -480,13 +507,13 @@ func (t *TPM) unmarshalLocked(in []byte) (*Key, error) { hashed := sha256.Sum256(in) str := cryptobyte.String(in) - var sz uint16 - var savedPriv []byte - if !str.ReadUint16(&sz) || !str.ReadBytes(&savedPriv, int(sz)) { - return nil, errors.New("parse error") - } - var savedPub []byte - if !str.ReadUint16(&sz) || !str.ReadBytes(&savedPub, int(sz)) { + var version uint8 + var srkName, savedPriv, savedPub cryptobyte.String + if !str.ReadUint8(&version) || version != keyFormatVersion || + !str.ReadUint16LengthPrefixed(&srkName) || + !str.ReadUint16LengthPrefixed(&savedPriv) || + !str.ReadUint16LengthPrefixed(&savedPub) || + !str.Empty() { return nil, errors.New("parse error") } priv, err := tpm2.Unmarshal[tpm2.TPM2BPrivate](savedPriv) @@ -497,14 +524,34 @@ func (t *TPM) unmarshalLocked(in []byte) (*Key, error) { if err != nil { return nil, fmt.Errorf("TPM2_Unmarshal: %w", err) } + pubContents, err := pub.Contents() + if err != nil { + return nil, fmt.Errorf("TPM2_Unmarshal: %w", err) + } + name, err := tpm2.ObjectName(pubContents) + if err != nil { + return nil, fmt.Errorf("TPM2_Unmarshal: %w", err) + } + + srk, err := t.srkLocked() + if err != nil { + return nil, err + } + if !bytes.Equal(srk.Name.Buffer, srkName) { + return nil, ErrWrongTPM + } out := &Key{ t: t, id: hex.EncodeToString(hashed[:]), + name: *name, priv: *priv, pub: *pub, keyb: in, } + if err := out.parsePublic(pubContents); err != nil { + return nil, err + } if t.loadedKey == out.id { t.flushLocked() } @@ -522,6 +569,7 @@ var _ crypto.Signer = (*Key)(nil) type Key struct { t *TPM id string + name tpm2.TPM2BName priv tpm2.TPM2BPrivate pub tpm2.TPM2BPublic keyb []byte @@ -537,54 +585,48 @@ func (k *Key) loadLocked() error { } k.t.flushLocked() - srk, flush, err := k.t.srk() + srk, err := k.t.srkLocked() if err != nil { return err } - defer flush() loadResp, err := tpm2.Load{ - ParentHandle: srk, - InPrivate: k.priv, - InPublic: k.pub, + ParentHandle: tpm2.AuthHandle{ + Handle: srk.Handle, + Name: srk.Name, + Auth: k.t.sessionLocked(nil), + }, + InPrivate: k.priv, + InPublic: k.pub, }.Execute(k.t.tpm) if err != nil { return fmt.Errorf("TPM2_Load: %w", err) } + if !bytes.Equal(loadResp.Name.Buffer, k.name.Buffer) { + tpm2.FlushContext{FlushHandle: loadResp.ObjectHandle}.Execute(k.t.tpm) + return errors.New("TPM2_Load: unexpected object name") + } k.t.loadedKey = k.id k.t.loadedHandle = loadResp.ObjectHandle - if k.publicKey == nil { - if err := k.getPublicLocked(); err != nil { - return err - } - } return nil } -func (k *Key) getPublicLocked() error { - readPublicResp, err := tpm2.ReadPublic{ObjectHandle: k.t.loadedHandle}.Execute(k.t.tpm) - if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) - } - outPublic, err := readPublicResp.OutPublic.Contents() - if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) - } - +// parsePublic extracts the key's parameters from its public area. +func (k *Key) parsePublic(outPublic *tpm2.TPMTPublic) error { switch tpm2.TPMAlgID(outPublic.Type) { case tpm2.TPMAlgRSA: rsaParms, err := outPublic.Parameters.RSADetail() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } rsaPubKeyN, err := outPublic.Unique.RSA() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } rsaPubKey, err := tpm2.RSAPub(rsaParms, rsaPubKeyN) if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } k.publicKey = rsaPubKey k.keyType = TypeRSA @@ -593,15 +635,15 @@ func (k *Key) getPublicLocked() error { case tpm2.TPMAlgECC: eccParms, err := outPublic.Parameters.ECCDetail() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } c, err := tpm2.TPMECCCurve(eccParms.CurveID).Curve() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } eccPoint, err := outPublic.Unique.ECC() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } k.publicKey = &ecdsa.PublicKey{ Curve: c, @@ -614,12 +656,12 @@ func (k *Key) getPublicLocked() error { case tpm2.TPMAlgSymCipher: symParms, err := outPublic.Parameters.SymDetail() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } if symParms.Sym.Algorithm == tpm2.TPMAlgAES { bits, err := symParms.Sym.KeyBits.AES() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } k.keyType = TypeAES k.bits = int(*bits) @@ -628,13 +670,13 @@ func (k *Key) getPublicLocked() error { case tpm2.TPMAlgKeyedHash: keyedHashParms, err := outPublic.Parameters.KeyedHashDetail() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %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) + return fmt.Errorf("parsePublic: %w", err) } switch details.HashAlg { case tpm2.TPMAlgSHA384: @@ -659,41 +701,21 @@ func (k *Key) Marshal() ([]byte, error) { // Public returns the public key. func (k *Key) Public() crypto.PublicKey { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil - } return k.publicKey } // Type returns the key type. func (k *Key) Type() KeyType { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return 0 - } return k.keyType } // Bits returns the key size. func (k *Key) Bits() int { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return 0 - } return k.bits } // Curve returns the key curve ID. func (k *Key) Curve() elliptic.Curve { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil - } return k.curve } @@ -726,10 +748,8 @@ func (k *Key) hmacLocked(message []byte) ([]byte, error) { resp, err := tpm2.Hmac{ Handle: tpm2.AuthHandle{ Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), + Name: k.name, + Auth: k.t.sessionLocked(k.t.objectAuth, tpm2.AESEncryption(128, tpm2.EncryptInOut)), }, Buffer: tpm2.TPM2BMaxBuffer{ Buffer: message, @@ -781,10 +801,8 @@ func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signatur resp, err := tpm2.Sign{ KeyHandle: tpm2.AuthHandle{ Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), + Name: k.name, + Auth: k.t.sessionLocked(k.t.objectAuth, tpm2.AESEncryption(128, tpm2.EncryptIn)), }, Digest: tpm2.TPM2BDigest{ Buffer: digest, @@ -814,10 +832,8 @@ func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signatur resp, err := tpm2.Sign{ KeyHandle: tpm2.AuthHandle{ Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), + Name: k.name, + Auth: k.t.sessionLocked(k.t.objectAuth, tpm2.AESEncryption(128, tpm2.EncryptIn)), }, Digest: tpm2.TPM2BDigest{ Buffer: digest, @@ -881,29 +897,18 @@ func addASN1IntBytes(b *cryptobyte.Builder, bytes []byte) { // (AES-GCM) using a single-use data key that is wrapped by the TPM key. There // is no practical size limit. func (k *Key) Encrypt(cleartext []byte) (ciphertext []byte, err error) { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil, err - } switch k.keyType { case TypeRSA: - resp, err := tpm2.RSAEncrypt{ - KeyHandle: k.t.loadedHandle, - Message: tpm2.TPM2BPublicKeyRSA{ - Buffer: cleartext, - }, - InScheme: tpm2.TPMTRSADecrypt{ - Scheme: tpm2.TPMAlgOAEP, - Details: tpm2.NewTPMUAsymScheme(tpm2.TPMAlgOAEP, &tpm2.TPMSEncSchemeOAEP{HashAlg: tpm2.TPMAlgSHA256}), - }, - }.Execute(k.t.tpm) - if err != nil { - return nil, fmt.Errorf("TPM2_RSAEncrypt: %w", err) - } - return resp.OutData.Buffer, nil + // Only the public key is needed. Encrypting in software keeps + // the cleartext off the bus. + return rsa.EncryptOAEP(sha256.New(), rand.Reader, k.publicKey.(*rsa.PublicKey), cleartext, nil) case TypeAES: + k.t.mu.Lock() + defer k.t.mu.Unlock() + if err := k.loadLocked(); err != nil { + return nil, err + } return k.aesEncryptLocked(cleartext) default: @@ -931,10 +936,8 @@ func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, opts crypto.DecrypterOpts) resp, err := tpm2.RSADecrypt{ KeyHandle: tpm2.AuthHandle{ Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), + Name: k.name, + Auth: k.t.sessionLocked(k.t.objectAuth, tpm2.AESEncryption(128, tpm2.EncryptInOut)), }, CipherText: tpm2.TPM2BPublicKeyRSA{ Buffer: ciphertext, @@ -1079,10 +1082,8 @@ func (k *Key) tpmAESLocked(in, iv []byte, decrypt bool) ([]byte, error) { resp, err := tpm2.EncryptDecrypt2{ KeyHandle: tpm2.AuthHandle{ Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), + Name: k.name, + Auth: k.t.sessionLocked(k.t.objectAuth, tpm2.AESEncryption(128, tpm2.EncryptInOut)), }, Message: tpm2.TPM2BMaxBuffer{ Buffer: in, @@ -1098,22 +1099,59 @@ func (k *Key) tpmAESLocked(in, iv []byte, decrypt bool) ([]byte, error) { return resp.OutData.Buffer, nil } -func (t *TPM) srk() (tpm2.NamedHandle, func(), error) { +// srkLocked returns the storage root key (SRK), creating it if needed. The SRK +// is the parent of all the keys, and it is used to salt the sessions that +// protect the commands sent to the TPM. It stays loaded until Close. +func (t *TPM) srkLocked() (tpm2.NamedHandle, error) { + if t.srk.Handle != 0 { + return t.srk, nil + } createPrimaryResp, err := tpm2.CreatePrimary{ PrimaryHandle: tpm2.AuthHandle{ Handle: tpm2.TPMRHOwner, - Auth: tpm2.PasswordAuth(t.ownerAuth), + // No salt is available yet. The HMAC session keeps + // the owner auth value off the bus. + Auth: tpm2.HMAC(tpm2.TPMAlgSHA256, 16, tpm2.Auth(t.ownerAuth)), }, InPublic: tpm2.New2B(tpm2.RSASRKTemplate), }.Execute(t.tpm) if err != nil { - return tpm2.NamedHandle{}, nil, fmt.Errorf("TPM2_CreatePrimary: %w", err) + return tpm2.NamedHandle{}, fmt.Errorf("TPM2_CreatePrimary: %w", err) + } + flush := func() { + tpm2.FlushContext{FlushHandle: createPrimaryResp.ObjectHandle}.Execute(t.tpm) } - srk := tpm2.NamedHandle{ + pub, err := createPrimaryResp.OutPublic.Contents() + if err != nil { + flush() + return tpm2.NamedHandle{}, fmt.Errorf("TPM2_CreatePrimary: %w", err) + } + name, err := tpm2.ObjectName(pub) + if err != nil { + flush() + return tpm2.NamedHandle{}, fmt.Errorf("TPM2_CreatePrimary: %w", err) + } + if !bytes.Equal(name.Buffer, createPrimaryResp.Name.Buffer) { + flush() + return tpm2.NamedHandle{}, errors.New("TPM2_CreatePrimary: unexpected object name") + } + t.srk = tpm2.NamedHandle{ Handle: createPrimaryResp.ObjectHandle, - Name: createPrimaryResp.Name, + Name: *name, } - return srk, func() { - tpm2.FlushContext{FlushHandle: srk}.Execute(t.tpm) - }, nil + t.srkPublic = *pub + return t.srk, nil +} + +// sessionLocked returns a one-time HMAC session, salted with the SRK, to +// authorize the use of an object with the given auth value. The auth value is +// never sent to the TPM, the TPM's response is authenticated, and opts can +// enable encryption of the first command and/or response parameter. The SRK +// must already be loaded. +func (t *TPM) sessionLocked(auth []byte, opts ...tpm2.AuthOption) tpm2.Session { + opts = append([]tpm2.AuthOption{ + tpm2.Auth(auth), + tpm2.Salted(t.srk.Handle, t.srkPublic), + }, opts...) + return tpm2.HMAC(tpm2.TPMAlgSHA256, 16, opts...) } diff --git a/tpm_test.go b/tpm_test.go index 47d8329..0e007fc 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -31,12 +31,15 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha256" + "errors" "fmt" + "io" "slices" "testing" "github.com/google/go-tpm-tools/simulator" "github.com/google/go-tpm/tpm2" + "github.com/google/go-tpm/tpm2/transport" ) func TestRSA(t *testing.T) { @@ -465,12 +468,128 @@ func TestClearInvalidatesKeys(t *testing.T) { Auth: tpm2.PasswordAuth(nil), }, }.Execute(tpm.tpm) + // TPM2_Clear flushed the SRK. + tpm.srk = tpm2.NamedHandle{} tpm.mu.Unlock() if err != nil { t.Fatalf("TPM2_Clear: %v", err) } - if _, err := tpm.UnmarshalKey(b); err == nil { - t.Fatal("tpm.UnmarshalKey should fail after TPM2_Clear") + if _, err := tpm.UnmarshalKey(b); !errors.Is(err, ErrWrongTPM) { + t.Fatalf("tpm.UnmarshalKey after TPM2_Clear: got %v, want %v", err, ErrWrongTPM) + } +} + +func TestWrongTPM(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + key, err := tpm.CreateKey() + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + b, err := key.Marshal() + if err != nil { + t.Fatalf("key.Marshal: %v", err) + } + tpm.Close() + + rwc2, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + other, err := New(WithTPM(rwc2)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer other.Close() + if _, err := other.UnmarshalKey(b); !errors.Is(err, ErrWrongTPM) { + t.Fatalf("tpm.UnmarshalKey on different TPM: got %v, want %v", err, ErrWrongTPM) + } +} + +// recorder records all the bytes exchanged with the TPM. +type recorder struct { + io.ReadWriteCloser + buf bytes.Buffer +} + +func (r *recorder) Read(b []byte) (int, error) { + n, err := r.ReadWriteCloser.Read(b) + r.buf.Write(b[:n]) + return n, err +} + +func (r *recorder) Write(b []byte) (int, error) { + r.buf.Write(b) + return r.ReadWriteCloser.Write(b) +} + +func TestBusConfidentiality(t *testing.T) { + var ( + ownerAuth = []byte("owner-secret-passphrase") + objectAuth = []byte("object-secret-passphrase") + payload = []byte("very-secret-payload-0123456789") + ) + + sim, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + if _, err := (tpm2.HierarchyChangeAuth{ + AuthHandle: tpm2.AuthHandle{ + Handle: tpm2.TPMRHOwner, + Auth: tpm2.PasswordAuth(nil), + }, + NewAuth: tpm2.TPM2BAuth{Buffer: ownerAuth}, + }).Execute(transport.FromReadWriteCloser(sim)); err != nil { + t.Fatalf("TPM2_HierarchyChangeAuth: %v", err) + } + rec := &recorder{ReadWriteCloser: sim} + + tpm, err := New(WithTPM(rec), WithOwnerAuth(ownerAuth), WithObjectAuth(objectAuth)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + for _, opt := range []KeyOption{WithRSA(2048), WithAES(128), WithHMAC(256)} { + key, err := tpm.CreateKey(opt) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + switch key.Type() { + case TypeRSA, TypeAES: + enc, err := key.Encrypt(payload) + if err != nil { + t.Fatalf("key.Encrypt: %v", err) + } + dec, err := key.Decrypt(nil, enc, nil) + if err != nil { + t.Fatalf("key.Decrypt: %v", err) + } + if !bytes.Equal(dec, payload) { + t.Fatalf("key.Decrypt() = %q, want %q", dec, payload) + } + case TypeHMAC: + mac, err := key.HMAC(payload) + if err != nil { + t.Fatalf("key.HMAC: %v", err) + } + if bytes.Contains(rec.buf.Bytes(), mac) { + t.Error("HMAC output seen on the bus") + } + } + } + + for _, secret := range [][]byte{ownerAuth, objectAuth, payload} { + if bytes.Contains(rec.buf.Bytes(), secret) { + t.Errorf("%q seen on the bus", secret) + } } } From e7d4281f1923e84eff05ce9ed8860b967bb9d86a Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 11:01:46 -0700 Subject: [PATCH 05/12] Reject keys not generated inside the TPM UnmarshalKey accepted any object that loaded under the SRK. Anyone with access to the TPM can create or import objects under the SRK, including objects whose sensitive data they choose (SensitiveDataOrigin clear) or that can be duplicated (FixedTPM/FixedParent clear). If such a blob replaced a stored key, the application would silently use a key known to the attacker. UnmarshalKey now requires FixedTPM, FixedParent, SensitiveDataOrigin, and UserWithAuth, and a supported key type. Otherwise it returns ErrInvalidKey. Co-Authored-By: Claude Opus 5.5 --- tpm.go | 11 ++++++++ tpm_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/tpm.go b/tpm.go index ff7016b..fc58731 100644 --- a/tpm.go +++ b/tpm.go @@ -70,6 +70,7 @@ var ( ErrWrongKeyType = errors.New("operation not implemented with this key type") ErrInvalidCurve = errors.New("invalid curve id") ErrDecrypt = errors.New("decryption error") + ErrInvalidKey = errors.New("invalid or unsupported key") ErrWrongTPM = errors.New("key was created with a different TPM or storage root key") ) @@ -614,6 +615,13 @@ func (k *Key) loadLocked() error { // parsePublic extracts the key's parameters from its public area. func (k *Key) parsePublic(outPublic *tpm2.TPMTPublic) error { + // Only accept keys that were generated inside this TPM and that can't + // leave it. Anyone with access to the TPM can create or import keys + // under the SRK, including keys with sensitive data that they know. + attrs := outPublic.ObjectAttributes + if !attrs.FixedTPM || !attrs.FixedParent || !attrs.SensitiveDataOrigin || !attrs.UserWithAuth { + return ErrInvalidKey + } switch tpm2.TPMAlgID(outPublic.Type) { case tpm2.TPMAlgRSA: rsaParms, err := outPublic.Parameters.RSADetail() @@ -690,6 +698,9 @@ func (k *Key) parsePublic(outPublic *tpm2.TPMTPublic) error { } } } + if k.keyType == 0 { + return ErrInvalidKey + } return nil } diff --git a/tpm_test.go b/tpm_test.go index 0e007fc..a7dc8c3 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -40,6 +40,7 @@ import ( "github.com/google/go-tpm-tools/simulator" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" + "golang.org/x/crypto/cryptobyte" ) func TestRSA(t *testing.T) { @@ -593,3 +594,74 @@ func TestBusConfidentiality(t *testing.T) { } } } + +func TestRejectKnownSensitiveKey(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + // Create an HMAC key under the SRK with a key value chosen by the + // caller, i.e. a key that isn't secret. + tpm.mu.Lock() + srk, err := tpm.srkLocked() + if err != nil { + tpm.mu.Unlock() + t.Fatalf("srkLocked: %v", err) + } + createResp, err := tpm2.Create{ + ParentHandle: tpm2.AuthHandle{ + Handle: srk.Handle, + Name: srk.Name, + Auth: tpm.sessionLocked(nil), + }, + InSensitive: tpm2.TPM2BSensitiveCreate{ + Sensitive: &tpm2.TPMSSensitiveCreate{ + Data: tpm2.NewTPMUSensitiveCreate(&tpm2.TPM2BSensitiveData{ + Buffer: []byte("attacker-known-key"), + }), + }, + }, + InPublic: tpm2.New2B(tpm2.TPMTPublic{ + Type: tpm2.TPMAlgKeyedHash, + NameAlg: tpm2.TPMAlgSHA256, + ObjectAttributes: tpm2.TPMAObject{ + FixedTPM: true, + FixedParent: true, + UserWithAuth: true, + SignEncrypt: true, + }, + Parameters: tpm2.NewTPMUPublicParms( + tpm2.TPMAlgKeyedHash, + &tpm2.TPMSKeyedHashParms{ + Scheme: tpm2.TPMTKeyedHashScheme{ + Scheme: tpm2.TPMAlgHMAC, + Details: tpm2.NewTPMUSchemeKeyedHash( + tpm2.TPMAlgHMAC, + &tpm2.TPMSSchemeHMAC{HashAlg: tpm2.TPMAlgSHA256}, + ), + }, + }, + ), + }), + }.Execute(tpm.tpm) + tpm.mu.Unlock() + if err != nil { + t.Fatalf("TPM2_Create: %v", err) + } + + var b cryptobyte.Builder + b.AddUint8(keyFormatVersion) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(srk.Name.Buffer) }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(tpm2.Marshal(createResp.OutPrivate)) }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(tpm2.Marshal(createResp.OutPublic)) }) + + if _, err := tpm.UnmarshalKey(b.BytesOrPanic()); !errors.Is(err, ErrInvalidKey) { + t.Fatalf("tpm.UnmarshalKey: got %v, want %v", err, ErrInvalidKey) + } +} From fa8c827ee321871135f50cab9b97869b7bd5499d Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 11:03:03 -0700 Subject: [PATCH 06/12] Add WithSigningOnly and WithDecryptionOnly key options RSA and ECC keys are created with both the sign and decrypt attributes. The TPM requires such keys to have no fixed scheme, so anyone who can use the key with the TPM directly can also use schemes this package never uses, e.g. raw RSA or PKCS#1 v1.5 decryption. New opt-in key options: - WithSigningOnly (RSA, ECC): the decrypt attribute is cleared. - WithDecryptionOnly (RSA): the sign attribute is cleared and the scheme is fixed to RSA-OAEP with SHA-256. The default is unchanged (dual-use) and is now documented. Sign, Encrypt, and Decrypt return ErrKeyUsage when the key doesn't allow the operation, and the TPM enforces the same restrictions. Co-Authored-By: Claude Opus 5.5 --- tpm.go | 90 +++++++++++++++++++++++++++++++++++------ tpm_test.go | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 13 deletions(-) diff --git a/tpm.go b/tpm.go index fc58731..a4c77af 100644 --- a/tpm.go +++ b/tpm.go @@ -71,6 +71,7 @@ var ( ErrInvalidCurve = errors.New("invalid curve id") ErrDecrypt = errors.New("decryption error") ErrInvalidKey = errors.New("invalid or unsupported key") + ErrKeyUsage = errors.New("operation not permitted by the key's usage") ErrWrongTPM = errors.New("key was created with a different TPM or storage root key") ) @@ -173,8 +174,17 @@ type keyOptions struct { keyType KeyType bits int curve elliptic.Curve + usage keyUsage } +type keyUsage int + +const ( + usageAny keyUsage = iota + usageSign + usageDecrypt +) + // KeyOption is an option that can be passed to CreateKey. type KeyOption func(*keyOptions) @@ -196,6 +206,29 @@ func WithECC(curve elliptic.Curve) KeyOption { } } +// WithSigningOnly restricts a new RSA or ECC key to signing. The key can't be +// used to decrypt. +// +// By default, RSA and ECC keys can be used for both signing and decryption. +// The TPM requires such dual-use keys to have no fixed scheme, so they can +// also be used with other schemes than the ones that this package uses (e.g. +// raw RSA decryption), by anyone who can use the key directly with the TPM. +func WithSigningOnly() KeyOption { + return func(opts *keyOptions) { + opts.usage = usageSign + } +} + +// WithDecryptionOnly restricts a new RSA key to RSA-OAEP decryption with +// SHA-256. The key can't be used to sign. +// +// See [WithSigningOnly]. +func WithDecryptionOnly() KeyOption { + return func(opts *keyOptions) { + opts.usage = usageDecrypt + } +} + // WithAES indicates that an AES key should be created. func WithAES(bits int) KeyOption { return func(opts *keyOptions) { @@ -235,6 +268,13 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { for _, o := range opts { o(&opt) } + switch { + case opt.usage == usageAny: + case opt.keyType == TypeRSA: + case opt.keyType == TypeECC && opt.usage == usageSign: + default: + return nil, fmt.Errorf("key usage not supported with %s keys", opt.keyType) + } t.flushLocked() srk, err := t.srkLocked() @@ -246,6 +286,16 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { switch opt.keyType { case TypeRSA: + rsaScheme := tpm2.TPMTRSAScheme{Scheme: tpm2.TPMAlgNull} + if opt.usage == usageDecrypt { + rsaScheme = tpm2.TPMTRSAScheme{ + Scheme: tpm2.TPMAlgOAEP, + Details: tpm2.NewTPMUAsymScheme( + tpm2.TPMAlgOAEP, + &tpm2.TPMSEncSchemeOAEP{HashAlg: tpm2.TPMAlgSHA256}, + ), + } + } unique := make([]byte, opt.bits/8) if _, err := io.ReadFull(rand.Reader, unique); err != nil { return nil, fmt.Errorf("rand: %w", err) @@ -263,12 +313,13 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { NoDA: false, EncryptedDuplication: false, Restricted: false, - Decrypt: true, - SignEncrypt: true, + Decrypt: opt.usage != usageSign, + SignEncrypt: opt.usage != usageDecrypt, }, Parameters: tpm2.NewTPMUPublicParms( tpm2.TPMAlgRSA, &tpm2.TPMSRSAParms{ + Scheme: rsaScheme, KeyBits: tpm2.TPMKeyBits(opt.bits), }, ), @@ -317,7 +368,7 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { NoDA: false, EncryptedDuplication: false, Restricted: false, - Decrypt: true, + Decrypt: opt.usage != usageSign, SignEncrypt: true, }, Parameters: tpm2.NewTPMUPublicParms( @@ -568,16 +619,18 @@ var _ crypto.Signer = (*Key)(nil) // Key performs cryptographic operations via the TPM. It implements the // [crypto.Signer] and [crypto.Decrypter] interfaces. type Key struct { - t *TPM - id string - name tpm2.TPM2BName - priv tpm2.TPM2BPrivate - pub tpm2.TPM2BPublic - keyb []byte - keyType KeyType - bits int - curve elliptic.Curve - publicKey crypto.PublicKey + t *TPM + id string + name tpm2.TPM2BName + priv tpm2.TPM2BPrivate + pub tpm2.TPM2BPublic + keyb []byte + keyType KeyType + bits int + curve elliptic.Curve + publicKey crypto.PublicKey + canSign bool + canDecrypt bool } func (k *Key) loadLocked() error { @@ -622,6 +675,8 @@ func (k *Key) parsePublic(outPublic *tpm2.TPMTPublic) error { if !attrs.FixedTPM || !attrs.FixedParent || !attrs.SensitiveDataOrigin || !attrs.UserWithAuth { return ErrInvalidKey } + k.canSign = attrs.SignEncrypt + k.canDecrypt = attrs.Decrypt switch tpm2.TPMAlgID(outPublic.Type) { case tpm2.TPMAlgRSA: rsaParms, err := outPublic.Parameters.RSADetail() @@ -776,6 +831,9 @@ func (k *Key) hmacLocked(message []byte) ([]byte, error) { // 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) { + if !k.canSign { + return nil, ErrKeyUsage + } k.t.mu.Lock() defer k.t.mu.Unlock() if err := k.loadLocked(); err != nil { @@ -908,6 +966,9 @@ func addASN1IntBytes(b *cryptobyte.Builder, bytes []byte) { // (AES-GCM) using a single-use data key that is wrapped by the TPM key. There // is no practical size limit. func (k *Key) Encrypt(cleartext []byte) (ciphertext []byte, err error) { + if k.keyType == TypeRSA && !k.canDecrypt { + return nil, ErrKeyUsage + } switch k.keyType { case TypeRSA: // Only the public key is needed. Encrypting in software keeps @@ -933,6 +994,9 @@ func (k *Key) Encrypt(cleartext []byte) (ciphertext []byte, err error) { // label) or a [*rsa.OAEPOptions]. The TPM requires OAEP labels to be // null-terminated, so a non-empty Label must end with a 0x00 byte. func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) { + if k.keyType == TypeRSA && !k.canDecrypt { + return nil, ErrKeyUsage + } k.t.mu.Lock() defer k.t.mu.Unlock() if err := k.loadLocked(); err != nil { diff --git a/tpm_test.go b/tpm_test.go index a7dc8c3..e3a9483 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -665,3 +665,115 @@ func TestRejectKnownSensitiveKey(t *testing.T) { t.Fatalf("tpm.UnmarshalKey: got %v, want %v", err, ErrInvalidKey) } } + +func TestKeyUsage(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + payload := []byte("Hello World!") + hashed := sha256.Sum256(payload) + + t.Run("RSA signing only", func(t *testing.T) { + key, err := tpm.CreateKey(WithRSA(2048), WithSigningOnly()) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + sig, err := key.Sign(nil, hashed[:], crypto.SHA256) + if err != nil { + t.Fatalf("key.Sign: %v", err) + } + if err := rsa.VerifyPKCS1v15(key.Public().(*rsa.PublicKey), crypto.SHA256, hashed[:], sig); err != nil { + t.Fatalf("VerifyPKCS1v15: %v", err) + } + if _, err := key.Encrypt(payload); !errors.Is(err, ErrKeyUsage) { + t.Fatalf("key.Encrypt: got %v, want %v", err, ErrKeyUsage) + } + enc, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, key.Public().(*rsa.PublicKey), payload, nil) + if err != nil { + t.Fatalf("rsa.EncryptOAEP: %v", err) + } + if _, err := key.Decrypt(nil, enc, nil); !errors.Is(err, ErrKeyUsage) { + t.Fatalf("key.Decrypt: got %v, want %v", err, ErrKeyUsage) + } + // The TPM enforces it too. + key.canDecrypt = true + if _, err := key.Decrypt(nil, enc, nil); err == nil { + t.Fatal("TPM decrypted with a signing-only key") + } + }) + + t.Run("RSA decryption only", func(t *testing.T) { + key, err := tpm.CreateKey(WithRSA(2048), WithDecryptionOnly()) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + enc, err := key.Encrypt(payload) + if err != nil { + t.Fatalf("key.Encrypt: %v", err) + } + dec, err := key.Decrypt(nil, enc, nil) + if err != nil { + t.Fatalf("key.Decrypt: %v", err) + } + if !bytes.Equal(dec, payload) { + t.Fatalf("key.Decrypt() = %q, want %q", dec, payload) + } + if _, err := key.Sign(nil, hashed[:], crypto.SHA256); !errors.Is(err, ErrKeyUsage) { + t.Fatalf("key.Sign: got %v, want %v", err, ErrKeyUsage) + } + // The TPM enforces it too. + key.canSign = true + if _, err := key.Sign(nil, hashed[:], crypto.SHA256); err == nil { + t.Fatal("TPM signed with a decryption-only key") + } + // Only the key's scheme (OAEP) can be used, not raw RSA. + tpm.mu.Lock() + _, err = tpm2.RSADecrypt{ + KeyHandle: tpm2.AuthHandle{ + Handle: tpm.loadedHandle, + Name: key.name, + Auth: tpm.sessionLocked(nil), + }, + CipherText: tpm2.TPM2BPublicKeyRSA{Buffer: enc}, + InScheme: tpm2.TPMTRSADecrypt{Scheme: tpm2.TPMAlgRSAES}, + }.Execute(tpm.tpm) + tpm.mu.Unlock() + if err == nil { + t.Fatal("TPM decrypted with a scheme other than the key's scheme") + } + }) + + t.Run("ECC signing only", func(t *testing.T) { + key, err := tpm.CreateKey(WithECC(elliptic.P256()), WithSigningOnly()) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + sig, err := key.Sign(nil, hashed[:], crypto.SHA256) + if err != nil { + t.Fatalf("key.Sign: %v", err) + } + if !ecdsa.VerifyASN1(key.Public().(*ecdsa.PublicKey), hashed[:], sig) { + t.Fatal("VerifyASN1 failed") + } + }) + + t.Run("Unsupported", func(t *testing.T) { + for _, opts := range [][]KeyOption{ + {WithECC(elliptic.P256()), WithDecryptionOnly()}, + {WithAES(128), WithSigningOnly()}, + {WithAES(128), WithDecryptionOnly()}, + {WithHMAC(256), WithDecryptionOnly()}, + } { + if _, err := tpm.CreateKey(opts...); err == nil { + t.Errorf("tpm.CreateKey should have failed") + } + } + }) +} From 34968044e1202a9d15d8d2140dddef043c112b4b Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 11:03:30 -0700 Subject: [PATCH 07/12] Reject SHA-1 and mismatched digest lengths in Sign SHA-1 is not collision resistant and shouldn't be used for new signatures. Sign now also checks that the digest length matches the hash function instead of relying on the TPM. Co-Authored-By: Claude Opus 5.5 --- tpm.go | 23 +++++++++++------------ tpm_test.go | 8 ++++++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/tpm.go b/tpm.go index a4c77af..47bd947 100644 --- a/tpm.go +++ b/tpm.go @@ -829,7 +829,7 @@ func (k *Key) hmacLocked(message []byte) ([]byte, error) { } // Sign signs a digest with the key (RSA and ECC) or computes the HMAC -// (HMAC keys). +// (HMAC keys). The digest must be a SHA-256, SHA-384, or SHA-512 hash. func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { if !k.canSign { return nil, ErrKeyUsage @@ -840,19 +840,18 @@ func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signatur return nil, err } - var hashAlg tpm2.TPMSSchemeHash - switch h := opts.HashFunc(); h { - case crypto.SHA1: - hashAlg = tpm2.TPMSSchemeHash{HashAlg: tpm2.TPMAlgSHA1} - case crypto.SHA256: - hashAlg = tpm2.TPMSSchemeHash{HashAlg: tpm2.TPMAlgSHA256} - case crypto.SHA384: - hashAlg = tpm2.TPMSSchemeHash{HashAlg: tpm2.TPMAlgSHA384} - case crypto.SHA512: - hashAlg = tpm2.TPMSSchemeHash{HashAlg: tpm2.TPMAlgSHA512} - default: + h := opts.HashFunc() + if h == crypto.SHA1 { return nil, fmt.Errorf("unexpected hash %v", h) } + tpmHash, err := tpmHashAlg(h) + if err != nil { + return nil, err + } + if len(digest) != h.Size() { + return nil, fmt.Errorf("digest length %d, want %d for %v", len(digest), h.Size(), h) + } + hashAlg := tpm2.TPMSSchemeHash{HashAlg: tpmHash} switch k.keyType { case TypeRSA: diff --git a/tpm_test.go b/tpm_test.go index e3a9483..2cca25c 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -30,6 +30,7 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "crypto/sha1" "crypto/sha256" "errors" "fmt" @@ -122,6 +123,13 @@ func TestRSA(t *testing.T) { if err := rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed[:], sig); err != nil { t.Fatalf("VerifyPKCS1v15: %v", err) } + sha1Hashed := sha1.Sum([]byte(payload)) + if _, err := key.Sign(nil, sha1Hashed[:], crypto.SHA1); err == nil { + t.Fatal("Sign() with SHA-1 should have failed") + } + if _, err := key.Sign(nil, hashed[:16], crypto.SHA256); err == nil { + t.Fatal("Sign() with short digest should have failed") + } pssOptions := &rsa.PSSOptions{SaltLength: 32, Hash: crypto.SHA256} sig2, err := key.Sign(nil, hashed[:], pssOptions) if err != nil { From 45dc17aa6ab5c669f162e85f9b78c5d3dff9dd3d Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 11:03:53 -0700 Subject: [PATCH 08/12] Only flush leftover transient objects on /dev/tpm0 New flushed every transient object visible on the TPM connection. With /dev/tpmrm0 there's nothing to flush, and a TPM passed in with WithTPM may be shared, so this could destroy other users' objects. Leftover objects are now flushed only when New opens /dev/tpm0 itself, since that device can only be opened by one process at a time. New also closes the device if this step fails. Co-Authored-By: Claude Opus 5.5 --- tpm.go | 32 ++++++++++++++++++++++++-------- tpm_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/tpm.go b/tpm.go index 47bd947..e6b3d89 100644 --- a/tpm.go +++ b/tpm.go @@ -95,7 +95,8 @@ func (t KeyType) String() string { // Option is an option that can be passed to New. type Option func(*TPM) -// WithTPM specifies an already open TPM device to use. +// WithTPM specifies an already open TPM device to use. The TPM may be shared +// with other users, so New leaves existing objects alone. func WithTPM(rwc io.ReadWriteCloser) Option { return func(t *TPM) { t.tpm = transport.FromReadWriteCloser(rwc) @@ -119,6 +120,9 @@ func WithObjectAuth(pp []byte) Option { } // New returns a new TPM that's ready to use. +// +// Without [WithTPM], New opens /dev/tpmrm0, or /dev/tpm0 if the kernel +// resource manager isn't available. func New(opts ...Option) (*TPM, error) { var tpm TPM for _, o := range opts { @@ -127,31 +131,43 @@ func New(opts ...Option) (*TPM, error) { if tpm.tpm == nil { t, err := linuxtpm.Open("/dev/tpmrm0") if errors.Is(err, os.ErrNotExist) { - t, err = linuxtpm.Open("/dev/tpm0") + if t, err = linuxtpm.Open("/dev/tpm0"); err != nil { + return nil, err + } + // Without the resource manager, transient objects + // left behind by a previous process stay loaded. + // /dev/tpm0 can only be opened by one process at a + // time, so they can't belong to anyone else. + if err := flushTransientHandles(t); err != nil { + t.Close() + return nil, err + } } if err != nil { return nil, err } tpm.tpm = t } + return &tpm, nil +} - // Flush any existing transient handles. +func flushTransientHandles(t transport.TPM) error { capResp, err := tpm2.GetCapability{ Capability: tpm2.TPMCapHandles, Property: uint32(tpm2.TPMHTTransient) << 24, PropertyCount: 100, - }.Execute(tpm.tpm) + }.Execute(t) if err != nil { - return nil, fmt.Errorf("TPM2_GetCapability: %w", err) + return fmt.Errorf("TPM2_GetCapability: %w", err) } handles, err := capResp.CapabilityData.Data.Handles() if err != nil { - return nil, fmt.Errorf("TPM2_GetCapability(Handles): %w", err) + return fmt.Errorf("TPM2_GetCapability(Handles): %w", err) } for _, h := range handles.Handle { - tpm2.FlushContext{FlushHandle: h}.Execute(tpm.tpm) + tpm2.FlushContext{FlushHandle: h}.Execute(t) } - return &tpm, nil + return nil } var _ io.Closer = (*TPM)(nil) diff --git a/tpm_test.go b/tpm_test.go index 2cca25c..46fea8b 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -785,3 +785,27 @@ func TestKeyUsage(t *testing.T) { } }) } + +func TestNewKeepsOtherObjects(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + other, err := tpm2.CreatePrimary{ + PrimaryHandle: tpm2.TPMRHOwner, + InPublic: tpm2.New2B(tpm2.ECCSRKTemplate), + }.Execute(transport.FromReadWriteCloser(rwc)) + if err != nil { + t.Fatalf("TPM2_CreatePrimary: %v", err) + } + + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + if _, err := (tpm2.ReadPublic{ObjectHandle: other.ObjectHandle}).Execute(tpm.tpm); err != nil { + t.Fatalf("Object created before New is gone: %v", err) + } +} From c3a3838c8deac97eb771c735fd97f086408febe7 Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 11:04:04 -0700 Subject: [PATCH 09/12] Return a copy of the serialized key from Marshal Marshal returned the key's internal buffer, so a caller modifying the result would corrupt the key's stored blob. Co-Authored-By: Claude Opus 5.5 --- tpm.go | 2 +- tpm_test.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tpm.go b/tpm.go index e6b3d89..221fae7 100644 --- a/tpm.go +++ b/tpm.go @@ -778,7 +778,7 @@ func (k *Key) parsePublic(outPublic *tpm2.TPMTPublic) error { // Marshal returns the serialized version of the key, which can be stored // offline, and later unmarshaled with [TPM.UnmarshalKey]. func (k *Key) Marshal() ([]byte, error) { - return k.keyb, nil + return slices.Clone(k.keyb), nil } // Public returns the public key. diff --git a/tpm_test.go b/tpm_test.go index 46fea8b..d62474f 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -411,7 +411,12 @@ func TestMarshal(t *testing.T) { if err != nil { t.Fatalf("key.Marshal: %v", err) } - contexts = append(contexts, b) + contexts = append(contexts, slices.Clone(b)) + // The caller owns the returned slice. + clear(b) + if b2, _ := key.Marshal(); !bytes.Equal(b2, contexts[i]) { + t.Fatal("Modifying the output of key.Marshal changed the key") + } payload := []byte(fmt.Sprintf("Payload %d", i)) enc, err := key.Encrypt(payload) From 0ceb4df6fa6798fbd418e1262706273f7ec216c3 Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 11:05:50 -0700 Subject: [PATCH 10/12] Update go and go deps Go 1.24 no longer gets security fixes. Build with go1.27.1, require go 1.26, and update golang.org/x/crypto, golang.org/x/sys, and go-tpm-tools. govulncheck reports no reachable vulnerabilities. One advisory in golang.org/x/crypto (GO-2026-5932) has no fix yet and isn't reachable from this package. Co-Authored-By: Claude Opus 5.5 --- .goversion | 2 +- go.mod | 10 +++++----- go.sum | 26 ++++++++++++++------------ 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.goversion b/.goversion index 7a429d6..08002f8 100644 --- a/.goversion +++ b/.goversion @@ -1 +1 @@ -1.24.6 +1.27.1 diff --git a/go.mod b/go.mod index 6f13c48..08a0a43 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/c2FmZQ/tpm -go 1.24.0 +go 1.26.0 -toolchain go1.24.6 +toolchain go1.27.1 require ( github.com/google/go-tpm v0.9.8 - github.com/google/go-tpm-tools v0.4.7 - golang.org/x/crypto v0.48.0 + github.com/google/go-tpm-tools v0.4.10 + golang.org/x/crypto v0.57.0 ) -require golang.org/x/sys v0.41.0 // indirect +require golang.org/x/sys v0.48.0 // indirect diff --git a/go.sum b/go.sum index 3da1865..528ab5f 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,24 @@ -github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc h1:SG12DWUUM5igxm+//YX5Yq4vhdoRnOG9HkCodkOn+YU= -github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= +github.com/GoogleCloudPlatform/confidential-space/server v0.0.0-20260915221818-a6c0685fb9fc h1:LPSfe+IaXHrYCNzEVZ4O48UdwNxtiNIEwKgIyU4Aawg= +github.com/GoogleCloudPlatform/confidential-space/server v0.0.0-20260915221818-a6c0685fb9fc/go.mod h1:M77DH8QPPCBBzfdwZPIWpAZdDHJZu0icyq6HTZAEojI= +github.com/google/go-configfs-tsm v0.3.3 h1:8mrlZLYrFFxyc8PFpT1piBUFDEYBVsBjAkFCwqQ2f9Y= +github.com/google/go-configfs-tsm v0.3.3/go.mod h1:in2lmJDGaYEiPOJY4vlq4lGXjkR/GcxN1k7o5oR2qn0= github.com/google/go-sev-guest v0.14.0 h1:dCb4F3YrHTtrDX3cYIPTifEDz7XagZmXQioxRBW4wOo= github.com/google/go-sev-guest v0.14.0/go.mod h1:SK9vW+uyfuzYdVN0m8BShL3OQCtXZe/JPF7ZkpD3760= -github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 h1:+MoPobRN9HrDhGyn6HnF5NYo4uMBKaiFqAtf/D/OB4A= -github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g= +github.com/google/go-tdx-guest v0.3.2-0.20250814004405-ffb0869e6f4d h1:Ff8goEP/ue2/rZT5qyoRicuySCYDbAXEZS8Cf1fgsUo= +github.com/google/go-tdx-guest v0.3.2-0.20250814004405-ffb0869e6f4d/go.mod h1:uHy3VaNXNXhl0fiPxKqTxieeouqQmW6A0EfLcaeCYBk= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws= -github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ= +github.com/google/go-tpm-tools v0.4.10 h1:yKN4HVEaYXYR12gwRSCZ/Ej+5HYP8NClIKGAsc4/mC0= +github.com/google/go-tpm-tools v0.4.10/go.mod h1:5m0NX6bdl0DmD9R2cVBQROaKOeSSFL7FPcgeuQsXkLI= github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= From 2a1e8b3163243ba5c5933f9a802d24a7825b46a4 Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 11:06:04 -0700 Subject: [PATCH 11/12] Document security properties and refresh README example Co-Authored-By: Claude Opus 5.5 --- README.md | 15 +++++++++++---- tpm.go | 7 +++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 79b9e73..72d173a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ care loading the right key in the TPM, as needed. 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. +Keys are created under a storage root key (SRK) in the owner hierarchy, so +clearing the TPM invalidates them. All commands that use keys are protected +by HMAC sessions salted with the SRK: auth values are never sent to the TPM +in cleartext, secret parameters are encrypted, and responses are +authenticated. Serialized keys are bound to the SRK that they were created +under. + ## Example: ```go @@ -106,14 +113,14 @@ func main() { $ go run ./example --sim Key type: RSA-2048 -Saved key: 02e002de0020dd5702c97c8bc5c59cfb5ecfb8c193092738c9a648814206b2257db1616bfa090010c0aa1d491afccbd61604980c3bdcf09a3bb520bf95f067f24a73ebfe201dbcd7fd7b6527419a7279d6bc34eb1438929b16a6cee8b7fe0fa326dbb5cbbce18b428d8fe688da83385b6fb0d0dbe9b7ce389169dfaa218cef452c6a4ec39887f1a889513542105aadac781c37a56056c8950417698f5d890d2c2815378311c242aff2c6cdbb8d293c8eb854dc974287e0525256f2bbb406838b34178a9a5165d3b5e6ed56a1c32349bdf140f91fae960d9c0ec6d571986e94efc02ba8b7516e2f345754323ee0b17536127eae7025e8c4573dcac585d32bff2612cdc04b333e91aa8b027db8cafe269a645e6390228fd2e5413d40663e90e485dc14dc0b2938fb1b7b6649343dc509f0935eee2dbf957d7d0383889e68a5ff9ff6cc6bc944bc2b68645b36c00f4d34f10c5467bbb3253028495b4393c133b294c630095dd90af669800ccc824f38d894193d613687b47d64a3906aa0eff579d758f5216dd68e13066d31f7318bc0f186e5b7b82109585c78c2b79515669674bb657845824aaa1980f41948c68ddcf367e3a6861f6c380e7103b4d0c9efdc2c4d603965039841bccf204d7d117b4e2018d6304a451cd31a7d96a6c4f909ac0ac629941f45735e6aec7d9d6e4bfe9a6e8cdacc588eda02a0314b199a8548b7fed72e3fe160c09c87b4328f57cc5eb0cfa1686b3b2dfc7678238e9e686e37ea95ac993153078dc3f854fbaaf36025e241ab0f9bda9ae643523e3f142c2d6c80df87922daa080bfd295796e27e73e65c5351286475f73e68a365c3c466aa586bd63a53288a857b1311ad57f6c91adcc38b1984a2478f38f57bd45d256bc42bf25a04b13705c30a58f99370ad258e512aeeaa31e8ad465c7b6d3132e1d65f1bb184b66e352118f3e702e35ce0eff9c5368c47da007213143659447b0f5b911948e66d0b5a96c92152a2416d0f39d72db434146417561733638edc412706b44c5b8dec2668011801160001000b000600720000001000100800000000000100c1eb6d8227a6496320fb38146f585c4ec5445ffb2cd98686934e26d4913d47c6ace469f9c6ec421878ca13369a008b609889eb084f9a5b93ed82eebdad82487dbad04ed4f0dd4f7bd2efd7832103b29a4ce23810c53327c9c836d61b27e8bfc63a3b1652be6b71a9f112783b9654eb90e0c7b13d7616a64cbd7c50b9526a9ca629875d06903a21d8d0f37b902739ef4bc1a43e41331daa129cbddd6a5c260775b8a5dc73fc1cbb25a689aaa366b88af8d2f51c2c96871dfb0d6bfb1b2353dae8c0ba9783a7ea36421fa1870fe1f47a98781e296ebd65b723dbe9c6e48c6753d9b3f9502ff8f7ca490cac746a52131cbf4a86985a5c4dad03fb50e42c0d7e46a9 +Saved key: 010022000b3d8e02b755a6bf844c51eb62a9f6aad4d5363aa0f8f504ee31af32ed67e851f202e002de002073f34d6185fb2540c0e25c3c7c1701ba1d577d209b4363d9e86a0f23fd73bb0f001046c597eb180179b1b71973c95409847cfb983ec2bdbeb4c1b3f7f79577ff9e8647412270a3725ec0b3924095e15198754255d366892182e71a77f846b8316c29b83139f547e5c89a4460d222f0d7319e290e166857c09a7b44b0c4e6adac613a69f10d1748f462efe33d158e586ce691497f4e4361106b36201ef36d356f8ebde12de2bd0154f634e5d80ecf272dcf223fea9f21da273079e9eba0fa8061e6e6d4fa3049d45dff89c9deae626da092a6d05320507d5244d96fbb1c7deac0f0a1e8a98545149e6adf12560d3a26ccf19a10bef44e15a2a1460518a6560e0303df0e96f96707a83ab6dbe7cc828cd434de7c95d9bf3b2b355ddb374b477f00c8ba17ef8d4e1b57fc6365149941abaac2d456ea91d7cff8b63266871803bc70658a0dc7b968dabeeddce77fba5c6b2315852ade5011d142ae8bff5d09d9ce7cedaa561c323abb0c8ed0e2918bcbc13b1fdb34e174383630d017a95b0e48abef7b4e944b2fc6fc4e3c84d5d12bba68177afbef818f35a747369829100f6f55986308f3fb619db63291ed75bb435a1acf386ba8ff96f046e3ccc1a87194a2e0c37b7308e774016ed0d9101659e3fb238a9c7e8743b9376843ff549dd7a21933540b31dd7dc1b841400917027f513c27ca619a3ef3532b709bfad32931c6eb54e820e681f29ba669d908ed05ca508d489ced426a7776c7e4822b3322c607314453b5fa5d89b46ce9c653ecb7b50238928bc55983d22519faac2f5fda6afbbc9ebf318d12e8fd5515e3866c3b841212b432ffd1a7445a179c4618b8a962500132b14f52c5b39446fc789ddbe45a07417ea67218a020f509f51bcf0534009dd325daba8ee0ca91337120ba953d71a8dcc0cfe5896c8180175079d4f693ac4137b377e8a8dd86dab16248a446dc49fefaca9484a7122d2baf4e648e7387b2b542c41f3bf47e13e7260aadc3b0b9775406fcebb31e006a79a68f4151ebc767011801160001000b000600720000001000100800000000000100d094d5d76532d5fae8d7b8252007d97f831da2edae73131fa164c4e81b9f9b82618bd7c63d3a2b6b07e97e78b38df3556a98db73e7d1f2f4465061c90a17388abb6cc093d1bfbb216ba8f94be4a116db2f327fa4c3b0d4ce261970c700ce94d4eb0e8972a08c62680487caec35a24a14822f91d1aae80988ca3b4a18a8e15f2b133d7e1de477fec7788c8be1b2ea77a73b869a3ac7457bf4863fcad5a0e7eec80cddd60fe7869827141469ed68054b63babb3976cececf48782d3676b62dcbcf0258bc8a018d8950c0a76fb53a6597a9b1a8893020cbcfc7b0c5ff784e23fbeff66860a6bb862f2c78202d6929dfa7253b282758b6d53ecc30f650a85cc4949b Payload: "Hello world!" -Encrypted with TPM: b8f3d8181fdb13950f76af65f7641c944e346e6b9cb5729426fb5eb0786350e3a77148c051c80386ecd1e3f3a9b51df3f741877872609b65160a56188f2d40981fbd663f896b7939263b0156a2b023cab6c9bbb2dae1283b9e89ad66aab628d3333bc99d0e00227161a39e10ad2ff56a9ff6366e2f17bc720d48641944d62c1cf9ff36bef75661175cd92b0af1b11f0845d4053f0409110009911fe438ca4c6e71f56e2246ceb998d91718d979a7d46ee931345a2e530a327b51add1d52aa34b58a984a54cebfb66a98222e161b6967d29e94b0f680a650b9a05f34fc52430030a1bd9019d224173477c6962e889ee9ab3d33424919dc8fcf985f2b80ff59aea -Signature: 7b13438f77557a3bd5bb7d732e949c526bfd9d3ba0e8a3b782fe7d07559c429943bf0c2048d699f8ab9f7b45ecffa5d32cb5537d1aafe81b14f2f44ab8aa5b7aa94e3d61aed0c3fd19d32c66c0eb58112fe8453a111cca0bbdc3895eb21576b1e2b237ef7825eb9fab8c10fb2d124c1ce8f7ef8f32e8d12519c2da3cd34268336d26b0a15b0db28d8301e57b5a0db7ad25b857753b6d83edac5e38077d5fed3c30a5d09fd34c5ca9c02c7f867ac0209b84c1b815f4b85979f6357d4ff19540176a77aa52a62d8588a8254914ce09cab692573f91a50c73238dfe009d069ba3d595c1eb7c93297c4f0c3aa02b779d880b3065d4ffd93dfd4b870d915ac010b1f1 +Encrypted with TPM: bafc3dd90da5986058712675e4d23b8b3aa9e222c20251e0da405a3effca7ea14d76a87b7c5709399f937b4a475523b618910992425b8c17a00221e327664c9f177d47dbf16eb0c1d4d7c6285e326454157f3251c2f34aa49a4fb693938959d5df57cb7f8a5671104cc80ef50b9752ef9ac78faa64bc89e5e6728ed454078b778be2ab9b29f1513ed030e0364bf1506ba170b08238f7aa9e66bec34ba19e340048c5aaa93ebcdd4deab51c6117bd633b8f62a612cfa11f4291e2d300a7d8caa8e94ccc8a6382f98c4fafde5d1c58854adc05673ab8e85ee80f8db2025c3d00060b333c00c92f615791edd60ca419df86923db3acb420dcaaf867edca2cf0c356 +Signature: c8b19e81c0452f31ca3316b11c04b86b7aab00dc82d4ca5869d9aa2bd733f240b8b67a21e194a365acd1ab36cedf78c0d548e0f52758ab33ad82cea95e15d3a53d1722c7ca3da33451fc285645219f9fa0d6d2d95264233db18d016e31b20730ee194a0d4b2c955b7ed73c350bcebf20708592023547d8855cca279d7b0ac0e481823c37732712de2ce79fff4b6a6e99ad67cac795f4eb9628010846d34fd38d1a1134d964b7d85ddf95f7eda6475f8ad6a6a1695585b0f7bdc8273c949a69315fe0e942e4071f0dba55adab0a1d03dc9e279429da299f08db3e44f432625f474575cc0beceb344209a94991ee61d4b09fb02266d779b4b80c75d28a2c5e64d9 Verify signature: OK Decrypted with TPM: "Hello world!" -Encrypted with rsa.EncryptOAEP: 37057d43cccdf5b0155db7c14c2641cb0e4d58c021a31dcd098f548f3eddfb9217024031ee232cd5c7618996c15e99df69132ff2a7cc4974878ad369d0f26db72758a91f587799ca4826d3826927f4626e937a1566e861919175328a9b6b9d3bcd8ddc7a6e0222cd15dc49a3abf9b423c143fb1b27e9072e57967f5653d6e2d224d14a16812b75584f2e7313cf69cab4cfbf527f8e9480095eb65ca929573e0de8c4dc06e6ad0170694491c9d02bd0a16b299219be317063d2fdc6102e157d109b8369c39d5f1cbb64a42b6dd3627bba4c044869d396269afb6bca503e2a9f3ef65cf3ab5a4d9d463e3488963c370cf3a95094b26ab215db064a7dbfca95d220 +Encrypted with rsa.EncryptOAEP: 316a2510c0b39b777aec5fcc66531371b1f45c03d4fd64cb06a71f77fc4f4c09124183648b14da1c7c17ffd99e2dbe504bfabd810fd81ec0a4346f9bbe1d81c9b500447eb455e38df2fd21c25c597d2f46b7d676d837deab425979a83318a05247390b1ec418fe3dd93b062b9c85d4bcfdd11fcba0ef93ffd0fc6689e357d11d05905f40680c39bb007f3997b8b9bd94e86e6d193ee4426bb97df2e760c01c79d657a2f8bc358b237a1e18fba44da660a22fba4719d74d30bd39704b7c30e4a105e736e38515e4751fd288e007ecfb788b2da4b8eee27715463afb8e6813c0836ffa20f938bd64c6f7d673da14bd25d951ce0cefbfa50151903293fb868dcbc4 Decrypted with TPM: "Hello world!" ``` diff --git a/tpm.go b/tpm.go index 221fae7..ad8157f 100644 --- a/tpm.go +++ b/tpm.go @@ -30,6 +30,13 @@ // // 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. +// +// Keys are created under a storage root key (SRK) in the owner hierarchy, so +// clearing the TPM invalidates them. All commands that use keys are protected +// by HMAC sessions salted with the SRK: auth values are never sent to the TPM +// in cleartext, secret parameters are encrypted, and responses are +// authenticated. Serialized keys are bound to the SRK that they were created +// under. package tpm import ( From 3341f612e98d9a916f9978ec60e1097cd8fa64b9 Mon Sep 17 00:00:00 2001 From: Robin Thellend Date: Wed, 7 Oct 2026 11:37:36 -0700 Subject: [PATCH 12/12] Fix issues found in code review - Accept PSS salt lengths that match the hash (Auto, EqualsHash, or the hash size) for SHA-256/384/512, and verify the TPM's salt length. - Reuse salted HMAC sessions for key operations instead of starting a new one for every command. - Recover when the TPM loses the SRK, sessions, or loaded key: flush the cached state and retry once. - Also flush leftover sessions when opening /dev/tpm0. - Make ECC keys signing-only. - Return ErrInvalidCurve instead of panicking with WithECC(nil). - Don't alias the AES-GCM output with the additional data. - Don't reload a key that is already loaded in UnmarshalKey. - Drop the meaningless SRK name check and document that the SRK is trusted on first use. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XBPwKpCGCUikziy99vn1jz --- README.md | 6 + tpm.go | 473 +++++++++++++++++++++++++++++++++++----------------- tpm_test.go | 205 ++++++++++++++++++++++- 3 files changed, 528 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index 72d173a..3543a1e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ in cleartext, secret parameters are encrypted, and responses are authenticated. Serialized keys are bound to the SRK that they were created under. +The SRK itself is trusted on first use: this package doesn't verify that it +belongs to a genuine TPM (e.g. with the endorsement key's certificate). A +device that impersonates the TPM when a key is created can capture that key's +auth value and parameters. Once a key exists, a different device can't use it, +or complete the sessions that protect it. + ## Example: ```go diff --git a/tpm.go b/tpm.go index ad8157f..459f8d2 100644 --- a/tpm.go +++ b/tpm.go @@ -37,6 +37,12 @@ // in cleartext, secret parameters are encrypted, and responses are // authenticated. Serialized keys are bound to the SRK that they were created // under. +// +// The SRK itself is trusted on first use: this package doesn't verify that it +// belongs to a genuine TPM (e.g. with the endorsement key's certificate). A +// device that impersonates the TPM when a key is created can capture that +// key's auth value and parameters. Once a key exists, a different device can't +// use it, or complete the sessions that protect it. package tpm import ( @@ -142,10 +148,11 @@ func New(opts ...Option) (*TPM, error) { return nil, err } // Without the resource manager, transient objects - // left behind by a previous process stay loaded. - // /dev/tpm0 can only be opened by one process at a - // time, so they can't belong to anyone else. - if err := flushTransientHandles(t); err != nil { + // and sessions left behind by a previous process + // stay loaded. /dev/tpm0 can only be opened by one + // process at a time, so they can't belong to anyone + // else. + if err := flushLeftoverHandles(t); err != nil { t.Close() return nil, err } @@ -158,21 +165,27 @@ func New(opts ...Option) (*TPM, error) { return &tpm, nil } -func flushTransientHandles(t transport.TPM) error { - capResp, err := tpm2.GetCapability{ - Capability: tpm2.TPMCapHandles, - Property: uint32(tpm2.TPMHTTransient) << 24, - PropertyCount: 100, - }.Execute(t) - if err != nil { - return fmt.Errorf("TPM2_GetCapability: %w", err) - } - handles, err := capResp.CapabilityData.Data.Handles() - if err != nil { - return fmt.Errorf("TPM2_GetCapability(Handles): %w", err) - } - for _, h := range handles.Handle { - tpm2.FlushContext{FlushHandle: h}.Execute(t) +func flushLeftoverHandles(t transport.TPM) error { + for _, ht := range []tpm2.TPMHT{ + tpm2.TPMHTTransient, + tpm2.TPMHTHMACSession, // loaded sessions + tpm2.TPMHTPolicySession, // saved sessions + } { + capResp, err := tpm2.GetCapability{ + Capability: tpm2.TPMCapHandles, + Property: uint32(ht) << 24, + PropertyCount: 100, + }.Execute(t) + if err != nil { + return fmt.Errorf("TPM2_GetCapability: %w", err) + } + handles, err := capResp.CapabilityData.Data.Handles() + if err != nil { + return fmt.Errorf("TPM2_GetCapability(Handles): %w", err) + } + for _, h := range handles.Handle { + tpm2.FlushContext{FlushHandle: h}.Execute(t) + } } return nil } @@ -191,6 +204,15 @@ type TPM struct { loadedHandle tpm2.TPMHandle srk tpm2.NamedHandle srkPublic tpm2.TPMTPublic + // Reusable sessions that authorize the use of keys, with parameter + // encryption in both directions, or only for the command. + inOutSession session + inSession session +} + +type session struct { + s tpm2.Session + close func() error } type keyOptions struct { @@ -229,13 +251,13 @@ func WithECC(curve elliptic.Curve) KeyOption { } } -// WithSigningOnly restricts a new RSA or ECC key to signing. The key can't be -// used to decrypt. +// WithSigningOnly restricts a new RSA key to signing. The key can't be used to +// decrypt. ECC keys are always signing-only. // -// By default, RSA and ECC keys can be used for both signing and decryption. -// The TPM requires such dual-use keys to have no fixed scheme, so they can -// also be used with other schemes than the ones that this package uses (e.g. -// raw RSA decryption), by anyone who can use the key directly with the TPM. +// By default, RSA keys can be used for both signing and decryption. The TPM +// requires such dual-use keys to have no fixed scheme, so they can also be +// used with other schemes than the ones that this package uses (e.g. raw RSA +// decryption), by anyone who can use the key directly with the TPM. func WithSigningOnly() KeyOption { return func(opts *keyOptions) { opts.usage = usageSign @@ -276,11 +298,16 @@ func WithHMAC(bits int) KeyOption { func (t *TPM) CreateKey(opts ...KeyOption) (*Key, error) { t.mu.Lock() defer t.mu.Unlock() - b, err := t.createLocked(opts...) - if err != nil { - return nil, err - } - return t.unmarshalLocked(b) + var key *Key + err := t.runLocked(func() error { + b, err := t.createLocked(opts...) + if err != nil { + return err + } + key, err = t.unmarshalLocked(b) + return err + }) + return key, err } func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { @@ -291,6 +318,10 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { for _, o := range opts { o(&opt) } + if opt.keyType == TypeECC && opt.usage == usageAny { + // This package doesn't implement decryption with ECC keys. + opt.usage = usageSign + } switch { case opt.usage == usageAny: case opt.keyType == TypeRSA: @@ -355,11 +386,6 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { } case TypeECC: - uniqueSize := opt.curve.Params().BitSize / 8 - unique := make([]byte, 2*uniqueSize) - if _, err := io.ReadFull(rand.Reader, unique); err != nil { - return nil, fmt.Errorf("rand: %w", err) - } var curve tpm2.TPMECCCurve var hash tpm2.TPMAlgID switch opt.curve { @@ -378,6 +404,11 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { default: return nil, ErrInvalidCurve } + uniqueSize := opt.curve.Params().BitSize / 8 + unique := make([]byte, 2*uniqueSize) + if _, err := io.ReadFull(rand.Reader, unique); err != nil { + return nil, fmt.Errorf("rand: %w", err) + } public = tpm2.TPMTPublic{ Type: tpm2.TPMAlgECC, NameAlg: hash, @@ -391,7 +422,7 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { NoDA: false, EncryptedDuplication: false, Restricted: false, - Decrypt: opt.usage != usageSign, + Decrypt: false, SignEncrypt: true, }, Parameters: tpm2.NewTPMUPublicParms( @@ -519,7 +550,7 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { InPublic: tpm2.New2B(public), }.Execute(t.tpm) if err != nil { - return nil, fmt.Errorf("TPM2_Create: %w", err) + return nil, tpmError("TPM2_Create", err) } priv := tpm2.Marshal(createResp.OutPrivate) @@ -546,20 +577,77 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { // // The SRK name pins the key to the storage root key that it was created // under. When the key is loaded, the TPM's SRK must have the same name, and -// all commands are protected by sessions salted with that SRK. A different -// device pretending to be the TPM cannot complete these sessions. +// the commands that use the key are protected by sessions salted with that +// SRK. A different device pretending to be the TPM cannot complete these +// sessions. const keyFormatVersion = 1 // Close closes the connections to the TPM. func (t *TPM) Close() error { t.mu.Lock() defer t.mu.Unlock() + t.resetLocked() + return t.tpm.Close() +} + +// resetLocked flushes the loaded key, the sessions, and the SRK. They are +// recreated when needed. +func (t *TPM) resetLocked() { t.flushLocked() + for _, s := range []*session{&t.inOutSession, &t.inSession} { + if s.s != nil { + s.close() + *s = session{} + } + } if t.srk.Handle != 0 { tpm2.FlushContext{FlushHandle: t.srk.Handle}.Execute(t.tpm) t.srk = tpm2.NamedHandle{} } - return t.tpm.Close() +} + +// runLocked runs f, which sends commands to the TPM. The SRK, the sessions, +// and the loaded key are kept across calls. If the TPM no longer has them, +// e.g. after a TPM restart, or because another user of a shared TPM flushed +// them, they are recreated and f is retried once. +func (t *TPM) runLocked(f func() error) error { + err := f() + var cmdErr *commandError + if !errors.As(err, &cmdErr) { + return err + } + var rc tpm2.TPMRC + if !errors.As(cmdErr.err, &rc) { + // The command or the response was lost, or the response + // didn't validate. The sessions may be out of sync with the + // TPM. + t.resetLocked() + return err + } + if !errors.Is(rc, tpm2.TPMRCHandle) && + (rc < tpm2.TPMRCReferenceH0 || rc > tpm2.TPMRCReferenceS6) { + return err + } + t.resetLocked() + return f() +} + +// commandError is an error returned by a TPM command. +type commandError struct { + cmd string + err error +} + +func tpmError(cmd string, err error) error { + return &commandError{cmd: cmd, err: err} +} + +func (e *commandError) Error() string { + return e.cmd + ": " + e.err.Error() +} + +func (e *commandError) Unwrap() error { + return e.err } func (t *TPM) flushLocked() { @@ -574,7 +662,13 @@ func (t *TPM) flushLocked() { func (t *TPM) UnmarshalKey(b []byte) (*Key, error) { t.mu.Lock() defer t.mu.Unlock() - return t.unmarshalLocked(b) + var key *Key + err := t.runLocked(func() error { + var err error + key, err = t.unmarshalLocked(b) + return err + }) + return key, err } func (t *TPM) unmarshalLocked(in []byte) (*Key, error) { @@ -627,9 +721,6 @@ func (t *TPM) unmarshalLocked(in []byte) (*Key, error) { if err := out.parsePublic(pubContents); err != nil { return nil, err } - if t.loadedKey == out.id { - t.flushLocked() - } if err := out.loadLocked(); err != nil { return nil, err } @@ -671,13 +762,18 @@ func (k *Key) loadLocked() error { ParentHandle: tpm2.AuthHandle{ Handle: srk.Handle, Name: srk.Name, - Auth: k.t.sessionLocked(nil), + // The SRK's auth value is empty, its private area is + // encrypted by the SRK, and the name of the loaded + // object is verified below. The commands that use the + // object are authorized with sessions salted with the + // SRK, which also cover the object's name. + Auth: tpm2.PasswordAuth(nil), }, InPrivate: k.priv, InPublic: k.pub, }.Execute(k.t.tpm) if err != nil { - return fmt.Errorf("TPM2_Load: %w", err) + return tpmError("TPM2_Load", err) } if !bytes.Equal(loadResp.Name.Buffer, k.name.Buffer) { tpm2.FlushContext{FlushHandle: loadResp.ObjectHandle}.Execute(k.t.tpm) @@ -810,15 +906,28 @@ func (k *Key) Curve() elliptic.Curve { // 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) + return k.run(func() ([]byte, error) { + return k.hmacLocked(message) + }) +} + +// run loads the key and runs f, which uses it. +func (k *Key) run(f func() ([]byte, error)) ([]byte, error) { + k.t.mu.Lock() + defer k.t.mu.Unlock() + var out []byte + err := k.t.runLocked(func() error { + if err := k.loadLocked(); err != nil { + return err + } + var err error + out, err = f() + return err + }) + return out, err } func (k *Key) hmacLocked(message []byte) ([]byte, error) { @@ -834,11 +943,15 @@ func (k *Key) hmacLocked(message []byte) ([]byte, error) { return nil, ErrWrongKeyType } + sess, err := k.t.keySessionLocked(true) + if err != nil { + return nil, err + } resp, err := tpm2.Hmac{ Handle: tpm2.AuthHandle{ Handle: k.t.loadedHandle, Name: k.name, - Auth: k.t.sessionLocked(k.t.objectAuth, tpm2.AESEncryption(128, tpm2.EncryptInOut)), + Auth: sess, }, Buffer: tpm2.TPM2BMaxBuffer{ Buffer: message, @@ -846,7 +959,7 @@ func (k *Key) hmacLocked(message []byte) ([]byte, error) { HashAlg: hashAlg, }.Execute(k.t.tpm) if err != nil { - return nil, fmt.Errorf("TPM2_HMAC: %w", err) + return nil, tpmError("TPM2_HMAC", err) } return resp.OutHMAC.Buffer, nil } @@ -857,12 +970,6 @@ func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signatur if !k.canSign { return nil, ErrKeyUsage } - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil, err - } - h := opts.HashFunc() if h == crypto.SHA1 { return nil, fmt.Errorf("unexpected hash %v", h) @@ -876,70 +983,94 @@ func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signatur } hashAlg := tpm2.TPMSSchemeHash{HashAlg: tpmHash} + var scheme tpm2.TPMTSigScheme switch k.keyType { case TypeRSA: - scheme := tpm2.TPMTSigScheme{ + scheme = tpm2.TPMTSigScheme{ Scheme: tpm2.TPMAlgRSASSA, Details: tpm2.NewTPMUSigScheme(tpm2.TPMAlgRSASSA, &hashAlg), } if pss, ok := opts.(*rsa.PSSOptions); ok { - if pss.SaltLength != 32 { - return nil, fmt.Errorf("unexpected pss salt length %d, want 32", pss.SaltLength) + // The TPM's salt is as long as the digest. + switch pss.SaltLength { + case rsa.PSSSaltLengthAuto, rsa.PSSSaltLengthEqualsHash, h.Size(): + default: + return nil, fmt.Errorf("unexpected pss salt length %d, want %d", pss.SaltLength, h.Size()) + } + scheme = tpm2.TPMTSigScheme{ + Scheme: tpm2.TPMAlgRSAPSS, + Details: tpm2.NewTPMUSigScheme(tpm2.TPMAlgRSAPSS, &hashAlg), } - scheme.Scheme = tpm2.TPMAlgRSAPSS - scheme.Details = tpm2.NewTPMUSigScheme(tpm2.TPMAlgRSAPSS, &hashAlg) } - resp, err := tpm2.Sign{ - KeyHandle: tpm2.AuthHandle{ - Handle: k.t.loadedHandle, - Name: k.name, - Auth: k.t.sessionLocked(k.t.objectAuth, tpm2.AESEncryption(128, tpm2.EncryptIn)), - }, - Digest: tpm2.TPM2BDigest{ - Buffer: digest, - }, - InScheme: scheme, - Validation: tpm2.TPMTTKHashCheck{ - Tag: tpm2.TPMSTHashCheck, - }, - }.Execute(k.t.tpm) - if err != nil { - return nil, fmt.Errorf("TPM2_Sign: %w", err) + + case TypeECC: + scheme = tpm2.TPMTSigScheme{ + Scheme: tpm2.TPMAlgECDSA, + Details: tpm2.NewTPMUSigScheme(tpm2.TPMAlgECDSA, &hashAlg), } - if resp.Signature.SigAlg == tpm2.TPMAlgRSASSA { - sig, err := resp.Signature.Signature.RSASSA() - if err != nil { - return nil, err - } - return sig.Sig.Buffer, nil + + case TypeHMAC: + return k.run(func() ([]byte, error) { + return k.hmacLocked(digest) + }) + + default: + return nil, ErrWrongKeyType + } + + sig, err := k.run(func() ([]byte, error) { + return k.signLocked(digest, scheme) + }) + if err != nil { + return nil, err + } + if scheme.Scheme == tpm2.TPMAlgRSAPSS { + // Older TPMs use the longest possible salt. + if err := rsa.VerifyPSS(k.publicKey.(*rsa.PublicKey), h, digest, sig, &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}); err != nil { + return nil, fmt.Errorf("TPM2_Sign: unexpected pss salt length: %w", err) } - sig, err := resp.Signature.Signature.RSAPSS() + } + return sig, nil +} + +func (k *Key) signLocked(digest []byte, scheme tpm2.TPMTSigScheme) ([]byte, error) { + sess, err := k.t.keySessionLocked(false) + if err != nil { + return nil, err + } + resp, err := tpm2.Sign{ + KeyHandle: tpm2.AuthHandle{ + Handle: k.t.loadedHandle, + Name: k.name, + Auth: sess, + }, + Digest: tpm2.TPM2BDigest{ + Buffer: digest, + }, + InScheme: scheme, + Validation: tpm2.TPMTTKHashCheck{ + Tag: tpm2.TPMSTHashCheck, + }, + }.Execute(k.t.tpm) + if err != nil { + return nil, tpmError("TPM2_Sign", err) + } + switch resp.Signature.SigAlg { + case tpm2.TPMAlgRSASSA: + sig, err := resp.Signature.Signature.RSASSA() if err != nil { return nil, err } return sig.Sig.Buffer, nil - case TypeECC: - resp, err := tpm2.Sign{ - KeyHandle: tpm2.AuthHandle{ - Handle: k.t.loadedHandle, - Name: k.name, - Auth: k.t.sessionLocked(k.t.objectAuth, tpm2.AESEncryption(128, tpm2.EncryptIn)), - }, - Digest: tpm2.TPM2BDigest{ - Buffer: digest, - }, - InScheme: tpm2.TPMTSigScheme{ - Scheme: tpm2.TPMAlgECDSA, - Details: tpm2.NewTPMUSigScheme(tpm2.TPMAlgECDSA, &hashAlg), - }, - Validation: tpm2.TPMTTKHashCheck{ - Tag: tpm2.TPMSTHashCheck, - }, - }.Execute(k.t.tpm) + case tpm2.TPMAlgRSAPSS: + sig, err := resp.Signature.Signature.RSAPSS() if err != nil { - return nil, fmt.Errorf("TPM2_Sign: %w", err) + return nil, err } + return sig.Sig.Buffer, nil + + case tpm2.TPMAlgECDSA: sig, err := resp.Signature.Signature.ECDSA() if err != nil { return nil, err @@ -951,11 +1082,8 @@ 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 + return nil, fmt.Errorf("TPM2_Sign: unexpected signature algorithm %v", resp.Signature.SigAlg) } } @@ -998,12 +1126,9 @@ func (k *Key) Encrypt(cleartext []byte) (ciphertext []byte, err error) { return rsa.EncryptOAEP(sha256.New(), rand.Reader, k.publicKey.(*rsa.PublicKey), cleartext, nil) case TypeAES: - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil, err - } - return k.aesEncryptLocked(cleartext) + return k.run(func() ([]byte, error) { + return k.aesEncryptLocked(cleartext) + }) default: return nil, ErrWrongKeyType @@ -1019,47 +1144,54 @@ func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, opts crypto.DecrypterOpts) if k.keyType == TypeRSA && !k.canDecrypt { return nil, ErrKeyUsage } - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil, err - } switch k.keyType { case TypeRSA: hashAlg, label, err := oaepParams(opts) if err != nil { return nil, err } - resp, err := tpm2.RSADecrypt{ - KeyHandle: tpm2.AuthHandle{ - Handle: k.t.loadedHandle, - Name: k.name, - Auth: k.t.sessionLocked(k.t.objectAuth, tpm2.AESEncryption(128, tpm2.EncryptInOut)), - }, - CipherText: tpm2.TPM2BPublicKeyRSA{ - Buffer: ciphertext, - }, - InScheme: tpm2.TPMTRSADecrypt{ - Scheme: tpm2.TPMAlgOAEP, - Details: tpm2.NewTPMUAsymScheme(tpm2.TPMAlgOAEP, &tpm2.TPMSEncSchemeOAEP{HashAlg: hashAlg}), - }, - Label: tpm2.TPM2BData{ - Buffer: label, - }, - }.Execute(k.t.tpm) - if err != nil { - return nil, fmt.Errorf("TPM2_RSADecrypt: %w", err) - } - return resp.Message.Buffer, nil + return k.run(func() ([]byte, error) { + return k.rsaDecryptLocked(ciphertext, hashAlg, label) + }) case TypeAES: - return k.aesDecryptLocked(ciphertext) + return k.run(func() ([]byte, error) { + return k.aesDecryptLocked(ciphertext) + }) default: return nil, ErrWrongKeyType } } +func (k *Key) rsaDecryptLocked(ciphertext []byte, hashAlg tpm2.TPMIAlgHash, label []byte) ([]byte, error) { + sess, err := k.t.keySessionLocked(true) + if err != nil { + return nil, err + } + resp, err := tpm2.RSADecrypt{ + KeyHandle: tpm2.AuthHandle{ + Handle: k.t.loadedHandle, + Name: k.name, + Auth: sess, + }, + CipherText: tpm2.TPM2BPublicKeyRSA{ + Buffer: ciphertext, + }, + InScheme: tpm2.TPMTRSADecrypt{ + Scheme: tpm2.TPMAlgOAEP, + Details: tpm2.NewTPMUAsymScheme(tpm2.TPMAlgOAEP, &tpm2.TPMSEncSchemeOAEP{HashAlg: hashAlg}), + }, + Label: tpm2.TPM2BData{ + Buffer: label, + }, + }.Execute(k.t.tpm) + if err != nil { + return nil, tpmError("TPM2_RSADecrypt", err) + } + return resp.Message.Buffer, nil +} + // oaepParams returns the TPM hash algorithm and label to use for RSA-OAEP // decryption with the given options. func oaepParams(opts crypto.DecrypterOpts) (tpm2.TPMIAlgHash, []byte, error) { @@ -1138,7 +1270,9 @@ func (k *Key) aesEncryptLocked(cleartext []byte) ([]byte, error) { return nil, err } nonce := make([]byte, aead.NonceSize()) - return aead.Seal(header, nonce, cleartext, header), nil + out := make([]byte, aesHeaderSize, aesHeaderSize+len(cleartext)+aead.Overhead()) + copy(out, header) + return aead.Seal(out, nonce, cleartext, header), nil } func (k *Key) aesDecryptLocked(ciphertext []byte) ([]byte, error) { @@ -1176,11 +1310,15 @@ func newDataKeyAEAD(dataKey []byte) (cipher.AEAD, error) { } func (k *Key) tpmAESLocked(in, iv []byte, decrypt bool) ([]byte, error) { + sess, err := k.t.keySessionLocked(true) + if err != nil { + return nil, err + } resp, err := tpm2.EncryptDecrypt2{ KeyHandle: tpm2.AuthHandle{ Handle: k.t.loadedHandle, Name: k.name, - Auth: k.t.sessionLocked(k.t.objectAuth, tpm2.AESEncryption(128, tpm2.EncryptInOut)), + Auth: sess, }, Message: tpm2.TPM2BMaxBuffer{ Buffer: in, @@ -1191,7 +1329,7 @@ func (k *Key) tpmAESLocked(in, iv []byte, decrypt bool) ([]byte, error) { }, }.Execute(k.t.tpm) if err != nil { - return nil, fmt.Errorf("TPM2_EncryptDecrypt2: %w", err) + return nil, tpmError("TPM2_EncryptDecrypt2", err) } return resp.OutData.Buffer, nil } @@ -1213,7 +1351,7 @@ func (t *TPM) srkLocked() (tpm2.NamedHandle, error) { InPublic: tpm2.New2B(tpm2.RSASRKTemplate), }.Execute(t.tpm) if err != nil { - return tpm2.NamedHandle{}, fmt.Errorf("TPM2_CreatePrimary: %w", err) + return tpm2.NamedHandle{}, tpmError("TPM2_CreatePrimary", err) } flush := func() { tpm2.FlushContext{FlushHandle: createPrimaryResp.ObjectHandle}.Execute(t.tpm) @@ -1223,15 +1361,14 @@ func (t *TPM) srkLocked() (tpm2.NamedHandle, error) { flush() return tpm2.NamedHandle{}, fmt.Errorf("TPM2_CreatePrimary: %w", err) } + // The name is computed from the public area, instead of using the + // name from the response. Nothing authenticates the response, so the + // SRK is trusted on first use. See the package documentation. name, err := tpm2.ObjectName(pub) if err != nil { flush() return tpm2.NamedHandle{}, fmt.Errorf("TPM2_CreatePrimary: %w", err) } - if !bytes.Equal(name.Buffer, createPrimaryResp.Name.Buffer) { - flush() - return tpm2.NamedHandle{}, errors.New("TPM2_CreatePrimary: unexpected object name") - } t.srk = tpm2.NamedHandle{ Handle: createPrimaryResp.ObjectHandle, Name: *name, @@ -1245,6 +1382,9 @@ func (t *TPM) srkLocked() (tpm2.NamedHandle, error) { // never sent to the TPM, the TPM's response is authenticated, and opts can // enable encryption of the first command and/or response parameter. The SRK // must already be loaded. +// +// Starting a salted session is expensive. Keys are used with the reusable +// sessions from keySessionLocked instead. func (t *TPM) sessionLocked(auth []byte, opts ...tpm2.AuthOption) tpm2.Session { opts = append([]tpm2.AuthOption{ tpm2.Auth(auth), @@ -1252,3 +1392,32 @@ func (t *TPM) sessionLocked(auth []byte, opts ...tpm2.AuthOption) tpm2.Session { }, opts...) return tpm2.HMAC(tpm2.TPMAlgSHA256, 16, opts...) } + +// keySessionLocked returns a reusable HMAC session, salted with the SRK, to +// authorize the use of a key, like sessionLocked. The command parameter is +// encrypted, and so is the response parameter if encryptResponse is true. +// The TPM rejects sessions that encrypt the response of commands that can't +// encrypt it, like TPM2_Sign. +func (t *TPM) keySessionLocked(encryptResponse bool) (tpm2.Session, error) { + p, dir := &t.inSession, tpm2.EncryptIn + if encryptResponse { + p, dir = &t.inOutSession, tpm2.EncryptInOut + } + if p.s != nil { + return p.s, nil + } + srk, err := t.srkLocked() + if err != nil { + return nil, err + } + s, closer, err := tpm2.HMACSession(t.tpm, tpm2.TPMAlgSHA256, 16, + tpm2.Auth(t.objectAuth), + tpm2.Salted(srk.Handle, t.srkPublic), + tpm2.AESEncryption(128, dir), + ) + if err != nil { + return nil, tpmError("TPM2_StartAuthSession", err) + } + *p = session{s: s, close: closer} + return s, nil +} diff --git a/tpm_test.go b/tpm_test.go index d62474f..eb14b7d 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -32,6 +32,8 @@ import ( "crypto/rsa" "crypto/sha1" "crypto/sha256" + _ "crypto/sha512" + "encoding/binary" "errors" "fmt" "io" @@ -139,11 +141,11 @@ func TestRSA(t *testing.T) { t.Fatalf("VerifyPSS: %v", err) } - tpm.objectAuth = []byte("wrong") + setObjectAuth(tpm, []byte("wrong")) if _, err := key.Decrypt(nil, enc, nil); err == nil { t.Fatal("tpm.Decrypt should have failed") } - tpm.objectAuth = []byte(keyPassphrase) + setObjectAuth(tpm, []byte(keyPassphrase)) } } @@ -264,11 +266,11 @@ func TestAES(t *testing.T) { t.Fatal("tpm.Decrypt should have failed with truncated ciphertext") } - tpm.objectAuth = []byte("wrong") + setObjectAuth(tpm, []byte("wrong")) if _, err := key.Decrypt(nil, enc, nil); err == nil { t.Fatal("tpm.Decrypt should have failed") } - tpm.objectAuth = []byte(keyPassphrase) + setObjectAuth(tpm, []byte(keyPassphrase)) } } @@ -777,6 +779,22 @@ func TestKeyUsage(t *testing.T) { } }) + t.Run("ECC default", func(t *testing.T) { + key, err := tpm.CreateKey(WithECC(elliptic.P256())) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + if !key.canSign || key.canDecrypt { + t.Fatalf("canSign = %v, canDecrypt = %v, want signing only", key.canSign, key.canDecrypt) + } + }) + + t.Run("Invalid curve", func(t *testing.T) { + if _, err := tpm.CreateKey(WithECC(nil)); !errors.Is(err, ErrInvalidCurve) { + t.Fatalf("tpm.CreateKey: got %v, want %v", err, ErrInvalidCurve) + } + }) + t.Run("Unsupported", func(t *testing.T) { for _, opts := range [][]KeyOption{ {WithECC(elliptic.P256()), WithDecryptionOnly()}, @@ -814,3 +832,182 @@ func TestNewKeepsOtherObjects(t *testing.T) { t.Fatalf("Object created before New is gone: %v", err) } } + +// setObjectAuth changes the auth value that tpm uses with keys, as if it had +// been created with a different WithObjectAuth. +func setObjectAuth(tpm *TPM, auth []byte) { + tpm.mu.Lock() + defer tpm.mu.Unlock() + tpm.objectAuth = auth + tpm.resetLocked() +} + +func TestRSAPSS(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + key, err := tpm.CreateKey(WithRSA(2048)) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + pub := key.Public().(*rsa.PublicKey) + + for _, h := range []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512} { + hh := h.New() + hh.Write([]byte("Hello World!")) + digest := hh.Sum(nil) + + for _, saltLength := range []int{rsa.PSSSaltLengthAuto, rsa.PSSSaltLengthEqualsHash, h.Size()} { + sig, err := key.Sign(nil, digest, &rsa.PSSOptions{SaltLength: saltLength, Hash: h}) + if err != nil { + t.Fatalf("Sign(%v, %d): %v", h, saltLength, err) + } + if err := rsa.VerifyPSS(pub, h, digest, sig, &rsa.PSSOptions{SaltLength: h.Size()}); err != nil { + t.Fatalf("VerifyPSS(%v, %d): %v", h, saltLength, err) + } + } + if _, err := key.Sign(nil, digest, &rsa.PSSOptions{SaltLength: h.Size() + 1, Hash: h}); err == nil { + t.Fatalf("Sign(%v) with unsupported salt length should have failed", h) + } + } +} + +// cmdCounter counts the commands sent to the TPM. +type cmdCounter struct { + io.ReadWriteCloser + counts map[tpm2.TPMCC]int +} + +func (c *cmdCounter) Write(b []byte) (int, error) { + if len(b) >= 10 { + c.counts[tpm2.TPMCC(binary.BigEndian.Uint32(b[6:10]))]++ + } + return c.ReadWriteCloser.Write(b) +} + +func TestSessionReuse(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + counter := &cmdCounter{ReadWriteCloser: rwc, counts: make(map[tpm2.TPMCC]int)} + tpm, err := New(WithTPM(counter), WithObjectAuth([]byte("passphrase"))) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + key1, err := tpm.CreateKey(WithRSA(2048)) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + key2, err := tpm.CreateKey(WithAES(128)) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + hashed := sha256.Sum256([]byte("Hello World!")) + use := func() { + if _, err := key1.Sign(nil, hashed[:], crypto.SHA256); err != nil { + t.Fatalf("key1.Sign: %v", err) + } + enc, err := key1.Encrypt([]byte("Hello World!")) + if err != nil { + t.Fatalf("key1.Encrypt: %v", err) + } + if _, err := key1.Decrypt(nil, enc, nil); err != nil { + t.Fatalf("key1.Decrypt: %v", err) + } + if enc, err = key2.Encrypt([]byte("Hello World!")); err != nil { + t.Fatalf("key2.Encrypt: %v", err) + } + if _, err := key2.Decrypt(nil, enc, nil); err != nil { + t.Fatalf("key2.Decrypt: %v", err) + } + } + use() + clear(counter.counts) + for range 5 { + use() + } + if n := counter.counts[tpm2.TPMCCStartAuthSession]; n != 0 { + t.Errorf("TPM2_StartAuthSession called %d times, want 0", n) + } + if n := counter.counts[tpm2.TPMCCCreatePrimary]; n != 0 { + t.Errorf("TPM2_CreatePrimary called %d times, want 0", n) + } +} + +func TestRecoverFlushedHandles(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + key, err := tpm.CreateKey(WithRSA(2048)) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + hashed := sha256.Sum256([]byte("Hello World!")) + enc, err := key.Encrypt([]byte("Hello World!")) + if err != nil { + t.Fatalf("key.Encrypt: %v", err) + } + if _, err := key.Sign(nil, hashed[:], crypto.SHA256); err != nil { + t.Fatalf("key.Sign: %v", err) + } + if _, err := key.Decrypt(nil, enc, nil); err != nil { + t.Fatalf("key.Decrypt: %v", err) + } + + // Everything that the TPM object loaded disappears, e.g. because + // another user of the TPM flushed it. + raw := transport.FromReadWriteCloser(rwc) + if err := flushLeftoverHandles(raw); err != nil { + t.Fatalf("flushLeftoverHandles: %v", err) + } + for _, ht := range []tpm2.TPMHT{tpm2.TPMHTTransient, tpm2.TPMHTHMACSession, tpm2.TPMHTPolicySession} { + resp, err := tpm2.GetCapability{ + Capability: tpm2.TPMCapHandles, + Property: uint32(ht) << 24, + PropertyCount: 100, + }.Execute(raw) + if err != nil { + t.Fatalf("TPM2_GetCapability: %v", err) + } + handles, err := resp.CapabilityData.Data.Handles() + if err != nil { + t.Fatalf("TPM2_GetCapability(Handles): %v", err) + } + if len(handles.Handle) != 0 { + t.Fatalf("Handles of type %v not flushed: %v", ht, handles.Handle) + } + } + + if _, err := key.Sign(nil, hashed[:], crypto.SHA256); err != nil { + t.Fatalf("key.Sign after flush: %v", err) + } + if err := flushLeftoverHandles(raw); err != nil { + t.Fatalf("flushLeftoverHandles: %v", err) + } + if _, err := key.Decrypt(nil, enc, nil); err != nil { + t.Fatalf("key.Decrypt after flush: %v", err) + } + if err := flushLeftoverHandles(raw); err != nil { + t.Fatalf("flushLeftoverHandles: %v", err) + } + if _, err := tpm.CreateKey(WithECC(elliptic.P256())); err != nil { + t.Fatalf("tpm.CreateKey after flush: %v", err) + } +}