0% found this document useful (0 votes)
172 views30 pages

The JWT Handbook 2

The document discusses JSON Web Tokens (JWTs), which are compact, URL-safe means of representing claims to be transferred between two parties. JWTs can be signed using JSON Web Signatures (JWS) and encrypted using JSON Web Encryption (JWE) to provide secure transmission of claims. Common uses of JWTs involve establishing the identity of a party by transferring standard claims like a username (sub claim). The document will cover the structure of signed and encrypted JWTs as well as algorithms for signing, encrypting, and representing cryptographic keys.

Uploaded by

MOHAMED HOUSNI
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
172 views30 pages

The JWT Handbook 2

The document discusses JSON Web Tokens (JWTs), which are compact, URL-safe means of representing claims to be transferred between two parties. JWTs can be signed using JSON Web Signatures (JWS) and encrypted using JSON Web Encryption (JWE) to provide secure transmission of claims. Common uses of JWTs involve establishing the identity of a party by transferring standard claims like a username (sub claim). The document will cover the structure of signed and encrypted JWTs as well as algorithms for signing, encrypting, and representing cryptographic keys.

Uploaded by

MOHAMED HOUSNI
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 30

4 JSON Web Signatures 30

4.1 Structure of a Signed JWT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30


4.1.1 Algorithm Overview for Compact Serialization . . . . . . . . . . . . . . . . . 32
4.1.2 Practical Aspects of Signing Algorithms . . . . . . . . . . . . . . . . . . . . . 33
4.1.3 JWS Header Claims . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.1.4 JWS JSON Serialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.1.4.1 Flattened JWS JSON Serialization . . . . . . . . . . . . . . . . . . . 38
4.2 Signing and Validating Tokens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.2.1 HS256: HMAC + SHA-256 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2.2 RS256: RSASSA + SHA256 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2.3 ES256: ECDSA using P-256 and SHA-256 . . . . . . . . . . . . . . . . . . . . 40

5 JSON Web Encryption (JWE) 41


5.1 Structure of an Encrypted JWT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
5.1.1 Key Encryption Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
5.1.1.1 Key Management Modes . . . . . . . . . . . . . . . . . . . . . . . . 46
5.1.1.2 Content Encryption Key (CEK) and JWE Encryption Key . . . . . 47
The JWT Handbook 5.1.2 Content Encryption Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . 48
5.1.3 The Header . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
5.1.4 Algorithm Overview for Compact Serialization . . . . . . . . . . . . . . . . . 49
5.1.5 JWE JSON Serialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Sebastián E. Peyrott, Auth0 Inc. 5.1.5.1 Flattened JWE JSON Serialization . . . . . . . . . . . . . . . . . . 52
5.2 Encrypting and Decrypting Tokens . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.2.1 Introduction: Managing Keys with node-jose . . . . . . . . . . . . . . . . . . 52
Version 0.14.1, 2016-2018 5.2.2 AES-128 Key Wrap (Key) + AES-128 GCM (Content) . . . . . . . . . . . . 54
5.2.3 RSAES-OAEP (Key) + AES-128 CBC + SHA-256 (Content) . . . . . . . . . 54
5.2.4 ECDH-ES P-256 (Key) + AES-128 GCM (Content) . . . . . . . . . . . . . . 55
5.2.5 Nested JWT: ECDSA using P-256 and SHA-256 (Signature) + RSAES-
OAEP (Encrypted Key) + AES-128 CBC + SHA-256 (Encrypted Content) . 55
5.2.6 Decryption . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

6 JSON Web Keys (JWK) 58


6.1 Structure of a JSON Web Key . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
6.1.1 JSON Web Key Set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60

7 JSON Web Algorithms 61


7.1 General Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
7.1.1 Base64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
7.1.1.1 Base64-URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
7.1.1.2 Sample Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
7.1.2 SHA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
7.2 Signing Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
7.2.1 HMAC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
7.2.1.1 HMAC + SHA256 (HS256) . . . . . . . . . . . . . . . . . . . . . . . 71
7.2.2 RSA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
7.2.2.1 Choosing e, d and n . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
7.2.2.2 Basic Signing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

2
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Claims are definitions or assertions made about a certain party or object. Some of these claims
and their meaning are defined as part of the JWT spec. Others are user defined. The magic
Special Thanks behind JWTs is that they standardize certain claims that are useful in the context of some common
operations. For example, one of these common operations is establishing the identity of certain
party. So one of the standard claims found in JWTs is the sub (from “subject”) claim. We will take
a deeper look at each of the standard claims in chapter 3.
Another key aspect of JWTs is the possiblity of signing them, using JSON Web Signatures (JWS,
In no special order: Prosper Otemuyiwa (for providing the federated identity example from RFC 75156 ), and/or encrypting them, using JSON Web Encryption (JWE, RFC 75167 ). Together
chapter 2), Diego Poza (for reviewing this work and keeping my hands free while I worked on it), with JWS and JWE, JWTs provide a powerful, secure solution to many different problems.
Matías Woloski (for reviewing the hard parts of this work), Martín Gontovnikas (for putting
up with my requests and doing everything to make work amenable), Bárbara Mercedes Muñoz
Cruzado (for making everything look nice), Alejo Fernández and Víctor Fernández (for doing 1.2 What problem does it solve?
the frontend and backend work to distribute this handbook), Sergio Fruto (for going out of his
way to help teammates), Federico Jack (for keeping everything running and still finding the time
Although the main purpose of JWTs is to transfer claims between two parties, arguably the most
to listen to each and everyone).
important aspect of this is the standardization effort in the form of a simple, optionally validated
and/or encrypted, container format. Ad hoc solutions to this same problem have been implemented
both privately and publicly in the past. Older standards8 for establishing claims about certain
parties are also available. What JWT brings to the table is a simple, useful, standard container
format.
Although the definition given is a bit abstract so far, it is not hard to imagine how they can be used:
login systems (although other uses are possible). We will take a closer look at practical applications
in chapter 2. Some of these applications include:
• Authentication
• Authorization
• Federated identity
• Client-side sessions (“stateless” sessions)
• Client-side secrets

1.3 A little bit of history


The JSON Object Signing and Encryption group (JOSE) was formed in the year 20119 . The
group’s objective was to “standardize the mechanism for integrity protection (signature and MAC)
and encryption as well as the format for keys and algorithm identifiers to support interoperability of
security services for protocols that use JSON ”. By year 2013 a series of drafts, including a cookbook
6 https://tools.ietf.org/html/rfc7515
7 https://tools.ietf.org/html/rfc7516
8 https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language
9 https://datatracker.ietf.org/wg/jose/history/

4 6
Chapter 2

Practical Applications

Before taking a deep dive into the structure and construction of a JWT, we will take a look at
several practical applications. This chapter will give you a sense of the complexity (or simplicity)
of common JWT-based solutions used in the industry today. All code is available from public
repositories1 for your convenience. Be aware that the following demonstrations are not meant to be
used in production. Test cases, logging, and security best practices are all essential for production-
ready code. These samples are for educational purposes only and thus remain simple and to the Figure 2.2: Signature Stripping
point.

2.1.1.2 Cross-Site Request Forgery (CSRF)


2.1 Client-side/Stateless Sessions
Cross-site request forgery attacks attempt to perform requests against sites where the user is logged
The so-called stateless sessions are in fact nothing more than client-side data. The key aspect of in by tricking the user’s browser into sending a request from a different site. To accomplish this,
this application lies in the use of signing and possibly encryption to authenticate and protect the a specially crafted site (or item) must contain the URL to the target. A common example is an
contents of the session. Client-side data is subject to tampering. As such it must be handled with <img> tag embedded in a malicious page with the src pointing to the attack’s target. For instance:
great care by the backend. <!-- This is embedded in another domain's site -->
JWTs, by virtue of JWS and JWE, can provide various types of signatures and encryption. Sig- <img src="http://target.site.com/add-user?user=name&grant=admin">
natures are useful to validate the data against tampering. Encryption is useful to protect the data The above <img> tag will send a request to target.site.com every time the page that contains it
from being read by third parties. is loaded. If the user had previously logged in to target.site.com and the site used a cookie to
Most of the time sessions need only be signed. In other words, there is no security or privacy keep the session active, this cookie will be sent as well. If the target site does not implement any
concern when data stored in them is read by third parties. A common example of a claim that CSRF mitigation techniques, the request will be handled as a valid request on behalf of the user.
can usually be safely read by third parties is the sub claim (“subject”). The subject claim usually JWTs, like any other client-side data, can be stored as cookies.
identifies one of the parties to the other (think of user IDs or emails). It is not a requirement that
this claim be unique. In other words, additional claims may be required to uniquely identify a user.
This is left to the users to decide.
A claim that may not be appropriately left in the open could be an “items” claim representing a
user’s shopping cart. This cart might be filled with items that the user is about to purchase and
1 https://github.com/auth0/jwt-handbook-samples

8 10
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpdGVtcyI6WzAsMiw0XSwiaWF0IjoxNDkzMTM5NjU5LCJleHAiOjE0OTMxNDMyNTl9.
932ZxtZzy1qhLXs932hd04J58Ihbg5_g_rIrj-Z16Js
To render the items in the cart, the frontend only needs to retrieve it from its cookie:
function populateCart() {
const cartElem = $('#cart');
cartElem.empty();

const cartToken = Cookies.get('cart');


if(!cartToken) {
return;
}

const cart = jwt_decode(cartToken).items;

cart.forEach(itemId => {
const name = items.find(item => item.id == itemId).name;
cartElem.append(`<li>${name}</li>`);
});
}
Note that the frontend does not check the signature, it simply decodes the JWT so it can display
its contents. The actual checks are performed by the backend. All JWTs are verified.
Figure 2.4: Persistent Cross Site Scripting
Here is the backend check for the validity of the cart JWT implemented as an Express middleware:
function cartValidator(req, res, next) {
if(!req.cookies.cart) {
req.cart = { items: [] };
} else {
try {
req.cart = {
items: jwt.verify(req.cookies.cart,
process.env.AUTH0_CART_SECRET,
cartVerifyJwtOptions).items
};
} catch(e) {
req.cart = { items: [] };
}
}

next();
}
When items are added, the backend constructs a new JWT with the new item in it and a new
Figure 2.5: Reflective Cross Site Scripting signature:
app.get('/protected/add_item', idValidator, cartValidator, (req, res) => {

12 14
scope: 'openid profile purchase', 2.2.1 Access and Refresh Tokens
responseType: 'id_token token',
redirectUri: 'http://localhost:3000/auth/', Access and refresh tokens are two types of tokens you will see a lot when analyzing different federated
responseMode: 'form_post' identity solutions. We will briefly explain what they are and how they help in the context of
}); authentication and authorization.
Both concepts are usually implemented in the context of the OAuth2 specification10 . The OAuth2
//(...)
spec defines a series of steps necessary to provide access to resources by separating access from
ownership (in other words, it allows several parties with different access levels to access the same
$('#login-button').on('click', function(event) {
resource). Several parts of these steps are implementation defined. That is, competing OAuth2
auth0.authorize();
implementations may not be interoperable. For instance, the actual binary format of the tokens is
});
not specified. Their purpose and functionality is.
The audience claim must match the one setup for your API endpoint using the Auth0 dashboard.
Access tokens are tokens that give those who have them access to protected resources. These
The Auth0 authentication and authorization server displays a login screen with our settings and tokens are usually short-lived and may have an expiration date embedded in them. They may also
then redirects back to our application at a specific path with the tokens we requested. These are carry or be associated with additional information (for instance, an access token may carry the IP
handled by our backend which simply sets them as cookies: address from which requests are allowed). This additional data is implementation defined.
app.post('/auth', (req, res) => { Refresh tokens, on the other hand, allow clients to request new access tokens. For instance,
res.cookie('access_token', req.body.access_token, { after an access token has expired, a client may perform a request for a new access token to the
httpOnly: true, authorization server. For this request to be satisfied, a refresh token is required. In contrast to
maxAge: req.body.expires_in * 1000 access tokens, refresh tokens are usually long-lived.
}); 10 https://tools.ietf.org/html/rfc6749#section-1.4
res.cookie('id_token', req.body.id_token, {
maxAge: req.body.expires_in * 1000
});
res.redirect('/');
});
Implementing CSRF mitigation techniques is left as an exercise for the reader. The full example
for this code can be found in the samples/stateless-sessions directory.

