Key-Identifier is part of the session

pull/55/head
Richard Körber 2017-04-15 17:20:31 +02:00
parent c667aba488
commit 1b058f2753
11 changed files with 182 additions and 36 deletions

View File

@ -59,6 +59,7 @@ public abstract class AcmeResource implements Serializable {
*/
protected void setLocation(URI location) {
this.location = Objects.requireNonNull(location, "location");
session.setKeyIdentifier(this.location);
}
/**

View File

@ -87,6 +87,10 @@ public class RegistrationBuilder {
public Registration create(Session session) throws AcmeException {
LOG.debug("create");
if (session.getKeyIdentifier() != null) {
throw new IllegalArgumentException("session already seems to have a Registration");
}
try (Connection conn = session.provider().connect()) {
JSONBuilder claims = new JSONBuilder();
claims.putResource(Resource.NEW_REG);
@ -97,7 +101,7 @@ public class RegistrationBuilder {
claims.put("terms-of-service-agreed", termsOfServiceAgreed);
}
conn.sendSignedRequest(session.resourceUri(Resource.NEW_REG), claims, session);
conn.sendJwkSignedRequest(session.resourceUri(Resource.NEW_REG), claims, session);
conn.accept(HttpURLConnection.HTTP_CREATED);
URI location = conn.getLocation();

View File

@ -44,6 +44,7 @@ public class Session {
private final URI serverUri;
private KeyPair keyPair;
private URI keyIdentifier;
private AcmeProvider provider;
private byte[] nonce;
private JSON directoryJson;
@ -97,6 +98,20 @@ public class Session {
this.keyPair = keyPair;
}
/**
* Gets the key identifier of the ACME account.
*/
public URI getKeyIdentifier() {
return keyIdentifier;
}
/**
* Sets the key identifier of the ACME account.
*/
public void setKeyIdentifier(URI keyIdentifier) {
this.keyIdentifier = keyIdentifier;
}
/**
* Gets the last nonce, or {@code null} if the session is new.
*/

View File

@ -47,7 +47,8 @@ public interface Connection extends AutoCloseable {
void sendRequest(URI uri, Session session) throws AcmeException;
/**
* Sends a signed POST request.
* Sends a signed POST request. Ensures that the session has a KeyIdentifier set that
* is used in the "kid" protected header.
*
* @param uri
* {@link URI} to send the request to.
@ -58,6 +59,19 @@ public interface Connection extends AutoCloseable {
*/
void sendSignedRequest(URI uri, JSONBuilder claims, Session session) throws AcmeException;
/**
* Sends a signed POST request. If the session's KeyIdentifier is set, a "kid"
* protected header is sent. If not, a "jwk" protected header is sent.
*
* @param uri
* {@link URI} to send the request to.
* @param claims
* {@link JSONBuilder} containing claims. Must not be {@code null}.
* @param session
* {@link Session} instance to be used for signing and tracking
*/
void sendJwkSignedRequest(URI uri, JSONBuilder claims, Session session) throws AcmeException;
/**
* Checks if the HTTP response status is in the given list of acceptable HTTP states,
* otherwise raises an {@link AcmeException} matching the error.

View File

@ -147,6 +147,15 @@ public class DefaultConnection implements Connection {
@Override
public void sendSignedRequest(URI uri, JSONBuilder claims, Session session) throws AcmeException {
if (session.getKeyIdentifier() == null) {
throw new IllegalStateException("session has no KeyIdentifier set");
}
sendJwkSignedRequest(uri, claims, session);
}
@Override
public void sendJwkSignedRequest(URI uri, JSONBuilder claims, Session session) throws AcmeException {
Objects.requireNonNull(uri, "uri");
Objects.requireNonNull(claims, "claims");
Objects.requireNonNull(session, "session");
@ -170,12 +179,15 @@ public class DefaultConnection implements Connection {
conn.setDoOutput(true);
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(keypair.getPublic());
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toString());
jws.getHeaders().setObjectHeaderValue("nonce", Base64Url.encode(session.getNonce()));
jws.getHeaders().setObjectHeaderValue("url", uri);
jws.getHeaders().setJwkHeaderValue("jwk", jwk);
if (session.getKeyIdentifier() != null) {
jws.getHeaders().setObjectHeaderValue("kid", session.getKeyIdentifier());
} else {
jws.getHeaders().setJwkHeaderValue("jwk", jwk);
}
jws.setAlgorithmHeaderValue(keyAlgorithm(jwk));
jws.setKey(keypair.getPrivate());
byte[] outputData = jws.getCompactSerialization().getBytes(DEFAULT_CHARSET);

View File

@ -47,14 +47,17 @@ public class RegistrationBuilderTest {
@Override
public void sendSignedRequest(URI uri, JSONBuilder claims, Session session) {
assertThat(session, is(notNullValue()));
if (resourceUri.equals(uri)) {
isUpdate = false;
assertThat(claims.toString(), sameJSONAs(getJson("newRegistration")));
} else if (locationUri.equals(uri)) {
isUpdate = true;
} else {
fail("bad URI");
}
assertThat(uri, is(locationUri));
assertThat(isUpdate, is(false));
isUpdate = true;
}
@Override
public void sendJwkSignedRequest(URI uri, JSONBuilder claims, Session session) {
assertThat(session, is(notNullValue()));
assertThat(uri, is(resourceUri));
assertThat(claims.toString(), sameJSONAs(getJson("newRegistration")));
isUpdate = false;
}
@Override
@ -86,10 +89,21 @@ public class RegistrationBuilderTest {
builder.addContact("mailto:foo@example.com");
builder.agreeToTermsOfService();
Registration registration = builder.create(provider.createSession());
Session session = provider.createSession();
Registration registration = builder.create(session);
assertThat(registration.getLocation(), is(locationUri));
assertThat(registration.getTermsOfServiceAgreed(), is(true));
assertThat(session.getKeyIdentifier(), is(locationUri));
try {
RegistrationBuilder builder2 = new RegistrationBuilder();
builder2.agreeToTermsOfService();
builder2.create(session);
fail("registered twice on same session");
} catch (IllegalArgumentException ex) {
// expected
}
provider.close();
}

View File

@ -120,9 +120,11 @@ public class RegistrationTest {
}
};
Registration registration = new Registration(provider.createSession(), locationUri);
Session session = provider.createSession();
Registration registration = new Registration(session, locationUri);
registration.update();
assertThat(session.getKeyIdentifier(), is(locationUri));
assertThat(registration.getLocation(), is(locationUri));
assertThat(registration.getTermsOfServiceAgreed(), is(true));
assertThat(registration.getContacts(), hasSize(1));

View File

@ -72,11 +72,13 @@ public class SessionTest {
assertThat(session, not(nullValue()));
assertThat(session.getServerUri(), is(serverUri));
assertThat(session.getKeyPair(), is(keyPair));
assertThat(session.getKeyIdentifier(), is(nullValue()));
Session session2 = new Session("https://example.com/acme", keyPair);
assertThat(session2, not(nullValue()));
assertThat(session2.getServerUri(), is(serverUri));
assertThat(session2.getKeyPair(), is(keyPair));
assertThat(session2.getKeyIdentifier(), is(nullValue()));
try {
new Session("#*aBaDuRi*#", keyPair);
@ -94,6 +96,7 @@ public class SessionTest {
KeyPair kp1 = TestUtils.createKeyPair();
KeyPair kp2 = TestUtils.createDomainKeyPair();
URI serverUri = URI.create(TestUtils.ACME_SERVER_URI);
URI keyIdentifierUri = URI.create(TestUtils.ACME_SERVER_URI + "/acct/1");
Session session = new Session(serverUri, kp1);
@ -106,6 +109,10 @@ public class SessionTest {
session.setKeyPair(kp2);
assertThat(session.getKeyPair(), is(kp2));
assertThat(session.getKeyIdentifier(), is(nullValue()));
session.setKeyIdentifier(keyIdentifierUri);
assertThat(session.getKeyIdentifier(), is(keyIdentifierUri));
assertThat(session.getServerUri(), is(serverUri));
}

View File

@ -56,7 +56,8 @@ import org.shredzone.acme4j.util.TestUtils;
*/
public class DefaultConnectionTest {
private URI requestUri = URI.create("http://example.com/acme/");;
private URI requestUri = URI.create("http://example.com/acme/");
private URI keyIdentifierUri = URI.create(TestUtils.ACME_SERVER_URI + "/acct/1");
private HttpURLConnection mockUrlConnection;
private HttpConnector mockHttpConnection;
private Session session;
@ -581,7 +582,81 @@ public class DefaultConnectionTest {
}) {
JSONBuilder cb = new JSONBuilder();
cb.put("foo", 123).put("bar", "a-string");
conn.sendSignedRequest(requestUri, cb, DefaultConnectionTest.this.session);
session.setKeyIdentifier(keyIdentifierUri);
conn.sendSignedRequest(requestUri, cb, session);
}
verify(mockUrlConnection).setRequestMethod("POST");
verify(mockUrlConnection).setRequestProperty("Accept", "application/json");
verify(mockUrlConnection).setRequestProperty("Accept-Charset", "utf-8");
verify(mockUrlConnection).setRequestProperty("Accept-Language", "ja-JP");
verify(mockUrlConnection).setRequestProperty("Content-Type", "application/jose+json");
verify(mockUrlConnection).connect();
verify(mockUrlConnection).setDoOutput(true);
verify(mockUrlConnection).setFixedLengthStreamingMode(outputStream.toByteArray().length);
verify(mockUrlConnection).getOutputStream();
verify(mockUrlConnection, atLeast(0)).getHeaderFields();
verifyNoMoreInteractions(mockUrlConnection);
String serialized = new String(outputStream.toByteArray(), "utf-8");
String[] written = CompactSerializer.deserialize(serialized);
String header = Base64Url.decodeToUtf8String(written[0]);
String claims = Base64Url.decodeToUtf8String(written[1]);
String signature = written[2];
StringBuilder expectedHeader = new StringBuilder();
expectedHeader.append('{');
expectedHeader.append("\"nonce\":\"").append(Base64Url.encode(nonce1)).append("\",");
expectedHeader.append("\"url\":\"").append(requestUri).append("\",");
expectedHeader.append("\"alg\":\"RS256\",");
expectedHeader.append("\"kid\":\"").append(keyIdentifierUri).append('"');
expectedHeader.append('}');
assertThat(header, sameJSONAs(expectedHeader.toString()));
assertThat(claims, sameJSONAs("{\"foo\":123,\"bar\":\"a-string\"}"));
assertThat(signature, not(isEmptyOrNullString()));
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(serialized);
jws.setKey(session.getKeyPair().getPublic());
assertThat(jws.verifySignature(), is(true));
}
/**
* Test signed POST requests without KeyIdentifier.
*/
@Test
public void testSendSignedRequestNoKid() throws Exception {
final byte[] nonce1 = "foo-nonce-1-foo".getBytes();
final byte[] nonce2 = "foo-nonce-2-foo".getBytes();
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
when(mockUrlConnection.getOutputStream()).thenReturn(outputStream);
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
@Override
public void resetNonce(Session session) throws AcmeException {
assertThat(session, is(sameInstance(DefaultConnectionTest.this.session)));
if (session.getNonce() == null) {
session.setNonce(nonce1);
} else {
fail("unknown nonce");
}
};
@Override
public void updateSession(Session session) {
assertThat(session, is(sameInstance(DefaultConnectionTest.this.session)));
if (session.getNonce() == nonce1) {
session.setNonce(nonce2);
} else {
fail("unknown nonce");
}
};
}) {
JSONBuilder cb = new JSONBuilder();
cb.put("foo", 123).put("bar", "a-string");
conn.sendJwkSignedRequest(requestUri, cb, session);
}
verify(mockUrlConnection).setRequestMethod("POST");
@ -619,10 +694,21 @@ public class DefaultConnectionTest {
JsonWebSignature jws = new JsonWebSignature();
jws.setCompactSerialization(serialized);
jws.setKey(DefaultConnectionTest.this.session.getKeyPair().getPublic());
jws.setKey(session.getKeyPair().getPublic());
assertThat(jws.verifySignature(), is(true));
}
/**
* Test signed POST requests without a required KeyIdentifier.
*/
@Test(expected = IllegalStateException.class)
public void testSendSignedRequestNoKidFailed() throws Exception {
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
JSONBuilder cb = new JSONBuilder();
conn.sendSignedRequest(requestUri, cb, session);
}
}
/**
* Test signed POST requests if there is no nonce.
*/
@ -635,7 +721,7 @@ public class DefaultConnectionTest {
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
JSONBuilder cb = new JSONBuilder();
conn.sendSignedRequest(requestUri, cb, DefaultConnectionTest.this.session);
conn.sendJwkSignedRequest(requestUri, cb, DefaultConnectionTest.this.session);
}
}

