Acme4J教程

时间:2020-01-09 10:35:40  来源:igfitidea点击:

Acme4j是一个Java工具包,使我们能够自动创建免费的TLS / SSL证书。 Acme4J通过与Let's Encrypt证书颁发机构(CA)进行通信来获取TLS / SSL证书。与手动更新证书相比,自动生成证书是一个很大的优势。自动化后,我们可以节省时间,并且可以更频繁地更改证书,从而降低了证书被泄露的风险。

我们可以通过Acme4J GitHub存储库找到Acme4J。

Acme4J Maven依赖关系

本教程中的Java示例是使用Java 8,Acme4J Client v.2.1和Acme4J Utils v.0.3创建的。以下是示例中使用的工具箱版本的Maven依赖关系:

<dependency>
    <groupId>org.shredzone.acme4j</groupId>
    <artifactId>acme4j-client</artifactId>
    <version>2.1</version>
</dependency>

<dependency>
    <groupId>org.shredzone.acme4j</groupId>
    <artifactId>acme4j-utils</artifactId>
    <version>0.3</version>
</dependency>

Acme4J阶段

使用Acme4J从Let's Encrypt获取证书需要以下阶段。

  • 为"加密"帐户创建一个私钥。
  • 使用上一阶段中生成的私钥创建一个"让我们加密"帐户。
  • 创建一个证书订单并将其发送到Acme4J,以获得证书。

这些阶段中的每个阶段都可以在时间上彼此分离地执行。实际上,阶段1和2通常只执行一次。对每个所需的新证书重复阶段3.

阶段1:为我们加密帐户创建私钥

在Acme4J中使用"让我们加密"的第一步是为"让我们加密"帐户创建私钥。这是在我们实际创建帐户之前完成的。首先,创建私钥,然后使用该私钥创建"加密"帐户。

这是一个Java类,能够创建与Acme4J一起使用的私钥:

import org.shredzone.acme4j.util.KeyPairUtils;

import java.io.FileWriter;
import java.io.IOException;
import java.security.KeyPair;

public class AccountKeyPairCreation {

    private String certificateFilePath =  null;

    public AccountKeyPairCreation(String certificateFilePath) {
        this.certificateFilePath = certificateFilePath;
    }

    public void execute() throws IOException {
        // Create a private / public key pair to attach to your CA account.
        String keyPairFile = "data/jenkov-com-ca-account-key-pair.pem";
        KeyPair accountKeyPair = createAndSaveKeyPair(keyPairFile);

    }

    private KeyPair createAndSaveKeyPair(String keyPairFile) throws IOException {
        KeyPair accountKeyPair = KeyPairUtils.createKeyPair(2048);

        // Write key to disk, so it can be reused another time.
        try (FileWriter fw = new FileWriter(keyPairFile)) {
            KeyPairUtils.writeKeyPair(accountKeyPair, fw);
        }
        return accountKeyPair;
    }
}

创建私钥的是execute()方法。私钥存储在文件中,以备后用。

我们应该保留此私钥。每当我们与Let's Encrypt帐户进行互动时,都将需要它,因此请不要丢失它。另外,请勿让任何人访问它。有权访问我们私钥的任何人都可以代表我们与"让我们加密"互动!

阶段2:创建一个"让我们加密"帐户

为Let's Encrypt帐户创建私钥后,需要创建一个Let's Encrypt帐户。

帐户创建过程的结果是,我们从"加密"中获取了一个帐户URL。我们需要存储此URL供以后使用。将来在订购证书时使用帐户URL。

这是一个可以创建"让我们加密"帐户的类:

import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.AccountBuilder;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.KeyPairUtils;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.security.KeyPair;

public class AccountCreation {

    private String accountKeyPairFilePath = null;
    private String accountUrlFilePath = null;
    private String letsEncryptUrl = null;

    public AccountCreation accountKeyPairFilePath(String filePath){
        this.accountKeyPairFilePath = filePath;
        return this;
    }

    public AccountCreation accountUrlFilePath(String filePath){
        this.accountUrlFilePath = filePath;
        return this;
    }

    public AccountCreation letsEncryptUrl(String url){
        this.letsEncryptUrl = url;
        return this;
    }

