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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue