Kind of new to the JWE Decryption thing. I have a server that performs the JWE and sent it to client based on a key that is shared between server and client.
I am using a Jose4j for decrypting and am getting this error
java.lang.NullPointerException: The plaintext payload for the JWE has not been set.
I am using the sample code as shown in this link,Receiver part
https://bitbucket.org/b_c/jose4j/wiki/JWE%20Examples
I don't have any insight into the server am just writing the client. I am confused if the paylaod itself is not coming or that framework is goofing up trying to decrypt.
Any pointers to debug the issue is appreciated
Regards,
Aravind
That particular exception is only thrown from the getCompactSerialization() method when there is no payload set - getCompactSerialization() is the last step on sending/encrypting side to create the JWE. If you are decrypting, you shouldn't be calling that. Maybe you've got an accidental call somewhere? Otherwise, the code your using as well as an example raw JWE value might help troubleshot (and keys, if it's just a test and you can share them).
The JWE needs 2 levels of Decryption before getting plain text payload.
So first for JWE to JWS.
then from JWS to JWT after verifying signature. below code will do that.
// That other party, the receiver, can then use JsonWebEncryption to decrypt the message.
JsonWebEncryption receiverJwe = new JsonWebEncryption();
// Set the compact serialization on new Json Web Encryption object
//This is the received payload JWE payload
receiverJwe.setCompactSerialization(result.toString());
// Symmetric encryption, like we are doing here, requires that both parties have the same key.
// The key will have had to have been securely exchanged out-of-band somehow.
receiverJwe.setKey(secretKeySpec);
// Set the "alg" header, which indicates the key management mode for this JWE.
// In this example we are using the direct key management mode, which means
// the given key will be used directly as the content encryption key.
//receiverJwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.DIRECT);
//receiverJwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256);
// Get the message that was encrypted in the JWE. This step performs the actual decryption steps.
String jwsPayload = receiverJwe.getPlaintextString();
// And do whatever you need to do with the clear text message.
System.out.println("plaintext: " + jwsPayload);
// Create a new JsonWebSignature object
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(jwsPayload);
jws.setKey(secretKeySpec);
boolean signatureVerified = jws.verifySignature();
// Do something useful with the result of signature verification
System.out.println("JWS Signature is valid: " + signatureVerified);
// Get the payload, or signed content, from the JWS
String payload = jws.getPayload();
// Do something useful with the content
System.out.println("JWS payload: " + payload);
Related
I have a token issued by Azure AD. I need to verify the token in my API, which is running on IBM platform.
I am writing the token verifier in Java,using
claims = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(accessToken);
The signingKeyResolver is returning a public key. No issues.
I use the following code to get the public key :
BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(key.getN()));
BigInteger exponent = new BigInteger(1, Base64.getUrlDecoder().decode(key.getE()));
RSAPublicKeySpec publicSpec = new RSAPublicKeySpec(modulus, exponent);
KeyFactory factory = KeyFactory.getInstance("RSA");
Provider provider = factory.getProvider();
PublicKey pubkey = factory.generatePublic(publicSpec);
return factory.generatePublic(publicSpec);
Can any one throw some light on Why the signature is invalid ? I observe one thing when I create the factory, the Provider name shows IBMJCEPLUS. Does it have any impact on the key generation ? If so, how do I create the correct factory for Microsoft issued keys ?
Looking for a general advice in creating the factory..
Several things.
the sample is incomplete, and seems mixed up. Is this the role of consumer doing a signature verification?
You are generating a public key which is not an expected behaviour for the verify method of a consumer, you typically 'use' the JWK retrieved from the JWKS URL provided by the JWT producer (AzureAD) not generate a new public key that was not used to generate the signature? To verify a signature, you need to reproduce the same signature by having all the inputs the server used when they generated it. A signature is simply a representation of the payload to ensure the payload was not modified, i.e. integrity check. This is the purpose of the signature and why you must have the same inputs to generate it on the client, wither you gain the inputs used out-of-band somehow (not how AzureAD works) or you use the JWKS to retrieve the appropriate public key for the verify method.
It appears you also expect there to be encrypted claims, based on the RSA reference in your example. If this is intended you would also require you to decipher the payload to access the cleartext. This requires you to have implemented the appropriate PKI. As a consumer (client) the private key must be generated and never shared (or retrieved). Encryption implemented correctly ensures that only the client intended to read the cleartext data 'can' decipher it with the private key, which it itself generated for this purpose, and only it (the client) has control of the private key. Have you the appropriate PKI to do the encrypted JWT properly? Or are you expecting to only utilise the HMAC-based JWT that has no encryption at all and relies on only signature verification (and therefore all claims are public, not protected).
To summaries, there are 2 distinct modes of JWT; 1) HMAC-based that uses signatures where the same inputs are needed on client to verify the signature as was used on server to generate the signature, and this is for integrity only, there is no encryption to protect data. 2) encryption-based JWT that uses a public key for signature verification and the private key used by clients to decipher the encrypted claims data.
1 requires PKI, 2 doesn't.
2 has no data protection and is useful for stateless claims that are intended to be public and therefore verification of the 'claims' before use is how you gain security assurance. Whereas 1 offers the security characteristic of confidentiality and if the PKI is done correctly (private keys are never shared) then the confidential claims data can have more private use cases.
I'm trying to understand the logic of using JSON web tokens with private/public keys (RS512) when signing a payload of data sent from a client (in this case, a React Native App) to my server.
I thought the whole point of private/public keys was to keep the private key private (on my server) and hand the public key to the person who's successfully logged into the app.
I thought, with each API request to my server, the authenticated user of the app would use the public key to create the JWT (on the client side) and the server would use the private key to verify the signature/payload from the API request.
It seems I have it backwards because everywhere I read, you're supposed to sign a JWT with the private key -- but that goes against my understanding of who's in possession of the keys.
Depending on how the keys are created, some private keys can have a passcode which is supposed to be secret! So if the private key and the secret is out in the open (in client-side code) how secure can that be?
And where does the encryption come in? If the user of the app is sending sensitive data in the API, am I supposed to encrypt the payload and sign it with the JWT on the client side and then let the server verify the JWT signature and decrypt the data?
This tutorial was helpful https://medium.com/#siddharthac6/json-web-token-jwt-the-right-way-of-implementing-with-node-js-65b8915d550e but it seems backwards.
Any explanation would definitely help because all of the on-line tutorials aren't making sense.
Thank you.
With JWT, the possession and the use of the key materials are exactly the same as any other contexts where cypher operations occur.
For signing:
The private key is owned by the issuer and is used to compute the signature.
The public key can be shared with all parties that need to verify the signature.
For encryption:
The private key is owned by the recipient and is used to decrypt the data.
The public key can be shared to any party that want to send sensitive data to the recipient.
The encryption is rarely used with JWT. Most of the time the HTTPS layer is sufficient and the token itself only contain a few information that are not sensitive (datatime, IDs...).
The issuer of the token (the authentication server) has a private key to generate signed tokens (JWS). These tokens are sent to the clients (an API server, a web/native application...).
The clients can verify the token with the public key. The key is usually fetched using a public URI.
If you have sensitive data that shall not be disclosed to a third party (phone numbers, personnal address...), then the encrypted tokens (JWE) is highly recommended.
In this case, each client (i.e. recipient of a token) shall have a private key and the issuer of the token must encrypt the token using the public key of each recipient. This means that the issuer of the token can select the appropriate key for a given client.
Hope this figure adds some clarity.
Solution for JWE in React Native and Node Backend
The hardest part was finding a method that works in both RN and Node because I can't just use any Node library in RN.
I'm transmitting all of the API calls over HTTPS.
Create a JWE to encrypt the token and payload simultaneously.
React Native App Code
import {JWK, JWE} from 'react-native-jose';
/**
* Create JWE encrypted web token
*
* #param payload
* #returns {Promise<string>}
*/
async function createJWEToken(payload = {}) {
// This is the Public Key created at login. It is stored in the App.
// I'm hard-coding the key here just for convenience but normally it
// would be kept in a Keychain, a flat file on the mobile device, or
// in React state to refer to before making the API call.
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApl9FLYsLnP10T98mT70e
qdAeHA8qDU5rmY8YFFlcOcy2q1dijpgfop8WyHu1ULufJJXm0PV20/J9BD2HqTAK
DZ+/qTv4glDJjyIlo/PIhehQJqSrdIim4fjuwkax9FOCuFQ9nesv32hZ6rbFjETe
QSxUPjNzsYGOuULWSR3cI8FuV9InlSZQ7q6dEunLPRf/rZujxiAxGzY8zrMehjM5
LNdl7qDEOsc109Yy3HBbOwUdJyyTg/GRPwklLogw9kkldz5+wMvwOT38IlkO2rCr
qJpqqt1KmxdOQNbeGwNzZiGiuYIdiQWjilq5a5K9e75z+Uivx+G3LfTxSAnebPlE
LwIDAQAB
-----END PUBLIC KEY-----`;
try {
const makeKey = pem => JWK.asKey(pem, 'pem');
const key = await makeKey(publicKey);
// This returns the encrypted JWE string
return await JWE.createEncrypt({
zip: true,
format: 'compact',
}, key).update(JSON.stringify(payload)).final();
} catch (err) {
throw new Error(err.message);
}
}
Node Backend
const keygen = require('generate-rsa-keypair');
const {JWK, JWE} = require('node-jose');
/**
* Create private/public keys for JWE encrypt/decrypt
*
* #returns {Promise<object>}
*
*/
async function createKeys() {
// When user logs in, create a standard RSA key-pair.
// The public key is returned to the user when he logs in.
// The private key stays on the server to decrypt the message with each API call.
// Keys are destroyed when the user logs out.
const keys = keygen();
const publicKey = keys.public;
const privateKey = keys.private;
return {
publicKey,
privateKey
};
}
/**
* Decrypt JWE Web Token
*
* #param input
* #returns {Promise<object>}
*/
async function decryptJWEToken(input) {
// This is the Private Key kept on the server. This was
// the key created along with the Public Key after login.
// The public key was sent to the App and the Private Key
// stays on the server.
// I'm hard-coding the key here just for convenience but
// normally it would be held in a database to
// refer during the API call.
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEApl9FLYsLnP10T98mT70eqdAeHA8qDU5rmY8YFFlcOcy2q1di
jpgfop8WyHu1ULufJJXm0PV20/J9BD2HqTAKDZ+/qTv4glDJjyIlo/PIhehQJqSr
dIim4fjuwkax9FOCuFQ9nesv32hZ6rbFjETeQSxUPjNzsYGOuULWSR3cI8FuV9In
lSZQ7q6dEunLPRf/rZujxiAxGzY8zrMehjM5LNdl7qDEOsc109Yy3HBbOwUdJyyT
g/GRPwklLogw9kkldz5+wMvwOT38IlkO2rCrqJpqqt1KmxdOQNbeGwNzZiGiuYId
iQWjilq5a5K9e75z+Uivx+G3LfTxSAnebPlELwIDAQABAoIBAQCmJ2FkMYhAmhOO
LRMK8ZntB876QN7DeT0WmAT5VaE4jE0mY1gnhp+Zfn53bKzQ2v/9vsNMjsjEtVjL
YlPY0QRJRPBZqG3wX5RcoUKsMaxip3dckHo3IL5h0YVJeucAVmKnimIbE6W03Xdn
ZG94PdMljYr4r9PsQ7JxLOHrFaoj/c7Dc7rd6M5cNtmcozqZsz6zVtqO1PGaNa4p
5mAj9UHtumIb49e3tHxr//JUwZv2Gqik0RKkjkrnUmFpHX4N+f81RLDnKsY4+wyI
bM5Gwq/2t8suZbwfHNFufytaRnRFjk+P6crPIpcfe05Xc+Y+Wq4yL62VY3wSS13C
EeUZ2FXpAoGBANPtw8De96TXsxdHcbmameWv4uepHUrYKq+7H+pJEGIfJf/1wsJ0
Gc6w2AE69WJVvCtTzP9XZmfiIze2sMR/ynhbUl9wOzakFpEh0+AmJUG+lUHOy4k2
Mdmu6GmeIM9azz6EXyfXuSZ39LHowS0Es1xaWRuu5kta73B5efz/hz2tAoGBAMj4
QR87z14tF6dPG+/OVM/hh9H5laKMaKCbesoXjvcRVkvi7sW8MbfxVlaRCpLbsSOs
cvAkc4oPY+iQt8fJWSJ1nwGJ0g7iuObLJh9w6P5C3udCGLcvqNbmQ9r+edy1IDBr
t7pdrFKiPFvaEEqYl06gVSsPCg041N6bRTJ1nEzLAoGAajSOVDqo6lA6bOEd6gDD
PSr+0E+c4WQhSD3Dibqh3jpz5aj4uFBMmptfNIaicGw8x43QfuoC5O6b7ZC9V0wf
YF+LkU6CLijfMk48iuky5Jao3/jNYW7qXofb6woWsTN2BoN52FKwc8nLs9jL7k6b
wB166Hem636f3cLS0moQEWUCgYABWjJN/IALuS/0j0K33WKSt4jLb+uC2YEGu6Ua
4Qe0P+idwBwtNnP7MeOL15QDovjRLaLkXMpuPmZEtVyXOpKf+bylLQE92ma2Ht3V
zlOzCk4nrjkuWmK/d3MzcQzu4EUkLkVhOqojMDZJw/DiH569B7UrAgHmTuCX0uGn
UkVH+wKBgQCJ+z527LXiV1l9C0wQ6q8lrq7iVE1dqeCY1sOFLmg/NlYooO1t5oYM
bNDYOkFMzHTOeTUwbuEbCO5CEAj4psfcorTQijMVy3gSDJUuf+gKMzVubzzmfQkV
syUSjC+swH6T0SiEFYlU1FTqTGKsOM68huorD/HEX64Bt9mMBFiVyA==
-----END RSA PRIVATE KEY-----`;
try {
const makeKey = pem => JWK.asKey(pem, 'pem');
const key = await makeKey(privateKey);
// This returns the decrypted data
return await JWE.createDecrypt(key).decrypt(input);
} catch (err) {
throw new Error(err.message);
}
}
jwt.io does a great job of explaining that there is more than one way to sign the JWT. Users may sign and verify with a single secret, or use a public/private key pair for verifying/signing respectively.
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
Although JWTs can be encrypted to also provide secrecy between
parties, we will focus on signed tokens. Signed tokens can verify the
integrity of the claims contained within it, while encrypted tokens
hide those claims from other parties. When tokens are signed using
public/private key pairs, the signature also certifies that only the
party holding the private key is the one that signed it.
am using jose library provided in http://jwt.io site, am trying to create jwt token using this library, but the generated token is saying invalid signature when pasted in http://jwt.io site and also when trying curl apple developer connect 401 unauthorized response! i don`t what is causing the issue.
// Create the Claims, which will be the content of the JWT
JwtClaims claims = new JwtClaims();
claims.setIssuer("69a6de78-7188-47e3-e053-5b8c7c11a4d1"); // who creates the token and signs it
claims.setAudience("appstoreconnect-v1"); // to whom the token is intended to be sent
claims.setExpirationTimeMinutesInTheFuture(20); // time when the token will expire (10 minutes from now)
claims.setIssuedAtToNow();
claims.setGeneratedJwtId(); // a unique identifier for the token
// Generate an EC key pair, which will be used for signing and verification of the JWT, wrapped in a JWK
EllipticCurveJsonWebKey senderJwk = EcJwkGenerator.generateJwk(EllipticCurves.P256);
// Give the JWK a Key ID (kid), which is just the polite thing to do
senderJwk.setKeyId("-----BEGIN PRIVATE KEY-----\n" +
"*******************" +
"-----END PRIVATE KEY-----");
// So we first create a JsonWebSignature object.
JsonWebSignature jws = new JsonWebSignature();
// The payload of the JWS is JSON content of the JWT Claims
jws.setPayload(claims.toJson());
// The JWT is signed using the sender's private key
jws.setKey(senderJwk.getPrivateKey());
// Set the Key ID (kid) header because it's just the polite thing to do.
// We only have one signing key in this example but a using a Key ID helps
// facilitate a smooth key rollover process
jws.setKeyIdHeaderValue(senderJwk.getKeyId());
// Set the signature algorithm on the JWT/JWS that will integrity protect the claims
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
jws.setHeader("typ","jwt");
// Sign the JWS and produce the compact serialization, which will be the inner JWT/JWS
// representation, which is a string consisting of three dot ('.') separated
// base64url-encoded parts in the form Header.Payload.Signature
String outJwt = jws.getCompactSerialization();
// Now you can do something with the JWT. Like send it to some other party
// over the clouds and through the interwebs.
System.out.println("JWT: " + outJwt);
curl -v -H 'Authorization: Bearer [signed token]'
"https://api.appstoreconnect.apple.com/v1/apps"
I'm not familiar with the library, but it looks like you are signing the token with a new, randomly generated key each time you run the code:
EllipticCurveJsonWebKey senderJwk = EcJwkGenerator.generateJwk(EllipticCurves.P256);
It looks like you are also setting the key ID to be a base-64 encoded private key (perhaps the one you intended to use?). The key ID isn't the key, it's just something that can be used to look it up in a key store, for example (According to Apple, the key ID for their API should be "Your private key ID from App Store Connect").
So I'd guess the reason you're getting an "invalid signature" error is because you are signing the token with a new key each time, and not the one you are using to verify it.
Java: Oracle jre1.8.0_45
Provider: BC, BouncyCastle v1.52
Cipher: AES 256bit keys (security policy installed)
AEAD Mode: GCM
Algo: AES/GCM/NoPadding
I have a perfectly working AES encryption/decryption with the parameters shown above. Then during my debug I added a simple TAMPERING simulation by changing data in the Encrypted buffer before decrypting. I expected an AEADBadTagException to be thrown but it didn't happen. I did NOT use any upedateAAD() yet, we are talking about pure ciphered data pay-load.
I do that tampering simply like this, I overwrite the first byte of the byte[] after it already contains the encrypted data and the 16 extra bytes of the authentication tag.
// set-up for encryption, key, IV, etc...
...
try
{
String sPlainText="The non-encrypted (AES) message.";
byte[] baEncrypted=oCIPH.doFinal(sPlainText.getBytes());
MetaLogbook.info(baEncrypted); // Shows well encrypted buffer
// Tampering simulations
baEncrypted[0]=0x67;
// re-initialize for decryption, same key and IV...
String sDecryptedText=new String(oCIPH.doFinal(baEncrypted),"UTF-8");
MetaLogbook.info(sDecryptedText);
// The above log line shows the plain text with a different first letter
// each time that i change 0x67 in other values. The rest of the message
// matches the plain text on input. I can see the 16 extra bytes of the
// authentication tag appended to the clear text.
}
catch(Exception e)
{
// I expected to come here due to a AEADBadTagException but I never
// come here.
MetaLogbook.error(e);
}
The resulting decrypted text changes at the first character as i change the value that i assign while simulating the tampering. It changes in a non-linear way. While 0x65 produces a 'c', 0x67 produces a '?' and so on. The rest of the plain message remains correct, only the first character of the decrypted output seems affected.
I understood from the standard Java 8 doc of the Cipher class that in AEAD GCM mode the authentication tag is created at encryption time (and it was because I see it in my encrypted output byte[] appended at the end) and verified at decryption time (and I provided the full output of encryption including the 16 bytes tag as decryption input) and if the tag would not verify with that data (including AAD data that i don't use now but will) it would throw a AEADBadTagException. In my code it doesn't do that.
I tried this with data that is a multiple of 16 bytes as well as with data that is not. The result is the same for both. If using the same tampering (0x67) value that first letter in the plain text output changes if the message gets longer but that makes sense. The mentioned erroneous first character 'c' becomes a '6' if i add some bytes to the message so that it is not a multiple of 16. In the used AES/GCM/NoPadding the length must not be a multiple of 16 anyway.
Is this a mis-understanding of the documentation, is there some other method that needs to be called to 'enable' this throw behaviour (I could find any), or doesn't BounceyCastle throw it (I understood that a provider needs to implement the crypto classes ISP such that everything behaves as described in the Java 8 Docs Cipher class.
I could not compare with the SunJCE provider because it doesn't support AES/GCM/NoPadding.
Does anyone has some extra info.
TIA
UPDATE 29/AUG: Added code to show that identical code throws with SunJCE and not with BC provider as part of discussion in comments.
private static void testing()
{
try
{
// Unremark these lines to see it work
//Security.addProvider(new BouncyCastleProvider()); // "BC"
//Cipher oCIPH=Cipher.getInstance("AES/GCM/NoPadding", "BC");
// Unremark these lines to see it fail
Cipher oCIPH=Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
// Make a quick and dirty IV and Symmetric Key
byte[] baIV="EECE34808EF2A9AC".getBytes("UTF-8");
byte[] baKey="010F05E3E0104EB59D10F37EA8D4BB6B".getBytes("UTF-8");
// Make IV and Key (well KeySpec for AES) object. Use IV parspec because
// defaults to 128bit Authentication tag size & works in both GCM & CBC.
IvParameterSpec ps=new IvParameterSpec(baIV);
SecretKeySpec sk=new SecretKeySpec(baKey,"AES");
// Unremakr one line, either shrtline (multiple of 16 bytes) or long line
//String sPlainText="The non-encrypted (AES) message.";
String sPlainText="The non-encrypted (AES) message. Everything after the . makes this NOT a multiple of 16 bytes.";
// Encrypt
oCIPH.init(Cipher.ENCRYPT_MODE, sk, ps);
byte[] baEncrypted=oCIPH.doFinal(sPlainText.getBytes());
// Decrypt
oCIPH.init(Cipher.DECRYPT_MODE, sk, ps);
String sDecryptedText=new String(oCIPH.doFinal(baEncrypted),"UTF-8");
}
catch(Exception e)
{
MetaLogbook.log("Security Tools Exception",e);
}
}
The code above can be run with SunJCE or with BouncyCastle by unremarking the the wanted lines at the top. In BC these code runs and does what it is expected to do. With SunJCE provider unremarked an error is thrown:
class java.security.InvalidAlgorithmParameterException: Unsupported
parameter: javax.crypto.spec.IvParameterSpec#4fccd51b
com.sun.crypto.provider.CipherCore.init (CipherCore.java:509)
com.sun.crypto.provider.AESCipher.engineInit (AESCipher.java:339)
javax.crypto.Cipher.init (Cipher.java:1394) javax.crypto.Cipher.init
(Cipher.java:1327)
The original post had two issues under discussion. One has been solved (the AEADBadTagException) the other remains pending (see UPDATE 29/AUG in original post).
The solved problem:
I had recently to write exception code for the usages of the Key/SecretKey Class with AES to use SecretKeySpec instead. The change induced an error which influence the path the code followed and was corrected now in the context of searching for the non throwing of the AEADBadTagException. The fact that all the rest kept working was because the flow change resulted to initialize both times for Encryption instead for Decryption the second time.
What I don't understand is that decryption worked correctly anyway. AES is a symmetric algorithm but it has an S-Box and Reverse S-Box and one would think that therefore encryption cannot just be used in stead of decryption as in full symmetric ciphers such as DES.
The second issue problem remains pending:
class java.security.InvalidAlgorithmParameterException: Unsupported parameter: javax.crypto.spec.IvParameterSpec#4fccd51b
It can be reproduced by the provided code by just selecting the providers at the top of the code and leave all the rest unchanged. The code works with BC, not with SunJCE.
I see there is a Metalogbook line that may have to be changed in your own logging code.
Although the SunJCE provider isn't one I use for the Ciphering and the GCM problem is solved as far as i am concerned I'll keep following this question to supply more information for the SunJCE throw if needed.
UPDATE: By doing some further digging I found the problem of the IVParamSpec throw. BC accepts this object for both CBC and GCM and defaults the authentication tag to 128bit. The SunJCE on the other hand requires specifically a GCMParamSpec object and an explicit size for the authentication tag for GCM and accepts the IVParamSpec for CBC but not for GCM.
I'm new to JWT, learning through standalone code to understand JWT API's. Below code sign and encrypt JWT token from sender's end and it get validated at receiver's end.
Library: JOSE 0.4.1
package com.one00bytes.jwt;
public class JWTSignEncryption {
public static void main(String[] args) throws Exception {
/***************************SENDER'S END ***********************************/
JwtClaims claims = new JwtClaims();
claims.setAudience("Admins");
claims.setIssuer("CA");
claims.setSubject("users");
claims.setClaim("email", "users#test.com");
claims.setClaim("Country", "Antartica");
System.out.println(claims.toJson());
//SIGNING
RsaJsonWebKey jsonSignKey = RsaJwkGenerator.generateJwk(2048);
JsonWebSignature jws = new JsonWebSignature();
jws.setKey(jsonSignKey.getPrivateKey());
jws.setPayload(claims.toJson());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA512);
String signedJwt = jws.getCompactSerialization();
System.out.println("Signed ::" + signedJwt);
//ENCRYPTING
RsaJsonWebKey keyEncrypt = RsaJwkGenerator.generateJwk(2048);
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey contentEncryptKey = keyGen.generateKey();
JsonWebEncryption jwe = new JsonWebEncryption();
jwe.setKey(keyEncrypt.getPublicKey());
jwe.setPayload(signedJwt);
jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.RSA_OAEP_256);
jwe.setContentEncryptionKey(contentEncryptKey.getEncoded());
jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_256_GCM);
SecureRandom iv = SecureRandom.getInstance("SHA1PRNG");
jwe.setIv(iv.generateSeed(32));
String encryptedJwt = jwe.getCompactSerialization();
System.out.println("Encrypted ::" + encryptedJwt);
/***************************RECEIVER'S END ***********************************/
JwtConsumer consumer = new JwtConsumerBuilder()
.setExpectedAudience("Admins")
.setExpectedIssuer("CA")
.setRequireSubject()
.setDecryptionKey(keyEncrypt.getPrivateKey())
.setVerificationKey(jsonSignKey.getPublicKey())
.build();
JwtClaims receivedClaims = consumer.processToClaims(encryptedJwt);
System.out.println("SUCESS :: JWT Validation :: " + receivedClaims);
}
}
Observing below exception when running this program:
Exception in thread "main" org.jose4j.jwt.consumer.InvalidJwtException: Unable to parse JWT Claim Set JSON: eyJhbGciOiJSUzUxMiJ9.eyJhdWQiOiJBZG1pbnMiLCJpc3MiOiJDQSIsInN1YiI6InVzZXJzIiwiaWF0IjoxNDM0NTM0MDgxLCJleHAiOjE0MzQ1MzQ2ODEsImp0aSI6IjJxUUpuMDVGY3RrLWF1VG1vVktuWXciLCJuYmYiOjE0MzQ1MzM5NjEsImVtYWlsIjoidXNlcnNAMTAwYnl0ZXMuY29tIiwiQ291bnRyeSI6IkFudGFydGljYSIsImhvYmJpZXMiOlsiQmxvZ2dpbmciLCJQbGF5aW5nIGNhcmRzIiwiR2FtZXMiXX0.soY_5Hbam569I-CnUW1F4GWdaqprh-XAOtAMOcb7zZSiRcIhXYUdJjEslrDbwphAP135SvmoXO4nVaVmo-d8oWREFYUeXEDzHbrqHNp7pp5pH6hGTJ5C4uE1UVzZ4bis3g_KEgZvEn31NnV4RcU_oRn2Q4inkrTlYKY-juEtCmpPQ0sSP4GiDbwVIfCj-kxZsKh_i9n28SSK890K3DIGiFWOUDwrnY4Yfr1UffsUS9ovyhtqrOcN4YsJR4XzGPaLehlR-qD7eOdAdmVb8RDtGKufNuCd7Q9OFfeKzBmGITHsvd6IPVYLLCfSCzO6PqQSIzkupl5D6HqoOqID8JZLxA
at org.jose4j.jwt.JwtClaims.<init>(JwtClaims.java:50)
at org.jose4j.jwt.JwtClaims.parse(JwtClaims.java:56)
at org.jose4j.jwt.consumer.JwtConsumer.process(JwtConsumer.java:267)
at org.jose4j.jwt.consumer.JwtConsumer.processToClaims(JwtConsumer.java:115)
at com.one00bytes.jwt.JWTSignEncryption.main(JWTSignEncryption.java:76)
Caused by: org.jose4j.lang.JoseException: Parsing error: org.jose4j.json.internal.json_simple.parser.ParseException: Unexpected character (e) at position 0.
at org.jose4j.json.JsonUtil.parseJson(JsonUtil.java:66)
at org.jose4j.jwt.JwtClaims.<init>(JwtClaims.java:45)
... 4 more
Caused by: org.jose4j.json.internal.json_simple.parser.ParseException: Unexpected character (e) at position 0.
at org.jose4j.json.internal.json_simple.parser.Yylex.yylex(Yylex.java:612)
at org.jose4j.json.internal.json_simple.parser.JSONParser.nextToken(JSONParser.java:269)
at org.jose4j.json.internal.json_simple.parser.JSONParser.parse(JSONParser.java:118)
at org.jose4j.json.internal.json_simple.parser.JSONParser.parse(JSONParser.java:81)
at org.jose4j.json.JsonUtil.parseJson(JsonUtil.java:62)
... 5 more
Signed JWT
eyJhbGciOiJSUzUxMiJ9.eyJhdWQiOiJBZG1pbnMiLCJpc3MiOiJDQSIsInN1YiI6InVzZXJzIiwiZW1haWwiOiJ1c2Vyc0B0ZXN0LmNvbSIsIkNvdW50cnkiOiJBbnRhcnRpY2EifQ.5Xu7v2MosIQmtAOlqfM2PE9eJeT0iZzL9x6RIvqx_PAHKer0ylo-0wT9eON_qX1H_QZekTWMf8ok4fxdZNv2KP_AkNqSKLXYJ65TjPnfcX8-dooDJM9txfRWOFqJWx4yj4CTMPNR6rNhizkC9jUaLisPIjogc_a_61qTSnvHXFnuaYmkovN2Y3WfuXjhUZCH98hodRL_ATg1_SpO0bPb7_N1Z76yrcv0RYQan0Y5kICWYdhHlk8Dw6I2fLMVsl3HiYiRq4XBJE8AY_g742Uq5kTS62PKohg3IjfRa-g2rjgKo1XW2sRLVc7vnns2L3TqESo5vgvorTjKnCTQKuHpIg
Encrypted JWT
eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.lZ2nqCeiPzsPmJShsrDD3uA55-06A649CMtwOyuY9nNzMtUGyzV-G8qc4w4ui1uWrtzypBs5Eyq4GfjnTtVHbcDVkS1HVc3tfxNAPY8dfjVrWNz59HyKt4bCjBdqqhBOdZezLtWB9aoWIwZoHLf4D8aUcVUtDsFELVcScmiQNtzHwvpDHZb4oxRfPl-OuOTkKA23C8lnnDMO1KUy8ZXHD4p0jQKAcaV877gYm8NbHDwOBEf-ItWJOGx2jV60apWd0hKqwfFR2QKD9wmGgXpbFZ08ro7X2fj8rTgKWhDgoBT_JVZdVFhVI4T4RLRDrCJqkyeciXhLm7W_xNhWBXAMrA.94SuB596ZLuUtw53wrofwN5jZXfT5f-ZarJhQc9Mj0M.0Ow5DXfilYX3ty49H4lNMNPljlWAFASc49zljhRSIIUSlmUHLZo0SAezn-n_FdxexAIYLk_FtRgnkMHDEyxJ1V1yHhqa1Jvdb36lTYyptqCJhMkOV1XGn58L4Z9QQmdrIZnn5iHxZ9-N1Jfjs0eoKiLBgR9O7ZEcs7QrWZVT6n_HrGrIloYQu_lFgmk5O7k47_15CVXaFqIohpHXETejoHEwjQj-iTToNRaHWNFAKvlpUBz4mUgk9RSIQCxK1GxxS8wxP44w5G4HdOIjFNwTsRDXeSZy0mU9zTNUCmDEUT9MFESfmVU1nPurdT-VoiPvVklbJZW8Sas0hWgqQkdQdP35nFY1sjCgfMB9iYUeEU-TCE219wkm1XXrLJwLEYZclL_4ckl4zExo2wb3Czwd8f5iO9fBQQWZ4mdwThK4VtZaPs1JEkxwGLI0SHA8Jr-e2PsDrkGEnxs74FsJ5MKluU2ZKvKcGXyQPaaTRa0ecJLD5-YYBuTtxOnU3gM_5aZm97pd_wiPk_h81r5aiwjSfRF3Ihxp37KNPfNOMJoA9xe2F51m1AvmjrOUgSM156LwmFyJFebVfarb9NPtJ_q1wU891sCu2Vmv520BR4QfIc-ayIwTVxLgZSN-BP7PhEJb_x8.XhZpINBxRdFFEgwPTcAgJg
Same code runs seperately for signing and encryption, but didn't run, if I include both.
Please help me to understand what I'm doing wrong.
Thanks In Advance
For a nested JWT (i.e. JWE[JWS[JSON Claims]] which is what you're dong), the "cty" (content type) header of the the JWE is supposed to have a value of "JWT" to indicate that the payload is itself a JWT. The definition of"cty" in the JWT spec, RFC 7519, talks about that a bit more. It helps the consumer/receiver know how to process things.
The exception you're seeing is the result of the library trying to parse the JWS compact serialization, which is the payload of the JWE, as JSON.
According to spec, you really should set the cty header to "JWT" on the JWE, which indicates that the JWE payload is itself a JWT. That can be done with jwe.setHeader(HeaderParameterNames.CONTENT_TYPE, "JWT"); or jwe.setContentTypeHeaderValue("JWT") as of v0.4.2.
You can also tell the JwtConsumer to be a bit more liberal in processing and make a best effort when the cty header isn’t present and the payload doesn’t parse as JSON but can be parsed into a JOSE object. This can be done with .setEnableLiberalContentTypeHandling() on the JwtConsumerBuilder.
Couple more observations:
You don't need to set the content encryption key or the IV on the JWE. The library uses a secure random to generate those for you with the appropriate lengths. So the following should be sufficent,
JsonWebEncryption jwe = new JsonWebEncryption();
jwe.setHeader(HeaderParameterNames.CONTENT_TYPE, "JWT");
jwe.setKey(keyEncrypt.getPublicKey());
jwe.setPayload(signedJwt);
jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.RSA_OAEP_256);
jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_256_GCM);
String encryptedJwt = jwe.getCompactSerialization();
System.out.println("Encrypted ::" + encryptedJwt);
Also, I'm guessing from the use of RSA_OAEP_256 and AES_256_GCM that you're using Bouncy Castle. I'd strongly recommend upgrading to jose4j 0.4.4 due to a security vulnerability that was identified when using the library with the Bouncy Castle security provider. See the Release Notes on v 0.4.4 for more info https://bitbucket.org/b_c/jose4j/wiki/Release%20Notes#!jose4j-044-july-24-2015
A JWT has as its payload, or Message, the UTF-8 representation of the Claims Set. From RFC 7519:
Let the Message be the octets of the UTF-8 representation of the
JWT Claims Set.
This is the case for both signed JWTs (which are JWS objects), and encrypted JWTs (using JWE):
if the JWT is a JWE, create a JWE using the Message as
the plaintext for the JWE; all steps specified in JWE for
creating a JWE MUST be followed.
Accordingly, for verification of an encrypted JWT, the payload is interpreted as a Claims Set:
Else, if the JWT is a JWE, follow the steps specified in
JWE for validating a JWE. Let the Message be the resulting
plaintext.
The mistake you have made in your program is using the serialization of the signed JWT as the payload of a JWE, but then attempting to process the resulting object as n encrypted JWE. Accordingly, the library attempts to interpret a serialized signed JWT (the JWS Flattened Serialization) as a serialized JWT Claims Set (a JSON object). This explains the exception you are getting:
Caused by: org.jose4j.lang.JoseException: Parsing error:
org.jose4j.json.internal.json_simple.parser.ParseException:
Unexpected character (e) at position 0.
It appears that you are attempting to produce a JWT that is both encrypted and authenticated. All JWE algorithms are authenticated encryption algorithms, so there is no need to do anything with JWS to achieve this - an encrypted JWT is sufficient.