Skip to content

Commit

Permalink
Generate JWT verification key based on the signing key.
Browse files Browse the repository at this point in the history
[#114128253] https://www.pivotaltracker.com/story/show/114128253

Signed-off-by: Filip Hanik <fhanik@pivotal.io>
  • Loading branch information
Jeremy Coffield authored and fhanik committed Feb 24, 2016
1 parent 2d11569 commit 49097f9
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 169 deletions.
Expand Up @@ -23,11 +23,9 @@ public class KeyPair {


public static final String SIGNING_KEY = "signingKey"; public static final String SIGNING_KEY = "signingKey";
public static final String SIGNING_KEY_PASSWORD = "signingKeyPassword"; public static final String SIGNING_KEY_PASSWORD = "signingKeyPassword";
public static final String VERIFICATION_KEY = "verificationKey";


private UUID id; private UUID id;
private String verificationKey = new RandomValueStringGenerator().generate(); private String signingKey = new RandomValueStringGenerator().generate();
private String signingKey = verificationKey;
private String signingKeyPassword; private String signingKeyPassword;


public KeyPair() { public KeyPair() {
Expand All @@ -36,18 +34,12 @@ public KeyPair() {
public KeyPair(HashMap<String, String> keymap) { public KeyPair(HashMap<String, String> keymap) {
this( this(
keymap.get(SIGNING_KEY), keymap.get(SIGNING_KEY),
keymap.get(VERIFICATION_KEY),
keymap.get(SIGNING_KEY_PASSWORD) keymap.get(SIGNING_KEY_PASSWORD)
); );
} }


public KeyPair(String signingKey, String verificationKey) { public KeyPair(String signingKey, String signingKeyPassword) {
this(signingKey, verificationKey, null);
}

public KeyPair(String signingKey, String verificationKey, String signingKeyPassword) {
this.signingKey = signingKey; this.signingKey = signingKey;
this.verificationKey = verificationKey;
this.signingKeyPassword = signingKeyPassword; this.signingKeyPassword = signingKeyPassword;
} }


Expand All @@ -63,14 +55,6 @@ public void setSigningKey(String signingKey) {
this.signingKey = signingKey; this.signingKey = signingKey;
} }


public String getVerificationKey() {
return verificationKey;
}

public void setVerificationKey(String verificationKey) {
this.verificationKey = verificationKey;
}

public String getSigningKeyPassword() { public String getSigningKeyPassword() {
return signingKeyPassword; return signingKeyPassword;
} }
Expand Down
Expand Up @@ -19,7 +19,6 @@


import static org.cloudfoundry.identity.uaa.zone.KeyPair.SIGNING_KEY; import static org.cloudfoundry.identity.uaa.zone.KeyPair.SIGNING_KEY;
import static org.cloudfoundry.identity.uaa.zone.KeyPair.SIGNING_KEY_PASSWORD; import static org.cloudfoundry.identity.uaa.zone.KeyPair.SIGNING_KEY_PASSWORD;
import static org.cloudfoundry.identity.uaa.zone.KeyPair.VERIFICATION_KEY;


public class KeyPairsMap { public class KeyPairsMap {


Expand All @@ -30,7 +29,7 @@ public KeyPairsMap(Map<String, ? extends Map<String, String>> unparsedMap) {
keys = new HashMap<>(); keys = new HashMap<>();
for (String kid : unparsedMap.keySet()) { for (String kid : unparsedMap.keySet()) {
Map<String, String> keys = unparsedMap.get(kid); Map<String, String> keys = unparsedMap.get(kid);
KeyPair keyPair = new KeyPair(keys.get(SIGNING_KEY), keys.get(VERIFICATION_KEY), keys.get(SIGNING_KEY_PASSWORD)); KeyPair keyPair = new KeyPair(keys.get(SIGNING_KEY), keys.get(SIGNING_KEY_PASSWORD));
this.keys.put(kid, keyPair); this.keys.put(kid, keyPair);
} }
} }
Expand Down
@@ -1,14 +1,6 @@
package org.cloudfoundry.identity.uaa.impl.config;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

/******************************************************************************* /*******************************************************************************
* Cloud Foundry * Cloud Foundry
* Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved. * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
* <p> * <p>
* This product is licensed to you under the Apache License, Version 2.0 (the "License"). * This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License. * You may not use this product except in compliance with the License.
Expand All @@ -18,6 +10,15 @@
* subcomponents is subject to the terms and conditions of the * subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file. * subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/ *******************************************************************************/
package org.cloudfoundry.identity.uaa.impl.config;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;


public class KeyPairsFactoryBean { public class KeyPairsFactoryBean {
private Map<String,Map<String,String>> keyPairsMap; private Map<String,Map<String,String>> keyPairsMap;


Expand Down
Expand Up @@ -12,8 +12,10 @@
*******************************************************************************/ *******************************************************************************/
package org.cloudfoundry.identity.uaa.oauth; package org.cloudfoundry.identity.uaa.oauth;


import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.bouncycastle.asn1.ASN1Sequence;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.jwt.crypto.sign.InvalidSignatureException; import org.springframework.security.jwt.crypto.sign.InvalidSignatureException;
import org.springframework.security.jwt.crypto.sign.MacSigner; import org.springframework.security.jwt.crypto.sign.MacSigner;
Expand All @@ -23,45 +25,51 @@
import org.springframework.security.jwt.crypto.sign.Signer; import org.springframework.security.jwt.crypto.sign.Signer;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;


import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.springframework.security.jwt.codec.Codecs.b64Decode;
import static org.springframework.security.jwt.codec.Codecs.utf8Encode;
import static org.springframework.util.StringUtils.isEmpty;


/** /**
* A class that knows how to provide the signing and verification keys * A class that knows how to provide the signing and verification keys
* *
* *
*/ */
public class SignerProvider implements InitializingBean { public class SignerProvider {


private final Log logger = LogFactory.getLog(getClass()); private final Log logger = LogFactory.getLog(getClass());
private String verifierKey = new RandomValueStringGenerator().generate(); private String verifierKey = new RandomValueStringGenerator().generate();
private String signingKey = verifierKey; private String signingKey = verifierKey;
private Signer signer = new MacSigner(verifierKey); private Signer signer = new MacSigner(verifierKey);
private SignatureVerifier verifier = new MacSigner(signingKey);
private String type = "MAC"; private String type = "MAC";
private final Base64.Encoder base64encoder = Base64.getMimeEncoder(64, "\n".getBytes());


@Override public SignerProvider() {
public void afterPropertiesSet() throws Exception { this(new RandomValueStringGenerator().generate());
if (signer instanceof RsaSigner) { }
type = "RSA";
RsaVerifier verifier;
try {
verifier = new RsaVerifier(verifierKey);
} catch (Exception e) {
throw new RuntimeException("Unable to create an RSA verifier from verifierKey", e);
}


byte[] test = "test".getBytes(); public SignerProvider(String signingKey) {
try { if (isEmpty(signingKey)) {
verifier.verify(test, signer.sign(test)); throw new IllegalArgumentException("Signing key cannot be empty");
logger.debug("Signing and verification RSA keys match");
} catch (InvalidSignatureException e) {
throw new RuntimeException("Signing and verification RSA keys do not match", e);
}
}
else {
Assert.state(this.signingKey == this.verifierKey,
"For MAC signing you do not need to specify the verifier key separately, and if you do it must match the signing key");
} }
setSigningKey(signingKey);
} }


public Signer getSigner() { public Signer getSigner() {
Expand Down Expand Up @@ -91,12 +99,7 @@ public boolean isPublic() {
} }


public SignatureVerifier getVerifier() { public SignatureVerifier getVerifier() {
if (isAssymetricKey(signingKey)) { return verifier;
return new RsaVerifier(verifierKey);
}
else {
return new MacSigner(verifierKey);
}
} }


public String getRevocationHash(List<String> salts) { public String getRevocationHash(List<String> salts) {
Expand All @@ -109,63 +112,72 @@ public String getRevocationHash(List<String> salts) {
} }


/** /**
* Sets the JWT signing key. It can be either a simple MAC key or an RSA * Sets the JWT signing key and corresponding key for verifying siugnatures produced by this class.
*
* The signing key can be either a simple MAC key or an RSA
* key. RSA keys should be in OpenSSH format, * key. RSA keys should be in OpenSSH format,
* as produced by <tt>ssh-keygen</tt>. * as produced by <tt>ssh-keygen</tt>.
* *
* @param key the key to be used for signing JWTs. * @param signingKey the key to be used for signing JWTs.
*/ */
public void setSigningKey(String key) { public void setSigningKey(String signingKey) {
Assert.hasText(key); Assert.hasText(signingKey);
key = key.trim(); signingKey = signingKey.trim();

this.signingKey = signingKey;


if (isAssymetricKey(signingKey)) {
KeyPair keyPair = parseKeyPair(signingKey);
signer = new RsaSigner(signingKey);


this.signingKey = key; pemEncodePublicKey(keyPair);


if (isAssymetricKey(key)) {
signer = new RsaSigner(key);
logger.debug("Configured with RSA signing key"); logger.debug("Configured with RSA signing key");
try {
verifier = new RsaVerifier(verifierKey);
} catch (Exception e) {
throw new RuntimeException("Unable to create an RSA verifier from verifierKey", e);
}

byte[] test = "test".getBytes();
try {
verifier.verify(test, signer.sign(test));
logger.debug("Signing and verification RSA keys match");
} catch (InvalidSignatureException e) {
throw new RuntimeException("Signing and verification RSA keys do not match", e);
}
type = "RSA";
} }
else { else {
// Assume it's an HMAC key // Assume it's an HMAC key
this.verifierKey = key; this.verifierKey = signingKey;
signer = new MacSigner(key); MacSigner macSigner = new MacSigner(signingKey);
signer = macSigner;
verifier = macSigner;

Assert.state(this.verifierKey == null || this.signingKey == this.verifierKey,
"For MAC signing you do not need to specify the verifier key separately, and if you do it must match the signing key");
type = "MAC";
} }
} }


protected void pemEncodePublicKey(KeyPair keyPair) {
String begin = "-----BEGIN PUBLIC KEY-----\n";
String end = "\n-----END PUBLIC KEY-----";
byte[] data = keyPair.getPublic().getEncoded();
String base64encoded = new String(base64encoder.encode(data));

verifierKey = begin + base64encoded + end;
}

/** /**
* @return true if the key has a public verifier * @return true if the key has a public verifier
*/ */
private boolean isAssymetricKey(String key) { private boolean isAssymetricKey(String key) {
return key.startsWith("-----BEGIN"); return key.startsWith("-----BEGIN");
} }


/**
* The key used for verifying signatures produced by this class. This is not
* used but is returned from the endpoint
* to allow resource servers to obtain the key.
*
* For an HMAC key it will be the same value as the signing key and does not
* need to be set. For and RSA key, it
* should be set to the String representation of the public key, in a
* standard format (e.g. OpenSSH keys)
*
* @param verifierKey the signature verification key (typically an RSA
* public key)
*/
public void setVerifierKey(String verifierKey) {
boolean valid = false;
try {
new RsaSigner(verifierKey);
} catch (Exception expected) {
// Expected
valid = true;
}
if (!valid) {
throw new IllegalArgumentException("Private key cannot be set as verifierKey property");
}
this.verifierKey = verifierKey;
}

/** /**
* This code is public domain. * This code is public domain.
* *
Expand Down Expand Up @@ -226,5 +238,54 @@ public static int murmurhash3x8632(byte[] data, int offset, int len, int seed) {
} }




private static Pattern PEM_DATA = Pattern.compile("-----BEGIN (.*)-----(.*)-----END (.*)-----", Pattern.DOTALL);

static KeyPair parseKeyPair(String pemData) {
Matcher m = PEM_DATA.matcher(pemData.trim());

if (!m.matches()) {
throw new IllegalArgumentException("String is not PEM encoded data");
}

String type = m.group(1);
final byte[] content = b64Decode(utf8Encode(m.group(2)));

PublicKey publicKey;
PrivateKey privateKey = null;

try {
KeyFactory fact = KeyFactory.getInstance("RSA");
if (type.equals("RSA PRIVATE KEY")) {
ASN1Sequence seq = ASN1Sequence.getInstance(content);
if (seq.size() != 9) {
throw new IllegalArgumentException("Invalid RSA Private Key ASN1 sequence.");
}
org.bouncycastle.asn1.pkcs.RSAPrivateKey key = org.bouncycastle.asn1.pkcs.RSAPrivateKey.getInstance(seq);
RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
RSAPrivateCrtKeySpec privSpec = new RSAPrivateCrtKeySpec(key.getModulus(), key.getPublicExponent(),
key.getPrivateExponent(), key.getPrime1(), key.getPrime2(), key.getExponent1(), key.getExponent2(),
key.getCoefficient());
publicKey = fact.generatePublic(pubSpec);
privateKey = fact.generatePrivate(privSpec);
} else if (type.equals("PUBLIC KEY")) {
KeySpec keySpec = new X509EncodedKeySpec(content);
publicKey = fact.generatePublic(keySpec);
} else if (type.equals("RSA PUBLIC KEY")) {
ASN1Sequence seq = ASN1Sequence.getInstance(content);
org.bouncycastle.asn1.pkcs.RSAPublicKey key = org.bouncycastle.asn1.pkcs.RSAPublicKey.getInstance(seq);
RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
publicKey = fact.generatePublic(pubSpec);
} else {
throw new IllegalArgumentException(type + " is not a supported format");
}


return new KeyPair(publicKey, privateKey);
}
catch (InvalidKeySpecException e) {
throw new RuntimeException(e);
}
catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
} }
Expand Up @@ -75,7 +75,7 @@ public void configureProvisioning() {
@Test @Test
public void tokenPolicy_configured_fromValuesInYaml() throws Exception { public void tokenPolicy_configured_fromValuesInYaml() throws Exception {
TokenPolicy tokenPolicy = new TokenPolicy(); TokenPolicy tokenPolicy = new TokenPolicy();
KeyPair key = new KeyPair(PRIVATE_KEY, PUBLIC_KEY, PASSWORD); KeyPair key = new KeyPair(PRIVATE_KEY, PASSWORD);
Map<String, KeyPair> keys = new HashMap<>(); Map<String, KeyPair> keys = new HashMap<>();
keys.put(ID, key); keys.put(ID, key);
tokenPolicy.setKeys(keys); tokenPolicy.setKeys(keys);
Expand All @@ -87,7 +87,6 @@ public void tokenPolicy_configured_fromValuesInYaml() throws Exception {
IdentityZoneConfiguration definition = zone.getConfig(); IdentityZoneConfiguration definition = zone.getConfig();
assertEquals(3600, definition.getTokenPolicy().getAccessTokenValidity()); assertEquals(3600, definition.getTokenPolicy().getAccessTokenValidity());
assertEquals(PASSWORD, definition.getTokenPolicy().getKeys().get(ID).getSigningKeyPassword()); assertEquals(PASSWORD, definition.getTokenPolicy().getKeys().get(ID).getSigningKeyPassword());
assertEquals(PUBLIC_KEY, definition.getTokenPolicy().getKeys().get(ID).getVerificationKey());
assertEquals(PRIVATE_KEY, definition.getTokenPolicy().getKeys().get(ID).getSigningKey()); assertEquals(PRIVATE_KEY, definition.getTokenPolicy().getKeys().get(ID).getSigningKey());
} }


Expand Down

0 comments on commit 49097f9

Please sign in to comment.