- changed status to open
JWT processing succeeds with modified signature
Hi,
I'm no expert on JOSE so this might be trivial to explain, but I noticed when following your very nice guide on "Validating bearer JWT access tokens" here (thank you for that)
http://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens
jwtProcessor.process succeeds even if I do some modifications at the end of the token.
For example if my Google token ends with something like .....npJlIiLAkQ and I change it to something like .....npJlIiLAkW, jwtProcessor.process(token, ctx) still succeeds.
So my question is if this is the expected behaviour, because in my understanding with this modification I basically change the signature which is an RSA encrypted SHA-256 hash and the validation still succeeds.
Thank you for any clarification!
My code is basically the same as in the tutorial
// Token from Google (abbreviated)
String token = "eyJhbGciOiJSUzI1NiIsImtpZCI.....npJlIiLAkW";
String jwkUri = "https://www.googleapis.com/oauth2/v3/certs";
// Set up a JWT processor to parse the tokens and then check their signature
// and validity time window (bounded by the "iat", "nbf" and "exp" claims)
ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();
// The public RSA keys to validate the signatures will be sourced from the
// OAuth 2.0 server's JWK set, published at a well-known URL. The RemoteJWKSet
// object caches the retrieved keys to speed up subsequent look-ups and can
// also gracefully handle key-rollover
JWKSource keySource = new RemoteJWKSet(new URL(jwkUri));
// The expected JWS algorithm of the access tokens (agreed out-of-band)
JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256;
// Configure the JWT processor with a key selector to feed matching public
// RSA keys sourced from the JWK set URL
JWSKeySelector keySelector = new JWSVerificationKeySelector(expectedJWSAlg, keySource);
jwtProcessor.setJWSKeySelector(keySelector);
// Process the token
SecurityContext ctx = null; // optional context parameter, not required here
JWTClaimsSet claimsSet = jwtProcessor.process(token, ctx);
// Print out the token claims set
System.out.println(claimsSet.toJSONObject());
Comments (8)
-
-
Thanks for the fast reply .. I always changed the last char to a adjacent one e.g. A to B
But I will look into my code as suggested and submit a reproducible test in a couple of days.
-
I created a small demo project here https://bitbucket.org/benji23/demo
If I run the following with my Google id_token, the id_token and modified id_token both get validated.
The code I am using is also listed below.
mvn clean package export TOKEN=eyJhbG.......3bsGmYg java -jar target/demo-0.0.1-SNAPSHOT-jar-with-dependencies.jar $TOKEN
package demo; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.jwk.source.RemoteJWKSet; import com.nimbusds.jose.proc.JWSKeySelector; import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import java.net.URL; import java.security.Security; public class Main { public static void main(String[] args) { if (args.length < 1) { System.out.println("Please provide a token"); return; } Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); // The access token to validate, typically submitted with a HTTP header like // Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InMxIn0.eyJzY3A... String googleToken = args[0]; String googleJwk = "https://www.googleapis.com/oauth2/v3/certs"; try { // 1. original token System.out.println("Original token\n" + googleToken + "\n"); System.out.println("Validate token..."); validateToken(googleToken, googleJwk); // 2. modified token char lastChar = googleToken.charAt(googleToken.length() - 1); char modChar = (char) (lastChar + 1); System.out.println("Modify last char " + lastChar + " => " + modChar + "\n"); String modifiedToken = googleToken.substring(0, googleToken.length() - 1) + modChar; System.out.println("Modified token\n" + modifiedToken + "\n"); System.out.println("Validate token..."); validateToken(modifiedToken, googleJwk); } catch (Exception e) { e.printStackTrace(); } } private static void validateToken(String token, String jwkUri) throws Exception { // Set up a JWT processor to parse the tokens and then check their signature // and validity time window (bounded by the "iat", "nbf" and "exp" claims) ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor(); // The public RSA keys to validate the signatures will be sourced from the // OAuth 2.0 server's JWK set, published at a well-known URL. The RemoteJWKSet // object caches the retrieved keys to speed up subsequent look-ups and can // also gracefully handle key-rollover JWKSource keySource = new RemoteJWKSet(new URL(jwkUri)); // The expected JWS algorithm of the access tokens (agreed out-of-band) JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256; // Configure the JWT processor with a key selector to feed matching public // RSA keys sourced from the JWK set URL JWSKeySelector keySelector = new JWSVerificationKeySelector(expectedJWSAlg, keySource); jwtProcessor.setJWSKeySelector(keySelector); // Process the token SecurityContext ctx = null; // optional context parameter, not required here JWTClaimsSet claimsSet = jwtProcessor.process(token, ctx); // Print out the token claims set System.out.println(claimsSet.toJSONObject()); } private static String hex(byte[] bytes) { StringBuffer hexString = new StringBuffer(); for (int i = 0; i < bytes.length; i++) { hexString.append(Integer.toHexString(0xFF & bytes[i])); } return hexString.toString(); } }
-
Thanks for the complete test, we'll now run it.
-
It's now clear what's happening :)
Apparently there's some zero padding at the end of the byte[] signature, so if you change the last char in a way that doesn't disturb the actual bytes, the signature will verify.
This was checked by decoding the modified and the original signature BASE64URL to byte arrays, and then comparing them. Even though the last char was modified, in byte[] form they are still identical. This may not work 100% for all signatures though.
// 1. original token System.out.println("Original token\n" + googleToken + "\n"); System.out.println("Validate token..."); validateToken(googleToken, googleJwk); SignedJWT googleJWT = SignedJWT.parse(googleToken); byte[] googleSig = googleJWT.getSignature().decode(); // 2. modified token char lastChar = googleToken.charAt(googleToken.length() - 1); char modChar = (char) (lastChar + 1); System.out.println("Modify last char " + lastChar + " => " + modChar + "\n"); String modifiedToken = googleToken.substring(0, googleToken.length() - 1) + modChar; SignedJWT modifiedJWT = SignedJWT.parse(modifiedToken); byte[] modifiedSig = modifiedJWT.getSignature().decode(); System.out.println("Signatures equal: " + Arrays.equals(googleSig, modifiedSig)); System.out.println("Modified token\n" + modifiedToken + "\n"); System.out.println("Validate token..."); validateToken(modifiedToken, googleJwk);
I'll try to send you a PR to the test.
-
It looks like RPs are blocked for your repo. Just insert the above array comparison lines from above, and rerun the test.
-
@benji23 @c2id-support Yeah, that's the side effect of padding
If there's padding and you flip one bit in the last char, there is a certain probability that the signature will not be affected.
A lesson of that is to never rely on comparing values on their b64 encoded string.
-
- changed status to invalid
Closing
- Log in to comment
Here is a test that was just added, using a Google ID token where the last signature char was replaced - 336f6bc.
The process call throws a BadJWSException just as expected:
Could you look at your code to see what else might be going on?
Or submit a reproducible test?