JWT Signatures and EdDSA

JWT Signatures and EdDSA

This article describes the use of the EdDSA signature algorithm in JOSE, in particular in conjunction with JWTs. JSON Object Signing and Encryption (JOSE) is a framework of technologies and standards for signing and encrypting JSON objects. The list includes the following specifications:

The framework is extensible. RFC8037, for example, describes how to apply EdDSA in JOSE and as part of that specification defines a new key type. Values, members and parameters are registered and maintained by IANA in the registries for JOSE.

JWT and JWS

A very popular format for tokens is the JSON Web Token (JWT) format. The standard defines a JSON structure for tokens as well as common claims. It also provides instructions on how to apply other JOSE standards, such as JSON Web Signatures (JWS) for signing tokens.

JSON Web Signatures can be serialized using either the JWS Compact Serialization or the JWS JSON Serialization. Signed JWTs are always serialized in a URL-safe manner using the JWS Compact Serialization. The result is a sequence of URL-safe parts separated by period ('.') characters. In technical terms, a signed JWT is constructed the following way:

BASE64URL(UTF8(JWS Protected Header)) + '.' + 
BASE64URL(UTF8(JWT Claims Set) + '.' + 
BASE64URL(JWS Signature)

The first part represents the JOSE header that describes the signature algorithm used to generate the signature. Consequently, the header must at least contain the alg parameter. In a self-contained JWT the header contains additional information required by a receiver to validate the JWT. In this case the header may for example also include the kid, x5t or jwk parameter. Since the header is part of the signature, it is also referred to as the JWS Protected Header.

The second part of a JWT is called the JWS Payload. It contains the message, that is the JSON object called the JWT Claims Set. Examples for claims that the payload may contain are the iss, sub and aud claims. The Curity Identity Server allows you to add any other claim to a token.

The third and final part of the JWT is the signature value. The signature covers both the header and payload of the JWT. It is calculated using the algorithm specified in the alg parameter in the header.

JWA

The original list of algorithms used with JOSE is defined in JSON Web Algorithms (JWA), which also specifies the registration process for new algorithms. In other words, the value in the alg parameter must be registered in the IANA "JSON Web Signature and Encryption Algorithms" registry. EdDSA was registered as a value for the EdDSA algorithm as part of specifying EdDSA for JOSE.

It is best practice for an application only accepting secure algorithms. An application should specify and list the supported algorithms that are considered secure in the application's context. The following table shows an excerpt of registered values for signatures based on a public key algorithm like RSA, Elliptic Curve DSA (ECDSA) or EdDSA.

Algorithm nameAlgorithm Description
RS256RSASSA-PKCS1-v1_5 using SHA-256
PS256RSASSA-PSS using SHA-256 and MGF1 with SHA-256
ES256ECDSA using P-256 and SHA-256
EdDSAEdDSA signature algorithms

Compliance

Standards and regulations typically include requirements regarding algorithms. For example, the Financial-Grade API Security Profile 1.0 Part 2, the FAPI 1.0 Advanced Profile, requires PS256 or ES256 for signatures. In particular, it explicitly discourages the use of RS256. The draft specification for FAPI 2.0 includes EdDSA in the list of approved algorithms.

JWK

Public key algorithms are based on a key pair. A key pair consists of a secret part, the private key, and a public part, the public key. A signature is created using the private key and verified with the public key. Thus, only the entity that possesses the private key can sign a JWT but anybody with the public key can verify the signature.

There are different formats to encode a key (or key pair). One example is PKCS #8, another one is the JSON Web Key (JWK) format.

The JWK format defines a JSON data structure for representing a key. The members of the JSON object are the parameters and properties of the key. Which parameters a key has and how they are encoded depends on the key type. The key type is stored in the kty parameter. Its value depends on the algorithm that the key is supposed to be used with. For example, "kty": "EC" defines a key for an elliptic curve, "kty": "RSA" is a key type to be used with RSA and EdDSA keys have "kty": "OKP".

EdDSA JWK Encoding

EdDSA is a signature scheme that is instantiated with recommended parameters for the Edward's Curves Ed25519 and Ed448. The private key is an integer and the public key is a point (x,y) on the curve. Despite EdDSA operating on elliptic curves, it uses a different signature scheme and different encoding rules than ECDSA.

EdDSA only encodes the y-coordinate of a curve point. Since there are two points on a curve with the same y-coordinate, one bit in the encoding is reserved to store the x-coordinate of the point as well. Consequently, an EdDSA public key is represented by a single parameter though it represents a point with two coordinates. This becomes clearly visible in the JWK format of an EdDSA public key, where the parameter x holds the encoded public key:

"kty": "OKP",
"kid": "-1909572257",
"alg":	"EdDSA",
"crv":	"Ed25519",
"x":	"XWxGtApfcqmKI7p0OKnF5JSEWMVoLsytFXLEP7xZ_l8" 

To be able to encode an EdDSA key as a JWK, a new key type was introduced: OKP, the Octet Key Pair. The crv parameter contains the name of the curve, that is either Ed25519 or Ed448. The parameter x contains the encoded public key, an octet stream that is the little-endian encoding of the public key point on the curve. Optionally, the JWK may also clarify via the alg parameter that this key is supposed to be used with the signing algorithm EdDSA though this information is redundant in this case.

Knowing the details of EdDSA encoding allows a developer to better understand cryptographic interfaces. Study the following code for retrieving an EdDSA public key in Java as an example:

    boolean xBit = ... // Bit for the x-coordinate
    BigInteger y = ... // y-coordinate

    // Load parameters from Ed25519
    NamedParameterSpec paramSpec = new NamedParameterSpec("Ed25519");
    EdECPublicKeySpec pubSpec = new EdECPublicKeySpec(paramSpec, new EdPoint(xBit, y));
 
    // Generate an EdDSA Public Key from the point on Ed25519
    KeyFactory kf = KeyFactory.getInstance("EdDSA");    
    PublicKey pubKey = kf.generatePublic(pubSpec);

The abstract above shows how the encoding of the public key (boolean xBit and BigInteger y), the recommended parameters (predefined as Ed25519) and the algorithm name (EdDSA) fit together.

To get the values for xBit and y in the Java example, decode the public key x from the JWK according to the specification:

// Byte array is in little endian encoding
// The most significant bit in final octet indicates if X is negative or not:
boolean xBit = (publicKeyBytes[b-1] & 0x80) != 0;

// Recover y value by clearing x-bit.
publicKeyBytes[b-1] = (byte)(publicKeyBytes[b-1] & 0x7f);

// Switch to big endian encoding
byte[] publicKeyBytesBE = new byte[b];
for(int i = 0; i < b; i++) {
    publicKeyBytesBE[i] = publicKeyBytes[b-1-i];
}

BigInteger y = new BigInteger(1, publicKeyBytesBE); 

Now use the variables xBit and y to generate a Public Key instance for the EdDSA key.

Keysize

EdDSA keys are much smaller than RSA keys. FAPI 1.0 requires 2048-bits or more for RSA keys and at least 160-bits for elliptic curve keys. The key length, that is the length of the private key, for Ed25519 is 256-bits and 456-bits for Ed448. Thus, EdDSA is suitable for Financial-Grade security. Consequently, EdDSA is listed as one of the approved algorithms in the draft specification of FAPI 2.0.

EdDSA JWS and JWT

Signing

A signed JWT has three parts:

  • JWS Protected Header
  • JWS Payload (JWT Claims Set)
  • JWS Signature

Both the JWS Protected Header and the JWT Claims Set are signed.

First, prepare the input for the signature. Start with the JWS Protected Header. Set alg to EdDSA as this is the algorithm that will be used to create the signature. Specify the key used for signing as well because it is useful later when verifying the signature.

{
  "alg": "EdDSA",
  "kid": "-1909572257"
}

Encode the header using the Base64URL encoding. The result for this example is:

eyJraWQiOiItMTkwOTU3MjI1NyIsImFsZyI6IkVkRFNBIn0

Then, prepare the message to sign. In the case of a signed JWT, the payload is the JWT Claims Set. The following example shows the claims of an access token created by the Curity Identity Server:

{
  "jti": "22916f3c-9093-4813-8397-f10e6b704b68",
  "delegationId": "b4ae47a7-625a-4630-9727-45764a712cce",
  "exp": 1655279109,
  "nbf": 1655278809,
  "scope": "read openid",
  "iss": "https://idsvr.example.com",
  "sub": "username",
  "aud": "api.example.com",
  "iat": 1655278809,
  "purpose": "access_token"
}

The payload is also Base64URL encoded. The following string is the encoded version of the claims from the access token:

eyJqdGkiOiIyMjkxNmYzYy05MDkzLTQ4MTMtODM5Ny1mMTBlNmI3MDRiNjgiLCJkZWxlZ2F0aW9uSWQiOiJiNGFlNDdhNy02MjVhLTQ2MzAtOTcyNy00NTc2NGE3MTJjY2UiLCJleHAiOjE2NTUyNzkxMDksIm5iZiI6MTY1NTI3ODgwOSwic2NvcGUiOiJyZWFkIG9wZW5pZCIsImlzcyI6Imh0dHBzOi8vaWRzdnIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VybmFtZSIsImF1ZCI6ImFwaS5leGFtcGxlLmNvbSIsImlhdCI6MTY1NTI3ODgwOSwicHVycG9zZSI6ImFjY2Vzc190b2tlbiJ9

Combine the JWS Protected Header and JWT Claims Set by concatenating the Base64URL encoded strings. The result is the JWS signing input (line break for readability only):

eyJraWQiOiItMTkwOTU3MjI1NyIsImFsZyI6IkVkRFNBIn0.
eyJqdGkiOiIyMjkxNmYzYy05MDkzLTQ4MTMtODM5Ny1mMTBlNmI3MDRiNjgiLCJkZWxlZ2F0aW9uSWQiOiJiNGFlNDdhNy02MjVhLTQ2MzAtOTcyNy00NTc2NGE3MTJjY2UiLCJleHAiOjE2NTUyNzkxMDksIm5iZiI6MTY1NTI3ODgwOSwic2NvcGUiOiJyZWFkIG9wZW5pZCIsImlzcyI6Imh0dHBzOi8vaWRzdnIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VybmFtZSIsImF1ZCI6ImFwaS5leGFtcGxlLmNvbSIsImlhdCI6MTY1NTI3ODgwOSwicHVycG9zZSI6ImFjY2Vzc190b2tlbiJ9

Finally, calculate the EdDSA signature over the JWS signing input using the private key. In Java, the code would look similar to the following:

PrivateKey privateKey = ...;
String jwsSigningInput = "eyJra...lbiJ9";
Signature sigAlg = Signature.getInstance("Ed25519");
sigAlg.initSign(privateKey);
sigAlg.update(jwsSigningInput.getBytes(StandardCharsets.US_ASCII));
byte[] signature = sigAlg.sign();

The signature is also Base64URL encoded. In this example the encoded signature results in the following string:

rjeE8D_e4RYzgvpu-nOwwx7PWMiZyDZwkwO6RiHR5t8g4JqqVokUKQt-oST1s45wubacfeDSFogOrIhe3UHDAg

Append the signature to the JWS signing input. The final JWT is a signed JWT. The result of the example is shown as follows (line breaks for readability only):

eyJraWQiOiItMTkwOTU3MjI1NyIsImFsZyI6IkVkRFNBIn0.
eyJqdGkiOiIyMjkxNmYzYy05MDkzLTQ4MTMtODM5Ny1mMTBlNmI3MDRiNjgiLCJkZWxlZ2F0aW9uSWQiOiJiNGFlNDdhNy02MjVhLTQ2MzAtOTcyNy00NTc2NGE3MTJjY2UiLCJleHAiOjE2NTUyNzkxMDksIm5iZiI6MTY1NTI3ODgwOSwic2NvcGUiOiJyZWFkIG9wZW5pZCIsImlzcyI6Imh0dHBzOi8vaWRzdnIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VybmFtZSIsImF1ZCI6ImFwaS5leGFtcGxlLmNvbSIsImlhdCI6MTY1NTI3ODgwOSwicHVycG9zZSI6ImFjY2Vzc190b2tlbiJ9.
rjeE8D_e4RYzgvpu-nOwwx7PWMiZyDZwkwO6RiHR5t8g4JqqVokUKQt-oST1s45wubacfeDSFogOrIhe3UHDAg

Tokensize

EdDSA signatures have a length of 512-bits for Ed25519 or 912-bits for Ed448. In comparison, RSA signatures are as long as the key, that is at least 2048-bits (i.e. about 2-4 times bigger). As a result, an EdDSA token is significantly smaller than one signed with PS256, for example.

Verifying

When verifying the signature of a JWT, first determine the algorithm and, if provided, the key from the JWS Protected Header of the JWT. Split the JWT at the period ('.') characters. The first part is the header. Decode the header with Base64URL. The header may look similar to the following:

{
  "alg": "EdDSA",
  "kid": "-1909572257"
}

Check the algorithm against a list of secure and supported algorithms for the application and context. In this case, alg must match EdDSA for an EdDSA signature.

Then look up the public key that matches the algorithm. In the example above, the header provides a key identifier for easy lookup:

"kty": "OKP",
"kid": "-1909572257",
"alg":	"EdDSA",
"crv":	"Ed25519",
"x":	"XWxGtApfcqmKI7p0OKnF5JSEWMVoLsytFXLEP7xZ_l8" 

The key is an EdDSA key, easily identified by the alg parameter in this case (kty and crv in other cases). The crv parameter contains the EdDSA variant Ed25519 that the key matches. This information is required for the signature verification.

Remember, that both the header and payload were signed. Thus, the input for signature verification is simply the JWT without the signature part. In this example, the input is the following string (line break for readability only):

eyJraWQiOiItMTkwOTU3MjI1NyIsImFsZyI6IkVkRFNBIn0.
eyJqdGkiOiIyMjkxNmYzYy05MDkzLTQ4MTMtODM5Ny1mMTBlNmI3MDRiNjgiLCJkZWxlZ2F0aW9uSWQiOiJiNGFlNDdhNy02MjVhLTQ2MzAtOTcyNy00NTc2NGE3MTJjY2UiLCJleHAiOjE2NTUyNzkxMDksIm5iZiI6MTY1NTI3ODgwOSwic2NvcGUiOiJyZWFkIG9wZW5pZCIsImlzcyI6Imh0dHBzOi8vaWRzdnIuZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VybmFtZSIsImF1ZCI6ImFwaS5leGFtcGxlLmNvbSIsImlhdCI6MTY1NTI3ODgwOSwicHVycG9zZSI6ImFjY2Vzc190b2tlbiJ9

Now verify the signature using Ed25519 as this is the EdDSA variant of the key. In Java, the code would look similar to the following:

PublicKey publicKey = ...;
String jwtWithoutSignature = "eyJra...lbiJ9";
String signature = "rjeE8...";
// "Ed25519" defines the parameters for EdDSA and must match public key
Signature sigAlg = Signature.getInstance("Ed25519");
// Use the public key for verification
sigAlg.initVerify(publicKey)
// Provide the message that was signed
sigAlg.update(jwtWithoutSignature.getBytes(StandardCharsets.US_ASCII));
// Verify the signature against the message and public key 
sigAlg.verify(Base64.getUrlDecoder().decode(signature));

Note, that there is a lot more to think about when validating a token than just verifying the signature. Refer to JWT Best Practices for guidance.

EdDSA vs RSA and ECDSA

EdDSA is known as a high performance signature algorithm with small key sizes and signatures. Using EdDSA signatures over RSA saves time, money and resources. EdDSA is also more secure than RSA.

EdDSA is deterministic and does not depend on a random number generator for security. The requirement of a nonce and random number generator to create one has lead to known vulnerabilities in ECDSA implementations in the past. In addition, EdDSA does not require expensive point validation for corner cases. Consequently, EdDSA is more secure than ECDSA because it is simply easier to implement.

As long as there are no other requirements that dictate the use of certain signature algorithms, switch to EdDSA if you can. It's fast, it's secure, and it's green.

Conclusion

EdDSA uses elliptic curves but is not compatible with ECDSA. This fact may be confusing but with some basic understanding about the parameters of an EdDSA key, the signature algorithm is easy to work with. Thanks to the EdDSA for JOSE specification it can be used in a standardized way to produce lightweight and secure signatures for JWTs.