From 3881669e22acdfea306518619858fbbbf38a3bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Sun, 13 Aug 2017 14:13:56 +0200 Subject: [PATCH] Fix setting the account's key identifier --- .../org/shredzone/acme4j/AccountBuilder.java | 54 ++++++++++++++----- .../org/shredzone/acme4j/util/AcmeUtils.java | 33 ++++++++++++ .../shredzone/acme4j/AccountBuilderTest.java | 12 +++-- .../shredzone/acme4j/util/AcmeUtilsTest.java | 10 ++++ .../org/shredzone/acme4j/util/TestUtils.java | 21 ++++++++ 5 files changed, 113 insertions(+), 17 deletions(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/AccountBuilder.java b/acme4j-client/src/main/java/org/shredzone/acme4j/AccountBuilder.java index 05dec0d1..e9a39bbd 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/AccountBuilder.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/AccountBuilder.java @@ -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 createExternalAccountBinding(String kid, KeyPair keyPair, URL resource) + private Map 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(); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java b/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java index 8c8d0d01..bfa5915a 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java @@ -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. * diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/AccountBuilderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/AccountBuilderTest.java index 1de6fdac..81c92718 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/AccountBuilderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/AccountBuilderTest.java @@ -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); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java index 8f20f8e5..34889fb7 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java @@ -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. */ diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java index 3e1b8e94..38115a2c 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java @@ -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.