Add ProofOfPossession challenge

pull/17/merge
Richard Körber 2015-12-24 16:24:34 +01:00
parent 8ada797df3
commit 0f56583c18
16 changed files with 531 additions and 40 deletions

View File

@ -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

View File

@ -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.
*

View File

@ -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()));
}

View File

@ -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();

View File

@ -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()));
}

View File

@ -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

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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
+ "}"));
}
}

View File

@ -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);

View File

@ -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.

View File

@ -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.

View File

@ -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", \

View File

@ -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.