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

View File

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

View File

@ -184,6 +184,16 @@ public class AcmeUtilsTest {
assertThat(type, is("ES512")); 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. * Test valid strings.
*/ */

View File

@ -45,6 +45,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import javax.crypto.SecretKey;
import org.hamcrest.BaseMatcher; import org.hamcrest.BaseMatcher;
import org.hamcrest.Description; import org.hamcrest.Description;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
@ -52,6 +54,7 @@ import org.jose4j.base64url.Base64Url;
import org.jose4j.json.JsonUtil; import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKey.OutputControlLevel; import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
import org.jose4j.keys.HmacKey;
import org.shredzone.acme4j.Problem; import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Session; import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.provider.AcmeProvider; 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 * Creates a standard certificate chain for testing. This certificate is read from a
* test resource and is guaranteed not to change between test runs. * test resource and is guaranteed not to change between test runs.