2.2 Federated Identity


Federated identity7 systems allow different, possibly unrelated, parties to share authentication and
authorization services with other parties. In other words, a user’s identity is centralized. There are
several solutions for federated identity management: SAML8 and OpenID Connect9 are two of the
most common ones. Certain companies provide specialized products that centralize authentication
and authorization. These may implement one of the standards mentioned above or use something
completely different. Some of these companies use JWTs for this purpose.
The use of JWTs for centralized authentication and authorization varies from company to company,
but the essential flow of the authorization process is:
7 https://auth0.com/blog/2015/09/23/what-is-and-how-does-single-sign-on-work/
8 http://saml.xml.org/saml-specifications
9 https://openid.net/connect/

16 18
to differentiate access levels to a resource, can carry an expiration date, and are signed to avoid $('#login-button').on('click', function(event) {
validation queries against the authorization server. Several federated identity providers issue access auth0.authorize({
tokens in JWT format. prompt: 'none'
});
JWTs may also be used for refresh tokens. There is less reason to use them for this purpose, though.
});
As refresh tokens require access to the authorization server, most of the time a simple UUID will
suffice, as there is no need for the token to carry a payload (it may be signed, though). Note the use of the prompt: 'none' parameter for the authorize call. The authorize call redi-
rects the user to the authorization server. With the none parameter, if the user has already given
authorization for an app to use his or her credentials for access to a protected resource, the autho-
2.2.3 JWTs and OpenID Connect rization server will simply redirect back to the application. This looks to the user as if he were
already logged-in in the app.
OpenID Connect11 is a standardization effort to bring typical use cases of OAuth2 under a common,
well-defined spec. As many details behind OAuth2 are left to the choice of implementers, OpenID In our example, there are two apps: app1.com and app2.com. Once a user has authorized both
Connect attempts to provide proper definitions for the missing parts. Specifically, OpenID Connect apps (which happens only once: the first time the user logs-in), any subsequent logins to any of
defines an API and data format to perform OAuth2 authorization flows. Additionally, it provides both apps will also allow the other app to login without presenting any login screens.
an authentication layer built on top of this flow. The data format chosen for some of its parts is To test this, see the README file for the example located in the
JSON Web Token. In particular, the ID token12 is a special type of token that carries information samples/single-sign-on-federated-identity directory to set up both applications and run
about the authenticated user. them. Once both are running, go to app1.com:300016 and app2.com:300117 and login. Then logout
from both apps. Now attempt to login to one of them. Then go back to the other one and login.
You will notice the login screen will be absent in both apps. The authorization server remembers
2.2.3.1 OpenID Connect Flows and JWTs
previous logins and can issue new access tokens when requested by any of those apps. Thus, as
long as the user has an authorization server session, he or she is already logged-in to both apps.
OpenID Connect defines several flows which return data in different ways. Some of this data may
be in JWT format. Implementing CSRF mitigation techniques is left as en exercise for the reader.
• Authorization flow: the client requests an authorization code to the authorization endpoint
(/authorize). This code can be used againt the token endpoint (/token) to request an ID
token (in JWT format), an access token or a refresh token.
• Implicit flow: the client requests tokens directly from the authorization endpoint
(/authorize). The tokens are specified in the request. If an ID token is requested, is is
returned in JWT format.
• Hybrid flow: the client requests both an authorization code and certain tokens from the
authorization endpoint (/authorize). If an ID token is requested, it is returned in JWT
format. If an ID token is not requested at this step, it may later by requested directly from
the token endpoint (/token).

2.2.4 Example

For this example we will use Auth013 as the authorization server. Auth0 allows for different identity
providers to be set dynamically. In other words, whenever a user attempts to login, changes made in
the authorization server may allow users to login with different identity providers (such as Twitter,
Facebook, etc). Applications need not commit to specific providers once deployed. So our example
11 https://openid.net/connect/
12 http://openid.net/specs/openid-connect-core-1_0.html#IDToken
13 https://auth0.com 16 http://app1.com:3000
17 http://app2.com:3001

20 22
{ which this JWT is considered invalid. Some implementations may allow for a certain skew
"alg": "HS256", between clocks (by considering this JWT to be valid for a few minutes after the expiration
"typ": "JWT" date).
}
• nbf: from not before (time). The opposite of the exp claim. A number representing a specific
The decoded payload is: date and time in the format “seconds since epoch” as defined by POSIX7 . This claim sets
the exact moment from which this JWT is considered valid. The current time and date must
{
be equal to or later than this date and time. Some implementations may allow for a certain
"sub": "1234567890",
skew.
"name": "John Doe",
"admin": true • iat: from issued at (time). A number representing a specific date and time (in the same
} format as exp and nbf ) at which this JWT was issued.
And the secret required for verifying the signature is secret. • jti: from JWT ID. A string representing a unique identifier for this JWT. This claim may be
used to differentiate JWTs with other similar content (preventing replays, for instance). It is
JWT.io4 is an interactive playground for learning more about JWTs. Copy the token
up to the implementation to guarantee uniqueness.
from above and see what happens when you edit it.
As you may have noticed, all names are short. This complies with one of the design requirements:
to keep JWTs as small as possible.
3.1 The Header String or URI: according to the JWT spec, a URI is interpreted as any string containing
a : character. It is up to the implementation to provide valid values.
Every JWT carries a header (also known as the JOSE header) with claims about itself. These
claims establish the algorithms used, whether the JWT is signed or encrypted, and in general, how
to parse the rest of the JWT. 3.2.2 Public and Private Claims
According to the type of JWT in question, more fields may be mandatory in the header. For instance, All claims that are not part of the registered claims section are either private or public claims.
encrypted JWTs carry information about the cryptographic algorithms used for key encryption and
content encryption. These fields are not present for unencrypted JWTs. • Private claims: are those that are defined by users (consumers and producers) of the JWTs.
In other words, these are ad hoc claims used for a particular case. As such, care must be
The only mandatory claim for an unencrypted JWT header is the alg claim: taken to prevent collisions.
• alg: the main algorithm in use for signing and/or decrypting this JWT. • Public claims: are claims that are either registered with the IANA JSON Web Token Claims
For unencrypted JWTs this claim must be set to the value none. registry8 (a registry where users can register their claims and thus prevent collisions), or named
using a collision resistant name (for instance, by prepending a namespace to its name).
Optional header claims include the typ and cty claims:
In practice, most claims are either registered claims or private claims. In general, most JWTs are
• typ: the media type5 of the JWT itself. This parameter is only meant to be used as a help issued with a specific purpose and a clear set of potential users in mind. This makes the matter of
for uses where JWTs may be mixed with other objects carrying a JOSE header. In practice, picking collision resistant names simple.
this rarely happens. When present, this claim should be set to the value JWT.
Just as in the JSON parsing rules, duplicate claims (duplicate JSON keys) are handled by keeping
• cty: the content type. Most JWTs carry specific claims plus arbitrary data as part of their only the last occurrence as the valid one. The JWT spec also makes it possible for implementations
payload. For this case, the content type claim must not be set. For instances where the to consider JWTs with duplicate claims as invalid. In practice, if you are not sure about the
payload is a JWT itself (a nested JWT), this claim must be present and carry the value JWT. implementation that will handle your JWTs, take care to avoid duplicate claims.
This tells the implementation that further processing of the nested JWT is required. Nested
7 http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_15
JWTs are rare, so the cty claim is rarely present in headers. 8 https://tools.ietf.org/html/rfc7519#section-10.1

So, for unencrypted JWTs, the header is simply:


4 https://jwt.io
5 http://www.iana.org/assignments/media-types/media-types.xhtml

24 26
4. Encode the byte array using the Base64-URL algorithm, removing trailing equal signs (=).
5. Concatenate the resulting strings, putting first the header, followed by a “.” character,
followed by the payload.
Validation of both the header and the payload (with respect to the presence of required claims and
the correct use of each claim) must be performed before encoding.

Chapter 4

JSON Web Signatures

