Skip to content

Security hardening: authenticated AES, salted TPM sessions, owner-hierarchy SRK - #4

Merged
rthellend merged 12 commits into
mainfrom
security-fixes
Oct 7, 2026
Merged

rthellend merged 12 commits into
mainfrom
security-fixes

Conversation

@rthellend

@rthellend rthellend commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Security hardening: authenticated AES, salted TPM sessions, owner-hierarchy SRK

This PR fixes security issues found during an internal review, along with follow-up fixes from code review. It contains breaking changes (see below).

Changes

  1. Authenticated AES encryption. AES keys no longer encrypt data directly with unauthenticated AES-CFB. Each message gets a random data key, wrapped by the TPM key, and the message is sealed in software with AES-GCM. The header is authenticated as additional data. This removes ciphertext malleability and the TPM's ~1KB message size limit.

  2. DecrypterOpts enforcement. RSA Decrypt passes the OAEP hash and label to the TPM instead of ignoring them. Options other than OAEP, such as PKCS#1 v1.5, are rejected. The MGF hash must match the label hash, and the TPM requires non-empty labels to end with a null byte.

  3. Salted HMAC sessions. Commands previously used plain password sessions. Commands that use keys now run in HMAC sessions salted with the SRK: auth values never cross the bus in cleartext, secret parameters are encrypted, and responses are authenticated. Key operations reuse two long-lived sessions, one encrypting command and response and one encrypting only the command (for TPM2_Sign), instead of starting a salted session for every command. TPM2_Load uses the SRK's empty auth value. The private area is already encrypted by the SRK, the loaded object's name is checked, and every later command on the key is covered by an SRK-salted session.

  4. Owner hierarchy. Keys are created under an SRK in the owner (storage) hierarchy instead of the endorsement hierarchy, so TPM2_Clear invalidates them. WithOwnerAuth sets the owner passphrase. Serialized keys record the name of the SRK they were created under, and loading them on a TPM with a different SRK fails with ErrWrongTPM.

  5. Import rejection. Keys must have FixedTPM, FixedParent, SensitiveDataOrigin and UserWithAuth set. Keys imported or created with known sensitive data are rejected with ErrInvalidKey.

  6. Single-purpose keys. New WithSigningOnly() and WithDecryptionOnly() options restrict RSA keys to one use, enforced by both the library (ErrKeyUsage) and the TPM. Decryption-only keys are fixed to RSA-OAEP with SHA-256. ECC keys are always signing-only.

  7. Signing. Sign rejects SHA-1 and digests whose length doesn't match the hash. RSA-PSS supports SHA-256, SHA-384 and SHA-512 with PSSSaltLengthEqualsHash, PSSSaltLengthAuto or a salt as long as the hash, which crypto/tls needs. The signature is checked after signing so that a TPM using a different salt length can't produce signatures that fail to verify.

  8. Robustness.

    • The SRK and sessions stay loaded for the life of the TPM. If the TPM loses them (for example after a TPM restart, or because another user of a shared TPM flushed handles), they're recreated and the operation is retried once. If a command or response is lost, or a response fails its HMAC check, the sessions are reset so they can't drift out of step with the TPM.
    • On /dev/tpm0 (no resource manager), objects and sessions left behind by a previous process are flushed at startup. TPMs shared through WithTPM are left alone.
    • WithECC(nil) returns ErrInvalidCurve instead of panicking.
  9. Hygiene. Marshal returns a copy of the serialized key. Go 1.26+ is required and dependencies are updated. The README and package doc describe the security properties.

Known limitation

The SRK is trusted on first use: it isn't checked against the endorsement key certificate. A device impersonating the TPM when a key is created could capture that key's auth value and parameters. After a key is created, a different device can't use it or complete the sessions that protect it.

Breaking changes

  • Serialized key format changed and now includes the SRK name. Keys from previous releases can't be loaded.
  • Hierarchy: keys are created under the owner hierarchy. WithEndorsementAuth was removed; use WithOwnerAuth.
  • AES ciphertext format changed to versioned AES-GCM. Ciphertexts from previous releases can't be decrypted.
  • ECC keys are signing-only and can't be used for decryption.
  • Sign rejects SHA-1.
  • Go 1.26+ is required.

Testing

Tests run against the go-tpm-tools simulator. They cover bus confidentiality (no auth values or payloads visible on the bus), rejection of known-sensitive keys, key usage restrictions, keys bound to the right TPM and SRK, keys invalidated by TPM2_Clear, RSA-PSS with each hash and salt length, session reuse, and recovery after all handles are flushed outside the library.

🤖 Generated with Claude Code

rthellend and others added 12 commits October 7, 2026 10:55
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBPwKpCGCUikziy99vn1jz
@rthellend
rthellend merged commit c1b3bf7 into main Oct 7, 2026
1 check passed
@rthellend
rthellend deleted the security-fixes branch October 7, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant