Hardware-bound API tokens¶
launchpadlib can use a USB FIDO2 security key to protect your Launchpad API credentials.
Ordinary launchpadlib logins (see OAuth authentication in the introduction) store a long-lived OAuth access token. Anyone who reads that file, or the keyring entry holding it, can act as you until you revoke the token.
A hardware-bound login stores a long-lived OAuth refresh grant instead. Launchpad binds the grant to one of your registered security keys, and launchpadlib must prove that it holds the key to exchange the grant for a short-lived ordinary OAuth access token. API requests then use launchpadlib’s usual OAuth 1 signing.
Hardware binding protects the stored grant, not each access token. A minted access token is an ordinary OAuth token for its short lifetime, but the stored grant cannot mint a new one without the security key.
Installation¶
Security-key support is optional. launchpadlib imports without python-fido2 and only loads it when a security-key operation starts. Install the extra:
pip install 'launchpadlib[fido2]'
On Debian and Ubuntu you can instead install:
apt install python3-fido2
The supported versions are fido2>=1.1.2,<3.
To check whether python-fido2 is available:
from launchpadlib.fido2compat import Fido2SupportError, require_fido2
try:
require_fido2()
except Fido2SupportError:
...
require_fido2() imports python-fido2 but does not look for a device or
prompt the user.
Security keys¶
Only USB (HID) authenticators are supported. NFC authenticators and platform passkeys are not.
HardwareKeyClient performs WebAuthn ceremonies with a local key. It
checks the HTTPS origin and the WebAuthn relying party ID before it talks to
the key, and closes every device it opened afterwards, even on failure.
ConsoleInteraction, the default, prompts on the console for a touch and
a PIN, and asks you to choose when several keys or several matching
credentials are available. NonInteractiveInteraction fails instead of
prompting; it cannot bypass what the key requires.
Talking to a USB key needs access to its hidraw device. If a connected
key is not found or cannot be opened, check the device permissions and the
FIDO2 udev rules from your distribution or key vendor. Avoid rules that
grant access to every hidraw device. Running as root is not required.
Registering a security key¶
register_hardware_key registers a security key with your Launchpad
account using an authenticated devel client:
from launchpadlib.launchpad import Launchpad
launchpad = Launchpad.login_with(
"hardware-key-registration", "production", version="devel"
)
authenticator = launchpad.register_hardware_key("My security key")
The WebAuthn origin defaults to the web root of the service root, such as
https://launchpad.net; pass origin to override it. Pass
interaction to change how you are prompted, or hardware_client to
use a configured HardwareKeyClient.
The returned authenticator is an API entry. Pass it directly when issuing grants. To revoke the key:
authenticator.revoke()
Grants bound to a revoked key can no longer mint access tokens. Access tokens already minted with it expire as well.
Issuing a hardware-bound grant¶
Issuing a grant is separate from logging in with it. Use
issue_hardware_bound_oauth_token on an authenticated devel client:
grant = launchpad.issue_hardware_bound_oauth_token(
consumer_key="hardware-client",
authenticator=authenticator,
permission="Change Anything",
)
consumer_keyThe OAuth consumer that will log in with the grant.
authenticatorThe registered authenticator entry that the grant is bound to.
permissionThe title of an OAuth permission, such as
"Read Non-Private Data"or"Change Anything".contextOptionally, a pillar name, or
distribution/sourcepackagename, to restrict the grant to.date_expiresOptionally, a timezone-aware expiry. Launchpad defaults to, and limits grants to, 365 days.
The result is a dictionary with oauth_token and oauth_token_secret.
Launchpad only returns the secret once, so pass the dictionary straight to
login_with_hardware_token or keep it secret.
A grant cannot be broader than the permission of the credentials used to issue it, and must keep their context restriction, if any.
To revoke a grant:
launchpad.revoke_hardware_bound_oauth_token(grant["oauth_token"])
This also expires access tokens already minted from the grant. Other grants bound to the same key are not affected. Revoking on the server does not remove a stored copy of the grant; logins using it will be rejected.
Logging in with a grant¶
Pass the issued grant to Launchpad.login_with_hardware_token. The
consumer must match the consumer_key used to issue the grant:
from launchpadlib.launchpad import Launchpad
launchpad = Launchpad.login_with_hardware_token(
"my-application",
"production",
account="example-user",
consumer_name="hardware-client",
grant=grant,
)
print(launchpad.me.name)
Like login_with, the default service root is staging. The login mints an
access token straight away, because fetching the service root must be
signed, so expect to touch the key once during login.
grant also accepts a HardwareTokenGrant. credential_id optionally
restricts the assertion to one key by its base64url WebAuthn credential ID;
this is not the numeric id of the authenticator entry.
The grant is stored once it has authenticated a request as account, so
a failed import does not replace a working stored grant. Later logins omit
grant and load it from the credential store:
launchpad = Launchpad.login_with_hardware_token(
"my-application",
"production",
account="example-user",
consumer_name="hardware-client",
)
If no grant is stored, HardwareTokenError is raised; there is no
fallback to browser authorization. Each service root and consumer pair
stores one grant, and account must match the account that owns it.
origin, interaction and hardware_client work as for
register_hardware_key. origin does not change where the token
exchange is sent. application_name, consumer_name,
credentials_file, credential_store, credential_save_failed,
launchpadlib_dir, timeout, proxy_info and version work as
for login_with, except that version defaults to devel.
Storing grants¶
Grants are stored in the system keyring by default, and only in memory
under sudo. Pass credentials_file, or set
LP_HARDWARE_CREDENTIALS_FILE, to store the grant in a file instead. The
file is written atomically and holds the grant’s secret, so protect it like
a password. credentials_file and credential_store cannot both be
used.
A stored record holds the refresh token and its secret, the account, the optional credential ID, the service root, the WebAuthn origin and the consumer key. Access tokens, challenges, assertions and PINs are never stored, and private keys never leave the security key.
Records are stored under v2: followed by the SHA-256 digest of a JSON
list of the service root and consumer key. The keyring service is
launchpadlib.hardware-token.v2.
Token renewal¶
An access token is reused until it is close to expiry, so ordinary requests do not need a touch each. Renewal happens while signing a request, the smaller of 60 seconds and a tenth of the token’s lifetime before it expires.
Minting is serialized, so concurrent requests share one new token instead of starting parallel ceremonies. The HTTP client itself is not thread-safe; use one client per thread.
If Launchpad rejects an access token as expired, invalid or unknown, it is
discarded. GET, HEAD and OPTIONS requests are retried once with a
new token; other requests are not, because repeating them could duplicate
their effects.
Requests to other hosts, such as file downloads, are sent without the
Authorization header.
The exchange protocol¶
HttpMintingTransport exchanges grants using two endpoints on the web
root of the service root:
POST <web root>/+hw-challengeSends
oauth_tokenandoauth_token_secret, form encoded. Returns JSON with a positive integerchallenge_idand WebAuthn request options underpublicKey.POST <web root>/+hw-access-tokenSends
oauth_token,oauth_token_secret,challenge_idand the JSON-encoded WebAuthnassertion, form encoded. Returns a form-encodedoauth_token,oauth_token_secret, ISO 8601expires_atand optionallp.context.
The exchange is authenticated by the grant, not signed with OAuth. The
challenge must be 32 bytes and its relying party ID must match the origin.
Redirects are not followed, responses are not cached and assertions are
never retried. HTTP 401, 403 and 410 raise HardwareTokenGrantError;
other failures raise HardwareTokenError. The transport refuses to send
secrets while httplib2.debuglevel is set, and honours
LP_DISABLE_SSL_CERTIFICATE_VALIDATION like ordinary logins.
To exchange grants another way, pass a minting_transport with two
methods:
request_challenge(grant)Return a
MintingChallenge(handle, options).verify_assertion(grant, handle, assertion)Return a
MintedAccessToken. If it has noexpires_at, the token is used until Launchpad rejects it.
Errors¶
HardwareKeyErrorA security-key operation failed.
HardwareKeyCancelledErrorThe user or the key cancelled the operation.
HardwareKeyTimeoutErrorThe security-key operation timed out.
Fido2SupportErrorpython-fido2 is not installed.
HardwareTokenErrorA hardware-bound grant or access token could not be issued, stored or minted. It is a subclass of
HardwareKeyError.HardwareTokenGrantErrorLaunchpad rejected a grant during the exchange.
Base64url encoding¶
launchpadlib.fido2compat provides encode_base64url and
decode_base64url for WebAuthn byte strings. Encoding never adds padding.
Decoding accepts optional padding but rejects the standard Base64 alphabet,
excess padding and non-canonical encodings.