mirror of https://github.com/shred/acme4j
Add ProofOfPossession challenge
parent
8ada797df3
commit
0f56583c18
|
@ -32,11 +32,8 @@ The ACME specifications are in draft status and subject to change.
|
|||
|
||||
See the [online documentation](http://www.shredzone.org/maven/acme4j/) for how to use _acme4j_. Or just have a look at [the source code of an example](https://github.com/shred/acme4j/blob/master/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java).
|
||||
|
||||
## Missing
|
||||
|
||||
The following feature is planned to be completed for the first beta release, but is still missing:
|
||||
|
||||
* `proofOfPossession-01` challenge support.
|
||||
|
||||
## Contribute
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import java.io.Serializable;
|
|||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import org.shredzone.acme4j.Account;
|
||||
import org.shredzone.acme4j.Status;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
|
||||
|
@ -48,15 +47,6 @@ public interface Challenge extends Serializable {
|
|||
*/
|
||||
String getValidated();
|
||||
|
||||
/**
|
||||
* Authorizes a {@link Challenge} by signing it with an {@link Account}. This is
|
||||
* required before triggering the challenge.
|
||||
*
|
||||
* @param account
|
||||
* {@link Account} to sign the challenge with
|
||||
*/
|
||||
void authorize(Account account);
|
||||
|
||||
/**
|
||||
* Sets the challenge state by reading the given JSON map.
|
||||
*
|
||||
|
|
|
@ -76,9 +76,17 @@ public class DnsChallenge extends GenericChallenge {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Authorizes the {@link Challenge} by signing it with an {@link Account}.
|
||||
*
|
||||
* @param account
|
||||
* {@link Account} to sign the challenge with
|
||||
*/
|
||||
public void authorize(Account account) {
|
||||
super.authorize(account);
|
||||
if (account == null) {
|
||||
throw new NullPointerException("account must not be null");
|
||||
}
|
||||
|
||||
authorization = getToken() + '.' + Base64Url.encode(jwkThumbprint(account.getKeyPair().getPublic()));
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ import org.jose4j.json.JsonUtil;
|
|||
import org.jose4j.jwk.JsonWebKey;
|
||||
import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
|
||||
import org.jose4j.lang.JoseException;
|
||||
import org.shredzone.acme4j.Account;
|
||||
import org.shredzone.acme4j.Status;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
|
||||
|
@ -81,13 +80,6 @@ public class GenericChallenge implements Challenge {
|
|||
return get(KEY_VALIDATED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authorize(Account account) {
|
||||
if (account == null) {
|
||||
throw new NullPointerException("account must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmarshall(Map<String, Object> map) {
|
||||
String type = map.get(KEY_TYPE).toString();
|
||||
|
|
|
@ -60,9 +60,17 @@ public class HttpChallenge extends GenericChallenge {
|
|||
return authorization;
|
||||
}
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Authorizes the {@link Challenge} by signing it with an {@link Account}.
|
||||
*
|
||||
* @param account
|
||||
* {@link Account} to sign the challenge with
|
||||
*/
|
||||
public void authorize(Account account) {
|
||||
super.authorize(account);
|
||||
if (account == null) {
|
||||
throw new NullPointerException("account must not be null");
|
||||
}
|
||||
|
||||
authorization = getToken() + '.' + Base64Url.encode(jwkThumbprint(account.getKeyPair().getPublic()));
|
||||
}
|
||||
|
||||
|
|
|
@ -13,15 +13,27 @@
|
|||
*/
|
||||
package org.shredzone.acme4j.challenge;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jose4j.base64url.Base64Url;
|
||||
import org.jose4j.json.JsonUtil;
|
||||
import org.jose4j.lang.JoseException;
|
||||
import org.shredzone.acme4j.Account;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
import org.shredzone.acme4j.util.ValidationBuilder;
|
||||
|
||||
/**
|
||||
* Implements the {@code proofOfPossession-01} challenge.
|
||||
* <p>
|
||||
* <em>TODO: Currently this challenge is not implemented.</em>
|
||||
*
|
||||
* @author Richard "Shred" Körber
|
||||
*/
|
||||
|
@ -33,18 +45,88 @@ public class ProofOfPossessionChallenge extends GenericChallenge {
|
|||
*/
|
||||
public static final String TYPE = "proofOfPossession-01";
|
||||
|
||||
private PublicKey accountKey;
|
||||
private Collection<X509Certificate> certs;
|
||||
private String validation;
|
||||
|
||||
/**
|
||||
* Gets the collection of {@link X509Certificate} known by the server.
|
||||
*/
|
||||
public Collection<X509Certificate> getCertificates() {
|
||||
return certs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorizes the challenge by signing it with the {@link Account} of the current
|
||||
* domain owner.
|
||||
*
|
||||
* @param ownerAccount
|
||||
* {@link Account} of the certificate holder
|
||||
* @param domainKeypair
|
||||
* {@link KeyPair} matching one of the requested certificates
|
||||
* @param domains
|
||||
* Domains to validate
|
||||
*/
|
||||
public void authorize(Account ownerAccount, KeyPair domainKeypair, String... domains) {
|
||||
importValidation(new ValidationBuilder()
|
||||
.domains(domains)
|
||||
.sign(ownerAccount, domainKeypair));
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a validation JWS.
|
||||
*
|
||||
* @param validation
|
||||
* JWS of the validation
|
||||
* @see ValidationBuilder
|
||||
*/
|
||||
public void importValidation(String validation) {
|
||||
try {
|
||||
Map<String, Object> json = JsonUtil.parseJson(validation);
|
||||
if (!json.keySet().containsAll(Arrays.asList("header", "payload", "signature"))) {
|
||||
throw new IllegalArgumentException("not a JWS");
|
||||
}
|
||||
} catch (JoseException ex) {
|
||||
throw new IllegalArgumentException("invalid JSON", ex);
|
||||
}
|
||||
|
||||
this.validation = validation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authorize(Account account) {
|
||||
super.authorize(account);
|
||||
accountKey = account.getKeyPair().getPublic();
|
||||
public void unmarshall(Map<String, Object> map) {
|
||||
super.unmarshall(map);
|
||||
|
||||
List<String> certData = get("certs");
|
||||
if (certData != null) {
|
||||
try {
|
||||
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
||||
|
||||
certs = new ArrayList<>(certData.size());
|
||||
for (String c : certData) {
|
||||
byte[] certDer = Base64Url.decode(c);
|
||||
try (ByteArrayInputStream in = new ByteArrayInputStream(certDer)) {
|
||||
certs.add((X509Certificate) certificateFactory.generateCertificate(in));
|
||||
}
|
||||
}
|
||||
} catch (CertificateException | IOException ex) {
|
||||
throw new IllegalArgumentException("Invalid certs", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void marshall(ClaimBuilder cb) {
|
||||
super.marshall(cb);
|
||||
cb.putKey("accountKey", accountKey);
|
||||
if (validation == null) {
|
||||
throw new IllegalStateException("not validated");
|
||||
}
|
||||
|
||||
try {
|
||||
cb.put(KEY_TYPE, getType());
|
||||
cb.put("authorization", JsonUtil.parseJson(validation));
|
||||
} catch (JoseException ex) {
|
||||
// should not happen, as the JSON is prevalidated in the setter
|
||||
throw new IllegalStateException("validation: invalid JSON", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -73,9 +73,17 @@ public class TlsSniChallenge extends GenericChallenge {
|
|||
return subject;
|
||||
}
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Authorizes the {@link Challenge} by signing it with an {@link Account}.
|
||||
*
|
||||
* @param account
|
||||
* {@link Account} to sign the challenge with
|
||||
*/
|
||||
public void authorize(Account account) {
|
||||
super.authorize(account);
|
||||
if (account == null) {
|
||||
throw new NullPointerException("account must not be null");
|
||||
}
|
||||
|
||||
authorization = getToken() + '.' + Base64Url.encode(jwkThumbprint(account.getKeyPair().getPublic()));
|
||||
|
||||
String hash = computeHash(authorization);
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2015 Richard "Shred" Körber
|
||||
* http://acme4j.shredzone.org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*/
|
||||
package org.shredzone.acme4j.util;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jose4j.jwk.PublicJsonWebKey;
|
||||
import org.jose4j.jws.AlgorithmIdentifiers;
|
||||
import org.jose4j.jws.JsonWebSignature;
|
||||
import org.jose4j.lang.JoseException;
|
||||
import org.shredzone.acme4j.Account;
|
||||
import org.shredzone.acme4j.challenge.ProofOfPossessionChallenge;
|
||||
|
||||
/**
|
||||
* Generates a validation string for {@link ProofOfPossessionChallenge}.
|
||||
*
|
||||
* @author Richard "Shred" Körber
|
||||
*/
|
||||
public class ValidationBuilder {
|
||||
|
||||
private final List<Map<String, Object>> identifiers = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a domain to the validation.
|
||||
*
|
||||
* @param domain
|
||||
* Domain to be added
|
||||
* @return {@code this}
|
||||
*/
|
||||
public ValidationBuilder domain(String domain) {
|
||||
if (domain == null || domain.isEmpty()) {
|
||||
throw new IllegalArgumentException("domain must not be empty or null");
|
||||
}
|
||||
|
||||
ClaimBuilder cb = new ClaimBuilder();
|
||||
cb.put("type", "dns").put("value", domain);
|
||||
identifiers.add(cb.toMap());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a collection of domains to the validation.
|
||||
*
|
||||
* @param domains
|
||||
* Domains to be added
|
||||
* @return {@code this}
|
||||
*/
|
||||
public ValidationBuilder domains(Collection<String> domains) {
|
||||
if (domains == null) {
|
||||
throw new NullPointerException("domains must not be null");
|
||||
}
|
||||
|
||||
for (String d : domains) {
|
||||
domain(d);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple domains to the validation.
|
||||
*
|
||||
* @param domains
|
||||
* Domains to be added
|
||||
* @return {@code this}
|
||||
*/
|
||||
public ValidationBuilder domains(String... domains) {
|
||||
return domains(Arrays.asList(domains));
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs with the given {@link KeyPair} and returns a signed JSON Web Signature
|
||||
* structure that can be used for validation.
|
||||
*
|
||||
* @param account
|
||||
* {@link Account} of the current domain owner
|
||||
* @param keypair
|
||||
* One of the {@link KeyPair} requested by the challenge
|
||||
* @return JWS validation object
|
||||
*/
|
||||
public String sign(Account account, KeyPair keypair) {
|
||||
if (account == null) {
|
||||
throw new NullPointerException("account must not be null");
|
||||
}
|
||||
if (keypair == null) {
|
||||
throw new NullPointerException("keypair must not be null");
|
||||
}
|
||||
|
||||
try {
|
||||
ClaimBuilder claims = new ClaimBuilder();
|
||||
claims.put("type", ProofOfPossessionChallenge.TYPE);
|
||||
claims.array("identifiers", identifiers.toArray());
|
||||
claims.putKey("accountKey", account.getKeyPair().getPublic());
|
||||
|
||||
final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(keypair.getPublic());
|
||||
|
||||
JsonWebSignature jws = new JsonWebSignature();
|
||||
jws.setPayload(claims.toString());
|
||||
jws.getHeaders().setJwkHeaderValue("jwk", jwk);
|
||||
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
|
||||
jws.setKey(keypair.getPrivate());
|
||||
jws.sign();
|
||||
|
||||
ClaimBuilder auth = new ClaimBuilder();
|
||||
auth.put("header", jws.getHeaders().getFullHeaderAsJsonString());
|
||||
auth.put("payload", jws.getEncodedPayload());
|
||||
auth.put("signature", jws.getEncodedSignature());
|
||||
return auth.toString();
|
||||
} catch (JoseException ex) {
|
||||
throw new IllegalArgumentException("Failed to sign", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2015 Richard "Shred" Körber
|
||||
* http://acme4j.shredzone.org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*/
|
||||
package org.shredzone.acme4j.challenge;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.Account;
|
||||
import org.shredzone.acme4j.Status;
|
||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||
import org.shredzone.acme4j.util.TestUtils;
|
||||
import org.shredzone.acme4j.util.ValidationBuilder;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ProofOfPossessionChallenge}.
|
||||
*
|
||||
* @author Richard "Shred" Körber
|
||||
*/
|
||||
public class ProofOfPossessionChallengeTest {
|
||||
|
||||
/**
|
||||
* Test that {@link ProofOfPossessionChallenge} generates a correct authorization key.
|
||||
*/
|
||||
@Test
|
||||
public void testProofOfPossessionChallenge() throws IOException {
|
||||
X509Certificate cert = TestUtils.createCertificate();
|
||||
KeyPair keypair = TestUtils.createKeyPair();
|
||||
Account account = new Account(keypair);
|
||||
KeyPair domainKeyPair = TestUtils.createDomainKeyPair();
|
||||
|
||||
ProofOfPossessionChallenge challenge = new ProofOfPossessionChallenge();
|
||||
challenge.unmarshall(TestUtils.getJsonAsMap("proofOfPossessionChallenge"));
|
||||
|
||||
assertThat(challenge.getCertificates(), contains(cert));
|
||||
|
||||
assertThat(challenge.getType(), is(ProofOfPossessionChallenge.TYPE));
|
||||
assertThat(challenge.getStatus(), is(Status.PENDING));
|
||||
|
||||
try {
|
||||
challenge.marshall(new ClaimBuilder());
|
||||
fail("marshall() without previous authorize()");
|
||||
} catch (IllegalStateException ex) {
|
||||
// expected
|
||||
}
|
||||
|
||||
challenge.authorize(account, domainKeyPair, "example.org");
|
||||
|
||||
ClaimBuilder cb = new ClaimBuilder();
|
||||
challenge.marshall(cb);
|
||||
|
||||
assertThat(cb.toString(), sameJSONAs("{\"type\"=\""
|
||||
+ ProofOfPossessionChallenge.TYPE + "\",\"authorization\"="
|
||||
+ new ValidationBuilder().domain("example.org").sign(account, domainKeyPair)
|
||||
+ "}"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that {@link ProofOfPossessionChallenge#importValidation(String)} works
|
||||
* correctly.
|
||||
*/
|
||||
@Test
|
||||
public void testImportValidation() throws IOException {
|
||||
KeyPair keypair = TestUtils.createKeyPair();
|
||||
Account account = new Account(keypair);
|
||||
KeyPair domainKeyPair = TestUtils.createDomainKeyPair();
|
||||
|
||||
String validation = new ValidationBuilder()
|
||||
.domain("example.org")
|
||||
.sign(account, domainKeyPair);
|
||||
|
||||
ProofOfPossessionChallenge challenge = new ProofOfPossessionChallenge();
|
||||
challenge.unmarshall(TestUtils.getJsonAsMap("proofOfPossessionChallenge"));
|
||||
challenge.importValidation(validation);
|
||||
|
||||
ClaimBuilder cb = new ClaimBuilder();
|
||||
challenge.marshall(cb);
|
||||
|
||||
assertThat(cb.toString(), sameJSONAs("{\"type\"=\""
|
||||
+ ProofOfPossessionChallenge.TYPE + "\",\"authorization\"=" + validation
|
||||
+ "}"));
|
||||
}
|
||||
|
||||
}
|
|
@ -311,7 +311,7 @@ public class AbstractAcmeClientTest {
|
|||
|
||||
TestableAbstractAcmeClient client = new TestableAbstractAcmeClient(connection);
|
||||
|
||||
Challenge challenge = new HttpChallenge();
|
||||
HttpChallenge challenge = new HttpChallenge();
|
||||
challenge.unmarshall(getJsonAsMap("triggerHttpChallenge"));
|
||||
challenge.authorize(testAccount);
|
||||
|
||||
|
|
|
@ -51,6 +51,11 @@ public final class TestUtils {
|
|||
public static final String KTY = "RSA";
|
||||
public static final String THUMBPRINT = "HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0";
|
||||
|
||||
public static final String D_N = "tP7p9wOe0NWocwLu7h233i1JqUPW1MeLeilyHY7oMKnXZFyf1l0saqLcrBtOj3EyaG6qVfpiLEWEIiuWclPYSR_QSt9lCi9xAoWbYq9-mqseehXPaejynlIMsP2UiCAenSHjJEer6Ug6nFelGVgav3mypwYFUdvc18wI00clKYhRAc4dZodilRzDTLy95V1S3RCxGf-lE0XYg7ieO_ovSMERtH_7NsjZnBiaE7mwm0YZzreCr8oSuHwhC63kgY27FnCgH0h63LICSPVVDJZPLcWAmSXv1k0qoVTsRzFutRN6RB_96wqTTBi8Qm98lyCpXcsxa3BH-4TCvLEaa2KkeQ";
|
||||
public static final String D_E = "AQAB";
|
||||
public static final String D_KTY = "RSA";
|
||||
public static final String D_THUMBPRINT = "0VPbh7-I6swlkBu0TrNKSQp6d69bukzeQA0ksuX3FFs";
|
||||
|
||||
private static final ResourceBundle JSON_RESOURCE = ResourceBundle.getBundle("json");
|
||||
|
||||
private TestUtils() {
|
||||
|
@ -103,8 +108,8 @@ public final class TestUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a standard key pair for testing. This keypair is read from a test resource
|
||||
* and is guaranteed not to change between test runs.
|
||||
* Creates a standard account {@link KeyPair} for testing. The key pair is read from a
|
||||
* test resource and is guaranteed not to change between test runs.
|
||||
* <p>
|
||||
* The constants {@link #N}, {@link #E}, {@link #KTY} and {@link #THUMBPRINT} are
|
||||
* related to the returned key pair and can be used for asserting results.
|
||||
|
@ -129,6 +134,33 @@ public final class TestUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standard domain key pair for testing. This keypair is read from a test
|
||||
* resource and is guaranteed not to change between test runs.
|
||||
* <p>
|
||||
* The constants {@link #D_N}, {@link #D_E}, {@link #D_KTY} and {@link #D_THUMBPRINT}
|
||||
* are related to the returned key pair and can be used for asserting results.
|
||||
*
|
||||
* @return {@link KeyPair} for testing
|
||||
*/
|
||||
public static KeyPair createDomainKeyPair() throws IOException {
|
||||
try {
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(KTY);
|
||||
|
||||
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(
|
||||
getResourceAsByteArray("/domain-public.key"));
|
||||
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
|
||||
|
||||
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(
|
||||
getResourceAsByteArray("/domain-private.key"));
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
|
||||
|
||||
return new KeyPair(publicKey, privateKey);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standard certificate for testing. This certificate is read from a test
|
||||
* resource and is guaranteed not to change between test runs.
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2015 Richard "Shred" Körber
|
||||
* http://acme4j.shredzone.org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*/
|
||||
package org.shredzone.acme4j.util;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jose4j.base64url.Base64Url;
|
||||
import org.jose4j.json.JsonUtil;
|
||||
import org.jose4j.lang.JoseException;
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.Account;
|
||||
|
||||
/**
|
||||
* Unit test for {@link ValidationBuilder}.
|
||||
*
|
||||
* @author Richard "Shred" Körber
|
||||
*/
|
||||
public class ValidationBuilderTest {
|
||||
|
||||
/**
|
||||
* Test if a correct JWS validation object is generated.
|
||||
*/
|
||||
@Test
|
||||
public void testValidationBuilder() throws IOException, JoseException {
|
||||
Account account = new Account(TestUtils.createKeyPair());
|
||||
KeyPair domainKeyPair = TestUtils.createDomainKeyPair();
|
||||
|
||||
assertThat(account.getKeyPair(), not(domainKeyPair));
|
||||
|
||||
ValidationBuilder vb = new ValidationBuilder();
|
||||
vb.domain("abc.de").domain("ef.gh");
|
||||
vb.domains("ijk.lm", "no.pq", "rst.uv");
|
||||
vb.domains(Arrays.asList("w.x", "y.z"));
|
||||
String json = vb.sign(account, domainKeyPair);
|
||||
|
||||
Map<String, Object> data = JsonUtil.parseJson(json);
|
||||
|
||||
String header = (String) data.get("header");
|
||||
String payload = Base64Url.decodeToUtf8String((String) data.get("payload"));
|
||||
String signature = (String) data.get("signature");
|
||||
|
||||
StringBuilder expectedHeader = new StringBuilder();
|
||||
expectedHeader.append('{');
|
||||
expectedHeader.append("\"alg\":\"RS256\",");
|
||||
expectedHeader.append("\"jwk\":{");
|
||||
expectedHeader.append("\"kty\":\"").append(TestUtils.D_KTY).append("\",");
|
||||
expectedHeader.append("\"e\":\"").append(TestUtils.D_E).append("\",");
|
||||
expectedHeader.append("\"n\":\"").append(TestUtils.D_N).append("\"");
|
||||
expectedHeader.append("}}");
|
||||
|
||||
StringBuilder expectedPayload = new StringBuilder();
|
||||
expectedPayload.append('{');
|
||||
expectedPayload.append("\"type\":\"proofOfPossession-01\",");
|
||||
expectedPayload.append("\"identifiers\":[");
|
||||
for (String d : Arrays.asList("abc.de", "ef.gh", "ijk.lm", "no.pq", "rst.uv", "w.x", "y.z")) {
|
||||
expectedPayload.append("{\"type\":\"dns\",\"value\":\"").append(d).append("\"}");
|
||||
if (!"y.z".equals(d)) {
|
||||
expectedPayload.append(',');
|
||||
}
|
||||
}
|
||||
expectedPayload.append("],\"accountKey\":{");
|
||||
expectedPayload.append("\"kty\":\"").append(TestUtils.KTY).append("\",");
|
||||
expectedPayload.append("\"e\":\"").append(TestUtils.E).append("\",");
|
||||
expectedPayload.append("\"n\":\"").append(TestUtils.N).append("\"");
|
||||
expectedPayload.append("}}");
|
||||
|
||||
assertThat(header, sameJSONAs(expectedHeader.toString()).allowingExtraUnexpectedFields());
|
||||
assertThat(payload, sameJSONAs(expectedPayload.toString()));
|
||||
assertThat(signature, not(isEmptyOrNullString()));
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
Binary file not shown.
|
@ -150,6 +150,12 @@ httpChallenge = \
|
|||
"token": "rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ" \
|
||||
}
|
||||
|
||||
proofOfPossessionChallenge = \
|
||||
{ \
|
||||
"type": "proofOfPossession-01", \
|
||||
"certs": ["MIIDVzCCAj-gAwIBAgIJAM4KDTzb0Y7NMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwHhcNMTUxMjEwMDAxMTA4WhcNMjUxMjA3MDAxMTA4WjBCMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0g3w4C8xbj_5lzJiDxk0HkEJeZeyruq-0AzOPMigJZ7zxZtX_KUxOIHrQ4qjcFhl0DmQImoM0wESU-kcsjAHCx8E1lgRVlVsMfLAQPHkg5UybqfadzKT3ALcSD-9F9mVIP6liC_6KzLTASmx6zM7j92KTl1ArObZr5mh0jvSNORrMhEC4Byn3-NTxjuHON1rWppCMwpeNNhFzaAig3O8PY8IyaLXNP2Ac5pXn0iW16S-Im9by7751UeW5a7DznmuMEM-WY640ffJDQ4-I64H403uAgvvSu-BGw8SEEZGuBCxoCnG1g6y6OvJyN5TgqFdGosAfm1u-_MP1seoPdpBQIDAQABo1AwTjAdBgNVHQ4EFgQUrie5ZLOrA_HuhW1b_CHjzEvj34swHwYDVR0jBBgwFoAUrie5ZLOrA_HuhW1b_CHjzEvj34swDAYDVR0TBAUwAwEB_zANBgkqhkiG9w0BAQsFAAOCAQEAkSOP0FUgIIUeJTObgXrenHzZpLAkqXi37dgdYuPhNveo3agueP51N7yIoh6YGShiJ73Rvr-lVYTwFXStrLih1Wh3tWvksMxnvocgd7l6USRb5_AgH7eHeFK4DoCAak2hUAcCLDRJN3XMhNLpyJhw7GJxowVIGUlxcW5Asrmh9qflfyMyjripTP3CdHobmNcNHyScjNncKj37m8vomel9acekTtDl2Ci7nLdE-3VqQCXMIfLiF3PO0gGpKei0RuVCSOG6W83zVInCPd_l3aluSR-f_VZlk8KGQ4As4uTQi89j-J1YepzG0ASMZpjVbXeIg5QBAywVxBh5XVTz37KN8A"] \
|
||||
}
|
||||
|
||||
tlsSniChallenge = \
|
||||
{ \
|
||||
"type":"tls-sni-01", \
|
||||
|
|
|
@ -1,3 +1,52 @@
|
|||
# Proof of Possession
|
||||
|
||||
This challenge is not yet implemented in _acme4j_.
|
||||
With the Proof of Possesion challenge, you prove to the CA that you are able to provide a verification document that is signed with a key that is known to the server. The main purpose of this challenge is to transfer the control of a domain to your account.
|
||||
|
||||
The challenge object contains a list of `X509Certificate`s that are already known to the CA:
|
||||
|
||||
```java
|
||||
ProofOfPossessionChallenge challenge =
|
||||
auth.findChallenge(ProofOfPossessionChallenge.TYPE);
|
||||
Collection<X509Certificate> certificates = challenge.getCertificates();
|
||||
```
|
||||
|
||||
In the next step, the _current owner of the domain_ authorizes the challenge, by signing it with a key pair that corresponds to one of the `certificates`:
|
||||
|
||||
```java
|
||||
Account ownerAccount = ... // Account of the domain owner
|
||||
KeyPair domainKeyPair = ... // Key pair matching a certificates
|
||||
String domain = ... // Domain to authorize
|
||||
|
||||
challenge.authorize(ownerAccount, domainKeyPair, domain);
|
||||
```
|
||||
|
||||
The challenge is completed when the domain is associated with the account of the `ownerAccount`, and the `domainKeyPair` matches one of the `certificates`.
|
||||
|
||||
## Importing a Validation
|
||||
|
||||
A problem with this challenge is that a third party needs to provide the account and domain key pairs for authorization.
|
||||
|
||||
There is a way to prepare the validation externally, and import a validation document into the challenge in a separate step. The validation document is signed by the domain owner, but does not contain any private keys.
|
||||
|
||||
_acme4j_ offers a `ValidationBuilder` class for generating the validation document:
|
||||
|
||||
```java
|
||||
Account ownerAccount = ... // Account of the domain owner
|
||||
KeyPair domainKeyPair = ... // Key pair matching a certificates
|
||||
|
||||
ValidationBuilder vb = new ValidationBuilder();
|
||||
vb.domain("example.org");
|
||||
String json = vb.sign(ownerAccount, domainKeyPair);
|
||||
```
|
||||
|
||||
This `json` string can be transported (e.g. via email) and then imported into the challenge:
|
||||
|
||||
```java
|
||||
String json = ... // validation document
|
||||
|
||||
ProofOfPossessionChallenge challenge =
|
||||
auth.findChallenge(ProofOfPossessionChallenge.TYPE);
|
||||
challenge.importValidation(json);
|
||||
```
|
||||
|
||||
The challenge is authorized now, and is ready to be executed.
|
||||
|
|
Loading…
Reference in New Issue