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 589a40cf8..e748c2769 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 @@ -5,14 +5,22 @@ import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.Provider; import java.security.PublicKey; +import java.security.Security; import java.security.Signature; import java.security.SignatureException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Enumeration; +import java.util.Iterator; import java.util.List; +import java.util.Map; import org.apache.commons.codec.binary.Base64; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.mitre.jwt.signer.AbstractJwtSigner; import org.mitre.jwt.signer.service.impl.KeyStore; import org.springframework.beans.factory.InitializingBean; @@ -24,7 +32,7 @@ import com.google.common.collect.Lists; * JWT Signer using either the RSA SHA-256, SHA-384, SHA-512 hash algorithm * * @author AANGANES, nemonik - * + * */ public class RsaSigner extends AbstractJwtSigner implements InitializingBean { @@ -32,36 +40,36 @@ public class RsaSigner extends AbstractJwtSigner implements InitializingBean { * an enum for mapping a JWS name to standard algorithm name * * @author nemonik - * + * */ public enum Algorithm { - //Algorithm constants - RS256("SHA256withRSA"), - RS384("SHA384withRSA"), - RS512("SHA512withRSA"); + // Algorithm constants + RS256("SHA256withRSA"), RS384("SHA384withRSA"), RS512("SHA512withRSA"); public static final String DEFAULT = Algorithm.RS256.toString(); - + /** - * Returns the Algorithm for the name - * @param name - * @return - */ - public static Algorithm getByName(String name) { - for (Algorithm correspondingType : Algorithm.values()) { - if (correspondingType.toString().equals(name)) { - return correspondingType; - } - } - - // corresponding type not found - throw new IllegalArgumentException("Algorithm name does not have a corresponding Algorithm"); - } - + * Returns the Algorithm for the name + * + * @param name + * @return + */ + public static Algorithm getByName(String name) { + for (Algorithm correspondingType : Algorithm.values()) { + if (correspondingType.toString().equals(name)) { + return correspondingType; + } + } + + // corresponding type not found + throw new IllegalArgumentException( + "Algorithm name does not have a corresponding Algorithm"); + } + private final String standardName; - - /** + + /** * Constructor of Algorithm * * @param standardName @@ -69,26 +77,31 @@ public class RsaSigner extends AbstractJwtSigner implements InitializingBean { Algorithm(String standardName) { this.standardName = standardName; } - - /** - * Return the Java standard algorithm name - * @return - */ - public String getStandardName() { - return standardName; - } + + /** + * Return the Java standard algorithm name + * + * @return + */ + public String getStandardName() { + return standardName; + } }; - private static Log logger = LogFactory.getLog(RsaSigner.class); - + private static Log logger = LogFactory.getLog(RsaSigner.class); + public static final String DEFAULT_PASSWORD = "changeit"; - + + static { + Security.addProvider(new BouncyCastleProvider()); + } + private KeyStore keystore; private String alias; private String password; - - private PrivateKey privateKey; - private PublicKey publicKey; + + private RSAPrivateKey privateKey; + private RSAPublicKey publicKey; private Signature signer; /** @@ -106,14 +119,15 @@ public class RsaSigner extends AbstractJwtSigner implements InitializingBean { public RsaSigner(String algorithmName, KeyStore keystore, String alias) { this(algorithmName, keystore, alias, DEFAULT_PASSWORD); } - + /** * @param algorithmName * @param keystore * @param alias * @param password */ - public RsaSigner(String algorithmName, KeyStore keystore, String alias, String password) { + public RsaSigner(String algorithmName, KeyStore keystore, String alias, + String password) { super(algorithmName); setKeystore(keystore); @@ -121,22 +135,23 @@ public class RsaSigner extends AbstractJwtSigner implements InitializingBean { setPassword(password); try { - signer = Signature.getInstance(Algorithm.getByName(algorithmName).getStandardName()); + signer = Signature.getInstance(Algorithm.getByName(algorithmName) + .getStandardName()); } catch (NoSuchAlgorithmException e) { // TODO Auto-generated catch block e.printStackTrace(); } } - + @Override public void afterPropertiesSet() throws Exception { KeyPair keyPair = keystore.getKeyPairForAlias(alias, password); - - publicKey = keyPair.getPublic(); - privateKey = keyPair.getPrivate(); - + + publicKey = (RSAPublicKey) keyPair.getPublic(); + privateKey = (RSAPrivateKey) keyPair.getPrivate(); + logger.debug("RSA Signer ready for business"); - + } @Override @@ -178,7 +193,7 @@ public class RsaSigner extends AbstractJwtSigner implements InitializingBean { public String getAlias() { return alias; } - + public KeyStore getKeystore() { return keystore; } @@ -203,7 +218,9 @@ public class RsaSigner extends AbstractJwtSigner implements InitializingBean { this.password = password; } - /* (non-Javadoc) + /* + * (non-Javadoc) + * * @see java.lang.Object#toString() */ @Override 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 2d354ae9f..10ae49a7f 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 @@ -1,18 +1,35 @@ package org.mitre.jwt.signer.service.impl; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.Provider; import java.security.PublicKey; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Date; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.bouncycastle.jce.X509Principal; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.x509.X509V3CertificateGenerator; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; -import org.springframework.util.StringUtils; /** * Creates and manages a JCE KeyStore @@ -20,18 +37,165 @@ import org.springframework.util.StringUtils; * @author nemonik * */ +@SuppressWarnings("deprecation") public class KeyStore implements InitializingBean { - // TODO: Doesn't have a provider attribute, getter/setter. Not sure we need. - private static Log logger = LogFactory.getLog(KeyStore.class); - private static final String TYPE = java.security.KeyStore.getDefaultType(); - private static final String PASSWORD = "changeit"; + public static final String TYPE = "BKS"; + public static final String PASSWORD = "changeit"; + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * Creates a certificate. + * + * @param domainName + * @param daysNotValidBefore + * @param daysNotValidAfter + * @return + */ + private static X509V3CertificateGenerator createCertificate( + String domainName, int daysNotValidBefore, int daysNotValidAfter) { + // BC docs say to use another, but it seemingly isn't included... + X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator(); + + v3CertGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + v3CertGen.setIssuerDN(new X509Principal("CN=" + domainName + + ", OU=None, O=None L=None, C=None")); + v3CertGen.setNotBefore(new Date(System.currentTimeMillis() + - (1000L * 60 * 60 * 24 * daysNotValidBefore))); + v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + + (1000L * 60 * 60 * 24 * daysNotValidAfter))); + v3CertGen.setSubjectDN(new X509Principal("CN=" + domainName + + ", OU=None, O=None L=None, C=None")); + return v3CertGen; + } + + /** + * Create an RSA KeyPair and insert into specified KeyStore + * + * @param location + * @param domainName + * @param alias + * @param keystorePassword + * @param aliasPassword + * @param daysNotValidBefore + * @param daysNotValidAfter + * @return + * @throws GeneralSecurityException + * @throws IOException + */ + public static java.security.KeyStore generateRsaKeyPair(String location, + String domainName, String alias, String keystorePassword, + String aliasPassword, int daysNotValidBefore, int daysNotValidAfter) + throws GeneralSecurityException, IOException { + + java.security.KeyStore ks = loadJceKeyStore(location, keystorePassword); + + KeyPairGenerator rsaKeyPairGenerator = KeyPairGenerator + .getInstance("RSA"); + rsaKeyPairGenerator.initialize(2048); + KeyPair rsaKeyPair = rsaKeyPairGenerator.generateKeyPair(); + + X509V3CertificateGenerator v3CertGen = createCertificate(domainName, + daysNotValidBefore, daysNotValidAfter); + + RSAPublicKey rsaPublicKey = (RSAPublicKey) rsaKeyPair.getPublic(); + RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) rsaKeyPair.getPrivate(); + + v3CertGen.setPublicKey(rsaPublicKey); + v3CertGen.setSignatureAlgorithm("SHA1withRSA"); // "MD5WithRSAEncryption"); + + // BC docs say to use another, but it seemingly isn't included... + X509Certificate certificate = v3CertGen + .generateX509Certificate(rsaPrivateKey); + + // if exist, overwrite + ks.setKeyEntry(alias, rsaPrivateKey, aliasPassword.toCharArray(), + new java.security.cert.Certificate[] { certificate }); + + storeJceKeyStore(location, keystorePassword, ks); + + return ks; + + } + + /** + * Creates or loads a JCE KeyStore + * @param location + * @param keystorePassword + * @return + * @throws GeneralSecurityException + * @throws IOException + */ + private static java.security.KeyStore loadJceKeyStore(String location, String keystorePassword) throws GeneralSecurityException, IOException { + java.security.KeyStore ks = java.security.KeyStore.getInstance(TYPE); + + File keystoreFile = new File(location); + if (!keystoreFile.exists()) { + ks.load(null, null); + } else { + InputStream ios = new FileInputStream(keystoreFile); + try { + ks.load(ios, keystorePassword.toCharArray()); + logger.info("Loaded keystore from " + location); + } finally { + ios.close(); + } + } + + return ks; + } + + public static void main(String[] args) { + + //TODO create a cmd-line to create the KeyStore? + + try { + KeyStore.generateRsaKeyPair("/tmp/keystore.jks", + "OpenID Connect Server", "test", KeyStore.PASSWORD, + KeyStore.PASSWORD, 30, 365); + } catch (GeneralSecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Store the JCE KeyStore + * + * @param location + * @param keystorePassword + * @param ks + * @throws FileNotFoundException + * @throws KeyStoreException + * @throws IOException + * @throws NoSuchAlgorithmException + * @throws CertificateException + */ + private static void storeJceKeyStore(String location, + String keystorePassword, java.security.KeyStore ks) + throws FileNotFoundException, KeyStoreException, IOException, + NoSuchAlgorithmException, CertificateException { + File keystoreFile = new File(location); + FileOutputStream fos = new FileOutputStream(keystoreFile); + try { + ks.store(fos, keystorePassword.toCharArray()); + } finally { + fos.close(); + } + + logger.info("Keystore created here: " + keystoreFile.getAbsolutePath()); + } private String password; + private Resource location; - private String type; private java.security.KeyStore keystore; @@ -39,15 +203,7 @@ public class KeyStore implements InitializingBean { * default constructor */ public KeyStore() { - this(PASSWORD, null, TYPE); - } - - /** - * @param password - * @param location - */ - public KeyStore(String password, Resource location) { - this(password, location, TYPE); + this(PASSWORD, null); } /** @@ -57,15 +213,11 @@ public class KeyStore implements InitializingBean { * the password used to unlock the keystore * @param location * the location of the keystore - * @param type - * the type of keystore. See Appendix A in the Java Cryptography - * Architecture API Specification & Reference for information - * about standard keystore types. + */ - public KeyStore(String password, Resource location, String type) { + public KeyStore(String password, Resource location) { setPassword(password); setLocation(location); - setType(type); } /* @@ -75,13 +227,12 @@ public class KeyStore implements InitializingBean { * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ @Override - public void afterPropertiesSet() throws IOException, - GeneralSecurityException { + public void afterPropertiesSet() throws Exception { InputStream inputStream = null; try { - keystore = java.security.KeyStore.getInstance(type); + keystore = java.security.KeyStore.getInstance(TYPE); inputStream = location.getInputStream(); keystore.load(inputStream, this.password.toCharArray()); @@ -91,6 +242,10 @@ public class KeyStore implements InitializingBean { inputStream.close(); } } + + if (keystore.size() == 0) { + throw new Exception("Keystore is empty; it has no entries"); + } } /** @@ -123,6 +278,10 @@ public class KeyStore implements InitializingBean { return null; } + public java.security.KeyStore getKeystore() { + return keystore; + } + public Resource getLocation() { return location; } @@ -131,8 +290,14 @@ public class KeyStore implements InitializingBean { return password; } - public String getType() { - return type; + public Provider getProvider() { + return keystore.getProvider(); + } + + + + public void setKeystore(java.security.KeyStore keystore) { + this.keystore = keystore; } public void setLocation(Resource location) { @@ -147,14 +312,6 @@ public class KeyStore implements InitializingBean { this.password = password; } - public void setType(String type) { - if (StringUtils.hasLength(type)) { - this.type = type; - } else { - throw new IllegalArgumentException("type must not be empty"); - } - } - /* * (non-Javadoc) * @@ -163,7 +320,6 @@ public class KeyStore implements InitializingBean { @Override public String toString() { return "KeyStore [password=" + password + ", location=" + location - + ", type=" + type + ", keystore=" + keystore + "]"; + + ", keystore=" + keystore + "]"; } - } diff --git a/server/src/main/java/org/mitre/jwt/signer/service/impl/KeystoreDefinitionParser.java b/server/src/main/java/org/mitre/jwt/signer/service/impl/KeystoreDefinitionParser.java index 8b202bf04..2ac006286 100644 --- a/server/src/main/java/org/mitre/jwt/signer/service/impl/KeystoreDefinitionParser.java +++ b/server/src/main/java/org/mitre/jwt/signer/service/impl/KeystoreDefinitionParser.java @@ -52,11 +52,6 @@ public class KeystoreDefinitionParser extends builder.addConstructorArgValue(resource); } } - - String type = element.getAttribute("type"); - if (StringUtils.hasText(type)) { - builder.addConstructorArgValue(type); - } } /* diff --git a/server/src/main/resources/org/mitre/jwt/signer/service/impl/jwt-signer.xsd b/server/src/main/resources/org/mitre/jwt/signer/service/impl/jwt-signer.xsd index 6eadd5419..fb986ea0b 100644 --- a/server/src/main/resources/org/mitre/jwt/signer/service/impl/jwt-signer.xsd +++ b/server/src/main/resources/org/mitre/jwt/signer/service/impl/jwt-signer.xsd @@ -18,7 +18,6 @@ - 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 f65789929..0fd6135fd 100644 --- a/server/src/main/webapp/WEB-INF/spring/application-context.xml +++ b/server/src/main/webapp/WEB-INF/spring/application-context.xml @@ -72,14 +72,13 @@ - - + + - + - diff --git a/server/src/main/webapp/WEB-INF/spring/keystore.jks b/server/src/main/webapp/WEB-INF/spring/keystore.jks index c2f6182ca..f1e0770ea 100644 Binary files a/server/src/main/webapp/WEB-INF/spring/keystore.jks and b/server/src/main/webapp/WEB-INF/spring/keystore.jks differ diff --git a/server/src/test/java/org/mitre/jwt/JwtTest.java b/server/src/test/java/org/mitre/jwt/JwtTest.java index ba6ababb8..d8aaafdd4 100644 --- a/server/src/test/java/org/mitre/jwt/JwtTest.java +++ b/server/src/test/java/org/mitre/jwt/JwtTest.java @@ -4,16 +4,30 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; import java.io.UnsupportedEncodingException; +import java.security.Security; import java.util.Date; import org.junit.Test; +import org.junit.runner.RunWith; import org.mitre.jwt.model.Jwt; import org.mitre.jwt.signer.JwtSigner; 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.JwtSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.KeyStore; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations={"classpath:test-context.xml"}) public class JwtTest { - + +// @Autowired +// KeyStore keystore; + @Test public void testToStringPlaintext() { Jwt jwt = new Jwt(); @@ -81,6 +95,40 @@ public class JwtTest { } +// @Test +// public void testGenerateRsaSignature() { +// Jwt jwt = new Jwt(); +// jwt.getHeader().setType("JWT"); +// jwt.getHeader().setAlgorithm("RS256"); +// jwt.getClaims().setExpiration(new Date(1300819380L * 1000L)); +// jwt.getClaims().setIssuer("joe"); +// jwt.getClaims().setClaim("http://example.com/is_root", Boolean.TRUE); +// +// // sign it +// System.out.println("keystore PROVIDER::" + keystore.getProvider()); +// +// JwtSigner signer = new RsaSigner(RsaSigner.Algorithm.DEFAULT, keystore, "test"); +// +// signer.sign(jwt); +// +// System.out.println("vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"); +// System.out.println("vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"); +// System.out.println("vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"); +// System.out.println(jwt.getSignature()); +// System.out.println("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"); +// System.out.println("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"); +// System.out.println("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"); +// +//// String signature = "p-63Jzz7mgi3H4hvW6MFB7lmPRZjhsL666MYkmpX33Y"; +//// String expected = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ." + signature; +//// +//// String actual = jwt.toString(); +//// +//// assertThat(actual, equalTo(expected)); +//// assertThat(jwt.getSignature(), equalTo(signature)); +// +// } + @Test public void testValidateHmacSignature() { // sign it 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 new file mode 100644 index 000000000..5cae53ffe --- /dev/null +++ b/server/src/test/java/org/mitre/jwt/signer/service/impl/KeyStoreTest.java @@ -0,0 +1,54 @@ +package org.mitre.jwt.signer.service.impl; + +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Key; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@SuppressWarnings("restriction") // I know... +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { + "file:src/main/webapp/WEB-INF/spring/application-context.xml", + "classpath:test-context.xml" }) +public class KeyStoreTest { + + @Autowired + @Qualifier("testKeystore") + KeyStore keystore; + + @Test + public void storeKeyPair() throws GeneralSecurityException, IOException { + + java.security.KeyStore ks = KeyStore.generateRsaKeyPair(keystore + .getLocation().getFile().getPath(), "OpenID Connect Server", + "test", KeyStore.PASSWORD, KeyStore.PASSWORD, 30, 30); + + keystore.setKeystore(ks); + + assertThat(ks, not(nullValue())); + } + + @Test + public void readKey() throws GeneralSecurityException { + + Key key = keystore.getKeystore().getKey("test", + KeyStore.PASSWORD.toCharArray()); + + System.out.println("-----BEGIN PRIVATE KEY-----"); + System.out + .println(new sun.misc.BASE64Encoder().encode(key.getEncoded())); + System.out.println("-----END PRIVATE KEY-----"); + + assertThat(key, not(nullValue())); + } +} diff --git a/server/src/test/resources/keystore.jks b/server/src/test/resources/keystore.jks new file mode 100644 index 000000000..6b21b9027 Binary files /dev/null and b/server/src/test/resources/keystore.jks differ diff --git a/server/src/test/resources/test-context.xml b/server/src/test/resources/test-context.xml index 158aff4dd..fcd72a9b5 100644 --- a/server/src/test/resources/test-context.xml +++ b/server/src/test/resources/test-context.xml @@ -1,7 +1,10 @@ + xmlns:jwt-signer="http://www.mitre.org/schema/openid-connect/jwt-signer" + xsi:schemaLocation= + "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd + http://www.mitre.org/schema/openid-connect/jwt-signer http://www.mitre.org/schema/openid-connect/jwt-signer/jwt-signer.xsd" > @@ -33,4 +36,7 @@ + + + \ No newline at end of file