Create tls-alpn-01 cert in challenge class

pull/140/head
Richard Körber 2023-05-19 10:20:07 +02:00
parent 16b02efe23
commit e22b47f140
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
3 changed files with 57 additions and 14 deletions

View File

@ -15,8 +15,14 @@ package org.shredzone.acme4j.challenge;
import static org.shredzone.acme4j.toolbox.AcmeUtils.sha256hash;
import java.io.IOException;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import org.shredzone.acme4j.Identifier;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.util.CertificateUtils;
/**
* Implements the {@value TYPE} challenge. It requires a specific certificate that can be
@ -63,6 +69,25 @@ public class TlsAlpn01Challenge extends TokenChallenge {
return sha256hash(getAuthorization());
}
/**
* Creates a self-signed {@link X509Certificate} for this challenge. The certificate
* is valid for 7 days.
*
* @param keypair
* A domain {@link KeyPair} to be used for the challenge
* @param id
* The {@link Identifier} that is to be validated
* @return Created certificate
* @since 3.0.0
*/
public X509Certificate createCertificate(KeyPair keypair, Identifier id) {
try {
return CertificateUtils.createTlsAlpn01Certificate(keypair, id, getAcmeValidation());
} catch (IOException ex) {
throw new IllegalArgumentException("Bad certificate parameters", ex);
}
}
@Override
protected boolean acceptable(String type) {
return TYPE.equals(type);

View File

@ -15,14 +15,19 @@ package org.shredzone.acme4j.challenge;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
import java.security.cert.CertificateParsingException;
import org.junit.jupiter.api.Test;
import org.shredzone.acme4j.Identifier;
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;
import org.shredzone.acme4j.util.KeyPairUtils;
/**
* Unit tests for {@link TlsAlpn01ChallengeTest}.
@ -54,4 +59,25 @@ public class TlsAlpn01ChallengeTest {
assertThatJson(response.toString()).isEqualTo("{}");
}
/**
* Test that {@link TlsAlpn01Challenge} generates a correct test certificate
*/
@Test
public void testTlsAlpn01Certificate() throws CertificateParsingException {
var challenge = new TlsAlpn01Challenge(login, getJSON("tlsAlpnChallenge"));
var keypair = KeyPairUtils.createKeyPair(2048);
var subject = Identifier.dns("example.com");
var certificate = challenge.createCertificate(keypair, subject);
// Only check the main requirements. Cert generation is fully tested in CertificateUtilsTest.
assertThat(certificate).isNotNull();
assertThat(certificate.getSubjectX500Principal().getName()).isEqualTo("CN=acme.invalid");
assertThat(certificate.getSubjectAlternativeNames().stream()
.map(l -> l.get(1))
.map(Object::toString)).contains(subject.getDomain());
assertThat(certificate.getCriticalExtensionOIDs()).contains(TlsAlpn01Challenge.ACME_VALIDATION_OID);
assertThatNoException().isThrownBy(() -> certificate.verify(keypair.getPublic()));
}
}

View File

@ -2,30 +2,22 @@
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. This challenge is specified in [RFC 8737](https://tools.ietf.org/html/rfc8737).
`TlsAlpn01Challenge` provides a byte array called `acmeValidation`:
You need to create a self-signed certificate with the domain to be validated set as the only _Subject Alternative Name_. The byte array provided by `challenge.getAcmeValidation()` must be set as DER encoded `OCTET STRING` extension with the object id `1.3.6.1.5.5.7.1.31`. It is required to set this extension as critical.
_acme4j_ does the heavy lifting for you though, and provides a certificate that is ready to use. It is valid for 7 days, which is ample of time to perform the validation.
```java
TlsAlpn01Challenge challenge = auth.findChallenge(TlsAlpn01Challenge.class);
Identifier identifier = auth.getIdentifier();
byte[] acmeValidation = challenge.getAcmeValidation();
```
You need to create a self-signed certificate with the domain to be validated set as the only _Subject Alternative Name_. The `acmeValidation` must be set as DER encoded `OCTET STRING` extension with the object id `1.3.6.1.5.5.7.1.31`. 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 `identifier` and the ALPN protocol `acme-tls/1`.
The `TlsAlpn01Challenge` class does not generate a self-signed certificate for you, as it would require _Bouncy Castle_. However, there is a utility method for this use case:
```java
KeyPair certKeyPair = KeyPairUtils.createKeyPair(2048);
X509Certificate cert = CertificateUtils.
createTlsAlpn01Certificate(certKeyPair, identifier, acmeValidation);
X509Certificate cert = challenge.createCertificate(certKeyPair, identifier);
```
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 your subject (`identifier`).
When the validation is completed, the `cert` and `certKeyPair` are not used anymore and can be disposed.
!!! note
The request is sent to port 443 only. If your domain has multiple IP addresses, the CA randomly selects some of them. There is no way to choose a different port or a fixed IP address.