JSON Web Signatures are probably the single most useful feature of JWTs. By combining a simple
data format with a well-defined series of signature algorithms, JWTs are quickly becoming the ideal
Figure 3.1: Compact Unsecured JWT Generation format for safely sharing data between clients and intermediaries.
The purpose of a signature is to allow one or more parties to establish the authenticity of the JWT.
3.4.1 Sample Code Authenticity in this context means the data contained in the JWT has not been tampered with. In
other words, any party that can perform a signature check can rely on the contents provided by
// URL-safe variant of Base64 the JWT. It is important to stress that a signature does not prevent other parties from reading the
function b64(str) { contents inside the JWT. This is what encryption is meant to do, and we will talk about that later
return new Buffer(str).toString('base64') in chapter 5.
.replace(/=/g, '') The process of checking the signature of a JWT is known as validation or validating a token. A
.replace(/\+/g, '-') token is considered valid when all the restrictions specified in its header and payload are satisfied.
.replace(/\//g, '_'); This is a very important aspect of JWTs: implementations are required to check a JWT up to the
} point specified by both its header and its payload (and, additionally, whatever the user requires).
So, a JWT may be considered valid even if it lacks a signature (if the header has the alg claim
function encode(h, p) { set to none). Additionally, even if a JWT has a valid signature, it may be considered invalid for
const headerEnc = b64(JSON.stringify(h)); other reasons (for instance, it may have expired, according to the exp claim). A common attack
const payloadEnc = b64(JSON.stringify(p)); against signed JWTs relies on stripping the signature and then changing the header to make it an
return `${headerEnc}.${payloadEnc}`; unsecured JWT. It is the responsibility of the user to make sure JWTs are validated according to
} their own requirements.
The full example is in file coding.js of the accompanying sample code. Signed JWTs are defined in the JSON Web Signature spec, RFC 75151 .

3.5 Parsing an Unsecured JWT 4.1 Structure of a Signed JWT


To arrive at the JSON representation from the compact serialization form, perform the following We have covered the structure of a JWT in chapter 3. We will review it here and take special note
steps: of its signature component.
1. Find the first period “.” character. Take the string before it (not including it.) 1 https://tools.ietf.org/html/rfc7515

28 30
4.1.1 Algorithm Overview for Compact Serialization key. In this specific variation of the algorithm, the private key can be used both to create a signed
message and to verify its authenticity. The public key, in contrast, can only be used to verify the
In order to discuss these algorithms in general, let’s first define some functions in a JavaScript 2015 authenticity of a message. Thus, this scheme allows for the secure distribution of a one-to-many
environment: message. Receiving parties can verify the authenticity of a message by keeping a copy of the public
key associated with it, but they cannot create new messages with it. This allows for different usage
• base64: a function that receives an array of octets and returns a new array of octets using
scenarios than shared-secret signing schemes such as HMAC. With HMAC + SHA-256, any party
the Base64-URL algorithm.
that can verify a message can also create new messages. For example, if a legitimate user turned
• utf8: a function that receives text in any encoding and returns an array of octets with UTF-8
malicious, he or she could modify messages without the other parties noticing. With a public-key
encoding.
scheme, a user who turned malicious would only have the public key in his or her possession and
• JSON.stringify: a function that takes a JavaScript object and serializes it to string form
so could not create new signed messages with it.
(JSON).
• sha256: a function that takes an array of octets and returns a new array of octets using the
SHA-256 algorithm.
• hmac: a function that takes a SHA function, an array of octets and a secret and returns a
new array of octets using the HMAC algorithm.
• rsassa: a function that takes a SHA function, an array of octets and the private key and
returns a new array of octets using the RSASSA algorithm.
For HMAC-based signing algorithms:
const encodedHeader = base64(utf8(JSON.stringify(header)));
const encodedPayload = base64(utf8(JSON.stringify(payload)));
const signature = base64(hmac(`${encodedHeader}.${encodedPayload}`,
secret, sha256));
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;
For public-key signing algorithms:
const encodedHeader = base64(utf8(JSON.stringify(header)));
const encodedPayload = base64(utf8(JSON.stringify(payload)));
const signature = base64(rsassa(`${encodedHeader}.${encodedPayload}`,
privateKey, sha256));
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;

Figure 4.2: One-to-many signing

32 34
4.1.3 JWS Header Claims "signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDx
w5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
JWS allows for special use cases that force the header to carry more claims. For instance, for }
public-key signing algorithms, it is possible to embed the URL to the public key as a claim. What ]
follows is the list of registered header claims available for JWS tokens. All of these claims are in }
addition to those available for unsecured JWTs, and are optional depending on how the signed JWT
This example encodes two signatures for the same payload: a RS256 signature and an ES256
is meant to be used.
signature.
• jku: JSON Web Key (JWK) Set URL. A URI pointing to a set of JSON-encoded public keys
used to sign this JWT. Transport security (such as TLS for HTTP) must be used to retrieve
the keys. The format of the keys is a JWK Set (see chapter 6). 4.1.4.1 Flattened JWS JSON Serialization

• jwk: JSON Web Key. The key used to sign this JWT in JSON Web Key format (see chapter JWS JSON serialization defines a simplified form for JWTs with only a single signature. This form
6). is known as flattened JWS JSON serialization. Flattened serialization removes the signatures array
• kid: Key ID. A user-defined string representing a single key used to sign this JWT. This and puts the elements of a single signature at the same level as the payload element.
claim is used to signal key signature changes to recipients (when multiple keys are used). For example, by removing one of the signatures from the previous example, a flattened JSON
• x5u: X.509 URL. A URI pointing to a set of X.509 (a certificate format standard) public serialization object would be:
certificates encoded in PEM form. The first certificate in the set must be the one used to {
sign this JWT. The subsequent certificates each sign the previous one, thus completing the "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQog
certificate chain. X.509 is defined in RFC 52807 . Transport security is required to transfer Imh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
the certificates. "protected": "eyJhbGciOiJFUzI1NiJ9",
• x5c: X.509 certificate chain. A JSON array of X.509 certificates used to sign this JWS. Each "header": { "kid": "e9bc097a-ce51-4036-9562-d2ade882db0d" },
certificate must be the Base64-encoded value of its DER PKIX representation. The first "signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFC
certificate in the array must be the one used to sign this JWT, followed by the rest of the gfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
certificates in the certificate chain. }

• x5t: X.509 certificate SHA-1 fingerprint. The SHA-1 fingerprint of the X.509 DER-encoded
certificate used to sign this JWT.
4.2 Signing and Validating Tokens
• x5t#S256: Identical to x5t, but uses SHA-256 instead of SHA-1.
• typ: Identical to the typ value for unencrypted JWTs, with additional values “JOSE” and The algorithms used for signing and validating tokens are explained in detail in chapter 7. Using
“JOSE+JSON” used to indicate compact serialization and JSON serialization, respectively. signed JWTs is simple enough in practice that you could apply the concepts explained so far to use
This is only used in cases where similar JOSE-header carrying objects are mixed with this them effectively. Furthermore, there are good libraries you can use to implement them conveniently.
JWT in a single container. We will go over the required and recommended algorithms using the most popular of these libraries
for JavaScript. Examples of other popular languages and libraries can be found in the accompanying
• crit: from critical. An array of strings with the names of claims that are present in this same code.
header used as implementation-defined extensions that must be handled by parsers of this
JWT. It must either contain the names of claims or not be present (the empty array is not a The following examples all make use of the popular jsonwebtoken JavaScript library.
valid value). import jwt from 'jsonwebtoken'; //var jwt = require('jsonwebtoken');

const payload = {
4.1.4 JWS JSON Serialization sub: "1234567890",
name: "John Doe",
The JWS spec defines a different type of serialization format that is not compact. This representa- admin: true
tion allows for multiple signatures in the same signed JWT. It is known as JWS JSON Serialization. };
7 https://tools.ietf.org/html/rfc5280

36 38
// Never forget to make this explicit to prevent JWS JWE
// signature stripping attacks.
algorithms: ['RS256'], Producer Private-key Public-key
}); Consumer Public-key Private-key

4.2.3 ES256: ECDSA using P-256 and SHA-256

ECDSA algorithms also make use of public keys. The math behind the algorithm is different,
though, so the steps to generate the keys are different as well. The “P-256” in the name of this
algorithm tells us exactly which version of the algorithm to use (more details about this in chapter
7). We can use OpenSSL to generate the key as well:
# Generate a private key (prime256v1 is the name of the parameters used
# to generate the key, this is the same as P-256 in the JWA spec).
openssl ecparam -name prime256v1 -genkey -noout -out ecdsa_private_key.pem
# Derive the public key from the private key
openssl ec -in ecdsa_private_key.pem -pubout -out ecdsa_public_key.pem
If you open these files you will note that there is much less data in them. This is one of the benefits
of ECDSA over RSA (more about this in chapter 7). The generated files are in PEM format as
well, so simply pasting them in your source will suffice.
// You can get this from private_key.pem above.
const privateEcdsaKey = `<YOUR-PRIVATE-ECDSA-KEY>`;

const signed = jwt.sign(payload, privateEcdsaKey, {


algorithm: 'ES256',
expiresIn: '5s'
});
// You can get this from public_key.pem above.
const publicEcdsaKey = `<YOUR-PUBLIC-ECDSA-KEY>`;

const decoded = jwt.verify(signed, publicEcdsaKey, {


// Never forget to make this explicit to prevent
// signature stripping attacks.
algorithms: ['ES256'],
});
Refer to chapter 2 for practical applications of these algorithms in the context of JWTs.

40 42
In the case of JWE, couldn’t we distribute the private-key to every party that wants to • ECDH-ES + AES-256 Key Wrap
send data to a consumer? Thus if a consumer can decrypt the data, he or she can be
Some of these algorithms require additional header parameters.
sure that it is also valid (because one cannot change data that cannot be decrypted).
Technically, it would be possible, but it wouldn’t make sense. Sharing the private-key is equivalent
to sharing the secret. So sharing the private-key in essence turns the scheme into a shared secret 5.1.1.1 Key Management Modes
scheme, without the actual benefits of public-keys (remember public-keys can be derived from
private-keys). The JWE specification defines different key management modes. These are, in essence, ways in
which the key used to encrypt the payload is determined. In particular, the JWE spec describes
For this reason encrypted JWTs are sometimes nested: an encrypted JWT serves as the container these modes of key management:
for a signed JWT. This way you get the benefits of both.
• Key Wrapping: the Content Encryption Key (CEK) is encrypted for the intended recipient
Note that all of this applies in situations where consumers are different entities from using a symmetric encryption algorithm.
producers. If the producer is the same entity that consumes the data, then a shared-
secret encrypted JWT provides the same guarantees as an encrypted and signed JWT.
JWE encrypted JWTs, regardless of having a nested signed JWT in them or not, carry
an authentication tag. This tag allows JWE JWTs to be validated. However, due to the
issues mentioned above, this signature does not apply for the same use cases as JWS
signatures. The purpose of this tag is to prevent padding oracle attacks1 or ciphertext
manipulation.

5.1 Structure of an Encrypted JWT


In contrast to signed and unsecured JWTs, encrypted JWTs have a different compact representation
(newlines inserted for readability):
Figure 5.2: Key wrapping
eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.
UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_
• Key Encryption: the CEK is encrypted for the intended recipient using an asymmetric
i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-
encryption algorithm.
YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8Otv
zlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-
cFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.
AxY8DCtDaGlsbGljb3RoZQ.
KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.
9hH0vgRfYgPnAHOd8stkvw
Although it may be hard to see in the example above, JWE Compact Serialization has five elements.
As in the case of JWS, these elements are separated by dots, and the data contained in them is
Base64-encoded.
The five elements of the compact representation are, in order:
1. The protected header: a header analogous to the JWS header.
2. The encrypted key: a symmetric key used to encrypt the ciphertext and other encrypted
data. This key is derived from the actual encryption key specified by the user and thus is
encrypted by it. Figure 5.3: Key encryption
1 https://en.wikipedia.org/wiki/Padding_oracle_attack
• Direct Key Agreement: a key agreement algorithm is used to pick the CEK.

44 46
empty JWE Encryption Key means the algorithm makes use of an externally provided key to either base64(initializationVector) + '.' + // Step 4
directly decrypt the data (Direct Encryption) or compute the actual CEK (Direct Key Agreement). base64(ciphertext) + '.' + // Step 6
base64(authenticationTag) // Step 6

5.1.2 Content Encryption Algorithms


