diff --git a/server/src/main/java/org/mitre/jwt/signer/impl/EcdsaSigner.java b/server/src/main/java/org/mitre/jwt/signer/impl/EcdsaSigner.java index 1a47bc99e..2ac0d86ad 100644 --- a/server/src/main/java/org/mitre/jwt/signer/impl/EcdsaSigner.java +++ b/server/src/main/java/org/mitre/jwt/signer/impl/EcdsaSigner.java @@ -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 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; } } \ No newline at end of file diff --git a/server/src/main/java/org/mitre/jwt/signer/impl/RsaSigner.java b/server/src/main/java/org/mitre/jwt/signer/impl/RsaSigner.java index 96f4e2e88..3c933e062 100644 --- a/server/src/main/java/org/mitre/jwt/signer/impl/RsaSigner.java +++ b/server/src/main/java/org/mitre/jwt/signer/impl/RsaSigner.java @@ -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) */ diff --git a/server/src/main/java/org/mitre/jwt/signer/service/impl/KeyStore.java b/server/src/main/java/org/mitre/jwt/signer/service/impl/KeyStore.java index 2f5ddcdaf..dc6431d36 100644 --- a/server/src/main/java/org/mitre/jwt/signer/service/impl/KeyStore.java +++ b/server/src/main/java/org/mitre/jwt/signer/service/impl/KeyStore.java @@ -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 + "]"; } - + } diff --git a/server/src/main/resources/keystore.jks b/server/src/main/resources/keystore.jks index b3f5de2df..714d95f28 100644 Binary files a/server/src/main/resources/keystore.jks and b/server/src/main/resources/keystore.jks differ diff --git a/server/src/main/webapp/WEB-INF/spring/application-context.xml b/server/src/main/webapp/WEB-INF/spring/application-context.xml index 1684f0212..f49067e85 100644 --- a/server/src/main/webapp/WEB-INF/spring/application-context.xml +++ b/server/src/main/webapp/WEB-INF/spring/application-context.xml @@ -60,7 +60,7 @@ - + diff --git a/server/src/test/java/org/mitre/jwt/JwtTest.java b/server/src/test/java/org/mitre/jwt/JwtTest.java index 214c7b9f5..c869627b0 100644 --- a/server/src/test/java/org/mitre/jwt/JwtTest.java +++ b/server/src/test/java/org/mitre/jwt/JwtTest.java @@ -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); diff --git a/server/src/test/java/org/mitre/jwt/signer/service/impl/KeyStoreTest.java b/server/src/test/java/org/mitre/jwt/signer/service/impl/KeyStoreTest.java index 1aa39a18d..f07a6cafd 100644 --- a/server/src/test/java/org/mitre/jwt/signer/service/impl/KeyStoreTest.java +++ b/server/src/test/java/org/mitre/jwt/signer/service/impl/KeyStoreTest.java @@ -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(); + } + } + } diff --git a/server/src/test/resources/keystore.jks b/server/src/test/resources/keystore.jks index b3f5de2df..714d95f28 100644 Binary files a/server/src/test/resources/keystore.jks and b/server/src/test/resources/keystore.jks differ