Nested signed and encrypted JSON Web Token (JWT)

Signing and encryption order

JSON Web Tokens (JWT) can be signed then encrypted to provide confidentiality of the claims.

While it's technically possible to perform the operations in any order to create a nested JWT, senders should first sign the JWT, then encrypt the resulting message.

Why is sign-then-encrypt the preferred order?

  • Prevents attacks in which the signature is stripped, leaving just an encrypted message.

  • Provides privacy for the signer.

  • Signatures over encrypted text are not considered valid in some jurisdictions.

Certain papers advocate applying a second signature after the encryption. This isn't required with standard JWE algorithms due to their use of authenticated encryption.

Producing a nested JWT

Let's create a JWT which is signed (JWS) with the sender's private RSA key and then encrypted (JWE) with the recipient's public RSA key.

For that sender and recipient each must first generate their own RSA key pairs, and distribute the public key of each generated pair to the other party.

Generate sender RSA key pair, make public key available to recipient:

import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.gen.*;

RSAKey senderJWK = new RSAKeyGenerator(2048)
    .keyID("123")
    .keyUse(KeyUse.SIGNATURE)
    .generate();
RSAKey senderPublicJWK = senderJWK.toPublicJWK();

Generate recipient RSA key pair, make public key available to sender:

import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.gen.*;

RSAKey recipientJWK = new RSAKeyGenerator(2048)
    .keyID("456")
    .keyUse(KeyUse.ENCRYPTION)
    .generate();
RSAKey recipientPublicJWK = recipientJWK.toPublicJWK();

The sender signs the JWT with their private key and then encrypts to the recipient:

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jose.jwk.*;

// Create JWT
SignedJWT signedJWT = new SignedJWT(
    new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(senderJWK.getKeyID()).build(),
    new JWTClaimsSet.Builder()
        .subject("alice")
        .issueTime(new Date())
        .issuer("https://c2id.com")
        .build());

// Sign the JWT
signedJWT.sign(new RSASSASigner(senderJWK));

// Create JWE object with signed JWT as payload
JWEObject jweObject = new JWEObject(
    new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
        .contentType("JWT") // required to indicate nested JWT
        .build(),
    new Payload(signedJWT));

// Encrypt with the recipient's public key
jweObject.encrypt(new RSAEncrypter(recipientPublicJWK));

// Serialise to JWE compact form
String jweString = jweObject.serialize();

Consuming a nested JWT

The recipient will first need to decrypt the JWE object, then extract the signed JWT from its payload and verify the signature.

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jose.jwk.*;

// Parse the JWE string
JWEObject jweObject = JWEObject.parse(jweString);

// Decrypt with private key
jweObject.decrypt(new RSADecrypter(recipientJWK));

// Extract payload
SignedJWT signedJWT = jweObject.getPayload().toSignedJWT();

assertNotNull("Payload not a signed JWT", signedJWT);

// Check the signature
assertTrue(signedJWT.verify(new RSASSAVerifier(senderPublicJWK)));

// Retrieve the JWT claims...
assertEquals("alice", signedJWT.getJWTClaimsSet().getSubject());