    public void execute() throws IOException, AcmeException {
        KeyPair keyPair = KeyPairUtils.readKeyPair(new FileReader(this.accountKeyPairFilePath));

        Session session = new Session(this.letsEncryptUrl);;
        Account account = new AccountBuilder()
                //.onlyExisting()
                .useKeyPair(keyPair)
                .agreeToTermsOfService()
                .create(session)
                ;

        URL accountLocationUrl = account.getLocation();
        try(FileWriter fileWriter = new FileWriter(this.accountUrlFilePath)){
            fileWriter.write(accountLocationUrl.toString());
        }
    }
}

execute()方法启动了帐户创建过程。该班级需要3项投入才能完成工作:

  • 包含"让我们加密"私钥的文件的路径。
  • 用于存储返回的帐户URL的路径。
  • "让我们加密" API端点的URL。

这是使用上述类的示例:

import com.jenkov.acme4j.commands.AccountCreation;
import org.shredzone.acme4j.exception.AcmeException;

import java.io.IOException;

public class AcmePhase2 {

    /*
        ACME phase 2 is the creation of an account with the CA.
     */
    public static void main(String[] args) throws IOException, AcmeException {

        AccountCreation accountCreation = new AccountCreation()
                .accountKeyPairFilePath ("le-account/account-private-key.pem")
                .accountUrlFilePath     ("le-account/account-url.txt")
                .letsEncryptUrl         ("acme://letsencrypt.org/staging")
                ;

        accountCreation.execute();

    }
}

阶段3:创建证书

阶段3是Acme4J阶段中最复杂的阶段。第三阶段本身包含多个较小的步骤。步骤在这里列出:

  • 创建一个Order对象并对其进行配置。
  • 处理返回的授权。
  • 下载证书

以下各节将更详细地说明这些步骤。

创建订单对象

阶段3由代码中的以下步骤组成:

Order order = account.newOrder()
            .domains(domains)
            .create();

    for (Authorization auth : order.getAuthorizations()) {
        if (auth.getStatus() != Status.VALID) {
            processAuth(auth);
        }
    }

    createCertificateSigningRequest(order, domains);
    downloadCertificate(order);

阶段3的第一部分包括创建一个代表证书订单的"订单"对象。 Order类的完全限定名称为org.shredzone.acme4j.Order。

调用create()导致将请求发送到Let's Encrypt。响应是一组授权,我们必须对其进行处理才能获得颁发的证书。

流程授权

一旦调用了order.create()方法,请求就会发送到Let's Encrypt。让我们的Encrypt响应我们需要处理的一组授权,以获取请求的证书。授权会验证"加密",即我们实际上拥有我们要为其申请证书的域。

授权有两种主要类型:

  • HTTP挑战
  • DNS挑战

HTTP挑战包括"让我们加密"(Let's Encrypt),它为我们提供了一些需要上传到托管给定域的Web服务器的数据。然后,让我们加密将下载该数据,如果成功,则将其视为我们拥有(或者至少管理)给定域的确认。

DNS挑战的工作原理与此类似,但需要对DNS进行一些配置。然后,让我们加密将检查此配置,并将其用作我们拥有域的验证。在本教程中,我将不讨论DNS挑战选项,而仅讨论HTTP挑战选项。

证书顺序中每个域将有一个授权。我们可以按以下方式获取和处理授权:

for (Authorization auth : order.getAuthorizations()) {
    if (auth.getStatus() != Status.VALID) {
        processAuth(auth);
    }
}

处理授权是在processAuth(auth)方法调用中完成的。该方法的外观如下:

private void processAuth(Authorization auth) throws AcmeException, InterruptedException {

    Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);

    String fileName    = challenge.getToken());
    String fileContent = challenge.getAuthorization();
    String domain      = challenge.getDomain();

    challenge.trigger();

    while (auth.getStatus() != Status.VALID) {
        Thread.sleep(3000L);
        auth.update();
    }
}

在" Authorization"对象(" auth"参数)内,存在一个或者多个挑战,其中一个必须满足。

上面的示例专门寻找一个HTTP授权对象,然后通过调用challenge.trigger()来触发该对象。在触发质询之前,我们必须从challenge.getAuthorization()中读取值,并将其以文件形式上传到Web服务器。该值必须可用的URL是:

http://${domain}/.well-known/acme-challenge/${token}

