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;
|
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.HttpURLConnection;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.security.KeyPair;
|
import java.security.PublicKey;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import org.jose4j.jwk.PublicJsonWebKey;
|
import org.jose4j.jwk.PublicJsonWebKey;
|
||||||
import org.jose4j.jws.JsonWebSignature;
|
import org.jose4j.jws.JsonWebSignature;
|
||||||
|
import org.jose4j.keys.HmacKey;
|
||||||
import org.jose4j.lang.JoseException;
|
import org.jose4j.lang.JoseException;
|
||||||
import org.shredzone.acme4j.connector.Connection;
|
import org.shredzone.acme4j.connector.Connection;
|
||||||
import org.shredzone.acme4j.connector.Resource;
|
import org.shredzone.acme4j.connector.Resource;
|
||||||
import org.shredzone.acme4j.exception.AcmeException;
|
import org.shredzone.acme4j.exception.AcmeException;
|
||||||
|
import org.shredzone.acme4j.util.AcmeUtils;
|
||||||
import org.shredzone.acme4j.util.JSONBuilder;
|
import org.shredzone.acme4j.util.JSONBuilder;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -43,6 +48,7 @@ public class AccountBuilder {
|
||||||
private Boolean termsOfServiceAgreed;
|
private Boolean termsOfServiceAgreed;
|
||||||
private Boolean onlyExisting;
|
private Boolean onlyExisting;
|
||||||
private String keyIdentifier;
|
private String keyIdentifier;
|
||||||
|
private SecretKey macKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a contact URI to the list of contacts.
|
* 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
|
* Sets a Key Identifier and MAC key provided by the CA. Use this if your CA requires
|
||||||
* individual account identification, e.g. your customer number.
|
* an individual account identification, e.g. your customer number.
|
||||||
*
|
*
|
||||||
* @param kid
|
* @param kid
|
||||||
* Key Identifier
|
* Key Identifier
|
||||||
|
* @param macKey
|
||||||
|
* MAC key
|
||||||
* @return itself
|
* @return itself
|
||||||
*/
|
*/
|
||||||
public AccountBuilder useKeyIdentifier(String kid) {
|
public AccountBuilder useKeyIdentifier(String kid, SecretKey macKey) {
|
||||||
if (kid != null && kid.isEmpty()) {
|
if (kid != null && kid.isEmpty()) {
|
||||||
throw new IllegalArgumentException("kid must not be empty");
|
throw new IllegalArgumentException("kid must not be empty");
|
||||||
}
|
}
|
||||||
|
this.macKey = requireNonNull(macKey, "macKey");
|
||||||
this.keyIdentifier = kid;
|
this.keyIdentifier = kid;
|
||||||
return this;
|
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.
|
* Creates a new account.
|
||||||
*
|
*
|
||||||
|
@ -135,8 +159,8 @@ public class AccountBuilder {
|
||||||
claims.put("terms-of-service-agreed", termsOfServiceAgreed);
|
claims.put("terms-of-service-agreed", termsOfServiceAgreed);
|
||||||
}
|
}
|
||||||
if (keyIdentifier != null) {
|
if (keyIdentifier != null) {
|
||||||
claims.put("external-account-binding",
|
claims.put("external-account-binding", createExternalAccountBinding(
|
||||||
createExternalAccountBinding(keyIdentifier, session.getKeyPair(), resourceUrl));
|
keyIdentifier, session.getKeyPair().getPublic(), macKey, resourceUrl));
|
||||||
}
|
}
|
||||||
if (onlyExisting != null) {
|
if (onlyExisting != null) {
|
||||||
claims.put("only-return-existing", onlyExisting);
|
claims.put("only-return-existing", onlyExisting);
|
||||||
|
@ -164,23 +188,27 @@ public class AccountBuilder {
|
||||||
*
|
*
|
||||||
* @param kid
|
* @param kid
|
||||||
* Key Identifier provided by the CA
|
* Key Identifier provided by the CA
|
||||||
* @param keyPair
|
* @param accountKey
|
||||||
* {@link KeyPair} of the account to be created
|
* {@link PublicKey} of the account to register
|
||||||
|
* @param macKey
|
||||||
|
* {@link SecretKey} to sign the key identifier with
|
||||||
* @param resource
|
* @param resource
|
||||||
* "new-account" resource URL
|
* "new-account" resource URL
|
||||||
* @return Created JSON structure
|
* @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 {
|
throws AcmeException {
|
||||||
try {
|
try {
|
||||||
PublicJsonWebKey keyJwk = PublicJsonWebKey.Factory.newPublicJwk(keyPair.getPublic());
|
PublicJsonWebKey keyJwk = PublicJsonWebKey.Factory.newPublicJwk(accountKey);
|
||||||
|
|
||||||
JsonWebSignature innerJws = new JsonWebSignature();
|
JsonWebSignature innerJws = new JsonWebSignature();
|
||||||
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(keyAlgorithm(keyJwk));
|
innerJws.setAlgorithmHeaderValue(macKeyAlgorithm(macKey));
|
||||||
innerJws.setKey(keyPair.getPrivate());
|
innerJws.setKey(macKey);
|
||||||
|
innerJws.setDoKeyValidation(false);
|
||||||
innerJws.sign();
|
innerJws.sign();
|
||||||
|
|
||||||
JSONBuilder outerClaim = new JSONBuilder();
|
JSONBuilder outerClaim = new JSONBuilder();
|
||||||
|
|
|
@ -29,6 +29,8 @@ import java.util.Base64;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import org.jose4j.base64url.Base64Url;
|
import org.jose4j.base64url.Base64Url;
|
||||||
import org.jose4j.jwk.EllipticCurveJsonWebKey;
|
import org.jose4j.jwk.EllipticCurveJsonWebKey;
|
||||||
import org.jose4j.jwk.JsonWebKey;
|
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.
|
* 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.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import org.jose4j.jws.JsonWebSignature;
|
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;
|
||||||
|
@ -28,6 +30,7 @@ import org.junit.Test;
|
||||||
import org.shredzone.acme4j.connector.Resource;
|
import org.shredzone.acme4j.connector.Resource;
|
||||||
import org.shredzone.acme4j.exception.AcmeException;
|
import org.shredzone.acme4j.exception.AcmeException;
|
||||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||||
|
import org.shredzone.acme4j.util.AcmeUtils;
|
||||||
import org.shredzone.acme4j.util.JSON;
|
import org.shredzone.acme4j.util.JSON;
|
||||||
import org.shredzone.acme4j.util.JSONBuilder;
|
import org.shredzone.acme4j.util.JSONBuilder;
|
||||||
import org.shredzone.acme4j.util.TestUtils;
|
import org.shredzone.acme4j.util.TestUtils;
|
||||||
|
@ -38,7 +41,7 @@ import org.shredzone.acme4j.util.TestUtils;
|
||||||
public class AccountBuilderTest {
|
public class AccountBuilderTest {
|
||||||
|
|
||||||
private URL resourceUrl = url("http://example.com/acme/resource");
|
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.
|
* Test if a new account can be created.
|
||||||
|
@ -118,6 +121,7 @@ public class AccountBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
public void testRegistrationWithKid() throws Exception {
|
public void testRegistrationWithKid() throws Exception {
|
||||||
String keyIdentifier = "NCC-1701";
|
String keyIdentifier = "NCC-1701";
|
||||||
|
SecretKey macKey = TestUtils.createSecretKey("SHA-256");
|
||||||
|
|
||||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -139,12 +143,12 @@ public class AccountBuilderTest {
|
||||||
String serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
|
String serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
|
||||||
JsonWebSignature jws = new JsonWebSignature();
|
JsonWebSignature jws = new JsonWebSignature();
|
||||||
jws.setCompactSerialization(serialized);
|
jws.setCompactSerialization(serialized);
|
||||||
jws.setKey(session.getKeyPair().getPublic());
|
jws.setKey(macKey);
|
||||||
assertThat(jws.verifySignature(), is(true));
|
assertThat(jws.verifySignature(), is(true));
|
||||||
|
|
||||||
assertThat(jws.getHeader("url"), is(resourceUrl.toString()));
|
assertThat(jws.getHeader("url"), is(resourceUrl.toString()));
|
||||||
assertThat(jws.getHeader("kid"), is(keyIdentifier));
|
assertThat(jws.getHeader("kid"), is(keyIdentifier));
|
||||||
assertThat(jws.getHeader("alg"), is("RS256"));
|
assertThat(jws.getHeader("alg"), is("HS256"));
|
||||||
|
|
||||||
String decodedPayload = jws.getPayload();
|
String decodedPayload = jws.getPayload();
|
||||||
StringBuilder expectedPayload = new StringBuilder();
|
StringBuilder expectedPayload = new StringBuilder();
|
||||||
|
@ -180,7 +184,7 @@ public class AccountBuilderTest {
|
||||||
provider.putTestResource(Resource.NEW_ACCOUNT, resourceUrl);
|
provider.putTestResource(Resource.NEW_ACCOUNT, resourceUrl);
|
||||||
|
|
||||||
AccountBuilder builder = new AccountBuilder();
|
AccountBuilder builder = new AccountBuilder();
|
||||||
builder.useKeyIdentifier(keyIdentifier);
|
builder.useKeyIdentifier(keyIdentifier, AcmeUtils.base64UrlEncode(macKey.getEncoded()));
|
||||||
|
|
||||||
Session session = provider.createSession();
|
Session session = provider.createSession();
|
||||||
Account account = builder.create(session);
|
Account account = builder.create(session);
|
||||||
|
|
|
@ -184,6 +184,16 @@ public class AcmeUtilsTest {
|
||||||
assertThat(type, is("ES512"));
|
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.
|
* Test valid strings.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -45,6 +45,8 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import org.hamcrest.BaseMatcher;
|
import org.hamcrest.BaseMatcher;
|
||||||
import org.hamcrest.Description;
|
import org.hamcrest.Description;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
|
@ -52,6 +54,7 @@ import org.jose4j.base64url.Base64Url;
|
||||||
import org.jose4j.json.JsonUtil;
|
import org.jose4j.json.JsonUtil;
|
||||||
import org.jose4j.jwk.JsonWebKey;
|
import org.jose4j.jwk.JsonWebKey;
|
||||||
import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
|
import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
|
||||||
|
import org.jose4j.keys.HmacKey;
|
||||||
import org.shredzone.acme4j.Problem;
|
import org.shredzone.acme4j.Problem;
|
||||||
import org.shredzone.acme4j.Session;
|
import org.shredzone.acme4j.Session;
|
||||||
import org.shredzone.acme4j.provider.AcmeProvider;
|
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
|
* Creates a standard certificate chain for testing. This certificate is read from a
|
||||||
* test resource and is guaranteed not to change between test runs.
|
* test resource and is guaranteed not to change between test runs.
|
||||||
|
|
Loading…
Reference in New Issue