Skip to content

fix(bindings): warn when Python SecureStorage gets keyring's failing or null backend - #469

Merged
bahdotsh merged 4 commits into
mainfrom
fix/python-keyring-fail-backend-warning
Oct 1, 2026
Merged

bahdotsh merged 4 commits into
mainfrom
fix/python-keyring-fail-backend-warning

Conversation

@kivtxs

@kivtxs kivtxs commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Summary

SecureStorage warns when keyring has resolved to a backend that cannot hold MLS keys. However, the check looked only at the class name. keyring's failing backend (keyring.backends.fail.Keyring) and null backend (keyring.backends.null.Keyring) are both classes named plain Keyring, so neither matched. A headless Linux host or container without a secret service gets the failing backend, which is exactly the case the warning exists for. On such a host, no warning was logged.

This PR:

  • also recognises the modules keyring.backends.fail and keyring.backends.null;
  • names the backend in full in the warning (for example keyring.backends.fail.Keyring);
  • keeps the existing class-name checks (Fail, Null, PlaintextKeyring), which now matter for third-party backends;
  • judges the first backend inside keyring's ChainerBackend (the one writes go to) rather than the chainer itself, and warns about an empty chain. This is defensive: a host whose only viable backend is keyrings.alt's plaintext file gets PlaintextKeyring directly, not a chain;
  • points a headless host at the built-in file stores (store_key / store_key_env).

Before, on a host whose keyring resolves to the failing backend: no warning.
After: one warning per SecureStorage construction:

keyring backend is 'keyring.backends.fail.Keyring': MLS keys will NOT be stored securely. Install a platform secret service (e.g. gnome-keyring, kwallet), or on a headless host pass store_key or store_key_env to ProtocolManager to use the built-in file stores.

Related issues

None.

Type of change

  • fix: bug fix

Checklist

  • Commits follow Conventional Commits (fix(bindings): ...)
  • CHANGELOG.md updated (under Fixed)
  • No Rust, UDL or generated-binding change. cargo fmt, cargo clippy, cargo test and cargo-deny are unaffected; CI runs them.
  • No new unsafe

Validation completed before opening

  • New tests: bindings/python/tests/test_secure_storage_backend_warning.py, 9 cases:
    • The failing, null and plaintext backends, and third-party backends named FailKeyring / NullKeyring, are each warned about exactly once, and the message names the backend in full and the store_key remedy.
    • A chain led by an insecure backend, and an empty chain, are warned about.
    • A platform backend, alone or leading a chain, is not warned about.
  • On current main: the failing and null cases fail (no warning). The plaintext and platform cases pass.
  • On this branch: 9 passed. The full Python binding suite passes (806 passed) on macOS arm64. Deleting any one condition of the check (module set, chain unwrap, empty chain, Fail marker, Null marker) fails at least one test.
  • Headless scenario: a Linux arm64 container with no secret service, using the default SecureStorage. The warning is logged and names keyring.backends.fail.Keyring.

Breaking changes

None. This changes log output only:

  • Storage behaviour is unchanged. A failing backend still fails every storage call, as before.
  • The warning's wording changed slightly: it now names the backend's module and uses a colon instead of a dash. Code that parses the old text would see the new form.

Notes for reviewers

  • Whether the SDK should refuse an insecure backend by default, rather than warn, is a separate policy decision. This PR does not change it.
  • The tests substitute keyring.get_keyring() with instances of keyring's own fail and null backends. The plaintext and platform backends are stand-ins built from a class name and module, because keyrings.alt is not a dependency.

…or null backend

SecureStorage warns when keyring resolved to a backend that cannot hold MLS
keys, but checked only the class name. keyring's failing and null backends are
both classes named Keyring (keyring.backends.fail, keyring.backends.null), so the
case the warning exists for, a host without a secret service, never matched.

Also check the backend's module, and name the backend in full in the warning.
The existing class-name checks are kept. Log output only.
The previous commit taught the insecure-backend warning that
keyring's failing and null backends are both classes named plain
`Keyring`. It still judged whatever get_keyring() handed back by
name, and that is not always the backend that holds the keys.

It turns out that once two or more backends have a positive
priority, keyring returns a ChainerBackend at priority 10 and lets
it delegate. That is exactly what a headless host gets after the
usual workaround, `pip install keyrings.alt`: a chain led by a
plaintext backend, whose class name matches nothing. No warning,
plaintext keys. The very case the warning exists for, again.

So unwrap the chain and judge its first backend, the one writes
reach. A chain with nothing in it stores nothing, so it warns too.

While at it, the warning only told people to install gnome-keyring
or kwallet. A server or container has a better answer now, the
built-in file stores behind `store_key`, so say that. The README
claimed keyring falls back to a null or plaintext backend; it falls
back to the failing one. And the tests only checked that "Keyring"
appeared in the message, which a warning that dropped the module
also passes. They now pin the qualified name.
The last round explained the ChainerBackend unwrap by saying that a
headless host with keyrings.alt gets a chainer, which hid the
plaintext backend. It doesn't. The chainer's priority is 10 only
when two or more backends rank above zero, and -1 otherwise, so a
host whose only viable backend is keyrings.alt's plaintext file
gets PlaintextKeyring *directly*. The class-name check already
caught that before this PR. With pycryptodome installed the chain
leads with EncryptedKeyring, which is correctly not warned.

Probed against keyring 25.7.0 and keyrings.alt 5.0.2 with the
platform backend made unviable. The unwrap is still right, writes
go to the first chained backend, but it is defensive, and the code
comment, the changelog and the test docstring now say so instead of
inventing a common case.

While at it, the "Fail" and "Null" class-name markers no longer
catch anything of keyring's own (the module check does), so they
only matter for third-party backends, and nothing pinned them:
deleting either one left the suite green. Say what they are for and
add a third-party case for each.
@bahdotsh
bahdotsh merged commit 8aae1c6 into main Oct 1, 2026
22 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants