Add JSON Web Tokens as a Non-persistent Token Provider

bp json-web-tokens

JSON Web Token is a type of non-persistent bearer token similar to the fernet tokens we use today. JWT is an open standard with actively maintained libraries.

Problem Description

We currently support one token format called fernet. The fernet token format is a non-persistent format based on a spec by Heroku and was made the default token format for keystone.

However, the fernet format has its own problems that make it non-ideal. The fernet spec is largely abandoned, making it hard to get changes into it and thereby into the cryptography implementation of it. Moreover, the fernet spec is not recognized by any standards body and therefore not as closely audited as an IETF standard, making it more susceptible to zero-day vulnerabilities. Addressing these vulnerabilities falls solely on the OpenStack, specifically the keystone, community.

It would be nice to offer a new type of token that is backed by a widely used standard. This also increases the chances of interoperability between the OpenStack ecosystem and other communities that support JWT.

Use Cases

  • As a operator, I want to use a non-persistent token provider that isn’t coupled to a symmetric encryption or signing implementation. An implementation built on asymmetric signing or encryption allows me to distribute public keys from one node to another instead of syncing a repository of symmetric keys. This makes it easier to deploy keystone nodes with read-only capabilities strictly used for token validation. The specific usecase for this allows me to deploy read-only regions keeping token validation within the region, while having tokens issued from a central identity management system in a separate region.

  • As an operator, I want to be have a token provider to fall back on in the event there is a security vulnerability in the fernet spec or the cryptography implementation consumed by keystone.

  • As a user, I want to be able to authenticate for a token that I can use with other pieces of software outside of the OpenStack ecosystem that prove my identity.

Proposed Change

Create a new non-persistent keystone token backend based on the JSON Web Token standard. These will behave in much the same way as our current fernet tokens do.

The token will be a signed JWT (JWS) containing the authentication payload. Note that signed tokens are web safe and integrity verified, but token payload is not opaque to its holder. It is possible to decode a token and inspect the payload with JWS tokens. Using nested JWE tokens are the JSON Web Token equivalent to Fernet tokens and they are encrypted and signed.

This implementation reserves the right to change, modify, or remove items in the payload at any point in time and for any reason. Decoding and relying on attributes within the payload is not a supported API, nor should it be assumed as one. Users should use formal APIs to request information from keystone. The development team will not prevent or stall changes to token payloads, which are internal implementation details of the token provider, due to users relying on those attributes in ways they shouldn’t. Likewise, deployment that consider information in the token payload sensitive should rely on Fernet to prevent that information from being exposed to users.

Similar to the Fernet, JWTs will require a key repository be set up to use for signing tokens. A new keystone-manage command will be added to handle secret generation and rotation which will likely re-use much of the utilities in the fernet_setup and fernet_rotate commands. The recommended algorithm to be used is ES256. Keystone should not expose the ability to end users to ask for a specific JWS algorithm. Support should be limited to only supported or trusted algorithms that the end user cannot specify. JWS tokens will be integrity verified with a private key and validated using a corresponding public key. Since the ES256 implementation only uses signing (as opposed to signed encrypted payloads), this adheres to slightly better security practices over fernet because private keys never have to be synced across keystone API nodes. Only public keys need to be transferred to other keystone API servers to validate tokens across a cluster.

The payload of the JWS will use the following registered claims:

  • the sub claim will be a required string containing the ID of the user who authenticated for the token

  • the exp claim will be a required numeric value for token expiration

  • the iat claim will be a required numeric value for the time a token was issued

The following private claims will be used to relay additional required information and will be prefixed with openstack to avoid collisions with future upstream claims:

  • openstack_methods will be a required claim denoting the authentication methods used to obtain the token

  • openstack_audit_ids will be a required claim containing the audit information associated to a token

  • openstack_system will be an optional claim only present in system-scoped tokens

  • openstack_domain_id will be an optional claim only present in domain-scoped tokens

  • openstack_project_id will be an optional claim only present in project-scoped tokens

  • openstack_trust_id will be an optional claim only present in trust-scoped tokens

  • openstack_app_cred_id will be an optional claim only present in application credential tokens

  • openstack_group_ids will be an optional claim only present in federated tokens to carry an ephemeral user’s group assignments

  • openstack_idp_id will be an optional claim only present in federated tokens to carry the ID of a user’s identity provider

  • openstack_protocol_id will be an optional claim only present in federated tokens to denote the protocol used by a federated user to authenticate

  • openstack_access_token will be an optional claim only present in OAuth tokens

