diff --git a/acme4j-example/src/main/java/org/shredzone/acme4j/example/SSLClientWithEabTest.java b/acme4j-example/src/main/java/org/shredzone/acme4j/example/SSLClientWithEabTest.java new file mode 100644 index 00000000..11dd5ed5 --- /dev/null +++ b/acme4j-example/src/main/java/org/shredzone/acme4j/example/SSLClientWithEabTest.java @@ -0,0 +1,456 @@ +/* + * 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.example; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.shredzone.acme4j.*; +import org.shredzone.acme4j.challenge.Challenge; +import org.shredzone.acme4j.challenge.Dns01Challenge; +import org.shredzone.acme4j.challenge.Http01Challenge; +import org.shredzone.acme4j.exception.AcmeException; +import org.shredzone.acme4j.util.KeyPairUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.security.KeyPair; +import java.security.Security; +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; + +/** + * A simple client test tool. + *
+ * Pass the names of the domains as parameters.
+ */
+public class SSLClientWithEabTest {
+ // File name of the User Key Pair
+ private static final File USER_KEY_FILE = new File("user.key");
+
+ // File name of the Domain Key Pair
+ private static final File DOMAIN_KEY_FILE = new File("domain.key");
+
+ // File name of the signed certificate
+ private static final File DOMAIN_CHAIN_FILE = new File("domain-chain.crt");
+
+ //Challenge type to be used
+ private static final ChallengeType CHALLENGE_TYPE = ChallengeType.HTTP;
+
+ // RSA key size of generated key pairs
+ private static final int KEY_SIZE = 2048;
+
+ private static final Logger LOG = LoggerFactory.getLogger(SSLClientWithEabTest.class);
+
+ private enum ChallengeType {HTTP, DNS}
+
+ /**
+ * Generates a certificate for the given domains. Also takes care for the registration
+ * process.
+ *
+ * @param domains
+ * Domains to get a common certificate for
+ * @param eabKid
+ * Value of --eab-kid
+ * @param eabHmacKey
+ * Value of --eab-hmac-key
+ * @param emailAddress
+ * Email address of account that owns the key information
+ */
+ public void fetchCertificate(Collection
+ * Keep this key pair in a safe place! In a production environment, you will not be
+ * able to access your account again if you should lose the key pair.
+ *
+ * @return User's {@link KeyPair}.
+ */
+ private KeyPair loadOrCreateUserKeyPair() throws IOException {
+ if (USER_KEY_FILE.exists()) {
+ // If there is a key file, read it
+ try (FileReader fr = new FileReader(USER_KEY_FILE)) {
+ return KeyPairUtils.readKeyPair(fr);
+ }
+
+ } else {
+ // If there is none, create a new key pair and save it
+ KeyPair userKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
+ try (FileWriter fw = new FileWriter(USER_KEY_FILE)) {
+ KeyPairUtils.writeKeyPair(userKeyPair, fw);
+ }
+ return userKeyPair;
+ }
+ }
+
+ /**
+ * Loads a domain key pair from {@link #DOMAIN_KEY_FILE}. If the file does not exist,
+ * a new key pair is generated and saved.
+ *
+ * @return Domain {@link KeyPair}.
+ */
+ private KeyPair loadOrCreateDomainKeyPair() throws IOException {
+ if (DOMAIN_KEY_FILE.exists()) {
+ try (FileReader fr = new FileReader(DOMAIN_KEY_FILE)) {
+ return KeyPairUtils.readKeyPair(fr);
+ }
+ } else {
+ KeyPair domainKeyPair = KeyPairUtils.createKeyPair();
+ try (FileWriter fw = new FileWriter(DOMAIN_KEY_FILE)) {
+ KeyPairUtils.writeKeyPair(domainKeyPair, fw);
+ }
+ return domainKeyPair;
+ }
+ }
+
+ /**
+ * Finds your {@link Account} at the ACME server. It will be found by your user's
+ * public key. If your key is not known to the server yet, a new account will be
+ * created.
+ *
+ * This is a simple way of finding your {@link Account}. A better way is to get the
+ * URL of your new account with {@link Account#getLocation()} and store it somewhere.
+ * If you need to get access to your account later, reconnect to it via {@link
+ * Session#login(URL, KeyPair)} by using the stored location.
+ *
+ * @param session
+ * {@link Session} to bind with
+ * @param eabKid
+ * Value of --eab-kid
+ * @param eabHmacKey
+ * Value of --eab-hmac-key
+ * @param emailAddress
+ * Email address of account that owns the key information
+ * @return {@link Account}
+ */
+ private Account findOrRegisterAccount(Session session, KeyPair accountKey, String eabKid, String eabHmacKey, String emailAddress) throws AcmeException {
+ // Ask the user to accept the TOS, if server provides us with a link.
+ Optional
+ * The verification of this challenge expects a file with a certain content to be
+ * reachable at a given path under the domain to be tested.
+ *
+ * This example outputs instructions that need to be executed manually. In a
+ * production environment, you would rather generate this file automatically, or maybe
+ * use a servlet that returns {@link Http01Challenge#getAuthorization()}.
+ *
+ * @param auth
+ * {@link Authorization} to find the challenge in
+ * @return {@link Challenge} to verify
+ */
+ public Challenge httpChallenge(Authorization auth) throws AcmeException {
+ // Find a single http-01 challenge
+ Http01Challenge challenge = auth.findChallenge(Http01Challenge.class)
+ .orElseThrow(() -> new AcmeException("Found no " + Http01Challenge.TYPE
+ + " challenge, don't know what to do..."));
+
+ // 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://{}/.well-known/acme-challenge/{}",
+ auth.getIdentifier().getDomain(), 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...");
+
+ StringBuilder message = new StringBuilder();
+ message.append("Please create a file in your web server's base directory.\n\n");
+ message.append("http://")
+ .append(auth.getIdentifier().getDomain())
+ .append("/.well-known/acme-challenge/")
+ .append(challenge.getToken())
+ .append("\n\n");
+ message.append("Content:\n\n");
+ message.append(challenge.getAuthorization());
+ acceptChallenge(message.toString());
+
+ return challenge;
+ }
+
+ /**
+ * Prepares a DNS challenge.
+ *
+ * The verification of this challenge expects a TXT record with a certain content.
+ *
+ * This example outputs instructions that need to be executed manually. In a
+ * production environment, you would rather configure your DNS automatically.
+ *
+ * @param auth
+ * {@link Authorization} to find the challenge in
+ * @return {@link Challenge} to verify
+ */
+ public Challenge dnsChallenge(Authorization auth) throws AcmeException {
+ // Find a single dns-01 challenge
+ Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE)
+ .map(Dns01Challenge.class::cast)
+ .orElseThrow(() -> new AcmeException("Found no " + Dns01Challenge.TYPE
+ + " challenge, don't know what to do..."));
+
+ // Output the challenge, wait for acknowledge...
+ LOG.info("Please create a TXT record:");
+ LOG.info("{} IN TXT {}",
+ Dns01Challenge.toRRName(auth.getIdentifier()), challenge.getDigest());
+ LOG.info("If you're ready, dismiss the dialog...");
+
+ StringBuilder message = new StringBuilder();
+ message.append("Please create a TXT record:\n\n");
+ message.append(Dns01Challenge.toRRName(auth.getIdentifier()))
+ .append(" IN TXT ")
+ .append(challenge.getDigest());
+ acceptChallenge(message.toString());
+
+ return challenge;
+ }
+
+ /**
+ * Presents the instructions for preparing the challenge validation, and waits for
+ * dismissal. If the user cancelled the dialog, an exception is thrown.
+ *
+ * @param message
+ * Instructions to be shown in the dialog
+ */
+ public void acceptChallenge(String message) throws AcmeException {
+ int option = JOptionPane.showConfirmDialog(null,
+ message,
+ "Prepare Challenge",
+ JOptionPane.OK_CANCEL_OPTION);
+ if (option == JOptionPane.CANCEL_OPTION) {
+ throw new AcmeException("User cancelled the challenge");
+ }
+ }
+
+ /**
+ * Presents the instructions for removing the challenge validation, and waits for
+ * dismissal.
+ *
+ * @param message
+ * Instructions to be shown in the dialog
+ */
+ public void completeChallenge(String message) {
+ JOptionPane.showMessageDialog(null,
+ message,
+ "Complete Challenge",
+ JOptionPane.INFORMATION_MESSAGE);
+ }
+
+ /**
+ * Presents the user a link to the Terms of Service, and asks for confirmation. If the
+ * user denies confirmation, an exception is thrown.
+ *
+ * @param agreement
+ * {@link URI} of the Terms of Service
+ */
+ public void acceptAgreement(URI agreement) throws AcmeException {
+ int option = JOptionPane.showConfirmDialog(null,
+ "Do you accept the Terms of Service?\n\n" + agreement,
+ "Accept ToS",
+ JOptionPane.YES_NO_OPTION);
+ if (option == JOptionPane.NO_OPTION) {
+ throw new AcmeException("User did not accept Terms of Service");
+ }
+ }
+
+ /**
+ * Invokes this example.
+ *
+ * @param args
+ * Domains to get a certificate for
+ */
+ public static void main(String... args) {
+ if (args.length == 0) {
+ System.err.println("Usage: ClientTest