DEV Community

Cover image for Signing and Encryption With JWS and JWE
İbrahim Gündüz
İbrahim Gündüz

Posted on • Originally published at Medium

Signing and Encryption With JWS and JWE

Today, we would like to cover two topics that are often confusing: signing and encryption, along with their concrete implementations. Let’s get started.

1. Signing:

In the digital world, signing data is a method used to ensure the integrity and authenticity of digital information. In other words, it allows us to verify that the data — and its associated proof — have not been altered since they were signed.

In modern web applications, this concept is often implemented using standards such as JSON Web Signature (JWS). JWS provides a compact and secure way to represent digitally signed data using JSON and cryptographic algorithms, making it ideal for transmitting trusted information between systems.

A JWS consists of three components: a header, a payload, and a signature. All components are represented as a Base64URL-encoded string and concatenated using (.) sign.

Base64Encode(header) + "." + 
Base64Encode(payload) + "." + 
Base64Encode(
  HMACSHA256(
    data=encodedHeader + "." + encodedPayload, 
    key=secretKey
  )
)
Enter fullscreen mode Exit fullscreen mode

A typical JWS looks like the example shown below:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoiYWRtaW4ifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Enter fullscreen mode Exit fullscreen mode

1.1. Header

The JWS header includes fields such as the algorithm (alg) used to generate the signature and the key identifier (kid), among others. The signing algorithm can use either symmetric or asymmetric cryptography. You can find the complete list of supported fields in RFC 7515 specs.

A typical JWS header looks like this:

{
  “Alg”: “HS256
}
Enter fullscreen mode Exit fullscreen mode

1.2. Payload

The payload should be an object containing the data to be signed. Since it will only be encoded, not encrypted, it should not include any sensitive information.

{
  “uid”: “ibrahim.gunduz”,
  “exp”: 1759589987057,
}
Enter fullscreen mode Exit fullscreen mode

1.3. Signature

The signature is the final component of a JWS. It contains the cryptographic result of applying the algorithm specified in the header to the encoded header and payload, as shown below.

HMACSHA256(
    data=encodedHeader + "." + encodedPayload, 
    key=secretKey
  )
Enter fullscreen mode Exit fullscreen mode

Finally, each component is encoded using Base64.

Okay… all clear. But where will we use this?

JSON Web Tokens (JWTs):

I guess this is one of the most common cases.

  • An authentication server issues a JWT token using a private or shared key and sends it to the client.
  • The client sends the token with every request to prove identity
  • Other services can verify the signature to confirm the token's validity and integrity.

API Request Signing:

This is a very useful approach for protecting an endpoint against tampered or unauthorized requests.

  • The sender signs the request using a private or shared key.
  • The recipient verifies the signature using the public or shared key to ensure that the request was not altered during transmission and that the sender is authenticated.

Let's look at a concrete Java example to see how data can be signed.

To simplify the creation of a JWS, we can use a well-known JSON Object Signing and Encryption (JOSE) library such as Nimbus JOSE + JWT.

<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>10.5</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

In the following example, the code signs the payload using the algorithm specified in the header and a 32-byte shared secret key. The same key is then used to verify the generated signature shown below.

// 32 byte of shared secret key to be used for HMAC by both parties
byte[] sharedSecretKey = "LbShN3gJt9INZ1bUekak882gAnVzpesd".getBytes();
JWSSigner signer = new MACSigner(sharedSecretKey);

// Prepare JWS header containing the algorithm
JWSHeader header = new JWSHeader(JWSAlgorithm.HS256);

// Prepare the payload
Payload payload = new Payload("{\"uid\": \"ibrahim.gunduz\", \"exp\": 1759589987057}");

// Create JWS object with header and payload
JWSObject jwsObject = new JWSObject(header, payload);

// Sign the JWS object
jwsObject.sign(signer);

// Serialize to compact form, produces something like
// eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiAiaWJyYWhpbS5ndW5kdXoiLCAiZXhwIjogMTc1OTU4OTk4NzA1N30.u8r0GHXtwPnGZEGGZUuKRP1SlLO1WeTfP4PFUOi_zpk
String jwsString = jwsObject.serialize();
Enter fullscreen mode Exit fullscreen mode

And the following code verifies the serialized JWS in jwsString using the same key.

JWSObject jwsObject = JWSObject.parse(jwsString);
JWSVerifier verifier = new MACVerifier(sharedSecretKey);

if (jwsObject.verify(verifier)) {
    System.out.println("Signature is VALID");
} else {
    System.out.println("Signature is INVALID");
}
Enter fullscreen mode Exit fullscreen mode

2. Encryption

It's a broader subject, but in short, encryption is the process of transforming information so that only authorized parties can decrypt it. In the context of JSON object signing and encryption, JWE is a standard that enables the exchange of encrypted data using JSON and base64.

