mirror of https://github.com/shred/acme4j
Add method to set arbitrary MAC algorithm (#141)
parent
4da80d4da7
commit
3ad325782b
|
@ -14,11 +14,14 @@
|
||||||
package org.shredzone.acme4j;
|
package org.shredzone.acme4j;
|
||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
import static org.jose4j.jws.AlgorithmIdentifiers.*;
|
||||||
|
import static org.shredzone.acme4j.toolbox.JoseUtils.macKeyAlgorithm;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
@ -55,6 +58,7 @@ import org.slf4j.LoggerFactory;
|
||||||
*/
|
*/
|
||||||
public class AccountBuilder {
|
public class AccountBuilder {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AccountBuilder.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AccountBuilder.class);
|
||||||
|
private static final Set<String> VALID_ALGORITHMS = Set.of(HMAC_SHA256, HMAC_SHA384, HMAC_SHA512);
|
||||||
|
|
||||||
private final List<URI> contacts = new ArrayList<>();
|
private final List<URI> contacts = new ArrayList<>();
|
||||||
private @Nullable Boolean termsOfServiceAgreed;
|
private @Nullable Boolean termsOfServiceAgreed;
|
||||||
|
@ -62,6 +66,7 @@ public class AccountBuilder {
|
||||||
private @Nullable String keyIdentifier;
|
private @Nullable String keyIdentifier;
|
||||||
private @Nullable KeyPair keyPair;
|
private @Nullable KeyPair keyPair;
|
||||||
private @Nullable SecretKey macKey;
|
private @Nullable SecretKey macKey;
|
||||||
|
private @Nullable String macAlgorithm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a contact URI to the list of contacts.
|
* Add a contact URI to the list of contacts.
|
||||||
|
@ -210,6 +215,25 @@ public class AccountBuilder {
|
||||||
return withKeyIdentifier(kid, new SecretKeySpec(encodedKey, "HMAC"));
|
return withKeyIdentifier(kid, new SecretKeySpec(encodedKey, "HMAC"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the MAC key algorithm that is provided by the CA. To be used in combination
|
||||||
|
* with key identifier. By default, the algorithm is deduced from the size of the
|
||||||
|
* MAC key. If a different size is needed, it can be set using this method.
|
||||||
|
*
|
||||||
|
* @param macAlgorithm
|
||||||
|
* the algorithm to be set in the {@code alg} field, e.g. {@code "HS512"}.
|
||||||
|
* @return itself
|
||||||
|
* @since 3.1.0
|
||||||
|
*/
|
||||||
|
public AccountBuilder withMacAlgorithm(String macAlgorithm) {
|
||||||
|
var algorithm = requireNonNull(macAlgorithm, "macAlgorithm");
|
||||||
|
if (!VALID_ALGORITHMS.contains(algorithm)) {
|
||||||
|
throw new IllegalArgumentException("Invalid MAC algorithm: " + macAlgorithm);
|
||||||
|
}
|
||||||
|
this.macAlgorithm = algorithm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new account.
|
* Creates a new account.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -254,9 +278,10 @@ public class AccountBuilder {
|
||||||
if (termsOfServiceAgreed != null) {
|
if (termsOfServiceAgreed != null) {
|
||||||
claims.put("termsOfServiceAgreed", termsOfServiceAgreed);
|
claims.put("termsOfServiceAgreed", termsOfServiceAgreed);
|
||||||
}
|
}
|
||||||
if (keyIdentifier != null) {
|
if (keyIdentifier != null && macKey != null) {
|
||||||
|
var algorithm = macAlgorithm != null ? macAlgorithm : macKeyAlgorithm(macKey);
|
||||||
claims.put("externalAccountBinding", JoseUtils.createExternalAccountBinding(
|
claims.put("externalAccountBinding", JoseUtils.createExternalAccountBinding(
|
||||||
keyIdentifier, keyPair.getPublic(), macKey, resourceUrl));
|
keyIdentifier, keyPair.getPublic(), macKey, algorithm, resourceUrl));
|
||||||
}
|
}
|
||||||
if (onlyExisting != null) {
|
if (onlyExisting != null) {
|
||||||
claims.put("onlyReturnExisting", onlyExisting);
|
claims.put("onlyReturnExisting", onlyExisting);
|
||||||
|
|
|
@ -115,12 +115,14 @@ public final class JoseUtils {
|
||||||
* {@link PublicKey} of the account to register
|
* {@link PublicKey} of the account to register
|
||||||
* @param macKey
|
* @param macKey
|
||||||
* {@link SecretKey} to sign the key identifier with
|
* {@link SecretKey} to sign the key identifier with
|
||||||
|
* @param macAlgorithm
|
||||||
|
* Algorithm of the MAC key
|
||||||
* @param resource
|
* @param resource
|
||||||
* "newAccount" resource URL
|
* "newAccount" resource URL
|
||||||
* @return Created JSON structure
|
* @return Created JSON structure
|
||||||
*/
|
*/
|
||||||
public static Map<String, Object> createExternalAccountBinding(String kid,
|
public static Map<String, Object> createExternalAccountBinding(String kid,
|
||||||
PublicKey accountKey, SecretKey macKey, URL resource) {
|
PublicKey accountKey, SecretKey macKey, String macAlgorithm, URL resource) {
|
||||||
try {
|
try {
|
||||||
var keyJwk = PublicJsonWebKey.Factory.newPublicJwk(accountKey);
|
var keyJwk = PublicJsonWebKey.Factory.newPublicJwk(accountKey);
|
||||||
|
|
||||||
|
@ -128,7 +130,7 @@ public final class JoseUtils {
|
||||||
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(macKeyAlgorithm(macKey));
|
innerJws.setAlgorithmHeaderValue(macAlgorithm);
|
||||||
innerJws.setKey(macKey);
|
innerJws.setKey(macKey);
|
||||||
innerJws.setDoKeyValidation(false);
|
innerJws.setDoKeyValidation(false);
|
||||||
innerJws.sign();
|
innerJws.sign();
|
||||||
|
|
|
@ -15,6 +15,7 @@ package org.shredzone.acme4j;
|
||||||
|
|
||||||
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatException;
|
||||||
import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
|
import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
|
||||||
import static org.shredzone.acme4j.toolbox.TestUtils.url;
|
import static org.shredzone.acme4j.toolbox.TestUtils.url;
|
||||||
|
|
||||||
|
@ -22,8 +23,13 @@ import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
|
|
||||||
|
import edu.umd.cs.findbugs.annotations.Nullable;
|
||||||
import org.jose4j.jwx.CompactSerializer;
|
import org.jose4j.jwx.CompactSerializer;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.shredzone.acme4j.connector.Resource;
|
import org.shredzone.acme4j.connector.Resource;
|
||||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||||
|
@ -105,11 +111,16 @@ public class AccountBuilderTest {
|
||||||
/**
|
/**
|
||||||
* Test if a new account with Key Identifier can be created.
|
* Test if a new account with Key Identifier can be created.
|
||||||
*/
|
*/
|
||||||
@Test
|
@ParameterizedTest
|
||||||
public void testRegistrationWithKid() throws Exception {
|
@CsvSource({
|
||||||
|
"SHA-256,HS256,", "SHA-384,HS384,", "SHA-512,HS512,",
|
||||||
|
"SHA-256,HS256,HS256", "SHA-384,HS384,HS384", "SHA-512,HS512,HS512",
|
||||||
|
"SHA-512,HS256,HS256"
|
||||||
|
})
|
||||||
|
public void testRegistrationWithKid(String keyAlg, String expectedMacAlg, @Nullable String macAlg) throws Exception {
|
||||||
var accountKey = TestUtils.createKeyPair();
|
var accountKey = TestUtils.createKeyPair();
|
||||||
var keyIdentifier = "NCC-1701";
|
var keyIdentifier = "NCC-1701";
|
||||||
var macKey = TestUtils.createSecretKey("SHA-256");
|
var macKey = TestUtils.createSecretKey(keyAlg);
|
||||||
|
|
||||||
var provider = new TestableConnectionProvider() {
|
var provider = new TestableConnectionProvider() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -127,7 +138,7 @@ public class AccountBuilderTest {
|
||||||
var encodedPayload = binding.get("payload").asString();
|
var encodedPayload = binding.get("payload").asString();
|
||||||
var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
|
var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
|
||||||
|
|
||||||
JoseUtilsTest.assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey);
|
JoseUtilsTest.assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey, expectedMacAlg);
|
||||||
|
|
||||||
return HttpURLConnection.HTTP_CREATED;
|
return HttpURLConnection.HTTP_CREATED;
|
||||||
}
|
}
|
||||||
|
@ -148,6 +159,9 @@ public class AccountBuilderTest {
|
||||||
var builder = new AccountBuilder();
|
var builder = new AccountBuilder();
|
||||||
builder.useKeyPair(accountKey);
|
builder.useKeyPair(accountKey);
|
||||||
builder.withKeyIdentifier(keyIdentifier, AcmeUtils.base64UrlEncode(macKey.getEncoded()));
|
builder.withKeyIdentifier(keyIdentifier, AcmeUtils.base64UrlEncode(macKey.getEncoded()));
|
||||||
|
if (macAlg != null) {
|
||||||
|
builder.withMacAlgorithm(macAlg);
|
||||||
|
}
|
||||||
|
|
||||||
var session = provider.createSession();
|
var session = provider.createSession();
|
||||||
var login = builder.createLogin(session);
|
var login = builder.createLogin(session);
|
||||||
|
@ -157,6 +171,18 @@ public class AccountBuilderTest {
|
||||||
provider.close();
|
provider.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if invalid mac algorithms are rejected.
|
||||||
|
*/
|
||||||
|
@ParameterizedTest
|
||||||
|
@NullAndEmptySource
|
||||||
|
@ValueSource(strings = {"foo", "null", "false", "none", "HS-256", "hs256", "HS128", "RS256"})
|
||||||
|
public void testRejectInvalidMacAlg(@Nullable String macAlg) {
|
||||||
|
assertThatException().isThrownBy(() -> {
|
||||||
|
new AccountBuilder().withMacAlgorithm(macAlg);
|
||||||
|
}).isInstanceOfAny(IllegalArgumentException.class, NullPointerException.class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test if an existing account is properly returned.
|
* Test if an existing account is properly returned.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -30,6 +30,8 @@ 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;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link JoseUtils}.
|
* Unit tests for {@link JoseUtils}.
|
||||||
|
@ -159,22 +161,23 @@ public class JoseUtilsTest {
|
||||||
/**
|
/**
|
||||||
* Test if an external account binding is correctly created.
|
* Test if an external account binding is correctly created.
|
||||||
*/
|
*/
|
||||||
@Test
|
@ParameterizedTest
|
||||||
public void testCreateExternalAccountBinding() throws Exception {
|
@CsvSource({"SHA-256,HS256", "SHA-384,HS384", "SHA-512,HS512", "SHA-512,HS256"})
|
||||||
|
public void testCreateExternalAccountBinding(String keyAlg, String macAlg) throws Exception {
|
||||||
var accountKey = TestUtils.createKeyPair();
|
var accountKey = TestUtils.createKeyPair();
|
||||||
var keyIdentifier = "NCC-1701";
|
var keyIdentifier = "NCC-1701";
|
||||||
var macKey = TestUtils.createSecretKey("SHA-256");
|
var macKey = TestUtils.createSecretKey(keyAlg);
|
||||||
var resourceUrl = url("http://example.com/acme/resource");
|
var resourceUrl = url("http://example.com/acme/resource");
|
||||||
|
|
||||||
var binding = JoseUtils.createExternalAccountBinding(
|
var binding = JoseUtils.createExternalAccountBinding(
|
||||||
keyIdentifier, accountKey.getPublic(), macKey, resourceUrl);
|
keyIdentifier, accountKey.getPublic(), macKey, macAlg, resourceUrl);
|
||||||
|
|
||||||
var encodedHeader = binding.get("protected").toString();
|
var encodedHeader = binding.get("protected").toString();
|
||||||
var encodedSignature = binding.get("signature").toString();
|
var encodedSignature = binding.get("signature").toString();
|
||||||
var encodedPayload = binding.get("payload").toString();
|
var encodedPayload = binding.get("payload").toString();
|
||||||
var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
|
var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
|
||||||
|
|
||||||
assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey);
|
assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey, macAlg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -282,9 +285,12 @@ public class JoseUtilsTest {
|
||||||
* Expected key identifier
|
* Expected key identifier
|
||||||
* @param macKey
|
* @param macKey
|
||||||
* Expected {@link SecretKey}
|
* Expected {@link SecretKey}
|
||||||
|
* @param macAlg
|
||||||
|
* Expected algorithm
|
||||||
*/
|
*/
|
||||||
public static void assertExternalAccountBinding(String serialized, URL resourceUrl,
|
public static void assertExternalAccountBinding(String serialized, URL resourceUrl,
|
||||||
String keyIdentifier, SecretKey macKey) {
|
String keyIdentifier, SecretKey macKey,
|
||||||
|
String macAlg) {
|
||||||
try {
|
try {
|
||||||
var jws = new JsonWebSignature();
|
var jws = new JsonWebSignature();
|
||||||
jws.setCompactSerialization(serialized);
|
jws.setCompactSerialization(serialized);
|
||||||
|
@ -293,7 +299,7 @@ public class JoseUtilsTest {
|
||||||
|
|
||||||
assertThat(jws.getHeader("url")).isEqualTo(resourceUrl.toString());
|
assertThat(jws.getHeader("url")).isEqualTo(resourceUrl.toString());
|
||||||
assertThat(jws.getHeader("kid")).isEqualTo(keyIdentifier);
|
assertThat(jws.getHeader("kid")).isEqualTo(keyIdentifier);
|
||||||
assertThat(jws.getHeader("alg")).isEqualTo("HS256");
|
assertThat(jws.getHeader("alg")).isEqualTo(macAlg);
|
||||||
|
|
||||||
var decodedPayload = jws.getPayload();
|
var decodedPayload = jws.getPayload();
|
||||||
var expectedPayload = new StringBuilder();
|
var expectedPayload = new StringBuilder();
|
||||||
|
|
|
@ -148,3 +148,5 @@ Account account = new AccountBuilder()
|
||||||
```
|
```
|
||||||
|
|
||||||
For your convenience, you can also pass a base64 encoded MAC Key as `String`.
|
For your convenience, you can also pass a base64 encoded MAC Key as `String`.
|
||||||
|
|
||||||
|
The MAC algorithm is automatically set from the size of the MAC key. If a different algorithm is required, it can be set using `withMacAlgorithm()`.
|
||||||
|
|
Loading…
Reference in New Issue