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;
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.security.KeyPair;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@ -55,6 +58,7 @@ import org.slf4j.LoggerFactory;
*/
public class AccountBuilder {
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 @Nullable Boolean termsOfServiceAgreed;
@ -62,6 +66,7 @@ public class AccountBuilder {
private @Nullable String keyIdentifier;
private @Nullable KeyPair keyPair;
private @Nullable SecretKey macKey;
private @Nullable String macAlgorithm;
/**
* Add a contact URI to the list of contacts.
@ -210,6 +215,25 @@ public class AccountBuilder {
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.
* <p>
@ -254,9 +278,10 @@ public class AccountBuilder {
if (termsOfServiceAgreed != null) {
claims.put("termsOfServiceAgreed", termsOfServiceAgreed);
}
if (keyIdentifier != null) {
if (keyIdentifier != null && macKey != null) {
var algorithm = macAlgorithm != null ? macAlgorithm : macKeyAlgorithm(macKey);
claims.put("externalAccountBinding", JoseUtils.createExternalAccountBinding(
keyIdentifier, keyPair.getPublic(), macKey, resourceUrl));
keyIdentifier, keyPair.getPublic(), macKey, algorithm, resourceUrl));
}
if (onlyExisting != null) {
claims.put("onlyReturnExisting", onlyExisting);

View File

@ -115,12 +115,14 @@ public final class JoseUtils {
* {@link PublicKey} of the account to register
* @param macKey
* {@link SecretKey} to sign the key identifier with
* @param macAlgorithm
* Algorithm of the MAC key
* @param resource
* "newAccount" resource URL
* @return Created JSON structure
*/
public static Map<String, Object> createExternalAccountBinding(String kid,
PublicKey accountKey, SecretKey macKey, URL resource) {
PublicKey accountKey, SecretKey macKey, String macAlgorithm, URL resource) {
try {
var keyJwk = PublicJsonWebKey.Factory.newPublicJwk(accountKey);
@ -128,7 +130,7 @@ public final class JoseUtils {
innerJws.setPayload(keyJwk.toJson());
innerJws.getHeaders().setObjectHeaderValue("url", resource);
innerJws.getHeaders().setObjectHeaderValue("kid", kid);
innerJws.setAlgorithmHeaderValue(macKeyAlgorithm(macKey));
innerJws.setAlgorithmHeaderValue(macAlgorithm);
innerJws.setKey(macKey);
innerJws.setDoKeyValidation(false);
innerJws.sign();

View File

@ -15,6 +15,7 @@ package org.shredzone.acme4j;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
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.url;
@ -22,8 +23,13 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyPair;
import edu.umd.cs.findbugs.annotations.Nullable;
import org.jose4j.jwx.CompactSerializer;
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.shredzone.acme4j.connector.Resource;
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
public void testRegistrationWithKid() throws Exception {
@ParameterizedTest
@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 keyIdentifier = "NCC-1701";
var macKey = TestUtils.createSecretKey("SHA-256");
var macKey = TestUtils.createSecretKey(keyAlg);
var provider = new TestableConnectionProvider() {
@Override
@ -127,7 +138,7 @@ public class AccountBuilderTest {
var encodedPayload = binding.get("payload").asString();
var serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
JoseUtilsTest.assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey);
JoseUtilsTest.assertExternalAccountBinding(serialized, resourceUrl, keyIdentifier, macKey, expectedMacAlg);
return HttpURLConnection.HTTP_CREATED;
}
@ -148,6 +159,9 @@ public class AccountBuilderTest {
var builder = new AccountBuilder();
builder.useKeyPair(accountKey);
builder.withKeyIdentifier(keyIdentifier, AcmeUtils.base64UrlEncode(macKey.getEncoded()));
if (macAlg != null) {
builder.withMacAlgorithm(macAlg);
}
var session = provider.createSession();
var login = builder.createLogin(session);
@ -157,6 +171,18 @@ public class AccountBuilderTest {
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.
*/

View File

@ -30,6 +30,8 @@ import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwx.CompactSerializer;
import org.jose4j.lang.JoseException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
/**
* Unit tests for {@link JoseUtils}.
@ -159,22 +161,23 @@ public class JoseUtilsTest {
/**
* Test if an external account binding is correctly created.
*/
@Test
public void testCreateExternalAccountBinding() throws Exception {
@ParameterizedTest
@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 keyIdentifier = "NCC-1701";
var macKey = TestUtils.createSecretKey("SHA-256");
var macKey = TestUtils.createSecretKey(keyAlg);
var resourceUrl = url("http://example.com/acme/resource");
var binding = JoseUtils.createExternalAccountBinding(
keyIdentifier, accountKey.getPublic(), macKey, resourceUrl);
keyIdentifier, accountKey.getPublic(), macKey, macAlg, resourceUrl);
var encodedHeader = binding.get("protected").toString();
var encodedSignature = binding.get("signature").toString();
var encodedPayload = binding.get("payload").toString();
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
* @param macKey
* Expected {@link SecretKey}
* @param macAlg
* Expected algorithm
*/
public static void assertExternalAccountBinding(String serialized, URL resourceUrl,
String keyIdentifier, SecretKey macKey) {
String keyIdentifier, SecretKey macKey,
String macAlg) {
try {
var jws = new JsonWebSignature();
jws.setCompactSerialization(serialized);
@ -293,7 +299,7 @@ public class JoseUtilsTest {
assertThat(jws.getHeader("url")).isEqualTo(resourceUrl.toString());
assertThat(jws.getHeader("kid")).isEqualTo(keyIdentifier);
assertThat(jws.getHeader("alg")).isEqualTo("HS256");
assertThat(jws.getHeader("alg")).isEqualTo(macAlg);
var decodedPayload = jws.getPayload();
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`.
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()`.