The PyJWT library is already present in the requirements repository and would be a convenient choice to use for this implementation. Both the PyJWT library and the JWCrypto library implement support for JWS. Since the implementation detailed in this specification is unique to ES256, a library that supports JWE isn’t necessary. If supporting another encrypted token type, like fernet, is a requirement in the future, then finding or contributing JWE support to the consuming library would be necessary.

Users will request and present tokens in exactly the same way they currently do with Fernet tokens. There is no need to add or change any APIs.

Key Setup & Rotation

Much like the Fernet implementation, a JWT provider will require a key rotation strategy. Since ES256 relies on asymmetric signing, the suggested rotation strategy will be slightly different than what is known with Fernet.

The Fernet implementation requires the usage of a staged key, which is just a key with a special name, in order to ensure tokens can be validated during the rotation process. This won’t be required with JWT and the following steps should be sufficient to perform key rotation without token invalidation due to missing signing keys. Assume the following steps are being performed on three different API servers, named A, B, and C, that need to validate tokens issued by each other.

  1. A key pair is created for each API server. A.priv, A.pub for A, B.priv, B.pub for B, and C.priv, C.pub for C.

  2. A copy of each public key is transferred to each API server. A, B, and C all have copies of A.pub, B.pub, and C.pub.

At this point, tokens issued from any API server can be validated anywhere. In the event a single API server needs to rotate key pairs:

  1. A new key pair is created for A called A’.priv and A’.pub. A is not configured to start signing tokens with A’.priv until all other nodes in the cluster have a copy of A’.pub.

  2. A’.pub is copied to the public key repository of each API server. So long as B and C have A’.pub they are ready to validate tokens signed by the new private key A’.priv.

  3. After B and C have been updated with copies of A’.pub, server A can be updated to start signing tokens with the new key A’.priv. Once all tokens signed with A.priv are expired, A.priv and A.pub can be removed from all servers. It is important to allow for a grace period between configuring A to use A’.priv and removing A.priv in order to prevent the premature invalidation of tokens that haven’t expired yet.

Note that the rotation process could be simplified slightly in step #1. The JWS specification goes into detail about serialization and support for multiple signatures in a single token. See JWS section 7 for more information. The PyJWT library does not support multiple signatures on a single token. If it did, it would be possible to configure server A in step #1 to sign tokens with multiple private keys. Servers B and C would still be able to validate tokens from A because they have a copy of the public key (A.pub) used to create one of the token’s signatures. The rest of the rotation process remains the same as far as propagating A’.pub to the other servers and eventually configuring A to only sign tokens with A’.priv. If, or when, PyJWT supports this functionality, support for multiple signatures in the JWS provider can be reconsidered.

Traditional asymmetric keys can be revoked using revocation lists. At this time we are not going to support a revocation list implementation for JWT key pairs. The operator has the ability to sync public keys accordingly when they rotate new keys in and out. Keystone will only use the public keys on disk to validate tokens. Is could change in the future, but for now it keeps the key rotation and key utilities with keystone simpler.

Crypto-Agility & Future Work

This specification is targeting a single algorithm for the initial JWT implementation. If and when keystone decides to expand the implementation to include additional algorithms, we should allow for flexibility between configured algorithms, which will make it easier for operators to switch from one algorithm to another if they need to.

For example, the validation process using a JWT token provider might support validating multiple blessed algorithms, allowing multiple tokens signed with different algorithms to be validated without require configuration changes except on the signing node.

For the time being, if a deployment is using JWTs and needs to exercise crypto-agility, it is recommended they convert to Fernet tokens.

Alternatives

Recently, there have been various efforts that help solve authenticated encryption. One of these efforts was sparked by a concern with JWT, namely the JOSE header. The issue detailed in the report was specific to users being able to specify algorithms and exploit a validation weakness in various JWT libraries. All python libraries have been patched, but keystone should specifically rely on validating algorithm usage and never assuming algorithms to be supplied by end users. Please see the full report for details on the vulnerability and why we are going to strictly validate this information.

There is a proof-of-concept implementation for Platform Agnostic Security Tokens, or PASETO that takes a more strict stance on algorithm validation and the intended audience of the token. The strict stance of versioned protocols with PASETO is certainly advantageous, but the implementation and idea are still in the incipient stage. It’s certainly worth noting that we should keep out eye on this development and re-evaluate it if, or when, it gets more adoption.

