Skip to content

Do not mark an LE link as authenticated after Just Works pairing - #1002

Open
deadcaf3 wants to merge 2 commits into
google:mainfrom
deadcaf3:worktree-fix-unauthenticated-attrib-unlock
Open

deadcaf3 wants to merge 2 commits into
google:mainfrom
deadcaf3:worktree-fix-unauthenticated-attrib-unlock

Conversation

@deadcaf3

@deadcaf3 deadcaf3 commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Connection.authenticated was set to true after every pairing, and again
whenever LE encryption was enabled. The GATT server's
READ_REQUIRES_AUTHENTICATION and WRITE_REQUIRES_AUTHENTICATION checks read
that flag, so a peer that paired with Just Works could read and write those
attributes. With the default PairingDelegate, Just Works needs no user
interaction. The keys stored for the same pairing were already marked
authenticated: false.

The flag is now derived from the key that the link is encrypted with.

Behavior

Link encrypted by authenticated before authenticated now
Just Works pairing true false
Passkey, numeric comparison or OOB pairing true true
Stored LTK from a Just Works pairing true false
Stored LTK from an authenticated pairing true true

Notes for review

  • After pairing, Device.on_pairing takes the flag from the keys it is given,
    which smp.Session.on_pairing already marks according to the pairing method.
    smp.py and the on_pairing signature are not changed.
  • When the link is encrypted with an LTK from the key store, the flags of that
    key are recorded in a new Connection.ltk_security when the key is given to
    the controller (Device.get_long_term_key as a peripheral, Device.encrypt
    as a central), and applied when the Encryption Change event arrives.
  • Connection.ltk_security is cleared when a pairing starts and when
    encryption fails, so that the flags of a stored key that did not encrypt the
    link are not applied to a later encryption.
  • Device.encrypt checks the role before it looks up the key, so a call on a
    peripheral does not record the flags of a key that is never used.
  • sc is set the same way. Before, it was set to true on any LE encryption
    change, including with a legacy LTK.
  • Classic links are not changed.
  • The reconnection tests only cover Secure Connections bonds. Re-encrypting
    with a legacy bond disconnects the link with the virtual controller, with or
    without this change.

Compatibility

  • Servers with attributes that require authentication now answer Insufficient
    Authentication to a peer that paired with Just Works. In this repository that
    applies to apps/pair.py and
    examples/run_gatt_server_with_pairing_delegate.py.
  • During a pairing, authenticated and sc are no longer set when encryption
    starts. They are set when the pairing completes, before the pairing event.

Testing

Format, lint and the test suite pass, and mypy reports nothing in the changed
files. Three tests (8 cases) are added to tests/self_test.py; the Just Works
cases fail without the change.

`Connection.authenticated` was set to true after every pairing, and again
whenever LE encryption was enabled, so the GATT server gave access to
attributes with READ/WRITE_REQUIRES_AUTHENTICATION to any peer that paired
with Just Works.

Derive the flag from the key that the link is encrypted with:
- after pairing, from the `authenticated` flag of the keys that the pairing
  generated, which is false for Just Works
- when encrypting with an LTK from the key store, from the `authenticated`
  flag of that key, applied once encryption is enabled. `sc` is set the same
  way, instead of always being set to true.

Classic links are not changed.

@zxzxwu zxzxwu left a comment

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.

LGTM, thanks! Left two minor non-blocking nits inline.

Also a tiny heads-up: a few lines in the PR description appear to have been truncated when pasting (e.g. under "Notes for review" and "Compatibility").

Comment thread bumble/device.py
ltk = keys.ltk_central.value
rand = keys.ltk_central.rand or b''
ediv = keys.ltk_central.ediv or 0
connection.ltk_security = (keys.ltk_central.authenticated, False)

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.

Nit: Consider moving the if connection.role != hci.Role.CENTRAL: check (just below) before looking up the key / setting connection.ltk_security, so a failed encrypt() call on a peripheral doesn't leave connection.ltk_security set.

@deadcaf3 deadcaf3 Oct 5, 2026 •

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.

e2e75c1 gets the roll check run first before key store lookup; peripheral never records key's flags.

Comment thread bumble/device.py
# The link is only as secure as the stored key it is encrypted with.
# When pairing, this is set in `on_pairing` instead.
connection.authenticated, connection.sc = connection.ltk_security
connection.ltk_security = None

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.

Nit: Should we also reset connection.ltk_security = None in on_connection_encryption_failure below so stale security flags aren't left on the connection if encryption with a stored LTK fails?

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.

e2e75c1 also clears ltk_security in on_connection_encryption_failure.

Address review comments:
- Device.encrypt checks the role before looking up the key, so a call on
  a peripheral does not leave Connection.ltk_security set.
- Connection.ltk_security is cleared when encryption fails.

This branch has not been deployed

No deployments
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.

2 participants