5.1.5 JWE JSON Serialization
The following are the content encryption algorithms, that is, the ones used to actually encrypt the
payload: In addition to compact serialization, JWE also defines a non-compact JSON representation. This
representation trades size for flexibility, allowing, amongst other things, encryption of the content
• AES CBC + HMAC SHA: AES 128 to 256-bits with Cipher Block Chaining and HMAC
for multiple recipients by using several public-keys at the same time. This is analogous to the
+ SHA-256 to 512 for validation.
multiple signatures allowed by JWS JSON Serialization.
• AES GCM: AES 128 to 256 using Galois Counter Mode.
JWE JSON Serialization is the printable text encoding of a JSON object with the following mem-
Of these, only two are required: AES-128 CBC + HMAC SHA-256, and AES-256 CBC + HMAC
bers:
SHA-512. The AES-128 and AES-256 variants using GCM are recommended.
• protected: Base64-encoded JSON object of the header claims to be protected (validated,
These algorithms are explained in detail in chapter 7.
not encrypted) by this JWE JWT. Optional. At least this element or the unprotected header
must be present.
5.1.3 The Header • unprotected: header claims that are not protected (validated) as a JSON object (not Base64-
encoded). Optional. At least this element or the protected header must be present.
Just like the header for JWS and unsecured JWTs, the header carries all the necessary information
for the JWT to be correctly processed by libraries. The JWE specification adapts the meanings of • iv: Base64 string of the initialization vector. Optional (only present when required by the
the registered claims defined in JWS to its own use, and adds a few claims of its own. These are algorithm).
the new and modified claims: • aad: Additional Authenticated Data. Base64 string of the additional data that is protected
• alg: identical to JWS, except it defines the algorithm to be used to encrypt and decrypt the (validated) by the encryption algorithm. If no AAD is supplied in the encryption step, this
Content Encryption Key (CEK). In other words, this algorithm is used to encrypt the actual member must be absent.
key that is later used to encrypt the content. • ciphertext: Base64-encoded string of the encrypted data.
• enc: the name of the algorithm used to encrypt the content using the CEK. • tag: Base64 string of the authentication tag generated by the encryption algorithm.
• zip: a compression algorithm to be applied to the encrypted data before encryption. This • recipients: a JSON array of JSON objects, each containing the necessary information for
parameter is optional. When it is absent, no compression is performed. A usual value for this decryption by each recipient.
is DEF, the common deflate algorithm2 .
The following are the members of the objects in the recipients array:
• jku: identical to JWS, except in this case the claim points to the public-key used to encrypt
the CEK. • header: a JSON object of unprotected header claims. Optional.
• encrypted_key: Base64-encoded JWE Encrypted Key. Only present when a JWE En-
• jkw: identical to JWS, except in this case the claim points to the public-key used to encrypt crypted Key is used.
the CEK.
The actual header used to decrypt a JWE JWT for a recipient is constructed from the union of
• kid: identical to JWS, except in this case the claim points to the public-key used to encrypt each header present. No repeated claims are allowed.
the CEK.
The format of the encrypted keys is described in chapter 6 (JSON Web Keys).
• x5u: identical to JWS, except in this case the claim points to the public-key used to encrypt
the CEK. The following example is taken from RFC 7516 (JWE):