...其中$$ domain是我们要为其请求域名的域名(由challenge.getDomain()返回),而$ {token}是由challenge.getToken返回的值( )`。

在上面的示例中," $ {domain}"的值存储在变量" domain"中," $ {token}"的值存储在" fileName"变量中,并且在该URL上找到的文件内容应是fileContent变量的内容。

如果代码在调用challenge.trigger()之前无法访问Web服务器上传挑战文件,则可以在调试器中运行代码,在调用challenge.trigger()之前设置一个断点,然后在该处暂停代码断点,从质询对象中读取令牌(文件名)和授权(文件内容),将其上载到Web服务器,然后在调试器中继续执行代码。

触发挑战后,我们需要等待,直到让我们加密验证了挑战。那就是在processAuth()方法的最后一部分中发生的事情。更具体地说,正是此块等待验证被验证为止:

while (auth.getStatus() != Status.VALID) {
        Thread.sleep(3000L);
        auth.update();
    }

创建证书签名请求

第三阶段的第二部分是创建实际的证书签名请求。看起来是这样的:

private void createCertificateSigningRequest(Order order, String ... domains) throws Exception {
    KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048);

    CSRBuilder csrb = new CSRBuilder();
    for(String domain : domains){
        csrb.addDomain(domain);
    }
    csrb.setOrganization(this.organization);
    csrb.sign(domainKeyPair);
    byte[] csr = csrb.getEncoded();

    csrb.write(new FileWriter(this.certificateSigningRequestFilePath));

    order.execute(csr);
}

首先,生成一个域公钥/私钥对。该密钥对与我们的"加密"帐户使用的密钥对不同。

其次,添加我们想要证书的所有域。

第三,我们将组织(组织)设置为证书签名请求。

第四,我们使用域密钥对对证书签名请求进行签名。

第五,将证书签名请求写到磁盘上只是为了供将来参考。

最后,我们执行订单。这会将证书签名请求发送到"让我们加密"。

下载证书

阶段3的第3部分是在执行证书签名命令之后下载证书。这是代码中的样子:

private void downloadCertificate(Order order) throws InterruptedException, AcmeException, IOException {
    while (order.getStatus() != Status.VALID) {
        Thread.sleep(3000L);
        order.update();
    }

    Certificate cert = order.getCertificate();

    try (FileWriter fw = new FileWriter(this.certificateFilePath)) {
        cert.writeCertificate(fw);
    }
}

首先,上面的代码一直等到" Order"对象的状态为" Status.VALID"。

其次,代码从"订单"对象获取新发行的证书。

第三,将证书写入文件。

完整代码示例

这是有关如何实现第3阶段的完整Java代码示例:

import org.shredzone.acme4j.*;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.CSRBuilder;
import org.shredzone.acme4j.util.KeyPairUtils;

import java.io.FileWriter;
import java.io.IOException;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.List;

public class OrderCertificate {

    public static void orderCertificate(Account account, Instant validUntil,
          String ... domains)
    throws AcmeException, InterruptedException, IOException {
        Order order = account.newOrder()
                .domains(domains)
                .notAfter(validUntil)
                .create();

        for (Authorization auth : order.getAuthorizations()) {
            if (auth.getStatus() != Status.VALID) {
                processAuth(auth);
            }
        }

        createCertificateSigningRequest(order, domains);

        downloadCertificate(order);
    }

    private static void downloadCertificate(Order order)
    throws InterruptedException, AcmeException, IOException {
        while (order.getStatus() != Status.VALID) {
            Thread.sleep(3000L);
            order.update();
        }

        Certificate cert = order.getCertificate();

        try (FileWriter fw = new FileWriter("jenkov-com-cert-chain.crt")) {
            cert.writeCertificate(fw);
        }
    }

    private static void createCertificateSigningRequest(Order order, String ... domains)
    throws IOException, AcmeException {
        KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048);

        CSRBuilder csrb = new CSRBuilder();
        for(String domain : domains){
            csrb.addDomain(domain);
    }
        csrb.setOrganization("Jenkov Aps");
        csrb.sign(domainKeyPair);
        byte[] csr = csrb.getEncoded();

        csrb.write(new FileWriter("example.csr"));

        order.execute(csr);
    }

    private static void processAuth(Authorization auth) throws AcmeException,
            InterruptedException {
        Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);

        challenge.trigger();

        while (auth.getStatus() != Status.VALID) {
            Thread.sleep(3000L);
            auth.update();
        }
    }

}

使用签发的证书

一旦下载了新发行的SSL / TLS证书,就需要使用它。确切的使用方式取决于我们使用的Web服务器。