001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2017 Richard "Shred" Körber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j;
015
016import static java.util.Collections.unmodifiableList;
017import static java.util.stream.Collectors.toList;
018import static java.util.stream.Collectors.toUnmodifiableList;
019
020import java.io.IOException;
021import java.net.URL;
022import java.security.KeyPair;
023import java.time.Duration;
024import java.time.Instant;
025import java.util.List;
026import java.util.Optional;
027import java.util.function.Consumer;
028
029import edu.umd.cs.findbugs.annotations.Nullable;
030import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
031import org.bouncycastle.pkcs.PKCS10CertificationRequest;
032import org.shredzone.acme4j.exception.AcmeException;
033import org.shredzone.acme4j.exception.AcmeNotSupportedException;
034import org.shredzone.acme4j.toolbox.JSON;
035import org.shredzone.acme4j.toolbox.JSON.Value;
036import org.shredzone.acme4j.toolbox.JSONBuilder;
037import org.shredzone.acme4j.util.CSRBuilder;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041/**
042 * A representation of a certificate order at the CA.
043 */
044public class Order extends AcmeJsonResource {
045    private static final long serialVersionUID = 5435808648658292177L;
046    private static final Logger LOG = LoggerFactory.getLogger(Order.class);
047
048    private transient @Nullable Certificate certificate = null;
049    private transient @Nullable Certificate autoRenewalCertificate = null;
050    private transient @Nullable List<Authorization> authorizations = null;
051
052    protected Order(Login login, URL location) {
053        super(login, location);
054    }
055
056    /**
057     * Returns the current status of the order.
058     * <p>
059     * Possible values are: {@link Status#PENDING}, {@link Status#READY},
060     * {@link Status#PROCESSING}, {@link Status#VALID}, {@link Status#INVALID}.
061     * If the server supports STAR, another possible value is {@link Status#CANCELED}.
062     */
063    public Status getStatus() {
064        return getJSON().get("status").asStatus();
065    }
066
067    /**
068     * Returns a {@link Problem} document with the reason if the order has failed.
069     */
070    public Optional<Problem> getError() {
071        return getJSON().get("error").map(v -> v.asProblem(getLocation()));
072    }
073
074    /**
075     * Gets the expiry date of the authorization, if set by the server.
076     */
077    public Optional<Instant> getExpires() {
078        return getJSON().get("expires").map(Value::asInstant);
079    }
080
081    /**
082     * Gets a list of {@link Identifier} that are connected to this order.
083     *
084     * @since 2.3
085     */
086    public List<Identifier> getIdentifiers() {
087        return getJSON().get("identifiers")
088                    .asArray()
089                    .stream()
090                    .map(Value::asIdentifier)
091                    .collect(toUnmodifiableList());
092    }
093
094    /**
095     * Gets the "not before" date that was used for the order.
096     */
097    public Optional<Instant> getNotBefore() {
098        return getJSON().get("notBefore").map(Value::asInstant);
099    }
100
101    /**
102     * Gets the "not after" date that was used for the order.
103     */
104    public Optional<Instant> getNotAfter() {
105        return getJSON().get("notAfter").map(Value::asInstant);
106    }
107
108    /**
109     * Gets the {@link Authorization} that are required to fulfil this order, in no
110     * specific order.
111     */
112    public List<Authorization> getAuthorizations() {
113        if (authorizations == null) {
114            var login = getLogin();
115            authorizations = getJSON().get("authorizations")
116                    .asArray()
117                    .stream()
118                    .map(Value::asURL)
119                    .map(login::bindAuthorization)
120                    .collect(toList());
121        }
122        return unmodifiableList(authorizations);
123    }
124
125    /**
126     * Gets the location {@link URL} of where to send the finalization call to.
127     * <p>
128     * For internal purposes. Use {@link #execute(byte[])} to finalize an order.
129     */
130    public URL getFinalizeLocation() {
131        return getJSON().get("finalize").asURL();
132    }
133
134    /**
135     * Gets the {@link Certificate}.
136     *
137     * @throws IllegalStateException
138     *         if the order is not ready yet. You must finalize the order first, and wait
139     *         for the status to become {@link Status#VALID}.
140     */
141    @SuppressFBWarnings("EI_EXPOSE_REP")    // behavior is intended
142    public Certificate getCertificate() {
143        if (certificate == null) {
144            certificate = getJSON().get("certificate")
145                    .map(Value::asURL)
146                    .map(getLogin()::bindCertificate)
147                    .orElseThrow(() -> new IllegalStateException("Order is not completed"));
148        }
149        return certificate;
150    }
151
152    /**
153     * Gets the STAR extension's {@link Certificate} if it is available.
154     *
155     * @since 2.6
156     * @throws IllegalStateException
157     *         if the order is not ready yet. You must finalize the order first, and wait
158     *         for the status to become {@link Status#VALID}. It is also thrown if the
159     *         order has been {@link Status#CANCELED}.
160     */
161    @SuppressFBWarnings("EI_EXPOSE_REP")    // behavior is intended
162    public Certificate getAutoRenewalCertificate() {
163        if (autoRenewalCertificate == null) {
164            autoRenewalCertificate = getJSON().get("star-certificate")
165                    .optional()
166                    .map(Value::asURL)
167                    .map(getLogin()::bindCertificate)
168                    .orElseThrow(() -> new IllegalStateException("Order is in an invalid state"));
169        }
170        return autoRenewalCertificate;
171    }
172
173    /**
174     * Finalizes the order.
175     * <p>
176     * If the finalization was successful, the certificate is provided via
177     * {@link #getCertificate()}.
178     * <p>
179     * Even though the ACME protocol uses the term "finalize an order", this method is
180     * called {@link #execute(KeyPair)} to avoid confusion with the problematic
181     * {@link Object#finalize()} method.
182     *
183     * @param domainKeyPair
184     *         The {@link KeyPair} that is going to be certified. This is <em>not</em>
185     *         your account's keypair!
186     * @see #execute(KeyPair, Consumer)
187     * @see #execute(PKCS10CertificationRequest)
188     * @see #execute(byte[])
189     * @since 3.0.0
190     */
191    public void execute(KeyPair domainKeyPair) throws AcmeException {
192        execute(domainKeyPair, csrBuilder -> {});
193    }
194
195    /**
196     * Finalizes the order (see {@link #execute(KeyPair)}).
197     * <p>
198     * This method also accepts a builderConsumer that can be used to add further details
199     * to the CSR (e.g. your organization). The identifiers (IPs, domain names, etc.) are
200     * automatically added to the CSR.
201     *
202     * @param domainKeyPair
203     *         The {@link KeyPair} that is going to be used together with the certificate.
204     *         This is not your account's keypair!
205     * @param builderConsumer
206     *         {@link Consumer} that adds further details to the provided
207     *         {@link CSRBuilder}.
208     * @see #execute(KeyPair)
209     * @see #execute(PKCS10CertificationRequest)
210     * @see #execute(byte[])
211     * @since 3.0.0
212     */
213    public void execute(KeyPair domainKeyPair, Consumer<CSRBuilder> builderConsumer) throws AcmeException {
214        try {
215            var csrBuilder = new CSRBuilder();
216            csrBuilder.addIdentifiers(getIdentifiers());
217            builderConsumer.accept(csrBuilder);
218            csrBuilder.sign(domainKeyPair);
219            execute(csrBuilder.getCSR());
220        } catch (IOException ex) {
221            throw new AcmeException("Failed to create CSR", ex);
222        }
223    }
224
225    /**
226     * Finalizes the order (see {@link #execute(KeyPair)}).
227     * <p>
228     * This method receives a {@link PKCS10CertificationRequest} instance of a CSR that
229     * was generated externally. Use this method to gain full control over the content of
230     * the CSR. The CSR is not checked by acme4j, but just transported to the CA. It is
231     * your responsibility that it matches to the order.
232     *
233     * @param csr
234     *         {@link PKCS10CertificationRequest} to be used for this order.
235     * @see #execute(KeyPair)
236     * @see #execute(KeyPair, Consumer)
237     * @see #execute(byte[])
238     * @since 3.0.0
239     */
240    public void execute(PKCS10CertificationRequest csr) throws AcmeException {
241        try {
242            execute(csr.getEncoded());
243        } catch (IOException ex) {
244            throw new AcmeException("Invalid CSR", ex);
245        }
246    }
247
248    /**
249     * Finalizes the order (see {@link #execute(KeyPair)}).
250     * <p>
251     * This method receives a byte array containing an encoded CSR that was generated
252     * externally. Use this method to gain full control over the content of the CSR. The
253     * CSR is not checked by acme4j, but just transported to the CA. It is your
254     * responsibility that it matches to the order.
255     *
256     * @param csr
257     *         Binary representation of a CSR containing the parameters for the
258     *         certificate being requested, in DER format
259     */
260    public void execute(byte[] csr) throws AcmeException {
261        LOG.debug("finalize");
262        try (var conn = getSession().connect()) {
263            var claims = new JSONBuilder();
264            claims.putBase64("csr", csr);
265
266            conn.sendSignedRequest(getFinalizeLocation(), claims, getLogin());
267        }
268        invalidate();
269    }
270
271    /**
272     * Checks if this order is auto-renewing, according to the ACME STAR specifications.
273     *
274     * @since 2.3
275     */
276    public boolean isAutoRenewing() {
277        return getJSON().get("auto-renewal")
278                    .optional()
279                    .isPresent();
280    }
281
282    /**
283     * Returns the earliest date of validity of the first certificate issued.
284     *
285     * @since 2.3
286     * @throws AcmeNotSupportedException if auto-renewal is not supported
287     */
288    public Optional<Instant> getAutoRenewalStartDate() {
289        return getJSON().getFeature("auto-renewal")
290                    .map(Value::asObject)
291                    .orElseGet(JSON::empty)
292                    .get("start-date")
293                    .optional()
294                    .map(Value::asInstant);
295    }
296
297    /**
298     * Returns the latest date of validity of the last certificate issued.
299     *
300     * @since 2.3
301     * @throws AcmeNotSupportedException if auto-renewal is not supported
302     */
303    public Instant getAutoRenewalEndDate() {
304        return getJSON().getFeature("auto-renewal")
305                    .map(Value::asObject)
306                    .orElseGet(JSON::empty)
307                    .get("end-date")
308                    .asInstant();
309    }
310
311    /**
312     * Returns the maximum lifetime of each certificate.
313     *
314     * @since 2.3
315     * @throws AcmeNotSupportedException if auto-renewal is not supported
316     */
317    public Duration getAutoRenewalLifetime() {
318        return getJSON().getFeature("auto-renewal")
319                    .optional()
320                    .map(Value::asObject)
321                    .orElseGet(JSON::empty)
322                    .get("lifetime")
323                    .asDuration();
324    }
325
326    /**
327     * Returns the pre-date period of each certificate.
328     *
329     * @since 2.7
330     * @throws AcmeNotSupportedException if auto-renewal is not supported
331     */
332    public Optional<Duration> getAutoRenewalLifetimeAdjust() {
333        return getJSON().getFeature("auto-renewal")
334                    .optional()
335                    .map(Value::asObject)
336                    .orElseGet(JSON::empty)
337                    .get("lifetime-adjust")
338                    .optional()
339                    .map(Value::asDuration);
340    }
341
342    /**
343     * Returns {@code true} if STAR certificates from this order can also be fetched via
344     * GET requests.
345     *
346     * @since 2.6
347     */
348    public boolean isAutoRenewalGetEnabled() {
349        return getJSON().getFeature("auto-renewal")
350                    .optional()
351                    .map(Value::asObject)
352                    .orElseGet(JSON::empty)
353                    .get("allow-certificate-get")
354                    .optional()
355                    .map(Value::asBoolean)
356                    .orElse(false);
357    }
358
359    /**
360     * Cancels an auto-renewing order.
361     *
362     * @since 2.3
363     */
364    public void cancelAutoRenewal() throws AcmeException {
365        if (!getSession().getMetadata().isAutoRenewalEnabled()) {
366            throw new AcmeNotSupportedException("auto-renewal");
367        }
368
369        LOG.debug("cancel");
370        try (var conn = getSession().connect()) {
371            var claims = new JSONBuilder();
372            claims.put("status", "canceled");
373
374            conn.sendSignedRequest(getLocation(), claims, getLogin());
375            setJSON(conn.readJsonResponse());
376        }
377    }
378
379    @Override
380    protected void invalidate() {
381        super.invalidate();
382        certificate = null;
383        autoRenewalCertificate = null;
384        authorizations = null;
385    }
386}