work on EcdsaSigner; and supporting unit test and code.
parent
6c7527aaec
commit
89f9961c1a
|
@ -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() {
|
||||
|
@ -143,22 +207,50 @@ public class EcdsaSigner extends AbstractJwtSigner implements InitializingBean {
|
|||
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) {
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
// split on the dots
|
||||
List<String> parts = Lists.newArrayList(Splitter.on(".").split(
|
||||
jwtString));
|
||||
|
||||
return false;
|
||||
if (parts.size() != 3) {
|
||||
throw new IllegalArgumentException("Invalid JWT format.");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
*/
|
||||
|
|
Binary file not shown.
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,17 +105,17 @@ 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);
|
||||
|
@ -117,14 +123,16 @@ public class KeyStoreTest {
|
|||
return ks;
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void storeKeyPair() throws GeneralSecurityException, IOException {
|
||||
public void storeRsaKeyPair() throws GeneralSecurityException, IOException {
|
||||
|
||||
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
|
||||
|
@ -137,12 +145,59 @@ public class KeyStoreTest {
|
|||
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
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue