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() {