work on EcdsaSigner; and supporting unit test and code.

pull/59/head
nemonik 2012-02-16 17:47:49 -05:00
parent 6c7527aaec
commit 89f9961c1a
8 changed files with 211 additions and 55 deletions

View File

@ -1,15 +1,27 @@
package org.mitre.jwt.signer.impl;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mitre.jwt.signer.AbstractJwtSigner;
import org.mitre.jwt.signer.service.impl.KeyStore;
import org.springframework.beans.factory.InitializingBean;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
public class EcdsaSigner extends AbstractJwtSigner implements InitializingBean {
/**
@ -63,6 +75,11 @@ public class EcdsaSigner extends AbstractJwtSigner implements InitializingBean {
}
};
private static Log logger = LogFactory.getLog(EcdsaSigner.class);
public static final String KEYPAIR_ALGORITHM = "EC";
public static final String DEFAULT_PASSWORD = "changeit";
private KeyStore keystore;
private String alias;
private String password;
@ -71,10 +88,28 @@ public class EcdsaSigner extends AbstractJwtSigner implements InitializingBean {
private PublicKey publicKey;
private Signature signer;
/**
* Default constructor
*/
public EcdsaSigner() {
this(Algorithm.DEFAULT, null, null, null);
this(Algorithm.DEFAULT, null, null, DEFAULT_PASSWORD);
}
/**
* @param algorithmName
* @param keystore
* @param alias
*/
public EcdsaSigner(String algorithmName, KeyStore keystore, String alias) {
this(algorithmName, keystore, alias, DEFAULT_PASSWORD);
}
/**
* @param algorithmName
* @param keystore
* @param alias
* @param password
*/
public EcdsaSigner(String algorithmName, KeyStore keystore, String alias, String password) {
super(algorithmName);
@ -83,36 +118,65 @@ public class EcdsaSigner extends AbstractJwtSigner implements InitializingBean {
setPassword(password);
try {
signer = Signature.getInstance(Algorithm.getByName(algorithmName).getStandardName());
signer = Signature.getInstance(Algorithm.getByName(algorithmName).getStandardName()); //, PROVIDER)
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* @param algorithmName
* @param publicKey
* @param privateKey
*/
public EcdsaSigner(String algorithmName, PublicKey publicKey, PrivateKey privateKey) {
super(algorithmName);
this.publicKey = publicKey;
this.privateKey = privateKey;
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
KeyPair keyPair = keystore.getKeyPairForAlias(alias, password);
publicKey = keyPair.getPublic();
privateKey = keyPair.getPrivate();
logger.debug( Algorithm.getByName(getAlgorithm()).getStandardName() + " ECDSA Signer ready for business");
}
@Override
protected String generateSignature(String signatureBase) {
/*
1) Generate a digital signature of the UTF-8 representation of the JWS Signing Input
using ECDSA P-256 SHA-256 with the desired private key. The output will be the
EC point (R, S), where R and S are unsigned integers.
2) Turn R and S into byte arrays in big endian order. Each array will be 32 bytes long.
3) Concatenate the two byte arrays in the order R and then S.
4) Base64url encode the resulting 64 byte array.
*/
try {
signer.initSign(privateKey);
signer.update(signatureBase.getBytes("UTF-8"));
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
byte[] sigBytes;
String sig = "";
try {
sigBytes = signer.sign();
sig = new String(Base64.encodeBase64URLSafe(sigBytes));
// strip off any padding
sig = sig.replace("=", "");
} catch (SignatureException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return sig;
}
public String getAlias() {
@ -142,23 +206,51 @@ public class EcdsaSigner extends AbstractJwtSigner implements InitializingBean {
public void setPassword(String password) {
this.password = password;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "EcdsaSigner [keystore=" + keystore + ", alias=" + alias
+ ", password=" + password + ", privateKey=" + privateKey
+ ", publicKey=" + publicKey + ", signer=" + signer + "]";
}
/* (non-Javadoc)
* @see org.mitre.jwt.signer.AbstractJwtSigner#verify(java.lang.String)
*/
@Override
public boolean verify(String jwtString) {
// split on the dots
List<String> parts = Lists.newArrayList(Splitter.on(".").split(
jwtString));
/*
1) Take the Encoded JWS Signature and base64url decode it into a byte array.
If decoding fails, the signed content MUST be rejected.
2) The output of the base64url decoding MUST be a 64 byte array.
3) Split the 64 byte array into two 32 byte arrays. The first array will be R and
the second S. Remember that the byte arrays are in big endian byte order;
please check the ECDSA validator in use to see what byte order it requires.
4) Submit the UTF-8 representation of the JWS Signing Input, R, S and the public
key (x, y) to the ECDSA P-256 SHA-256 validator.
5) If the validation fails, the signed content MUST be rejected.
*/
if (parts.size() != 3) {
throw new IllegalArgumentException("Invalid JWT format.");
}
return false;
String h64 = parts.get(0);
String c64 = parts.get(1);
String s64 = parts.get(2);
String signingInput = h64 + "." + c64;
try {
signer.initVerify(publicKey);
signer.update(signingInput.getBytes("UTF-8"));
signer.verify(s64.getBytes("UTF-8"));
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return true;
}
}

View File

@ -3,13 +3,11 @@ package org.mitre.jwt.signer.impl;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
import org.apache.commons.codec.binary.Base64;
@ -84,6 +82,7 @@ public class RsaSigner extends AbstractJwtSigner implements InitializingBean {
private static Log logger = LogFactory.getLog(RsaSigner.class);
public static final String KEYPAIR_ALGORITHM = "RSA";
public static final String DEFAULT_PASSWORD = "changeit";
private KeyStore keystore;
@ -132,10 +131,13 @@ public class RsaSigner extends AbstractJwtSigner implements InitializingBean {
}
}
/**
* Default constructor
* @param algorithmName
* @param publicKey
* @param privateKey
*/
public RsaSigner(String algorithmName, RSAPublicKey publicKey, RSAPrivateKey privateKey) {
public RsaSigner(String algorithmName, PublicKey publicKey, PrivateKey privateKey) {
super(algorithmName);
this.publicKey = publicKey;
this.privateKey = privateKey;
@ -162,12 +164,10 @@ public class RsaSigner extends AbstractJwtSigner implements InitializingBean {
signer.initSign(privateKey);
signer.update(signatureBase.getBytes("UTF-8"));
} catch (GeneralSecurityException e) {
System.out.println("boooom 1");
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
System.out.println("boooom 2");
e.printStackTrace();
}
@ -235,6 +235,9 @@ public class RsaSigner extends AbstractJwtSigner implements InitializingBean {
+ ", publicKey=" + publicKey + ", signer=" + signer + "]";
}
/* (non-Javadoc)
* @see org.mitre.jwt.signer.AbstractJwtSigner#verify(java.lang.String)
*/
/* (non-Javadoc)
* @see org.mitre.jwt.signer.AbstractJwtSigner#verify(java.lang.String)
*/

View File

@ -103,7 +103,7 @@ public class KeyStore implements InitializingBean {
// Get public key
PublicKey publicKey = cert.getPublicKey();
return new KeyPair(publicKey, (PrivateKey) key);
}
@ -152,5 +152,5 @@ public class KeyStore implements InitializingBean {
return "KeyStore [password=" + password + ", location=" + location
+ ", keystore=" + keystore + "]";
}
}

View File

@ -60,7 +60,7 @@
<jwt-signer:keystore id="defaultKeystore" location="classpath:keystore.jks" password="changeit" />
<jwt-signer:service id="defaultSignerService">
<jwt-signer:rsa bits="256" keystore-ref="defaultKeystore" key-alias="test" password="changeit" />
<jwt-signer:rsa bits="256" keystore-ref="defaultKeystore" key-alias="rsa" password="changeit" />
<jwt-signer:hmac bits="256" passphrase="changeit" />
</jwt-signer:service>

View File

@ -16,6 +16,7 @@ import org.mitre.jwt.signer.impl.HmacSigner;
import org.mitre.jwt.signer.impl.PlaintextSigner;
import org.mitre.jwt.signer.impl.RsaSigner;
import org.mitre.jwt.signer.service.impl.KeyStore;
import org.mitre.jwt.signer.service.impl.KeyStoreTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@ -85,7 +86,12 @@ public class JwtTest {
jwt.getClaims().setIssuer("joe");
jwt.getClaims().setClaim("http://example.com/is_root", Boolean.TRUE);
JwtSigner signer = new RsaSigner(RsaSigner.Algorithm.RS256.toString(), keystore, "test", "changeit");
KeyStoreTest.generateKeyPair(keystore,
RsaSigner.KEYPAIR_ALGORITHM, 2048,
"SHA256WithRSAEncryption", "OpenID Connect Server",
"rsa", RsaSigner.DEFAULT_PASSWORD, 30, 365);
JwtSigner signer = new RsaSigner(RsaSigner.Algorithm.RS256.toString(), keystore, "rsa", RsaSigner.DEFAULT_PASSWORD);
((RsaSigner)signer).afterPropertiesSet();
signer.sign(jwt);

View File

@ -4,12 +4,15 @@ import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
@ -20,6 +23,8 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mitre.jwt.signer.impl.EcdsaSigner;
import org.mitre.jwt.signer.impl.RsaSigner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
@ -27,8 +32,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SuppressWarnings("deprecation")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:test-context.xml" })
@ContextConfiguration(locations = { "classpath:test-context.xml" })
public class KeyStoreTest {
@Autowired
@ -81,17 +85,19 @@ public class KeyStoreTest {
* @throws GeneralSecurityException
* @throws IOException
*/
public static java.security.KeyStore generateRsaKeyPair(KeyStore keystore,
String domainName, String alias, String aliasPassword, int daysNotValidBefore, int daysNotValidAfter)
public static java.security.KeyStore generateKeyPair(KeyStore keystore,
String keyPairAlgorithm, int keySize, String signatureAlgorithm,
String domainName, String alias, String aliasPassword,
int daysNotValidBefore, int daysNotValidAfter)
throws GeneralSecurityException, IOException {
java.security.KeyStore ks = keystore.getKeystore();
KeyPairGenerator rsaKeyPairGenerator = null;
rsaKeyPairGenerator = KeyPairGenerator.getInstance("RSA");
rsaKeyPairGenerator = KeyPairGenerator.getInstance(keyPairAlgorithm);
rsaKeyPairGenerator.initialize(2048);
rsaKeyPairGenerator.initialize(keySize);
KeyPair rsaKeyPair = rsaKeyPairGenerator.generateKeyPair();
// BC sez X509V3CertificateGenerator is deprecated and the docs say to
@ -99,32 +105,56 @@ public class KeyStoreTest {
X509V3CertificateGenerator v3CertGen = createCertificate(domainName,
daysNotValidBefore, daysNotValidAfter);
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) rsaKeyPair.getPrivate();
PrivateKey privateKey = rsaKeyPair.getPrivate();
v3CertGen.setPublicKey(rsaKeyPair.getPublic());
v3CertGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
v3CertGen.setSignatureAlgorithm(signatureAlgorithm);
// BC docs say to use another, but it seemingly isn't included...
X509Certificate certificate = v3CertGen
.generateX509Certificate(rsaPrivateKey);
.generateX509Certificate(privateKey);
// if exist, overwrite
ks.setKeyEntry(alias, rsaPrivateKey, aliasPassword.toCharArray(),
ks.setKeyEntry(alias, privateKey, aliasPassword.toCharArray(),
new java.security.cert.Certificate[] { certificate });
keystore.setKeystore(ks);
return ks;
}
@Test
public void storeKeyPair() throws GeneralSecurityException, IOException {
public void storeRsaKeyPair() throws GeneralSecurityException, IOException {
java.security.KeyStore ks = null;
java.security.KeyStore ks = null;
try {
ks = KeyStoreTest.generateRsaKeyPair(keystore, "OpenID Connect Server", "storeKeyPair", "changeit", 30, 365);
ks = KeyStoreTest.generateKeyPair(keystore,
RsaSigner.KEYPAIR_ALGORITHM, 2048,
"SHA256WithRSAEncryption", "OpenID Connect Server",
"rsa", RsaSigner.DEFAULT_PASSWORD, 30, 365);
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
assertThat(ks, not(nullValue()));
}
@Test
public void storeEcKeyPair() throws GeneralSecurityException, IOException {
java.security.KeyStore ks = null;
try {
ks = KeyStoreTest.generateKeyPair(keystore,
EcdsaSigner.KEYPAIR_ALGORITHM, 256, "SHA1withECDSA",
"OpenID Connect Server", "ec", EcdsaSigner.DEFAULT_PASSWORD, 30,
365);
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
@ -134,15 +164,40 @@ public class KeyStoreTest {
e.printStackTrace();
}
//KeyStoreTest.persistKeystoreToFile(ks, System.getProperty("java.io.tmpdir") + System.getProperty("path.separator") + "keystore.jks" , KeyStore.PASSWORD);
assertThat(ks, not(nullValue()));
}
@Test
public void readKey() throws GeneralSecurityException {
Key key = keystore.getKeystore().getKey("storeKeyPair",
Key key = keystore.getKeystore().getKey("rsa",
KeyStore.PASSWORD.toCharArray());
assertThat(key, not(nullValue()));
}
/**
* Saves the keystore for future use.
*
* @param keystore
* @param path
* @param password
* @throws GeneralSecurityException
* @throws IOException
*/
public static void persistKeystoreToFile(final java.security.KeyStore keystore,
final String path, final String password) throws GeneralSecurityException,
IOException {
FileOutputStream fos = new FileOutputStream(new File(path));
try {
keystore.store(fos, password.toCharArray());
System.out.println("Wrote keystore to " + path);
} finally {
fos.close();
}
}
}