mirror of https://github.com/shred/acme4j
Remove proof-of-possession challenge. Closes issue #4.
parent
bc8c8f24f0
commit
b8bfc5fa0f
|
@ -1,142 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 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.Registration;
|
|
||||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
|
||||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
|
||||||
import org.shredzone.acme4j.util.ValidationBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements the {@value TYPE} challenge.
|
|
||||||
*
|
|
||||||
* @author Richard "Shred" Körber
|
|
||||||
*/
|
|
||||||
public class ProofOfPossession01Challenge extends GenericChallenge {
|
|
||||||
private static final long serialVersionUID = 6212440828380185335L;
|
|
||||||
|
|
||||||
protected static final String KEY_CERTS = "certs";
|
|
||||||
protected static final String KEY_AUTHORIZATION = "authorization";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Challenge type name: {@value}
|
|
||||||
*/
|
|
||||||
public static final String TYPE = "proof-of-possession-01";
|
|
||||||
|
|
||||||
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 Registration} of the current
|
|
||||||
* domain owner.
|
|
||||||
*
|
|
||||||
* @param ownerRegistration
|
|
||||||
* {@link Registration} of the certificate holder
|
|
||||||
* @param domainKeypair
|
|
||||||
* {@link KeyPair} matching one of the requested certificates
|
|
||||||
* @param domains
|
|
||||||
* Domains to validate
|
|
||||||
*/
|
|
||||||
public void authorize(Registration ownerRegistration, KeyPair domainKeypair, String... domains) {
|
|
||||||
importValidation(new ValidationBuilder()
|
|
||||||
.domains(domains)
|
|
||||||
.sign(ownerRegistration, 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 unmarshall(Map<String, Object> map) {
|
|
||||||
super.unmarshall(map);
|
|
||||||
|
|
||||||
List<String> certData = get(KEY_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 AcmeProtocolException("Invalid certificates", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void respond(ClaimBuilder cb) {
|
|
||||||
if (validation == null) {
|
|
||||||
throw new IllegalStateException("not validated");
|
|
||||||
}
|
|
||||||
|
|
||||||
super.respond(cb);
|
|
||||||
|
|
||||||
try {
|
|
||||||
cb.put(KEY_AUTHORIZATION, JsonUtil.parseJson(validation));
|
|
||||||
} catch (JoseException ex) {
|
|
||||||
// should not happen, as the JSON is prevalidated in the setter
|
|
||||||
throw new AcmeProtocolException("validation: invalid JSON", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean acceptable(String type) {
|
|
||||||
return TYPE.equals(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -19,7 +19,6 @@ import org.shredzone.acme4j.AcmeClient;
|
||||||
import org.shredzone.acme4j.challenge.Challenge;
|
import org.shredzone.acme4j.challenge.Challenge;
|
||||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||||
import org.shredzone.acme4j.challenge.ProofOfPossession01Challenge;
|
|
||||||
import org.shredzone.acme4j.challenge.TlsSni01Challenge;
|
import org.shredzone.acme4j.challenge.TlsSni01Challenge;
|
||||||
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
||||||
import org.shredzone.acme4j.connector.Connection;
|
import org.shredzone.acme4j.connector.Connection;
|
||||||
|
@ -73,7 +72,6 @@ public abstract class AbstractAcmeClientProvider implements AcmeClientProvider {
|
||||||
case Dns01Challenge.TYPE: return new Dns01Challenge();
|
case Dns01Challenge.TYPE: return new Dns01Challenge();
|
||||||
case TlsSni01Challenge.TYPE: return new TlsSni01Challenge();
|
case TlsSni01Challenge.TYPE: return new TlsSni01Challenge();
|
||||||
case TlsSni02Challenge.TYPE: return new TlsSni02Challenge();
|
case TlsSni02Challenge.TYPE: return new TlsSni02Challenge();
|
||||||
case ProofOfPossession01Challenge.TYPE: return new ProofOfPossession01Challenge();
|
|
||||||
case Http01Challenge.TYPE: return new Http01Challenge();
|
case Http01Challenge.TYPE: return new Http01Challenge();
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.JsonWebSignature;
|
|
||||||
import org.jose4j.lang.JoseException;
|
|
||||||
import org.shredzone.acme4j.Registration;
|
|
||||||
import org.shredzone.acme4j.challenge.ProofOfPossession01Challenge;
|
|
||||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a validation string for {@link ProofOfPossession01Challenge}.
|
|
||||||
*
|
|
||||||
* @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 registration
|
|
||||||
* {@link Registration} of the current domain owner
|
|
||||||
* @param keypair
|
|
||||||
* One of the {@link KeyPair} requested by the challenge
|
|
||||||
* @return JWS validation object
|
|
||||||
*/
|
|
||||||
public String sign(Registration registration, KeyPair keypair) {
|
|
||||||
if (registration == null) {
|
|
||||||
throw new NullPointerException("registration must not be null");
|
|
||||||
}
|
|
||||||
if (keypair == null) {
|
|
||||||
throw new NullPointerException("keypair must not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ClaimBuilder claims = new ClaimBuilder();
|
|
||||||
claims.put("type", ProofOfPossession01Challenge.TYPE);
|
|
||||||
claims.array("identifiers", identifiers.toArray());
|
|
||||||
claims.putKey("accountKey", registration.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(SignatureUtils.keyAlgorithm(jwk));
|
|
||||||
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 AcmeProtocolException("Failed to sign", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -32,7 +32,6 @@ import org.junit.Test;
|
||||||
import org.shredzone.acme4j.challenge.Challenge;
|
import org.shredzone.acme4j.challenge.Challenge;
|
||||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||||
import org.shredzone.acme4j.challenge.ProofOfPossession01Challenge;
|
|
||||||
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,6 +41,8 @@ import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
||||||
*/
|
*/
|
||||||
public class AuthorizationTest {
|
public class AuthorizationTest {
|
||||||
|
|
||||||
|
private static final String SNAILMAIL_TYPE = "snail-01"; // a non-existent challenge
|
||||||
|
|
||||||
private Authorization authorization;
|
private Authorization authorization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,8 +102,8 @@ public class AuthorizationTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testFindChallenge() {
|
public void testFindChallenge() {
|
||||||
// ProofOfPossesionChallenge is not available at all
|
// A snail mail challenge is not available at all
|
||||||
Challenge c1 = authorization.findChallenge(ProofOfPossession01Challenge.TYPE);
|
Challenge c1 = authorization.findChallenge(SNAILMAIL_TYPE);
|
||||||
assertThat(c1, is(nullValue()));
|
assertThat(c1, is(nullValue()));
|
||||||
|
|
||||||
// HttpChallenge is available as standalone challenge
|
// HttpChallenge is available as standalone challenge
|
||||||
|
@ -140,7 +141,7 @@ public class AuthorizationTest {
|
||||||
instanceOf(TlsSni02Challenge.class)));
|
instanceOf(TlsSni02Challenge.class)));
|
||||||
|
|
||||||
// Finds smaller combinations as well
|
// Finds smaller combinations as well
|
||||||
Collection<Challenge> c4 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE, ProofOfPossession01Challenge.TYPE);
|
Collection<Challenge> c4 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE, SNAILMAIL_TYPE);
|
||||||
assertThat(c4, hasSize(2));
|
assertThat(c4, hasSize(2));
|
||||||
assertThat(c4, contains(instanceOf(Dns01Challenge.class),
|
assertThat(c4, contains(instanceOf(Dns01Challenge.class),
|
||||||
instanceOf(TlsSni02Challenge.class)));
|
instanceOf(TlsSni02Challenge.class)));
|
||||||
|
@ -155,7 +156,7 @@ public class AuthorizationTest {
|
||||||
assertThat(c6, is(nullValue()));
|
assertThat(c6, is(nullValue()));
|
||||||
|
|
||||||
// Does not find challenges that have not been provided
|
// Does not find challenges that have not been provided
|
||||||
Collection<Challenge> c7 = authorization.findCombination(ProofOfPossession01Challenge.TYPE);
|
Collection<Challenge> c7 = authorization.findCombination(SNAILMAIL_TYPE);
|
||||||
assertThat(c7, is(nullValue()));
|
assertThat(c7, is(nullValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.Registration;
|
|
||||||
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 ProofOfPossession01Challenge}.
|
|
||||||
*
|
|
||||||
* @author Richard "Shred" Körber
|
|
||||||
*/
|
|
||||||
public class ProofOfPossessionChallengeTest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that {@link ProofOfPossession01Challenge} generates a correct authorization key.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testProofOfPossessionChallenge() throws IOException {
|
|
||||||
X509Certificate cert = TestUtils.createCertificate();
|
|
||||||
KeyPair keypair = TestUtils.createKeyPair();
|
|
||||||
Registration reg = new Registration(keypair);
|
|
||||||
KeyPair domainKeyPair = TestUtils.createDomainKeyPair();
|
|
||||||
|
|
||||||
ProofOfPossession01Challenge challenge = new ProofOfPossession01Challenge();
|
|
||||||
challenge.unmarshall(TestUtils.getJsonAsMap("proofOfPossessionChallenge"));
|
|
||||||
|
|
||||||
assertThat(challenge.getCertificates(), contains(cert));
|
|
||||||
|
|
||||||
assertThat(challenge.getType(), is(ProofOfPossession01Challenge.TYPE));
|
|
||||||
assertThat(challenge.getStatus(), is(Status.PENDING));
|
|
||||||
|
|
||||||
try {
|
|
||||||
challenge.respond(new ClaimBuilder());
|
|
||||||
fail("marshall() without previous authorize()");
|
|
||||||
} catch (IllegalStateException ex) {
|
|
||||||
// expected
|
|
||||||
}
|
|
||||||
|
|
||||||
challenge.authorize(reg, domainKeyPair, "example.org");
|
|
||||||
|
|
||||||
ClaimBuilder cb = new ClaimBuilder();
|
|
||||||
challenge.respond(cb);
|
|
||||||
|
|
||||||
assertThat(cb.toString(), sameJSONAs("{\"type\"=\""
|
|
||||||
+ ProofOfPossession01Challenge.TYPE + "\",\"authorization\"="
|
|
||||||
+ new ValidationBuilder().domain("example.org").sign(reg, domainKeyPair)
|
|
||||||
+ "}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that {@link ProofOfPossession01Challenge#importValidation(String)} works
|
|
||||||
* correctly.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void testImportValidation() throws IOException {
|
|
||||||
KeyPair keypair = TestUtils.createKeyPair();
|
|
||||||
Registration reg = new Registration(keypair);
|
|
||||||
KeyPair domainKeyPair = TestUtils.createDomainKeyPair();
|
|
||||||
|
|
||||||
String validation = new ValidationBuilder()
|
|
||||||
.domain("example.org")
|
|
||||||
.sign(reg, domainKeyPair);
|
|
||||||
|
|
||||||
ProofOfPossession01Challenge challenge = new ProofOfPossession01Challenge();
|
|
||||||
challenge.unmarshall(TestUtils.getJsonAsMap("proofOfPossessionChallenge"));
|
|
||||||
challenge.importValidation(validation);
|
|
||||||
|
|
||||||
ClaimBuilder cb = new ClaimBuilder();
|
|
||||||
challenge.respond(cb);
|
|
||||||
|
|
||||||
assertThat(cb.toString(), sameJSONAs("{\"type\"=\""
|
|
||||||
+ ProofOfPossession01Challenge.TYPE + "\",\"authorization\"=" + validation
|
|
||||||
+ "}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -24,7 +24,6 @@ import org.shredzone.acme4j.AcmeClient;
|
||||||
import org.shredzone.acme4j.challenge.Challenge;
|
import org.shredzone.acme4j.challenge.Challenge;
|
||||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||||
import org.shredzone.acme4j.challenge.ProofOfPossession01Challenge;
|
|
||||||
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,20 +100,16 @@ public class AbstractAcmeClientProviderTest {
|
||||||
assertThat(c3, not(nullValue()));
|
assertThat(c3, not(nullValue()));
|
||||||
assertThat(c3, instanceOf(Dns01Challenge.class));
|
assertThat(c3, instanceOf(Dns01Challenge.class));
|
||||||
|
|
||||||
Challenge c4 = provider.createChallenge(ProofOfPossession01Challenge.TYPE);
|
Challenge c4 = provider.createChallenge(org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE);
|
||||||
assertThat(c4, not(nullValue()));
|
assertThat(c4, not(nullValue()));
|
||||||
assertThat(c4, instanceOf(ProofOfPossession01Challenge.class));
|
assertThat(c4, instanceOf(org.shredzone.acme4j.challenge.TlsSni01Challenge.class));
|
||||||
|
|
||||||
Challenge c5 = provider.createChallenge(org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE);
|
Challenge c5 = provider.createChallenge(TlsSni02Challenge.TYPE);
|
||||||
assertThat(c5, not(nullValue()));
|
assertThat(c5, not(nullValue()));
|
||||||
assertThat(c5, instanceOf(org.shredzone.acme4j.challenge.TlsSni01Challenge.class));
|
assertThat(c5, instanceOf(TlsSni02Challenge.class));
|
||||||
|
|
||||||
Challenge c6 = provider.createChallenge(TlsSni02Challenge.TYPE);
|
Challenge c6 = provider.createChallenge("foobar-01");
|
||||||
assertThat(c6, not(nullValue()));
|
assertThat(c6, is(nullValue()));
|
||||||
assertThat(c6, instanceOf(TlsSni02Challenge.class));
|
|
||||||
|
|
||||||
Challenge c7 = provider.createChallenge("foobar-01");
|
|
||||||
assertThat(c7, is(nullValue()));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
provider.createChallenge(null);
|
provider.createChallenge(null);
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.Registration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
Registration reg = new Registration(TestUtils.createKeyPair());
|
|
||||||
KeyPair domainKeyPair = TestUtils.createDomainKeyPair();
|
|
||||||
|
|
||||||
assertThat(reg.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(reg, 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\":\"proof-of-possession-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()));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -150,12 +150,6 @@ httpChallenge = \
|
||||||
"token": "rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ" \
|
"token": "rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ" \
|
||||||
}
|
}
|
||||||
|
|
||||||
proofOfPossessionChallenge = \
|
|
||||||
{ \
|
|
||||||
"type": "proof-of-possession-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 = \
|
tlsSniChallenge = \
|
||||||
{ \
|
{ \
|
||||||
"type":"tls-sni-01", \
|
"type":"tls-sni-01", \
|
||||||
|
|
|
@ -12,4 +12,3 @@ The ACME specifications define these standard challenges:
|
||||||
* [dns-01](./dns-01.html)
|
* [dns-01](./dns-01.html)
|
||||||
* [tls-sni-01](./tls-sni-01.html)
|
* [tls-sni-01](./tls-sni-01.html)
|
||||||
* [tls-sni-02](./tls-sni-02.html)
|
* [tls-sni-02](./tls-sni-02.html)
|
||||||
* [proof-of-possession-01](./proof-of-possession-01.html)
|
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
# proof-of-possession-01 Challenge
|
|
||||||
|
|
||||||
With the `proof-of-possession-01` 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 authorization of a domain to your account.
|
|
||||||
|
|
||||||
The challenge object contains a list of `X509Certificate`s that are already known to the CA:
|
|
||||||
|
|
||||||
```java
|
|
||||||
ProofOfPossession01Challenge challenge =
|
|
||||||
auth.findChallenge(ProofOfPossession01Challenge.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
|
|
||||||
Registration ownerRegistration = ... // Registration of the domain owner
|
|
||||||
KeyPair domainKeyPair = ... // Key pair matching a certificate
|
|
||||||
String domain = ... // Domain to authorize
|
|
||||||
|
|
||||||
challenge.authorize(ownerRegistration, domainKeyPair, domain);
|
|
||||||
```
|
|
||||||
|
|
||||||
The challenge is completed when the domain is associated with the account of the `ownerRegistration`, 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
|
|
||||||
Registration ownerRegistration = ... // Registration of the domain owner
|
|
||||||
KeyPair domainKeyPair = ... // Key pair matching a certificates
|
|
||||||
|
|
||||||
ValidationBuilder vb = new ValidationBuilder();
|
|
||||||
vb.domain("example.org");
|
|
||||||
String json = vb.sign(ownerRegistration, domainKeyPair);
|
|
||||||
```
|
|
||||||
|
|
||||||
This `json` string can be transported (e.g. via email) and then imported into the challenge:
|
|
||||||
|
|
||||||
```java
|
|
||||||
String json = ... // validation document
|
|
||||||
|
|
||||||
ProofOfPossession01Challenge challenge =
|
|
||||||
auth.findChallenge(ProofOfPossession01Challenge.TYPE);
|
|
||||||
challenge.importValidation(json);
|
|
||||||
```
|
|
||||||
|
|
||||||
The challenge is authorized now, and is ready to be executed.
|
|
|
@ -8,7 +8,7 @@ Individual CAs may offer further ways of recovery, which are not part of this do
|
||||||
|
|
||||||
## Contact-Based Recovery
|
## Contact-Based Recovery
|
||||||
|
|
||||||
> **CAUTION**: Contact-Based Recovery is [currently not supported by _Let's Encrypt_](https://github.com/letsencrypt/boulder/issues/432). If you should lose your key pair, you are stuck. All you can do at the moment is to register a new account and then recover your domains by using the [Proof of Possession](../challenge/proof-of-possession.html) challenge in combination with the domain key pairs.
|
> **CAUTION**: Contact-Based Recovery is [currently not supported by _Let's Encrypt_](https://github.com/letsencrypt/boulder/issues/432). If you should lose your key pair, you are stuck.
|
||||||
|
|
||||||
On this recovery method, the CA contacts the account owner via one of the contact addresses given on account creation. The owner is asked to take some action (e.g. clicking on a link in an email). If it was successful, the account data is transferred to the new account.
|
On this recovery method, the CA contacts the account owner via one of the contact addresses given on account creation. The owner is asked to take some action (e.g. clicking on a link in an email). If it was successful, the account data is transferred to the new account.
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,6 @@
|
||||||
<item name="dns-01" href="challenge/dns-01.html"/>
|
<item name="dns-01" href="challenge/dns-01.html"/>
|
||||||
<item name="tls-sni-01" href="challenge/tls-sni-01.html"/>
|
<item name="tls-sni-01" href="challenge/tls-sni-01.html"/>
|
||||||
<item name="tls-sni-02" href="challenge/tls-sni-02.html"/>
|
<item name="tls-sni-02" href="challenge/tls-sni-02.html"/>
|
||||||
<item name="proof-of-possession-01" href="challenge/proof-of-possession-01.html"/>
|
|
||||||
</item>
|
</item>
|
||||||
<item name="CAs" href="ca/index.html">
|
<item name="CAs" href="ca/index.html">
|
||||||
<item name="Let's Encrypt" href="ca/letsencrypt.html"/>
|
<item name="Let's Encrypt" href="ca/letsencrypt.html"/>
|
||||||
|
|
Loading…
Reference in New Issue