mirror of https://github.com/shred/acme4j
Project start
parent
df44e2b80a
commit
7e07a0e2e4
6
.project
6
.project
|
@ -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>
|
||||
|
|
39
README.md
39
README.md
|
@ -1,7 +1,5 @@
|
|||
# ACME Java Client 
|
||||
|
||||
*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!
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# Generic
|
||||
org.shredzone.acme4j.provider.GenericAcmeClientProvider
|
|
@ -0,0 +1 @@
|
|||
This is the client part of the Java ACME client.
|
|
@ -0,0 +1,6 @@
|
|||
a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover {
|
||||
background: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
*.key
|
||||
*.crt
|
||||
*.csr
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
org.slf4j.simpleLogger.log.org.shredzone.acme4j = debug
|
|
@ -0,0 +1 @@
|
|||
An example about how to use the Java ACME client.
|
|
@ -0,0 +1,6 @@
|
|||
a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover {
|
||||
background: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# Let's Encrypt: https://letsencrypt.org
|
||||
org.shredzone.acme4j.provider.LetsEncryptAcmeClientProvider
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
Let's Encrypt service provider for the Java ACME client.
|
|
@ -0,0 +1,6 @@
|
|||
a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover {
|
||||
background: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Some utility classes for using the Java ACME client.
|
|
@ -0,0 +1,6 @@
|
|||
a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover {
|
||||
background: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
|
@ -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,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>
|
|
@ -0,0 +1 @@
|
|||
This is a Java ACME client.
|
|
@ -0,0 +1,6 @@
|
|||
a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover {
|
||||
background: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
Loading…
Reference in New Issue