mirror of https://github.com/shred/acme4j
Fix setting the account's key identifier
parent
7c88a2cdac
commit
3881669e22
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue