switched signing & validation service to use JWK natively for keys

pull/306/merge
Justin Richer 2013-03-28 16:43:26 -04:00
parent f54dddd8c0
commit 6cc50e7cd5
8 changed files with 113 additions and 154 deletions

View File

@ -3,7 +3,6 @@
*/
package org.mitre.openid.connect.client.keypublisher;
import java.security.PublicKey;
import java.util.Map;
import java.util.UUID;
@ -17,6 +16,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProce
import org.springframework.web.servlet.ModelAndView;
import com.google.common.base.Strings;
import com.nimbusds.jose.jwk.JWK;
/**
* @author jricher
@ -32,9 +32,6 @@ public class ClientKeyPublisher implements BeanDefinitionRegistryPostProcessor {
private String jwkViewName = "jwkKeyList";
private String x509ViewName;
/**
* If either the jwkPublishUrl or x509PublishUrl fields are set on this bean, set up a listener on that URL to publish keys.
*/
@ -82,26 +79,13 @@ public class ClientKeyPublisher implements BeanDefinitionRegistryPostProcessor {
public ModelAndView publishClientJwk() {
// map from key id to key
Map<String, PublicKey> keys = signingAndValidationService.getAllPublicKeys();
Map<String, JWK> keys = signingAndValidationService.getAllPublicKeys();
// TODO: check if keys are empty, return a 404 here or just an empty list?
return new ModelAndView(jwkViewName, "keys", keys);
}
/**
* Return a view to publish all keys in x509 format. Only used if x509publishUrl is set.
* @return
*/
public ModelAndView publishClientx509() {
// map from key id to key
Map<String, PublicKey> keys = signingAndValidationService.getAllPublicKeys();
// TODO: check if keys are empty, return a 404 here or just an empty list?
return new ModelAndView(x509ViewName, "keys", keys);
}
/**
* @return the jwkPublishUrl
*/

View File

@ -24,6 +24,14 @@ public class JWKSetKeyStore implements InitializingBean {
private Resource location;
public JWKSetKeyStore() {
}
public JWKSetKeyStore(JWKSet jwkSet) {
this.jwkSet = jwkSet;
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@ -80,8 +88,7 @@ public class JWKSetKeyStore implements InitializingBean {
}
/**
* Pass through to underlying JwK set and return its keys.
* @return
* Get the list of keys in this keystore. This is a passthrough to the underlying JWK Set
*/
public List<JWK> getKeys() {
return jwkSet.getKeys();

View File

@ -28,7 +28,7 @@ public interface JwtSigningAndValidationService {
/**
* Get all public keys for this service, mapped by their Key ID
*/
public Map<String, PublicKey> getAllPublicKeys();
public Map<String, JWK> getAllPublicKeys();
/**
* Checks the signature of the given JWT against all configured signers,

View File

@ -16,10 +16,8 @@
package org.mitre.jwt.signer.service.impl;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mitre.jose.keystore.JWKSetKeyStore;
@ -47,6 +45,7 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid
// map of identifier to signer
private Map<String, JWSSigner> signers = new HashMap<String, JWSSigner>();
// map of identifier to verifier
private Map<String, JWSVerifier> verifiers = new HashMap<String, JWSVerifier>();
@ -56,22 +55,50 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid
private JWSAlgorithm defaultAlgorithm;
private JWKSetKeyStore keyStore;
// map of identifier to key
private Map<String, JWK> keys = new HashMap<String, JWK>();
/**
* default constructor
* Build this service based on the keys given. All public keys will be used
* to make verifiers, all private keys will be used to make signers.
*
* @param keys
* A map of key identifier to key
*
* @throws InvalidKeySpecException
* If the keys in the JWKs are not valid
* @throws NoSuchAlgorithmException
* If there is no appropriate algorithm to tie the keys to.
*/
public DefaultJwtSigningAndValidationService() {
}
public DefaultJwtSigningAndValidationService(Map<String, JWK> keys) throws NoSuchAlgorithmException, InvalidKeySpecException {
this.keys = keys;
buildSignersAndVerifiers();
}
/**
* Create a new validation service from the given set of verifiers (no signing)
* Build this service based on the given keystore. All keys must have a key
* id ({@code kid}) field in order to be used.
*
* @param keyStore
* the keystore to load all keys from
*
* @throws InvalidKeySpecException
* If the keys in the JWKs are not valid
* @throws NoSuchAlgorithmException
* If there is no appropriate algorithm to tie the keys to.
*/
public DefaultJwtSigningAndValidationService(Map<String, JWSVerifier> verifiers) {
this.verifiers = verifiers;
}
public DefaultJwtSigningAndValidationService(JWKSetKeyStore keyStore) throws NoSuchAlgorithmException, InvalidKeySpecException {
// convert all keys in the keystore to a map based on key id
for (JWK key : keyStore.getKeys()) {
if (!Strings.isNullOrEmpty(key.getKeyID())) {
this.keys.put(key.getKeyID(), key);
} else {
throw new IllegalArgumentException("Tried to load a key from a keystore without a 'kid' field: " + key);
}
}
buildSignersAndVerifiers();
}
/**
* @return the defaultSignerKeyId
*/
@ -105,20 +132,7 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid
return null;
}
}
/**
* @return the keyStore
*/
public JWKSetKeyStore getKeyStore() {
return keyStore;
}
/**
* @param keyStore the keyStore to set
*/
public void setKeyStore(JWKSetKeyStore keyStore) {
this.keyStore = keyStore;
}
/*
* (non-Javadoc)
*
@ -127,49 +141,54 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid
*/
@Override
public void afterPropertiesSet() throws NoSuchAlgorithmException, InvalidKeySpecException{
if (keyStore != null) {
// if we have a keystore, load it up into our signers
List<JWK> keys = keyStore.getKeys();
for (JWK jwk : keys) {
if (!Strings.isNullOrEmpty(jwk.getKeyID())) {
if (jwk instanceof RSAKey) {
// build RSA signers & verifiers
RSASSASigner signer = new RSASSASigner(((RSAKey) jwk).toRSAPrivateKey());
RSASSAVerifier verifier = new RSASSAVerifier(((RSAKey) jwk).toRSAPublicKey());
signers.put(jwk.getKeyID(), signer);
verifiers.put(jwk.getKeyID(), verifier);
} else if (jwk instanceof ECKey) {
// build EC signers & verifiers
// TODO: add support for EC keys
logger.warn("EC Keys are not yet supported.");
} else if (jwk instanceof OctetSequenceKey) {
// build HMAC signers & verifiers
MACSigner signer = new MACSigner(((OctetSequenceKey) jwk).toByteArray());
MACVerifier verifier = new MACVerifier(((OctetSequenceKey) jwk).toByteArray());
signers.put(jwk.getKeyID(), signer);
verifiers.put(jwk.getKeyID(), verifier);
} else {
logger.warn("Unknown key type: " + jwk);
}
} else {
logger.warn("Found a key with no KeyId: " + jwk);
}
}
if (keys == null) {
throw new IllegalArgumentException("Signing and validation service must have at least one key configured.");
}
buildSignersAndVerifiers();
logger.info("DefaultJwtSigningAndValidationService is ready: " + this.toString());
}
/**
* Build all of the signers and verifiers for this based on the key map.
* @throws InvalidKeySpecException If the keys in the JWKs are not valid
* @throws NoSuchAlgorithmException If there is no appropriate algorithm to tie the keys to.
*/
private void buildSignersAndVerifiers() throws NoSuchAlgorithmException, InvalidKeySpecException {
for (Map.Entry<String, JWK> jwkEntry : keys.entrySet()) {
String id = jwkEntry.getKey();
JWK jwk = jwkEntry.getValue();
if (jwk instanceof RSAKey) {
// build RSA signers & verifiers
RSASSASigner signer = new RSASSASigner(((RSAKey) jwk).toRSAPrivateKey());
RSASSAVerifier verifier = new RSASSAVerifier(((RSAKey) jwk).toRSAPublicKey());
signers.put(id, signer);
verifiers.put(id, verifier);
} else if (jwk instanceof ECKey) {
// build EC signers & verifiers
// TODO: add support for EC keys
logger.warn("EC Keys are not yet supported.");
} else if (jwk instanceof OctetSequenceKey) {
// build HMAC signers & verifiers
MACSigner signer = new MACSigner(((OctetSequenceKey) jwk).toByteArray());
MACVerifier verifier = new MACVerifier(((OctetSequenceKey) jwk).toByteArray());
signers.put(id, signer);
verifiers.put(id, verifier);
} else {
logger.warn("Unknown key type: " + jwk);
}
}
}
/**
* Sign a jwt in place using the configured default signer.
*/
@ -207,19 +226,19 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid
}
@Override
public Map<String, PublicKey> getAllPublicKeys() {
Map<String, PublicKey> keys = new HashMap<String, PublicKey>();
public Map<String, JWK> getAllPublicKeys() {
Map<String, JWK> pubKeys = new HashMap<String, JWK>();
// pull all keys out of the verifiers if we know how
for (String keyId : verifiers.keySet()) {
JWSVerifier verifier = verifiers.get(keyId);
if (verifier instanceof RSASSAVerifier) {
// we know how to do RSA public keys
keys.put(keyId, ((RSASSAVerifier) verifier).getPublicKey());
for (String keyId : keys.keySet()) {
JWK key = keys.get(keyId);
JWK pub = key.toPublicJWK();
if (pub != null) {
pubKeys.put(keyId, pub);
}
}
return keys;
return pubKeys;
}
}

View File

@ -13,6 +13,7 @@ import java.util.concurrent.ExecutionException;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.mitre.jose.keystore.JWKSetKeyStore;
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Service;
@ -80,32 +81,9 @@ public class JWKSetSigningAndValidationServiceCacheService {
String jsonString = restTemplate.getForObject(key, String.class);
JWKSet jwkSet = JWKSet.parse(jsonString);
Map<String, JWSVerifier> verifiers = new HashMap<String, JWSVerifier>();
JWKSetKeyStore keyStore = new JWKSetKeyStore(jwkSet);
for (JWK jwk : jwkSet.getKeys()) {
if (jwk.getKeyType().equals(KeyType.RSA)) {
// we can handle RSA
RSAKey rsa = (RSAKey)jwk;
byte[] modulusByte = rsa.getModulus().decode();
BigInteger modulus = new BigInteger(1, modulusByte);
byte[] exponentByte = rsa.getPublicExponent().decode();
BigInteger exponent = new BigInteger(1, exponentByte);
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
KeyFactory factory = KeyFactory.getInstance("RSA");
RSAPublicKey pub = (RSAPublicKey) factory.generatePublic(spec);
JWSVerifier verifier = new RSASSAVerifier(pub);
verifiers.put(jwk.getKeyID(), verifier);
}
}
JwtSigningAndValidationService service = new DefaultJwtSigningAndValidationService(verifiers);
JwtSigningAndValidationService service = new DefaultJwtSigningAndValidationService(keyStore);
return service;

View File

@ -20,11 +20,7 @@ package org.mitre.openid.connect.view;
import java.io.IOException;
import java.io.Writer;
import java.math.BigInteger;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
@ -35,12 +31,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.AbstractView;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.Use;
import com.nimbusds.jose.util.Base64URL;
/**
* @author jricher
@ -58,29 +50,9 @@ public class JwkKeyListView extends AbstractView {
//BiMap<String, PublicKey> keyMap = (BiMap<String, PublicKey>) model.get("keys");
Map<String, PublicKey> keys = (Map<String, PublicKey>) model.get("keys");
Map<String, JWK> keys = (Map<String, JWK>) model.get("keys");
List<JWK> jwks = new ArrayList<JWK>();
for (String keyId : keys.keySet()) {
PublicKey key = keys.get(keyId);
if (key instanceof RSAPublicKey) {
RSAPublicKey rsa = (RSAPublicKey) key;
BigInteger mod = rsa.getModulus();
BigInteger exp = rsa.getPublicExponent();
// FIXME: this assumes RS256
RSAKey rsaKey = new RSAKey(Base64URL.encode(mod.toByteArray()), Base64URL.encode(exp.toByteArray()), Use.SIGNATURE, JWSAlgorithm.RS256, keyId);
jwks.add(rsaKey);
} // TODO: deal with non-RSA key types
}
JWKSet jwkSet = new JWKSet(jwks);
JWKSet jwkSet = new JWKSet(new ArrayList<JWK>(keys.values()));
try {

View File

@ -15,7 +15,6 @@
******************************************************************************/
package org.mitre.openid.connect.web;
import java.security.PublicKey;
import java.util.Map;
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
@ -23,7 +22,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.nimbusds.jose.jwk.JWK;
@Controller
public class JsonWebKeyEndpoint {
@ -34,8 +34,8 @@ public class JsonWebKeyEndpoint {
@RequestMapping(value = "/jwk", produces = "application/json")
public String getJwk(Model m) {
// map from key id to signer
Map<String, PublicKey> keys = jwtService.getAllPublicKeys();
// map from key id to key
Map<String, JWK> keys = jwtService.getAllPublicKeys();
// TODO: check if keys are empty, return a 404 here or just an empty list?

View File

@ -7,9 +7,8 @@
<property name="location" value="classpath:keystore.jwks" />
</bean>
<bean id="defaultsignerService"
class="org.mitre.jwt.signer.service.impl.DefaultJwtSigningAndValidationService">
<property name="keyStore" ref="defaultKeyStore" />
<bean id="defaultsignerService" class="org.mitre.jwt.signer.service.impl.DefaultJwtSigningAndValidationService">
<constructor-arg name="keyStore" ref="defaultKeyStore" />
<property name="defaultSignerKeyId" value="rsa1" />
<property name="defaultSigningAlgorithmName" value="RS256" />
</bean>