JWE enables data encryption at different levels. Parties can share a secret key to encrypt and decrypt the content, or use an asymmetric encryption algorithm to encrypt the Content Encryption Key (CEK) included in the structure. In this case, the party creating the JWE generates a random CEK, encrypts it with the specified algorithm in the algheader using the public key, and encrypts the content with the algorithm specified in the enc header using the CEK. When it comes to decryption, the party holding the private key decrypts the encrypted CEK with the specified algorithm in the lagheader, and decrypts the content with the encryption algorithm specified in enc header using the decrypted CEK.

In the following section, you can see how a JWE is compactly serialized.

BASE64URL(UTF8(JWE Protected Header)) || '.' ||
      BASE64URL(JWE Encrypted CEK) || '.' ||
      BASE64URL(JWE Initialization Vector) || '.' ||
      BASE64URL(JWE Ciphertext) || '.' ||
      BASE64URL(JWE Authentication Tag)
Enter fullscreen mode Exit fullscreen mode

JWE consists of five main components: the Header, Encrypted Content Encryption Key, Initialization Vector, Ciphertext, and Authentication Tag.

2.1. Header

JWE header is similar to JWS header; however, it contains some additional optional and mandatory parameters. The header includes two mandatory parameters that define the Content Encryption Key (CEK) encryption algorithm (alg) and the content encryption algorithm (enc). While the alg field can be one of the supported symmetric or asymmetric encryption algorithms, the enc field should always be one of the supported symmetric algorithms.

The encryption algorithms supported by JOSE are defined in a separate standard called JSON Web Algorithms (JWA), specified in RFC 7518.

You can find supported CEK encryption algorithms in Section 4.1.of RFC-7518, and content encryption algorithms in Section 5.1 of RFC-7518.

2.2. Encrypted Content Encryption Key

Content Encryption Key (CEK) is a key included in a JWE in encrypted form. It is used to encrypt the content using the algorithm specified in the enc header.

When the alg header is set to "dir", it means the JWE uses direct encryption, and the key (CEK) is already shared between the parties. In this case, the Encrypted Key field is empty, since the key encryption step is skipped.

2.3. Initialization Vector

The Initialization Vector (IV) is a value included in JWE in Base64URL-encoded form. It ensures that encrypting the same content with the same key produces different ciphertexts, preventing repeated or predictable results. The length of the value may depend on the algorithm used to encrypt the content. While some algorithms require an Initialization Vector (IV), others do not.

2.4. Ciphertext

The output of the encryption process.

2.5. Authentication Tag

The Authentication Tag is the last part of a JWE. It is produced by the content encryption algorithm to ensure the integrity and authenticity of the ciphertext and the associated data (such as the protected header).

Okay, I think we’ve covered enough theory. Let’s create an example Java code to encrypt plain text using a shared key.

First, create a random, 32-byte-length secret key to be shared among the parties.

int keyBitLength = EncryptionMethod.A128GCM.cekBitLength();

KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(keyBitLength);

SecretKey sharedKey = keyGen.generateKey();

// You can serialize the key by encoding the sharedKey
//    String serializedKey = Base64URL.encode(sharedKey.getEncoded()).toString();
//    System.out.println(serializedKey);

// Output: 
// pg7QFAuqlLbTN86xLuDGsg
Enter fullscreen mode Exit fullscreen mode

Encrypt the plainText using the shared key.

String plainText = "{“cardToken”: \"5105105105105100”, \"email\": \"john.doe@test.net\"}";

//  Create the JWE header
JWEHeader header = new JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A128GCM);

// Create the JWE object with the payload
Payload payload = new Payload(plainText);

// Create the JWE object and encrypt it
JWEObject jweObject = new JWEObject(header, payload);
jweObject.encrypt(new DirectEncrypter(sharedKey));

String jweString = jweObject.serialize();
Enter fullscreen mode Exit fullscreen mode

And finally, we get an output like the following:

eyJlbmMiOiJBMTI4R0NNIiwiYWxnIjoiZGlyIn0..98EHWOdJqqJLZn5Q.ZYmjDgScIWi0IcYoZwliMU-ZjwOr5CXGqEPhUa9BweOOyIhA0hlQIRLDWkDJpXuTOkHzb_81KTDWfvoE0F9wXVUKwOS3.cjC_Ig6bvxcEO1F3jTolYQ
Enter fullscreen mode Exit fullscreen mode

And this is how we decrypt the JWE string above:

JWEObject jweObject = JWEObject.parse(jweString);
DirectDecrypter decrypter = new DirectDecrypter(sharedKey);
jweObject.decrypt(decrypter);

String decrypted = jweObject.getPayload().toString();
Enter fullscreen mode Exit fullscreen mode

Finally, we’ll see the original plaintext shown below:

