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;
018
019import java.io.IOException;
020import java.io.Serial;
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    @Serial
047    private static final long serialVersionUID = 5435808648658292177L;
048    private static final Logger LOG = LoggerFactory.getLogger(Order.class);
049
050    private transient @Nullable Certificate certificate = 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                    .toList();
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     * Returns whether this is a STAR certificate ({@code true}) or a standard certificate
158     * ({@code false}).
159     *
160     * @since 3.5.0
161     */
162    public boolean isAutoRenewalCertificate() {
163        return getJSON().contains("star-certificate");
164    }
165
166    /**
167     * Finalizes the order.
168     * <p>
169     * If the finalization was successful, the certificate is provided via
170     * {@link #getCertificate()}.
171     * <p>
172     * Even though the ACME protocol uses the term "finalize an order", this method is
173     * called {@link #execute(KeyPair)} to avoid confusion with the problematic
174     * {@link Object#finalize()} method.
175     *
176     * @param domainKeyPair
177     *         The {@link KeyPair} that is going to be certified. This is <em>not</em>
178     *         your account's keypair!
179     * @see #execute(KeyPair, Consumer)
180     * @see #execute(PKCS10CertificationRequest)
181     * @see #execute(byte[])
182     * @see #waitUntilReady(Duration)
183     * @see #waitForCompletion(Duration)
184     * @since 3.0.0
185     */
186    public void execute(KeyPair domainKeyPair) throws AcmeException {
187        execute(domainKeyPair, csrBuilder -> {});
188    }
189
190    /**
191     * Finalizes the order (see {@link #execute(KeyPair)}).
192     * <p>
193     * This method also accepts a builderConsumer that can be used to add further details
194     * to the CSR (e.g. your organization). The identifiers (IPs, domain names, etc.) are
195     * automatically added to the CSR.
196     *
197     * @param domainKeyPair
198     *         The {@link KeyPair} that is going to be used together with the certificate.
199     *         This is not your account's keypair!
200     * @param builderConsumer
201     *         {@link Consumer} that adds further details to the provided
202     *         {@link CSRBuilder}.
203     * @see #execute(KeyPair)
204     * @see #execute(PKCS10CertificationRequest)
205     * @see #execute(byte[])
206     * @see #waitUntilReady(Duration)
207     * @see #waitForCompletion(Duration)
208     * @since 3.0.0
209     */
210    public void execute(KeyPair domainKeyPair, Consumer<CSRBuilder> builderConsumer) throws AcmeException {
211        try {
212            var csrBuilder = new CSRBuilder();
213            csrBuilder.addIdentifiers(getIdentifiers());
214            builderConsumer.accept(csrBuilder);
215            csrBuilder.sign(domainKeyPair);
216            execute(csrBuilder.getCSR());
217        } catch (IOException ex) {
218            throw new AcmeException("Failed to create CSR", ex);
219        }
220    }
221
222    /**
223     * Finalizes the order (see {@link #execute(KeyPair)}).
224     * <p>
225     * This method receives a {@link PKCS10CertificationRequest} instance of a CSR that
226     * was generated externally. Use this method to gain full control over the content of
227     * the CSR. The CSR is not checked by acme4j, but just transported to the CA. It is
228     * your responsibility that it matches to the order.
229     *
230     * @param csr
231     *         {@link PKCS10CertificationRequest} to be used for this order.
232     * @see #execute(KeyPair)
233     * @see #execute(KeyPair, Consumer)
234     * @see #execute(byte[])
235     * @see #waitUntilReady(Duration)
236     * @see #waitForCompletion(Duration)
237     * @since 3.0.0
238     */
239    public void execute(PKCS10CertificationRequest csr) throws AcmeException {
240        try {
241            execute(csr.getEncoded());
242        } catch (IOException ex) {
243            throw new AcmeException("Invalid CSR", ex);
244        }
245    }
246
247    /**
248     * Finalizes the order (see {@link #execute(KeyPair)}).
249     * <p>
250     * This method receives a byte array containing an encoded CSR that was generated
251     * externally. Use this method to gain full control over the content of the CSR. The
252     * CSR is not checked by acme4j, but just transported to the CA. It is your
253     * responsibility that it matches to the order.
254     *
255     * @param csr
256     *         Binary representation of a CSR containing the parameters for the
257     *         certificate being requested, in DER format
258     * @see #waitUntilReady(Duration)
259     * @see #waitForCompletion(Duration)
260     */
261    public void execute(byte[] csr) throws AcmeException {
262        LOG.debug("finalize");
263        try (var conn = getSession().connect()) {
264            var claims = new JSONBuilder();
265            claims.putBase64("csr", csr);
266
267            conn.sendSignedRequest(getFinalizeLocation(), claims, getLogin());
268        }
269        invalidate();
270    }
271
272    /**
273     * Waits until the order is ready for finalization.
274     * <p>
275     * Is is ready if it reaches {@link Status#READY}. The method will also return if the
276     * order already has another terminal state, which is either {@link Status#VALID} or
277     * {@link Status#INVALID}.
278     * <p>
279     * This method is synchronous and blocks the current thread.
280     *
281     * @param timeout
282     *         Timeout until a terminal status must have been reached
283     * @return Status that was reached
284     * @since 3.4.0
285     */
286    public Status waitUntilReady(Duration timeout)
287            throws AcmeException, InterruptedException {
288        return waitForStatus(EnumSet.of(Status.READY, Status.VALID, Status.INVALID), timeout);
289    }
290
291    /**
292     * Waits until the order finalization is completed.
293     * <p>
294     * Is is completed if it reaches either {@link Status#VALID} or
295     * {@link Status#INVALID}.
296     * <p>
297     * This method is synchronous and blocks the current thread.
298     *
299     * @param timeout
300     *         Timeout until a terminal status must have been reached
301     * @return Status that was reached
302     * @since 3.4.0
303     */
304    public Status waitForCompletion(Duration timeout)
305            throws AcmeException, InterruptedException {
306        return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout);
307    }
308
309    /**
310     * Checks if this order is auto-renewing, according to the ACME STAR specifications.
311     *
312     * @since 2.3
313     */
314    public boolean isAutoRenewing() {
315        return getJSON().get("auto-renewal")
316                    .optional()
317                    .isPresent();
318    }
319
320    /**
321     * Returns the earliest date of validity of the first certificate issued.
322     *
323     * @since 2.3
324     * @throws AcmeNotSupportedException if auto-renewal is not supported
325     */
326    public Optional<Instant> getAutoRenewalStartDate() {
327        return getJSON().getFeature("auto-renewal")
328                    .map(Value::asObject)
329                    .orElseGet(JSON::empty)
330                    .get("start-date")
331                    .optional()
332                    .map(Value::asInstant);
333    }
334
335    /**
336     * Returns the latest date of validity of the last certificate issued.
337     *
338     * @since 2.3
339     * @throws AcmeNotSupportedException if auto-renewal is not supported
340     */
341    public Instant getAutoRenewalEndDate() {
342        return getJSON().getFeature("auto-renewal")
343                    .map(Value::asObject)
344                    .orElseGet(JSON::empty)
345                    .get("end-date")
346                    .asInstant();
347    }
348
349    /**
350     * Returns the maximum lifetime of each certificate.
351     *
352     * @since 2.3
353     * @throws AcmeNotSupportedException if auto-renewal is not supported
354     */
355    public Duration getAutoRenewalLifetime() {
356        return getJSON().getFeature("auto-renewal")
357                    .optional()
358                    .map(Value::asObject)
359                    .orElseGet(JSON::empty)
360                    .get("lifetime")
361                    .asDuration();
362    }
363
364    /**
365     * Returns the pre-date period of each certificate.
366     *
367     * @since 2.7
368     * @throws AcmeNotSupportedException if auto-renewal is not supported
369     */
370    public Optional<Duration> getAutoRenewalLifetimeAdjust() {
371        return getJSON().getFeature("auto-renewal")
372                    .optional()
373                    .map(Value::asObject)
374                    .orElseGet(JSON::empty)
375                    .get("lifetime-adjust")
376                    .optional()
377                    .map(Value::asDuration);
378    }
379
380    /**
381     * Returns {@code true} if STAR certificates from this order can also be fetched via
382     * GET requests.
383     *
384     * @since 2.6
385     */
386    public boolean isAutoRenewalGetEnabled() {
387        return getJSON().getFeature("auto-renewal")
388                    .optional()
389                    .map(Value::asObject)
390                    .orElseGet(JSON::empty)
391                    .get("allow-certificate-get")
392                    .optional()
393                    .map(Value::asBoolean)
394                    .orElse(false);
395    }
396
397    /**
398     * Cancels an auto-renewing order.
399     *
400     * @since 2.3
401     */
402    public void cancelAutoRenewal() throws AcmeException {
403        if (!getSession().getMetadata().isAutoRenewalEnabled()) {
404            throw new AcmeNotSupportedException("auto-renewal");
405        }
406
407        LOG.debug("cancel");
408        try (var conn = getSession().connect()) {
409            var claims = new JSONBuilder();
410            claims.put("status", "canceled");
411
412            conn.sendSignedRequest(getLocation(), claims, getLogin());
413            setJSON(conn.readJsonResponse());
414        }
415    }
416
417    /**
418     * Returns the selected profile.
419     *
420     * @since 3.5.0
421     * @throws AcmeNotSupportedException if profile is not supported
422     */
423    public String getProfile() {
424        return getJSON().getFeature("profile").asString();
425    }
426
427    @Override
428    protected void invalidate() {
429        super.invalidate();
430        certificate = null;
431        authorizations = null;
432    }
433}