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.Objects.requireNonNull;
017import static java.util.stream.Collectors.toList;
018import static org.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier;
019
020import java.security.cert.X509Certificate;
021import java.time.Duration;
022import java.time.Instant;
023import java.util.Collection;
024import java.util.LinkedHashSet;
025import java.util.Objects;
026import java.util.Set;
027
028import edu.umd.cs.findbugs.annotations.Nullable;
029import org.shredzone.acme4j.connector.Resource;
030import org.shredzone.acme4j.exception.AcmeException;
031import org.shredzone.acme4j.exception.AcmeNotSupportedException;
032import org.shredzone.acme4j.toolbox.JSONBuilder;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036/**
037 * Start a new certificate {@link Order}.
038 * <p>
039 * Use {@link Login#newOrder()} or {@link Account#newOrder()} to create a new
040 * {@link OrderBuilder} instance. Both methods are identical.
041 */
042public class OrderBuilder {
043    private static final Logger LOG = LoggerFactory.getLogger(OrderBuilder.class);
044
045    private final Login login;
046
047    private final Set<Identifier> identifierSet = new LinkedHashSet<>();
048    private @Nullable Instant notBefore;
049    private @Nullable Instant notAfter;
050    private @Nullable String replaces;
051    private boolean autoRenewal;
052    private @Nullable Instant autoRenewalStart;
053    private @Nullable Instant autoRenewalEnd;
054    private @Nullable Duration autoRenewalLifetime;
055    private @Nullable Duration autoRenewalLifetimeAdjust;
056    private boolean autoRenewalGet;
057
058    /**
059     * Create a new {@link OrderBuilder}.
060     *
061     * @param login
062     *            {@link Login} to bind with
063     */
064    protected OrderBuilder(Login login) {
065        this.login = login;
066    }
067
068    /**
069     * Adds a domain name to the order.
070     *
071     * @param domain
072     *            Name of a domain to be ordered. May be a wildcard domain if supported by
073     *            the CA. IDN names are accepted and will be ACE encoded automatically.
074     * @return itself
075     */
076    public OrderBuilder domain(String domain) {
077        return identifier(Identifier.dns(domain));
078    }
079
080    /**
081     * Adds domain names to the order.
082     *
083     * @param domains
084     *            Collection of domain names to be ordered. May be wildcard domains if
085     *            supported by the CA. IDN names are accepted and will be ACE encoded
086     *            automatically.
087     * @return itself
088     */
089    public OrderBuilder domains(String... domains) {
090        for (var domain : requireNonNull(domains, "domains")) {
091            domain(domain);
092        }
093        return this;
094    }
095
096    /**
097     * Adds a collection of domain names to the order.
098     *
099     * @param domains
100     *            Collection of domain names to be ordered. May be wildcard domains if
101     *            supported by the CA. IDN names are accepted and will be ACE encoded
102     *            automatically.
103     * @return itself
104     */
105    public OrderBuilder domains(Collection<String> domains) {
106        requireNonNull(domains, "domains").forEach(this::domain);
107        return this;
108    }
109
110    /**
111     * Adds an {@link Identifier} to the order.
112     *
113     * @param identifier
114     *            {@link Identifier} to be added to the order.
115     * @return itself
116     * @since 2.3
117     */
118    public OrderBuilder identifier(Identifier identifier) {
119        identifierSet.add(requireNonNull(identifier, "identifier"));
120        return this;
121    }
122
123    /**
124     * Adds a collection of {@link Identifier} to the order.
125     *
126     * @param identifiers
127     *            Collection of {@link Identifier} to be added to the order.
128     * @return itself
129     * @since 2.3
130     */
131    public OrderBuilder identifiers(Collection<Identifier> identifiers) {
132        requireNonNull(identifiers, "identifiers").forEach(this::identifier);
133        return this;
134    }
135
136    /**
137     * Sets a "not before" date in the certificate. May be ignored by the CA.
138     *
139     * @param notBefore "not before" date
140     * @return itself
141     */
142    public OrderBuilder notBefore(Instant notBefore) {
143        if (autoRenewal) {
144            throw new IllegalArgumentException("cannot combine notBefore with autoRenew");
145        }
146        this.notBefore = requireNonNull(notBefore, "notBefore");
147        return this;
148    }
149
150    /**
151     * Sets a "not after" date in the certificate. May be ignored by the CA.
152     *
153     * @param notAfter "not after" date
154     * @return itself
155     */
156    public OrderBuilder notAfter(Instant notAfter) {
157        if (autoRenewal) {
158            throw new IllegalArgumentException("cannot combine notAfter with autoRenew");
159        }
160        this.notAfter = requireNonNull(notAfter, "notAfter");
161        return this;
162    }
163
164    /**
165     * Enables short-term automatic renewal of the certificate, if supported by the CA.
166     * <p>
167     * Automatic renewals cannot be combined with {@link #notBefore(Instant)} or
168     * {@link #notAfter(Instant)}.
169     *
170     * @return itself
171     * @since 2.3
172     */
173    public OrderBuilder autoRenewal() {
174        if (notBefore != null || notAfter != null) {
175            throw new IllegalArgumentException("cannot combine notBefore/notAfter with autoRenewal");
176        }
177        this.autoRenewal = true;
178        return this;
179    }
180
181    /**
182     * Notifies the CA that the ordered certificate will replace a previously issued
183     * certificate. The certificate is identified by its ARI unique identifier.
184     * <p>
185     * Optional, only supported if the CA provides renewal information. However, in this
186     * case the client <em>should</em> include this field.
187     *
188     * @param uniqueId
189     *         Certificate's renewal unique identifier.
190     * @return itself
191     * @draft This method is currently based on an RFC draft. It may be changed or removed
192     * without notice to reflect future changes to the draft. SemVer rules do not apply
193     * here.
194     * @since 3.2.0
195     */
196    public OrderBuilder replaces(String uniqueId) {
197        autoRenewal();
198        this.replaces = Objects.requireNonNull(uniqueId);
199        return this;
200    }
201
202    /**
203     * Notifies the CA that the ordered certificate will replace a previously issued
204     * certificate.
205     * <p>
206     * Optional, only supported if the CA provides renewal information. However, in this
207     * case the client <em>should</em> include this field.
208     *
209     * @param certificate
210     *         Certificate to be replaced
211     * @return itself
212     * @draft This method is currently based on an RFC draft. It may be changed or removed
213     * without notice to reflect future changes to the draft. SemVer rules do not apply
214     * here.
215     * @since 3.2.0
216     */
217    public OrderBuilder replaces(X509Certificate certificate) {
218        return replaces(getRenewalUniqueIdentifier(certificate));
219    }
220
221    /**
222     * Notifies the CA that the ordered certificate will replace a previously issued
223     * certificate.
224     * <p>
225     * Optional, only supported if the CA provides renewal information. However, in this
226     * case the client <em>should</em> include this field.
227     *
228     * @param certificate
229     *         Certificate to be replaced
230     * @return itself
231     * @draft This method is currently based on an RFC draft. It may be changed or removed
232     * without notice to reflect future changes to the draft. SemVer rules do not apply
233     * here.
234     * @since 3.2.0
235     */
236    public OrderBuilder replaces(Certificate certificate) {
237        return replaces(certificate.getCertificate());
238    }
239
240    /**
241     * Sets the earliest date of validity of the first issued certificate. If not set,
242     * the start date is the earliest possible date.
243     * <p>
244     * Implies {@link #autoRenewal()}.
245     *
246     * @param start
247     *            Start date of validity
248     * @return itself
249     * @since 2.3
250     */
251    public OrderBuilder autoRenewalStart(Instant start) {
252        autoRenewal();
253        this.autoRenewalStart = requireNonNull(start, "start");
254        return this;
255    }
256
257    /**
258     * Sets the latest date of validity of the last issued certificate. If not set, the
259     * CA's default is used.
260     * <p>
261     * Implies {@link #autoRenewal()}.
262     *
263     * @param end
264     *            End date of validity
265     * @return itself
266     * @see Metadata#getAutoRenewalMaxDuration()
267     * @since 2.3
268     */
269    public OrderBuilder autoRenewalEnd(Instant end) {
270        autoRenewal();
271        this.autoRenewalEnd = requireNonNull(end, "end");
272        return this;
273    }
274
275    /**
276     * Sets the maximum validity period of each certificate. If not set, the CA's
277     * default is used.
278     * <p>
279     * Implies {@link #autoRenewal()}.
280     *
281     * @param duration
282     *            Duration of validity of each certificate
283     * @return itself
284     * @see Metadata#getAutoRenewalMinLifetime()
285     * @since 2.3
286     */
287    public OrderBuilder autoRenewalLifetime(Duration duration) {
288        autoRenewal();
289        this.autoRenewalLifetime = requireNonNull(duration, "duration");
290        return this;
291    }
292
293    /**
294     * Sets the amount of pre-dating each certificate. If not set, the CA's
295     * default (0) is used.
296     * <p>
297     * Implies {@link #autoRenewal()}.
298     *
299     * @param duration
300     *            Duration of certificate pre-dating
301     * @return itself
302     * @since 2.7
303     */
304    public OrderBuilder autoRenewalLifetimeAdjust(Duration duration) {
305        autoRenewal();
306        this.autoRenewalLifetimeAdjust = requireNonNull(duration, "duration");
307        return this;
308    }
309
310    /**
311     * Announces that the client wishes to fetch the auto-renewed certificate via GET
312     * request. If not used, the STAR certificate can only be fetched via POST-as-GET
313     * request. {@link Metadata#isAutoRenewalGetAllowed()} must return {@code true} in
314     * order for this option to work.
315     * <p>
316     * This option is only needed if you plan to fetch the STAR certificate via other
317     * means than by using acme4j. acme4j is fetching certificates via POST-as-GET
318     * request.
319     * <p>
320     * Implies {@link #autoRenewal()}.
321     *
322     * @return itself
323     * @since 2.6
324     */
325    public OrderBuilder autoRenewalEnableGet() {
326        autoRenewal();
327        this.autoRenewalGet = true;
328        return this;
329    }
330
331    /**
332     * Sends a new order to the server, and returns an {@link Order} object.
333     *
334     * @return {@link Order} that was created
335     */
336    public Order create() throws AcmeException {
337        if (identifierSet.isEmpty()) {
338            throw new IllegalArgumentException("At least one identifer is required");
339        }
340
341        var session = login.getSession();
342
343        if (autoRenewal && !session.getMetadata().isAutoRenewalEnabled()) {
344            throw new AcmeNotSupportedException("auto-renewal");
345        }
346
347        LOG.debug("create");
348        try (var conn = session.connect()) {
349            var claims = new JSONBuilder();
350            claims.array("identifiers", identifierSet.stream().map(Identifier::toMap).collect(toList()));
351
352            if (notBefore != null) {
353                claims.put("notBefore", notBefore);
354            }
355            if (notAfter != null) {
356                claims.put("notAfter", notAfter);
357            }
358
359            if (autoRenewal) {
360                var arClaims = claims.object("auto-renewal");
361                if (autoRenewalStart != null) {
362                    arClaims.put("start-date", autoRenewalStart);
363                }
364                if (autoRenewalStart != null) {
365                    arClaims.put("end-date", autoRenewalEnd);
366                }
367                if (autoRenewalLifetime != null) {
368                    arClaims.put("lifetime", autoRenewalLifetime);
369                }
370                if (autoRenewalLifetimeAdjust != null) {
371                    arClaims.put("lifetime-adjust", autoRenewalLifetimeAdjust);
372                }
373                if (autoRenewalGet) {
374                    arClaims.put("allow-certificate-get", autoRenewalGet);
375                }
376            }
377
378            if (replaces != null) {
379                claims.put("replaces", replaces);
380            }
381
382            conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
383
384            var order = new Order(login, conn.getLocation());
385            order.setJSON(conn.readJsonResponse());
386            return order;
387        }
388    }
389
390}