{“cardToken”: "5105105105105100”, "email": "john.doe@test.net"}
Enter fullscreen mode Exit fullscreen mode

We can also perform JWE encryption using an asymmetric encryption algorithm, where the party holding the public key encrypts the information and the party holding the private key decrypts it.

First, create a public/private key pair. We haven’t mentioned this area, but there is also another standard from JOSE called JSON Web Key, described in RFC-7517

RSAKey jwk = new RSAKeyGenerator(2048)
                .keyUse(KeyUse.SIGNATURE)
                .keyID(UUID.randomUUID().toString())
                .issueTime(new Date())
                .generate();

Enter fullscreen mode Exit fullscreen mode

Normally, we can use the JWK created at runtime, but to make this example closer to a real-world scenario, let’s assume we need to share the serialized public key with the party that will encrypt the information.

String publicJwk = jwk.toPublicJWK().toString();
// publicJwk: {"kty":"RSA","e":"AQAB","use":"sig","kid":"c1a9d4a7-c115-49b8-853f-431e7d1c8920","iat":1759680456,"n":"wKye7A-oGaQqZC8LPH935Y826BZ39w_DQPEW2q5JExDe7RHHd4x2M8WqLi7ubPm7D9lvsGDCRiee2ogPJFdUXd03ZhehXb8xwkguDcMLAAolj6dgGXE2GK6P-4Z-gFtrMNP1IL0rsxeE9IanoakpUxt_O7qXHsJfc_G4hDm999fNq_zyqlpEA-uzRTwODQdzBlLup-WH5uAUtSoCtdJGecwBp6MkaIRBqZTE_wMuw9VyhWm_UeJ6LfdJVZ1GQ2iKsYCw-2Y7weHU8Ln3bxPPf0nL6wg6ZPO0uCedziyXcK64jDt391asR6Qf2h6-YnkN8iAe68i-dpKz6KQtdOWVKw"}
Enter fullscreen mode Exit fullscreen mode

The party holds the public key, parses the JSON, and encrypts the plaintext to be transmitted.

String plainText = "{“cardToken”: \"5105105105105100”, \"email\": \"john.doe@test.net\"}";
// The publicJwk extracted from JWK previously generated.
String publicJwk = //... 

RSAKey key = RSAKey.parse(publicJwk);

JWEHeader header = new JWEHeader(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM);
Payload payload = new Payload(plainText);
JWEObject jweObject = new JWEObject(header, payload);

jweObject.encrypt(new RSAEncrypter(key));

String jweString = jweObject.serialize();

// Generated JWE
// eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.PRwtWEoWlY8WtWBpLYfVTItGyf79RniV40CGmLv3NlLs0bdPK20HmZKEqzFxJE4wIUnd9IEWxNI6OKvoIikCH8n3LLuUwCNuYEjAcY-eHVLPKL8y6IuVKAR2HPk5OWgJe86P02B4umUt4O4zoXauRHWRRctgp6kklqx-Nbc_SQG8lTJ9lBm3cizjvscIdanNg4243v7DNrrTsV63N2GwtV3yguO29CbpHOpbJJ6lwB3mJ_GVbmvq_IC6Bu7ESg-ip3Yb1cnCDAjal42ppfjWMMootEwyMq_ytVGDMi5eAOXzjgLy63UjKuvd_i4a1sU_5ZLM4mvQ_LTQF52j03zkIA._JsZ35w6PsNEkgSS.afN9P2aLFx-B88ibOrXjDd71ycfbtD54uanS0cAXbVyqInPyeUYjQ72lyYV6iDu1K7EOt_jta1EhGq8BHfcAjsqMIdWG.9rif2zVcVsuCKk3_N6Lxug
Enter fullscreen mode Exit fullscreen mode

Finally, the party that issued the key pair decrypts the JWE, as shown below.

String jwkString = // JWK initially created and serialized
RSAKey jwk = RSAKey.parse(jwkString);

// Ciphertext previously created
String jweString = "eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.PRwtWEoWlY8WtWBpLYfVTItGyf79RniV40CGmLv3NlLs0bdPK20HmZKEqzFxJE4wIUnd9IEWxNI6OKvoIikCH8n3LLuUwCNuYEjAcY-eHVLPKL8y6IuVKAR2HPk5OWgJe86P02B4umUt4O4zoXauRHWRRctgp6kklqx-Nbc_SQG8lTJ9lBm3cizjvscIdanNg4243v7DNrrTsV63N2GwtV3yguO29CbpHOpbJJ6lwB3mJ_GVbmvq_IC6Bu7ESg-ip3Yb1cnCDAjal42ppfjWMMootEwyMq_ytVGDMi5eAOXzjgLy63UjKuvd_i4a1sU_5ZLM4mvQ_LTQF52j03zkIA._JsZ35w6PsNEkgSS.afN9P2aLFx-B88ibOrXjDd71ycfbtD54uanS0cAXbVyqInPyeUYjQ72lyYV6iDu1K7EOt_jta1EhGq8BHfcAjsqMIdWG.9rif2zVcVsuCKk3_N6Lxug";

JWEObject jweObject = JWEObject.parse(jweString);
RSADecrypter decrypter = new RSADecrypter(jwk);

jweObject.decrypt(decrypter);
String decryptedText = jweObject.getPayload().toString();
// Decrypted text
// {“cardToken”: "5105105105105100”, "email": "john.doe@test.net"}
Enter fullscreen mode Exit fullscreen mode

Hope you found it helpful. Thanks for reading!

Credits

Top comments (0)