Fix setting the account's key identifier

pull/55/head
Richard Körber 2017-08-13 14:13:56 +02:00
parent 7c88a2cdac
commit 3881669e22
5 changed files with 113 additions and 17 deletions

View File

@ -13,22 +13,27 @@
*/
package org.shredzone.acme4j;
import static org.shredzone.acme4j.util.AcmeUtils.keyAlgorithm;
import static java.util.Objects.requireNonNull;
import static org.shredzone.acme4j.util.AcmeUtils.macKeyAlgorithm;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.crypto.SecretKey;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.keys.HmacKey;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.AcmeUtils;
import org.shredzone.acme4j.util.JSONBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -43,6 +48,7 @@ public class AccountBuilder {
private Boolean termsOfServiceAgreed;
private Boolean onlyExisting;
private String keyIdentifier;
private SecretKey macKey;
/**
* Add a contact URI to the list of contacts.
@ -95,21 +101,39 @@ public class AccountBuilder {
}
/**
* Sets a Key Identifier provided by the CA. Use this if your CA requires an
* individual account identification, e.g. your customer number.
* Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires
* an individual account identification, e.g. your customer number.
*
* @param kid
* Key Identifier
* @param macKey
* MAC key
* @return itself
*/
public AccountBuilder useKeyIdentifier(String kid) {
public AccountBuilder useKeyIdentifier(String kid, SecretKey macKey) {
if (kid != null && kid.isEmpty()) {
throw new IllegalArgumentException("kid must not be empty");
}
this.macKey = requireNonNull(macKey, "macKey");
this.keyIdentifier = kid;
return this;
}
/**
* Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires
* an individual account identification, e.g. your customer number.
*
* @param kid
* Key Identifier
* @param encodedMacKey
* Base64url encoded MAC key. It will be decoded for your convenience.
* @return itself
*/
public AccountBuilder useKeyIdentifier(String kid, String encodedMacKey) {
byte[] encodedKey = AcmeUtils.base64UrlDecode(requireNonNull(encodedMacKey, "encodedMacKey"));
return useKeyIdentifier(kid, new HmacKey(encodedKey));
}
/**
* Creates a new account.
*
@ -135,8 +159,8 @@ public class AccountBuilder {
claims.put("terms-of-service-agreed", termsOfServiceAgreed);
}
if (keyIdentifier != null) {
claims.put("external-account-binding",
createExternalAccountBinding(keyIdentifier, session.getKeyPair(), resourceUrl));
claims.put("external-account-binding", createExternalAccountBinding(
keyIdentifier, session.getKeyPair().getPublic(), macKey, resourceUrl));
}
if (onlyExisting != null) {
claims.put("only-return-existing", onlyExisting);
@ -164,23 +188,27 @@ public class AccountBuilder {
*
* @param kid
* Key Identifier provided by the CA
* @param keyPair
* {@link KeyPair} of the account to be created
* @param accountKey
* {@link PublicKey} of the account to register
* @param macKey
* {@link SecretKey} to sign the key identifier with
* @param resource
* "new-account" resource URL
* @return Created JSON structure
*/
private Map<String, Object> createExternalAccountBinding(String kid, KeyPair keyPair, URL resource)
private Map<String, Object> createExternalAccountBinding(String kid,
PublicKey accountKey, SecretKey macKey, URL resource)
throws AcmeException {
try {
PublicJsonWebKey keyJwk = PublicJsonWebKey.Factory.newPublicJwk(keyPair.getPublic());
PublicJsonWebKey keyJwk = PublicJsonWebKey.Factory.newPublicJwk(accountKey);
JsonWebSignature innerJws = new JsonWebSignature();
innerJws.setPayload(keyJwk.toJson());
innerJws.getHeaders().setObjectHeaderValue("url", resource);
innerJws.getHeaders().setObjectHeaderValue("kid", kid);
innerJws.setAlgorithmHeaderValue(keyAlgorithm(keyJwk));
innerJws.setKey(keyPair.getPrivate());
innerJws.setAlgorithmHeaderValue(macKeyAlgorithm(macKey));
innerJws.setKey(macKey);
innerJws.setDoKeyValidation(false);
innerJws.sign();
JSONBuilder outerClaim = new JSONBuilder();

View File

@ -29,6 +29,8 @@ import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.SecretKey;
import org.jose4j.base64url.Base64Url;
import org.jose4j.jwk.EllipticCurveJsonWebKey;
import org.jose4j.jwk.JsonWebKey;
@ -198,6 +200,37 @@ public final class AcmeUtils {
}
}
/**
* Analyzes the {@link SecretKey}, and returns the key algorithm
* identifier for {@link JsonWebSignature}.
*
* @param macKey
* {@link SecretKey} to analyze
* @return algorithm identifier
* @throws IllegalArgumentException
* there is no corresponding algorithm identifier for the key
*/
public static String macKeyAlgorithm(SecretKey macKey) {
if (!"HMAC".equals(macKey.getAlgorithm())) {
throw new IllegalArgumentException("Bad algorithm: " + macKey.getAlgorithm());
}
int size = macKey.getEncoded().length * 8;
switch (size) {
case 256:
return AlgorithmIdentifiers.HMAC_SHA256;
case 384:
return AlgorithmIdentifiers.HMAC_SHA384;
case 512:
return AlgorithmIdentifiers.HMAC_SHA512;
default:
throw new IllegalArgumentException("Bad key size: " + size);
}
}
/**
* Parses a RFC 3339 formatted date.
*

View File

@ -21,6 +21,8 @@ import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.crypto.SecretKey;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwx.CompactSerializer;
import org.jose4j.lang.JoseException;
@ -28,6 +30,7 @@ import org.junit.Test;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.util.AcmeUtils;
import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.JSONBuilder;
import org.shredzone.acme4j.util.TestUtils;
@ -38,7 +41,7 @@ import org.shredzone.acme4j.util.TestUtils;
public class AccountBuilderTest {
private URL resourceUrl = url("http://example.com/acme/resource");
private URL locationUrl = url("http://example.com/acme/account");;
private URL locationUrl = url("http://example.com/acme/account");
/**
* Test if a new account can be created.
@ -118,6 +121,7 @@ public class AccountBuilderTest {
@Test
public void testRegistrationWithKid() throws Exception {
String keyIdentifier = "NCC-1701";
SecretKey macKey = TestUtils.createSecretKey("SHA-256");
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
@ -139,12 +143,12 @@ public class AccountBuilderTest {
String serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(serialized);
jws.setKey(session.getKeyPair().getPublic());
jws.setKey(macKey);
assertThat(jws.verifySignature(), is(true));
assertThat(jws.getHeader("url"), is(resourceUrl.toString()));
assertThat(jws.getHeader("kid"), is(keyIdentifier));
assertThat(jws.getHeader("alg"), is("RS256"));
assertThat(jws.getHeader("alg"), is("HS256"));
String decodedPayload = jws.getPayload();
StringBuilder expectedPayload = new StringBuilder();
@ -180,7 +184,7 @@ public class AccountBuilderTest {
provider.putTestResource(Resource.NEW_ACCOUNT, resourceUrl);
AccountBuilder builder = new AccountBuilder();
builder.useKeyIdentifier(keyIdentifier);
builder.useKeyIdentifier(keyIdentifier, AcmeUtils.base64UrlEncode(macKey.getEncoded()));
Session session = provider.createSession();
Account account = builder.create(session);

View File

@ -184,6 +184,16 @@ public class AcmeUtilsTest {
assertThat(type, is("ES512"));
}
/**
* Test if MAC key algorithms are properly detected.
*/
@Test
public void testMacKey() throws Exception {
assertThat(macKeyAlgorithm(TestUtils.createSecretKey("SHA-256")), is("HS256"));
assertThat(macKeyAlgorithm(TestUtils.createSecretKey("SHA-384")), is("HS384"));
assertThat(macKeyAlgorithm(TestUtils.createSecretKey("SHA-512")), is("HS512"));
}
/**
* Test valid strings.
*/

View File

@ -45,6 +45,8 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.crypto.SecretKey;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
@ -52,6 +54,7 @@ import org.jose4j.base64url.Base64Url;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
import org.jose4j.keys.HmacKey;
import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.provider.AcmeProvider;
@ -222,6 +225,24 @@ public final class TestUtils {
}
}
/**
* Creates a HMAC key using the given hash algorithm.
*
* @param algorithm
* Name of the hash algorithm to be used
* @return {@link SecretKey} for testing
*/
public static SecretKey createSecretKey(String algorithm) throws IOException {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
md.update("Turpentine".getBytes()); // A random password
byte[] macKey = md.digest();
return new HmacKey(macKey);
} catch (NoSuchAlgorithmException ex) {
throw new IOException(ex);
}
}
/**
* Creates a standard certificate chain for testing. This certificate is read from a
* test resource and is guaranteed not to change between test runs.