Acme4J教程
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服务器。