View File

@ -43,6 +43,11 @@ public class DummyConnection implements Connection {
throw new UnsupportedOperationException();
}
@Override
public void sendJwkSignedRequest(URI uri, JSONBuilder claims, Session session) {
throw new UnsupportedOperationException();
}
@Override
public int accept(int... httpStatus) throws AcmeException {
throw new UnsupportedOperationException();

View File

@ -14,7 +14,7 @@
package org.shredzone.acme4j.it;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
import java.net.URI;
import java.security.KeyPair;
@ -25,7 +25,6 @@ import org.shredzone.acme4j.Registration;
import org.shredzone.acme4j.RegistrationBuilder;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.exception.AcmeConflictException;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeUnauthorizedException;
@ -47,6 +46,9 @@ public class RegistrationIT extends AbstractPebbleIT {
Registration reg = rb.create(session);
URI location = reg.getLocation();
assertIsPebbleUri(location);
URI keyIdentifier = session.getKeyIdentifier();
assertIsPebbleUri(keyIdentifier);
assertThat(keyIdentifier, is(location));
// TODO: Not yet supported by Pebble
/*
@ -120,22 +122,6 @@ public class RegistrationIT extends AbstractPebbleIT {
assertThat(newRegistration.getStatus(), is(Status.GOOD));
}
@Test
public void testDuplicate() throws AcmeException {
KeyPair keyPair = createKeyPair();
Session session = new Session(pebbleURI(), keyPair);
// First registration
new RegistrationBuilder().agreeToTermsOfService().create(session);
try {
new RegistrationBuilder().agreeToTermsOfService().create(session);
fail("Successfully registered KeyPair a second time");
} catch (AcmeConflictException ex) {
// This exception is expected
}
}
@Test
@Ignore // TODO: Not yet supported by Pebble
public void testDeactivate() throws AcmeException {