Skip to content

tpm2: Support safely rotating the lockout hierarchy authorization value#539

Merged
chrisccoulson merged 8 commits intocanonical:masterfrom
chrisccoulson:tpm2-safe-rotate-lockout-auth-value
Apr 24, 2026
Merged

tpm2: Support safely rotating the lockout hierarchy authorization value#539
chrisccoulson merged 8 commits intocanonical:masterfrom
chrisccoulson:tpm2-safe-rotate-lockout-auth-value

Conversation

@chrisccoulson
Copy link
Copy Markdown
Contributor

This updates the Connection.EnsureProvisioned API to safely support
rotating the authorization value for the lockout hierarchy, which can be
used during reprovisioning. This is particularly required during a
factory reset, when secrets should be rotated.

The lockout hierarchy is used on successful boots to reset the DA
counter. A single authorization failure makes the lockout hierarchy
unavailable for the preprogrammed recovery time (currently 24 hours).
This makes it challenging to update the value because it's not possible
to atomically update both the value in the TPM and the value stored in
persistent storage.

This PR works around this by using a transient authorization policy that
permits the use of the TPM2_HierarchyChangeAuth command with a signed
assertion (TPM2_PolicySigned), using a key that's derived from the
original authorization value. An authorization policy is used by default
for other uses of the lockout hierarchy as this provides a way to detect
a transient state resulting from a previously interrupted update before
using the incorrect authorization value.

The WithProvisionNewLockoutAuthValue option no longer allows an
authorization value to be specified directly. One is derived
automatically from the supplied random source, with the length
determined by the value of the TPM_PT_CONTEXT_HASH property. A callback
is supplied so that the authorization data can be stored to persistent
storage. This happens several times during an update.

@chrisccoulson chrisccoulson force-pushed the tpm2-safe-rotate-lockout-auth-value branch 4 times, most recently from b48db59 to 3c9ab81 Compare April 22, 2026 14:26
This updates the Connection.EnsureProvisioned API to safely support
rotating the authorization value for the lockout hierarchy, which can be
used during reprovisioning. This is particularly required during a
factory reset, when secrets should be rotated.

The lockout hierarchy is used on successful boots to reset the DA
counter. A single authorization failure makes the lockout hierarchy
unavailable for the preprogrammed recovery time (currently 24 hours).
This makes it challenging to update the value because it's not possible
to atomically update both the value in the TPM and the value stored in
persistent storage.

This PR works around this by using a transient authorization policy that
permits the use of the TPM2_HierarchyChangeAuth command with a signed
assertion (TPM2_PolicySigned), using a key that's derived from the
original authorization value. An authorization policy is used by default
for other uses of the lockout hierarchy as this provides a way to detect
a transient state resulting from a previously interrupted update before
using the incorrect authorization value.

The WithProvisionNewLockoutAuthValue option no longer allows an
authorization value to be specified directly. One is derived
automatically from the supplied random source, with the length
determined by the value of the TPM_PT_CONTEXT_HASH property. A callback
is supplied so that the authorization data can be stored to persistent
storage. This happens several times during an update.
@chrisccoulson chrisccoulson force-pushed the tpm2-safe-rotate-lockout-auth-value branch from 3c9ab81 to cf97c51 Compare April 22, 2026 14:27
@chrisccoulson chrisccoulson marked this pull request as ready for review April 22, 2026 14:28
@pedronis pedronis requested a review from valentindavid April 22, 2026 15:38
Comment thread tpm2/provisioning.go Outdated
//
// This option will also resume a previously interrupted update, as long as the most recent authorization
// data is supplied to [WithLockoutAuthData].
func WithProvisionNewLockoutAuthValue(rand io.Reader, syncData func([]byte) error) EnsureProvisionedOption {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to call it WithProvisionNewLockoutAuthData (Data not Value) so people do not think they should call WithLockoutAuthValue. Or maybe use some types for []byte to make them distinct.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds ok.

Comment thread tpm2/lockoutauth.go
Copy link
Copy Markdown
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, did a pass

Comment thread tpm2/provisioning.go
case tpm2.IsTPMWarning(err, tpm2.WarningLockout, command):
return ErrTPMLockout
case tpm2.IsTPMSessionError(err, tpm2.ErrorPolicyFail, command, 1):
return ErrInvalidLockoutAuthPolicy
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this dropped?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's impossible to hit this case now because authorizeLockout returns ErrInvalidLockoutAuthPolicy instead.

Comment thread tpm2/provisioning_test.go Outdated
return nil
}

opts := []EnsureProvisionedOption{WithLockoutAuthValue(nil), WithProvisionNewLockoutAuthValue(bytes.NewReader(data.lockoutAuthBytes), syncLockoutAuthData)}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these options is what to try if we didn't have a stored auth data/params yet?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, although perhaps there should be an explicit WithUnconfiguredLockoutAuth option instead.

Comment thread tpm2/lockoutauth.go
case params.AuthPolicy != nil && session.Handle().Type() == tpm2.HandleTypeHMACSession:
// authorization was performed with a HMAC session when we have policy data,
// which only happens if the lockout hierarchy has no policy set.
return ErrLockoutAuthNotInitialized
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests don't' end up here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's weird - lockoutauthSuite.TestResetDictionaryAttackLockAuthPolicyUnset is meant to hit this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it was a bug in the test :) a2ef396

Comment thread tpm2/lockoutauth.go Outdated
err = m.tpm.HierarchyChangeAuth(m.tpm.LockoutHandleContext(), m.authParams.NewAuthValue, session, m.tpm.HmacSession().IncludeAttrs(tpm2.AttrCommandEncrypt))
default:
// We're using HMAC auth
err = m.tpm.HierarchyChangeAuth(m.tpm.LockoutHandleContext(), m.authParams.NewAuthValue, session.IncludeAttrs(tpm2.AttrCommandEncrypt))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this case is not reached by tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that it's not actually possible to hit this case, so I've removed it.

Comment thread tpm2/lockoutauth.go
Comment thread tpm2/lockoutauth.go
…rupted

These are merged into a new error: ErrLockoutAuthInvalid. There's no
point in having 2 different errors for conditions that require the same
resolution (calling EnsureProvisioned again).
@chrisccoulson
Copy link
Copy Markdown
Contributor Author

Ok, I've pushed some updates:

  • WithProvisionNewLockoutAuthValue is now WithProvisionNewLockoutAuthData.
  • There is a new WithUnconfiguredLockoutAuth option to avoid having to use WithLockoutAuthValue(nil) on a fresh install.
  • Along with this, there is a new ErrLockoutAuthInitialized which will be returned if Connection.EnsureProvisioned is called with the new option if the lockout hierarchy already has an authorization value (as an alternative to just tripping the lockout).
  • I've merged ErrInvalidLockoutAuthPolicy and ErrLockoutAuthUpdateInterrupted into a new error - ErrLockoutAuthInvalid, as there's no need to distinguish between these 2 cases given that they have the same resolution (need to call Connection.EnsureProvisioned again with the WithProvisionNewLockoutAuthData option).

Copy link
Copy Markdown
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you

@chrisccoulson chrisccoulson merged commit c00dcff into canonical:master Apr 24, 2026
2 checks passed
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.

3 participants