#27: initial implementation of Elliptic Curve signatures with tests. Still have a bit more code coverage to add before I'm happy.

This commit is contained in:
Les Hazlewood 2015-05-06 22:41:53 -07:00
parent 1bda828a0d
commit 8d86973b4b
8 changed files with 239 additions and 15 deletions

View File

@ -102,7 +102,7 @@
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
<scope>runtime</scope> <!-- Not required at compile time -->
<scope>compile</scope>
<optional>true</optional>
</dependency>

View File

@ -242,7 +242,7 @@ public class DefaultJwtParser implements JwtParser {
String algName = algorithm.getValue();
String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " +
"algorithm, but the specified signing key of type " + key.getClass().getName() +
" may not be used to verify " + algName + " signatures. Because the specified " +
" may not be used to validate " + algName + " signatures. Because the specified " +
"signing key reflects a specific and expected algorithm, and the JWT does not reflect " +
"this algorithm, it is likely that the JWT was not expected and therefore should not be " +
"trusted. Another possibility is that the parser was configured with the incorrect " +

View File

@ -46,7 +46,7 @@ public class DefaultSignatureValidatorFactory implements SignatureValidatorFacto
case ES256:
case ES384:
case ES512:
throw new UnsupportedOperationException("Elliptic Curve digests are not yet supported.");
return new EllipticCurveSignatureValidator(alg, key);
default:
String msg = "Unrecognized algorithm '" + alg.name() + "'. This is a bug. Please submit a ticket " +
"via the project issue tracker.";

View File

@ -46,7 +46,7 @@ public class DefaultSignerFactory implements SignerFactory {
case ES256:
case ES384:
case ES512:
throw new UnsupportedOperationException("Elliptic Curve digests are not yet supported.");
return new EllipticCurveSigner(alg, key);
default:
String msg = "Unrecognized algorithm '" + alg.name() + "'. This is a bug. Please submit a ticket " +
"via the project issue tracker.";

View File

@ -0,0 +1,66 @@
/*
* Copyright (C) 2015 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.lang.Assert;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.util.HashMap;
import java.util.Map;
abstract class EllipticCurveProvider extends SignatureProvider {
private static final Map<SignatureAlgorithm, String> EC_SIG_ALG_NAMES = createEcSigAlgNames();
private static Map<SignatureAlgorithm, String> createEcSigAlgNames() {
Map<SignatureAlgorithm, String> m =
new HashMap<SignatureAlgorithm, String>(); //EC alg name to EC alg signature name
m.put(SignatureAlgorithm.ES256, "SHA256withECDSA");
m.put(SignatureAlgorithm.ES384, "SHA384withECDSA");
m.put(SignatureAlgorithm.ES512, "SHA512withECDSA");
return m;
}
protected EllipticCurveProvider(SignatureAlgorithm alg, Key key) {
super(alg, key);
Assert.isTrue(alg.isEllipticCurve(), "SignatureAlgorithm must be an Elliptic Curve algorithm.");
}
protected Signature createSignatureInstance() {
return newSignatureInstance();
}
protected Signature newSignatureInstance() {
try {
String sigAlgName = EC_SIG_ALG_NAMES.get(alg);
if (sigAlgName == null) {
throw new NoSuchAlgorithmException("No EllipticCurve signature algorithm for algorithm " + alg +
". This is a bug. Please report this to the project issue tracker.");
}
return Signature.getInstance(sigAlgName);
} catch (NoSuchAlgorithmException e) {
String msg = "Unavailable Elliptic Curve Signature algorithm.";
if (!alg.isJdkStandard()) {
msg += " This is not a standard JDK algorithm. Try including BouncyCastle in the runtime classpath.";
}
throw new SignatureException(msg, e);
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (C) 2015 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
public class EllipticCurveSignatureValidator extends EllipticCurveProvider implements SignatureValidator {
public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Key key) {
super(alg, key);
Assert.isTrue(!(key instanceof ECPrivateKey),
"Elliptic Curve signature validation requires an ECPublicKey. ECPrivateKeys may not be used.");
Assert.isTrue(key instanceof ECPublicKey,
"Elliptic Curve Signature validation requires either an ECPublicKey instance.");
}
@Override
public boolean isValid(byte[] data, byte[] signature) {
Signature sig = createSignatureInstance();
PublicKey publicKey = (PublicKey) key;
try {
return doVerify(sig, publicKey, data, signature);
} catch (Exception e) {
String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage();
throw new SignatureException(msg, e);
}
}
protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature)
throws InvalidKeyException, java.security.SignatureException {
sig.initVerify(publicKey);
sig.update(data);
return sig.verify(signature);
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2015 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
public class EllipticCurveSigner extends EllipticCurveProvider implements Signer {
public EllipticCurveSigner(SignatureAlgorithm alg, Key key) {
super(alg, key);
if (!(key instanceof ECPrivateKey)) {
String msg = "Elliptic Curve signatures must be computed using an ECPrivateKey. The specified key of " +
"type " + key.getClass().getName() + " is not an ECPrivateKey.";
throw new IllegalArgumentException(msg);
}
}
@Override
public byte[] sign(byte[] data) {
try {
return doSign(data);
} catch (InvalidKeyException e) {
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);
}
}
protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException {
PrivateKey privateKey = (PrivateKey)key;
Signature sig = createSignatureInstance();
sig.initSign(privateKey);
sig.update(data);
return sig.sign();
}
}

View File

@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.impl.DefaultHeader
import io.jsonwebtoken.impl.DefaultJwsHeader
import io.jsonwebtoken.impl.TextCodec
import org.bouncycastle.jce.ECNamedCurveTable
import org.testng.annotations.Test
import javax.crypto.Mac
@ -34,6 +35,8 @@ import static org.testng.Assert.*
class JwtsTest {
private static final SecureRandom RANDOM = new SecureRandom();
@Test
void testHeaderWithNoArgs() {
def header = Jwts.header()
@ -374,14 +377,38 @@ class JwtsTest {
testRsa(SignatureAlgorithm.RS512, 1024, true);
}
@Test
void testES256() {
testEC(SignatureAlgorithm.ES256)
}
@Test
void testES384() {
testEC(SignatureAlgorithm.ES384)
}
@Test
void testES512() {
testEC(SignatureAlgorithm.ES512)
}
@Test
void testES256WithPrivateKeyValidation() {
try {
testEC(SignatureAlgorithm.ES256, true)
fail("EC private keys cannot be used to validate EC signatures.")
} catch (UnsupportedJwtException e) {
assertEquals e.cause.message, "Elliptic Curve signature validation requires an ECPublicKey. ECPrivateKeys may not be used."
}
}
//Asserts correct/expected behavior discussed in https://github.com/jwtk/jjwt/issues/20
@Test
void testParseClaimsJwsWithUnsignedJwt() {
//create random signing key for testing:
Random random = new SecureRandom();
byte[] key = new byte[64];
random.nextBytes(key);
RANDOM.nextBytes(key);
String notSigned = Jwts.builder().setSubject("Foo").compact();
@ -398,9 +425,8 @@ class JwtsTest {
void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() {
//create random signing key for testing:
Random random = new SecureRandom();
byte[] key = new byte[64];
random.nextBytes(key);
RANDOM.nextBytes(key);
//this is a 'real', valid JWT:
String compact = Jwts.builder().setSubject("Joe").signWith(SignatureAlgorithm.HS256, key).compact();
@ -497,11 +523,7 @@ class JwtsTest {
}
}
static void testRsa(SignatureAlgorithm alg) {
testRsa(alg, 1024, false);
}
static void testRsa(SignatureAlgorithm alg, int keySize, boolean verifyWithPrivateKey) {
static void testRsa(SignatureAlgorithm alg, int keySize=1024, boolean verifyWithPrivateKey=false) {
KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA");
keyGenerator.initialize(keySize);
@ -529,9 +551,8 @@ class JwtsTest {
static void testHmac(SignatureAlgorithm alg) {
//create random signing key for testing:
Random random = new SecureRandom();
byte[] key = new byte[64];
random.nextBytes(key);
RANDOM.nextBytes(key);
def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root':true]
@ -543,5 +564,28 @@ class JwtsTest {
assertEquals token.body, claims
}
static void testEC(SignatureAlgorithm alg, boolean verifyWithPrivateKey=false) {
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", "BC");
g.initialize(ECNamedCurveTable.getParameterSpec(alg.getJcaName()), RANDOM);
KeyPair pair = g.generateKeyPair();
PublicKey publicKey = pair.getPublic()
PrivateKey privateKey = pair.getPrivate()
def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root':true]
String jwt = Jwts.builder().setClaims(claims).signWith(alg, privateKey).compact();
def key = publicKey;
if (verifyWithPrivateKey) {
key = privateKey;
}
def token = Jwts.parser().setSigningKey(key).parse(jwt);
assertEquals token.header, [alg: alg.name()]
assertEquals token.body, claims
}
}