• x5c: identical to JWS, except in this case the claim points to the public-key used to encrypt {
the CEK. "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",
"unprotected": { "jku":"https://server.example.com/keys.jwks" },
2 https://tools.ietf.org/html/rfc1951
"recipients":[

48 50
5.1.5.1 Flattened JWE JSON Serialization 5.2.2 AES-128 Key Wrap (Key) + AES-128 GCM (Content)

As with JWS, JWE defines a flat JSON serialization. This serialization form can only be used for AES-128 Key Wrap and AES-128 GCM are symmetric key algorithms. This means that the same
a single recipient. In this form, the recipients array is replaced by a header and encrypted_key key is required for both encryption and decryption. The key for “example-1” that we generated
pair or elements (i.e., the keys of a single object of the recipients array take its place). before is one such key. In AES-128 Key Wrap, this key is used to wrap a randomly generated key,
which is then used to encrypt the content using the AES-128 GCM algorithm. It would also be
This is the flattened representation of the example from the previous section resulting from only
possible to use this key directly (Direct Encryption mode).
including the first recipient:
function encrypt(key, options, plaintext) {
{
return jose.JWE.createEncrypt(options, key)
"protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",
.update(plaintext)
"unprotected": { "jku":"https://server.example.com/keys.jwks" },
.final();
"header": { "alg":"RSA1_5","kid":"2011-04-29" },
}
"encrypted_key":
"UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-
function a128gcm(compact) {
kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKx
const key = keystore.get('example-1');
GHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3
const options = {
YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPh
format: compact ? 'compact' : 'general',
cCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPg
contentAlg: 'A128GCM'
wCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A",
};
"iv": "AxY8DCtDaGlsbGljb3RoZQ",
"ciphertext": "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY",
return encrypt(key, options, JSON.stringify(payload));
"tag": "Mz-VPPyU4RlcuYv1IwIvzw"
}
}
The node-jose library works primarily with promises6 . The object returned by a128gcm is a
promise. The createEncrypt function can encrypt whatever content is passed to it. In other
5.2 Encrypting and Decrypting Tokens words, it is not necessary for the content to be a JWT (though most of the time it will be). It is
for this reason that JSON.stringify must be called before passing the data to that function.
The following examples show how to perform encryption using the popular node-jose5 library. This
library is a bit more complex than jsonwebtoken (used for the JWS examples), as it covers much 5.2.3 RSAES-OAEP (Key) + AES-128 CBC + SHA-256 (Content)
more ground.
The only thing that changes between invocations of the createEncrypt function are the options
passed to it. Therefore, it is just as easy to use a public/private-key pair. Rather than passing
5.2.1 Introduction: Managing Keys with node-jose the symmetric key to createEncrypt, one simply passes either the public or the private-key (for
encryption only the public key is required, though this one can be derived from the private key).
For the purposes of the following examples, we will need to use encryption keys in various forms.
For readability purposes, we simply use the private key, but in practice the public key will most
This is managed by node-jose through a keystore. A keystore is an object that manages
likely be used in this step.
keys. We will generate and add a few keys to our keystore so that we can use them later in the
examples. You might recall from JWS examples that such an abstraction was not required for the function encrypt(key, options, plaintext) {
jsonwebtoken library. The keystore abstraction is an implementation detail of node-jose. You return jose.JWE.createEncrypt(options, key)
may find other similar abstractions in other languages and libraries. .update(plaintext)
.final();
To create an empty keystore and add a few keys of different types:
}
// Create an empty keystore
const keystore = jose.JWK.createKeyStore(); function rsa(compact) {
5 https://github.com/cisco/node-jose#basics 6 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

52 54
const signingPromise = jose.JWS.createSign(signingKey)
.update(JSON.stringify(payload))
.final();

const promise = new Promise((resolve, reject) => {

signingPromise.then(result => {
const options = { Chapter 6
format: compact ? 'compact' : 'general',
contentAlg: 'A128CBC-HS256'
};
resolve(encrypt(encryptionKey, options, JSON.stringify(result)));
}, error => {
JSON Web Keys (JWK)
reject(error);
});

});
To complete the picture of JWT, JWS, and JWE we now come to the JSON Web Key (JWK) spec-
ification. This specification deals with the different representations for the keys used for signatures
return promise;
and encryption. Although there are established representations for all keys, the JWK specification
}
aims at providing a unified representation for all keys supported in the JSON Web Algorithms
As can be seen in the example above, node-jose can also be used for signing. There is nothing (JWA) specification. A unified representation format for keys allows easy sharing and keeps keys
precluding the use of other libraries (such as jsonwebtoken) for that purpose. However, given the independent from the intricacies of other key exchange formats.
necessity of node-jose, there is no point in adding dependencies and using inconsistent APIs.
JWS and JWE do support a different type of key format: X.509 certificates. These are quite
Performing the signing step first is only possible because JWE mandates authenticated common and can carry more information than a JWK. X.509 certificates can be embedded in
encryption. In other words, the encryption algorithm must also perform the signing JWKs, and JWKs can be constructed from them.
step. The reasons JWS and JWE can be combined in a useful way, in spite of JWE’s
Keys are specified in different header claims. Literal JWKs are put under the jwk claim. The jku
authentication, were described at the beginning of chapter 5. For other schemes (i.e.,
claim, on the other hand, can point to a set of keys stored under a URL. Both of these claims are
for general encryption + signature), the norm is to first encrypt, then sign. This is to
in JWK format.
prevent manipulation of the ciphertext that can result in encryption attacks. It is also
the reason that JWE mandates the presence of an authentication tag. A sample JWK:
{
5.2.6 Decryption "kty":"EC",
"crv":"P-256",
Decryption is as simple as encryption. As with encryption, the payload must be converted between "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
different data formats explicitly. "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE",
// Decryption test "use":"enc",
a128gcm(true).then(result => { "kid":"1"
jose.JWE.createDecrypt(keystore.get('example-1')) }
.decrypt(result)
.then(decrypted => {
decrypted.payload = JSON.parse(decrypted.payload);
console.log(`Decrypted result: ${JSON.stringify(decrypted)}`);
}, error => {
console.log(error);

56 58
6.1.1 JSON Web Key Set
0A 17 R 34 i 51 z
The JWK spec admits groups of keys. These are known as “JWK Sets”. These sets carry more 1B 18 S 35 j 52 0
than one key. The meaning of the keys as a group and the meaning of the order of these keys is 2C 19 T 36 k 53 1
user defined. 3D 20 U 37 l 54 2
4E 21 V 38 m 55 3
A JSON Web Key Set is simply a JSON object with a keys member. This member is a JSON array 5F 22 W 39 n 56 4
of JWKs. 6G 23 X 40 o 57 5
Sample JWK Set: 7H 24 Y 41 p 58 6
8I 25 Z 42 q 59 7
{ 9J 26 a 43 r 60 8
"keys": [ 10 K 27 b 44 s 61 9
{ 11 L 28 c 45 t 62 +
"kty":"EC", 12 M 29 d 46 u 63 /
"crv":"P-256", 13 N 30 e 47 v
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 14 O 31 f 48 w (pad) =
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 15 P 32 g 49 x
"use":"enc", 16 Q 33 h 50 y
"kid":"1"
},
In Base64 encoding, each character represents 6 bits of the original data. Encoding is performed in
{ groups of four encoded characters. So, 24 bits of original data are taken together and encoded as
"kty":"RSA", four Base64 characters. Since the original data is expected to be a sequence of 8-bit values, the 24
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx bits are formed by concatenating three 8-bit values from left to right.
4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs
tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2 Base64 encoding:
QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI 3 x 8-bit values -> 24-bit concatenated data -> 4 x 6-bit characters
SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb
w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"alg":"RS256",
"kid":"2011-04-29"
}
]
}
In this example, two public-keys are available. The first one is of elliptic curve type and is limited
to encryption operations by the use claim. The second one is of RSA type and is associated with
a specific algorithm (RS256) by the alg claim. This means this second key is meant to be used for
signatures.

Figure 7.1: Base64 encoding

60 62
01011111 01010101 10101010 00111100
let concat = input[i] << 16; Extra 1 at the end:
result += (table[concat >>> (24 - 6)]); 01011111 01010101 10101010 00111100 1
N zeroes:
if(remaining > 1) { 01011111 01010101 10101010 00111100 10000000 ...0...
concat |= input[i + 1] << 8; Padded message:
result += table[(concat >>> (24 - 12)) & 0x3F]; 01011111 01010101 10101010 00111100 10000000 ...0... 00000000 00100000

if(remaining > 2) {
concat |= input[i + 2];
result += table[(concat >>> (24 - 18)) & 0x3F] +
table[concat & 0x3F];
} else {
result += table[(concat >>> (24 - 18)) & 0x3F] + "=";
}
} else { Figure 7.2: SHA padding
result += table[(concat >>> (24 - 12)) & 0x3F] + "==";
}
} A simple implementation in JavaScript could be:
function padMessage(message) {
return result;
if(!(message instanceof Uint8Array) && !(message instanceof Int8Array)) {
}
throw new Error("unsupported message container");
}
7.1.2 SHA
const bitLength = message.length * 8;
The Secure Hash Algorithm (SHA) used in the JWT specs is defined in FIPS-1802 . It is not to const fullLength = bitLength + 65; //Extra 1 + message size.
be confused with the SHA-13 family of algorithms, which have been deprecated since 2010. To let paddedLength = (fullLength + (512 - fullLength % 512)) / 32;
differentiate this family from the previous one, this family is sometimes called SHA-2. let padded = new Uint32Array(paddedLength);

The algorithms in RFC 4634 are SHA-224, SHA-256, SHA-384, and SHA-512. Of importance for for(let i = 0; i < message.length; ++i) {
JWT are SHA-256 and SHA-512. We will focus on the SHA-256 variant and explain its differences padded[Math.floor(i / 4)] |= (message[i] << (24 - (i % 4) * 8));
with regard to the other variants. }
As do many hashing algorithms, SHA works by processing the input in fixed-size chunks, applying
a series of mathematical operations and then accummulating the result by performing an operation padded[Math.floor(message.length / 4)] |= (0x80 << (24 - (message.length % 4) * 8));
with the previous iteration results. Once all fixed-size input chunks are processed, the digest is said // TODO: support messages with bitLength longer than 2^32
to be computed. padded[padded.length - 1] = bitLength;

The SHA family of algorithms were designed to avoid collisions and produce radically different return padded;
output even when the input is only slightly changed. It is for this reason they are considered secure: }
it is computationally infeasible to find collisions for different inputs, or to compute the original
input from the produced digest. The resulting padded message is then processed in 512-bit blocks. The implementation below follows
the algorithm described in the specification step by step. All operations are performed on 32-bit
The algorithm requires a series of predefined functions: integers.
2 http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
export default function sha256(message, returnBytes) {
3 https://en.wikipedia.org/wiki/SHA-1
// Initial hash values
const h_ = Uint32Array.of(

64 66
h_[4] = (e + h_[4]) >>> 0; (how many bits are processed per iteration)
h_[5] = (f + h_[5]) >>> 0; K be the secret key
h_[6] = (g + h_[6]) >>> 0; K' be the actual key used by the HMAC algorithm
h_[7] = (h + h_[7]) >>> 0; L be the length of the output of the hash function
} ipad be the byte 0x36 repeated B times
opad be the byte 0x5C repeated B times
//(...) message be the input message
} || be the concatenation function
The variable k holds a series of constants, which are defined in the specification.
HMAC(message) = H(K' XOR opad || H(K' XOR ipad || message))
The final result is in the variable h_[0..7]. The only missing step is to present it in readable form:
K' is computed from the secret key K as follows:
if(returnBytes) {
If K is shorter than B, zeroes are appended until K is of B length. The result is K'. If K is longer
const result = new Uint8Array(h_.length * 4);
than B, H is applied to K. The result is K'. If K is exactly B bytes, it is used as is (K is K').
h_.forEach((value, index) => {
const i = index * 4; Here is a sample implementation in JavaScript:
result[i ] = (value >>> 24) & 0xFF;
export default function hmac(hashFn, blockSizeBits, secret, message, returnBytes) {
result[i + 1] = (value >>> 16) & 0xFF;
if(!(message instanceof Uint8Array)) {
result[i + 2] = (value >>> 8) & 0xFF;
throw new Error('message must be of Uint8Array');
result[i + 3] = (value >>> 0) & 0xFF;
}
});
const blockSizeBytes = blockSizeBits / 8;
return result;
} else {
const ipad = new Uint8Array(blockSizeBytes);
function toHex(n) {
const opad = new Uint8Array(blockSizeBytes);
let str = (n >>> 0).toString(16);
ipad.fill(0x36);
let result = "";
opad.fill(0x5c);
for(let i = str.length; i < 8; ++i) {
result += "0";
const secretBytes = stringToUtf8(secret);
}
let paddedSecret;
return result + str;
if(secretBytes.length <= blockSizeBytes) {
}
const diff = blockSizeBytes - secretBytes.length;
let result = "";
paddedSecret = new Uint8Array(blockSizeBytes);
h_.forEach(n => {
paddedSecret.set(secretBytes);
result += toHex(n);
} else {
});
paddedSecret = hashFn(secretBytes);
return result;
}
}
Although it works, note that the implementation above is not optimal (and does not support const ipadSecret = ipad.map((value, index) => {
messages longer than 232 ). return value ^ paddedSecret[index];
});
Other variants of the SHA-2 family (such as SHA-512) simply change the size of the block processed
const opadSecret = opad.map((value, index) => {
in each iteration and alter the constants and their size. In particular, SHA-512 requires 64-bit math
return value ^ paddedSecret[index];
to be available. In other words, to turn the sample implementation above into SHA-512, a separate
});
library for 64-bit math is required (as JavaScript only supports 32-bit bitwise operations and 64-bit
floating-point math).
// HMAC(message) = H(K' XOR opad || H(K' XOR ipad || message))

68 70
const encodedHeader = base64(utf8(JSON.stringify(header))); numbers are chosen as n. For instance, if n is the result of multiplying two prime numbers8 , it is
const encodedPayload = base64(utf8(JSON.stringify(payload))); much harder to find its factors (of which those prime numbers are the only possible factors).
const signature = base64(hmac(`${encodedHeader}.${encodedPayload}`, secret, sha256));
If n were the result of multiplying two non-prime numbers, it would be much easier to
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;
find its factors. Why? Because non-prime numbers have divisors other than themselves
Verification is just as easy: (by defintion), and these divisors are in turn divisors of any number multiplied with
them. In other words, any divisor to a factor of a number is also a factor of the number.
export function jwtVerifyAndDecode(jwt, secret) {
Or, in other terms, if n has non-prime factors, it has more than two factors. So, if
if(!isString(jwt) || !isString(secret)) {
n is the result of multiplying two prime-numbers, it has exactly two factors (the least
throw new TypeError('jwt and secret must be strings');
possible number of factors without being a prime number itself). The lesser the number
}
of factors, the harder it is to find them.
const split = jwt.split('.'); When two different and big prime numbers are picked and then multiplied, the result is another
if(split.length !== 3) { big number (called a semiprime). But this big number has an added special property: it is really
throw new Error('Invalid JWT format'); hard to factor. Even the most efficient factoring algorithms, such as the general number field sieve9 ,
} cannot factor big numbers that are the result of multiplying big primes in reasonable time frames.
To give a sense of scale, in 2009 a 768-bit number (232 decimal digits) was factored10 after 2 years
const header = JSON.parse(unb64(split[0])); of work by a cluster of computers. Typical applications of RSA make use of 2048-bit or bigger
if(header.alg !== 'HS256') { numbers.
throw new Error(`Wrong algorithm: ${header.alg}`);
Shor’s algorithm11 is a special kind of factoring algorithm that could change things
}
drastically in the future. While most factoring algorithms are classical in nature (that
is, they operate on classical computers), Shor’s algorithm relies on quantum computers12 .
const jwtUnprotected = `${split[0]}.${split[1]}`;
Quantum computers take advantage of the nature of certain quantum phenomena to
const signature =
speed up several classical operations. In particular, Shor’s algorithm could speed up
b64(hmac(sha256, 512, secret, stringToUtf8(jwtUnprotected), true));
factorization, bringing its complexity into the realm of polynomial time complexity
(rather than exponential). This is much more efficient than any of the current classical
return {
algorithms. It is speculated that if such algorithm were to be runnable on a quantum
header: header,
computer, current RSA keys would become useless. A practical quantum computer as
payload: JSON.parse(unb64(split[1])),
required by Shor’s algorithm has not been developed yet, but this is an acive area of
valid: signature == split[2]
research at the moment.
};
} Although currently integer factorization is computationally infeasible for large semiprimes, there
is no mathematical proof that this should be the case. In other words, in the future there might
The signature is split from the JWT and a new signature is computed. If the new signature matches
appear algorithms that solve integer factorization in reasonable timeframes. The same can be said
the one included in the JWT, then the signature is valid.
of RSA.
You can use the function above as follows:
With this said, we can now focus on the actual algorithm. The basic principle is captured in this
const secret = 'secret'; expression:
const encoded = jwtEncode({}, {sub: "test@test.com"}, secret);
e d
const decoded = jwtVerifyAndDecode(encoded, secret); (m ) ≡m(mod n)
This code is available in the hs256.js file of the samples included6 with this handbook.
Figure 7.4: RSA basic expression
6 https://github.com/auth0/jwt-handbook-samples
8 https://en.wikipedia.org/wiki/Prime_number
9 https://en.wikipedia.org/wiki/General_number_field_sieve
10 http://eprint.iacr.org/2010/006
11 https://en.wikipedia.org/wiki/Shor%27s_algorithm
12 https://en.wikipedia.org/wiki/Quantum_computing

72 74
public parties. This is also the reason one of those values is picked: the public value can be chosen otherwise it is not.
to be as small as possible. This speeds up computation without compromising the safety of the
In JavaScript:
algorithm.
/**
* Verifies a signature for a message using the RSASSA algorithm as defined
7.2.2.2 Basic Signing * in PKCS#1.
* @param {publicKey} RSA private key, an object with
Signing in RSA is performed as follows: * three members: size (size in bits), n (the modulus) and
1. A message digest is produced from the message to be signed by a hash function. * e (the public exponent), both bigInts
2. This digest is then raised to the power of d modulo n (which is part of the private key). * (big-integer library).
3. The result is attached to the message as the signature. * @param {hashFn} the hash function as required by PKCS#1,
* it should take a Uint8Array and return a Uint8Array
When a recipient holding the public key wants to verify the authenticity of the message, he or she * @param {hashType} A symbol identifying the type of hash function passed.
can reverse the operation as follows: * For now, only "SHA-256" is supported. See the "hashTypes"
1. The signature is raised to the power of e modulo n. The resulting value is the reference digest * object for possible values.
value. * @param {message} A String or Uint8Array with arbitrary data to verify
2. A message digest is produced from the message using the same hash function as in the signing * @param {signature} A Uint8Array with the signature
step. * @return {Boolean} true if the signature is valid, false otherwise.
3. The results from step 1 and 2 are compared. If they match, the signing party must be in */
possession of the private key. export function verifyPkcs1v1_5(publicKey,
hashFn,
This signature/verification scheme is known as “signature scheme with appendix” (SSA). This hashType,
scheme requires the original message to be available to verify the message. In other words, they do message,
not allow message recovery from the signature (message and signature remain separate). signature) {
if(signature.length !== publicKey.size / 8) {
7.2.2.3 RS256: RSASSA PKCS1 v1.5 using SHA-256 throw new Error('invalid signature length');
}
Now that we have a basic notion of how RSA works, we can focus on a specific variant: PKCS#1
RSASSA v1.5 using SHA-256, also known as RS256 in the JWA specification. const intSignature = os2ip(signature);
const intVerification = rsavp1(publicKey, intSignature);
14
The Public Key Cryptography Standard #1 (PKCS #1) specification defines a series of primitives, const verificationMessage = i2osp(intVerification, publicKey.size / 8);
formats and encryption schemes based on the RSA algorithm. These elements work together to
provide a detailed implementation of RSA usable in modern computing platforms. RSASSA is one const encodedMessage =
of the schemes defined in it, and it allows the use of RSA for signatures. emsaPkcs1v1_5(hashFn, hashType, publicKey.size / 8, message);

return uint8ArrayEquals(encodedMessage, verificationMessage);


7.2.2.3.1 Algorithm
}
To produce a signature:
1. Apply the EMSA-PKCS1-V1_5-ENCODE primitive to the message (an array of octets). 7.2.2.3.1.1 EMSA-PKCS1-v1_5 primitive
The result is the encoded message. This primitive makes use of a hash function (usually
a SHA family hash function such as SHA-256). This primitive accepts an expected encoded This primitive takes three elements:
message length. In this case, it will be the length in octets of the RSA number n (the key • The message
length). • The intended length of the result
14 https://www.ietf.org/rfc/rfc3447.txt • And the hash function to use (which must be one of the options from step 2)
1. Apply the selected hash function to the message.

76 78
The RSASP1 primitive takes the private key and a message representative and produces a signature function rsasp1(privateKey, intMessage) {
representative. if(intMessage.isNegative() ||
intMessage.greaterOrEquals(privateKey.n)) {
• Let n and d be the RSA numbers for the private key.
throw new Error("message representative out of range");
• Let m be the message representative.
}
1. Check the message representative is in range: between 0 and n - 1.
2. Compute the result as follows: // result = intMessage ^ d (mod n)
return intMessage.modPow(privateKey.d, privateKey.n);
d
s=m mod (n) }
For verifications, the RSAVP1 primitive is used instead:
Figure 7.8: RSASP1 result
export function rsavp1(publicKey, intSignature) {
if(intSignature.isNegative() ||
PKCS#1 defines an alternative, computationally convenient way of storing the private
intSignature.greaterOrEquals(publicKey.n)) {
key: rather than keeping n and d in it, a combination of different precomputed values for
throw new Error("message representative out of range");
certain operations are stored. These values can be directly used in certain operations
}
and can speed up computations significantly. Most private keys are stored this way.
Storing the private key as n and d is valid, though.
// result = intSignature ^ e (mod n)
return intSignature.modPow(publicKey.e, publicKey.n);
7.2.2.3.1.4 RSAVP1 primitive }

The RSAVP1 primitive takes a public key and an integer signature representative and produces an Finally, the EMSA-PKCS1-v1_5 primitive performs most of the hard work by transforming the mes-
integer message representative. sage into its encoded and padded representation.

• Let n and e be the RSA numbers for the public key. function emsaPkcs1v1_5(hashFn, hashType, expectedLength, message) {
• Let s be the integer signature representative. if(hashType !== hashTypes.sha256) {
throw new Error("Unsupported hash type");
1. Check the message representative is in range: between 0 and n - 1. }
2. Compute the result as follows:
const digest = hashFn(message, true);
m= se mod ( n)
// DER is a stricter set of BER, this (fortunately) works:
Figure 7.9: RSAVP1 result const berWriter = new Ber.Writer();
berWriter.startSequence();
berWriter.startSequence();
7.2.2.3.1.5 I2OSP primitive
// SHA-256 OID
The I2OSP primitive takes an integer representative and produces an array of octets. berWriter.writeOID("2.16.840.1.101.3.4.2.1");
berWriter.writeNull();
• Let len be the expected length of the array of octets.
berWriter.endSequence();
• Let x be the integer representative.
berWriter.writeBuffer(Buffer.from(digest), ASN1.OctetString);
1. If x > 256len then the integer is too large and the arguments are wrong. berWriter.endSequence();
2. Compute the base-256 representation of the integer:
// T is the name of this element in RFC 3447
len−1 len−2
x=x 1⋅256 + x 2⋅256 +...+ x len−1⋅256+ x len const t = berWriter.buffer;

Figure 7.10: I2OSP decomposition if(expectedLength < (t.length + 11)) {


throw new Error('intended encoded message length too short');

80 82
const jwtUnprotected = `${encHeader}.${encPayload}`; Copy the output of running the rs256.js sample19 into the JWT area at JWT.io20 . Then copy
const signature = b64( the contents of pubtestkey.pem to the public-key area in the same page and the JWT will be
pkcs1v1_5.sign(privateKey, successfully validated.
msg => sha256(msg, true),
hashTypes.sha256, stringToUtf8(jwtUnprotected)));
7.2.2.4 PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
return `${jwtUnprotected}.${signature}`;
} RSASSA-PSS is another signature scheme with appendix based on RSA. “PSS” stands for Proba-
bilistic Signature Scheme, in contrast with the usual deterministic approach. This scheme makes
This function is very similar to the jwtEncode function for HS256 shown in the HMAC section. use of a cryptographically secure random number generator. If a secure RNG is not available, the
Verification is just as simple: resulting signature and verification operations provide a level of security comparable to determin-
istic approaches. This way RSASSA-PSS results in a net improvement over PKCS v1.5 signatures
/** for best case scenarios. In the wild, however, both PSS and PKCS v1.5 schemes remain unbroken.
* Verifies a signature for a message using the RSASSA algorithm as defined
* in PKCS#1. RSASSA-PSS is defined in Public Key Cryptography Standard #1 (PKCS #1)21 and is not available
* @param {publicKey} RSA private key, an object with in earlier versions of the standard.
* three members: size (size in bits), n (the modulus) and
* e (the public exponent), both bigInts 7.2.2.4.1 Algorithm
* (big-integer library).
* @param {hashFn} the hash function as required by PKCS#1, To produce a signature:
* it should take a Uint8Array and return a Uint8Array 1. Apply the EMSA-PSS-ENCODE primitive to the message. The primitive takes a param-
* @param {hashType} A symbol identifying the type of hash function passed. eter that should be the number of bits in the modulus of the key minus 1. The result is the
* For now, only "SHA-256" is supported. See the "hashTypes" encoded message.
* object for possible values. 2. Apply the OS2IP primitive to the encoded message. The result is the integer message
* @param {message} A String or Uint8Array with arbitrary data to verify representative. OS2IP is the acronym for “Octet-String to Integer Primitive”.
* @param {signature} A Uint8Array with the signature 3. Apply the RSASP1 primitive to the integer message representative using the private key.
* @return {Boolean} true if the signature is valid, false otherwise. The result is the integer signature representative.
*/ 4. Apply the I2OSP primitive to convert the integer signature representative to an array of
export function verifyPkcs1v1_5(publicKey, octets (the signature). I2OSP is the acronym for “Integer to Octet-String Primitive”.
hashFn,
hashType, A possible implementation in JavaScript, given the primitives mentioned above, could look like:
message, export function signPss(privateKey, hashFn, hashType, message) {
signature) { if(hashType !== hashTypes.sha256) {
if(signature.length !== publicKey.size / 8) { throw new Error('unsupported hash type');
throw new Error('invalid signature length'); }
}
const encodedMessage = emsaPssEncode(hashFn,
const intSignature = os2ip(signature); hashType,
const intVerification = rsavp1(publicKey, intSignature); mgf1.bind(null, hashFn),
const verificationMessage = i2osp(intVerification, publicKey.size / 8); 256 / 8, //size of hash
privateKey.size - 1,
const encodedMessage = message);
emsaPkcs1v1_5(hashFn, hashType, publicKey.size / 8, message); const intMessage = os2ip(encodedMessage);
19 https://github.com/auth0/jwt-handbook-samples/blob/master/rs256.js
return uint8ArrayEquals(encodedMessage, verificationMessage);
20 https://jwt.io
} 21 https://www.ietf.org/rfc/rfc3447.txt

84 86
MGF1 takes a seed value and the intended length of the output as inputs. The maximum length of Note that the intended length used as input is expressed in bits. For the following examples,
the output is defined as 232 . MGF1 uses internally a configurable hash function. PS256 specifies this consider:
hash function as SHA-256.
const expectedLength = Math.ceil(expectedLengthBits / 8);
1. If the intended length is bigger than 232 , stop with error “mask too long”.
1. Hash the message to be verified using the selected hash function.
2. Iterate from 0 to the ceiling of the intended length divided the length of the hash function
output minus 1 (ceiling(intendedLength / hashLength) - 1) doing the following opera- const digest1 = hashFn(message, true);
tions:
2. If the expected length is smaller than the hash length plus the salt length plus 2, consider the
1. Let c = i2osp(counter, 4) where counter is the current value of the iteration counter.
signature invalid.
2. Let t = t.concat(hash(seed.concat(c))) where t is preserved between iterations,
hash is the selected hash function (SHA-256) and seed is the input seed value. if(expectedLength < (digest1.length + saltLength + 2)) {
3. Output the leftmost intended length octets from the last value of t as the result of the return false;
function. }
3. Check that the last byte of the encoded message of the signature has the value of 0xBC
7.2.2.4.1.2 EMSA-PSS-ENCODE primitive if(verificationMessage[verificationMessage.length - 1] !== 0xBC) {
The primitive takes two elements: return false;
}
• The message to be encoded as an octet sequence.
• The intended maximum length of the result in bits. 4. Split the encoded message into two elements. The first element has a length of
expectedLength - hashLength - 1. The second element starts at the end of the
This primitive can be parameterized by the following elements: first and has a length of hashLength.
• A hash function. In the case of PS256 this SHA-256. const maskedLength = expectedLength - digest1.length - 1;
• A mask generation function. In the case of PS256 this is MGF1. const masked = verificationMessage.subarray(0, maskedLength);
• An intended length for the salt used internally. const digest2 = verificationMessage.subarray(maskedLength,
These parameters are all specified by PS256, so they are not configurable and for the purposes of maskedLength + digest1.length);
this description are considered constants. 5. Check that the leftmost 8 * expectedLength - expectedLengthBits (the expected length
Note that the intended length used as input is expressed in bits. For the following examples, in bits minus the requested length in bits) bits of masked are 0.
consider: const zeroBits = 8 * expectedLength - expectedLengthBits;
const intendedLength = Math.ceil(intendedLengthBits / 8); const zeroBitsMask = 0xFF >>> zeroBits;
if((masked[0] & (~zeroBitsMask)) !== 0) {
1. If the input is bigger than the maximum length of the hash function, stop. If not, apply the return false;
hash function to the message. }
const hashed1 = sha256(inputMessage); 6. Pass the second element extracted from step 4 (the digest) to the selected MGF function.
2. If the intended length of the message is less than the length of the hash plus the length of the Request the result to have a length of expectedLength - hashLength - 1.
salt plus 2, stop with an error. const dbMask = mgf(maskedLength, digest2);
if(intendedLength < (hashed1.length + intendedSaltLength + 2)) { 7. For each byte from first element extracted in step 4 (masked) apply the XOR function using
throw new Error('Encoding error'); the corresponding byte from the element computed in the last step (dbMask).
}
const db = new Uint8Array(masked.length);
3. Generate a random octet sequence of the length of the salt. for(let i = 0; i < db.length; ++i) {
4. Concatenate eight zero-valued octets with the hash of the message and the salt. db[i] = masked[i] ^ dbMask[i];
const m = [0,0,0,0,0,0,0,0, ...hashed1, ...salt]; }

5. Apply the hash function to the result of the previous step.

88 90
const value = hashFn(Uint8Array.of(...seed, ...c), true); 7.2.3 Elliptic Curve
result.set(value, i * hashSize);
} Elliptic Curve (EC) algorithms, just like RSA, rely on a class of mathematical problems that are
return result.subarray(0, expectedLength); intractable for certain conditions. Intractability refers to the possibility of finding a solution given
} enough resources, but that, in practice, is hard to achieve. While RSA relies on the intractability
of the factoring problem24 (finding the prime factors of a big coprime number), elliptic curve
export function emsaPssEncode(hashFn,
algorithms rely on the intractability of the elliptic curve discrete logarithm problem.
hashType,
mgf, Elliptic curves are discribed by the following equation:
saltLength,
expectedLengthBits, y 2=x 3 +ax+b
message) {
const expectedLength = Math.ceil(expectedLengthBits / 8); Figure 7.11: Elliptic curve equation

const digest1 = hashFn(message, true);


if(expectedLength < (digest1.length + saltLength + 2)) { By setting a and b to different values, we get the following sample curves:
throw new Error('encoding error'); 24 https://en.wikipedia.org/wiki/Integer_factorization
}

const salt = crypto.randomBytes(saltLength);


const m = Uint8Array.of(...(new Uint8Array(8)),
...digest1,
...salt);
const digest2 = hashFn(m, true);
const ps = new Uint8Array(expectedLength - saltLength - digest2.length - 2);
const db = Uint8Array.of(...ps, 0x01, ...salt);
const dbMask = mgf(db.length, digest2);
const masked = db.map((value, index) => value ^ dbMask[index]);

const zeroBits = 8 * expectedLength - expectedLengthBits;


const zeroBitsMask = 0xFF >>> zeroBits;
masked[0] &= zeroBitsMask;

return Uint8Array.of(...masked, ...digest2, 0xbc);


}
export function emsaPssVerify(hashFn,
hashType,
mgf,
saltLength,
expectedLengthBits,
message,
verificationMessage) {
const expectedLength = Math.ceil(expectedLengthBits / 8);

const digest1 = hashFn(message, true);


if(expectedLength < (digest1.length + saltLength + 2)) {

92 94
A prime field is a field that contains a prime number p of elements. All elements and arithmetic 1. If k~i~ is 1 then let Q be the result of adding Q to N (elliptic-curve addition).
operations are implemented modulo p (the prime number of elements). 2. Let N be the result of doubling N (elliptic-curve doubling).
4. Return Q.
By making the field finite, the algorithms used to perform mathematical operations change. In
particular, the discrete logarithm26 must be used instead of the ordinary logarithm. Logarithms Sample implementation in JavaScript:
find the value of k for expressions of the following form:
function ecMultiply(P, k, modulus) {
let N = Object.assign({}, p);
a k =c let Q = {
log a (c)=k x: bigInt(0),
y: bigInt(0)
Figure 7.13: Exponential function };

for(k = bigInt(k); !k.isZero(); k = k.shiftRight(1)) {


There is no known efficient, general purpose algorithm for computing the discrete logarithm. This if(k.isOdd()) {
limitation makes the discrete logarithm ideal for cryptography. Elliptic curves, as used by cryptog- Q = ecAdd(Q, N, modulus);
raphy, exploit this limitation to provide secure asymmetric encryption and signature operations. }
The intractability of the discrete logarithm problem depends on carefully choosing the parameters N = ecDouble(N, modulus);
of the field on which it is to be used. This means that for elliptic curve cryptography to be effective, }
certain parameters must be chosen with great care. Elliptic curve algorithms have been the target
in past attacks due to misuse27 . return Q;
}
An interesting aspect of Elliptic-Curve cryptography is that key sizes can be smaller while providing
a similar level of security compared to bigger keys used in RSA. This allows cryptography even on One thing to note is that in modular arithmetic, division is implemented as the multiplication
memory limited devices. In general terms, a 256-bit elliptic-curve key is similar to a 3072-bit RSA between the numerator and the inverse of the divisor.
key in cryptographic strength. JavaScript versions of these operations can be found in the samples repository28 in the ecdsa.js
file. These naive implementations, though functional, are vulnerable to timing attacks. Production-
7.2.3.1 Elliptic-Curve Arithmetic ready implementations use different algorithms that take these attacks into account.

For the purposes of implementing elliptic-curve signatures, it is necessary to implement elliptic- 7.2.3.2 Elliptic-Curve Digital Signature Algorithm (ECDSA)
curve arithmetic. The three basic operations are: point addition, point doubling, and point scalar
multiplication. All three operations result in valid points on the same curve. The Elliptic-Curve Digital Signature Algorithm (ECDSA) was developed by a committee for the
American National Standards Institute (ANSI)29 . The standard is X9.6330 . The standard specifies
7.2.3.1.1 Point Addition all the needed parameters for proper use of elliptic-curves for signatures in a secure way. The
26 https://en.wikipedia.org/wiki/Discrete_logarithm
JWA specification relies on this specification (and FIPS 186-431 ) for picking curve parameters and
27 https://safecurves.cr.yp.to/ specifying the algorithm.
For use with JWTs, JWA specifies that the input to signing algorithm is the Base64 encoded header
and payload, just like any other signing algorithm, but the result is two integers r and s rather
than one. These integers are to be converted to 32-byte sequences in big-endian order, which are
then concatenated to form a single 64-byte signature.
export default function jwtEncode(header, payload, privateKey) {
if(typeof header !== 'object' || typeof payload !== 'object') {
28 https://github.com/auth0/jwt-handbook-samples/
29 https://www.ansi.org/
30 https://webstore.ansi.org/RecordDetail.aspx?sku=ANSI+X9.63-2011+(R2017)
31 http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf

96 98
valid: valid 1. Compute the digest of the message to sign using a cryptographically secure hash function.
}; Let this number be e.
} 2. Use a cryptographically secure random number generator to pick a number k in the range 1
to n - 1.
Again, the procedure for checking the validity of the signature is similar to RSA and HMAC. In
3. Mutiply the base point G with k (mod q).
this case, values r and s must be retrieved from the 64-byte JWT signature. The first 32 bytes are
4. Let r be the result of taking the x coordinate of the point from the previous step modulo the
the element r, and the remaining 32 bytes are the element s. To convert these values into numbers
order of G (n).
we can use the os2ip primitive from PKCS.
5. If r is zero repeat steps 2 to 5 until it is not zero.
6. Let d be the private key and s the result of:
7.2.3.2.1 Elliptic-Curve Domain Parameters
dr +e
Elliptic-curve operations as used by ECDSA depend on a few key parameters: s≡ (mod n)
k
• p or q: the prime used to define the prime field32 on which arithmetic operations are performed.
Prime field operations use modular arithmetic33 . Figure 7.18: s
• a: coefficient of x in the curve equation.
• b: constant in the curve equation (y-intercept). 7. If s is zero, repeat steps 2 to 7 until it is not zero.
• G: a valid curve point used as base point for elliptic-curve operations. The base point is used The signature is the tuple r and s. For the purposes of JWA, r and s are represented as two 32-byte
in arithmetic operations to obtain other points on the curve. octet sequences concatenated (first r and then s).
• n: the order of base point G. This parameter is the number of valid points in the curve that
can be constructed by using point G as base point. Sample implementation:
For elliptic-curve operations to be secure, these parameters must be chosen carefully. In the context export function sign(privateKey, hashFn, hashType, message) {
of JWA, there are only three curves that are considered valid: P-256, P-384, and P-521. These curves if(hashType !== hashTypes.sha256) {
are defined in FIPS 186-434 and other associated standards. throw new Error('unsupported hash type');
}
For our code sample, we will be using curve P-256:
const p256 = { // Algorithm as described in ANS X9.62-1998, 5.3
q: bigInt('00ffffffff00000001000000000000' +
'000000000000ffffffffffffffffff' + const e = bigInt(hashFn(message), 16);
'ffffff', 16),
// order of base point let r;
n: bigInt('115792089210356248762697446949407573529996955224135760342' + let s;
'422259061068512044369'), do {
// base point let k;
G: { do {
x: bigInt('6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0' + // Warning: use a secure RNG here
'f4a13945d898c296', 16), k = bigInt.randBetween(1, p256.nMin1);
y: bigInt('4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ece' + const point = ecMultiply(p256.G, k, p256.q);
'cbb6406837bf51f5', 16) r = point.x.fixedMod(p256.n);
}, } while(r.isZero());
//a: bigInt(-3)
a: bigInt('00ffffffff00000001000000000000' + const dr = r.multiply(privateKey.d);
'000000000000ffffffffffffffffff' + const edr = dr.add(e);
32 https://en.wikipedia.org/wiki/Finite_field s = edr.multiply(k.modInv(p256.n)).fixedMod(p256.n);
33 https://en.wikipedia.org/wiki/Modular_arithmetic } while(s.isZero());
34 http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf

return {

100 102
return v.compare(signature.r) === 0; These objects are encoded using the JWS Compact Serialization format to produce something like
} this:
An important part of the algorithm that is often overlooked is the check of validity of the public eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
key. This has been a source of attacks in the past36 . If an attacker controls the public key that a eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
verifying party uses for validation of the message signature, and the public key is not validated as a XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o
point on the curve, the attacker can craft a special public key that can be used to leak information
This is a signed JWT. Signed JWTs in compact format are simply the header and payload objects
to the attacker. This is particularly important in the light of key agreement protocols, some of
encoded using Base64-URL encoding and separated by a dot (.). The last part of the compact
which are used for encrypting JWTs.
representation is the signature. In other words, the format is:
This sample implementation can be found in the samples repository37 in the ecdsa.js file.
[Base64-URL encoded header].[Base64-URL encoded payload].[Signature]
This only applies to signed tokens. Encrypted tokens have a different serialized compact format
7.3 Future Updates that also relies on Base64-URL encoding and dot-separated fields.
If you want to play with JWTs and see how they are encoded/decoded, check JWT.io4 .
The JWA specification has many more algorithms. In future versions of this handbook we will go
over the remaining algorithms.
8.1.1 “alg: none” Attack

As we mentioned before, JWTs carry two JSON objects with important information, the header
and the payload. The header includes information about the algorithm used by the JWT to sign
or encrypt the data contained in it. Signed JWTs sign both the header and the payload, while
encrypted JWTs only encrypt the payload (the header must always be readable).
In the case of signed tokens, although the signature does protect the header and payload against
tampering, it is possible to rewrite the JWT without using the signature and changing the data
contained in it. How does this work?
Take for instance a JWT with a certain header and payload. Something like this:
header: {
alg: "HS256",
typ: "JWT"
},
payload: {
sub: "joe"
role: "user"
}
Now, let’s say you encode this token into its compact serialized form with a signature and a signing
key of value “secret”. We can use JWT.io5 for that. The result is (newlines inserted for readability):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJqb2UiLCJyb2xlIjoidXNlciJ9.
vqf3WzGLAxHW-X7UP-co3bU_lSUdVjF2MKtLtSU1kzU
Go ahead and paste that on JWT.io6 .
4 https://jwt.io
36 http://blogs.adobe.com/security/2017/03/critical-vulnerability-uncovered-in-json-encryption.html 5 https://jwt.io
37 https://github.com/auth0/jwt-handbook-samples/ 6 https://jwt.io

104 106
8.1.2 RS256 Public-Key as HS256 Secret Attack On the other hand, HMAC shared secrets, as used by JWTs, are optimized for speed. This allows
many sign/verify operations to be performed efficiently but make brute force attacks easier8 . So,
This attack is similar to the "alg": "none" attack and also relies on ambiguity in the API of certain the length of the shared secret for HS256/384/512 is of the utmost importance. In fact, JSON Web
JWT libraries. Our sample token will be similar to the one for that attack. In this case, however, Algorithms9 defines the minimum key length to be equal to the size in bits of the hash function
rather than removing the signature, we will construct a valid signature that the verification library used along with the HMAC algorithm:
will also consider valid by relying on a loophole in many APIs. First, consider the typical function
“A key of the same size as the hash output (for instance, 256 bits for”HS256“) or larger
signature of some JWT libraries for the verification function:
MUST be used with this algorithm.” - JSON Web Algorithms (RFC 7518), 3.2 HMAC
function jwtDecode(token, secretOrPublicKey) { with SHA-2 Functions10
// (...)
In other words, many passwords that could be used in other contexts are simply not good enough for
}
use with HMAC-signed JWTs. 256-bits equals 32 ASCII characters, so if you are using something
As you can see here, this function is essentially identical to the one from the "alg": "none" attack. human readable, consider that number to be the minimum number of characters to include in the
If verification is successful, the token is decoded and returned, otherwise an exception is thrown. secret. Another good option is to switch to RS256 or other public-key algorithms, which are much
In this case, however, the function also accepts a public key as the second parameter. In a way, more robust and flexible. This is not simply a hypothetical attack, it has been shown that brute
this makes sense: both the public key and the shared secret are usually strings or byte arrays, so force attacks for HS256 are simple enough to perform11 if the shared secret is too short.
from the point of view of the necessary types for that function argument, a single argument can
represent both a public key (for RS, ES, or PS algorithms) and a shared secret (for HS algorithms).
This type of function signature is common for many JWT libraries. 8.1.4 Wrong Stacked Encryption + Signature Verification Assumptions
Now, suppose the attacker gets an encoded token signed with an RSA key pair. It looks like this Signatures provide protection against tampering. That is, although they don’t protect data from
(newlines inserted for readability): being readable, they make it immutable: any changes to the data result in an invalid signature.
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2UiLCJyb2xlIjoidXNlciJ9. Encryption, on the other hand, makes data unreadable unless you know the shared key or private
QDjcv11Kcb69THVLKMErYqzy9htWlCDtBdonVR5SX4geZa_R8StjwUuuskveUsdJVgjgXwMso7p key.
uAJZzoE9LEr9XCxau7SF1ddws4ONiqxSVXZbO0pSgbKm3FpkVz4Jyy4oNTs- For many applications, signatures are all that is necessary. However, for sensitive data, encryption
bIYyE0xf8snFlT1MbBWcG5psnuG04IEle4s may be required. JWTs support both: signatures and encryption.
Decoded: It is very common to wrongly assume that encryption also provides protection against tampering in
header: { all cases. The rationale for this assumption is usually something like this: “if the data can’t be read,
"alg": "RS256", how would an attacker be able to modify it for their benefit?”. Unfortunately, this underestimates
"typ": "JWT" attackers and their knowledge of the algorithms involved in the process.
}, Some encryption/decryption algorithms produce output regardless of the validity of the data passed
payload: { to them. In other words, even if the encrypted data was modified, something will come out of the
"sub": "joe", decryption process. Blindly modifying data usually results in garbage as output, but to a malicious
"role": "user" attacker this may be enough to get access to a system. For example, consider a JWT payload that
} looks like this:
This token is signed with an RSA key-pair. RSA signatures are produced with the private key, {
while verification is done with the public key. Whoever verifies the token in the future could make "sub": "joe",
a call to our hypothetical jwtDecode function from before like so: "admin": false
const publicKey = '...'; }
const decoded = jwtDecode(token, publicKey); As we can see here, the admin claim is simply a boolean. If an attacker can manage to produce
But here’s the problem: the public key is, like the name implies, usually public. The attacker may a change in the decrypted data that results in that boolean value being flipped, he or she may
get his or her hands on it, and that should be OK. But what if the attacker were to create a new 8 https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/
token using the following scheme. First, the attacker modifies the header and chooses HS256 as the 9 https://tools.ietf.org/html/rfc7518
10 https://tools.ietf.org/html/rfc7518#section-3.2
signing algorithm:
11 https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/

108 110
enough numbers. This problem prevents the recovery of the private key from a public key, an To prevent these attacks, token validation must rely on either unique, per-service keys or secrets, or
encrypted message, and its plaintext. When compared to RSA, another public-key algorithm which specific claims. For instance, this token could include an aud claim specifying the intended audience.
is also supported by JSON Web Algorithms, elliptic-curves provide a similar level of strength while This way, even if the signature is valid, the token cannot be used on other services that share the
requiring smaller keys. same secret or signing key.
Elliptic-curves, as required for cryptographic operations, are defined over finite fields. In other
words, they operate on sets of discrete numbers (rather than all real numbers). This means that 8.1.6.2 Same Recipient/Cross JWT
all numbers involved in cryptographic elliptic-curve operations are integers.
All mathematical operations of elliptic-curves result in valid points over the curve. In other words, This attack is similar to the previous one, but rather than relying on a token issued for a different
the math for elliptic-curves is defined in such a way that invalid points are simply not possible. recipient, in this case, the recipient is the same. What changes in this case is that the attacker sends
If an invalid point is produced, then there is an error in the inputs to the operations. The main the token to a different service rather the one intended for (inside the same company or service
arithmetic operations on elliptic curves are: provider).

• Point addition: adding two points on the same curve resulting in a third point on the same Let’s imagine a token with the following payload:
curve. {
• Point doubling: adding a point to itself, resulting in a new point on the same curve. "sub": "joe",
• Scalar multiplication: multiplying a single point on the curve by a scalar number, defined "perms": "write",
as repeatedly adding that number to itself k times (where k is the scalar value). "aud": "cool-company/user-database",
All cryptographic operations on elliptic-curves rely on these arithmetic operations. Some implemen- "iss": "cool-company"
tations, however, fail to validate the inputs to them. In elliptic-curve cryptography, the public key }
is a point on the elliptic curve, while the private key is simply a number that sits within a special, This token looks much more secure. We have an issuer (iss) claim, an audience (aud) claim, and a
but very big, range. If inputs to these operations are not validated, the arithmetic operations may permissions (perm) claim. The API for which this token was issued checks all of these claims even
produce seemingly valid results even when they are not. These results, when used in the context of if the signature of the token is valid. This way, even if the attacker manages to get his or her hands
cryptographic operations such as decryption, can be used to recover the private key. This attack on a token signed with the same private key or secret, he or she cannot use it to operate on this
has been demonstrated in the past16 . This class of attacks are known as invalid curve attacks17 . service if it’s not intended for it.
Good-quality implementations always check that all inputs passed to any public function are valid.
This includes verifying that public-keys are a valid elliptic-curve point for the chosen curve and However, cool-company has other public services. One of these services, the cool-company/item-database
that private keys sit inside the valid range of values. service, has recently been upgraded to check claims along with the token signature. However,
during the upgrades, the team in charge of selecting the claims that would be validated made a
mistake: they did not validate the aud claim correctly. Rather than checking for an exact match,
8.1.6 Substitution Attacks they decided to check for the presence of the cool-company string. It turns out that the other
service, the hypothetical cool-company/user-database service, emits tokens that also pass this
Substitution attacks are a class of attacks where an attacker manages to intercept at least two check. In other words, an attacker could use the token intended for the user-database service in
different tokens. The attacker then manages to use one or both of these tokens for purposes other place for the token for the item-database service. This would grant the attacker write permissions
than the one they were intended for. to the item database when he or she should only have write permissions for the user database!
There are two types of substitution attacks: same recipient (called cross JWT in the draft), and
different recipient.

8.1.6.1 Different Recipient

Different recipient attacks work by sending a token intended for one recipient to a different recip-
ient. Let’s say there is an authorization server that issues tokens for a third party service. The
authorization token is a signed JWT with the following payload:
16 http://blogs.adobe.com/security/2017/03/critical-vulnerability-uncovered-in-json-encryption.html
17 http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.107.3920&rep=rep1&type=pdf

112 114
selected to prevent giving attackers control. Libraries used to rely on the header alg claim to select more specific by specifying a certain internal format for them. The iss claim, for instance, could
the algorithm for validation. From the moment attacks like these were seen in the wild18 , libraries be an URL of the subsystem that issued that token, rather than the name of the company, making
have switched to at least providing the option of explicitly specifying the selected algorithms for it harder to be reused.
validation, disregarding what is specified in the header. Still, some libraries provide the option
of using whatever is specified in the header, so developers must take care to always use explicit
algorithm selection. 8.3 Conclusion

8.2.2 Use Appropriate Algorithms JSON Web Tokens are a tool that makes use of cryptography. Like all tools that do so, and
especially those that are used to handle sensitive information, they should be used with care. Their
Although the JSON Web Algorithms spec declares a series of recommended and required algorithms, apparent simplicity may confuse some developers and make them think that using JWTs is just a
picking the right one for a specific scenario is still up to the users. For example, a JWT signed matter of picking the right shared secret or public key algorithm. Unfortunately, as we have seen
with an HMAC signature may be enough for storing a small token from your single-server, single- above, that is not the case. It is of the utmost importance to follow the best practices for each
page web application in a user’s browser. In contrast, a shared secret algorithm would be sorely tool in your toolbox, and JWTs are no exception. This includes picking battle-tested, high-quality
inconvenient in a federated identity scenario. libraries; validating payload and header claims; choosing the right algorithms; making sure strong
keys are generated; paying attention to the subtleties of each API; among other things. If all of this
Another way of thinking about this is to consider all JWTs invalid unless that validation algorithm seems daunting, consider offloading some of the burdens to external providers. Auth019 is one such
is acceptable to the application. In other words, even if the validating party has the keys and provider. If you cannot do this, consider these recommendations carefully, and remember: don’t
the means necessary for validating a token, it should still be considered invalid if the validation roll your own crypto20 , rely on tried and tested code.
algorithm is not the right one for the application. This is also another way of saying what we
mentioned in our previous recommendation: always perform algorithm verification.

8.2.3 Always Perform All Validations

In the case of nested tokens, it is necessary to always perform all validation steps as declared in
the headers of each token. In other words, it is not sufficient to decrypt or validate the outermost
token and then skip validation for the inner ones. Even in the case of only having signed JWTs, it
is necessary to validate all signatures. This is a source of common mistakes in applications that use
JWTs to carry other JWTs issued by external parties.

8.2.4 Always Validate Cryptographic Inputs

As we have shown in the attacks section before, certain cryptographic operations are not well
defined for inputs outside their range of operation. These invalid inputs can be exploited to produce
unexpected results, or to extract sensitive information that may lead to a full compromise (i.e. the
attackers getting hold of a private key).
In the case of elliptic-curve operations, our example from before, libraries must always validate
public-keys before using them (i.e. confirming they represent a valid point on the selected curve).
These types of checks are normally handled by the underlying cryptographic library. Developers
must make sure that their library of choice performs these validations, or they must add the
necessary code to perform them at the application level. Failing to do so can result in compromise
of their private key(s).
18 https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
19 https://auth0.com
20 https://security.stackexchange.com/questions/18197/why-shouldnt-we-roll-our-own

116 118

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy