mirror of https://github.com/shred/acme4j
Set individual key identifier on account creation
parent
f841daa5b6
commit
c4f75497c7
|
@ -13,12 +13,19 @@
|
||||||
*/
|
*/
|
||||||
package org.shredzone.acme4j;
|
package org.shredzone.acme4j;
|
||||||
|
|
||||||
|
import static org.shredzone.acme4j.util.AcmeUtils.keyAlgorithm;
|
||||||
|
|
||||||
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.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.jose4j.jwk.PublicJsonWebKey;
|
||||||
|
import org.jose4j.jws.JsonWebSignature;
|
||||||
|
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;
|
||||||
|
@ -34,6 +41,7 @@ public class RegistrationBuilder {
|
||||||
|
|
||||||
private List<URI> contacts = new ArrayList<>();
|
private List<URI> contacts = new ArrayList<>();
|
||||||
private Boolean termsOfServiceAgreed;
|
private Boolean termsOfServiceAgreed;
|
||||||
|
private String keyIdentifier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a contact URI to the list of contacts.
|
* Add a contact URI to the list of contacts.
|
||||||
|
@ -73,6 +81,22 @@ public class RegistrationBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a Key Identifier provided by the CA. Use this if your CA requires an
|
||||||
|
* individual account identification, e.g. your customer number.
|
||||||
|
*
|
||||||
|
* @param kid
|
||||||
|
* Key Identifier
|
||||||
|
* @return itself
|
||||||
|
*/
|
||||||
|
public RegistrationBuilder useKeyIdentifier(String kid) {
|
||||||
|
if (kid != null && kid.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("kid must not be empty");
|
||||||
|
}
|
||||||
|
this.keyIdentifier = kid;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new account.
|
* Creates a new account.
|
||||||
*
|
*
|
||||||
|
@ -88,6 +112,8 @@ public class RegistrationBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Connection conn = session.provider().connect()) {
|
try (Connection conn = session.provider().connect()) {
|
||||||
|
URL resourceUrl = session.resourceUrl(Resource.NEW_ACCOUNT);
|
||||||
|
|
||||||
JSONBuilder claims = new JSONBuilder();
|
JSONBuilder claims = new JSONBuilder();
|
||||||
if (!contacts.isEmpty()) {
|
if (!contacts.isEmpty()) {
|
||||||
claims.put("contact", contacts);
|
claims.put("contact", contacts);
|
||||||
|
@ -95,16 +121,57 @@ public class RegistrationBuilder {
|
||||||
if (termsOfServiceAgreed != null) {
|
if (termsOfServiceAgreed != null) {
|
||||||
claims.put("terms-of-service-agreed", termsOfServiceAgreed);
|
claims.put("terms-of-service-agreed", termsOfServiceAgreed);
|
||||||
}
|
}
|
||||||
|
if (keyIdentifier != null) {
|
||||||
|
claims.put("external-account-binding",
|
||||||
|
createExternalAccountBinding(keyIdentifier, session.getKeyPair(), resourceUrl));
|
||||||
|
}
|
||||||
|
|
||||||
conn.sendJwkSignedRequest(session.resourceUrl(Resource.NEW_ACCOUNT), claims, session);
|
conn.sendJwkSignedRequest(resourceUrl, claims, session);
|
||||||
conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_CREATED);
|
conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_CREATED);
|
||||||
|
|
||||||
URL location = conn.getLocation();
|
URL location = conn.getLocation();
|
||||||
|
|
||||||
Registration reg = new Registration(session, location);
|
Registration reg = new Registration(session, location);
|
||||||
|
if (keyIdentifier != null) {
|
||||||
|
session.setKeyIdentifier(keyIdentifier);
|
||||||
|
}
|
||||||
reg.unmarshal(conn.readJsonResponse());
|
reg.unmarshal(conn.readJsonResponse());
|
||||||
return reg;
|
return reg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a JSON structure for external account binding.
|
||||||
|
*
|
||||||
|
* @param kid
|
||||||
|
* Key Identifier provided by the CA
|
||||||
|
* @param keyPair
|
||||||
|
* {@link KeyPair} of the account to be created
|
||||||
|
* @param resource
|
||||||
|
* "new-account" resource URL
|
||||||
|
* @return Created JSON structure
|
||||||
|
*/
|
||||||
|
private Map<String, Object> createExternalAccountBinding(String kid, KeyPair keyPair, URL resource)
|
||||||
|
throws AcmeException {
|
||||||
|
try {
|
||||||
|
PublicJsonWebKey keyJwk = PublicJsonWebKey.Factory.newPublicJwk(keyPair.getPublic());
|
||||||
|
|
||||||
|
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.sign();
|
||||||
|
|
||||||
|
JSONBuilder outerClaim = new JSONBuilder();
|
||||||
|
outerClaim.put("protected", innerJws.getHeaders().getEncodedHeader());
|
||||||
|
outerClaim.put("signature", innerJws.getEncodedSignature());
|
||||||
|
outerClaim.put("payload", innerJws.getEncodedPayload());
|
||||||
|
return outerClaim.toMap();
|
||||||
|
} catch (JoseException ex) {
|
||||||
|
throw new AcmeException("Could not create external account binding", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,16 @@ 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 org.jose4j.jws.JsonWebSignature;
|
||||||
|
import org.jose4j.jwx.CompactSerializer;
|
||||||
|
import org.jose4j.lang.JoseException;
|
||||||
import org.junit.Test;
|
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.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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link RegistrationBuilder}.
|
* Unit tests for {@link RegistrationBuilder}.
|
||||||
|
@ -107,4 +111,82 @@ public class RegistrationBuilderTest {
|
||||||
provider.close();
|
provider.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if a new registration with Key Identifier can be created.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testRegistrationWithKid() throws Exception {
|
||||||
|
String keyIdentifier = "NCC-1701";
|
||||||
|
|
||||||
|
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||||
|
@Override
|
||||||
|
public void sendJwkSignedRequest(URL url, JSONBuilder claims, Session session) {
|
||||||
|
try {
|
||||||
|
assertThat(session, is(notNullValue()));
|
||||||
|
assertThat(url, is(resourceUrl));
|
||||||
|
|
||||||
|
JSON binding = claims.toJSON()
|
||||||
|
.get("external-account-binding")
|
||||||
|
.required()
|
||||||
|
.asObject();
|
||||||
|
|
||||||
|
String encodedHeader = binding.get("protected").asString();
|
||||||
|
String encodedSignature = binding.get("signature").asString();
|
||||||
|
String encodedPayload = binding.get("payload").asString();
|
||||||
|
|
||||||
|
String serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
|
||||||
|
JsonWebSignature jws = new JsonWebSignature();
|
||||||
|
jws.setCompactSerialization(serialized);
|
||||||
|
jws.setKey(session.getKeyPair().getPublic());
|
||||||
|
assertThat(jws.verifySignature(), is(true));
|
||||||
|
|
||||||
|
assertThat(jws.getHeader("url"), is(resourceUrl.toString()));
|
||||||
|
assertThat(jws.getHeader("kid"), is(keyIdentifier));
|
||||||
|
assertThat(jws.getHeader("alg"), is("RS256"));
|
||||||
|
|
||||||
|
String decodedPayload = jws.getPayload();
|
||||||
|
StringBuilder expectedPayload = new StringBuilder();
|
||||||
|
expectedPayload.append('{');
|
||||||
|
expectedPayload.append("\"kty\":\"").append(TestUtils.KTY).append("\",");
|
||||||
|
expectedPayload.append("\"e\":\"").append(TestUtils.E).append("\",");
|
||||||
|
expectedPayload.append("\"n\":\"").append(TestUtils.N).append("\"");
|
||||||
|
expectedPayload.append("}");
|
||||||
|
assertThat(decodedPayload, sameJSONAs(expectedPayload.toString()));
|
||||||
|
} catch (JoseException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
fail("decoding inner payload failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int accept(int... httpStatus) throws AcmeException {
|
||||||
|
assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_CREATED));
|
||||||
|
return HttpURLConnection.HTTP_CREATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getLocation() {
|
||||||
|
return locationUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSON readJsonResponse() {
|
||||||
|
return JSON.empty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
provider.putTestResource(Resource.NEW_ACCOUNT, resourceUrl);
|
||||||
|
|
||||||
|
RegistrationBuilder builder = new RegistrationBuilder();
|
||||||
|
builder.useKeyIdentifier(keyIdentifier);
|
||||||
|
|
||||||
|
Session session = provider.createSession();
|
||||||
|
Registration registration = builder.create(session);
|
||||||
|
|
||||||
|
assertThat(registration.getLocation(), is(locationUrl));
|
||||||
|
assertThat(session.getKeyIdentifier(), is(keyIdentifier));
|
||||||
|
|
||||||
|
provider.close();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue