Skip to content

Commit a6738d5

Browse files
authored
Merge pull request auth0#92 from auth0/feat-es-verify
Add ES Algorithm Verification
2 parents 4ff0da2 + ad06ef4 commit a6738d5

34 files changed

Lines changed: 1173 additions & 59 deletions

lib/src/main/java/com/auth0/jwtdecodejava/JWTVerifier.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212
class JWTVerifier {
1313
private final Algorithm algorithm;
14-
private final Map<String, Object> claims;
14+
final Map<String, Object> claims;
1515

1616
private JWTVerifier(Algorithm algorithm, Map<String, Object> claims) {
1717
this.algorithm = algorithm;

lib/src/main/java/com/auth0/jwtdecodejava/SignUtils.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
import org.apache.commons.codec.binary.Base64;
55
import org.apache.commons.codec.binary.StringUtils;
66

7-
import java.security.*;
8-
9-
class SignUtils {
7+
abstract class SignUtils {
108

119
/**
1210
* Decodes a given String from it's Base64 string representation into a UTF-8 String.

lib/src/main/java/com/auth0/jwtdecodejava/algorithms/Algorithm.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public abstract class Algorithm {
1313
private final String description;
1414

1515
/**
16-
* Creates a new Algorithms instance using SHA256withRSA. Tokens specify this as "RS256".
16+
* Creates a new Algorithm instance using SHA256withRSA. Tokens specify this as "RS256".
1717
*
1818
* @param publicKey the key to use in the verify instance.
1919
* @return a valid RSA256 Algorithm.
@@ -23,7 +23,7 @@ public static Algorithm RSA256(PublicKey publicKey) {
2323
}
2424

2525
/**
26-
* Creates a new Algorithms instance using SHA384withRSA. Tokens specify this as "RS384".
26+
* Creates a new Algorithm instance using SHA384withRSA. Tokens specify this as "RS384".
2727
*
2828
* @param publicKey the key to use in the verify instance.
2929
* @return a valid RSA384 Algorithm.
@@ -33,7 +33,7 @@ public static Algorithm RSA384(PublicKey publicKey) {
3333
}
3434

3535
/**
36-
* Creates a new Algorithms instance using SHA512withRSA. Tokens specify this as "RS512".
36+
* Creates a new Algorithm instance using SHA512withRSA. Tokens specify this as "RS512".
3737
*
3838
* @param publicKey the key to use in the verify instance.
3939
* @return a valid RSA512 Algorithm.
@@ -43,7 +43,7 @@ public static Algorithm RSA512(PublicKey publicKey) {
4343
}
4444

4545
/**
46-
* Creates a new Algorithms instance using HmacSHA256. Tokens specify this as "HS256".
46+
* Creates a new Algorithm instance using HmacSHA256. Tokens specify this as "HS256".
4747
*
4848
* @param secret the secret to use in the verify instance.
4949
* @return a valid HMAC256 Algorithm.
@@ -53,7 +53,7 @@ public static Algorithm HMAC256(String secret) {
5353
}
5454

5555
/**
56-
* Creates a new Algorithms instance using HmacSHA384. Tokens specify this as "HS384".
56+
* Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384".
5757
*
5858
* @param secret the secret to use in the verify instance.
5959
* @return a valid HMAC384 Algorithm.
@@ -63,7 +63,7 @@ public static Algorithm HMAC384(String secret) {
6363
}
6464

6565
/**
66-
* Creates a new Algorithms instance using HmacSHA512. Tokens specify this as "HS512".
66+
* Creates a new Algorithm instance using HmacSHA512. Tokens specify this as "HS512".
6767
*
6868
* @param secret the secret to use in the verify instance.
6969
* @return a valid HMAC512 Algorithm.
@@ -72,6 +72,36 @@ public static Algorithm HMAC512(String secret) {
7272
return new HMACAlgorithm("HS512", "HmacSHA512", secret);
7373
}
7474

75+
/**
76+
* Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256".
77+
*
78+
* @param publicKey the key to use in the verify instance.
79+
* @return a valid ECDSA256 Algorithm.
80+
*/
81+
public static Algorithm ECDSA256(PublicKey publicKey) {
82+
return new ECDSAAlgorithm("ES256", "SHA256withECDSA", 32, publicKey);
83+
}
84+
85+
/**
86+
* Creates a new Algorithm instance using SHA384withECDSA. Tokens specify this as "ES384".
87+
*
88+
* @param publicKey the key to use in the verify instance.
89+
* @return a valid ECDSA384 Algorithm.
90+
*/
91+
public static Algorithm ECDSA384(PublicKey publicKey) {
92+
return new ECDSAAlgorithm("ES384", "SHA384withECDSA", 48, publicKey);
93+
}
94+
95+
/**
96+
* Creates a new Algorithm instance using SHA512withECDSA. Tokens specify this as "ES512".
97+
*
98+
* @param publicKey the key to use in the verify instance.
99+
* @return a valid ECDSA512 Algorithm.
100+
*/
101+
public static Algorithm ECDSA512(PublicKey publicKey) {
102+
return new ECDSAAlgorithm("ES512", "SHA512withECDSA", 66, publicKey);
103+
}
104+
75105
public static Algorithm none() {
76106
return new NoneAlgorithm();
77107
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.auth0.jwtdecodejava.algorithms;
2+
3+
import javax.crypto.Mac;
4+
import javax.crypto.spec.SecretKeySpec;
5+
import java.security.*;
6+
7+
class CryptoHelper {
8+
9+
boolean verifyMacFor(String algorithm, byte[] secretBytes, byte[] contentBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException {
10+
final Mac mac = Mac.getInstance(algorithm);
11+
mac.init(new SecretKeySpec(secretBytes, algorithm));
12+
byte[] result = mac.doFinal(contentBytes);
13+
return MessageDigest.isEqual(result, signatureBytes);
14+
}
15+
16+
boolean verifySignatureFor(String algorithm, PublicKey publicKey, byte[] contentBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
17+
final Signature s = Signature.getInstance(algorithm);
18+
s.initVerify(publicKey);
19+
s.update(contentBytes);
20+
return s.verify(signatureBytes);
21+
}
22+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.auth0.jwtdecodejava.algorithms;
2+
3+
import com.auth0.jwtdecodejava.exceptions.SignatureVerificationException;
4+
import org.apache.commons.codec.binary.Base64;
5+
6+
import java.security.InvalidKeyException;
7+
import java.security.NoSuchAlgorithmException;
8+
import java.security.PublicKey;
9+
import java.security.SignatureException;
10+
11+
class ECDSAAlgorithm extends Algorithm {
12+
13+
private final CryptoHelper crypto;
14+
private final int ecNumberSize;
15+
private final PublicKey publicKey;
16+
17+
ECDSAAlgorithm(CryptoHelper crypto, String id, String algorithm, int ecNumberSize, PublicKey publicKey) {
18+
super(id, algorithm);
19+
if (publicKey == null) {
20+
throw new IllegalArgumentException("The PublicKey cannot be null");
21+
}
22+
this.ecNumberSize = ecNumberSize;
23+
this.publicKey = publicKey;
24+
this.crypto = crypto;
25+
}
26+
27+
ECDSAAlgorithm(String id, String algorithm, int ecNumberSize, PublicKey publicKey) {
28+
this(new CryptoHelper(), id, algorithm, ecNumberSize, publicKey);
29+
}
30+
31+
PublicKey getPublicKey() {
32+
return publicKey;
33+
}
34+
35+
@Override
36+
public void verify(String[] jwtParts) throws SignatureVerificationException {
37+
try {
38+
String content = String.format("%s.%s", jwtParts[0], jwtParts[1]);
39+
byte[] signature = Base64.decodeBase64(jwtParts[2]);
40+
if (!isDERSignature(signature)) {
41+
signature = JOSEToDER(signature);
42+
}
43+
boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, content.getBytes(), signature);
44+
45+
if (!valid) {
46+
throw new SignatureVerificationException(this);
47+
}
48+
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
49+
throw new SignatureVerificationException(this, e);
50+
}
51+
}
52+
53+
private boolean isDERSignature(byte[] signature) {
54+
// DER Structure: http://crypto.stackexchange.com/a/1797
55+
// Should begin with 0x30 and have exactly the expected length
56+
return signature[0] == 0x30 && signature.length != ecNumberSize * 2;
57+
}
58+
59+
private byte[] JOSEToDER(byte[] joseSignature) throws SignatureException {
60+
if (joseSignature.length != ecNumberSize * 2) {
61+
throw new SignatureException(String.format("The signature length was invalid. Expected %d bytes but received %d", ecNumberSize * 2, joseSignature.length));
62+
}
63+
64+
// Retrieve R and S number's length and padding.
65+
int rPadding = countPadding(joseSignature, 0, ecNumberSize);
66+
int sPadding = countPadding(joseSignature, ecNumberSize, joseSignature.length);
67+
int rLength = ecNumberSize - rPadding;
68+
int sLength = ecNumberSize - sPadding;
69+
70+
int length = 2 + rLength + 2 + sLength;
71+
if (length > 255) {
72+
throw new SignatureException("Invalid ECDSA signature format");
73+
}
74+
75+
byte[] derSignature;
76+
int offset;
77+
if (length > 0x7f) {
78+
derSignature = new byte[3 + length];
79+
derSignature[1] = (byte) 0x81;
80+
offset = 2;
81+
} else {
82+
derSignature = new byte[2 + length];
83+
offset = 1;
84+
}
85+
86+
// DER Structure: http://crypto.stackexchange.com/a/1797
87+
// Header with length info
88+
derSignature[0] = (byte) 0x30;
89+
derSignature[offset++] = (byte) length;
90+
derSignature[offset++] = (byte) 0x02;
91+
derSignature[offset++] = (byte) rLength;
92+
93+
// R number
94+
System.arraycopy(joseSignature, 0, derSignature, offset + (rLength - ecNumberSize), ecNumberSize);
95+
offset += rLength;
96+
97+
// S number length
98+
derSignature[offset++] = (byte) 0x02;
99+
derSignature[offset++] = (byte) sLength;
100+
101+
// S number
102+
System.arraycopy(joseSignature, ecNumberSize, derSignature, offset + (sLength - ecNumberSize), ecNumberSize);
103+
104+
return derSignature;
105+
}
106+
107+
private int countPadding(byte[] bytes, int fromIndex, int toIndex) {
108+
int padding = 0;
109+
while (fromIndex + padding < toIndex && bytes[fromIndex + padding] == 0) {
110+
padding++;
111+
}
112+
return bytes[fromIndex + padding] > 0x7f ? padding : padding - 1;
113+
}
114+
}

lib/src/main/java/com/auth0/jwtdecodejava/algorithms/HMACAlgorithm.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,25 @@
33
import com.auth0.jwtdecodejava.exceptions.SignatureVerificationException;
44
import org.apache.commons.codec.binary.Base64;
55

6-
import javax.crypto.Mac;
7-
import javax.crypto.spec.SecretKeySpec;
86
import java.security.InvalidKeyException;
9-
import java.security.MessageDigest;
107
import java.security.NoSuchAlgorithmException;
118

129
class HMACAlgorithm extends Algorithm {
1310

11+
private final CryptoHelper crypto;
1412
private final String secret;
1513

16-
HMACAlgorithm(String id, String algorithm, String secret) {
14+
HMACAlgorithm(CryptoHelper crypto, String id, String algorithm, String secret) {
1715
super(id, algorithm);
1816
if (secret == null) {
1917
throw new IllegalArgumentException("The Secret cannot be null");
2018
}
2119
this.secret = secret;
20+
this.crypto = crypto;
21+
}
22+
23+
HMACAlgorithm(String id, String algorithm, String secret) {
24+
this(new CryptoHelper(), id, algorithm, secret);
2225
}
2326

2427
String getSecret() {
@@ -28,16 +31,16 @@ String getSecret() {
2831
@Override
2932
public void verify(String[] jwtParts) throws SignatureVerificationException {
3033
try {
31-
Mac mac = Mac.getInstance(getDescription());
32-
mac.init(new SecretKeySpec(secret.getBytes(), getDescription()));
3334
String message = String.format("%s.%s", jwtParts[0], jwtParts[1]);
34-
byte[] result = mac.doFinal(message.getBytes());
35-
boolean valid = MessageDigest.isEqual(result, Base64.decodeBase64(jwtParts[2]));
35+
byte[] signature = Base64.decodeBase64(jwtParts[2]);
36+
boolean valid = crypto.verifyMacFor(getDescription(), secret.getBytes(), message.getBytes(), signature);
37+
3638
if (!valid) {
3739
throw new SignatureVerificationException(this);
3840
}
39-
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
41+
} catch (IllegalStateException | InvalidKeyException | NoSuchAlgorithmException e) {
4042
throw new SignatureVerificationException(this, e);
4143
}
4244
}
45+
4346
}

lib/src/main/java/com/auth0/jwtdecodejava/algorithms/RSAAlgorithm.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,27 @@
33
import com.auth0.jwtdecodejava.exceptions.SignatureVerificationException;
44
import org.apache.commons.codec.binary.Base64;
55

6-
import java.security.*;
6+
import java.security.InvalidKeyException;
7+
import java.security.NoSuchAlgorithmException;
8+
import java.security.PublicKey;
9+
import java.security.SignatureException;
710

811
class RSAAlgorithm extends Algorithm {
912

1013
private final PublicKey publicKey;
14+
private CryptoHelper crypto;
1115

12-
RSAAlgorithm(String id, String algorithm, PublicKey publicKey) {
16+
RSAAlgorithm(CryptoHelper crypto, String id, String algorithm, PublicKey publicKey) {
1317
super(id, algorithm);
1418
if (publicKey == null) {
1519
throw new IllegalArgumentException("The PublicKey cannot be null");
1620
}
1721
this.publicKey = publicKey;
22+
this.crypto = crypto;
23+
}
24+
25+
RSAAlgorithm(String id, String algorithm, PublicKey publicKey) {
26+
this(new CryptoHelper(), id, algorithm, publicKey);
1827
}
1928

2029
PublicKey getPublicKey() {
@@ -25,10 +34,9 @@ PublicKey getPublicKey() {
2534
public void verify(String[] jwtParts) throws SignatureVerificationException {
2635
try {
2736
String content = String.format("%s.%s", jwtParts[0], jwtParts[1]);
28-
Signature s = Signature.getInstance(getDescription());
29-
s.initVerify(publicKey);
30-
s.update(content.getBytes());
31-
boolean valid = s.verify(Base64.decodeBase64(jwtParts[2]));
37+
byte[] signature = Base64.decodeBase64(jwtParts[2]);
38+
boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, content.getBytes(), signature);
39+
3240
if (!valid) {
3341
throw new SignatureVerificationException(this);
3442
}

lib/src/main/java/com/auth0/jwtdecodejava/impl/HeaderDeserializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public HeaderImpl deserialize(JsonParser p, DeserializationContext ctxt) throws
2525
Map<String, JsonNode> tree = p.getCodec().readValue(p, new TypeReference<Map<String, JsonNode>>() {
2626
});
2727
if (tree == null) {
28-
throw new JWTDecodeException("Null map");
28+
throw new JWTDecodeException("Parsing the Header's JSON resulted on a Null map");
2929
}
3030
return new HeaderImpl(tree);
3131
}

lib/src/main/java/com/auth0/jwtdecodejava/impl/PayloadDeserializer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOE
2929
Map<String, JsonNode> tree = p.getCodec().readValue(p, new TypeReference<Map<String, JsonNode>>() {
3030
});
3131
if (tree == null) {
32-
throw new JWTDecodeException("Null map");
32+
throw new JWTDecodeException("Parsing the Payload's JSON resulted on a Null map");
3333
}
3434

3535
String issuer = getString(tree, PublicClaims.ISSUER);
@@ -43,7 +43,7 @@ public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOE
4343
return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree);
4444
}
4545

46-
private String[] getStringOrArray(Map<String, JsonNode> tree, String claimName) throws JWTDecodeException {
46+
String[] getStringOrArray(Map<String, JsonNode> tree, String claimName) throws JWTDecodeException {
4747
JsonNode node = tree.get(claimName);
4848
if (node == null || node.isNull() || !(node.isArray() || node.isTextual())) {
4949
return null;

0 commit comments

Comments
 (0)