mirror of https://github.com/shred/acme4j
Use Pebble for integration tests
- Build and run Pebble in a docker container - Move integration tests into a separate module - Add simple servers for http, dns, and tls-sni challenges - Add integration tests for ordering a certificate - Documentationpull/55/head
parent
39a45a53a5
commit
809978d188
|
@ -1,15 +1,22 @@
|
|||
|
||||
image: maven:3-jdk-8
|
||||
|
||||
build:
|
||||
tags:
|
||||
- maven
|
||||
- docker
|
||||
script:
|
||||
- mvn -B clean compile
|
||||
- mvn clean compile
|
||||
|
||||
test:
|
||||
tags:
|
||||
- maven
|
||||
- docker
|
||||
script:
|
||||
- mvn -B org.jacoco:jacoco-maven-plugin:prepare-agent verify -Dmaven.test.failure.ignore=true -DskipITs=false -DpebbleHost=pebble
|
||||
- mvn -B -P ci org.jacoco:jacoco-maven-plugin:prepare-agent verify -Dmaven.test.failure.ignore=true
|
||||
- mvn -B sonar:sonar -Dsonar.gitlab.commit_sha=$CI_BUILD_REF -Dsonar.gitlab.ref_name=$CI_BUILD_REF_NAME
|
||||
|
||||
deploy:
|
||||
tags:
|
||||
- maven
|
||||
- docker
|
||||
script:
|
||||
- mvn -B install site:site
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2017 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.it;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.Authorization;
|
||||
import org.shredzone.acme4j.Order;
|
||||
import org.shredzone.acme4j.Registration;
|
||||
import org.shredzone.acme4j.RegistrationBuilder;
|
||||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.Status;
|
||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.util.TestUtils;
|
||||
|
||||
/**
|
||||
* Tests the complete process of ordering a certificate.
|
||||
*/
|
||||
public class OrderIT extends AbstractPebbleIT {
|
||||
|
||||
@Test
|
||||
public void testOrder() throws AcmeException, IOException {
|
||||
KeyPair keyPair = createKeyPair();
|
||||
Session session = new Session(pebbleURI(), keyPair);
|
||||
|
||||
Registration reg = new RegistrationBuilder().agreeToTermsOfService().create(session);
|
||||
|
||||
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
|
||||
Instant notBefore = Instant.now();
|
||||
Instant notAfter = notBefore.plus(Duration.ofDays(20L));
|
||||
|
||||
Order order = reg.orderCertificate(csr, notBefore, notAfter);
|
||||
assertThat(order.getCsr(), is(csr));
|
||||
assertThat(order.getNotBefore(), is(notBefore));
|
||||
assertThat(order.getNotAfter(), is(notAfter));
|
||||
assertThat(order.getStatus(), is(Status.PENDING));
|
||||
|
||||
for (Authorization auth : order.getAuthorizations()) {
|
||||
processAuthorization(auth);
|
||||
}
|
||||
}
|
||||
|
||||
private void processAuthorization(Authorization auth) throws AcmeException {
|
||||
assertThat(auth.getDomain(), is("example.com"));
|
||||
if (auth.getStatus() == Status.PENDING) {
|
||||
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
|
||||
assertThat(challenge, is(notNullValue()));
|
||||
challenge.trigger();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>acme4j-it</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
|
@ -0,0 +1,164 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
*
|
||||
* acme4j - ACME Java client
|
||||
*
|
||||
* Copyright (C) 2017 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.
|
||||
*
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.shredzone.acme4j</groupId>
|
||||
<artifactId>acme4j</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>acme4j-it</artifactId>
|
||||
|
||||
<name>acme4j IT</name>
|
||||
<description>acme4j Integration Tests</description>
|
||||
|
||||
<properties>
|
||||
<skipITs>true</skipITs>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<!-- Profile with integration tests. Requires docker! -->
|
||||
<!-- mvn -P ci clean install -->
|
||||
<id>ci</id>
|
||||
<properties>
|
||||
<skipITs>false</skipITs>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>docker-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>start</id>
|
||||
<phase>pre-integration-test</phase>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>start</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>stop</id>
|
||||
<phase>post-integration-test</phase>
|
||||
<goals>
|
||||
<goal>stop</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>docker-maven-plugin</artifactId>
|
||||
<version>0.20.1</version>
|
||||
|
||||
<configuration>
|
||||
<logStdout>true</logStdout>
|
||||
<verbose>true</verbose>
|
||||
|
||||
<images>
|
||||
<image>
|
||||
<alias>pebble</alias>
|
||||
<name>acme4j/pebble:${project.version}</name>
|
||||
|
||||
<build>
|
||||
<from>golang:1.7</from>
|
||||
<optimise>true</optimise>
|
||||
<runCmds>
|
||||
<run>go get -u -v -d gopkg.in/square/go-jose.v2</run>
|
||||
<run>go get -u -v -d github.com/jmhodges/clock</run>
|
||||
<run>go get -u -v -d github.com/letsencrypt/pebble || true</run>
|
||||
<run>go test github.com/letsencrypt/pebble/...</run>
|
||||
<run>go install github.com/letsencrypt/pebble/...</run>
|
||||
</runCmds>
|
||||
<ports>
|
||||
<port>14000</port>
|
||||
</ports>
|
||||
<cmd>
|
||||
<shell>pebble -config /etc/pebble/pebble-config.json</shell>
|
||||
</cmd>
|
||||
<assembly>
|
||||
<mode>dir</mode>
|
||||
<targetDir>/etc/pebble</targetDir>
|
||||
<inline>
|
||||
<fileSet>
|
||||
<directory>src/test/pebble</directory>
|
||||
<outputDirectory>.</outputDirectory>
|
||||
</fileSet>
|
||||
</inline>
|
||||
</assembly>
|
||||
</build>
|
||||
|
||||
<run>
|
||||
<network>
|
||||
<mode>host</mode>
|
||||
</network>
|
||||
<extraHosts>
|
||||
<host>example.com:127.0.0.1</host>
|
||||
</extraHosts>
|
||||
</run>
|
||||
</image>
|
||||
</images>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.shredzone.acme4j</groupId>
|
||||
<artifactId>acme4j-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.shredzone.acme4j</groupId>
|
||||
<artifactId>acme4j-utils</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.nanohttpd</groupId>
|
||||
<artifactId>nanohttpd</artifactId>
|
||||
<version>${nanohttpd.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dnsjava</groupId>
|
||||
<artifactId>dnsjava</artifactId>
|
||||
<version>${dnsjava.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2017 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.it.server;
|
||||
|
||||
import static java.util.Collections.synchronizedMap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.xbill.DNS.DClass;
|
||||
import org.xbill.DNS.Flags;
|
||||
import org.xbill.DNS.Header;
|
||||
import org.xbill.DNS.Message;
|
||||
import org.xbill.DNS.Name;
|
||||
import org.xbill.DNS.Rcode;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.Section;
|
||||
import org.xbill.DNS.TXTRecord;
|
||||
import org.xbill.DNS.Type;
|
||||
|
||||
/**
|
||||
* A very simple and very stupid DNS server. It just responds to TXT queries of the
|
||||
* given domains, and refuses to answer anything else.
|
||||
* <p>
|
||||
* This server can be used to validate {@code dns-01} challenges.
|
||||
*/
|
||||
public class DnsServer {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DnsServer.class);
|
||||
private static final int UDP_SIZE = 512;
|
||||
private static final long TTL = 86321L;
|
||||
|
||||
private final Map<String, String> txtRecords = synchronizedMap(new HashMap<>());
|
||||
private Thread thread = null;
|
||||
private volatile boolean running = false;
|
||||
private volatile boolean listening = false;
|
||||
|
||||
/**
|
||||
* Adds a TXT record to the DNS server. If the domain already has a TXT record
|
||||
* attached, it will be replaced.
|
||||
*
|
||||
* @param domain
|
||||
* Domain to attach the TXT record to
|
||||
* @param txt
|
||||
* TXT record to attach
|
||||
*/
|
||||
public void addTxtRecord(String domain, String txt) {
|
||||
txtRecords.put(domain.replaceAll("\\.$", ""), txt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a TXT record from the domain.
|
||||
*
|
||||
* @param domain
|
||||
* Domain to remove the TXT record from
|
||||
*/
|
||||
public void removeTxtRecord(String domain) {
|
||||
txtRecords.remove(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the DNS server.
|
||||
*
|
||||
* @param port
|
||||
* Port to listen to
|
||||
*/
|
||||
public void start(int port) {
|
||||
if (thread != null) {
|
||||
throw new IllegalStateException("Server is already running");
|
||||
}
|
||||
|
||||
running = true;
|
||||
thread = new Thread(() -> serveUDP(port));
|
||||
thread.setName("DNS server");
|
||||
thread.start();
|
||||
LOG.info("dns-01 server listening at port {}", port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the DNS server.
|
||||
*/
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
running = false;
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server was started up and is listening to connections.
|
||||
*/
|
||||
public boolean isListening() {
|
||||
return listening;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an UDP socket and processes incoming messages.
|
||||
*
|
||||
* @param port
|
||||
* Port to listen at
|
||||
*/
|
||||
private void serveUDP(int port) {
|
||||
try (DatagramSocket sock = new DatagramSocket(port)) {
|
||||
listening = true;
|
||||
while (running) {
|
||||
process(sock);
|
||||
}
|
||||
listening = false;
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Failed to open UDP socket", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a DNS query.
|
||||
*
|
||||
* @param sock
|
||||
* Socket to listen to
|
||||
*/
|
||||
private void process(DatagramSocket sock) {
|
||||
try {
|
||||
byte[] in = new byte[UDP_SIZE];
|
||||
|
||||
// Read the question
|
||||
DatagramPacket indp = new DatagramPacket(in, UDP_SIZE);
|
||||
indp.setLength(UDP_SIZE);
|
||||
sock.receive(indp);
|
||||
Message msg = new Message(in);
|
||||
Header header = msg.getHeader();
|
||||
|
||||
Record question = msg.getQuestion();
|
||||
|
||||
// Prepare a response
|
||||
Message response = new Message(header.getID());
|
||||
response.getHeader().setFlag(Flags.QR);
|
||||
response.addRecord(question, Section.QUESTION);
|
||||
|
||||
Name name = question.getName();
|
||||
String txt = txtRecords.get(name.toString(true));
|
||||
if (question.getType() == Type.TXT && txt != null) {
|
||||
response.addRecord(new TXTRecord(name, DClass.IN, TTL, txt), Section.ANSWER);
|
||||
LOG.info("dns-01: {} {} IN TXT \"{}\"", name, TTL, txt);
|
||||
} else {
|
||||
response.getHeader().setRcode(Rcode.NXDOMAIN);
|
||||
LOG.warn("dns-01: Cannot answer: {}", question);
|
||||
}
|
||||
|
||||
// Send the response
|
||||
byte[] resp = response.toWire();
|
||||
DatagramPacket outdp = new DatagramPacket(resp, resp.length, indp.getAddress(), indp.getPort());
|
||||
sock.send(outdp);
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Failed to process query", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2017 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.it.server;
|
||||
|
||||
import static java.util.Collections.synchronizedMap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
import fi.iki.elonen.NanoHTTPD.Response.Status;
|
||||
|
||||
/**
|
||||
* A very simple web server that will answer at the {@code .well-known/acme-challenge}
|
||||
* path, returning the challenge to the given token.
|
||||
* <p>
|
||||
* This server can be used to validate {@code http-01} challenges.
|
||||
*/
|
||||
public class HttpServer {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HttpServer.class);
|
||||
|
||||
private static final String TOKEN_PATH = "/.well-known/acme-challenge/";
|
||||
private static final Pattern TOKEN_PATTERN = Pattern.compile("^" + Pattern.quote(TOKEN_PATH) + "([^/]+)$");
|
||||
|
||||
private final Map<String, String> tokenMap = synchronizedMap(new HashMap<>());
|
||||
private NanoHTTPD server;
|
||||
|
||||
/**
|
||||
* Adds a token to the server's well-known challenges. If the token was already set,
|
||||
* the challenge will be replaced.
|
||||
*
|
||||
* @param token
|
||||
* Token the server will respond to
|
||||
* @param challenge
|
||||
* Challenge the server will respond with
|
||||
*/
|
||||
public void addToken(String token, String challenge) {
|
||||
tokenMap.put(token, challenge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a token from the server's well-known challenges.
|
||||
*
|
||||
* @param token
|
||||
* Token to remove
|
||||
*/
|
||||
public void removeToken(String token) {
|
||||
tokenMap.remove(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the HTTP server.
|
||||
*
|
||||
* @param port
|
||||
* Port to listen at
|
||||
*/
|
||||
public void start(int port) {
|
||||
if (server != null) {
|
||||
throw new IllegalStateException("Server is already running");
|
||||
}
|
||||
|
||||
server = new NanoHTTPD(port) {
|
||||
@Override
|
||||
public Response serve(IHTTPSession session) {
|
||||
String path = session.getUri().replaceAll("//+", "/");
|
||||
|
||||
Matcher m = TOKEN_PATTERN.matcher(path);
|
||||
if (!m.matches()) {
|
||||
return newFixedLengthResponse(Status.NOT_FOUND, "text/plain", "not found: "+ path + "\n");
|
||||
}
|
||||
|
||||
String token = m.group(1);
|
||||
String content = tokenMap.get(token);
|
||||
|
||||
if (content == null) {
|
||||
LOG.warn("http-01: unknown token " + token);
|
||||
return newFixedLengthResponse(Status.NOT_FOUND, "text/plain", "unknown token: "+ token + "\n");
|
||||
}
|
||||
|
||||
LOG.info("http-01: " + token + " -> " + content);
|
||||
return newFixedLengthResponse(Status.OK, "text/plain", content);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
|
||||
LOG.info("http-01 server listening at port {}", port);
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Failed to start http-01 server", ex);
|
||||
server = null;
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the HTTP server.
|
||||
*/
|
||||
public void stop() {
|
||||
if (server != null) {
|
||||
server.stop();
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server was started up and is listening to connections.
|
||||
*/
|
||||
public boolean isListening() {
|
||||
return server != null && server.wasStarted();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2017 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.it.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLServerSocket;
|
||||
import javax.net.ssl.SSLServerSocketFactory;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A very simple TnsSni server. It waits for a connection and performs a TLS handshake
|
||||
* returning the matching certificate to the requested domain.
|
||||
* <p>
|
||||
* This server can be used to validate {@code tls-sni-02} challenges.
|
||||
*/
|
||||
public class TlsSniServer {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TlsSniServer.class);
|
||||
private static final char[] PASSWORD = "shibboleet".toCharArray();
|
||||
|
||||
private KeyStore keyStore = null;
|
||||
private Thread thread = null;
|
||||
private volatile boolean running = false;
|
||||
private volatile boolean listening = false;
|
||||
|
||||
/**
|
||||
* Adds a certificate to the set of known certificates.
|
||||
* <p>
|
||||
* The certificate's CN and SANs are used for SNI.
|
||||
*
|
||||
* @param alias
|
||||
* Internal alias
|
||||
* @param privateKey
|
||||
* Private key to be used with this certificate
|
||||
* @param cert
|
||||
* {@link X509Certificate} to be added
|
||||
*/
|
||||
public void addCertificate(String alias, PrivateKey privateKey, X509Certificate cert) {
|
||||
initKeyStore();
|
||||
|
||||
try {
|
||||
keyStore.setKeyEntry(alias, privateKey, PASSWORD, new Certificate[] {cert});
|
||||
} catch (KeyStoreException ex) {
|
||||
throw new IllegalArgumentException("Failed to add certificate " + alias, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a certificate.
|
||||
*
|
||||
* @param alias
|
||||
* Internal alias of the certificate to remove
|
||||
*/
|
||||
public void removeCertificate(String alias) {
|
||||
initKeyStore();
|
||||
|
||||
try {
|
||||
keyStore.deleteEntry(alias);
|
||||
} catch (KeyStoreException ex) {
|
||||
throw new IllegalArgumentException("Failed to remove certificate " + alias, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the TlsSni server.
|
||||
*
|
||||
* @param port
|
||||
* Port to listen to
|
||||
*/
|
||||
public void start(int port) {
|
||||
if (thread != null) {
|
||||
throw new IllegalStateException("Server is already running");
|
||||
}
|
||||
|
||||
running = true;
|
||||
thread = new Thread(() -> serve(port));
|
||||
thread.setName("tls-sni server");
|
||||
thread.start();
|
||||
LOG.info("tls-sni server listening at port {}", port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the TlsSni server.
|
||||
*/
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
running = false;
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server was started up and is listening to connections.
|
||||
*/
|
||||
public boolean isListening() {
|
||||
return listening;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an SSL server socket and processes incoming requests.
|
||||
*
|
||||
* @param port
|
||||
* Port to listen at
|
||||
*/
|
||||
private void serve(int port) {
|
||||
SSLContext sslContext = createSSLContext();
|
||||
SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
|
||||
|
||||
while (running) {
|
||||
try (SSLServerSocket sslServerSocket = (SSLServerSocket)
|
||||
sslServerSocketFactory.createServerSocket(port)){
|
||||
listening = true;
|
||||
process(sslServerSocket);
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Failed to process query", ex);
|
||||
}
|
||||
}
|
||||
|
||||
listening = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept and process an incoming request. Only the TLS handshake is used here.
|
||||
* Incoming data is just consumed, and the socket is closed after that.
|
||||
*
|
||||
* @param sslServerSocket
|
||||
* {@link SSLServerSocket} to accept connections from
|
||||
* @throws IOException
|
||||
* if the request could not be processed
|
||||
*/
|
||||
private void process(SSLServerSocket sslServerSocket) throws IOException {
|
||||
try (SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept()) {
|
||||
sslSocket.setEnabledCipherSuites(sslSocket.getSupportedCipherSuites());
|
||||
sslSocket.startHandshake();
|
||||
|
||||
SSLSession sslSession = sslSocket.getSession();
|
||||
X509Certificate cert = (X509Certificate) sslSession.getLocalCertificates()[0];
|
||||
LOG.info("tls-sni: {}", domainsToString(cert));
|
||||
|
||||
try (InputStream in = sslSocket.getInputStream()) {
|
||||
while (in.read() >= 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily initializes the {@link KeyStore} instance to be used. The key store is empty
|
||||
* after initialization.
|
||||
*/
|
||||
private void initKeyStore() {
|
||||
if (keyStore == null) {
|
||||
try {
|
||||
keyStore = KeyStore.getInstance("JKS");
|
||||
keyStore.load(null, null);
|
||||
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException
|
||||
| IOException ex) {
|
||||
throw new IllegalStateException("Failed to create key store", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link SSLContext} that uses the internal {@link KeyStore} for key and
|
||||
* trust management.
|
||||
*
|
||||
* @return {@link SSLContext} instance
|
||||
*/
|
||||
private SSLContext createSSLContext() {
|
||||
initKeyStore();
|
||||
|
||||
try {
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("NewSunX509");
|
||||
keyManagerFactory.init(keyStore, PASSWORD);
|
||||
KeyManager[] km = keyManagerFactory.getKeyManagers();
|
||||
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
|
||||
trustManagerFactory.init(keyStore);
|
||||
TrustManager[] tm = trustManagerFactory.getTrustManagers();
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLSv1");
|
||||
sslContext.init(km, tm, null);
|
||||
|
||||
return sslContext;
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException
|
||||
| UnrecoverableKeyException ex) {
|
||||
throw new IllegalStateException("Could not create SSLContext", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all SANs of the given certificate and returns them as a string.
|
||||
*
|
||||
* @param cert
|
||||
* {@link X509Certificate} to read the SANs from
|
||||
* @return String of all SAN names joined together and separated by comma
|
||||
*/
|
||||
private String domainsToString(X509Certificate cert) {
|
||||
try {
|
||||
return cert.getSubjectAlternativeNames().stream()
|
||||
.filter(c -> ((Number) c.get(0)).intValue() == GeneralName.dNSName)
|
||||
.map(c -> (String) c.get(1))
|
||||
.collect(Collectors.joining(", "));
|
||||
} catch (CertificateParsingException ex) {
|
||||
throw new IllegalArgumentException("bad certificate", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
acme4j Integration Tests
|
||||
========================
|
||||
|
||||
This module contains an Integration Test of _acme4j_.
|
||||
|
||||
It builds a [Pebble](https://github.com/letsencrypt/pebble) docker image and runs it. After that, a number of integration tests are performed.
|
||||
|
||||
How to Use
|
||||
----------
|
||||
|
||||
Integration tests are disabled by default, to ensure that _acme4j_ can be build on systems with not much more than a _maven_ installation. For running the integration tests, _Docker_ must be available.
|
||||
|
||||
To enable the integration tests, use the `ci` profile when building the _acme4j_ project:
|
||||
|
||||
```
|
||||
mvn -P ci clean install
|
||||
```
|
||||
|
||||
It will build and run a Pebble server, perform the integration tests, and stop the Pebble server after that.
|
||||
|
||||
The Pebble server needs to connect to servers that are provided by the maven integration tests. For this reason, the Pebble server must run on the same machine where maven is running, so the servers are available via `localhost`.
|
||||
|
||||
Starting Pebble manually
|
||||
------------------------
|
||||
|
||||
You can also run Pebble on your machine, to run the integration tests inside your IDE.
|
||||
|
||||
To do so, change to the `acme4j-it` module, then run `docker:build` to download and build the Pebble image, and `docker:start` to start the Pebble server:
|
||||
|
||||
```
|
||||
cd acme4j-it
|
||||
mvn docker:build
|
||||
mvn docker:start
|
||||
```
|
||||
|
||||
To stop the server:
|
||||
|
||||
```
|
||||
mvn docker:stop
|
||||
```
|
||||
|
||||
GitLab CI
|
||||
---------
|
||||
|
||||
_acme4j_ contains a GitLab CI configuration file.
|
||||
|
||||
The CI runner should be set up with a `shell` executor. Maven and Docker should be installed on the CI runner, and the shell executor user should be able to use both.
|
||||
|
||||
The tags `maven` and `docker` are used to select the executor.
|
||||
|
||||
`acme4j-it` API
|
||||
---------------
|
||||
|
||||
The `acme4j-it` module provides test servers for the `http-01`, `dns-01` and `tls-sni-02` challenges. You can use these classes for your own projects. However, they are not part of the official _acme4j_ API and subject to change without notice.
|
||||
|
||||
Note that these servers are very simple implementations without any security measures. They are tailor-made for integration tests. Do not use them in production code!
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
*
|
||||
* acme4j - ACME Java client
|
||||
*
|
||||
* Copyright (C) 2017 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.
|
||||
*
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/DECORATION/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd">
|
||||
<publishDate position="right"/>
|
||||
<version position="right"/>
|
||||
<body>
|
||||
<links>
|
||||
<item name="GitHub" href="https://github.com/shred/acme4j"/>
|
||||
</links>
|
||||
<breadcrumbs>
|
||||
<item name="shredzone.org" href="https://shredzone.org"/>
|
||||
<item name="acme4j" href="../index.html"/>
|
||||
<item name="acme4j-it" href="index.html"/>
|
||||
</breadcrumbs>
|
||||
<menu name="Main">
|
||||
<item name="Description" href="index.html"/>
|
||||
</menu>
|
||||
<menu ref="modules"/>
|
||||
<menu ref="reports"/>
|
||||
</body>
|
||||
|
||||
<skin>
|
||||
<groupId>org.apache.maven.skins</groupId>
|
||||
<artifactId>maven-fluido-skin</artifactId>
|
||||
<version>1.6</version>
|
||||
</skin>
|
||||
</project>
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2017 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.it;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.Authorization;
|
||||
import org.shredzone.acme4j.Certificate;
|
||||
import org.shredzone.acme4j.Order;
|
||||
import org.shredzone.acme4j.Registration;
|
||||
import org.shredzone.acme4j.RegistrationBuilder;
|
||||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.Status;
|
||||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
|
||||
import org.shredzone.acme4j.it.server.DnsServer;
|
||||
import org.shredzone.acme4j.it.server.HttpServer;
|
||||
import org.shredzone.acme4j.it.server.TlsSniServer;
|
||||
import org.shredzone.acme4j.util.CSRBuilder;
|
||||
import org.shredzone.acme4j.util.CertificateUtils;
|
||||
|
||||
/**
|
||||
* Tests a complete certificate order with different challenges.
|
||||
*/
|
||||
public class OrderIT extends PebbleITBase {
|
||||
|
||||
private static final int TLS_SNI_PORT = 5001;
|
||||
private static final int HTTP_PORT = 5002;
|
||||
private static final int DNS_PORT = 5003;
|
||||
|
||||
private static final String TEST_DOMAIN = "example.com";
|
||||
|
||||
private static TlsSniServer tlsSniServer;
|
||||
private static HttpServer httpServer;
|
||||
private static DnsServer dnsServer;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
tlsSniServer = new TlsSniServer();
|
||||
tlsSniServer.start(TLS_SNI_PORT);
|
||||
|
||||
httpServer = new HttpServer();
|
||||
httpServer.start(HTTP_PORT);
|
||||
|
||||
dnsServer = new DnsServer();
|
||||
dnsServer.start(DNS_PORT);
|
||||
|
||||
await().until(() -> tlsSniServer.isListening()
|
||||
&& httpServer.isListening()
|
||||
&& dnsServer.isListening());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void shutdown() {
|
||||
tlsSniServer.stop();
|
||||
httpServer.stop();
|
||||
dnsServer.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a certificate can be ordered via tns-sni-02 challenge.
|
||||
*/
|
||||
@Test
|
||||
public void testTlsSniValidation() throws Exception {
|
||||
orderCertificate(TEST_DOMAIN, auth -> {
|
||||
TlsSni02Challenge challenge = auth.findChallenge(TlsSni02Challenge.TYPE);
|
||||
assertThat(challenge, is(notNullValue()));
|
||||
|
||||
KeyPair challengeKey = createKeyPair();
|
||||
|
||||
X509Certificate cert = CertificateUtils.createTlsSni02Certificate(
|
||||
challengeKey, challenge.getSubject(), challenge.getSanB());
|
||||
|
||||
tlsSniServer.addCertificate(challenge.getSubject(), challengeKey.getPrivate(), cert);
|
||||
return challenge;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a certificate can be ordered via http-01 challenge.
|
||||
*/
|
||||
@Test
|
||||
public void testHttpValidation() throws Exception {
|
||||
orderCertificate(TEST_DOMAIN, auth -> {
|
||||
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
|
||||
assertThat(challenge, is(notNullValue()));
|
||||
|
||||
httpServer.addToken(challenge.getToken(), challenge.getAuthorization());
|
||||
return challenge;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a certificate can be ordered via dns-01 challenge.
|
||||
*/
|
||||
@Test
|
||||
@Ignore // TODO PEBBLE: cannot query our dnsServer yet...
|
||||
public void testDnsValidation() throws Exception {
|
||||
orderCertificate(TEST_DOMAIN, auth -> {
|
||||
Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE);
|
||||
assertThat(challenge, is(notNullValue()));
|
||||
|
||||
dnsServer.addTxtRecord(TEST_DOMAIN, challenge.getDigest());
|
||||
return challenge;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the complete process of ordering a certificate.
|
||||
*
|
||||
* @param domain
|
||||
* Name of the domain to order a certificate for
|
||||
* @param validator
|
||||
* {@link Validator} that finds and prepares a {@link Challenge} for domain
|
||||
* validation
|
||||
*/
|
||||
private void orderCertificate(String domain, Validator validator) throws Exception {
|
||||
KeyPair keyPair = createKeyPair();
|
||||
Session session = new Session(pebbleURI(), keyPair);
|
||||
|
||||
Registration registration = new RegistrationBuilder()
|
||||
.agreeToTermsOfService()
|
||||
.create(session);
|
||||
|
||||
KeyPair domainKeyPair = createKeyPair();
|
||||
|
||||
CSRBuilder csr = new CSRBuilder();
|
||||
csr.addDomain(domain);
|
||||
csr.sign(domainKeyPair);
|
||||
byte[] encodedCsr = csr.getEncoded();
|
||||
|
||||
Instant notBefore = Instant.now();
|
||||
Instant notAfter = notBefore.plus(Duration.ofDays(20L));
|
||||
|
||||
Order order = registration.orderCertificate(encodedCsr, notBefore, notAfter);
|
||||
assertThat(order.getCsr(), is(encodedCsr));
|
||||
assertThat(order.getNotBefore(), is(notBefore));
|
||||
assertThat(order.getNotAfter(), is(notAfter));
|
||||
assertThat(order.getStatus(), is(Status.PENDING));
|
||||
|
||||
for (Authorization auth : order.getAuthorizations()) {
|
||||
assertThat(auth.getDomain(), is(domain));
|
||||
assertThat(auth.getStatus(), is(Status.PENDING));
|
||||
|
||||
Challenge challenge = validator.prepare(auth);
|
||||
challenge.trigger();
|
||||
|
||||
await()
|
||||
.pollInterval(3, SECONDS)
|
||||
.timeout(30, SECONDS)
|
||||
.conditionEvaluationListener(cond -> updateAuth(auth))
|
||||
.until(auth::getStatus, not(Status.PENDING));
|
||||
|
||||
if (auth.getStatus() != Status.VALID) {
|
||||
fail("Authorization failed");
|
||||
}
|
||||
}
|
||||
|
||||
order.update();
|
||||
|
||||
Certificate certificate = order.getCertificate();
|
||||
X509Certificate cert = certificate.getCertificate();
|
||||
assertThat(cert, not(nullValue()));
|
||||
assertThat(cert.getNotAfter(), not(nullValue()));
|
||||
assertThat(cert.getNotBefore(), not(nullValue()));
|
||||
assertThat(cert.getSubjectX500Principal().getName(), is("CN=" + domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely updates the authorization, catching checked exceptions.
|
||||
*
|
||||
* @param auth
|
||||
* {@link Authorization} to update
|
||||
*/
|
||||
private void updateAuth(Authorization auth) {
|
||||
try {
|
||||
auth.update();
|
||||
} catch (AcmeException ex) {
|
||||
throw new AcmeLazyLoadingException(auth, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private static interface Validator {
|
||||
Challenge prepare(Authorization auth) throws Exception;
|
||||
}
|
||||
|
||||
}
|
|
@ -19,17 +19,18 @@ import static org.junit.Assert.assertThat;
|
|||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.shredzone.acme4j.util.KeyPairUtils;
|
||||
|
||||
/**
|
||||
* Superclass for all Pebble related integration tests.
|
||||
* <p>
|
||||
* These tests require a running
|
||||
* <a href="https://github.com/letsencrypt/pebble">Pebble</a> ACME test server at
|
||||
* localhost port 14000.
|
||||
* localhost port 14000. The host and port can be changed via the system property
|
||||
* {@code pebbleHost} and {@code pebblePort} respectively.
|
||||
*/
|
||||
public abstract class AbstractPebbleIT {
|
||||
public abstract class PebbleITBase {
|
||||
|
||||
private final String pebbleHost = System.getProperty("pebbleHost", "localhost");
|
||||
private final int pebblePort = Integer.parseInt(System.getProperty("pebblePort", "14000"));
|
||||
|
@ -47,13 +48,7 @@ public abstract class AbstractPebbleIT {
|
|||
* @return Created {@link KeyPair}, guaranteed to be unknown to the Pebble server
|
||||
*/
|
||||
protected KeyPair createKeyPair() {
|
||||
try {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(2048);
|
||||
return keyGen.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
throw new InternalError(ex);
|
||||
}
|
||||
return KeyPairUtils.createKeyPair(2048);
|
||||
}
|
||||
|
||||
/**
|
|
@ -32,7 +32,7 @@ import org.shredzone.acme4j.exception.AcmeUnauthorizedException;
|
|||
/**
|
||||
* Registration related integration tests.
|
||||
*/
|
||||
public class RegistrationIT extends AbstractPebbleIT {
|
||||
public class RegistrationIT extends PebbleITBase {
|
||||
|
||||
@Test
|
||||
public void testCreate() throws AcmeException {
|
||||
|
@ -55,16 +55,14 @@ public class RegistrationIT extends AbstractPebbleIT {
|
|||
// assertThat(reg.getStatus(), is(Status.GOOD));
|
||||
assertThat(reg.getTermsOfServiceAgreed(), is(true));
|
||||
|
||||
// TODO PEBBLE: missing
|
||||
/*
|
||||
// Bind another Registration object
|
||||
Session session2 = new Session(pebbleURI(), keyPair);
|
||||
Registration reg2 = Registration.bind(session2, location);
|
||||
assertThat(reg2.getLocation(), is(location));
|
||||
assertThat(reg2.getContacts(), contains(URI.create("mailto:acme@example.com")));
|
||||
assertThat(reg2.getStatus(), is(Status.GOOD));
|
||||
assertThat(reg2.getTermsOfServiceAgreed(), is(true));
|
||||
*/
|
||||
// TODO PEBBLE: Not supported yet
|
||||
// Session session2 = new Session(pebbleURI(), keyPair);
|
||||
// Registration reg2 = Registration.bind(session2, location);
|
||||
// assertThat(reg2.getLocation(), is(location));
|
||||
// assertThat(reg2.getContacts(), contains(URI.create("mailto:acme@example.com")));
|
||||
// assertThat(reg2.getStatus(), is(Status.GOOD));
|
||||
// assertThat(reg2.getTermsOfServiceAgreed(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
|
@ -29,7 +29,7 @@ import org.shredzone.acme4j.exception.AcmeException;
|
|||
/**
|
||||
* Session related integration tests.
|
||||
*/
|
||||
public class SessionIT extends AbstractPebbleIT {
|
||||
public class SessionIT extends PebbleITBase {
|
||||
|
||||
private final KeyPair keyPair = createKeyPair();
|
||||
|
||||
|
@ -52,9 +52,6 @@ public class SessionIT extends AbstractPebbleIT {
|
|||
public void testResources() throws AcmeException {
|
||||
Session session = new Session(pebbleURI(), keyPair);
|
||||
|
||||
// TODO: Not yet supported by Pebble
|
||||
// assertIsPebbleUrl(session.resourceUrl(Resource.KEY_CHANGE));
|
||||
|
||||
assertIsPebbleUrl(session.resourceUrl(Resource.NEW_NONCE));
|
||||
assertIsPebbleUrl(session.resourceUrl(Resource.NEW_REG));
|
||||
assertIsPebbleUrl(session.resourceUrl(Resource.NEW_ORDER));
|
||||
|
@ -67,7 +64,6 @@ public class SessionIT extends AbstractPebbleIT {
|
|||
Metadata meta = session.getMetadata();
|
||||
assertThat(meta, not(nullValue()));
|
||||
|
||||
// Pebble does not currently deliver any metadata
|
||||
assertThat(meta.getTermsOfService(), is(URI.create("data:text/plain,Do%20what%20thou%20wilt")));
|
||||
assertThat(meta.getWebsite(), is(nullValue()));
|
||||
assertThat(meta.getCaaIdentities(), is(empty()));
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"pebble": {
|
||||
"listenAddress": "0.0.0.0:14000",
|
||||
"httpPort": 5002,
|
||||
"tlsPort": 5001
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
org.slf4j.simpleLogger.log.org.shredzone.acme4j = debug
|
13
pom.xml
13
pom.xml
|
@ -52,16 +52,19 @@
|
|||
|
||||
<properties>
|
||||
<bouncycastle.version>1.56</bouncycastle.version>
|
||||
<dnsjava.version>2.1.7</dnsjava.version>
|
||||
<jose4j.version>0.5.5</jose4j.version>
|
||||
<nanohttpd.version>2.3.1</nanohttpd.version>
|
||||
<slf4j.version>1.7.25</slf4j.version>
|
||||
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
|
||||
<skipITs>true</skipITs>
|
||||
<project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>acme4j-client</module>
|
||||
<module>acme4j-utils</module>
|
||||
<module>acme4j-example</module>
|
||||
<module>acme4j-it</module>
|
||||
</modules>
|
||||
|
||||
<build>
|
||||
|
@ -137,7 +140,7 @@
|
|||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.0.1</version>
|
||||
<version>3.0.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
|
@ -222,5 +225,11 @@
|
|||
<version>[2.0,)</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
Loading…
Reference in New Issue