Set individual key identifier on account creation

pull/55/head
Richard Körber 2017-05-02 13:14:24 +02:00
parent f841daa5b6
commit c4f75497c7
2 changed files with 150 additions and 1 deletions

View File

@ -13,12 +13,19 @@
*/
package org.shredzone.acme4j;
import static org.shredzone.acme4j.util.AcmeUtils.keyAlgorithm;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.security.KeyPair;
import java.util.ArrayList;
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.Resource;
import org.shredzone.acme4j.exception.AcmeException;
@ -34,6 +41,7 @@ public class RegistrationBuilder {
private List<URI> contacts = new ArrayList<>();
private Boolean termsOfServiceAgreed;
private String keyIdentifier;
/**
* Add a contact URI to the list of contacts.
@ -73,6 +81,22 @@ public class RegistrationBuilder {
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.
*
@ -88,6 +112,8 @@ public class RegistrationBuilder {
}
try (Connection conn = session.provider().connect()) {
URL resourceUrl = session.resourceUrl(Resource.NEW_ACCOUNT);
JSONBuilder claims = new JSONBuilder();
if (!contacts.isEmpty()) {
claims.put("contact", contacts);
@ -95,16 +121,57 @@ public class RegistrationBuilder {
if (termsOfServiceAgreed != null) {
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);
URL location = conn.getLocation();
Registration reg = new Registration(session, location);
if (keyIdentifier != null) {
session.setKeyIdentifier(keyIdentifier);
}
reg.unmarshal(conn.readJsonResponse());
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);
}
}
}

View File

@ -21,12 +21,16 @@ import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.net.HttpURLConnection;
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.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.JSONBuilder;
import org.shredzone.acme4j.util.TestUtils;
/**
* Unit tests for {@link RegistrationBuilder}.
@ -107,4 +111,82 @@ public class RegistrationBuilderTest {
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();
}
}