mirror of https://github.com/jwtk/jjwt.git
Fixed ECDSA Signing and verification to use R + S curve points as per spec https://tools.ietf.org/html/rfc7515#page-45
This commit is contained in:
parent
29f980c5c9
commit
a73e0044b8
26
pom.xml
26
pom.xml
|
@ -307,12 +307,13 @@
|
|||
<lineRate>100</lineRate>
|
||||
<branchRate>90</branchRate>
|
||||
</regex>
|
||||
<regex>
|
||||
<pattern>io.jsonwebtoken.impl.crypto.ECDSA</pattern>
|
||||
<lineRate>80</lineRate>
|
||||
<branchRate>60</branchRate>
|
||||
</regex>
|
||||
</regexes>
|
||||
</check>
|
||||
<formats>
|
||||
<format>xml</format>
|
||||
<format>html</format>
|
||||
</formats>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
|
@ -374,7 +375,6 @@
|
|||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>jdk8</id>
|
||||
|
@ -442,4 +442,20 @@
|
|||
</profile>
|
||||
</profiles>
|
||||
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>cobertura-maven-plugin</artifactId>
|
||||
<version>2.7</version>
|
||||
<configuration>
|
||||
<formats>
|
||||
<format>xml</format>
|
||||
<format>html</format>
|
||||
</formats>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -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() {}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
|
|
Loading…
Reference in New Issue