For now, if keystone supplies strict algorithm validation to the JWT implementation, we should be able to offer a comparable backup option to fernet.

Security Impact

Since JWT is a widely used web standard, this will have a net positive impact on security. The implementation will use asymmetric signing, reducing risk of having to replicate or transfer private keys from one host to another. Since the token payloads are signed, data within the token will be readable to anyone who has the token. The token can only be validated using the corresponding public key of the private key used to sign the token originally. These will still be bearer tokens and so interception of one must still be guarded against.

Known Vulnerabilities

There is a documented vulnerability that affected several JWT libraries, including one library written in Python.

In most cases, JSON Web Tokens will have a header, payload, and signature where each section is delimited by a period (.). The header contains an important piece of information, which is how the token’s integrity is protected. This is stored as the alg attribute of the header. The library verifying the token uses the algorithm specified in the header to perform an integrity check and compares its results to the signature portion of the token.

Security concerns have been documented and raised that describe the issues with allowing clients to dictate algorithms used for token verification. This is a concern specifically with applications that support asymmetric and symmetric signing. An attacker could effectively bypass the verification check of a token by using a published, or known, public key to generate a JWT with a symmetric signing algorithm.

This would be applicable if keystone supported signed tokens and encrypted tokens with the same token provider implementation. This vulnerability has been addressed across various libraries after its discovery, but keystone should be aware of the overall technique that lead to it in the first place. We can mitigate this type of vulnerability in keystone by:

  • Ensuring keystone doesn’t blindly allow end users to specify which algorithm is used to verify the integrity of a token (e.g., only implementing support for ES256)

  • Ensure the alg supplied in the token header is only ever populated by keystone

  • Ensure keystone only issues tokens of a single encryption or signing strategy (e.g., not allowing users to get signed token and encrypted tokens from the same server, thus mixing asymmetric and symmetric key usage at runtime)

Specifics about the vulnerability can be found in the report.

Notifications Impact

Notifications for JWTs will behave in the same way that they do for fernet tokens, including for revocation events.

Other End User Impact

This will have no end user impact. They will request and use JWTs in exactly the same way that they currently use fernet tokens.

Performance Impact

It will be worth investigating performance differences between token providers that use asymmetric signing (JWT) and symmetric encryption (fernet). These difference, if significant, should be published in documentation as it might be useful for operators when choosing a token provider.

Other Deployer Impact

This is an optional, opt-in feature that will not be the default, so deployers will not be affected unless they choose to use JWT. In that case, deployers will need to set up a key repository before using JWTs. The key repository will contain asymmetric key pairs rather than just secret keys. The deployer will need to take care to sync and rotate keys the way they do with fernet tokens.

Developer Impact

The new token type will reuse much of the work already done for fernet tokens and will follow similar code paths, so this will be relatively easy to maintain.

Implementation

Assignee(s)

Primary assignee:

Gage Hugo (gagehugo) Lance Bragstad (lbragstad) XiYuan Wang (wxy)

Work Items

  • Refactor the fernet utilities modules to be generic enough to work with JWT or inheritable

  • Add a keystone-manage command to set up and rotate JWT signing keys

  • Generalize the TokenFormatter class to support JWT

  • Refactor the fernet token provider module to be inheritable or generic

  • Add a keystone doctor command to validate the setup in the same way that fernet is validated

Dependencies

There are three different libraries we can use to implement this functionality.

  1. PyJWT

    This library only supports token signing, or JWS. It does not support JWE, or authenticated encryption, yet. A minimum version of 1.0.1 is required, but this library is already included in OpenStack global requirements repository.

  2. python-jose

    This library only supports token signing, or JWS. It does not support JWE, or authenticated encryption, yet. This library is not included in OpenStack global requirements.

  3. JWCrypto

    This library supports both JWS and JWE, but it is not included in OpenStack global requirements.

  1. Authlib

    This library supports both JWS and JWE, but its licensing is incompatible with OpenStack as it is AGPL.

Given the fact that the initial implementation of JWT is not going to rely on nested JWT tokens or encrypted payloads, it’s safe to assume that signing support will be sufficient. The PyJWT library is already included in global requirements and we don’t have a case to not use that specific library, which is compatible with OpenStack licensing.

Documentation Impact

The new [token]/provider configuration option will need to be documented, as will the new keystone-manage commands.

References