mirror of https://github.com/shred/acme4j
Add support for tls-alpn-01 challenge
parent
231bad3560
commit
568438a1f3
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2018 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.shredzone.acme4j.toolbox.AcmeUtils.sha256hash;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
|
||||
import org.shredzone.acme4j.Login;
|
||||
import org.shredzone.acme4j.toolbox.JSON;
|
||||
|
||||
/**
|
||||
* Implements the {@value TYPE} challenge.
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
public class TlsAlpn01Challenge extends TokenChallenge {
|
||||
private static final long serialVersionUID = -5590351078176091228L;
|
||||
|
||||
/**
|
||||
* Challenge type name: {@value}
|
||||
*/
|
||||
public static final String TYPE = "tls-alpn-01";
|
||||
|
||||
/**
|
||||
* OID of the {@code acmeValidation-v1} extension.
|
||||
*/
|
||||
public static final String ACME_VALIDATION_V1_OID = "1.3.6.1.5.5.7.1.30.1";
|
||||
|
||||
/**
|
||||
* {@code acme-tls/1} protocol.
|
||||
*/
|
||||
public static final String ACME_TLS_1_PROTOCOL = "acme-tls/1";
|
||||
|
||||
/**
|
||||
* Creates a new generic {@link TlsAlpn01Challenge} object.
|
||||
*
|
||||
* @param login
|
||||
* {@link Login} the resource is bound with
|
||||
* @param data
|
||||
* {@link JSON} challenge data
|
||||
*/
|
||||
public TlsAlpn01Challenge(Login login, JSON data) {
|
||||
super(login, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value that is to be used as {@code acmeValidation-v1} extension in
|
||||
* the test certificate.
|
||||
*/
|
||||
public byte[] getAcmeValidationV1() {
|
||||
return sha256hash(getAuthorization());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean acceptable(String type) {
|
||||
return TYPE.equals(type);
|
||||
}
|
||||
|
||||
}
|
|
@ -27,6 +27,7 @@ import org.shredzone.acme4j.Session;
|
|||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TokenChallenge;
|
||||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.DefaultConnection;
|
||||
|
@ -71,6 +72,7 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
|
|||
|
||||
map.put(Dns01Challenge.TYPE, Dns01Challenge::new);
|
||||
map.put(Http01Challenge.TYPE, Http01Challenge::new);
|
||||
map.put(TlsAlpn01Challenge.TYPE, TlsAlpn01Challenge::new);
|
||||
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.junit.Test;
|
|||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
|
||||
|
@ -66,6 +67,11 @@ public class AuthorizationTest {
|
|||
Challenge c3 = authorization.findChallenge(Dns01Challenge.TYPE);
|
||||
assertThat(c3, is(notNullValue()));
|
||||
assertThat(c3, is(instanceOf(Dns01Challenge.class)));
|
||||
|
||||
// TlsAlpn01Challenge is available
|
||||
Challenge c4 = authorization.findChallenge(TlsAlpn01Challenge.TYPE);
|
||||
assertThat(c4, is(notNullValue()));
|
||||
assertThat(c4, is(instanceOf(TlsAlpn01Challenge.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -104,6 +110,7 @@ public class AuthorizationTest {
|
|||
|
||||
provider.putTestChallenge("http-01", Http01Challenge::new);
|
||||
provider.putTestChallenge("dns-01", Dns01Challenge::new);
|
||||
provider.putTestChallenge("tls-alpn-01", TlsAlpn01Challenge::new);
|
||||
|
||||
Authorization auth = new Authorization(login, locationUrl);
|
||||
auth.update();
|
||||
|
@ -116,7 +123,8 @@ public class AuthorizationTest {
|
|||
|
||||
assertThat(auth.getChallenges(), containsInAnyOrder(
|
||||
provider.getChallenge(Http01Challenge.TYPE),
|
||||
provider.getChallenge(Dns01Challenge.TYPE)));
|
||||
provider.getChallenge(Dns01Challenge.TYPE),
|
||||
provider.getChallenge(TlsAlpn01Challenge.TYPE)));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
@ -191,6 +199,7 @@ public class AuthorizationTest {
|
|||
|
||||
provider.putTestChallenge("http-01", Http01Challenge::new);
|
||||
provider.putTestChallenge("dns-01", Dns01Challenge::new);
|
||||
provider.putTestChallenge("tls-alpn-01", TlsAlpn01Challenge::new);
|
||||
|
||||
Authorization auth = new Authorization(login, locationUrl);
|
||||
|
||||
|
@ -238,6 +247,7 @@ public class AuthorizationTest {
|
|||
|
||||
provider.putTestChallenge("http-01", Http01Challenge::new);
|
||||
provider.putTestChallenge("dns-01", Dns01Challenge::new);
|
||||
provider.putTestChallenge("tls-alpn-01", TlsAlpn01Challenge::new);
|
||||
|
||||
Authorization auth = new Authorization(login, locationUrl);
|
||||
|
||||
|
@ -256,7 +266,8 @@ public class AuthorizationTest {
|
|||
|
||||
assertThat(auth.getChallenges(), containsInAnyOrder(
|
||||
provider.getChallenge(Http01Challenge.TYPE),
|
||||
provider.getChallenge(Dns01Challenge.TYPE)));
|
||||
provider.getChallenge(Dns01Challenge.TYPE),
|
||||
provider.getChallenge(TlsAlpn01Challenge.TYPE)));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
@ -286,6 +297,7 @@ public class AuthorizationTest {
|
|||
|
||||
provider.putTestChallenge("http-01", Http01Challenge::new);
|
||||
provider.putTestChallenge("dns-01", Dns01Challenge::new);
|
||||
provider.putTestChallenge("tls-alpn-01", TlsAlpn01Challenge::new);
|
||||
|
||||
Authorization auth = new Authorization(login, locationUrl);
|
||||
auth.deactivate();
|
||||
|
@ -302,6 +314,7 @@ public class AuthorizationTest {
|
|||
|
||||
provider.putTestChallenge(Http01Challenge.TYPE, Http01Challenge::new);
|
||||
provider.putTestChallenge(Dns01Challenge.TYPE, Dns01Challenge::new);
|
||||
provider.putTestChallenge(TlsAlpn01Challenge.TYPE, TlsAlpn01Challenge::new);
|
||||
provider.putTestChallenge(DUPLICATE_TYPE, Challenge::new);
|
||||
|
||||
Authorization authorization = new Authorization(login, locationUrl);
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2018 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.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
|
||||
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.Login;
|
||||
import org.shredzone.acme4j.Status;
|
||||
import org.shredzone.acme4j.toolbox.AcmeUtils;
|
||||
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||
import org.shredzone.acme4j.toolbox.TestUtils;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link TlsAlpn01ChallengeTest}.
|
||||
*/
|
||||
public class TlsAlpn01ChallengeTest {
|
||||
private static final String TOKEN =
|
||||
"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ";
|
||||
private static final String KEY_AUTHORIZATION =
|
||||
"rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0";
|
||||
|
||||
private Login login = TestUtils.login();
|
||||
|
||||
/**
|
||||
* Test that {@link TlsAlpn01Challenge} generates a correct authorization key.
|
||||
*/
|
||||
@Test
|
||||
public void testTlsAlpn01Challenge() throws IOException {
|
||||
TlsAlpn01Challenge challenge = new TlsAlpn01Challenge(login, getJSON("tlsAlpnChallenge"));
|
||||
|
||||
assertThat(challenge.getType(), is(TlsAlpn01Challenge.TYPE));
|
||||
assertThat(challenge.getStatus(), is(Status.PENDING));
|
||||
assertThat(challenge.getToken(), is(TOKEN));
|
||||
assertThat(challenge.getAuthorization(), is(KEY_AUTHORIZATION));
|
||||
assertThat(challenge.getAcmeValidationV1(), is(AcmeUtils.sha256hash(KEY_AUTHORIZATION)));
|
||||
|
||||
JSONBuilder response = new JSONBuilder();
|
||||
challenge.prepareResponse(response);
|
||||
|
||||
assertThat(response.toString(), sameJSONAs("{}").allowingExtraUnexpectedFields());
|
||||
}
|
||||
|
||||
}
|
|
@ -29,6 +29,7 @@ import org.shredzone.acme4j.Session;
|
|||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TokenChallenge;
|
||||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.connector.DefaultConnection;
|
||||
|
@ -146,6 +147,10 @@ public class AbstractAcmeProviderTest {
|
|||
assertThat(c3, not(nullValue()));
|
||||
assertThat(c3, instanceOf(Dns01Challenge.class));
|
||||
|
||||
Challenge c4 = provider.createChallenge(login, getJSON("tlsAlpnChallenge"));
|
||||
assertThat(c4, not(nullValue()));
|
||||
assertThat(c4, instanceOf(TlsAlpn01Challenge.class));
|
||||
|
||||
JSON json6 = new JSONBuilder()
|
||||
.put("type", "foobar-01")
|
||||
.put("url", "https://example.com/some/challenge")
|
||||
|
|
|
@ -10,6 +10,11 @@
|
|||
"url": "https://example.com/authz/asdf/1",
|
||||
"token": "DGyRejmCefe7v4NfDGDKfA"
|
||||
},
|
||||
{
|
||||
"type": "tls-alpn-01",
|
||||
"url": "https://example.com/authz/asdf/1",
|
||||
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
|
||||
},
|
||||
{
|
||||
"type": "duplicate-01",
|
||||
"url": "https://example.com/authz/asdf/3"
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"type": "tls-alpn-01",
|
||||
"url": "https://example.com/acme/authz/0",
|
||||
"status": "pending",
|
||||
"token": "rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ"
|
||||
}
|
|
@ -17,6 +17,12 @@
|
|||
"status": "pending",
|
||||
"url": "https://example.com/authz/asdf/1",
|
||||
"token": "DGyRejmCefe7v4NfDGDKfA"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tls-alpn-01",
|
||||
"status": "pending",
|
||||
"url": "https://example.com/authz/asdf/2",
|
||||
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -13,16 +13,35 @@
|
|||
*/
|
||||
package org.shredzone.acme4j.util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
import javax.annotation.WillClose;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||
|
||||
/**
|
||||
* Utility class offering convenience methods for certificates.
|
||||
|
@ -32,6 +51,14 @@ import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
|||
@ParametersAreNonnullByDefault
|
||||
public final class CertificateUtils {
|
||||
|
||||
/**
|
||||
* The {@code acmeValidation-v1} object identifier.
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
public static final ASN1ObjectIdentifier ACME_VALIDATION_V1 =
|
||||
new ASN1ObjectIdentifier(TlsAlpn01Challenge.ACME_VALIDATION_V1_OID).intern();
|
||||
|
||||
private CertificateUtils() {
|
||||
// utility class without constructor
|
||||
}
|
||||
|
@ -54,4 +81,56 @@ public final class CertificateUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a self-signed {@link X509Certificate} that can be used for the
|
||||
* {@link TlsAlpn01Challenge}. The certificate is valid for 7 days.
|
||||
*
|
||||
* @param keypair
|
||||
* A domain {@link KeyPair} to be used for the challenge
|
||||
* @param subject
|
||||
* The subject (domain name) that is to be validated
|
||||
* @param acmeValidationV1
|
||||
* The value that is returned by
|
||||
* {@link TlsAlpn01Challenge#getAcmeValidationV1()}
|
||||
* @return Created certificate
|
||||
* @since 2.1
|
||||
*/
|
||||
public static X509Certificate createTlsAlpn01Certificate(KeyPair keypair, String subject, byte[] acmeValidationV1)
|
||||
throws IOException {
|
||||
Objects.requireNonNull(keypair, "keypair");
|
||||
Objects.requireNonNull(subject, "subject");
|
||||
if (acmeValidationV1 == null || acmeValidationV1.length != 32) {
|
||||
throw new IllegalArgumentException("Bad acmeValidationV1 parameter");
|
||||
}
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
final String signatureAlg = "SHA256withRSA";
|
||||
|
||||
try {
|
||||
X500Name issuer = new X500Name("CN=acme.invalid");
|
||||
BigInteger serial = BigInteger.valueOf(now);
|
||||
Instant notBefore = Instant.ofEpochMilli(now);
|
||||
Instant notAfter = notBefore.plus(Duration.ofDays(7));
|
||||
|
||||
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||
issuer, serial, Date.from(notBefore), Date.from(notAfter),
|
||||
issuer, keypair.getPublic());
|
||||
|
||||
GeneralName[] gns = new GeneralName[1];
|
||||
gns[0] = new GeneralName(GeneralName.dNSName, subject);
|
||||
certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns));
|
||||
|
||||
certBuilder.addExtension(ACME_VALIDATION_V1, true, acmeValidationV1);
|
||||
|
||||
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlg);
|
||||
|
||||
byte[] cert = certBuilder.build(signerBuilder.build(keypair.getPrivate())).getEncoded();
|
||||
|
||||
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(cert));
|
||||
} catch (CertificateException | OperatorCreationException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,9 +23,22 @@ import java.io.InputStream;
|
|||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.DEROctetString;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||
import org.shredzone.acme4j.toolbox.AcmeUtils;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link CertificateUtils}.
|
||||
|
@ -67,4 +80,58 @@ public class CertificateUtilsTest {
|
|||
constructor.newInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if
|
||||
* {@link CertificateUtils#createTlsAlpn01Certificate(KeyPair, String, byte[])}
|
||||
* creates a good certificate.
|
||||
*/
|
||||
@Test
|
||||
public void testCreateTlsSni02Certificate() throws IOException, CertificateParsingException {
|
||||
KeyPair keypair = KeyPairUtils.createKeyPair(2048);
|
||||
String subject = "example.com";
|
||||
byte[] acmeValidationV1 = AcmeUtils.sha256hash("rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ");
|
||||
|
||||
X509Certificate cert = CertificateUtils.createTlsAlpn01Certificate(keypair, subject, acmeValidationV1);
|
||||
|
||||
Instant now = Instant.now();
|
||||
Instant end = now.plus(Duration.ofDays(8));
|
||||
|
||||
assertThat(cert, not(nullValue()));
|
||||
assertThat(cert.getNotAfter(), is(greaterThan(Date.from(now))));
|
||||
assertThat(cert.getNotAfter(), is(lessThan(Date.from(end))));
|
||||
assertThat(cert.getNotBefore(), is(lessThanOrEqualTo(Date.from(now))));
|
||||
|
||||
assertThat(cert.getSubjectX500Principal().getName(), is("CN=acme.invalid"));
|
||||
assertThat(getSANs(cert), contains(subject));
|
||||
|
||||
assertThat(cert.getCriticalExtensionOIDs(), hasItem(TlsAlpn01Challenge.ACME_VALIDATION_V1_OID));
|
||||
|
||||
byte[] encodedExtensionValue = cert.getExtensionValue(TlsAlpn01Challenge.ACME_VALIDATION_V1_OID);
|
||||
assertThat(encodedExtensionValue, is(notNullValue()));
|
||||
|
||||
try (ASN1InputStream asn = new ASN1InputStream(new ByteArrayInputStream(encodedExtensionValue))) {
|
||||
DEROctetString derOctetString = (DEROctetString) asn.readObject();
|
||||
assertThat(derOctetString.getOctets(), is(acmeValidationV1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all DNSName SANs from a certificate.
|
||||
*
|
||||
* @param cert
|
||||
* {@link X509Certificate}
|
||||
* @return Set of DNSName
|
||||
*/
|
||||
private Set<String> getSANs(X509Certificate cert) throws CertificateParsingException {
|
||||
Set<String> result = new HashSet<>();
|
||||
|
||||
for (List<?> list : cert.getSubjectAlternativeNames()) {
|
||||
if (((Number) list.get(0)).intValue() == GeneralName.dNSName) {
|
||||
result.add((String) list.get(1));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,3 +8,7 @@ The ACME specifications define these standard challenges:
|
|||
|
||||
* [http-01](./http-01.html)
|
||||
* [dns-01](./dns-01.html)
|
||||
|
||||
_acme4j_ also supports these non-standard challenges:
|
||||
|
||||
* [tls-alpn-01](./tls-alpn-01.html)
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# tls-alpn-01 Challenge
|
||||
|
||||
With the `tls-alpn-01` challenge, you prove to the CA that you are able to control the web server of the domain to be authorized, by letting it respond to a request with a specific self-signed cert utilizing the ALPN extension.
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
|
||||
This challenge is not part of the ACME specifications. It is specified [in a separate IETF document](https://tools.ietf.org/html/draft-shoemaker-acme-tls-alpn-00) and is still work in progress.
|
||||
</div>
|
||||
|
||||
`TlsAlpn01Challenge` provides a byte array called `acmeValidationV1`:
|
||||
|
||||
```java
|
||||
TlsSni02Challenge challenge = auth.findChallenge(TlsSni02Challenge.TYPE);
|
||||
|
||||
byte[] acmeValidationV1 = challenge.getAcmeValidationV1();
|
||||
```
|
||||
|
||||
You need to create a self-signed certificate with the domain to be validated set as the only _Subject Alternative Name_. The `acmeValidationV1` must be set as octet string extension with the object id `1.3.6.1.5.5.7.1.30.1`. It is required to set this extension as critical.
|
||||
|
||||
After that, configure your web server so it will use this certificate on an incoming TLS request having the SNI `subject` and the ALPN protocol `acme-tls/1`.
|
||||
|
||||
The `TlsAlpn01Challenge` class does not generate a self-signed certificate, as it would require _Bouncy Castle_. However, there is a utility method in the _acme4j-utils_ module for this use case:
|
||||
|
||||
```java
|
||||
String subject = auth.getDomain();
|
||||
KeyPair certKeyPair = KeyPairUtils.createKeyPair(2048);
|
||||
|
||||
X509Certificate cert = CertificateUtils.
|
||||
createTlsAlpn01Certificate(certKeyPair, subject, acmeValidationV1);
|
||||
```
|
||||
|
||||
Now use `cert` and `certKeyPair` to let your web server respond to TLS requests containing an ALPN extension with the value `acme-tls/1` and a SNI extension containing `subject`.
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
The request is sent to port 443 only. If your domain has multiple IP addresses, the CA randomly selects one of them. There is no way to choose a different port or a fixed IP address.
|
||||
</div>
|
|
@ -45,6 +45,7 @@
|
|||
<item name="Challenges" href="challenge/index.html">
|
||||
<item name="http-01" href="challenge/http-01.html"/>
|
||||
<item name="dns-01" href="challenge/dns-01.html"/>
|
||||
<item name="tls-alpn-01" href="challenge/tls-alpn-01.html"/>
|
||||
</item>
|
||||
<item name="CAs" href="ca/index.html">
|
||||
<item name="Let's Encrypt" href="ca/letsencrypt.html"/>
|
||||
|
|
Loading…
Reference in New Issue