Add method to set arbitrary MAC algorithm (#141)

pull/144/head
Richard Körber 2023-09-22 11:20:31 +02:00
parent 4da80d4da7
commit 3ad325782b
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
5 changed files with 76 additions and 15 deletions

View File

@ -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);

View File

@ -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();

View File

@ -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.
*/ */

View File

@ -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();

View File

@ -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()`.