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.EnumSet;
026import java.util.List;
027import java.util.Optional;
028import java.util.function.Consumer;
029
030import edu.umd.cs.findbugs.annotations.Nullable;
031import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
032import org.bouncycastle.pkcs.PKCS10CertificationRequest;
033import org.shredzone.acme4j.exception.AcmeException;
034import org.shredzone.acme4j.exception.AcmeNotSupportedException;
035import org.shredzone.acme4j.toolbox.JSON;
036import org.shredzone.acme4j.toolbox.JSON.Value;
037import org.shredzone.acme4j.toolbox.JSONBuilder;
038import org.shredzone.acme4j.util.CSRBuilder;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042/**
043 * A representation of a certificate order at the CA.
044 */
045public class Order extends AcmeJsonResource implements PollableResource {
046    private static final long serialVersionUID = 5435808648658292177L;
047    private static final Logger LOG = LoggerFactory.getLogger(Order.class);
048
049    private transient @Nullable Certificate certificate = null;
050    private transient @Nullable Certificate autoRenewalCertificate = null;
051    private transient @Nullable List<Authorization> authorizations = null;
052
053    protected Order(Login login, URL location) {
054        super(login, location);
055    }
056
057    /**
058     * Returns the current status of the order.
059     * <p>
060     * Possible values are: {@link Status#PENDING}, {@link Status#READY},
061     * {@link Status#PROCESSING}, {@link Status#VALID}, {@link Status#INVALID}.
062     * If the server supports STAR, another possible value is {@link Status#CANCELED}.
063     */
064    @Override
065    public Status getStatus() {
066        return getJSON().get("status").asStatus();
067    }
068
069    /**
070     * Returns a {@link Problem} document with the reason if the order has failed.
071     */
072    public Optional<Problem> getError() {
073        return getJSON().get("error").map(v -> v.asProblem(getLocation()));
074    }
075
076    /**
077     * Gets the expiry date of the authorization, if set by the server.
078     */
079    public Optional<Instant> getExpires() {
080        return getJSON().get("expires").map(Value::asInstant);
081    }
082
083    /**
084     * Gets a list of {@link Identifier} that are connected to this order.
085     *
086     * @since 2.3
087     */
088    public List<Identifier> getIdentifiers() {
089        return getJSON().get("identifiers")
090                    .asArray()
091                    .stream()
092                    .map(Value::asIdentifier)
093                    .collect(toUnmodifiableList());
094    }
095
096    /**
097     * Gets the "not before" date that was used for the order.
098     */
099    public Optional<Instant> getNotBefore() {
100        return getJSON().get("notBefore").map(Value::asInstant);
101    }
102
103    /**
104     * Gets the "not after" date that was used for the order.
105     */
106    public Optional<Instant> getNotAfter() {
107        return getJSON().get("notAfter").map(Value::asInstant);
108    }
109
110    /**
111     * Gets the {@link Authorization} that are required to fulfil this order, in no
112     * specific order.
113     */
114    public List<Authorization> getAuthorizations() {
115        if (authorizations == null) {
116            var login = getLogin();
117            authorizations = getJSON().get("authorizations")
118                    .asArray()
119                    .stream()
120                    .map(Value::asURL)
121                    .map(login::bindAuthorization)
122                    .collect(toList());
123        }
124        return unmodifiableList(authorizations);
125    }
126
127    /**
128     * Gets the location {@link URL} of where to send the finalization call to.
129     * <p>
130     * For internal purposes. Use {@link #execute(byte[])} to finalize an order.
131     */
132    public URL getFinalizeLocation() {
133        return getJSON().get("finalize").asURL();
134    }
135
136    /**
137     * Gets the {@link Certificate}.
138     *
139     * @throws IllegalStateException
140     *         if the order is not ready yet. You must finalize the order first, and wait
141     *         for the status to become {@link Status#VALID}.
142     */
143    @SuppressFBWarnings("EI_EXPOSE_REP")    // behavior is intended
144    public Certificate getCertificate() {
145        if (certificate == null) {
146            certificate = getJSON().get("star-certificate")
147                    .optional()
148                    .or(() -> getJSON().get("certificate").optional())
149                    .map(Value::asURL)
150                    .map(getLogin()::bindCertificate)
151                    .orElseThrow(() -> new IllegalStateException("Order is not completed"));
152        }
153        return certificate;
154    }
155
156    /**
157     * Gets the STAR extension's {@link Certificate} if it is available.
158     *
159     * @since 2.6
160     * @throws IllegalStateException
161     *         if the order is not ready yet. You must finalize the order first, and wait
162     *         for the status to become {@link Status#VALID}. It is also thrown if the
163     *         order has been {@link Status#CANCELED}.
164     * @deprecated Use {@link #getCertificate()} for STAR certificates as well.
165     */
166    @Deprecated
167    @SuppressFBWarnings("EI_EXPOSE_REP")    // behavior is intended
168    public Certificate getAutoRenewalCertificate() {
169        if (autoRenewalCertificate == null) {
170            autoRenewalCertificate = getJSON().get("star-certificate")
171                    .optional()
172                    .map(Value::asURL)
173                    .map(getLogin()::bindCertificate)
174                    .orElseThrow(() -> new IllegalStateException("Order is in an invalid state"));
175        }
176        return autoRenewalCertificate;
177    }
178
179    /**
180     * Returns whether this is a STAR certificate ({@code true}) or a standard certificate
181     * ({@code false}).
182     *
183     * @since 3.5.0
184     */
185    public boolean isAutoRenewalCertificate() {
186        return getJSON().contains("star-certificate");
187    }
188
189    /**
190     * Finalizes the order.
191     * <p>
192     * If the finalization was successful, the certificate is provided via
193     * {@link #getCertificate()}.
194     * <p>
195     * Even though the ACME protocol uses the term "finalize an order", this method is
196     * called {@link #execute(KeyPair)} to avoid confusion with the problematic
197     * {@link Object#finalize()} method.
198     *
199     * @param domainKeyPair
200     *         The {@link KeyPair} that is going to be certified. This is <em>not</em>
201     *         your account's keypair!
202     * @see #execute(KeyPair, Consumer)
203     * @see #execute(PKCS10CertificationRequest)
204     * @see #execute(byte[])
205     * @see #waitUntilReady(Duration)
206     * @see #waitForCompletion(Duration)
207     * @since 3.0.0
208     */
209    public void execute(KeyPair domainKeyPair) throws AcmeException {
210        execute(domainKeyPair, csrBuilder -> {});
211    }
212
213    /**
214     * Finalizes the order (see {@link #execute(KeyPair)}).
215     * <p>
216     * This method also accepts a builderConsumer that can be used to add further details
217     * to the CSR (e.g. your organization). The identifiers (IPs, domain names, etc.) are
218     * automatically added to the CSR.
219     *
220     * @param domainKeyPair
221     *         The {@link KeyPair} that is going to be used together with the certificate.
222     *         This is not your account's keypair!
223     * @param builderConsumer
224     *         {@link Consumer} that adds further details to the provided
225     *         {@link CSRBuilder}.
226     * @see #execute(KeyPair)
227     * @see #execute(PKCS10CertificationRequest)
228     * @see #execute(byte[])
229     * @see #waitUntilReady(Duration)
230     * @see #waitForCompletion(Duration)
231     * @since 3.0.0
232     */
233    public void execute(KeyPair domainKeyPair, Consumer<CSRBuilder> builderConsumer) throws AcmeException {
234        try {
235            var csrBuilder = new CSRBuilder();
236            csrBuilder.addIdentifiers(getIdentifiers());
237            builderConsumer.accept(csrBuilder);
238            csrBuilder.sign(domainKeyPair);
239            execute(csrBuilder.getCSR());
240        } catch (IOException ex) {
241            throw new AcmeException("Failed to create CSR", ex);
242        }
243    }
244
245    /**
246     * Finalizes the order (see {@link #execute(KeyPair)}).
247     * <p>
248     * This method receives a {@link PKCS10CertificationRequest} instance of a CSR that
249     * was generated externally. Use this method to gain full control over the content of
250     * the CSR. The CSR is not checked by acme4j, but just transported to the CA. It is
251     * your responsibility that it matches to the order.
252     *
253     * @param csr
254     *         {@link PKCS10CertificationRequest} to be used for this order.
255     * @see #execute(KeyPair)
256     * @see #execute(KeyPair, Consumer)
257     * @see #execute(byte[])
258     * @see #waitUntilReady(Duration)
259     * @see #waitForCompletion(Duration)
260     * @since 3.0.0
261     */
262    public void execute(PKCS10CertificationRequest csr) throws AcmeException {
263        try {
264            execute(csr.getEncoded());
265        } catch (IOException ex) {
266            throw new AcmeException("Invalid CSR", ex);
267        }
268    }
269
270    /**
271     * Finalizes the order (see {@link #execute(KeyPair)}).
272     * <p>
273     * This method receives a byte array containing an encoded CSR that was generated
274     * externally. Use this method to gain full control over the content of the CSR. The
275     * CSR is not checked by acme4j, but just transported to the CA. It is your
276     * responsibility that it matches to the order.
277     *
278     * @param csr
279     *         Binary representation of a CSR containing the parameters for the
280     *         certificate being requested, in DER format
281     * @see #waitUntilReady(Duration)
282     * @see #waitForCompletion(Duration)
283     */
284    public void execute(byte[] csr) throws AcmeException {
285        LOG.debug("finalize");
286        try (var conn = getSession().connect()) {
287            var claims = new JSONBuilder();
288            claims.putBase64("csr", csr);
289
290            conn.sendSignedRequest(getFinalizeLocation(), claims, getLogin());
291        }
292        invalidate();
293    }
294
295    /**
296     * Waits until the order is ready for finalization.
297     * <p>
298     * Is is ready if it reaches {@link Status#READY}. The method will also return if the
299     * order already has another terminal state, which is either {@link Status#VALID} or
300     * {@link Status#INVALID}.
301     * <p>
302     * This method is synchronous and blocks the current thread.
303     *
304     * @param timeout
305     *         Timeout until a terminal status must have been reached
306     * @return Status that was reached
307     * @since 3.4.0
308     */
309    public Status waitUntilReady(Duration timeout)
310            throws AcmeException, InterruptedException {
311        return waitForStatus(EnumSet.of(Status.READY, Status.VALID, Status.INVALID), timeout);
312    }
313
314    /**
315     * Waits until the order finalization is completed.
316     * <p>
317     * Is is completed if it reaches either {@link Status#VALID} or
318     * {@link Status#INVALID}.
319     * <p>
320     * This method is synchronous and blocks the current thread.
321     *
322     * @param timeout
323     *         Timeout until a terminal status must have been reached
324     * @return Status that was reached
325     * @since 3.4.0
326     */
327    public Status waitForCompletion(Duration timeout)
328            throws AcmeException, InterruptedException {
329        return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout);
330    }
331
332    /**
333     * Checks if this order is auto-renewing, according to the ACME STAR specifications.
334     *
335     * @since 2.3
336     */
337    public boolean isAutoRenewing() {
338        return getJSON().get("auto-renewal")
339                    .optional()
340                    .isPresent();
341    }
342
343    /**
344     * Returns the earliest date of validity of the first certificate issued.
345     *
346     * @since 2.3
347     * @throws AcmeNotSupportedException if auto-renewal is not supported
348     */
349    public Optional<Instant> getAutoRenewalStartDate() {
350        return getJSON().getFeature("auto-renewal")
351                    .map(Value::asObject)
352                    .orElseGet(JSON::empty)
353                    .get("start-date")
354                    .optional()
355                    .map(Value::asInstant);
356    }
357
358    /**
359     * Returns the latest date of validity of the last certificate issued.
360     *
361     * @since 2.3
362     * @throws AcmeNotSupportedException if auto-renewal is not supported
363     */
364    public Instant getAutoRenewalEndDate() {
365        return getJSON().getFeature("auto-renewal")
366                    .map(Value::asObject)
367                    .orElseGet(JSON::empty)
368                    .get("end-date")
369                    .asInstant();
370    }
371
372    /**
373     * Returns the maximum lifetime of each certificate.
374     *
375     * @since 2.3
376     * @throws AcmeNotSupportedException if auto-renewal is not supported
377     */
378    public Duration getAutoRenewalLifetime() {
379        return getJSON().getFeature("auto-renewal")
380                    .optional()
381                    .map(Value::asObject)
382                    .orElseGet(JSON::empty)
383                    .get("lifetime")
384                    .asDuration();
385    }
386
387    /**
388     * Returns the pre-date period of each certificate.
389     *
390     * @since 2.7
391     * @throws AcmeNotSupportedException if auto-renewal is not supported
392     */
393    public Optional<Duration> getAutoRenewalLifetimeAdjust() {
394        return getJSON().getFeature("auto-renewal")
395                    .optional()
396                    .map(Value::asObject)
397                    .orElseGet(JSON::empty)
398                    .get("lifetime-adjust")
399                    .optional()
400                    .map(Value::asDuration);
401    }
402
403    /**
404     * Returns {@code true} if STAR certificates from this order can also be fetched via
405     * GET requests.
406     *
407     * @since 2.6
408     */
409    public boolean isAutoRenewalGetEnabled() {
410        return getJSON().getFeature("auto-renewal")
411                    .optional()
412                    .map(Value::asObject)
413                    .orElseGet(JSON::empty)
414                    .get("allow-certificate-get")
415                    .optional()
416                    .map(Value::asBoolean)
417                    .orElse(false);
418    }
419
420    /**
421     * Cancels an auto-renewing order.
422     *
423     * @since 2.3
424     */
425    public void cancelAutoRenewal() throws AcmeException {
426        if (!getSession().getMetadata().isAutoRenewalEnabled()) {
427            throw new AcmeNotSupportedException("auto-renewal");
428        }
429
430        LOG.debug("cancel");
431        try (var conn = getSession().connect()) {
432            var claims = new JSONBuilder();
433            claims.put("status", "canceled");
434
435            conn.sendSignedRequest(getLocation(), claims, getLogin());
436            setJSON(conn.readJsonResponse());
437        }
438    }
439
440    /**
441     * Returns the selected profile.
442     *
443     * @since 3.5.0
444     * @throws AcmeNotSupportedException if profile is not supported
445     */
446    public String getProfile() {
447        return getJSON().getFeature("profile").asString();
448    }
449
450    @Override
451    protected void invalidate() {
452        super.invalidate();
453        certificate = null;
454        autoRenewalCertificate = null;
455        authorizations = null;
456    }
457}