Improve documentation

- Rearranged all chapters. It makes content easier to find, as it is not
  buried in unrelated information now.
- Reviewed the content.
- Fixed broken links.
- Added documentation about Renewal Information and Exceptions
pull/147/head
Richard Körber 2023-11-24 11:00:29 +01:00
parent e3cc271cd8
commit f9b3242f4c
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
17 changed files with 637 additions and 498 deletions

View File

@ -16,7 +16,7 @@ It is an independent open source implementation that is not affiliated with or e
* Supports [RFC 8738](https://tools.ietf.org/html/rfc8738) IP identifier validation
* Supports [RFC 8739](https://tools.ietf.org/html/rfc8739) short-term automatic certificate renewal (experimental)
* Supports [RFC 8823](https://tools.ietf.org/html/rfc8823) for S/MIME certificates (experimental)
* Supports [draft-ietf-acme-ari-01](https://www.ietf.org/id/draft-ietf-acme-ari-01.html) for renewal information
* Supports [draft-ietf-acme-ari-01](https://www.ietf.org/archive/id/draft-ietf-acme-ari-01.html) for renewal information
* Easy to use Java API
* Requires JRE 11 or higher
* Built with maven, packages available at [Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.shredzone.acme4j%22)

View File

@ -9,4 +9,4 @@ Web site: [Let's Encrypt](https://letsencrypt.org)
## Note
* Java 8u101 or higher is required for connecting to the _Let's Encrypt_ servers.
* Let's Encrypt does not support `Account.getOrders()`. Invocation will throw an `AcmeNotSupportedException`.

View File

@ -20,7 +20,7 @@ Latest version: ![maven central](https://shredzone.org/maven-central/org.shredzo
* Supports [RFC 8738](https://tools.ietf.org/html/rfc8738) IP identifier validation
* Supports [RFC 8739](https://tools.ietf.org/html/rfc8739) short-term automatic certificate renewal (experimental)
* Supports [RFC 8823](https://tools.ietf.org/html/rfc8823) for S/MIME certificates (experimental)
* Supports [draft-ietf-acme-ari-01](https://www.ietf.org/id/draft-ietf-acme-ari-01.html) for renewal information
* Supports [draft-ietf-acme-ari-01](https://www.ietf.org/archive/id/draft-ietf-acme-ari-01.html) for renewal information
* Easy to use Java API
* Requires JRE 11 or higher
* Built with maven, packages available at [Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.shredzone.acme4j%22)

View File

@ -1,19 +1,13 @@
# Register a new Account
# Account and Login
The first step is to register an account with the CA.
If it is the first time you interact with the CA, you will need to register an account first.
Your account needs a key pair. The public key is used to identify your account, while the private key is used to sign the requests to the ACME server.
Your account requires a key pair. The public key is used to identify your account, while the private key is used to sign the requests to the ACME server.
!!! note
Your private key is never transmitted to the ACME server.
The private key is never transmitted to the ACME server.
After the CA has created your account, it returns an account URL. You will need both the key pair and the account URL for logging into the account later.
## Creating an Account Key Pair
You can use external tools like `openssl` or standard Java methods to create a key pair.
A more convenient way is to use the `KeyPairUtils` class. This call generates a RSA key pair with a 2048 bit key:
You can use external tools like `openssl` or standard Java methods to create the key pair. A more convenient way is to use the `KeyPairUtils` class. This call generates a RSA key pair with a 2048 bit key:
```java
KeyPair accountKeyPair = KeyPairUtils.createKeyPair(2048);
@ -25,28 +19,14 @@ You can also create an elliptic curve key pair:
KeyPair accountKeyPair = KeyPairUtils.createECKeyPair("secp256r1");
```
The key pair can be saved to a PEM file using `KeyPairUtils.writeKeyPair()`, and read back later using `KeyPairUtils.readKeyPair()`.
!!! danger
Your key pair is the only way to access your account. If you should lose it, you will be locked out from your account and certificates. The API does not offer a way to recover access after a key loss. The only way is to contact the CA and hope for assistance. For this reason, it is strongly recommended to keep the key pair in a safe place!
Your key pair is the only way to access your account. **If you should lose it, you will be locked out from your account and certificates.** The API does not offer a way to recover access after a key loss. The only way is to contact the CA's support and hope for assistance. For this reason, it is strongly recommended to keep a copy of the key pair in a safe place!
To save a `KeyPair` (actually, the private key of the key pair) to a pem file, you can use this snippet:
## Creating an Account
```java
try (FileWriter fw = new FileWriter("keypair.pem")) {
KeyPairUtils.writeKeyPair(accountKeyPair, fw);
}
```
The following snippet reads the private key from a pem file, and returns a `KeyPair`.
```java
try (FileReader fr = New FileReader("keypair.pem")) {
return KeyPairUtils.readKeyPair(fr);
}
```
## Register an Account
Now create an `AccountBuilder`, optionally add some contact information, agree to the terms of service, set the key pair, then invoke `create()`:
The `AccountBuilder` will take care for creating a new account. Instantiate a builder, optionally add some contact information, agree to the terms of service, set the key pair, then invoke `create()`:
```java
Account account = new AccountBuilder()
@ -58,83 +38,18 @@ Account account = new AccountBuilder()
URL accountLocationUrl = account.getLocation();
```
If the account was successfully created, you will get an `Account` object in return. Invoking its `getLocation()` method will return the location URL of your account. Unlike your key pair, the location is a public information that does not need security precautions.
Now you have a key pair and the account's location URL. This is all you need for [logging in](login.md).
!!! note
Even if it is tempting to do so, you should not invoke `agreeToTermsOfService()` automatically, but let the user confirm the terms of service first. To get a link to the current terms of service, you can invoke `session.getMetadata().getTermsOfService()`.
If the CA changes the terms of service and requires an explicit agreement to the new terms, an `AcmeUserActionRequiredException` is thrown. Its `getInstance()` method returns the URL of a document that gives instructions about how to agree to the new terms of service. There is no way to automatize this process.
If the account was successfully created, you will get an `Account` object in return. Invoking its `getLocation()` method will return the location URL of your account.
## Find out your Account's Location URL
It is recommended to store the location URL along with your key pair. While this is not strictly necessary, it will make it much easier to log into the account later. Unlike your key pair, the location does not need security precautions. The location can [easily be recovered if lost](#login-without-account-url).
If you only have your account's `KeyPair`, you can use the `AccountBuilder` to find out the location `URL` of your account.
## External Account Binding
```java
Account account = new AccountBuilder()
.onlyExisting() // Do not create a new account
.useKeyPair(keyPair)
.create(session);
At some CAs, you need to create a customer account on their website first, and associate it with your ACME account and keypair later. The CA indicates that this process is required if `session.getMetadata().isExternalAccountRequired()` returns `true`.
URL accountLocationUrl = account.getLocation();
```
If you do not have an account yet, an exception is raised instead, and no new account is created.
You can recover your account URL that way, but remember that is is not possible to recover your account's key pair.
## Update your Account
At some point, you may want to update your account. For example your contact address might have changed. To do so, invoke `Account.modify()`, perform the changes, and invoke `commit()` to make them permanent.
The following example adds another email address.
```java
account.modify()
.addContact("mailto:acme2@example.com")
.commit();
```
You can also get the list of contacts via` getContacts()`, and modify or remove contact `URI`s there. However, some CAs do not allow to remove all contacts.
!!! note
`AccountBuilder` only accepts contact addresses when a _new account_ is created. To modify an existing account, use `Account.modify()` as described in this section.
## Account Key Roll-Over
It is also possible to change the key pair that is associated with your account, for example if you suspect that your key has been compromised.
The following example changes the key pair:
```java
KeyPair newKeyPair = ... // new KeyPair to be used
account.changeKey(newKeyPair);
```
After a successful change, all subsequent calls related to this account must use the new key pair. The key is automatically updated on the `Login` that is bound to this `Account` instance.
The old key pair can be disposed of after that. However, I recommend to keep a backup of the old key pair until the key change was proven to be successful, by making a subsequent call with the new key pair. Otherwise you might lock yourself out from your account if the key change should have failed silently, for whatever reason.
## Deactivate an Account
You can deactivate your account if you don't need it any more:
```java
account.deactivate();
```
Depending on the CA, the related authorizations may be automatically deactivated as well. The certificates may still be valid until expiration or explicit revocation. If you want to make sure the certificates are invalidated as well, revoke them prior to deactivation of your account.
!!! danger
There is no way to reactivate the account once it is deactivated!
## Custom Key Identifier
Some CAs may require you to send a custom Key Identifier, to associate your ACME account with an existing customer account at your CA. The CA indicates that a custom key identifier is required if `session.getMetadata().isExternalAccountRequired()` returns `true`.
Your CA provides you with a _Key Identifier_ and a _MAC Key_ for this purpose. You can pass it to the builder using the `withKeyIdentifier()` method:
In this case, your CA provides you a _Key Identifier_ (or _KID_) and a _MAC Key_ (or _HMAC Key_). You can pass these credentials to the builder using the `withKeyIdentifier()` method:
```java
String kid = ... // Key Identifier
@ -145,8 +60,112 @@ Account account = new AccountBuilder()
.withKeyIdentifier(kid, macKey)
.useKeyPair(keyPair)
.create(session);
URL accountLocationUrl = account.getLocation();
```
For your convenience, you can also pass a base64 encoded MAC Key as `String`.
The MAC algorithm is automatically set from the size of the MAC key. If a different algorithm is required, it can be set using `withMacAlgorithm()`.
!!! note
The MAC algorithm is automatically derived from the size of the MAC key. If a different algorithm is required, it can be set using `withMacAlgorithm()`.
## Login
After creating an account, you need to login into it. You get a `Login` object by providing your account information to the session:
```java
KeyPair accountKeyPair = ... // account's key pair
URL accountLocationUrl = ... // account's URL
Login login = session.login(accountLocationUrl, accountKeyPair);
```
Creating a `Login` object is very cheap. You can always create and dispose them as needed. There is no need to cache or pool them.
!!! tip
It is possible to have multiple parallel `Login`s into different accounts in a single session. This is useful if your software handles the certificates of more than one account.
## Login on Creation
If it is more convenient, you can also get a ready to use `Login` object from the `AccountBuilder` when creating a new account:
```java
Login login = new AccountBuilder()
.addContact("mailto:acme@example.com")
.agreeToTermsOfService()
.useKeyPair(keyPair)
.createLogin(session);
URL accountLocationUrl = login.getAccountLocation();
```
## Login without Account URL
As mentioned above, you will need your account key pair and the account URL for logging in. If you do not know the URL, you can log into your account by creating a new account with the same key pair. The CA will detect that an account with that key is already present, and return the existing one instead.
To avoid that an actual new account is created by accident, you can use the `AccountBuilder.onlyExisting()` method:
```java
Login login = new AccountBuilder()
.onlyExisting() // Do not create a new account
.agreeToTermsOfService()
.useKeyPair(keyPair)
.createLogin(session);
URL accountLocationUrl = login.getAccountLocation();
```
It will return a `Login` object just from your keypair, or throw an error if the key was not known to the server.
Remember that there is no way to log into your account without the key pair!
## Updating the Contacts
At some point, you may want to update your account. For example your contact address might have changed. To do so, invoke `Account.modify()`, perform the changes, and invoke `commit()` to make them permanent.
The following example adds another email address.
```java
Account account = login.getAccount();
account.modify()
.addContact("mailto:acme2@example.com")
.commit();
```
You can also get the list of contacts via `getContacts()`, and modify or remove contact `URI`s there. However, some CAs do not allow to remove all contacts.
!!! note
`AccountBuilder` only accepts contact addresses when a _new account_ is created. To modify an existing account, use `Account.modify()` as described in this section. It is not possible to modify the account using the `AccountBuilder` on an existing account.
## Changing the Account Key
It is recommended to change the account key from time to time, e.g if you suspect that your key has been compromised, or if a staff member with knowledge of the key has left the company.
To change the key pair that is associated with your account, you can use the `Account.changeKey()` method:
```java
KeyPair newKeyPair = ... // new KeyPair to be used
Account account = login.getAccount();
account.changeKey(newKeyPair);
```
After a successful change, all subsequent calls related to this account must use the new key pair. The key is automatically updated on the `Login` that was bound to this `Account` instance, so it can be used further. Other existing `Login` instances to the account need to be recreated.
The old key pair can be disposed of after that. However, better keep a backup of the old key pair until the key change was proven to be successful, by making a subsequent call with the new key pair. Otherwise you might lock yourself out from your account if the key change should have failed silently, for whatever reason.
## Account Deactivation
You can deactivate your account if you don't need it any more:
```java
account.deactivate();
```
Depending on the CA, the related authorizations may be automatically deactivated as well. If you want to be on the safe side, you can deactivate all authorizations manually, using `Authorization.deactivate()`.
The issued certificates may still be valid until expiration or explicit revocation. If you want to make sure the certificates are invalidated as well, [revoke](revocation.md) them prior to deactivation of your account.
!!! danger
There is no way to reactivate the account once it has been deactivated!

View File

@ -0,0 +1,72 @@
# Advanced Topics
## Change of TOS
If the CA changes the terms of service and requires an explicit agreement to the new terms, an `AcmeUserActionRequiredException` will be thrown. Its `getInstance()` method returns the URL of a human-readable web document that gives instructions about how to agree to the new terms of service (e.g. by clicking on a confirmation button).
Unfortunately, the `AcmeUserActionRequiredException` can be thrown at any time _acme4j_ is contacting the CA, and won't go away by itself.
There is no way to automatize this process. It requires human interaction, even on a Saturday night. Note that this is a limitation of the ACME protocol, not _acme4j_.
## Custom CSR
Usually _acme4j_ takes the hassle of creating a simple CSR for you. If you need more control over the CSR file, you can provide a PKCS#10 CSR file, either as `PKCS10CertificationRequest` instance or as DER formatted binary. The CSR must provide exactly the domains that you had passed to the `order()`, otherwise the finalization will fail on server side.
To create a CSR, you can use command like tools like `openssl` or Java frameworks like [Bouncy Castle](http://www.bouncycastle.org/java.html).
For your convenience, there is a [`CSRBuilder`](../acme4j-client/apidocs/org.shredzone.acme4j/org/shredzone/acme4j/util/CSRBuilder.html) that simplifies the CSR generation and should be sufficient for most use cases.
```java
KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption
CSRBuilder csrb = new CSRBuilder();
csrb.addDomain("example.org");
csrb.addDomain("www.example.org");
csrb.addDomain("m.example.org");
csrb.setOrganization("The Example Organization")
csrb.sign(domainKeyPair);
csrb.write(new FileWriter("example.csr")); // Write to file
byte[] csr = csrb.getEncoded(); // Get a binary representation
```
The `CSRBuilder` also accepts IP addresses and `Identifier` for generating the CSR:
```java
CSRBuilder csrb = new CSRBuilder();
csrb.addIP(InetAddress.getByName("192.168.1.2"));
csrb.addIdentifier(Identifier.ip("192.168.2.3"));
csrb.sign(domainKeyPair);
```
The `CSRBuilder` is used internally for creating the CSR, and you can take influence on the generated CSR by using the `Order.execute(KeyPair domainKeyPair, Consumer<CSRBuilder> builderConsumer)` method.
## Domain Pre-Authorization
It is possible to pro-actively authorize a domain, without ordering a certificate yet. This can be useful to find out what challenges are requested by the CA to authorize a domain. It may also help to speed up the ordering process, as already completed authorizations do not need to be completed again when ordering the certificate in the near future.
```java
Account account = ... // your Account object
String domain = ... // Domain name to authorize
Authorization auth = account.preAuthorizeDomain(domain);
```
!!! note
Some CAs may not offer domain pre-authorization, `preAuthorizeDomain()` will then fail and throw an `AcmeNotSupportedException`. Some CAs may limit pre-authorization to certain domain types (e.g. non-wildcard) and throw an `AcmeServerException` otherwise.
## Localized Error Messages
By default, _acme4j_ will send your system's default locale as `Accept-Language` header to the CA (with a fallback to any other language). If the language is supported by the CA, it will return localized error messages.
To select another language, use `Session.setLocale()`. The change will only affect that session, so you can have multiple sessions with different locale settings.
## Network Settings
You can use `Session.networkSettings()` to change some network parameters for the session.
* If a proxy must be used for internet connections, you can set a `ProxySelector` instance via `setProxySelector()`.
* To change network timeouts, use `setTimeout()`. The default timeout is 10 seconds. You can either increase the timeout for poor network connections, or reduce it to fail early on network errors. The change affects connection and read timeouts.
* If you need authentication (e.g. for the proxy), you can set an `Authenticator` via `setAuthenticator()`. Be careful here! Most code snippets I have found on the internet will send out the full proxy credentials to anyone who is asking. You should check `Authenticator.getRequestorType()` and make sure it is `RequestorType.PROXY` before sending the proxy credentials.
* _acme4j_ accepts HTTP `gzip` compression by default. If it should impede debugging, it can be disabled via `setCompressionEnabled(false)`.

View File

@ -1,94 +0,0 @@
# Certificates
Once you completed all the previous steps, it's time to download the signed certificate.
But first we need to wait until the certificate is available for download. Again, a primitive way is to poll the status:
```java
Order order = ... // your Order object from the previous step
while (order.getStatus() != Status.VALID) {
Thread.sleep(3000L);
order.update();
}
```
This is a very simple example. You should limit the number of loop iterations, and also handle the case that the status could turn to `INVALID`.
`update()` may throw an `AcmeRetryAfterException`, giving an estimated instant in `getRetryAfter()` when the certificate is available. You should then wait until that moment has been reached, before trying again. The state of your `Order` instance is still updated when this exception is thrown.
!!! tip
If the status is `PENDING`, you have not completed all authorizations yet.
As soon as the status is `VALID`, you can retrieve a `Certificate` object:
```java
Certificate cert = order.getCertificate();
```
The `Certificate` object offers methods to get the certificate or the certificate chain.
```java
X509Certificate cert = cert.getCertificate();
List<X509Certificate> chain = cert.getCertificateChain();
```
`cert` only contains the plain certificate. However, most servers require the certificate `chain` that also contains all intermediate certificates up to the root CA.
Congratulations! You have just created your first certificate via _acme4j_.
## Save the Certificate
The `Certificate` object provides a method to write a certificate file that can be used for most web servers, like _Apache_, _nginx_, but also other servers like _postfix_ or _dovecot_:
```java
try (FileWriter fw = new FileWriter("cert-chain.crt")) {
cert.writeCertificate(fw)
}
```
## Renewal
Certificates are only valid for a limited time, and need to be renewed before expiry.
!!! tip
You can find out the expiry date of a `X509Certificate` by invoking its `getNotAfter()` method.
A certificate can be renewed a few days before its expiry. There is no explicit method for certificate renewal. To renew it, just [order](order.md) the certificate again.
## Revocation
To revoke a certificate, just invoke the respective method:
```java
cert.revoke();
```
Optionally, you can provide a revocation reason that the ACME server may use when generating OCSP responses and CRLs.
```java
cert.revoke(RevocationReason.KEY_COMPROMISE);
```
If you cannot create a `Certificate` object because you don't know the certificate's location URL, you can also use an alternative method that only requires a `Login` and the certificate itself:
```java
Login login = ... // login to your account
X509Certificate cert = ... // certificate to revoke
Certificate.revoke(login, cert, RevocationReason.KEY_COMPROMISE);
```
## Revocation without Account Key Pair
If you have lost your account key, you can still revoke a certificate as long as you still own the domain key pair that was used for signing the CSR. `Certificate` provides a special method for this case.
```java
KeyPair domainKeyPair = ... // the key pair that was used for signing the CSR
X509Certificate cert = ... // certificate to revoke
Certificate.revoke(session, domainKeyPair, cert, RevocationReason.KEY_COMPROMISE);
```
!!! warning
There is no way to revoke a certificate if you have lost both your account's key pair and your domain's key pair.

View File

@ -0,0 +1,45 @@
# Session and Connection
Central part of the communication with the CA is a [`Session`](../acme4j-client/apidocs/org.shredzone.acme4j/org/shredzone/acme4j/Session.html) object. It is used to track the communication with the ACME server.
The first step is to create such a `Session` instance.
## Standard URIs
The `Session` constructor expects the URI of the ACME server's _directory_, as it is documented by the CA. For example, this is how to connect to the _Let's Encrypt_ staging server.
```java
Session session
= new Session("https://acme-staging-v02.api.letsencrypt.org/directory");
```
The Session now knows where to locate the service endpoints. However, no actual connection to the server is done yet. The connection to the CA is handled later by a generic provider.
## ACME URIs
Such an URI is hard to remember and might even change in the future. For this reason, special ACME URIs should be preferred (if available):
```java
Session session = new Session("acme://letsencrypt.org/staging");
```
Instead of a generic provider, this call uses a specialized _Let's Encrypt_ provider.
The _Let's Encrypt_ staging server is meant to be used for testing purposes only. The issued certificates are functional, but as the issuer certificate is not known to browsers, it will lead to an error if the certificate is validated.
To use the _Let's Encrypt_ production server, you only need to change the ACME URI:
```java
Session session = new Session("acme://letsencrypt.org");
```
## Metadata
CAs can provide metadata related to their ACME server. They are evaluated with the `session.getMetadata()` method.
```java
Metadata meta = session.getMetadata();
Optional<URI> tos = meta.getTermsOfService();
Optional<URL> website = meta.getWebsite();
```

View File

@ -41,13 +41,13 @@ This is an example debug log output for creating a new account. This example con
Usually _acme4j_ first logs the action that is taken:
```log
```text
[main] DEBUG org.shredzone.acme4j.AccountBuilder - create
```
If there is no cached copy of the CA's directory, it is fetched now.
```log
```text
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - GET https://localhost:14000/dir
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Length: 406
@ -60,7 +60,7 @@ You can see that a `GET` request to the directory `https://localhost:14000/dir`
If _acme4j_ has no current nonce, it will fetch a new one from the `newNonce` endpoint found in the directory. A `HEAD` request is sufficient for that.
```log
```text
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEAD https://localhost:14000/nonce-plz
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Replay-Nonce: Os_sBjfWzVZenwwjvLrwXA
@ -73,7 +73,7 @@ In the bottom line, the `Replay Nonce` is repeated that was found in the respons
Now _acme4j_ sends a `POST` request to the `newAccount` endpoint. As `Payload`, it will consent to the terms of service, and give optional account information (like the account e-mail address if given). You can also see the `JWS Header` that was sent with the request. It contains the target URL, the *public* JWK of your new account, the nonce from above, and the key algorithm.
```log
```text
[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils - POST https://localhost:14000/sign-me-up
[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils - Payload: {"termsOfServiceAgreed":true}
[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils - JWS Header: {"url":"https://localhost:14000/sign-me-up","jwk":{"kty":"RSA","n":"jyTwiSJACtW_SW-aiihQS5Y5QR704zUwjhlevY0oK-y5wP7SlIc2hq2OPVRarCzjhOxZl2AQFzM5VCR7xRDcnIn9t_pl7Mgsnx9hKDS9yQ24YXzhQ4cMEVVuqwcHvXqPdWDSoCZ1ccMqiiPyBSNGQTXMPY5PBxMOR47XwOb4eNMOPqnzVio3MEtL2wphtEonP3MY6pxJJzzel04wSCRZ4n06reqwER3KwRFPnRpRxAgmSEot5IBLIT3jj-amT5sD7YoUDbPmLk23zgDBIhX88fkClilg1W-fUi1XxYZomEPGvV7OrE1yszt4YDPqKgjJT8t2JPy__1ri-8rZgSxn5Q","e":"AQAB"},"nonce":"Os_sBjfWzVZenwwjvLrwXA","alg":"RS256"}
@ -81,7 +81,7 @@ Now _acme4j_ sends a `POST` request to the `newAccount` endpoint. As `Payload`,
This is a possible response of the server:
```log
```text
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Replay-Nonce: mmnKF6lBuisPWhj9kkFMRA
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Length: 491
@ -102,7 +102,7 @@ Errors are usually sent as JSON problem structure. In the next example we have t
Again, we see the `POST` request to the `newAccount` endpoint. It uses the nonce `I6rXikEqxJ0aRwu1RvspNw` in the `JWS Header`. That nonce might have already been used in a previous request and is invalid now.
```log
```text
[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils - POST https://localhost:14000/sign-me-up
[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils - Payload: {"contact":["mailto:acme@example.com"],"termsOfServiceAgreed":true}
[main] DEBUG org.shredzone.acme4j.toolbox.JoseUtils - JWS Header: {"url":"https://localhost:14000/sign-me-up","jwk":{"kty":"RSA","n":"y5i_8yG9IlL8ra2UWSK12Zy-dS0BYFvu2lerAoJQmYBwtPreOXu4OoIU6ZySAsMxlu2gMLaib62DFAFckEwQP4Bu8yJ4MWdSsiPu6pEs0SAvC61e3lYyDPbSG7FMykhWg5pjbK_NJ4Ysk64DrSA4kc0vxo54YKgxZfzObr4CHBZDaJmkTVtRndI7a8mNFO9pDlfHyb3UyZZPsg3kAUbnI9n3pZatdlGrv6eonbNAREjLvplGEI0_8B08S5fDcm6MqNarxNQIXlEhGDNoYLMGi5tM6CzsfXosHz42Umcym0EXvT1VjfoZMacSDsXleSRwjgewz486LDMErZSc0aUPSQ","e":"AQAB"},"nonce":"I6rXikEqxJ0aRwu1RvspNw","alg":"RS256"}
@ -110,7 +110,7 @@ Again, we see the `POST` request to the `newAccount` endpoint. It uses the nonce
The server responds with a `400 Bad Request` and an `application/problem+json` document:
```log
```text
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Cache-Control: public, max-age=0, no-cache
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Replay-Nonce: LDDZAGcBuKYpuNlFTCxPYw
[main] DEBUG org.shredzone.acme4j.connector.DefaultConnection - HEADER Content-Length: 147
@ -125,6 +125,6 @@ In the `Result JSON`, you can see a JSON problem document. The `type` and `detai
Fortunately, bad nonces are handled by _acme4j_ internally. It will just resend the request with a new nonce.
```log
```text
[main] INFO org.shredzone.acme4j.connector.DefaultConnection - Bad Replay Nonce, trying again (attempt 1/10)
```

View File

@ -0,0 +1,87 @@
# Exceptions
_acme4j_ methods can throw a number of exceptions. All exceptions are derived from `AcmeException` and are checked exceptions. Only `AcmeLazyLoadingException` and `AcmeProtocolException` are runtime exceptions.
```text
Exception
├ AcmeException
│ ├ AcmeNetworkException
│ ├ AcmeRetryAfterException
│ └ AcmeServerException
│ ├ AcmeRateLimitedException
│ ├ AcmeUnauthorizedException
│ └ AcmeUserActionRequiredException
└ RuntimeException
├ AcmeLazyLoadingException
└ AcmeProtocolException
└ AcmeNotSupportedException
```
## AcmeException
This is the root class of all checked _acme4j_ exceptions.
## AcmeNetworkException
This is an `AcmeException` that is thrown on generic network errors during communication (e.g. network timeout).
The exception provides the causing `IOException`.
## AcmeRetryAfterException
This `AcmeException` shows that a server-side process has not been completed yet, and gives an estimation when the process might be completed.
It can only be thrown when invoking `update()` or one of the getters.
!!! note
The internal state of the resource is still updated.
The given estimation is only a proposal. This exception can be safely ignored. However, an earlier attempt to update the state will likely throw this exception again.
## AcmeServerException
An `AcmeException` that is thrown when the server responded with an error. The cause of the error is returned as `Problem` object.
A few special cases are throwing a subclass exception, which is easier to handle.
## AcmeRateLimitedException
This `AcmeServerException` shows that the client has exceeded a rate limit of the server, and the request was denied because of that.
The exception provides a `Problem` instance that further explains what rate limit has been exceeded. Optionally it also provides an `Instant` when the request is expected to succeed again. It also provides `URL`s to human-readable documents with further information about the rate limit.
## AcmeUnauthorizedException
An `AcmeServerException` that indicates that the client has insufficient permissions for the attempted request. For example, this exception is thrown when an account tries to access a resource that belongs to a different account.
## AcmeUserActionRequiredException
This `AcmeServerException` is thrown when an user action is required. The most likely reason is that the Terms of Service have been changed and must be confirmed before proceeding.
The exception provides a `Problem` object with a detailed reason, a link to a web page with further instructions to be taken by a human, and an optional link to the new Terms of Service.
## AcmeLazyLoadingException
This is a runtime exception that is thrown when an `AcmeException` occurs while a resource lazily tries to update its current state from the server.
After construction, all [resources](persistence.md) do not hold the state of the resource yet. For this reason, it is cheap to construct resources, as it does not involve network traffic.
To fetch the current state of the resource, `update()` can be invoked. In case of an error, the `update()` method throws a checked `AcmeException`.
All getter methods of a resource invoke `update()` implicitly if the current state is unknown. However, it would make usage much more complex if every getter could throw the checked `AcmeException`. For this reason, the getters wrap the `AcmeException` into a runtime `AcmeLazyLoadingException`.
If you want to avoid this exception to be thrown, you can invoke `update()` on the resource, and handle the `AcmeException` there in a single place. After that, the getters won't throw an `AcmeLazyLoadingException` anymore.
The exception returns the resource type, the resource location, and the `AcmeException` that was thrown.
## AcmeProtocolException
This is a runtime exception that is thrown if the server response was unexpected and violates the RFC. _acme4j_ was unable to parse the response properly.
An example would be that the server returned an invalid JSON structure that could not be parsed.
## AcmeNotSupportedException
This is an `AcmeProtocolException` that is thrown if the server does not support the requested feature. This can be because the feature is optional, or because the server is not fully RFC compliant.
The exception provides a description of the feature that was missing.

View File

@ -2,10 +2,16 @@
_acme4j_ is a client library that helps connecting to ACME servers without worrying about specification details.
To get a certificate from your ACME CA, these steps need to be performed:
This is the main part of the documentation about how to use _acme4j_.
* [Start a Session](session.md)
* [Register a new Account](account.md)
* [Login into an Account](login.md)
* [Order a Certificate](order.md)
* [Download a Certificate](certificate.md)
* [Session and Connection](connecting.md): How to connect to an ACME server
* [Account and Login](account.md): How to create an account and login
* [Certificate Ordering](order.md): How to order a certificate for your domain
* [Certificate Renewal](renewal.md): How to renew a certificate in time before expiration
* [Certificate Revocation](revocation.md): How to revoke a certificate
* [Resources and Persistence](persistence.md): How to persist ACME resources
* [Exceptions](exceptions.md): About all _acme4j_ exceptions
* [Advanced Topics](advanced.md): Advanced topics about _acme4j_ and ACME
* [Debugging](debugging.md): Enabling and reading the debug log output
The first three chapters are essential, as they describe all necessary steps for getting a signed certificate.

View File

@ -1,69 +0,0 @@
# Login
Technically, the `Account` instance is all you need to proceed with the certification process.
However, you certainly want to come back later, for example if you want to renew a certificate. This is when you need to log into your existing CA account.
## Logging into an Account
You get a `Login` object by providing your account information to the session:
```java
KeyPair accountKeyPair = ... // account's key pair
URL accountLocationUrl = ... // account's URL
Login login = session.login(accountLocationUrl, accountKeyPair);
```
Now you can simply get your `Account` instance from the `Login`:
```java
Account account = login.getAccount();
```
!!! tip
It is possible to have multiple `Login`s to different accounts per session. This is useful if your software handles the certificates of more than one account.
## Login on Creation
If it is more convenient to you, you can also get a ready to use `Login` object from the `AccountBuilder` when creating a new account:
```java
Login login = new AccountBuilder()
.addContact("mailto:acme@example.com")
.agreeToTermsOfService()
.useKeyPair(keyPair)
.createLogin(session);
URL accountLocationUrl = login.getAccountLocation();
Account account = login.getAccount();
```
## Resource Binding
If you know the URL of an ACME resource, you can bind it to a `Login` instance and get a resource object. The resource must be related to the account that is logged in.
For example, this is the way to get an `Authorization` object from an authorization URL:
```java
URL authorizationURL = ... // authorization URL
Authorization auth = login.bindAuthorization(authorizationURL);
```
You can bind `Authorization`, `Certificate`, `Order`, and `Challenge` resources that way. To get the resource URL, use the `getLocation()` method of the resource object.
## Serialization
All resource objects are serializable, so the current state of the object can be frozen by Java's serialization mechanism.
However the `Login` the object is bound with is _not_ serialized! The reason is that besides volatile data, the `Login` object contains a copy of your private key. Not serializing it prevents that you unintentionally reveal your private key in a place with lowered access restrictions.
This means that a deserialized object is not bound to a `Login` yet. It is required to rebind it to a `Login`, by invoking the `rebind()` method of the resource object.
!!! note
Serialization is only meant for short term storage at runtime, not for long term persistence.
For long term persistence, store the location URL of the resource, then bind it at later time like mentioned above.
!!! warning
Do not share serialized data between different versions of _acme4j_.

View File

@ -1,83 +1,102 @@
# Order a Certificate
# Certificate Ordering
Once you have your account set up, you are ready to order certificates.
Use your `Account` object to order the certificate, by using the `newOrder()` method. It returns an `OrderBuilder` object that helps you to collect the parameters of the order. You can give one or more domain names. Optionally you can also give your desired `notBefore` and `notAfter` dates for the generated certificate, but it is at the discretion of the CA to use (or ignore) these values.
## Creating an Order
Use `Account.newOrder()` to start ordering a new certificate. It returns an `OrderBuilder` object that helps you to collect the parameters of the order. You can give one or more domain names. Optionally you can also give your desired `notBefore` and `notAfter` dates for the generated certificate, but it is at the discretion of the CA to use (or ignore) these values.
```java
Account account = ... // your Account object
Order order = account.newOrder()
.domains("example.org", "www.example.org", "m.example.org")
.notAfter(Instant.now().plus(Duration.ofDays(20L)))
.notAfter(Instant.now().plus(Duration.ofDays(20L))) // optional
.create();
```
!!! note
The number of domains per certificate may be limited. See your CA's documentation for the limits.
The `Order` resource contains a collection of `Authorization`s that can be read from the `getAuthorizations()` method. In order to get the certificate, you must process _all of them_ that are in a `PENDING` state.
## Authorization
The `Order` resource contains a collection of `Authorization` objects that can be read from the `getAuthorizations()` method.
Each `Authorization` is associated with one of the domains in your order. `Authorization.getIdentifier()` returns that identifier. Before you can retrieve your certificate, you must process _all_ authorizations that are in a `PENDING` state.
```java
for (Authorization auth : order.getAuthorizations()) {
if (auth.getStatus() == Status.PENDING) {
processAuth(auth);
log.info("Authorizing " + auth.getIdentifier());
// process auth by performing a challenge, see below
:
:
}
}
```
## Process an Authorization
If all `Authorization` objects are in status `VALID`, you are ready to [finalize your order](#finalizing-the-order).
The `Authorization` instance contains further details about how you can prove ownership of your domain. An ACME server offers one or more authorization methods, called `Challenge`s.
## Challenge
`getChallenges()` returns a collection of all `Challenge`s offered by the CA for domain ownership validation. You only need to complete _one_ of them to successfully authorize your domain.
The `Authorization` instance contains further details about how you can prove the ownership of your domain. An ACME server offers one or more authorization methods, called `Challenge`.
`Authorization.getChallenges()` returns a collection of all `Challenge`s offered by the CA for domain ownership validation. You only need to complete _one_ of them to successfully authorize your domain. You would usually pick the challenge that is best suited for your infrastructure.
!!! tip
See [here](../challenge/index.md) for a description of all standard challenges. However, your CA may not offer all of the standard types, and may offer additional, proprietary challenge types.
The simplest way is to invoke `findChallenge()`, stating the challenge type your system is able to provide (either as challenge name or challenge class type):
```java
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE); // by name
Http01Challenge challenge = auth.findChallenge(Http01Challenge.class); // by type
Optional<Http01Challenge> challenge = auth.findChallenge(Http01Challenge.TYPE); // by name
Optional<Http01Challenge> challenge = auth.findChallenge(Http01Challenge.class); // by type
```
It returns a properly casted `Challenge` object, or `null` if your challenge type was not acceptable. In this example, your system is able to respond to a [http-01](../challenge/http-01.md) challenge.
It returns a properly casted `Challenge` object, or _empty_ if your challenge type was not offered by the CA. In this example, your system choses `Http01Challenge` because it is able to respond to a [http-01](../challenge/http-01.md) challenge.
!!! tip
Passing the challenge class is preferred over the challenge name, as type checks are performed at compile time here. Passing in the challenge name might result in a `ClassCastException` at runtime.
Passing the challenge type is preferred over the challenge name, as type checks are performed at compile time here. Passing in the challenge name might result in a `ClassCastException` at runtime.
The returned `Challenge` resource provides all the data that is necessary for a successful verification of your domain ownership. Your response depends on the challenge type (see the [documentation of challenges](../challenge/index.md)).
The returned `Challenge` resource provides all the data that is necessary for a successful verification of your domain ownership (see the documentation of the individual challenges).
After you have performed the necessary steps to set up the response to the challenge (e.g. configuring your web server or modifying your DNS records), the ACME server is told to test your response:
After you have performed the necessary steps to set up the response to the challenge (e.g. configuring your web server or modifying your DNS records), you tell the ACME server that you are ready for validation:
```java
challenge.trigger();
```
Now you have to wait for the server to test your response and set the authorization status to `VALID` or `INVALID`. The easiest (but admittedly also the ugliest) way is to poll the status:
Now you have to wait for the server to check your response. If the checks are completed, the CA will set the authorization status to `VALID` or `INVALID`. The easiest (but admittedly also the ugliest) way is to poll the status:
```java
while (auth.getStatus() != Status.VALID) {
while (!EnumSet.of(Status.VALID, Status.INVALID).contains(auth.getStatus())) {
Thread.sleep(3000L);
auth.update();
}
```
This is a very simple example. You should limit the number of loop iterations, and also handle the case that the status could turn to `INVALID`. If you know when the CA server actually requested your response (e.g. when you notice a HTTP request on the response file), you should start polling after that event.
This is a very simple example which can be improved in many ways:
The CA server may start the validation immediately after `trigger()` is invoked, so make sure your server is ready to respond to requests before invoking `trigger()`. Otherwise the challenge might fail immediately.
* Limit the number of checks, to avoid endless loops if an authorization is stuck on server side.
* Wait with the status checks until the CA has accessed the response for the first time (e.g. after an incoming HTTP request to the response file).
* Use an asynchronous architecture instead of a blocking `Thread.sleep()`.
* Check if `auth.update()` throws an `AcmeRetryAfterException`, and wait for the next update until `AcmeRetryAfterException.getRetryAfter()`. (The state of the `Authorization` instance is still updated when this exception is thrown.)
Also keep your response available until the status has changed to `VALID` or `INVALID`. The ACME server may check your response multiple times, and from different IPs.
The CA server may start with the validation immediately after `trigger()` is invoked, so make sure your server is ready to respond to requests before invoking `trigger()`. Otherwise the challenge might fail instantly.
`update()` may throw an `AcmeRetryAfterException`, giving an estimated instant in `getRetryAfter()` when the authorization is completed. You should then wait until that moment has been reached, before trying again. The state of the `Authorization` instance is still updated when this exception is thrown.
Also keep your response available until the status has changed to `VALID` or `INVALID`. The ACME server may check your response multiple times, and from different IPs! If the status gets `VALID` or `INVALID`, the response you have set up before is not needed anymore. It can (and should) be removed.
When the authorization status is `VALID`, you have successfully authorized your domain.
!!! tip
A common mistake is that the server infrastructure is not completely ready when `trigger()` is invoked (e.g. caches are not purged, services are still restarting, synchronization between instances is still in progress). Also, do not tear down the challenge response too early, as the CA might perform multiple checks.
The response you have set up before is not needed any more. You can (and should) remove it now.
If your authorization status turned to `VALID`, you have successfully authorized your domain, and you are ready for the next step.
## Finalize the Order
## Finalizing the Order
After successfully completing all authorizations, the order needs to be finalized.
First of all, you will need to generate a new key pair that is used for certification and encryption. You can use [`KeyPairUtils`](../acme4j-client/apidocs/org.shredzone.acme4j.utils/org/shredzone/acme4j/util/KeyPairUtils.html) for generating a new key pair for your domain.
First of all, you will need to generate a key pair that is used for certification and encryption of the domain. Similar to the account key pair, you can either use external tool, Java's own crypto framework, or use the [`KeyPairUtils`](../acme4j-client/apidocs/org.shredzone.acme4j.utils/org/shredzone/acme4j/util/KeyPairUtils.html).
!!! tip
Never use your account key pair as domain key pair, but always generate separate key pairs!
@ -90,7 +109,7 @@ KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption
order.execute(domainKeyPair);
```
_acme4j_ will automatically take care of creating a minimal CSR for this order. If you need to expand this CSR (e.g. with your company name), you can do so:
_acme4j_ will automatically take care of creating a minimal CSR for this order internally. If you need to expand this CSR (e.g. with your company name), you can do so:
```java
order.execute(domainKeyPair, csr -> {
@ -100,102 +119,96 @@ order.execute(domainKeyPair, csr -> {
It depends on the CA if other CSR properties (like _Organization_, _Organization Unit_) are accepted. Some may even require these properties to be set, while others may ignore them when generating the certificate.
You can also create a custom CSR, and pass it to the order with either `execute(PKCS10CertificationRequest csr)` or `execute(byte[] csr)`.
!!! note
The correct technical term is _finalization_ of an order, according to RFC-8555. However, Java has a method called `Object.finalize()` which is problematic and should not be used. To avoid confusion with that method, the finalization methods are intentionally called `execute`.
According to RFC-8555, the correct technical term is _finalization_ of an order. However, Java has a method called `Object.finalize()` which is problematic and should not be used. To avoid confusion with that method, the finalization methods are intentionally called `execute` in _acme4j_.
## Using CSR Files
## Retrieving the Certificate
If you need more control over the CSR file, you can also provide a PKCS#10 CSR file, either as `PKCS10CertificationRequest` instance or as DER formatted binary. A single domain may be set as _Common Name_. Multiple domains must be provided as _Subject Alternative Name_. You must provide exactly the domains that you had passed to the `order()` method above, otherwise the finalization will fail.
Once you completed all the previous steps, it is finally time to download the signed certificate.
You can use command like tools like `openssl` or Java frameworks like [Bouncy Castle](http://www.bouncycastle.org/java.html) for generating the CSR file. There is also a [`CSRBuilder`](../acme4j-client/apidocs/org.shredzone.acme4j.utils/org/shredzone/acme4j/util/CSRBuilder.html) for your convenience.
But first we need to wait until the certificate is available for download. Again, a primitive way is to poll the status:
```java
Order order = ... // your Order object from the previous step
while (!EnumSet.of(Status.VALID, Status.INVALID).contains(order.getStatus())) {
Thread.sleep(3000L);
order.update();
}
```
This is a very simple example which can be improved in many ways:
* Limit the number of checks, to avoid endless loops if the order is stuck on server side.
* Use an asynchronous architecture instead of a blocking `Thread.sleep()`.
* Check if `order.update()` throws an `AcmeRetryAfterException`, and wait for the next update until `AcmeRetryAfterException.getRetryAfter()`. (The state of the `Order` instance is still updated when this exception is thrown.)
!!! tip
If the status is `PENDING`, you have not completed all authorizations yet.
!!! note
Always check the status before downloading the certificate, even if it seems that the CA sets the status to `VALID` immediately.
As soon as the status turns `VALID`, you can retrieve a `Certificate` object:
```java
Certificate cert = order.getCertificate();
```
The `Certificate` object offers methods to get the certificate or the certificate chain.
```java
X509Certificate cert = cert.getCertificate();
List<X509Certificate> chain = cert.getCertificateChain();
```
`cert` only contains your leaf certificate. However, most servers require the certificate `chain` that also contains all intermediate certificates up to the root CA.
You can write the certificate chain to disk using the `Certificate.writeCertificate()` method. It will create a `.crt` file that is accepted by most servers (like _Apache_, _nginx_, _postfix_, _dovecot_, etc.).
**Congratulations! You have just created your first certificate via _acme4j_.**
## List all Orders
To get a list of all current orders of your account, invoke `Account.getOrders()`.
Note that for reasons lying in the ACME protocol, the result is an `Iterator<Order>` and not a list. Also, any invocation of `Iterator.next()` can initiate a network call to the CA, and may throw an `AcmeProtocolException` if there was an error.
!!! important
This method is a mandatory part of RFC-8555. Still, as of now, this functionality has not been implemented in all [Boulder](https://github.com/letsencrypt/boulder) based CAs (like Let's Encrypt) and will throw an `AcmeNotSupportedException`. Also see [this issue](https://github.com/letsencrypt/boulder/issues/3335). At the moment, the only workaround is to store `Order` location URLs (or other resource URLs) locally along with the certificates.
## Wildcard Certificates
If supported by the CA, you can also generate a wildcard certificate that is valid for all subdomains of a domain, by prefixing the domain name with `*.` (e.g. `*.example.org`). The domain itself is not covered by the wildcard certificate and also needs to be added to the order if necessary.
!!! note
_acme4j_ accepts all kind of wildcard notations (e.g. `www.*.example.org`, `*.*.example.org`). However, those notations are not specified. They may be rejected by the CA, or may not work as expected.
You must be able to prove ownership of the domain that you want to order a wildcard certificate for (i.e. for `*.example.org` ownership of `example.org` needs to be proven). The corresponding `Authorization` resource only refers to that domain, and does not contain the wildcard notation. However, the `Authorization.isWildcard()` method will reveal that this authorization is related to a wildcard certificate.
The following example creates an `Order` for `example.org` and `*.example.org`:
```java
KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption
CSRBuilder csrb = new CSRBuilder();
csrb.addDomain("example.org");
csrb.addDomain("www.example.org");
csrb.addDomain("m.example.org");
csrb.setOrganization("The Example Organization")
csrb.sign(domainKeyPair);
byte[] csr = csrb.getEncoded();
```
Optionally the CSR can be written as a file, so you can use it again (e.g. for renewal of the certificate).
```java
csrb.write(new FileWriter("example.csr"));
```
After that, finalize the order by providing the CSR:
```java
order.execute(csr);
```
## Wildcard Certificates
You can also generate a wildcard certificate that is valid for all subdomains of a domain, by prefixing the domain name with `*.` (e.g. `*.example.org`). The domain itself is not covered by the wildcard certificate, and also needs to be added to the order if necessary.
!!! note
_acme4j_ accepts all kind of wildcard notations (e.g. `www.*.example.org`, `*.*.example.org`). However, those notations are not specified and may be rejected by your CA.
You must be able to prove ownership of the domain that you want to order a wildcard certificate for. The corresponding `Authorization` resource only refers to that domain, and does not contain the wildcard notation.
The following example creates an `Order` and a CSR for `example.org` and `*.example.org`:
```java
Order order = account.newOrder()
.domains("example.org", "*.example.org")
.create();
KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption
CSRBuilder csrb = new CSRBuilder();
csrb.addDomain("example.org"); // example.org itself, if necessary
csrb.addDomain("*.example.org"); // wildcard for all subdomains
csrb.sign(domainKeyPair);
byte[] csr = csrb.getEncoded();
order.execute(csr);
order.execute(domainKeyPair);
```
In the subsequent authorization process, you would have to prove ownership of the `example.org` domain.
In the subsequent authorization process, you would only have to prove ownership of the `example.org` domain.
!!! note
Some CAs may reject wildcard certificate orders, may only offer a limited set of `Challenge`s, or may involve `Challenge`s that are not documented here. Refer to your CA's documentation to find out about the wildcard certificate policy.
Some CAs may reject wildcard certificate orders at all, may only offer a limited set of challenge types, or may require special challenge types that are not documented here. Refer to your CA's documentation to find out about the wildcard certificate policy.
## Pre-Authorize a Domain
## IP Identifiers
It is possible to pro-actively authorize a domain. This can be useful to find out what challenges are requested by the CA to authorize a domain, before actually ordering a certificate. It may also help to speed up the ordering process, as already completed authorizations do not need to be completed again when ordering the certificate.
```java
Account account = ... // your Account object
String domain = ... // Domain name to authorize
Authorization auth = account.preAuthorizeDomain(domain);
```
!!! note
Some CAs may not offer domain pre-authorization. `preAuthorizeDomain()` will then fail and throw an `AcmeException`.
!!! note
Some CAs may not offer wildcard domain pre-authorization, but only wildcard domain orders.
## Deactivate an Authorization
It is possible to deactivate an `Authorization`, for example if you sell the associated domain.
```java
auth.deactivate();
```
!!! tip
It is not documented if the deactivation of an authorization also revokes the related certificate. If the certificate should be revoked, revoke it manually before deactivation.
## Use IP Identifiers
_acme4j_ supports IP identifier validation, as specified in [RFC 8738](https://tools.ietf.org/html/rfc8738). It permits validation of IP addresses instead of domain names. If your CA offers ACME IP support, you can add IP `Identifier` objects to the order:
Besides domains, _acme4j_ also supports IP identifier validation as specified in [RFC 8738](https://tools.ietf.org/html/rfc8738). If your CA offers ACME IP support, you can add IP `Identifier` objects to the order:
```java
Order order = account.newOrder()
@ -206,62 +219,3 @@ Order order = account.newOrder()
```
The example also shows how to add domain names as DNS `Identifier` objects. Adding domain names via `domain()` is just a shortcut notation for it.
The `CSRBuilder` also accepts IP addresses and `Identifier` for generating the CSR:
```java
CSRBuilder csrb = new CSRBuilder();
csrb.addIP(InetAddress.getByName("192.168.1.2"));
csrb.addIdentifier(Identifier.ip("192.168.2.3"));
csrb.sign(domainKeyPair);
byte[] csr = csrb.getEncoded();
```
## Short-Term Automatic Renewal
_acme4j_ supports [RFC 8739](https://tools.ietf.org/html/rfc8739) for short-term automatic renewal of certificates.
!!! warning
The _ACME STAR_ support is experimental. There is currently no known ACME server implementing this extension.
To find out if the CA supports the STAR extension, check the metadata:
```java
if (session.getMetadata().isAutoRenewalEnabled()) {
// CA supports STAR!
}
```
If STAR is supported, you can enable automatic renewals by adding `autoRenewal()` to the order parameters:
```java
Order order = account.newOrder()
.domain("example.org")
.autoRenewal()
.create();
```
You can use `autoRenewalStart()`, `autoRenewalEnd()`, `autoRenewalLifetime()` and `autoRenewalLifetimeAdjust()` to change the time span and frequency of automatic renewals. You cannot use `notBefore()` and `notAfter()` in combination with `autoRenewal()` though.
The `Metadata` object also holds the accepted renewal limits (see `Metadata.getAutoRenewalMinLifetime()` and `Metadata.getAutoRenewalMaxDuration()`).
After the validation process is completed and the order is finalized, the STAR certificate is available via `Order.getAutoRenewalCertificate()` (_not_ `Order.getCertificate()`)!
Use `Certificate.getLocation()` to retrieve the URL of your certificate. It is renewed automatically, so you will always be able to download the latest issue of the certificate from this URL.
!!! note
STAR based certificates cannot be revoked. However, as it is the nature of these certs to be short-lived, this does not pose an actual security issue.
To download the latest certificate issue, you can bind the certificate URL to your `Login` and then use the `Certificate` object.
```java
URL certificateUrl = ... // URL of the certificate
Certificate cert = login.bindCertificate(certificateUrl);
X509Certificate latestCertificate = cert.getCertificate();
```
If supported by the CA, it is possible to negotiate that the certificate can also be downloaded via `GET` request. First use `Metadata.isAutoRenewalGetAllowed()` to check if this option is supported by the CA. If it is, add `autoRenewalEnableGet()` to the order parameters to enable it. After the order was finalized, you can use any HTTP client to download the latest certificate from the certificate URL by a `GET` request.
Use `Order.cancelAutoRenewal()` to terminate automatical certificate renewals.

View File

@ -0,0 +1,49 @@
# Resources and Persistence
All CA related resources are derived from the `AcmeResource` class:
* `Account`: Represents your account
* `Authorization`: Authorization of a domain or identifier
* `Certificate`: A certificate
* `Challenge` (and subclasses): Challenge to proof domain ownership
* `Order`: A certificate order
* `RenewalInfo`: Renewal information
These classes reflect the state of the corresponding resource on the ACME server side. Except of `Certificate`, they also keep a copy of the current resource state that can be updated via `update()`.
## Resource Location
All resources possess a unique resource URL on the CA server. To get that URL, invoke the `getLocation()` method. This is the best way to retrieve a permanent resource reference for local persistence (e.g. in a database).
## Resource Binding
To revive an `AcmeResource` object from its location URL, you can bind it to your `Login` by using the resource location URL and the corresponding method:
* `Login.bindAuthorization()` takes an authorization URL and returns the corresponding `Authorization` object.
* `Login.bindCertificate()` takes a certificate URL and returns the corresponding `Certificate` object.
* `Login.bindOrder()` takes an order URL and returns the corresponding `Order` object.
* `Login.bindRenewalInfo()` takes an renewal info URL and returns the corresponding `RenewalInfo` object.
There are two methods for binding a `Challenge`:
* `Login.bindChallenge(URL location)` binds to a challenge URL and returns a `Challenge` instance. You will need to check yourself if the `Challenge` is of the expected type, and eventually cast it to the correct type.
* `bindChallenge(URL location, Class type)` is similar to the method above, but will do the casting for you. You will get the challenge object in your expected type. If the challenge at the location is of a different type, an `AcmeProtocolException` will be thrown.
There is no way to bind an `Account`. To retrieve your account resource, simply invoke `Login.getAccount()`.
!!! note
You can only bind resources that belong to your account.
## Serialization
All resource objects are serializable, so the current state of the object can also be frozen by Java's serialization mechanism.
However the `Login` that the object is bound to is _not_ serialized! This is because in addition to volatile data, the `Login` object also holds a copy of your private key. Not serializing it prevents you from accidentally exposing your private key in a place with lowered access restrictions.
After deserialization, an object is not bound to a `Login` yet. It is required to rebind it by invoking the `rebind()` method of the resource object.
!!! note
Serialization is only meant for short term storage at runtime, not for long term persistence. For long term persistence, always store the location URL of the resource, then bind it at later time like mentioned above.
!!! warning
Do not share serialized data between different versions of _acme4j_.

View File

@ -0,0 +1,75 @@
# Certificate Renewal
Certificates are only valid for a limited time, and need to be renewed before expiry.
To read the expiration date of your certificate, use `X509Certificate.getNotAfter()`. The certificate is eligible to be renewed a few days or weeks before its expiry. Check the documentation of your CA about a recommended time window. Also do not postpone the renewal to the last minute, as there can always be unexpected network issues that delay the issuance of a renewed certificate.
!!! tip
Some CAs send a notification mail to your account's mail addresses in time before expiration. However you should not rely on those mails, and only use them as an ultimate warning.
## How to Renew
There is no special path for renewing a certificate. To renew it, just [order](order.md) the certificate again.
## Renewal Information
_acme4j_ supports the [draft-ietf-acme-ari-01](https://www.ietf.org/archive/id/draft-ietf-acme-ari-01.html) draft.
You can check if the CA offers renewal information by invoking `Certificate.hasRenewalInfo()`. If it does, you can get a suggested time window for certificate nenewal by invoking `Certificate.getRenewalInfo()`.
After you renewed the certificate and made sure the old certificate is not used anymore, you can invoke `Certificate.markAsReplaced()` on the _old_ certificate. It will show the CA that the renewal is completed, and that the old certificate can be revoked.
!!! note
The next version of the draft contains changes that are not backward compatible. _acme4j_ only supports version 01 of the draft at the moment. Also, because of the dynamic nature of the draft, all parts of the API that are related to this draft may be changed or removed without notice. SemVer rules do not apply here.
## Short-Term Automatic Renewal
_acme4j_ supports [RFC 8739](https://tools.ietf.org/html/rfc8739) for Short-Term Automatic Renewal (STAR) of certificates.
To find out if the CA supports the STAR extension, check the metadata:
```java
if (session.getMetadata().isAutoRenewalEnabled()) {
// CA supports STAR!
}
```
If STAR is supported, you can enable automatic renewals by adding `autoRenewal()` to the order parameters:
```java
Order order = account.newOrder()
.domain("example.org")
.autoRenewal()
.create();
```
You can also use `autoRenewalStart()`, `autoRenewalEnd()`, `autoRenewalLifetime()` and `autoRenewalLifetimeAdjust()` to change the time span and frequency of automatic renewals. You cannot use `notBefore()` and `notAfter()` in combination with `autoRenewal()` though.
The `Metadata` object also holds the accepted renewal limits (see `Metadata.getAutoRenewalMinLifetime()` and `Metadata.getAutoRenewalMaxDuration()`).
!!! important
After your order is finalized, you must use `Order.getAutoRenewalCertificate()` to retrieve a STAR certificate! Do not use `Order.getCertificate()` here.
The STAR certificates are automatically renewed by the CA. You will always find the latest certificate at the certificate location URL.
To download the latest certificate issue, you can bind the certificate URL to your `Login` and then use the `Certificate` object.
```java
URL certificateUrl = ... // URL of the certificate
Certificate cert = login.bindCertificate(certificateUrl);
X509Certificate latestCertificate = cert.getCertificate();
```
!!! note
STAR based certificates cannot be revoked. However, as it is the nature of these certs to be very short-lived, this does not pose an actual security issue.
### Fetching STAR certificates via GET
Usually the STAR certificate must be fetched from the location URL by an authorized `POST-as-GET` request. If supported by the CA, it is possible to change the method to a plain `GET` request, so the certificate can be fetched by a simple HTTP client (like curl) without authentication.
To enable this `GET` method, first check if it is offered by the CA, by invoking `Metadata.isAutoRenewalGetAllowed()`. If it is true, add `autoRenewalEnableGet()` to the order options. After the order was finalized, the certificate will be available via both `GET` and `POST-as-GET` methods.
### Cancelling Auto-Renewals
Use `Order.cancelAutoRenewal()` to terminate automatical certificate renewals.

View File

@ -0,0 +1,43 @@
# Certificate Revocation
To revoke a certificate, just invoke the respective method:
```java
cert.revoke();
```
Optionally, you can provide a revocation reason that the ACME server may use when generating OCSP responses and CRLs.
```java
cert.revoke(RevocationReason.KEY_COMPROMISE);
```
There are different reasons for a certificate revocation. If you have sold or deleted the associated domain, you should also deactivate the respective `Authorization` using `Authorization.deactivate()`. Otherwise the new owner of the domain might have problems to get a certificate because the domain name is still associated with your account.
!!! tip
It is not documented if the deactivation of an authorization also revokes the related certificate automatically. If in doubt, revoke the certificate yourself before deactivation.
## Without Certificate URL
If you cannot create a `Certificate` object because you don't know the certificate's location URL, you can also use an alternative method that only requires a `Login` and the certificate itself:
```java
Login login = ... // login to your account
X509Certificate cert = ... // certificate to revoke
Certificate.revoke(login, cert, RevocationReason.KEY_COMPROMISE);
```
## Without Account Key
If you have lost your account key, you can still revoke a certificate as long as you still own the domain key pair that was used for the order. `Certificate` provides a special method for this case.
```java
KeyPair domainKeyPair = ... // the key pair used for order (not your account key pair)
X509Certificate cert = ... // certificate to revoke
Certificate.revoke(session, domainKeyPair, cert, RevocationReason.KEY_COMPROMISE);
```
!!! warning
There is no automatized way to revoke a certificate if you have lost both your account's key pair and your domain's key pair.

View File

@ -1,51 +0,0 @@
# Creating a Session
Central part of the communication is a [`Session`](../acme4j-client/apidocs/org.shredzone.acme4j/org/shredzone/acme4j/Session.html) object. A session is used to track the communication with the ACME server.
The first step is to create a `Session` instance. The `Session` constructor expects the URI of the ACME server's directory service, as it is documented by the CA. For example, this is how to connect to the _Let's Encrypt_ staging server:
```java
Session session
= new Session("https://acme-staging-v02.api.letsencrypt.org/directory");
```
However, such an URI is hard to remember and might even change in the future. For this reason, special ACME URIs should be preferred:
```java
Session session = new Session("acme://letsencrypt.org/staging");
```
Instead of a generic provider, this call uses a special _Let's Encrypt_ provider.
The _Let's Encrypt_ staging server is meant to be used for testing purposes only. The issued certificates are functional, but as the issuer certificate is not known to browsers, it will lead to a certificate error. Later you only need to change the ACME URI in order to use the _Let's Encrypt_ production server.
```java
Session session = new Session("acme://letsencrypt.org");
```
## Metadata
Some CAs provide metadata related to their ACME server:
```java
Metadata meta = session.getMetadata();
URI tos = meta.getTermsOfService();
URL website = meta.getWebsite();
```
`meta` is never `null`, even if the server did not provide any metadata. All of the `Metadata` getters are optional though, and may return `null` if the respective information was not provided by the server.
## Locale
`Session.setLocale()` allows to select a different locale. Errors will be returned in that language, if supported by the CA.
By default, the system's default locale is used.
## Network Settings
You can use `Session.networkSettings()` to change some network parameters for the session.
* If a proxy must be used for internet connections, you can set a `ProxySelector` instance via `setProxySelector()`.
* To change network timeouts, use `setTimeout()`. The default timeout is 10 seconds. You can either increase the timeout on poor network connections, or reduce it to fail early on network errors.
* If you need authentication (e.g. for the proxy), you can set an `Authenticator` via `setAuthenticator()`. Be careful here! Most code snippets I have found on the internet will send out the proxy credentials to anyone who is asking. You should check `Authenticator.getRequestorType()` and make sure it is `RequestorType.PROXY` before sending the proxy credentials.
* _acme4j_ accepts HTTP `gzip` compression by default. If it should impede debugging, it can be disabled via `setCompressionEnabled(false)`.

View File

@ -24,12 +24,15 @@ nav:
- 'acme4j-it': acme4j-it/apidocs/index.html
- Usage:
- 'usage/index.md'
- 'usage/session.md'
- 'usage/connecting.md'
- 'usage/account.md'
- 'usage/login.md'
- 'usage/order.md'
- 'usage/certificate.md'
- 'debugging.md'
- 'usage/renewal.md'
- 'usage/revocation.md'
- 'usage/persistence.md'
- 'usage/exceptions.md'
- 'usage/advanced.md'
- 'usage/debugging.md'
- 'faq.md'
- Challenges:
- 'challenge/index.md'