diff --git a/pom.xml b/pom.xml
index bd0dec0e..585e4700 100644
--- a/pom.xml
+++ b/pom.xml
@@ -307,12 +307,13 @@
100
90
+
+ io.jsonwebtoken.impl.crypto.ECDSA
+ 80
+ 60
+
-
- xml
- html
-
@@ -374,7 +375,6 @@
-
jdk8
@@ -442,4 +442,20 @@
+
+
+
+ org.codehaus.mojo
+ cobertura-maven-plugin
+ 2.7
+
+
+ xml
+ html
+
+
+
+
+
+
diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java b/src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java
new file mode 100644
index 00000000..d7d6a76e
--- /dev/null
+++ b/src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java
@@ -0,0 +1,177 @@
+package io.jsonwebtoken.impl.crypto;
+
+
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.lang.JOSEException;
+
+
+/**
+ * Elliptic Curve Digital Signature Algorithm (ECDSA) functions and utilities.
+ */
+public class ECDSA {
+
+
+ /**
+ * Returns the expected signature byte array length (R + S parts) for
+ * the specified ECDSA algorithm.
+ *
+ * @param alg The ECDSA algorithm. Must be supported and not
+ * {@code null}.
+ *
+ * @return The expected byte array length for the signature.
+ *
+ * @throws JOSEException If the algorithm is not supported.
+ */
+ public static int getSignatureByteArrayLength(final SignatureAlgorithm alg)
+ throws JOSEException {
+
+ switch (alg) {
+ case ES256: return 64;
+ case ES384: return 96;
+ case ES512: return 132;
+ default:
+ throw new JOSEException("unsupported algorithm: " + alg.name());
+ }
+ }
+
+
+ /**
+ * Transcodes the JCA ASN.1/DER-encoded signature into the concatenated
+ * R + S format expected by ECDSA JWS.
+ *
+ * @param derSignature The ASN1./DER-encoded. Must not be {@code null}.
+ * @param outputLength The expected length of the ECDSA JWS signature.
+ *
+ * @return The ECDSA JWS encoded signature.
+ *
+ * @throws JOSEException If the ASN.1/DER signature format is invalid.
+ */
+ public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int outputLength)
+ throws JOSEException {
+
+ if (derSignature.length < 8 || derSignature[0] != 48) {
+ throw new JOSEException("Invalid ECDSA signature format");
+ }
+
+ int offset;
+ if (derSignature[1] > 0) {
+ offset = 2;
+ } else if (derSignature[1] == (byte) 0x81) {
+ offset = 3;
+ } else {
+ throw new JOSEException("Invalid ECDSA signature format");
+ }
+
+ byte rLength = derSignature[offset + 1];
+
+ int i;
+ for (i = rLength; (i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0); i--) {
+ // do nothing
+ }
+
+ byte sLength = derSignature[offset + 2 + rLength + 1];
+
+ int j;
+ for (j = sLength; (j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0); j--) {
+ // do nothing
+ }
+
+ int rawLen = Math.max(i, j);
+ rawLen = Math.max(rawLen, outputLength / 2);
+
+ if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset
+ || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength
+ || derSignature[offset] != 2
+ || derSignature[offset + 2 + rLength] != 2) {
+ throw new JOSEException("Invalid ECDSA signature format");
+ }
+
+ final byte[] concatSignature = new byte[2 * rawLen];
+
+ System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i);
+ System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j);
+
+ return concatSignature;
+ }
+
+
+
+ /**
+ * Transcodes the ECDSA JWS signature into ASN.1/DER format for use by
+ * the JCA verifier.
+ *
+ * @param jwsSignature The JWS signature, consisting of the
+ * concatenated R and S values. Must not be
+ * {@code null}.
+ *
+ * @return The ASN.1/DER encoded signature.
+ *
+ * @throws JOSEException If the ECDSA JWS signature format is invalid.
+ */
+ public static byte[] transcodeSignatureToDER(byte[] jwsSignature)
+ throws JOSEException {
+
+ int rawLen = jwsSignature.length / 2;
+
+ int i = rawLen;
+
+ while((i > 0) && (jwsSignature[rawLen - i] == 0)) i--;
+
+ int j = i;
+
+ if (jwsSignature[rawLen - i] < 0) {
+ j += 1;
+ }
+
+ int k = rawLen;
+
+ while ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) k--;
+
+ int l = k;
+
+ if (jwsSignature[2 * rawLen - k] < 0) {
+ l += 1;
+ }
+
+ int len = 2 + j + 2 + l;
+
+ if (len > 255) {
+ throw new JOSEException("Invalid ECDSA signature format");
+ }
+
+ int offset;
+
+ final byte derSignature[];
+
+ if (len < 128) {
+ derSignature = new byte[2 + 2 + j + 2 + l];
+ offset = 1;
+ } else {
+ derSignature = new byte[3 + 2 + j + 2 + l];
+ derSignature[1] = (byte) 0x81;
+ offset = 2;
+ }
+
+ derSignature[0] = 48;
+ derSignature[offset++] = (byte) len;
+ derSignature[offset++] = 2;
+ derSignature[offset++] = (byte) j;
+
+ System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i);
+
+ offset += j;
+
+ derSignature[offset++] = 2;
+ derSignature[offset++] = (byte) l;
+
+ System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k);
+
+ return derSignature;
+ }
+
+
+ /**
+ * Prevents public instantiation.
+ */
+ private ECDSA() {}
+}
diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java
index 62097184..172521af 100644
--- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java
+++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java
@@ -15,16 +15,16 @@
*/
package io.jsonwebtoken.impl.crypto;
-import io.jsonwebtoken.SignatureAlgorithm;
-import io.jsonwebtoken.SignatureException;
-import io.jsonwebtoken.lang.Assert;
-
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.PublicKey;
import java.security.Signature;
import java.security.interfaces.ECPublicKey;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.SignatureException;
+import io.jsonwebtoken.lang.Assert;
+
public class EllipticCurveSignatureValidator extends EllipticCurveProvider implements SignatureValidator {
private static final String EC_PUBLIC_KEY_REQD_MSG =
@@ -40,7 +40,8 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple
Signature sig = createSignatureInstance();
PublicKey publicKey = (PublicKey) key;
try {
- return doVerify(sig, publicKey, data, signature);
+ byte[] derSignature = ECDSA.transcodeSignatureToDER(signature);
+ return doVerify(sig, publicKey, data, derSignature);
} catch (Exception e) {
String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage();
throw new SignatureException(msg, e);
diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java
index 4c80268f..41ce6c9b 100644
--- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java
+++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java
@@ -15,15 +15,16 @@
*/
package io.jsonwebtoken.impl.crypto;
-import io.jsonwebtoken.SignatureAlgorithm;
-import io.jsonwebtoken.SignatureException;
-
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.interfaces.ECPrivateKey;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.SignatureException;
+import io.jsonwebtoken.lang.JOSEException;
+
public class EllipticCurveSigner extends EllipticCurveProvider implements Signer {
public EllipticCurveSigner(SignatureAlgorithm alg, Key key) {
@@ -43,14 +44,16 @@ public class EllipticCurveSigner extends EllipticCurveProvider implements Signer
throw new SignatureException("Invalid Elliptic Curve PrivateKey. " + e.getMessage(), e);
} catch (java.security.SignatureException e) {
throw new SignatureException("Unable to calculate signature using Elliptic Curve PrivateKey. " + e.getMessage(), e);
+ } catch (JOSEException e) {
+ throw new SignatureException("Unable to convert signature to JOSE format. " + e.getMessage(), e);
}
}
- protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException {
+ protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JOSEException {
PrivateKey privateKey = (PrivateKey)key;
Signature sig = createSignatureInstance();
sig.initSign(privateKey);
sig.update(data);
- return sig.sign();
+ return ECDSA.transcodeSignatureToConcat(sig.sign(), ECDSA.getSignatureByteArrayLength(alg));
}
}
diff --git a/src/main/java/io/jsonwebtoken/lang/JOSEException.java b/src/main/java/io/jsonwebtoken/lang/JOSEException.java
new file mode 100644
index 00000000..5b611909
--- /dev/null
+++ b/src/main/java/io/jsonwebtoken/lang/JOSEException.java
@@ -0,0 +1,22 @@
+package io.jsonwebtoken.lang;
+
+
+/**
+ * Javascript Object Signing and Encryption (JOSE) exception.
+ */
+public class JOSEException extends Exception {
+
+
+ private static final long serialVersionUID = 1L;
+
+
+ /**
+ * Creates a new JOSE exception with the specified message.
+ *
+ * @param message The exception message.
+ */
+ public JOSEException(final String message) {
+
+ super(message);
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy
index e595bd89..6a7de843 100644
--- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy
@@ -17,16 +17,20 @@ package io.jsonwebtoken.impl.crypto
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.SignatureException
-
-import java.security.InvalidKeyException
-import java.security.PublicKey
-import java.security.Signature
-
+import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.Test
+
+import java.security.*
+import java.security.spec.X509EncodedKeySpec
+
import static org.junit.Assert.*
class EllipticCurveSignatureValidatorTest {
+ static {
+ Security.addProvider(new BouncyCastleProvider())
+ }
+
@Test
void testDoVerifyWithInvalidKeyException() {
@@ -53,4 +57,22 @@ class EllipticCurveSignatureValidatorTest {
assertSame se.cause, ex
}
}
+
+ @Test
+ void ecdsaSignatureComplianceTest() {
+ def fact = KeyFactory.getInstance("ECDSA", "BC");
+ def publicKey = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg="
+ def pub = fact.generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)))
+ def v = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, pub)
+ def verifier = { token ->
+ def signatureStart = token.lastIndexOf('.')
+ def withoutSignature = token.substring(0, signatureStart)
+ def signature = token.substring(signatureStart + 1)
+ assert v.isValid(withoutSignature.getBytes("US-ASCII"), Base64.getUrlDecoder().decode(signature)), "Signature do not match that of other implementations"
+ }
+ //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1
+ verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ")
+ //Test verification for token created using https://github.com/jwt/ruby-jwt/tree/v1.5.4
+ verifier("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn")
+ }
}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy
index 4587f1f6..c353a7dd 100644
--- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy
@@ -17,6 +17,7 @@ package io.jsonwebtoken.impl.crypto
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.SignatureException
+import io.jsonwebtoken.lang.JOSEException
import java.security.InvalidKeyException
import java.security.KeyPair
@@ -79,6 +80,35 @@ class EllipticCurveSignerTest {
}
}
+ @Test
+ void testDoSignWithJoseSignatureFormatException() {
+
+ KeyPair kp = EllipticCurveProvider.generateKeyPair()
+ PublicKey publicKey = kp.getPublic();
+ PrivateKey privateKey = kp.getPrivate();
+
+ String msg = 'foo'
+ final JOSEException ex = new JOSEException(msg)
+
+ def signer = new EllipticCurveSigner(SignatureAlgorithm.ES256, privateKey) {
+ @Override
+ protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JOSEException {
+ throw ex
+ }
+ }
+
+ byte[] bytes = new byte[16]
+ SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes)
+
+ try {
+ signer.sign(bytes)
+ fail();
+ } catch (SignatureException se) {
+ assertEquals se.message, 'Unable to convert signature to JOSE format. ' + msg
+ assertSame se.cause, ex
+ }
+ }
+
@Test
void testDoSignWithJdkSignatureException() {