From c4f75497c761345753d5c0f281c3d698f83744e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Tue, 2 May 2017 13:14:24 +0200 Subject: [PATCH] Set individual key identifier on account creation --- .../shredzone/acme4j/RegistrationBuilder.java | 69 +++++++++++++++- .../acme4j/RegistrationBuilderTest.java | 82 +++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/RegistrationBuilder.java b/acme4j-client/src/main/java/org/shredzone/acme4j/RegistrationBuilder.java index 00fd5f06..764f1920 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/RegistrationBuilder.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/RegistrationBuilder.java @@ -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 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 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); + } + } + } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationBuilderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationBuilderTest.java index c9840ab2..a6c912b5 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationBuilderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationBuilderTest.java @@ -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(); + } + }