Project start

pull/17/merge
Richard Körber 2015-12-09 01:24:03 +01:00
parent df44e2b80a
commit 7e07a0e2e4
74 changed files with 3468 additions and 10 deletions

View File

@ -10,11 +10,6 @@
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
@ -24,6 +19,5 @@
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>
</projectDescription>

View File

@ -1,7 +1,5 @@
# ACME Java Client ![build status](http://jenkins.shredzone.net/buildStatus/icon?job=acme4j)
*SOURCE IS COMING SOON!*
This is a Java client for the [ACME](https://tools.ietf.org/html/draft-ietf-acme-acme-01) protocol.
ACME is a protocol that a certificate authority (CA) and an applicant can use to automate the process of verification and certificate issuance.
@ -10,10 +8,43 @@ This Java client helps connecting to an ACME server, and performing all necessar
It is an independent open source implementation that is not affiliated with or endorsed by _Let's Encrypt_. The source code can be found at [GitHub](https://github.com/shred/acme4j) and is distributed under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).
Alpha Release!
--------------
Please note that even though _acme4j_ is already usable, it is currently in an early alpha state. This means that:
* _acme4j_ is not feature complete yet (see the "Missing" section below).
* The API is not stable. It may change in a manner not compatible to previous versions and without prior notice.
* _acme4j_ is not thoroughly tested yet, and may still have major bugs.
Features
--------
* Easy to use Java API
* Requires JRE 7 or higher
* Built with maven (package will be available shortly at Maven Central)
* Small, only requires [jose4j](https://bitbucket.org/b_c/jose4j/wiki/Home) and [slf4j](http://www.slf4j.org/) as dependencies. [Bouncy Castle](https://www.bouncycastle.org/java.html) is recommended for some features, but is not required.
* Built with maven (package will be made available at Maven Central as soon as beta state is reached)
* Small, only requires [jose4j](https://bitbucket.org/b_c/jose4j/wiki/Home) and [slf4j](http://www.slf4j.org/) as dependencies. [Bouncy Castle](https://www.bouncycastle.org/java.html) is recommended, but not required.
How to Use
----------
_acme4j_ consists of a few modules:
* _acme4j-client_: This is the main module. It contains the ACME client and everything needed for communication with an ACME server.
* _acme4j-letsencrypt_: A _Let's Encrypt_ service. Just add it as dependency, it will neatly plug into the client.
* _acme4j-utils_: Some utility classes that may be helpful for creating key pairs, certificates, and certificate signing requests. Requires [Bouncy Castle](https://www.bouncycastle.org/java.html).
* _acme4j-example_: An example tool that performs all steps for registering a new account at _Let's Encrypt_ and getting a certificate for a set of domain names. This is a good starting point to find out how _acme4j_ is used.
Missing
-------
The following features are planned to be completed for the first beta release, but are still missing:
* Support of account recovery and certificate revocation.
* `proofOfPossession-01` and `tls-sni-01` challenge support.
* Extensive unit tests.
* Better error handling.
* Some hardening (like plausibility checks).
* Full documentation.
_acme4j_ is open source software. Feel free to send in pull requests!

23
acme4j-client/.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>acme4j-client</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>

43
acme4j-client/pom.xml Normal file
View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
*
* acme4j - ACME Java 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.
*
-->
<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>0.1-SNAPSHOT</version>
</parent>
<artifactId>acme4j-client</artifactId>
<name>acme4j client</name>
<description>ACME client for Java</description>
<dependencies>
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>${jose4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
</project>

View File

View File

@ -0,0 +1,46 @@
/*
* 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;
import java.security.KeyPair;
/**
* Represents an account at the ACME server.
* <p>
* An account is identified by its {@link KeyPair}.
*
* @author Richard "Shred" Körber
*/
public class Account {
private final KeyPair keyPair;
/**
* Creates a new {@link Account} instance.
*
* @param keyPair
* {@link KeyPair} that identifies the account.
*/
public Account(KeyPair keyPair) {
this.keyPair = keyPair;
}
/**
* The {@link KeyPair} that belongs to this account.
*/
public KeyPair getKeyPair() {
return keyPair;
}
}

View File

@ -0,0 +1,103 @@
/*
* 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;
import java.net.URI;
import java.security.cert.X509Certificate;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.exception.AcmeException;
/**
* An {@link AcmeClient} is used for communication with an ACME server.
* <p>
* Use {@link AcmeClientFactory} to generate instances.
*
* @author Richard "Shred" Körber
*/
public interface AcmeClient {
/**
* Registers a new account.
*
* @param account
* {@link Account} to register
* @param registration
* {@link Registration} containing registration data
*/
void newRegistration(Account account, Registration registration) throws AcmeException;
/**
* Updates an existing account.
*
* @param account
* {@link Account} that is registered
* @param registration
* {@link Registration} containing updated registration data. Set the
* account location via {@link Registration#setLocation(URI)}!
*/
void updateRegistration(Account account, Registration registration) throws AcmeException;
/**
* Creates a new {@link Authorization} for a domain.
*
* @param account
* {@link Account} the authorization is related to
* @param auth
* {@link Authorization} containing the domain name
*/
void newAuthorization(Account account, Authorization auth) throws AcmeException;
/**
* Triggers a {@link Challenge}. The ACME server is requested to validate the
* response. Note that the validation is performed asynchronously.
*
* @param account
* {@link Account} to be used for conversation
* @param challenge
* {@link Challenge} to trigger
*/
void triggerChallenge(Account account, Challenge challenge) throws AcmeException;
/**
* Updates the {@link Challenge} instance. It contains the current state.
*
* @param account
* {@link Account} to be used for conversation
* @param challenge
* {@link Challenge} to update
*/
void updateChallenge(Account account, Challenge challenge) throws AcmeException;
/**
* Request a certificate.
*
* @param account
* {@link Account} to be used for conversation
* @param csr
* PKCS#10 Certificate Signing Request to be sent to the server
* @return {@link URI} the certificate can be downloaded from
*/
URI requestCertificate(Account account, byte[] csr) throws AcmeException;
/**
* Downloads a certificate.
*
* @param certUri
* Certificate {@link URI}
* @return Downloaded {@link X509Certificate}
*/
X509Certificate downloadCertificate(URI certUri) throws AcmeException;
}

View File

@ -0,0 +1,67 @@
/*
* 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;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.provider.AcmeClientProvider;
/**
* Generates {@link AcmeClient} instances.
* <p>
* An {@link AcmeClient} is generated by an {@link AcmeClientProvider}. There are generic
* providers and providers tailor-made for specific ACME servers. All providers are
* managed via Java's {@link ServiceLoader} API.
*
* @author Richard "Shred" Körber
*/
public final class AcmeClientFactory {
private AcmeClientFactory() {
// utility class without constructor
}
/**
* Connects to an ACME server and provides an {@link AcmeClient} for communication.
*
* @param serverUri
* URI of the ACME server. This can either be a http/https URI to the
* server's directory service, or a special acme URI for specific
* implementations.
* @return {@link AcmeClient} for communication with the server
*/
public static AcmeClient connect(String serverUri) throws AcmeException {
List<AcmeClientProvider> candidates = new ArrayList<>();
for (AcmeClientProvider acp : ServiceLoader.load(AcmeClientProvider.class)) {
if (acp.accepts(serverUri)) {
candidates.add(acp);
}
}
if (candidates.isEmpty()) {
throw new AcmeException("No ACME provider found for " + serverUri);
} else if (candidates.size() > 1) {
throw new IllegalArgumentException("There are " + candidates.size() + " "
+ AcmeClientProvider.class.getSimpleName() + " accepting " + serverUri
+ ". Please check your classpath.");
} else {
AcmeClientProvider provider = candidates.get(0);
return provider.connect(serverUri);
}
}
}

View File

@ -0,0 +1,153 @@
/*
* 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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.shredzone.acme4j.challenge.Challenge;
/**
* Represents an authorization request at the ACME server.
*
* @author Richard "Shred" Körber
*/
public class Authorization {
private String domain;
private String status;
private Date expires;
private List<Challenge> challenges;
private List<List<Challenge>> combinations;
/**
* Gets the domain name to be authorized.
*/
public String getDomain() {
return domain;
}
/**
* Sets the domain name to authorize.
*/
public void setDomain(String domain) {
this.domain = domain;
}
/**
* Gets the authorization status.
*/
public String getStatus() {
return status;
}
/**
* Sets the authorization status.
*/
public void setStatus(String status) {
this.status = status;
}
/**
* Gets the expiry date of the authorization, if set by the server.
*/
public Date getExpires() {
return expires;
}
/**
* Sets the expiry date of the authorization.
*/
public void setExpires(Date expires) {
this.expires = expires;
}
/**
* Gets a list of all challenges available by the server.
*/
public List<Challenge> getChallenges() {
return challenges;
}
/**
* Sets a list of all challenges available by the server.
*/
public void setChallenges(List<Challenge> challenges) {
this.challenges = challenges;
}
/**
* Gets all combinations of challenges supported by the server.
*/
public List<List<Challenge>> getCombinations() {
return combinations;
}
/**
* Sets all combinations of challenges supported by the server.
*/
public void setCombinations(List<List<Challenge>> combinations) {
this.combinations = combinations;
}
/**
* Finds a single {@link Challenge} of the given type. Responding to this
* {@link Challenge} is sufficient for authorization. This is a convenience call to
* {@link #findCombination(String...)}.
*
* @param type
* Challenge name (e.g. "http-01")
* @return {@link Challenge} matching that name, or {@code null} if there is no such
* challenge, or the challenge alone is not sufficient for authorization.
* @throws ClassCastException
* if the type does not match the expected Challenge class type
*/
@SuppressWarnings("unchecked")
public <T extends Challenge> T findChallenge(String type) {
Collection<Challenge> result = findCombination(type);
return (result != null ? (T) result.iterator().next() : null);
}
/**
* Finds a combination of {@link Challenge} types that the client supports. The client
* has to respond to <em>all</em> of the {@link Challenge}s returned.
*
* @param types
* Challenge name or names (e.g. "http-01"), in no particular order.
* @return Matching {@link Challenge} combination, or {@code null} if the ACME server
* does not support this challenge combination. The challenges are returned
* in no particular order.
*/
public Collection<Challenge> findCombination(String... types) {
Collection<String> reference = Arrays.asList(types);
for (List<Challenge> combination : combinations) {
Collection<String> combinationTypes = new ArrayList<>();
for (Challenge c : combination) {
combinationTypes.add(c.getType());
}
if (reference.size() == combinationTypes.size()
&& reference.containsAll(combinationTypes)) {
return combination;
}
}
return null;
}
}

View File

@ -0,0 +1,68 @@
/*
* 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;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a registration at the ACME server.
*
* @author Richard "Shred" Körber
*/
public class Registration {
private String agreementUrl;
private List<String> contacts = new ArrayList<>();
private URI location;
/**
* Returns the URL of the agreement document the user is required to accept.
*/
public String getAgreementUrl() {
return agreementUrl;
}
/**
* Sets the URL of the agreement document the user is required to accept.
*/
public void setAgreementUrl(String agreementUrl) {
this.agreementUrl = agreementUrl;
}
/**
* List of contact email addresses.
*/
public List<String> getContacts() {
return contacts;
}
/**
* Location URI of the registration at the server. Returned from the server after
* successfully creating or updating a registration.
*/
public URI getLocation() {
return location;
}
/**
* Location URI of the registration at the server. Must be set when updating the
* registration.
*/
public void setLocation(URI location) {
this.location = location;
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.net.URI;
import java.util.Date;
import java.util.Map;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.util.ClaimBuilder;
/**
* A challenge.
*
* @author Richard "Shred" Körber
*/
public interface Challenge {
/**
* Challenge status enumeration.
*/
public enum Status {
PENDING, PROCESSING, VALID, INVALID, REVOKED, UNKNOWN;
}
/**
* Returns the challenge type by name (e.g. "http-01").
*/
String getType();
/**
* Returns the {@link URI} of the challenge.
*/
URI getUri();
/**
* Returns the current status of the challenge.
*/
Status getStatus();
/**
* Returns the validation date, if returned by the server.
*/
Date getValidated();
/**
* Authorizes a {@link Challenge} by signing it with an {@link Account}. This is
* required before triggering the challenge.
*
* @param account
* {@link Account} to sign the challenge with
*/
void authorize(Account account);
/**
* Sets the challenge state by reading the given JSON map.
*
* @param map
* JSON map containing the challenge data
*/
void unmarshall(Map<String, Object> map);
/**
* Copies the current challenge state to the claim builder, as preparation for
* triggering it.
*
* @param cb
* {@link ClaimBuilder} to copy the challenge state to
*/
void marshall(ClaimBuilder cb);
}

View File

@ -0,0 +1,69 @@
/*
* 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 org.jose4j.base64url.Base64Url;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.util.ClaimBuilder;
/**
* Implements the {@code dns-01} challenge.
*
* @author Richard "Shred" Körber
*/
public class DnsChallenge extends GenericChallenge {
/**
* Challenge type name.
*/
public static final String TYPE = "dns-01";
private String authorization;
/**
* Returns the token to be used for this challenge.
*/
public String getToken() {
return get(KEY_TOKEN);
}
/**
* Sets the token to be used.
*/
public void setToken(String token) {
put(KEY_TOKEN, token);
}
/**
* Returns the authorization string to be used for the response.
*/
public String getAuthorization() {
return authorization;
}
@Override
public void authorize(Account account) {
super.authorize(account);
authorization = getToken() + '.' + Base64Url.encode(jwkThumbprint(account.getKeyPair().getPublic()));
}
@Override
public void marshall(ClaimBuilder cb) {
if (authorization == null) {
throw new IllegalStateException("Challenge has not been authorized yet.");
}
cb.put(KEY_KEY_AUTHORIZSATION, authorization);
}
}

View File

@ -0,0 +1,139 @@
/*
* 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.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.util.ClaimBuilder;
/**
* A generic implementation of {@link Challenge}. It can be used as a base class for
* actual challenge implemenation, but it is also used if the ACME server offers a
* proprietary challenge that is unknown to acme4j.
*
* @author Richard "Shred" Körber
*/
public class GenericChallenge implements Challenge {
protected static final String KEY_TYPE = "type";
protected static final String KEY_STATUS = "status";
protected static final String KEY_URI = "uri";
protected static final String KEY_VALIDATED = "validated";
protected static final String KEY_TOKEN = "token";
protected static final String KEY_KEY_AUTHORIZSATION = "keyAuthorization";
private final Map<String, Object> data = new HashMap<>();
@Override
public String getType() {
return get(KEY_TYPE);
}
@Override
public Status getStatus() {
String status = get(KEY_STATUS);
return (status != null ? Status.valueOf(status.toUpperCase()) : Status.PENDING);
}
@Override
public URI getUri() {
try {
return new URI(get(KEY_URI).toString());
} catch (URISyntaxException ex) {
throw new IllegalStateException("Invalid URI", ex);
}
}
@Override
public Date getValidated() {
return get(KEY_VALIDATED);
}
@Override
public void authorize(Account account) {
// Standard implementation does nothing...
}
@Override
public void unmarshall(Map<String, Object> map) {
data.clear();
data.putAll(map);
}
@Override
public void marshall(ClaimBuilder cb) {
cb.putAll(data);
}
/**
* Gets a value from the challenge state.
*
* @param key
* Key
* @return Value, or {@code null} if not set
*/
@SuppressWarnings("unchecked")
protected <T> T get(String key) {
return (T) data.get(key);
}
/**
* Puts a value to the challenge state.
*
* @param key
* Key
* @param value
* Value, may be {@code null}
*/
protected void put(String key, Object value) {
data.put(key, value);
}
/**
* Computes a JWK Thumbprint. It is frequently used in responses.
*
* @param key
* {@link Key} to create a thumbprint of
* @return Thumbprint, SHA-256 hashed
* @see <a href="https://tools.ietf.org/html/rfc7638">RFC 7638</a>
*/
public static byte[] jwkThumbprint(Key key) {
try {
final JsonWebKey jwk = JsonWebKey.Factory.newJwk(key);
// We need to use ClaimBuilder to bring the keys in lexicographical order.
ClaimBuilder cb = new ClaimBuilder();
cb.putAll(jwk.toParams(OutputControlLevel.PUBLIC_ONLY));
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(cb.toString().getBytes("UTF-8"));
return md.digest();
} catch (JoseException | NoSuchAlgorithmException | UnsupportedEncodingException ex) {
throw new IllegalArgumentException("Cannot compute key thumbprint", ex);
}
}
}

View File

@ -0,0 +1,73 @@
/*
* 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 org.jose4j.base64url.Base64Url;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.util.ClaimBuilder;
/**
* Implements the {@code http-01} challenge.
*
* @author Richard "Shred" Körber
*/
public class HttpChallenge extends GenericChallenge {
/**
* Challenge type name.
*/
public static final String TYPE = "http-01";
private String authorization;
/**
* Returns the token to be used for this challenge.
*/
public String getToken() {
return get(KEY_TOKEN);
}
/**
* Sets the token to be used.
*/
public void setToken(String token) {
put(KEY_TOKEN, token);
}
/**
* Returns the authorization string to be used for the response.
* <p>
* <em>NOTE:</em> The response file must only contain the returned String (UTF-8
* or ASCII encoded). There must not be any other leading or trailing characters
* (like white-spaces or line breaks). Otherwise the challenge will fail.
*/
public String getAuthorization() {
return authorization;
}
@Override
public void authorize(Account account) {
super.authorize(account);
authorization = getToken() + '.' + Base64Url.encode(jwkThumbprint(account.getKeyPair().getPublic()));
}
@Override
public void marshall(ClaimBuilder cb) {
if (authorization == null) {
throw new IllegalStateException("Challenge has not been authorized yet.");
}
cb.put(KEY_KEY_AUTHORIZSATION, authorization);
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.security.Key;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.util.ClaimBuilder;
/**
* Implements the {@code proofOfPossession-01} challenge.
* <p>
* <em>TODO: Currently this challenge is not implemented.</em>
*
* @author Richard "Shred" Körber
*/
public class ProofOfPossessionChallenge extends GenericChallenge {
/**
* Challenge type name.
*/
public static final String TYPE = "proofOfPossession-01";
private Key accountKey;
@Override
public void authorize(Account account) {
super.authorize(account);
accountKey = account.getKeyPair().getPublic();
}
@Override
public void marshall(ClaimBuilder cb) {
super.marshall(cb);
cb.putKey("accountKey", accountKey);
}
}

View File

@ -0,0 +1,46 @@
/*
* 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;
/**
* Implements the {@code tls-sni-01} challenge.
* <p>
* <em>TODO: Currently this challenge is not implemented.</em>
*
* @author Richard "Shred" Körber
*/
public class TlsSniChallenge extends GenericChallenge {
/**
* Challenge type name.
*/
public static final String TYPE = "tls-sni-01";
public String getToken() {
return get(KEY_TOKEN);
}
public void setToken(String token) {
put(KEY_TOKEN, token);
}
public int getN() {
return get("n");
}
public void setN(int n) {
put("n", n);
}
}

View File

@ -0,0 +1,313 @@
/*
* 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.connector;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyPair;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.EnumMap;
import java.util.Map;
import org.jose4j.base64url.Base64Url;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeServerException;
import org.shredzone.acme4j.provider.AcmeClientProvider;
import org.shredzone.acme4j.util.ClaimBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Connects to the ACME server and offers different methods for invoking the API.
*
* @author Richard "Shred" Körber
*/
public class Connection implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(Connection.class);
private final AcmeClientProvider provider;
private HttpURLConnection conn;
public Connection(AcmeClientProvider provider) {
this.provider = provider;
}
@Override
public void close() {
conn = null;
}
/**
* Forcedly starts a new {@link Session}. Usually this method is not required, as a
* session is automatically started if necessary.
*
* @param uri
* {@link URI} a HEAD request is sent to for starting the session
* @param session
* {@link Session} instance to be used for tracking
*/
public void startSession(URI uri, Session session) throws AcmeException {
try {
LOG.debug("Initial replay nonce from {}", uri);
HttpURLConnection conn = provider.openConnection(uri);
conn.setRequestMethod("HEAD");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.connect();
session.setNonce(getNonceFromHeader(conn));
} catch (IOException ex) {
throw new AcmeException("Failed to request a nonce", ex);
}
}
/**
* Sends a simple GET request.
*
* @param uri
* {@link URI} to send the request to.
* @return HTTP response code
*/
public int sendRequest(URI uri) throws AcmeException {
try {
LOG.debug("GET {}", uri);
conn = provider.openConnection(uri);
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setDoOutput(false);
conn.connect();
throwException();
return conn.getResponseCode();
} catch (IOException ex) {
throw new AcmeException("API access failed", ex);
}
}
/**
* Sends a signed POST request.
*
* @param uri
* {@link URI} to send the request to.
* @param claims
* {@link ClaimBuilder} containing claims. Must not be {@code null}.
* @param session
* {@link Session} instance to be used for tracking
* @param account
* {@link Account} to be used for signing the request
* @return HTTP response code
*/
public int sendSignedRequest(URI uri, ClaimBuilder claims, Session session, Account account) throws AcmeException {
try {
KeyPair keypair = account.getKeyPair();
if (session.getNonce() == null) {
startSession(uri, session);
}
if (session.getNonce() == null) {
throw new AcmeException("No nonce available");
}
LOG.debug("POST {} with claims: {}", uri, claims);
conn = provider.openConnection(uri);
conn.setRequestMethod("POST");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
final JsonWebKey jwk = JsonWebKey.Factory.newJwk(keypair.getPublic());
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toString());
jws.getHeaders().setObjectHeaderValue("nonce", Base64Url.encode(session.getNonce()));
jws.getHeaders().setJwkHeaderValue("jwk", jwk);
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
jws.setKey(keypair.getPrivate());
byte[] outputData = jws.getCompactSerialization().getBytes("utf-8");
conn.setFixedLengthStreamingMode(outputData.length);
conn.connect();
try (OutputStream out = conn.getOutputStream()) {
out.write(outputData);
}
session.setNonce(getNonceFromHeader(conn));
throwException();
return conn.getResponseCode();
} catch (JoseException | IOException ex) {
throw new AcmeException("Failed to send request to " + uri, ex);
}
}
/**
* Reads a server response as JSON data.
*
* @return Map containing the parsed JSON data
*/
public Map<String, Object> readJsonResponse() throws AcmeException {
if (conn == null) {
throw new IllegalStateException("Not connected");
}
StringBuilder sb = new StringBuilder();
Map<String, Object> result = null;
try {
InputStream in = (conn.getResponseCode() < 400 ? conn.getInputStream() : conn.getErrorStream());
if (in != null) {
try (BufferedReader r = new BufferedReader(new InputStreamReader(in, "utf-8"))) {
sb.append(r.readLine());
}
result = JsonUtil.parseJson(sb.toString());
LOG.debug("Result JSON: {}", sb);
}
} catch (JoseException | IOException ex) {
throw new AcmeException("Failed to parse response: " + sb, ex);
}
return result;
}
/**
* Reads a certificate.
*
* @return {@link X509Certificate} that was read.
*/
public X509Certificate readCertificate() throws AcmeException {
if (conn == null) {
throw new IllegalStateException("Not connected");
}
try (InputStream in = conn.getInputStream()) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(in);
} catch (IOException ex) {
throw new AcmeException("Failed to read certificate", ex);
} catch (CertificateException ex) {
throw new AcmeException("Error while generating the X.509 certificate", ex);
}
}
/**
* Reads a resource directory.
*
* @return Map of {@link Resource} and the respective {@link URI} to invoke
*/
public Map<Resource, URI> readResourceMap() throws AcmeException {
EnumMap<Resource, URI> resourceMap = new EnumMap<>(Resource.class);
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
sb.append(reader.readLine());
} catch (IOException ex) {
throw new AcmeException("Could not read resource map", ex);
}
try {
Map<String, Object> result = JsonUtil.parseJson(sb.toString());
for (Map.Entry<String, Object> entry : result.entrySet()) {
Resource res = Resource.parse(entry.getKey());
if (res != null) {
URI uri = new URI(entry.getValue().toString());
resourceMap.put(res, uri);
}
}
LOG.debug("Resource directory: {}", resourceMap);
} catch (JoseException | URISyntaxException ex) {
throw new AcmeException("Could not parse resource map: " + sb, ex);
}
return resourceMap;
}
/**
* Gets a location from the {@code Location} header.
*
* @return Location {@link URI}, or {@code null} if no Location header was set
*/
public URI getLocation() throws AcmeException {
String location = conn.getHeaderField("Location");
if (location == null) {
return null;
}
try {
LOG.debug("Location: {}", location);
return new URI(location);
} catch (URISyntaxException ex) {
throw new AcmeException("Bad Location header: " + location);
}
}
/**
* Checks if the server returned an error, and if so, throws a {@link AcmeException}.
*
* @throws AcmeException
* if the server returned a JSON problem
*/
private void throwException() throws AcmeException {
if ("application/problem+json".equals(conn.getHeaderField("Content-Type"))) {
Map<String, Object> map = readJsonResponse();
String type = (String) map.get("type");
String detail = (String) map.get("detail");
throw new AcmeServerException(type, detail);
}
}
/**
* Extracts a nonce from the header.
*
* @param conn
* {@link HttpURLConnection} to read the headers from
* @return Nonce
* @throws AcmeException
* if there was no {@code Replay-Nonce} header, or the nonce was invalid
*/
private static byte[] getNonceFromHeader(HttpURLConnection conn) throws AcmeException {
String nonceHeader = conn.getHeaderField("Replay-Nonce");
if (nonceHeader == null) {
throw new AcmeException("No replay nonce");
}
LOG.debug("Replay Nonce: {}", nonceHeader);
return Base64Url.decode(nonceHeader);
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.connector;
/**
* Enumeration of resources.
*
* @author Richard "Shred" Körber
*/
public enum Resource {
NEW_REG("new-reg"), NEW_AUTHZ("new-authz"), NEW_CERT("new-cert");
/**
* Parses the string and returns a matching {@link Resource} instance.
*
* @param str
* String to parse
* @return {@link Resource} instance, or {@code null} if the resource is unknown
*/
public static Resource parse(String str) {
for (Resource r : values()) {
if (r.path().equals(str)) {
return r;
}
}
return null;
}
private final String path;
private Resource(String path) {
this.path = path;
}
/**
* Returns the resource path.
*/
public String path() {
return path;
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.connector;
/**
* A session for tracking communication parameters.
*
* @author Richard "Shred" Körber
*/
public class Session {
private byte[] nonce;
/**
* Gets the last nonce, or {@code null} if the session is new.
*/
public byte[] getNonce() {
return nonce;
}
/**
* Sets the nonce received by the server.
*/
public void setNonce(byte[] nonce) {
this.nonce = nonce;
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.exception;
/**
* A generic ACME exception.
*
* @author Richard "Shred" Körber
*/
public class AcmeException extends Exception {
private static final long serialVersionUID = -2935088954705632025L;
public AcmeException() {
super();
}
public AcmeException(String msg) {
super(msg);
}
public AcmeException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@ -0,0 +1,66 @@
/*
* 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.exception;
/**
* An exception that is thrown when the ACME server returned an error. It contains
* further details of the cause.
*
* @author Richard "Shred" Körber
*/
public class AcmeServerException extends AcmeException {
private static final long serialVersionUID = 5971622508467042792L;
private static final String ACME_ERROR_PREFIX = "urn:acme:error:";
private final String type;
/**
* Creates a new {@link AcmeServerException}.
*
* @param type
* System readable error type (e.g. {@code "urn:acme:error:malformed"})
* @param detail
* Human readable error message
*/
public AcmeServerException(String type, String detail) {
super(detail);
if (type == null) {
throw new IllegalArgumentException("Error type must not be null");
}
this.type = type;
}
/**
* Returns the error type.
*/
public String getType() {
return type;
}
/**
* Returns the ACME error type. This is the last part of the type URN, e.g.
* {@code "malformed"} on {@code "urn:acme:error:malformed"}.
*
* @return ACME error type, or {@code null} if this is not an {@code "urn:acme:error"}
*/
public String getAcmeErrorType() {
if (type.startsWith(ACME_ERROR_PREFIX)) {
return type.substring(ACME_ERROR_PREFIX.length());
} else {
return null;
}
}
}

View File

@ -0,0 +1,209 @@
/*
* 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.impl;
import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.AcmeClient;
import org.shredzone.acme4j.Authorization;
import org.shredzone.acme4j.Registration;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.connector.Session;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeServerException;
import org.shredzone.acme4j.util.ClaimBuilder;
/**
* An abstract implementation of the {@link AcmeClient} interface. It contains abstract
* methods for everything that is provider related.
*
* @author Richard "Shred" Körber
*/
public abstract class AbstractAcmeClient implements AcmeClient {
private final Session session = new Session();
/**
* Gets the {@link URI} for the given {@link Resource}. This may involve connecting to
* the server and getting a directory. The result should be cached in the client.
*
* @param resource
* {@link Resource} to get the {@link URI} of
* @return {@link URI}, or {@code null} if the server does not offer that resource
*/
protected abstract URI resourceUri(Resource resource) throws AcmeException;
/**
* Creates a {@link Challenge} instance for the given challenge type.
*
* @param type
* Challenge type
* @return {@link Challenge} instance
*/
protected abstract Challenge createChallenge(String type);
/**
* Connects to the server's API.
*
* @return {@link Connection} instance
*/
protected abstract Connection connect();
@Override
public void newRegistration(Account account, Registration registration) throws AcmeException {
try (Connection conn = connect()) {
ClaimBuilder claims = new ClaimBuilder();
claims.putResource(Resource.NEW_REG);
if (!registration.getContacts().isEmpty()) {
claims.put("contact", registration.getContacts());
}
if (registration.getAgreementUrl() != null) {
claims.put("agreement", registration.getAgreementUrl());
}
try {
conn.sendSignedRequest(resourceUri(Resource.NEW_REG), claims, session, account);
} catch (AcmeServerException ex) {
URI location = conn.getLocation();
if (location != null) {
registration.setLocation(location);
}
throw ex;
}
}
}
@Override
public void updateRegistration(Account account, Registration registration) throws AcmeException {
if (registration.getLocation() == null) {
throw new IllegalArgumentException("location must be set. Use newRegistration() if not known.");
}
try (Connection conn = connect()) {
ClaimBuilder claims = new ClaimBuilder();
claims.putResource("reg");
if (!registration.getContacts().isEmpty()) {
claims.put("contact", registration.getContacts());
}
if (registration.getAgreementUrl() != null) {
claims.put("agreement", registration.getAgreementUrl());
}
conn.sendSignedRequest(registration.getLocation(), claims, session, account);
registration.setLocation(conn.getLocation());
}
}
@Override
public void newAuthorization(Account account, Authorization auth) throws AcmeException {
try (Connection conn = connect()) {
ClaimBuilder claims = new ClaimBuilder();
claims.putResource(Resource.NEW_AUTHZ);
claims.object("identifier")
.put("type", "dns")
.put("value", auth.getDomain());
conn.sendSignedRequest(resourceUri(Resource.NEW_AUTHZ), claims, session, account);
Map<String, Object> result = conn.readJsonResponse();
@SuppressWarnings("unchecked")
Collection<Map<String, Object>> challenges =
(Collection<Map<String, Object>>) result.get("challenges");
List<Challenge> cr = new ArrayList<>();
for (Map<String, Object> c : challenges) {
Challenge ch = createChallenge((String) c.get("type"));
if (ch != null) {
ch.unmarshall(c);
cr.add(ch);
}
}
auth.setChallenges(cr);
@SuppressWarnings("unchecked")
Collection<List<Number>> combinations =
(Collection<List<Number>>) result.get("combinations");
if (combinations != null) {
List<List<Challenge>> cmb = new ArrayList<>(combinations.size());
for (List<Number> c : combinations) {
List<Challenge> clist = new ArrayList<>(c.size());
for (Number n : c) {
clist.add(cr.get(n.intValue()));
}
cmb.add(clist);
}
auth.setCombinations(cmb);
} else {
List<List<Challenge>> cmb = new ArrayList<>(1);
cmb.add(cr);
auth.setCombinations(cmb);
}
}
}
@Override
public void triggerChallenge(Account account, Challenge challenge) throws AcmeException {
try (Connection conn = connect()) {
ClaimBuilder claims = new ClaimBuilder();
claims.putResource("challenge");
challenge.marshall(claims);
conn.sendSignedRequest(challenge.getUri(), claims, session, account);
challenge.unmarshall(conn.readJsonResponse());
}
}
@Override
public void updateChallenge(Account account, Challenge challenge) throws AcmeException {
try (Connection conn = connect()) {
conn.sendRequest(challenge.getUri());
challenge.unmarshall(conn.readJsonResponse());
}
}
@Override
public URI requestCertificate(Account account, byte[] csr) throws AcmeException {
try (Connection conn = connect()) {
ClaimBuilder claims = new ClaimBuilder();
claims.putResource(Resource.NEW_CERT);
claims.putBase64("csr", csr);
conn.sendSignedRequest(resourceUri(Resource.NEW_CERT), claims, session, account);
// Optionally returns the certificate. Currently it is just ignored.
// X509Certificate cert = conn.readCertificate();
return conn.getLocation();
}
}
@Override
public X509Certificate downloadCertificate(URI certUri) throws AcmeException {
try (Connection conn = connect()) {
conn.sendRequest(certUri);
return conn.readCertificate();
}
}
}

View File

@ -0,0 +1,73 @@
/*
* 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.impl;
import java.net.URI;
import java.util.EnumMap;
import java.util.Map;
import org.shredzone.acme4j.AcmeClient;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.provider.AcmeClientProvider;
/**
* A generic implementation of {@link AcmeClient}. It uses an {@link AcmeClientProvider}
* for managing individual server features.
*
* @author Richard "Shred" Körber
*/
public class GenericAcmeClient extends AbstractAcmeClient {
private final AcmeClientProvider provider;
private final URI directoryUri;
private final Map<Resource, URI> directoryMap = new EnumMap<>(Resource.class);
/**
* Creates a new {@link GenericAcmeClient}.
*
* @param provider
* {@link AcmeClientProvider} creating this client
* @param directoryUri
* {@link URI} of the ACME server's directory service
*/
public GenericAcmeClient(AcmeClientProvider provider, URI directoryUri) {
this.provider = provider;
this.directoryUri = directoryUri;
}
@Override
protected Challenge createChallenge(String type) {
return provider.createChallenge(type);
}
@Override
protected Connection connect() {
return new Connection(provider);
}
@Override
protected URI resourceUri(Resource resource) throws AcmeException {
if (directoryMap.isEmpty()) {
try (Connection conn = connect()) {
conn.sendRequest(directoryUri);
directoryMap.putAll(conn.readResourceMap());
}
}
return directoryMap.get(resource);
}
}

View File

@ -0,0 +1,98 @@
/*
* 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.provider;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.DnsChallenge;
import org.shredzone.acme4j.challenge.GenericChallenge;
import org.shredzone.acme4j.challenge.HttpChallenge;
import org.shredzone.acme4j.challenge.ProofOfPossessionChallenge;
import org.shredzone.acme4j.challenge.TlsSniChallenge;
/**
* Abstract implementation of {@link AcmeClientProvider}. It consists of a challenge
* registry and a standard {@link #openConnection(URI)} implementation.
*
* @author Richard "Shred" Körber
*/
public abstract class AbstractAcmeClientProvider implements AcmeClientProvider {
private static final int TIMEOUT = 10000;
private final Map<String, Class<? extends Challenge>> challenges = new HashMap<>();
public AbstractAcmeClientProvider() {
registerBaseChallenges();
}
@Override
@SuppressWarnings("unchecked")
public <T extends Challenge> T createChallenge(String type) {
Class<? extends Challenge> clazz = challenges.get(type);
if (clazz == null) {
return (T) new GenericChallenge();
}
try {
return (T) clazz.newInstance();
} catch (InstantiationException | IllegalAccessException ex) {
throw new IllegalArgumentException("Could not create Challenge for type "
+ type, ex);
}
}
@Override
public HttpURLConnection openConnection(URI uri) throws IOException {
HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection();
conn.setConnectTimeout(TIMEOUT);
conn.setReadTimeout(TIMEOUT);
conn.setUseCaches(false);
conn.setRequestProperty("User-Agent", "acme4j");
return conn;
}
/**
* Registers an individual {@link Challenge}. If a challenge of that type is already
* registered, it will be replaced.
*
* @param type
* Challenge type string
* @param clazz
* Class implementing the {@link Challenge}. It must have a default
* constructor.
*/
protected void registerChallenge(String type, Class<? extends Challenge> clazz) {
challenges.put(type, clazz);
}
/**
* Registers all standard challenges as specified in the ACME specifications.
* <p>
* Subclasses may override this method in order to add further challenges. It is
* invoked on construction time.
*/
protected void registerBaseChallenges() {
registerChallenge(DnsChallenge.TYPE, DnsChallenge.class);
registerChallenge(TlsSniChallenge.TYPE, TlsSniChallenge.class);
registerChallenge(ProofOfPossessionChallenge.TYPE, ProofOfPossessionChallenge.class);
registerChallenge(HttpChallenge.TYPE, HttpChallenge.class);
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.provider;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.ServiceLoader;
import org.shredzone.acme4j.AcmeClient;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.GenericChallenge;
/**
* An {@link AcmeClientProvider} creates {@link AcmeClient} instances to be used for
* communicating with the ACME server. Implementations handle individual features of each
* ACME server.
* <p>
* Provider implementations must be registered with Java's {@link ServiceLoader}.
*
* @author Richard "Shred" Körber
*/
public interface AcmeClientProvider {
/**
* Checks if this provider accepts the given server URI.
*
* @param serverUri
* Server URI to test
* @return {@code true} if this provider accepts the server URI, {@code false}
* otherwise
*/
boolean accepts(String serverUri);
/**
* Connects to an {@link AcmeClient} for communication with the ACME server.
*
* @param serverUri
* Server URI to connect to
* @return {@link AcmeClient} connected to the server
*/
AcmeClient connect(String serverUri);
/**
* Creates a {@link Challenge} instance that is able to respond to the challenge of
* the given type.
*
* @param type
* Challenge type name
* @return Matching {@link Challenge} instance
* @throws ClassCastException
* if the expected {@link Challenge} type does not match the given type
* name.
* @throws IllegalArgumentException
* if the given type name cannot be resolved to any {@link Challenge}
* class. However, for unknown challenge types, a {@link GenericChallenge}
* instance should be returned.
*/
<T extends Challenge> T createChallenge(String type);
/**
* Opens a {@link HttpURLConnection} to the given {@link URI}. Implementations may
* configure the connection, e.g. pin it to a concrete SSL certificate.
*
* @param uri
* {@link URI} to connect to
* @return {@link HttpURLConnection} connected to the {@link URI}
*/
HttpURLConnection openConnection(URI uri) throws IOException;
}

View File

@ -0,0 +1,51 @@
/*
* 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.provider;
import java.net.URI;
import java.net.URISyntaxException;
import org.shredzone.acme4j.AcmeClient;
import org.shredzone.acme4j.impl.GenericAcmeClient;
/**
* A generic {@link AcmeClientProvider}. It should be working for all ACME servers
* complying to the ACME specifications.
* <p>
* The {@code serverUri} is either a http or https URI to the server's directory service.
*
* @author Richard "Shred" Körber
*/
public class GenericAcmeClientProvider extends AbstractAcmeClientProvider {
@Override
public boolean accepts(String serverUri) {
return serverUri.startsWith("http://") || serverUri.startsWith("https://");
}
@Override
public AcmeClient connect(String serverUri) {
if (!accepts(serverUri)) {
throw new IllegalArgumentException("This provider does not accept " + serverUri);
}
try {
URI directoryUri = new URI(serverUri);
return new GenericAcmeClient(this, directoryUri);
} catch (URISyntaxException ex) {
throw new IllegalArgumentException(serverUri, ex);
}
}
}

View File

@ -0,0 +1,159 @@
/*
* 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.Key;
import java.util.Map;
import java.util.TreeMap;
import org.jose4j.base64url.Base64Url;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.connector.Resource;
/**
* Builder for claim structures.
* <p>
* Example:
* <pre>
* ClaimBuilder cb = new ClaimBuilder();
* cb.put("foo", 123).put("bar", "hello world");
* cb.object("sub").put("data", "subdata");
* cb.array("array", 123, 456, 789);
* </pre>
*
* @author Richard "Shred" Körber
*/
public class ClaimBuilder {
private final Map<String, Object> data = new TreeMap<>();
/**
* Puts a claim. If a claim with the key exists, it will be replaced.
*
* @param key
* Claim key
* @param value
* Claim value
* @return {@code this}
*/
public ClaimBuilder put(String key, Object value) {
data.put(key, value);
return this;
}
/**
* Puts a resource claim.
*
* @param resource
* Resource name
* @return {@code this}
*/
public ClaimBuilder putResource(String resource) {
return put("resource", resource);
}
/**
* Puts a resource claim.
*
* @param resource
* {@link Resource}
* @return {@code this}
*/
public ClaimBuilder putResource(Resource resource) {
return putResource(resource.path());
}
/**
* Puts an entire map into the claim.
*
* @param map
* Map to put
* @return {@code this}
*/
public ClaimBuilder putAll(Map<String, Object> map) {
data.putAll(map);
return this;
}
/**
* Puts binary data to the claim. The data is base64 url encoded.
*
* @param key
* Claim key
* @param data
* Claim data
* @return {@code this}
*/
public ClaimBuilder putBase64(String key, byte[] data) {
return put(key, Base64Url.encode(data));
}
/**
* Puts a {@link Key} into the claim. The key is serializied as JWK.
*
* @param key
* Claim key
* @param publickey
* {@link Key} to serialize
* @return {@code this}
*/
public ClaimBuilder putKey(String key, Key publickey) {
try {
final JsonWebKey jwk = JsonWebKey.Factory.newJwk(publickey);
Map<String, Object> jwkParams = jwk.toParams(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
object(key).putAll(jwkParams);
return this;
} catch (JoseException ex) {
throw new IllegalArgumentException("Invalid key", ex);
}
}
/**
* Creates a sub-claim for the given key.
*
* @param key
* Key of the sub-claim
* @return Newly created {@link ClaimBuilder} for the sub-claim.
*/
public ClaimBuilder object(String key) {
ClaimBuilder subBuilder = new ClaimBuilder();
data.put(key, subBuilder.data);
return subBuilder;
}
/**
* Puts an array claim.
*
* @param key
* Claim key
* @param values
* Array of claim values
* @return {@code this}
*/
public ClaimBuilder array(String key, Object... values) {
data.put(key, values);
return this;
}
/**
* Returns a JSON representation of the claims.
*/
@Override
public String toString() {
return JsonUtil.toJson(data);
}
}

View File

View File

@ -0,0 +1,2 @@
# Generic
org.shredzone.acme4j.provider.GenericAcmeClientProvider

View File

@ -0,0 +1 @@
This is the client part of the Java ACME client.

View File

@ -0,0 +1,6 @@
a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover {
background: none;
padding-right: 0;
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
*
* acme4j - ACME Java 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.
*
-->
<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="Home" href="index.html"/>
<item name="Project Management" href="http://acme4j.shredzone.org"/>
</links>
<breadcrumbs>
<item name="shredzone.org" href="http://www.shredzone.org"/>
<item name="acme4j" href="../index.html"/>
<item name="acme4j-client" href="index.html"/>
</breadcrumbs>
<menu name="Main">
<item name="Introduction" href="index.html"/>
<item name="Project Management" href="http://acme4j.shredzone.org"/>
</menu>
<menu ref="modules"/>
<menu ref="reports"/>
</body>
</project>

View File

View File

3
acme4j-example/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.key
*.crt
*.csr

23
acme4j-example/.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>acme4j-example</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>

55
acme4j-example/pom.xml Normal file
View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
*
* acme4j - ACME Java 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.
*
-->
<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>0.1-SNAPSHOT</version>
</parent>
<artifactId>acme4j-example</artifactId>
<name>acme4j example</name>
<description>Example for using acme4j</description>
<dependencies>
<dependency>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-letsencrypt</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-utils</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>

View File

View File

@ -0,0 +1,206 @@
/*
* 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;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.net.URI;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import javax.swing.JOptionPane;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.HttpChallenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.CSRBuilder;
import org.shredzone.acme4j.util.CertificateUtils;
import org.shredzone.acme4j.util.KeyPairUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A simple client test tool.
* <p>
* Pass the names of the domains as parameters.
*
* @author Richard "Shred" Körber
*/
public class ClientTest {
private static final String AGREEMENT_URL = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf";
private static final File USER_KEY_FILE = new File("user.key");
private static final File DOMAIN_KEY_FILE = new File("domain.key");
private static final File DOMAIN_CERT_FILE = new File("domain.crt");
private static final File DOMAIN_CSR_FILE = new File("domain.crt");
private static final int KEY_SIZE = 2048;
private static final Logger LOG = LoggerFactory.getLogger(ClientTest.class);
/**
* Generates a certificate for the given domains. Also takes care for the registration
* process.
*
* @param domains
* Domains to get a common certificate for
* @param agreementUrl
* Agreement URL to be used for creating an account
*/
public void fetchCertificate(Collection<String> domains, String agreementUrl)
throws IOException, AcmeException {
// Load or create a key pair for the user's account
KeyPair userKeyPair;
if (USER_KEY_FILE.exists()) {
try (FileReader fr = new FileReader(USER_KEY_FILE)) {
userKeyPair = KeyPairUtils.readKeyPair(fr);
}
} else {
userKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
try (FileWriter fw = new FileWriter(USER_KEY_FILE)) {
KeyPairUtils.writeKeyPair(userKeyPair, fw);
}
}
// Create an Account instance for the user
Account account = new Account(userKeyPair);
// Create an AcmeClient for Let's Encrypt
AcmeClient client = AcmeClientFactory.connect("acme://letsencrypt.org");
// Register a new user
Registration reg = new Registration();
reg.setAgreementUrl(agreementUrl);
try {
client.newRegistration(account, reg);
LOG.info("Registered a new user, URI: " + reg.getLocation());
} catch (AcmeException ex) {
LOG.warn("Registration failed", ex);
// Try to update the user's account, maybe there was a new agreement url?
try {
client.updateRegistration(account, reg);
LOG.info("Updated user, URI: " + reg.getLocation());
} catch (AcmeException ex2) {
LOG.warn("Registration update failed, too. Giving up!", ex2);
return;
}
}
for (String domain : domains) {
// Create a new authorization
Authorization auth = new Authorization();
auth.setDomain(domain);
client.newAuthorization(account, auth);
LOG.info("New authorization for domain " + domain);
// Find a single http-01 challenge
HttpChallenge challenge = auth.findChallenge(HttpChallenge.TYPE);
if (challenge == null) {
LOG.error("Found no " + HttpChallenge.TYPE + " challenge, don't know what to do...");
return;
}
// Authorize the challenge
challenge.authorize(account);
// Output the challenge, wait for acknowledge...
LOG.info("Please create a file in your web server's base directory.");
LOG.info("It must be reachable at: http://" + domain + "/.well-known/acme-challenge/" + challenge.getToken());
LOG.info("File name: " + challenge.getToken());
LOG.info("Content: " + challenge.getAuthorization());
LOG.info("The file must not contain any leading or trailing whitespaces or line breaks!");
LOG.info("If you're ready, dismiss the dialog...");
JOptionPane.showMessageDialog(null, "OK?");
// Trigger the challenge
client.triggerChallenge(account, challenge);
// Poll for the challenge to complete
int attempts = 10;
while (challenge.getStatus() != Challenge.Status.VALID && attempts-- > 0) {
if (challenge.getStatus() == Challenge.Status.INVALID) {
LOG.error("Challenge failed... Giving up.");
return;
}
try {
Thread.sleep(3000L);
} catch (InterruptedException ex) {
LOG.warn("interrupted", ex);
}
client.updateChallenge(account, challenge);
}
if (attempts == 0) {
LOG.error("Failed to pass the challenge... Giving up.");
return;
}
}
// Load or create a key pair for the domain
KeyPair domainKeyPair;
if (DOMAIN_KEY_FILE.exists()) {
try (FileReader fr = new FileReader(DOMAIN_KEY_FILE)) {
domainKeyPair = KeyPairUtils.readKeyPair(fr);
}
} else {
domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
try (FileWriter fw = new FileWriter(DOMAIN_KEY_FILE)) {
KeyPairUtils.writeKeyPair(domainKeyPair, fw);
}
}
// Generate a CSR for the domain
CSRBuilder csrb = new CSRBuilder();
csrb.addDomains(domains);
csrb.sign(domainKeyPair);
try (Writer out = new FileWriter(DOMAIN_CSR_FILE)) {
csrb.write(out);
}
// Request a signed certificate
URI certificateUri = client.requestCertificate(account, csrb.getEncoded());
LOG.info("Success! The certificate for domains " + domains + " has been generated!");
LOG.info("Certificate URI: " + certificateUri);
// Download the certificate
X509Certificate cert = client.downloadCertificate(certificateUri);
try (FileWriter fw = new FileWriter(DOMAIN_CERT_FILE)) {
CertificateUtils.writeX509Certificate(cert, fw);
}
}
public static void main(String... args) {
if (args.length == 0) {
System.err.println("Usage: ClientTest <domain>...");
System.exit(1);
}
LOG.info("Starting up...");
Collection<String> domains = Arrays.asList(args);
try {
ClientTest ct = new ClientTest();
ct.fetchCertificate(domains, AGREEMENT_URL);
} catch (Exception ex) {
LOG.error("Failed to get a certificate for domains " + domains, ex);
}
}
}

View File

View File

@ -0,0 +1,2 @@
org.slf4j.simpleLogger.log.org.shredzone.acme4j = debug

View File

@ -0,0 +1 @@
An example about how to use the Java ACME client.

View File

@ -0,0 +1,6 @@
a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover {
background: none;
padding-right: 0;
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
*
* acme4j - ACME Java 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.
*
-->
<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="Home" href="index.html"/>
<item name="Project Management" href="http://acme4j.shredzone.org"/>
</links>
<breadcrumbs>
<item name="shredzone.org" href="http://www.shredzone.org"/>
<item name="acme4j" href="../index.html"/>
<item name="acme4j-example" href="index.html"/>
</breadcrumbs>
<menu name="Main">
<item name="Introduction" href="index.html"/>
<item name="Project Management" href="http://acme4j.shredzone.org"/>
</menu>
<menu ref="modules"/>
<menu ref="reports"/>
</body>
</project>

View File

View File

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>acme4j-letsencrypt</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>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
*
* acme4j - ACME Java 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.
*
-->
<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>0.1-SNAPSHOT</version>
</parent>
<artifactId>acme4j-letsencrypt</artifactId>
<name>acme4j Let's Encrypt</name>
<description>Let's Encrypt service provider for acme4j</description>
<dependencies>
<dependency>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-client</artifactId>
<version>${parent.version}</version>
</dependency>
</dependencies>
</project>

View File

View File

@ -0,0 +1,117 @@
/*
* 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.provider;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import org.shredzone.acme4j.AcmeClient;
import org.shredzone.acme4j.impl.GenericAcmeClient;
/**
* An {@link AcmeClientProvider} for <em>Let's Encrypt</em>.
* <p>
* The {@code serverUri} is {@code "acme://letsencrypt.org"} for the production server, and
* {@code "acme://letsencrypt.org/staging"} for a testing server.
* <p>
* If you want to use <em>Let's Encrypt</em>, always prefer to use this provider, as it
* takes care for the correct connection and SSL certificates.
*
* @author Richard "Shred" Körber
* @see <a href="https://letsencrypt.org/">Let's Encrypt</a>
*/
public class LetsEncryptAcmeClientProvider extends AbstractAcmeClientProvider {
private static final String V01_DIRECTORY_URI = "https://acme-v01.api.letsencrypt.org/directory";
private static final String STAGING_DIRECTORY_URI = "https://acme-staging.api.letsencrypt.org/directory";
private SSLSocketFactory sslSocketFactory;
@Override
public boolean accepts(String serverUri) {
return serverUri.startsWith("acme://letsencrypt.org");
}
@Override
public AcmeClient connect(String serverUri) {
String directoryUri;
switch (serverUri) {
case "acme://letsencrypt.org/staging":
directoryUri = STAGING_DIRECTORY_URI;
break;
case "acme://letsencrypt.org/v01":
case "acme://letsencrypt.org":
directoryUri = V01_DIRECTORY_URI;
break;
default:
throw new IllegalArgumentException("Unknown URI " + serverUri);
}
try {
return new GenericAcmeClient(this, new URI(directoryUri));
} catch (URISyntaxException ex) {
throw new IllegalArgumentException(directoryUri, ex);
}
}
@Override
public HttpURLConnection openConnection(URI uri) throws IOException {
HttpURLConnection conn = super.openConnection(uri);
if (conn instanceof HttpsURLConnection) {
((HttpsURLConnection) conn).setSSLSocketFactory(createSocketFactory());
}
return conn;
}
/**
* Lazily creates an {@link SSLSocketFactory} that exclusively accepts the Let's
* Encrypt certificate.
*/
private SSLSocketFactory createSocketFactory() throws IOException {
if (sslSocketFactory == null) {
try {
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(getClass().getResourceAsStream("/org/shredzone/acme4j/letsencrypt.truststore"),
"acme4j".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keystore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
sslSocketFactory = ctx.getSocketFactory();
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException
| KeyManagementException ex) {
throw new IOException("Could not create truststore", ex);
}
}
return sslSocketFactory;
}
}

View File

View File

@ -0,0 +1,2 @@
# Let's Encrypt: https://letsencrypt.org
org.shredzone.acme4j.provider.LetsEncryptAcmeClientProvider

View File

@ -0,0 +1 @@
Let's Encrypt service provider for the Java ACME client.

View File

@ -0,0 +1,6 @@
a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover {
background: none;
padding-right: 0;
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
*
* acme4j - ACME Java 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.
*
-->
<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="Home" href="index.html"/>
<item name="Project Management" href="http://acme4j.shredzone.org"/>
</links>
<breadcrumbs>
<item name="shredzone.org" href="http://www.shredzone.org"/>
<item name="acme4j" href="../index.html"/>
<item name="acme4j-letsencrypt" href="index.html"/>
</breadcrumbs>
<menu name="Main">
<item name="Introduction" href="index.html"/>
<item name="Project Management" href="http://acme4j.shredzone.org"/>
</menu>
<menu ref="modules"/>
<menu ref="reports"/>
</body>
</project>

View File

View File

23
acme4j-utils/.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>acme4j-utils</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>

54
acme4j-utils/pom.xml Normal file
View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
*
* acme4j - ACME Java 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.
*
-->
<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>0.1-SNAPSHOT</version>
</parent>
<artifactId>acme4j-utils</artifactId>
<name>acme4j utils</name>
<description>acme4j utilities</description>
<dependencies>
<dependency>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpg-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcmail-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
</project>

0
acme4j-utils/src/main/java/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,212 @@
/*
* 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.io.IOException;
import java.io.Writer;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
/**
* Generator for a CSR (Certificate Signing Request) suitable for ACME servers.
* <p>
* Requires {@code Bouncy Castle}.
*
* @author Richard "Shred" Körber
*/
public class CSRBuilder {
private static final String SIGNATURE_ALG = "SHA256withRSA";
private final X500NameBuilder namebuilder = new X500NameBuilder(X500Name.getDefaultStyle());
private final List<String> namelist = new ArrayList<>();
private PKCS10CertificationRequest csr = null;
/**
* Adds a domain name to the CSR. The first domain name added will also be the
* <em>Common Name</em>. All domain names will be added as <em>Subject
* Alternative Name</em>.
* <p>
* Note that ACME servers may not accept wildcard domains!
*/
public void addDomain(String domain) {
if (namelist.isEmpty()) {
namebuilder.addRDN(BCStyle.CN, domain);
}
namelist.add(domain);
}
/**
* Adds a {@link Collection} of domains.
*/
public void addDomains(Collection<String> domains) {
for (String domain : domains) {
addDomain(domain);
}
}
/**
* Adds multiple domain names.
*/
public void addDomains(String... domains) {
for (String domain : domains) {
addDomain(domain);
}
}
/**
* Sets the organization.
* <p>
* Note that it is at the discretion of the ACME server to accept this parameter.
*/
public void setOrganization(String o) {
namebuilder.addRDN(BCStyle.O, o);
}
/**
* Sets the organizational unit.
* <p>
* Note that it is at the discretion of the ACME server to accept this parameter.
*/
public void setOrganizationalUnit(String ou) {
namebuilder.addRDN(BCStyle.OU, ou);
}
/**
* Sets the city or locality.
* <p>
* Note that it is at the discretion of the ACME server to accept this parameter.
*/
public void setLocality(String l) {
namebuilder.addRDN(BCStyle.L, l);
}
/**
* Sets the state or province.
* <p>
* Note that it is at the discretion of the ACME server to accept this parameter.
*/
public void setState(String st) {
namebuilder.addRDN(BCStyle.ST, st);
}
/**
* Sets the country.
* <p>
* Note that it is at the discretion of the ACME server to accept this parameter.
*/
public void setCountry(String c) {
namebuilder.addRDN(BCStyle.C, c);
}
/**
* Signs the completed CSR.
*
* @param keypair
* {@link KeyPair} to sign the CSR with
*/
public void sign(KeyPair keypair) throws IOException {
if (namelist.isEmpty()) {
throw new IllegalStateException("No domain was set");
}
if (keypair == null) {
throw new IllegalArgumentException("keypair must not be null");
}
try {
GeneralName[] gns = new GeneralName[namelist.size()];
for (int ix = 0; ix < namelist.size(); ix++) {
gns[ix] = new GeneralName(GeneralName.dNSName, namelist.get(ix));
}
GeneralNames subjectAltName = new GeneralNames(gns);
PKCS10CertificationRequestBuilder p10Builder =
new JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic());
ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator();
extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltName);
p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate());
JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(SIGNATURE_ALG);
ContentSigner signer = csBuilder.build(keypair.getPrivate());
csr = p10Builder.build(signer);
} catch (OperatorCreationException ex) {
throw new IOException("Could not generate CSR", ex);
}
}
/**
* Gets the PKCS#10 certification request.
*/
public PKCS10CertificationRequest getCSR() {
if (csr == null) {
throw new IllegalStateException("sign CSR first");
}
return csr;
}
/**
* Gets an encoded PKCS#10 certification request.
*/
public byte[] getEncoded() throws IOException {
return getCSR().getEncoded();
}
/**
* Writes the signed certificate request to a file.
*
* @param w
* {@link Writer} to write the PEM file to
*/
public void write(Writer w) throws IOException {
if (csr == null) {
throw new IllegalStateException("sign CSR first");
}
try (PemWriter pw = new PemWriter(w)) {
pw.writeObject(new PemObject("CERTIFICATE REQUEST", getEncoded()));
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(namebuilder.build());
for (String domain : namelist) {
sb.append(",DNS=").append(domain.toString());
}
return sb.toString();
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.io.IOException;
import java.io.Writer;
import java.security.cert.X509Certificate;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
/**
* Utility class offering convenience methods for certificates.
* <p>
* Requires {@code Bouncy Castle}.
*
* @author Richard "Shred" Körber
*/
public final class CertificateUtils {
private CertificateUtils() {
// utility class without constructor
}
/**
* Writes an X.509 certificate PEM file.
*
* @param cert
* {@link X509Certificate} to write
* @param w
* {@link Writer} to write the PEM file to
*/
public static void writeX509Certificate(X509Certificate cert, Writer w) throws IOException {
try (JcaPEMWriter jw = new JcaPEMWriter(w)) {
jw.writeObject(cert);
}
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.openssl.PEMException;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
/**
* Utility class offering convenience methods for {@link KeyPair}.
* <p>
* Requires {@code Bouncy Castle}.
*
* @author Richard "Shred" Körber
*/
public class KeyPairUtils {
private KeyPairUtils() {
// utility class without constructor
}
/**
* Creates a new RSA {@link KeyPair}.
*
* @param keysize
* Key size
* @return Generated {@link KeyPair}
*/
public static KeyPair createKeyPair(int keysize) {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(keysize);
return keyGen.generateKeyPair();
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException(ex);
}
}
/**
* Reads a {@link KeyPair} from a PEM file.
*
* @param r
* {@link Reader} to read the PEM file from
* @return {@link KeyPair} read
*/
public static KeyPair readKeyPair(Reader r) throws IOException {
try (PEMParser parser = new PEMParser(r)) {
PEMKeyPair keyPair = (PEMKeyPair) parser.readObject();
return new JcaPEMKeyConverter().getKeyPair(keyPair);
} catch (PEMException ex) {
throw new IOException("Invalid PEM file", ex);
}
}
/**
* Writes a {@link KeyPair} PEM file.
*
* @param keypair
* {@link KeyPair} to write
* @param w
* {@link Writer} to write the PEM file to
*/
public static void writeKeyPair(KeyPair keypair, Writer w) throws IOException {
try (JcaPEMWriter jw = new JcaPEMWriter(w)) {
jw.writeObject(keypair);
}
}
}

View File

View File

@ -0,0 +1 @@
Some utility classes for using the Java ACME client.

View File

@ -0,0 +1,6 @@
a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover {
background: none;
padding-right: 0;
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
*
* acme4j - ACME Java 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.
*
-->
<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="Home" href="index.html"/>
<item name="Project Management" href="http://acme4j.shredzone.org"/>
</links>
<breadcrumbs>
<item name="shredzone.org" href="http://www.shredzone.org"/>
<item name="acme4j" href="../index.html"/>
<item name="acme4j-utils" href="index.html"/>
</breadcrumbs>
<menu name="Main">
<item name="Introduction" href="index.html"/>
<item name="Project Management" href="http://acme4j.shredzone.org"/>
</menu>
<menu ref="modules"/>
<menu ref="reports"/>
</body>
</project>

0
acme4j-utils/src/test/java/.gitignore vendored Normal file
View File

View File

168
pom.xml Normal file
View File

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
*
* acme4j - ACME Java 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.
*
-->
<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>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j</artifactId>
<version>0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>acme4j</name>
<description>ACME client for Java</description>
<url>http://acme4j.shredzone.org</url>
<inceptionYear>2015</inceptionYear>
<licenses>
<license>
<name>Apache License Version 2.0</name>
<url>LICENSE-APL.txt</url>
</license>
</licenses>
<scm>
<url>scm:git:git@github.com:shred/acme4j.git</url>
<connection>scm:git:git@github.com:shred/acme4j.git</connection>
<developerConnection>scm:git:git@github.com:shred/acme4j.git</developerConnection>
<tag>HEAD</tag>
</scm>
<issueManagement>
<system>GitHub</system>
<url>https://github.com/shred/acme4j/issues</url>
</issueManagement>
<developers>
<developer>
<id>shred</id>
<name>Richard Körber</name>
</developer>
</developers>
<properties>
<bouncycastle.version>1.52</bouncycastle.version>
<jose4j.version>0.4.4</jose4j.version>
<slf4j.version>1.7.10</slf4j.version>
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
</properties>
<modules>
<module>acme4j-client</module>
<module>acme4j-letsencrypt</module>
<module>acme4j-utils</module>
<module>acme4j-example</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<excludes>
<exclude>**/.gitignore</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.3</version>
<configuration>
<outputEncoding>UTF-8</outputEncoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>2.5</version>
<configuration>
<autoVersionSubmodules>true</autoVersionSubmodules>
<tagNameFormat>v@{project.version}</tagNameFormat>
<pushChanges>false</pushChanges>
<localCheckout>true</localCheckout>
</configuration>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>2.7</version>
<reportSets>
<reportSet>
<reports>
<report>dependencies</report>
<report>issue-tracking</report>
<report>license</report>
<report>summary</report>
<report>project-team</report>
<report>scm</report>
</reports>
</reportSet>
</reportSets>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.17</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<configuration>
<links>
<link>http://docs.oracle.com/javase/7/docs/api/</link>
<link>http://docs.oracle.com/javaee/7/api/</link>
</links>
<charset>UTF-8</charset>
<linksource>true</linksource>
</configuration>
</plugin>
</plugins>
</reporting>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>[4,)</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>[1.3,)</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>[1.9.5,)</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

1
src/site/apt/index.apt Normal file
View File

@ -0,0 +1 @@
This is a Java ACME client.

View File

@ -0,0 +1,6 @@
a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover {
background: none;
padding-right: 0;
}

37
src/site/site.xml Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
*
* acme4j - ACME Java 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.
*
-->
<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="Home" href="index.html"/>
<item name="Project Management" href="http://acme4j.shredzone.org"/>
</links>
<breadcrumbs>
<item name="shredzone.org" href="http://www.shredzone.org"/>
<item name="acme4j" href="index.html"/>
</breadcrumbs>
<menu name="Main">
<item name="Introduction" href="index.html"/>
<item name="Project Management" href="http://acme4j.shredzone.org"/>
</menu>
<menu ref="modules"/>
<menu ref="reports"/>
</body>
</project>