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     * Sets the earliest date of validity of the first issued certificate. If not set,
183     * the start date is the earliest possible date.
184     * <p>
185     * Implies {@link #autoRenewal()}.
186     *
187     * @param start
188     *            Start date of validity
189     * @return itself
190     * @since 2.3
191     */
192    public OrderBuilder autoRenewalStart(Instant start) {
193        autoRenewal();
194        this.autoRenewalStart = requireNonNull(start, "start");
195        return this;
196    }
197
198    /**
199     * Sets the latest date of validity of the last issued certificate. If not set, the
200     * CA's default is used.
201     * <p>
202     * Implies {@link #autoRenewal()}.
203     *
204     * @param end
205     *            End date of validity
206     * @return itself
207     * @see Metadata#getAutoRenewalMaxDuration()
208     * @since 2.3
209     */
210    public OrderBuilder autoRenewalEnd(Instant end) {
211        autoRenewal();
212        this.autoRenewalEnd = requireNonNull(end, "end");
213        return this;
214    }
215
216    /**
217     * Sets the maximum validity period of each certificate. If not set, the CA's
218     * default is used.
219     * <p>
220     * Implies {@link #autoRenewal()}.
221     *
222     * @param duration
223     *            Duration of validity of each certificate
224     * @return itself
225     * @see Metadata#getAutoRenewalMinLifetime()
226     * @since 2.3
227     */
228    public OrderBuilder autoRenewalLifetime(Duration duration) {
229        autoRenewal();
230        this.autoRenewalLifetime = requireNonNull(duration, "duration");
231        return this;
232    }
233
234    /**
235     * Sets the amount of pre-dating each certificate. If not set, the CA's
236     * default (0) is used.
237     * <p>
238     * Implies {@link #autoRenewal()}.
239     *
240     * @param duration
241     *            Duration of certificate pre-dating
242     * @return itself
243     * @since 2.7
244     */
245    public OrderBuilder autoRenewalLifetimeAdjust(Duration duration) {
246        autoRenewal();
247        this.autoRenewalLifetimeAdjust = requireNonNull(duration, "duration");
248        return this;
249    }
250
251    /**
252     * Announces that the client wishes to fetch the auto-renewed certificate via GET
253     * request. If not used, the STAR certificate can only be fetched via POST-as-GET
254     * request. {@link Metadata#isAutoRenewalGetAllowed()} must return {@code true} in
255     * order for this option to work.
256     * <p>
257     * This option is only needed if you plan to fetch the STAR certificate via other
258     * means than by using acme4j. acme4j is fetching certificates via POST-as-GET
259     * request.
260     * <p>
261     * Implies {@link #autoRenewal()}.
262     *
263     * @return itself
264     * @since 2.6
265     */
266    public OrderBuilder autoRenewalEnableGet() {
267        autoRenewal();
268        this.autoRenewalGet = true;
269        return this;
270    }
271
272    /**
273     * Notifies the CA that the ordered certificate will replace a previously issued
274     * certificate. The certificate is identified by its ARI unique identifier.
275     * <p>
276     * Optional, only supported if the CA provides renewal information. However, in this
277     * case the client <em>should</em> include this field.
278     *
279     * @param uniqueId
280     *         Certificate's renewal unique identifier.
281     * @return itself
282     * @draft This method is currently based on an RFC draft. It may be changed or removed
283     * without notice to reflect future changes to the draft. SemVer rules do not apply
284     * here.
285     * @since 3.2.0
286     */
287    public OrderBuilder replaces(String uniqueId) {
288        this.replaces = Objects.requireNonNull(uniqueId);
289        return this;
290    }
291
292    /**
293     * Notifies the CA that the ordered certificate will replace a previously issued
294     * certificate.
295     * <p>
296     * Optional, only supported if the CA provides renewal information. However, in this
297     * case the client <em>should</em> include this field.
298     *
299     * @param certificate
300     *         Certificate to be replaced
301     * @return itself
302     * @draft This method is currently based on an RFC draft. It may be changed or removed
303     * without notice to reflect future changes to the draft. SemVer rules do not apply
304     * here.
305     * @since 3.2.0
306     */
307    public OrderBuilder replaces(X509Certificate certificate) {
308        return replaces(getRenewalUniqueIdentifier(certificate));
309    }
310
311    /**
312     * Notifies the CA that the ordered certificate will replace a previously issued
313     * certificate.
314     * <p>
315     * Optional, only supported if the CA provides renewal information. However, in this
316     * case the client <em>should</em> include this field.
317     *
318     * @param certificate
319     *         Certificate to be replaced
320     * @return itself
321     * @draft This method is currently based on an RFC draft. It may be changed or removed
322     * without notice to reflect future changes to the draft. SemVer rules do not apply
323     * here.
324     * @since 3.2.0
325     */
326    public OrderBuilder replaces(Certificate certificate) {
327        return replaces(certificate.getCertificate());
328    }
329
330    /**
331     * Sends a new order to the server, and returns an {@link Order} object.
332     *
333     * @return {@link Order} that was created
334     */
335    public Order create() throws AcmeException {
336        if (identifierSet.isEmpty()) {
337            throw new IllegalArgumentException("At least one identifer is required");
338        }
339
340        var session = login.getSession();
341
342        if (autoRenewal && !session.getMetadata().isAutoRenewalEnabled()) {
343            throw new AcmeNotSupportedException("auto-renewal");
344        }
345
346        var hasAncestorDomain = identifierSet.stream()
347                .filter(id -> Identifier.TYPE_DNS.equals(id.getType()))
348                .anyMatch(id -> id.toMap().containsKey(Identifier.KEY_ANCESTOR_DOMAIN));
349        if (hasAncestorDomain && !login.getSession().getMetadata().isSubdomainAuthAllowed()) {
350            throw new AcmeNotSupportedException("ancestor-domain");
351        }
352
353        LOG.debug("create");
354        try (var conn = session.connect()) {
355            var claims = new JSONBuilder();
356            claims.array("identifiers", identifierSet.stream().map(Identifier::toMap).collect(toList()));
357
358            if (notBefore != null) {
359                claims.put("notBefore", notBefore);
360            }
361            if (notAfter != null) {
362                claims.put("notAfter", notAfter);
363            }
364
365            if (autoRenewal) {
366                var arClaims = claims.object("auto-renewal");
367                if (autoRenewalStart != null) {
368                    arClaims.put("start-date", autoRenewalStart);
369                }
370                if (autoRenewalStart != null) {
371                    arClaims.put("end-date", autoRenewalEnd);
372                }
373                if (autoRenewalLifetime != null) {
374                    arClaims.put("lifetime", autoRenewalLifetime);
375                }
376                if (autoRenewalLifetimeAdjust != null) {
377                    arClaims.put("lifetime-adjust", autoRenewalLifetimeAdjust);
378                }
379                if (autoRenewalGet) {
380                    arClaims.put("allow-certificate-get", autoRenewalGet);
381                }
382            }
383
384            if (replaces != null) {
385                claims.put("replaces", replaces);
386            }
387
388            conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
389
390            var order = new Order(login, conn.getLocation());
391            order.setJSON(conn.readJsonResponse());
392            return order;
393        }
394    }
395
396}