From 1972d52bedcd945570c8bd26557f8725f4666f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Wed, 21 Feb 2018 20:04:59 +0100 Subject: [PATCH] Review documentation, update to ACME v2 --- .../org/shredzone/acme4j/Certificate.java | 4 + acme4j-example/src/site/markdown/index.md.vm | 2 +- acme4j-it/src/site/markdown/index.md.vm | 47 +------- pom.xml | 2 +- src/site/markdown/ca/index.md | 36 ++---- src/site/markdown/ca/pebble.md | 2 +- src/site/markdown/challenge/dns-01.md | 2 + src/site/markdown/challenge/http-01.md | 7 +- src/site/markdown/challenge/index.md | 2 - src/site/markdown/development/provider.md | 12 +- src/site/markdown/development/testing.md | 11 +- src/site/markdown/index.md.vm | 18 ++- src/site/markdown/migration.md | 98 ++-------------- src/site/markdown/usage/account.md | 109 +++++++++++++++--- src/site/markdown/usage/certificate.md | 45 +++++--- src/site/markdown/usage/index.md | 41 +------ src/site/markdown/usage/login.md | 76 ++++++++++++ src/site/markdown/usage/order.md | 103 ++++++++--------- src/site/markdown/usage/session.md | 45 +++----- src/site/resources/css/site.css | 20 ++++ src/site/site.xml | 1 + 21 files changed, 354 insertions(+), 329 deletions(-) create mode 100644 src/site/markdown/usage/login.md create mode 100644 src/site/resources/css/site.css diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java index b12638a4..2c3ff879 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java @@ -54,6 +54,10 @@ public class Certificate extends AcmeResource { /** * Downloads the certificate chain. + *

+ * The certificate is downloaded lazily by the other methods. So usually there is no + * need to invoke this method, unless the download is to be enforced. If the + * certificate has been downloaded already, nothing will happen. * * @throws AcmeException * if the certificate could not be downloaded diff --git a/acme4j-example/src/site/markdown/index.md.vm b/acme4j-example/src/site/markdown/index.md.vm index 50ff7ebe..9efe97bf 100644 --- a/acme4j-example/src/site/markdown/index.md.vm +++ b/acme4j-example/src/site/markdown/index.md.vm @@ -16,4 +16,4 @@ You can also invoke the example tool via maven: mvn exec:java -Dexec.args="example.com example.org" ``` -Or just have a look at [the source code](https://github.com/shred/acme4j/blob/master/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java). +Or just have a look at [the source code](./apidocs/src-html/org/shredzone/acme4j/ClientTest.html). diff --git a/acme4j-it/src/site/markdown/index.md.vm b/acme4j-it/src/site/markdown/index.md.vm index fd0edd1a..94ed7572 100644 --- a/acme4j-it/src/site/markdown/index.md.vm +++ b/acme4j-it/src/site/markdown/index.md.vm @@ -5,45 +5,7 @@ This module contains an Integration Test of _acme4j_. It builds a [Pebble](https://github.com/letsencrypt/pebble) docker image and runs it. After that, a number of integration tests are performed. -How to Use ----------- - -Integration tests are disabled by default, to ensure that _acme4j_ can be build on systems with not much more than a _maven_ installation. For running the integration tests, _Docker_ must be available. - -To enable the integration tests, use the `ci` profile when building the _acme4j_ project: - -``` -mvn -P ci clean install -``` - -It will build and run a Pebble server, perform the integration tests, and stop the Pebble server after that. - -The Pebble server needs to connect to servers that are provided by the maven integration tests. For this reason, the Pebble server must run on the same machine where maven is running, so the servers are available via `localhost`. - -Starting Pebble manually ------------------------- - -You can also run Pebble on your machine, to run the integration tests inside your IDE. - -To do so, change to the `acme4j-it` module, then run `docker:build` to download and build the Pebble image, and `docker:start` to start the Pebble server: - -``` -cd acme4j-it -mvn docker:build -mvn docker:start -``` - -To stop the server: - -``` -mvn docker:stop -``` - -To remove the docker image (recommended if you want to rebuild Pebble after an update): - -``` -mvn docker:remove -``` +See the [Documentation](../development/testing.html) for how to run it GitLab CI --------- @@ -53,10 +15,3 @@ _acme4j_ contains a GitLab CI configuration file. The CI runner should be set up with a `shell` executor. Maven and Docker should be installed on the CI runner, and the shell executor user should be able to use both. The tags `maven` and `docker` are used to select the executor. - -`acme4j-it` API ---------------- - -The `acme4j-it` module provides test servers for the `http-01` and `dns-01` challenges. You can use these classes for your own projects. However, they are not part of the official _acme4j_ API and subject to change without notice. - -Note that these servers are very simple implementations without any security measures. They are tailor-made for integration tests. Do not use them in production code! diff --git a/pom.xml b/pom.xml index df1530e1..0ee27bab 100644 --- a/pom.xml +++ b/pom.xml @@ -182,10 +182,10 @@ maven-javadoc-plugin 3.0.0 - UTF-8 true syntax,reference true + en diff --git a/src/site/markdown/ca/index.md b/src/site/markdown/ca/index.md index 152e34dc..7fd624bb 100644 --- a/src/site/markdown/ca/index.md +++ b/src/site/markdown/ca/index.md @@ -2,36 +2,18 @@ _acme4j_ should support any CA providing an ACME server. -It is always possible to connect to an ACME server by passing in the CA's resource directory URL: - -```java -Session session = - new Session("https://acme-staging.api.letsencrypt.org/directory", accountKeyPair); -``` - -For some CAs there are also more specific ACME providers available via `acme` schemed URIs: - -```java -Session session = - new Session("acme://letsencrypt.org/staging", accountKeyPair); -``` - -Connecting via `acme` URI should always be preferred over using the directory URL. - -## Metadata - -Some CAs provide metadata related to their ACME server. This information can be retrieved via the `Session` object: - -```java -Metadata meta = session.getMetadata(); -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. - ## Available Providers The _acme4j_ package contains these providers: * [Let's Encrypt](./letsencrypt.html) * [Pebble](./pebble.html) + +More CAs may be supported in future releases of _acme4j_. + +Also, CAs can publish provider jar files that plug into _acme4j_ and offer extended support. + +

diff --git a/src/site/markdown/ca/pebble.md b/src/site/markdown/ca/pebble.md index b79b6829..1d6628e8 100644 --- a/src/site/markdown/ca/pebble.md +++ b/src/site/markdown/ca/pebble.md @@ -10,4 +10,4 @@ This ACME provider can be used to connect to a local Pebble server instance, mai * `acme://pebble/pebble.example.com` - Connect to a Pebble server at `pebble.example.com` and standard port 14000. * `acme://pebble/pebble.example.com:12345` - Connect to a Pebble server at `pebble.example.com` and port 12345. -Pebble contains an integrated web server that only accepts HTTP connections, so HTTPS connections are not supported by this provider. +Pebble uses a self-signed certificate for HTTPS connections. The Pebble provider accepts this certificate. diff --git a/src/site/markdown/challenge/dns-01.md b/src/site/markdown/challenge/dns-01.md index 93ddc438..cac0463a 100644 --- a/src/site/markdown/challenge/dns-01.md +++ b/src/site/markdown/challenge/dns-01.md @@ -6,7 +6,9 @@ With the `dns-01` challenge, you prove to the CA that you are able to control th ```java Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE); + String digest = challenge.getDigest(); +String domain = auth.getDomain(); ``` The CA expects a TXT record at `_acme-challenge.${domain}` with the `digest` string as value. diff --git a/src/site/markdown/challenge/http-01.md b/src/site/markdown/challenge/http-01.md index 9ff5e3ea..0ad3cb67 100644 --- a/src/site/markdown/challenge/http-01.md +++ b/src/site/markdown/challenge/http-01.md @@ -9,11 +9,12 @@ Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE); String token = challenge.getToken(); String content = challenge.getAuthorization(); +String domain = auth.getDomain(); ``` `token` is the name of the file that will be requested by the CA server. It must contain the `content` string, without any leading or trailing white spaces or line breaks. The `Content-Type` header must be either `text/plain` or absent. -The expected path is (assuming that `${domain}` is your domain and `${token}` is the token): +The expected path is (assuming that `${domain}` is the domain to be authorized, and `${token}` is the token): ``` http://${domain}/.well-known/acme-challenge/${token} @@ -21,4 +22,6 @@ http://${domain}/.well-known/acme-challenge/${token} The challenge is completed when the CA was able to download that file and found `content` in it. -> __Note:__ The request is sent to port 80 only. If your domain has multiple IP addresses, the CA randomly selects one of them. There is no way to choose a different port or a fixed IP address. + diff --git a/src/site/markdown/challenge/index.md b/src/site/markdown/challenge/index.md index 31ab276c..1814c6ed 100644 --- a/src/site/markdown/challenge/index.md +++ b/src/site/markdown/challenge/index.md @@ -4,8 +4,6 @@ Challenges are used to prove ownership of a domain. There are different kind of challenges. The most simple is maybe the HTTP challenge, where a file must be made available at the domain that is to be validated. It is assumed that you control the domain if you are able to publish a given file under a given path. -The CA offers one or more challenges. One of the challenges must be completed in order to prove ownership. - The ACME specifications define these standard challenges: * [http-01](./http-01.html) diff --git a/src/site/markdown/development/provider.md b/src/site/markdown/development/provider.md index 583ec31f..4bd2710e 100644 --- a/src/site/markdown/development/provider.md +++ b/src/site/markdown/development/provider.md @@ -3,13 +3,13 @@ Basically, it is possible to connect to any kind of ACME server just by connecting to the URL of its directory resource: ```java -Session session = new Session("https://acme-v02.api.letsencrypt.org/directory", accountKeyPair); +Session session = new Session("https://acme-v02.api.letsencrypt.org/directory"); ``` ACME providers are "plug-ins" to _acme4j_ that are specialized on a single CA. For example, the _Let's Encrypt_ provider offers URIs that are much easier to remember. The example above would look like this: ```java -Session session = new Session("acme://letsencrypt.org", accountKeyPair); +Session session = new Session("acme://letsencrypt.org"); ``` ## Writing your own Provider @@ -21,7 +21,7 @@ However, it is also possible to adapt the behavior of wide parts of _acme4j_ to A client provider implements the [`AcmeProvider`](../apidocs/org/shredzone/acme4j/provider/AcmeProvider.html) interface, but usually it is easier to extend [`AbstractAcmeProvider`](../apidocs/org/shredzone/acme4j/provider/AbstractAcmeProvider.html) and implement only these two methods: * `accepts(URI)` checks if the client provider is accepting the provided URI. Usually it would be an URI like `acme://example.com`. Note that the `http` and `https` schemes are reserved for the generic provider and cannot be used by other providers. -* `resolve(URI)` parses that URI and returns the corresponding URL of the directory service. +* `resolve(URI)` parses the URI and returns the corresponding URL of the directory service. The `AcmeProvider` implementation needs to be registered with Java's `ServiceLoader`. In the `META-INF/services` path of your project, create a file `org.shredzone.acme4j.provider.AcmeProvider` and write the fully qualified class name of your implementation into that file. @@ -37,7 +37,7 @@ The standard Java mechanisms are used to verify the HTTPS certificate provided b If your ACME server provides challenges that are not specified in the ACME protocol, there should be an own `Challenge` implementation for each of your challenge, by extending the [`Challenge`](../apidocs/org/shredzone/acme4j/challenge/Challenge.html) class. -In your `AcmeProvider` implementation, override the `createChallenge(Session, JSON)` method so it returns a new instance of your `Challenge` implementation when your individual challenge type is requested. All other types should be delegated to the super method. +In your `AcmeProvider` implementation, override the `createChallenge(Login, JSON)` method so it returns a new instance of your `Challenge` implementation when your individual challenge type is requested. All other types should be delegated to the super method. ## Amended Directory Service @@ -45,7 +45,9 @@ To override single entries of an ACME server's directory, or to use a static dir ## Adding your Provider to _acme4j_ -After you completed your provider code, you can send in a pull request and have it added to the _acme4j_ code base, if these preconditions are met: +After you completed your provider code, you can send in a pull request and apply for inclusion in the _acme4j_ code base. + +These preconditions must be met: * Your provider's source code must be published under the terms of [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0). * The source code of your ACME server must be publicly available under an [OSI compliant license](https://opensource.org/licenses/alphabetical). diff --git a/src/site/markdown/development/testing.md b/src/site/markdown/development/testing.md index 70785ce1..cc97f2d7 100644 --- a/src/site/markdown/development/testing.md +++ b/src/site/markdown/development/testing.md @@ -20,13 +20,20 @@ To run this server, you can use the Docker image mentioned above. You could also The `BammBammClient` class can be used to set the challenge responses and DNS records via the REST interface on port 14001. -Note that _BammBamm_ has its main focus on simplicity, and is only meant as a server for integration test purposes. Do not use _Bammbamm_ in production environments! It is neither hardened, nor feature complete. + ## Boulder It is also possible to run some tests against the [Boulder](https://github.com/letsencrypt/boulder) ACME server, but the setup is a little tricky. -First, build and start the integration test Docker servers as [explained above](#Integration_Tests). When the servers are started, find out the IP address of the _BammBamm_ server (`docker inspect bammbamm -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'`). You'll need it in the next step. +First, build and start the integration test Docker servers as [explained above](#Integration_Tests). When the servers are started, find out the IP address of the _BammBamm_ server: + +```bash +docker inspect bammbamm -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +``` Now set up a Docker instance of Boulder. Follow the instructions in the [Boulder README](https://github.com/letsencrypt/boulder#quickstart). When you are ready to start it, set the `FAKE_DNS` env variable to the IP address of _BammBamm_ you have found before. diff --git a/src/site/markdown/index.md.vm b/src/site/markdown/index.md.vm index bc2cfbd8..0e4c8413 100644 --- a/src/site/markdown/index.md.vm +++ b/src/site/markdown/index.md.vm @@ -24,6 +24,14 @@ _acme4j_ is available at Maven Central. Just add this snippet to your `pom.xml`: ``` +For Gradle: + +```groovy +dependencies { + compile group: '${project.groupId}', name: '${project.artifactId}-client', version: '${project.version}' +} +``` + There is also an optional utility module that will help you handling key pairs and certificates (but requires [Bouncy Castle](https://www.bouncycastle.org/java.html)): ```xml @@ -34,4 +42,12 @@ There is also an optional utility module that will help you handling key pairs a ``` -Now just have a look at [this source code](https://github.com/shred/acme4j/blob/master/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java) to see an example usage. +For Gradle: + +```groovy +dependencies { + compile group: '${project.groupId}', name: '${project.artifactId}-utils', version: '${project.version}' +} +``` + +Now just have a look at [this source code](./acme4j-example/apidocs/src-html/org/shredzone/acme4j/ClientTest.html) to see an example usage. diff --git a/src/site/markdown/migration.md b/src/site/markdown/migration.md index 69383250..a7c0a3b6 100644 --- a/src/site/markdown/migration.md +++ b/src/site/markdown/migration.md @@ -2,97 +2,15 @@ This document will help you migrate your code to the latest _acme4j_ version. -## Migration to Version 0.10 +## Migration to Version 2.0 -Starting with version 0.10, _acme4j_ requires Java 8 or higher. This is also reflected in the API. +_acme4j_ 2.0 fully supports the ACMEv2 protocol. Sadly, the ACMEv2 protocol is a major change. -The most noticeable change is that the old `java.util.Date` has been replaced by the new `java.time` API in the entire project. If you don't want to migrate your code to the new API, you can use `Date.from()` and `Date.toInstant()` to convert between the different date objects, where necessary. +There is no easy recipe to migrate your code to _acme4j_ 2.0. I recommend to have a look at the example, and read this documentation. Altogether, it shouldn't be too much work to update your code, though. -## Migration to Version 0.9 +## Migration from Version 2.0-SNAPSHOT (GitHub master branch) -Version 0.9 brought many changes to the internal API. However, this is only relevant if you run your own CA and make own extensions to _acme4j_ (e.g. if you implement a proprietary `Challenge`). If you use _acme4j_ only for retrieving certificates, you should not notice any changes. - -There is one exception: `Authorization.findCombinations()` previously returned `null` if it did not find a matching set of combinations. Now it returns an empty list instead, to avoid unnecessary `null` checks in your code. If you use this method, make sure your code correctly handles empty lists. - -If you use `Authorization.findChallenge()`, no changes are necessary to your code. - -## Migration to Version 0.6 - -With version 0.6, _acme4j_ underwent a major change to the API. - -In previous versions, the resource classes like `Registration` or `Authorization` were plain data transport objects, and an `AcmeClient` was used for the actual server communication. Now, the resource classes communicate directly with the server. The result is an API that is more object oriented. - -Instead of an `AcmeClient`, you need a `Session` object now. The `Session` is initialized with the ACME server URI and your account's key pair. - -```java -KeyPair keyPair = ... // your account KeyPair -Session session = new Session("acme://letsencrypt.org/staging", keyPair); -``` - -Instead of creating a plain `Registration` object, you now bind it to the session. - -```java -URL accountLocationUrl = ... // your account's URL -Registration registration = Registration.bind(session, accountLocationUrl); -``` - -You must know your account's location URL. Use a `RegistrationBuilder` if you do not know it, or if you want to register a new account: - -```java -Registration registration; -try { - // Try to create a new Registration... - registration = new RegistrationBuilder().create(session); -} catch (AcmeConflictException ex) { - // It failed because your key was already registered. - // Retrieve the registration location URL from the exception. - registration = Registration.bind(session, ex.getLocation()); -} -``` - -Let me give an example of how to use the resource objects. To start an authorization process for a domain, we previously needed a `Registration` object, an `Authorization` object, and an `AcmeClient` instance. - -This is the *old* way: - -```java -AcmeClient client = ... // your ACME client -Registration registration = ... // your Registration - -Authorization auth = new Authorization(); -auth.setDomain("example.org"); - -client.newAuthorization(registration, auth); -``` - -Now, the `Registration` object takes care of everything: - -```java -Registration registration = ... // your Registration - -Authorization auth = registration.authorizeDomain("example.org"); -``` - -As you can see, the authorization method that actually invokes the ACME server has moved from `AcmeClient` to `Registration`. - -Let's continue the example. We find and trigger a HTTP challenge. - -Previously, it worked like this: - -```java -Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE); -challenge.authorize(registration); -client.triggerChallenge(registration, challenge); -``` - -With the new API, it is more concise now: - -```java -Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE); -challenge.trigger(); -``` - -Note that the `authorize()` method is not needed any more, and has been removed without replacement. - -As a rule of thumb, you will find the action methods in one of the objects you previously passed as parameter to the `AcmeClient` method. For example, when you wrote `client.triggerChallenge(registration, challenge)`, you will find the new `trigger` method in either `registration` or `challenge` (here it's `challenge`). - -The API has also been cleaned up, with many confusing setter methods being removed. If you should miss a setter method, you was actually not supposed to invoke it anyway. +* The `Session` object has been split into `Session` and `Login`. The `Session` now only tracks the communication, and does not need an account key pair any more. +* To get a `Login` to an existing `Account`, use `Session.login()` with the account key pair and account URL. +* If you create a new `Account` using the `AccountBuilder`, you must pass in the key pair via `AccountBuilder.useKeyPair()`. +* You can find all resource bind methods in `Login` now. diff --git a/src/site/markdown/usage/account.md b/src/site/markdown/usage/account.md index ea6ff75b..deb12510 100644 --- a/src/site/markdown/usage/account.md +++ b/src/site/markdown/usage/account.md @@ -1,32 +1,91 @@ -# Register an Account +# Register a new Account -If it is the first time you connect to the ACME server, you need to register your account key. +The first step is to register an account with the CA. -To do so, create an `AccountBuilder`, optionally add some contact information, agree to the terms of service, then invoke `create()`. 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. You should store it somewhere, because you will need it later. Unlike your key pair, the location is a public information that does not need security precautions. +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. + + + +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 in the `acme4j-utils` module. This call generates a RSA key pair with a 2048 bit key: ```java -AccountBuilder builder = new AccountBuilder(); -builder.addContact("mailto:acme@example.com"); -builder.agreeToTermsOfService(); +KeyPair accountKeyPair = KeyPairUtils.createKeyPair(2048); +``` -Account account = builder.create(session); +You can also create an elliptic curve key pair: + +```java +KeyPair accountKeyPair = KeyPairUtils.createECKeyPair("secp256r1"); +``` + + + +To save a `KeyPair` (actually, the private key of the key pair) to a pem file, you can use this snippet: + +```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()`: + +```java +Account account = new AccountBuilder() + .addContact("mailto:acme@example.com") + .agreeToTermsOfService() + .useKeyPair(keyPair) + .create(session); URL accountLocationUrl = account.getLocation(); ``` -You should not invoke `agreeToTermsOfService()` automatically, but let the user confirm the terms of service. To get a link to the current terms of services, you can invoke `session.getMetadata().getTermsOfService()`. +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. -## Find out your account's location URL +Now you have a key pair and the account's location URL. This is all you need for [logging in](./login.html). -You can also use the `AccountBuilder` to find out the location URL of your existing account: + + +## Find out Your Account's Location URL + +If you only have your account's `KeyPair`, you can use the `AccountBuilder` to find out the location `URL` of your account. ```java -Account account = new AccountBuilder().onlyExisting().create(session); +Account account = new AccountBuilder() + .onlyExisting() // Do not create a new account + .useKeyPair(keyPair) + .create(session); + 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. @@ -39,6 +98,8 @@ account.modify() .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. + ## 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. @@ -51,7 +112,7 @@ 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 `Session` that was bound to this `Account` instance. +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. @@ -65,4 +126,26 @@ 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. -Be very careful: 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: + +```java +String kid = ... // Key Identifier +SecretKey macKey = ... // MAC Key + +Account account = new AccountBuilder() + .agreeToTermsOfService() + .withKeyIdentifier(kid, macKey) + .useKeyPair(keyPair) + .create(session); +``` + +For your convenience, you can also pass a base64 encoded MAC Key as `String`. diff --git a/src/site/markdown/usage/certificate.md b/src/site/markdown/usage/certificate.md index 5906dc95..c3716cb6 100644 --- a/src/site/markdown/usage/certificate.md +++ b/src/site/markdown/usage/certificate.md @@ -2,10 +2,29 @@ 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 -order.update(); // make sure we have the current state +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. + + + +As soon as the status is `VALID`, you can retrieve a `Certificate` object: + +```java Certificate cert = order.getCertificate(); ``` @@ -25,22 +44,19 @@ Congratulations! You have just created your first certificate via _acme4j_. 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");) { +try (FileWriter fw = new FileWriter("cert-chain.crt")) { cert.writeCertificate(fw) } ``` -## Recreate a Certificate object -To recreate a `Certificate` object from the location, just bind it: - -```java -URL locationUrl = ... // location URL from cert.getLocation() -Certificate cert = Certificate.bind(session, locationUrl); -``` - ## Renewal -Certificates are only valid for a limited time, and need to be renewed before expiry. To find out the expiry date of a `X509Certificate`, invoke its `getNotAfter()` method. +Certificates are only valid for a limited time, and need to be renewed before expiry. + + 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.html) the certificate again. @@ -58,16 +74,15 @@ Optionally, you can provide a revocation reason that the ACME server may use whe cert.revoke(RevocationReason.KEY_COMPROMISE); ``` -## Revocation without account key pair +## 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 key pair that was used for signing the CSR. `Certificate` provides a special method for this case. +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 -URI acmeServerUri = ... // uri of the ACME server KeyPair domainKeyPair = ... // the key pair that was used for signing the CSR X509Certificate cert = ... // certificate to revoke -Certificate.revoke(acmeServerUri, domainKeyPair, cert, RevocationReason.KEY_COMPROMISE); +Certificate.revoke(session, domainKeyPair, cert, RevocationReason.KEY_COMPROMISE); ``` Note that there is no way to revoke a certificate if you have lost both your account's key pair and your domain's key pair. diff --git a/src/site/markdown/usage/index.md b/src/site/markdown/usage/index.md index c7d80368..bbc8f922 100644 --- a/src/site/markdown/usage/index.md +++ b/src/site/markdown/usage/index.md @@ -2,41 +2,10 @@ _acme4j_ is a client library that helps connecting to ACME servers without worrying about specification details. -The ACME protocol uses a public key to identify your account, so the very first step is to create a key pair. You can use external tools or standard Java methods to create it. A more convenient way is to use the `KeyPairUtils` class in the `acme4j-utils` module. +To get a certificate from your ACME CA, these steps need to be performed: -This call will generate a RSA key pair with a 2048 bit key: - -```java -KeyPair keyPair = KeyPairUtils.createKeyPair(2048); -``` - -You can also create an elliptic curve key pair: - -```java -KeyPair keyPair = KeyPairUtils.createECKeyPair("secp256r1"); -``` - -> **CAUTION**: Your KeyPair is the only key to 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 ask for assistance. **It is strongly recommended to keep your key pair in a safe place!** - -To save a `KeyPair` (actually, the private key of the key pair) to a pem file, use this snippet: - -```java -try (FileWriter fw = new FileWriter("keypair.pem")) { - KeyPairUtils.writeKeyPair(keyPair, 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); -} -``` - -Now that you have created (and saved) your account's key pair, you can start with registering an account and getting your first certificate. These steps need to be performed: - -* [Create a Session object](./session.html) -* [Register and Create an Account](./account.html) -* [Order a Certifiate](./order.html) +* [Start a Session](./session.html) +* [Register a new Account](./account.html) +* [Login into an Account](./login.html) +* [Order a Certificate](./order.html) * [Download a Certificate](./certificate.html) diff --git a/src/site/markdown/usage/login.md b/src/site/markdown/usage/login.md new file mode 100644 index 00000000..be6c5d63 --- /dev/null +++ b/src/site/markdown/usage/login.md @@ -0,0 +1,76 @@ +# 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(); +``` + + + +## 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. + + + + diff --git a/src/site/markdown/usage/order.md b/src/site/markdown/usage/order.md index 295a2275..7c91c46a 100644 --- a/src/site/markdown/usage/order.md +++ b/src/site/markdown/usage/order.md @@ -2,7 +2,7 @@ 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. +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. ```java Account account = ... // your Account object @@ -13,13 +13,25 @@ Order order = account.newOrder() .create(); ``` -The `Order` resource contains a collection of `Authorization`s that can be read from the `getAuthorizations()` method. You must process _all of them_ in order to get the certificate. + + +The `Order` resource contains a collection of `Authorization`s that can be read from the `getAuthorizations()` method. You must process _all of them_ in order to get the certificate, except those with a `VALID` status. + +```java +for (Authorization auth : order.getAuthorizations()) { + if (auth.getStatus() != Status.VALID) { + processAuth(auth); + } +} +``` ## Process an Authorization The `Authorization` instance contains further details about how you can prove ownership of your domain. An ACME server offers combinations of different authorization methods, called `Challenge`s. -`getChallenges()` returns a collection of all `Challenge`s offered by the CA for domain ownership validation. You only need to use _one_ of them to successfully authorize your domain. +`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 simplest way is to invoke `findChallenge()`, stating the challenge type your system is able to provide: @@ -37,30 +49,32 @@ After you have performed the necessary steps to set up the response to the chall challenge.trigger(); ``` -Now you have to wait for the server to test your response and set the challenge status to `VALID`. The easiest (but admittedly also the ugliest) way is to poll the status: +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: ```java -while (challenge.getStatus() != Status.VALID) { - Thread.sleep(3000L); - challenge.update(); +while (auth.getStatus() != Status.VALID) { + Thread.sleep(3000L); + auth.update(); } ``` -This is a very simple example. You should limit the number of loop iterations, and always abort the loop when the status should 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. 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. -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. +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. -`update()` may throw an `AcmeRetryAfterException`, giving an estimated instant in `getRetryAfter()` when the challenge is completed. You should then wait until that moment has been reached, before trying again. The state of your `Challenge` instance is still updated when this exception is thrown. +`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. -When the challenge status is `VALID`, you have successfully authorized your domain. +When the authorization status is `VALID`, you have successfully authorized your domain. ## Finalize the Order -After successfully completing all authorizations, the order needs to be finalized by a PKCS#10 CSR file. 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. It depends on the CA if other CSR properties (_Organization_, _Organization Unit_ etc.) are accepted. Some may require these properties to be set, while others may ignore them when generating the certificate. +After successfully completing all authorizations, the order needs to be finalized by providing PKCS#10 CSR file. 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. It depends on the CA if other CSR properties (_Organization_, _Organization Unit_ etc.) are accepted. Some may require these properties to be set, while others may ignore them when generating the certificate. CSR files can be generated with command line tools like `openssl`. Unfortunately the standard Java does not offer classes for that, so you'd have to resort to [Bouncy Castle](http://www.bouncycastle.org/java.html) if you want to create a CSR programmatically. In the `acme4j-utils` module, there is a [`CSRBuilder`](../apidocs/org/shredzone/acme4j/util/CSRBuilder.html) for your convenience. You can also use [`KeyPairUtils`](../apidocs/org/shredzone/acme4j/util/KeyPairUtils.html) for generating a new key pair for your domain. -> __Important:__ Do not just use your account key pair as domain key pair, but always generate separate key pairs! + ```java KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption @@ -86,12 +100,15 @@ After that, finalize the order: order.execute(csr); ``` -> __Note:__ The number of domains per certificate may be limited. See your CA's documentation for the limits. - ## 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. + + 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`: @@ -108,13 +125,16 @@ 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); ``` In the subsequent authorization process, you would 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. + ## Pre-Authorize a Domain @@ -124,10 +144,13 @@ It is possible to pro-actively authorize a domain. This can be useful to find ou Account account = ... // your Account object String domain = ... // Domain name to authorize -Authorization auth = account.preAuthorizeDomain(String domain); +Authorization auth = account.preAuthorizeDomain(domain); ``` -> __Note:__ Some CAs may not offer domain pre-authorization. `preAuthorizeDomain()` will then fail and throw an exception. + ## Deactivate an Authorization @@ -137,42 +160,6 @@ It is possible to deactivate an `Authorization`, for example if you sell the ass auth.deactivate(); ``` -## Restore a Challenge - -Validating a challenge can take a considerable amount of time and is a candidate for asynchronous execution. This can be a problem if you need to keep the `Challenge` object for a later time or a different Java environment. - -To recreate a `Challenge` object at a later time, all you need is to store the original object's `location` property: - -```java -Challenge originalChallenge = ... // some Challenge instance -URL challengeUrl = originalChallenge.getLocation(); -``` - -Later, you restore the `Challenge` object by invoking `Challenge.bind()`. - -```java -URL challengeUrl = ... // challenge URL -Challenge restoredChallenge = Challenge.bind(session, challengeUrl); -``` - -The `restoredChallenge` already reflects the current state of the challenge. - -You can also restore `Order` and `Authorization` objects at a later execution: - -```java -// Get the location URL -Order originalOrder = ... // your Order instance -URL orderUrl = originalOrder.getLocation(); - -// Restore the order -Order restoredOrder = Order.bind(session, orderUrl); -``` - -```java -// Get the location URL -Authorization originalAuthorization = ... // your Authorization instance -URL authorizationUrl = originalAuthorization.getLocation(); - -// Restore the order -Authorization restoredAuthorization = Authorization.bind(session, authorizationUrl); -``` + diff --git a/src/site/markdown/usage/session.md b/src/site/markdown/usage/session.md index 0a6ada91..68ebdf35 100644 --- a/src/site/markdown/usage/session.md +++ b/src/site/markdown/usage/session.md @@ -1,49 +1,36 @@ # Creating a Session -Central part of the communication is a [`Session`](../apidocs/org/shredzone/acme4j/Session.html) object, which contains the URI of the target ACME server, and your account key pair. The ACME server identifies your account by your public key, and verifies that your requests are signed with a matching private key. Your private key is _never_ transferred to the ACME server! +Central part of the communication is a [`Session`](../apidocs/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` tracks the communication with your account at the ACME server. If you want to access multiple accounts, you will need a separate `Session` instance for each of them. - -```java -KeyPair keyPair = ... // your key pair -URI acmeServerUri = ... // uri of the ACME server - -Session session = new Session(acmeServerUri, keyPair); -``` - -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: +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.api.letsencrypt.org/directory", keyPair); + = 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. Also, Java accepts the certificate used by the _Let's Encrypt_ server since JDK 8u101, calls to their servers are likely to throw a certificate exception on older versions. - -For this reason, special ACME URIs should be preferred: +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", keyPair); +Session session = new Session("acme://letsencrypt.org/staging"); ``` -Instead of a generic provider, this call uses a special _Let's Encrypt_ provider that also accepts the _Let's Encrypt_ certificate. +Instead of a generic provider, this call uses a special _Let's Encrypt_ provider. -Now that you have a `Session` object, you can use it to bind ACME resource objects. For example, this is the way to get an `Account` object to an existing account: +## Metadata + +Some CAs provide metadata related to their ACME server: ```java -URL accountLocationUrl = ... // your account's URL, as returned by Account.getLocation() - -Account account = Account.bind(session, accountLocationUrl); +Metadata meta = session.getMetadata(); +URI tos = meta.getTermsOfService(); +URL website = meta.getWebsite(); ``` -You can create any of the resource objects `Account`, `Authorization`, `Challenge`, `Certificate` and `Order` like that, as long as you know the corresponding resource URL. To get the resource URL, use the `getLocation()` method. +`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. -## Serialization +## Locale -All resource objects are serializable, so the current state of the object can be frozen by Java's serialization mechanism. +`Session.setLocale()` allows to select a different locale. Errors will be returned in that language, if supported by the CA. -However the `Session` the object is bound with is _not_ serialized! This is because the `Session` 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 `Session` yet. It is required to rebind it to a `Session`, by invoking its `rebind()` method. - -Serialization is only meant for short term storage at runtime, not for long term persistence. Do not share serialized data between different versions of _acme4j_. +By default, the system's default locale is used. diff --git a/src/site/resources/css/site.css b/src/site/resources/css/site.css new file mode 100644 index 00000000..9ba040f3 --- /dev/null +++ b/src/site/resources/css/site.css @@ -0,0 +1,20 @@ +.alert { + border: none; + padding: 5px 10px; +} + +.alert-info { + border-left: 10px solid #3a87ad; +} + +.alert-warning { + border-left: 10px solid #c09853; +} + +.alert-danger { + border-left: 10px solid #b94a48; +} + +.alert p { + margin: 0; +} diff --git a/src/site/site.xml b/src/site/site.xml index 882f7ea6..509f86cf 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -34,6 +34,7 @@ +