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    private @Nullable String profile;
058
059    /**
060     * Create a new {@link OrderBuilder}.
061     *
062     * @param login
063     *            {@link Login} to bind with
064     */
065    protected OrderBuilder(Login login) {
066        this.login = login;
067    }
068
069    /**
070     * Adds a domain name to the order.
071     *
072     * @param domain
073     *            Name of a domain to be ordered. May be a wildcard domain if supported by
074     *            the CA. IDN names are accepted and will be ACE encoded automatically.
075     * @return itself
076     */
077    public OrderBuilder domain(String domain) {
078        return identifier(Identifier.dns(domain));
079    }
080
081    /**
082     * Adds domain names to the order.
083     *
084     * @param domains
085     *            Collection of domain names to be ordered. May be wildcard domains if
086     *            supported by the CA. IDN names are accepted and will be ACE encoded
087     *            automatically.
088     * @return itself
089     */
090    public OrderBuilder domains(String... domains) {
091        for (var domain : requireNonNull(domains, "domains")) {
092            domain(domain);
093        }
094        return this;
095    }
096
097    /**
098     * Adds a collection of domain names to the order.
099     *
100     * @param domains
101     *            Collection of domain names to be ordered. May be wildcard domains if
102     *            supported by the CA. IDN names are accepted and will be ACE encoded
103     *            automatically.
104     * @return itself
105     */
106    public OrderBuilder domains(Collection<String> domains) {
107        requireNonNull(domains, "domains").forEach(this::domain);
108        return this;
109    }
110
111    /**
112     * Adds an {@link Identifier} to the order.
113     *
114     * @param identifier
115     *            {@link Identifier} to be added to the order.
116     * @return itself
117     * @since 2.3
118     */
119    public OrderBuilder identifier(Identifier identifier) {
120        identifierSet.add(requireNonNull(identifier, "identifier"));
121        return this;
122    }
123
124    /**
125     * Adds a collection of {@link Identifier} to the order.
126     *
127     * @param identifiers
128     *            Collection of {@link Identifier} to be added to the order.
129     * @return itself
130     * @since 2.3
131     */
132    public OrderBuilder identifiers(Collection<Identifier> identifiers) {
133        requireNonNull(identifiers, "identifiers").forEach(this::identifier);
134        return this;
135    }
136
137    /**
138     * Sets a "not before" date in the certificate. May be ignored by the CA.
139     *
140     * @param notBefore "not before" date
141     * @return itself
142     */
143    public OrderBuilder notBefore(Instant notBefore) {
144        if (autoRenewal) {
145            throw new IllegalArgumentException("cannot combine notBefore with autoRenew");
146        }
147        this.notBefore = requireNonNull(notBefore, "notBefore");
148        return this;
149    }
150
151    /**
152     * Sets a "not after" date in the certificate. May be ignored by the CA.
153     *
154     * @param notAfter "not after" date
155     * @return itself
156     */
157    public OrderBuilder notAfter(Instant notAfter) {
158        if (autoRenewal) {
159            throw new IllegalArgumentException("cannot combine notAfter with autoRenew");
160        }
161        this.notAfter = requireNonNull(notAfter, "notAfter");
162        return this;
163    }
164
165    /**
166     * Enables short-term automatic renewal of the certificate, if supported by the CA.
167     * <p>
168     * Automatic renewals cannot be combined with {@link #notBefore(Instant)} or
169     * {@link #notAfter(Instant)}.
170     *
171     * @return itself
172     * @since 2.3
173     */
174    public OrderBuilder autoRenewal() {
175        if (notBefore != null || notAfter != null) {
176            throw new IllegalArgumentException("cannot combine notBefore/notAfter with autoRenewal");
177        }
178        this.autoRenewal = true;
179        return this;
180    }
181
182    /**
183     * Sets the earliest date of validity of the first issued certificate. If not set,
184     * the start date is the earliest possible date.
185     * <p>
186     * Implies {@link #autoRenewal()}.
187     *
188     * @param start
189     *            Start date of validity
190     * @return itself
191     * @since 2.3
192     */
193    public OrderBuilder autoRenewalStart(Instant start) {
194        autoRenewal();
195        this.autoRenewalStart = requireNonNull(start, "start");
196        return this;
197    }
198
199    /**
200     * Sets the latest date of validity of the last issued certificate. If not set, the
201     * CA's default is used.
202     * <p>
203     * Implies {@link #autoRenewal()}.
204     *
205     * @param end
206     *            End date of validity
207     * @return itself
208     * @see Metadata#getAutoRenewalMaxDuration()
209     * @since 2.3
210     */
211    public OrderBuilder autoRenewalEnd(Instant end) {
212        autoRenewal();
213        this.autoRenewalEnd = requireNonNull(end, "end");
214        return this;
215    }
216
217    /**
218     * Sets the maximum validity period of each certificate. If not set, the CA's
219     * default is used.
220     * <p>
221     * Implies {@link #autoRenewal()}.
222     *
223     * @param duration
224     *            Duration of validity of each certificate
225     * @return itself
226     * @see Metadata#getAutoRenewalMinLifetime()
227     * @since 2.3
228     */
229    public OrderBuilder autoRenewalLifetime(Duration duration) {
230        autoRenewal();
231        this.autoRenewalLifetime = requireNonNull(duration, "duration");
232        return this;
233    }
234
235    /**
236     * Sets the amount of pre-dating each certificate. If not set, the CA's
237     * default (0) is used.
238     * <p>
239     * Implies {@link #autoRenewal()}.
240     *
241     * @param duration
242     *            Duration of certificate pre-dating
243     * @return itself
244     * @since 2.7
245     */
246    public OrderBuilder autoRenewalLifetimeAdjust(Duration duration) {
247        autoRenewal();
248        this.autoRenewalLifetimeAdjust = requireNonNull(duration, "duration");
249        return this;
250    }
251
252    /**
253     * Announces that the client wishes to fetch the auto-renewed certificate via GET
254     * request. If not used, the STAR certificate can only be fetched via POST-as-GET
255     * request. {@link Metadata#isAutoRenewalGetAllowed()} must return {@code true} in
256     * order for this option to work.
257     * <p>
258     * This option is only needed if you plan to fetch the STAR certificate via other
259     * means than by using acme4j. acme4j is fetching certificates via POST-as-GET
260     * request.
261     * <p>
262     * Implies {@link #autoRenewal()}.
263     *
264     * @return itself
265     * @since 2.6
266     */
267    public OrderBuilder autoRenewalEnableGet() {
268        autoRenewal();
269        this.autoRenewalGet = true;
270        return this;
271    }
272
273    /**
274     * Notifies the CA of the desired profile of the ordered certificate.
275     * <p>
276     * Optional, only supported if the CA supports profiles. However, in this case the
277     * client <em>may</em> include this field.
278     *
279     * @param profile
280     *         Identifier of the desired profile
281     * @return itself
282     * @draft This method is currently based on RFC draft draft-aaron-acme-profiles. It
283     * may be changed or removed without notice to reflect future changes to the draft.
284     * SemVer rules do not apply here.
285     * @since 3.5.0
286     */
287    public OrderBuilder profile(String profile) {
288        this.profile = Objects.requireNonNull(profile);
289        return this;
290    }
291
292    /**
293     * Notifies the CA that the ordered certificate will replace a previously issued
294     * certificate. The certificate is identified by its ARI unique identifier.
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 uniqueId
300     *         Certificate's renewal unique identifier.
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(String uniqueId) {
308        this.replaces = Objects.requireNonNull(uniqueId);
309        return this;
310    }
311
312    /**
313     * Notifies the CA that the ordered certificate will replace a previously issued
314     * certificate.
315     * <p>
316     * Optional, only supported if the CA provides renewal information. However, in this
317     * case the client <em>should</em> include this field.
318     *
319     * @param certificate
320     *         Certificate to be replaced
321     * @return itself
322     * @draft This method is currently based on an RFC draft. It may be changed or removed
323     * without notice to reflect future changes to the draft. SemVer rules do not apply
324     * here.
325     * @since 3.2.0
326     */
327    public OrderBuilder replaces(X509Certificate certificate) {
328        return replaces(getRenewalUniqueIdentifier(certificate));
329    }
330
331    /**
332     * Notifies the CA that the ordered certificate will replace a previously issued
333     * certificate.
334     * <p>
335     * Optional, only supported if the CA provides renewal information. However, in this
336     * case the client <em>should</em> include this field.
337     *
338     * @param certificate
339     *         Certificate to be replaced
340     * @return itself
341     * @draft This method is currently based on an RFC draft. It may be changed or removed
342     * without notice to reflect future changes to the draft. SemVer rules do not apply
343     * here.
344     * @since 3.2.0
345     */
346    public OrderBuilder replaces(Certificate certificate) {
347        return replaces(certificate.getCertificate());
348    }
349
350    /**
351     * Sends a new order to the server, and returns an {@link Order} object.
352     *
353     * @return {@link Order} that was created
354     */
355    public Order create() throws AcmeException {
356        if (identifierSet.isEmpty()) {
357            throw new IllegalArgumentException("At least one identifer is required");
358        }
359
360        var session = login.getSession();
361
362        if (autoRenewal && !session.getMetadata().isAutoRenewalEnabled()) {
363            throw new AcmeNotSupportedException("auto-renewal");
364        }
365
366        if (autoRenewalGet && !session.getMetadata().isAutoRenewalGetAllowed()) {
367            throw new AcmeNotSupportedException("auto-renewal-get");
368        }
369
370        if (replaces != null && session.resourceUrlOptional(Resource.RENEWAL_INFO).isEmpty()) {
371            throw new AcmeNotSupportedException("renewal-information");
372        }
373
374        if (profile != null && !session.getMetadata().isProfileAllowed()) {
375            throw new AcmeNotSupportedException("profile");
376        }
377
378        if (profile != null && !session.getMetadata().isProfileAllowed(profile)) {
379            throw new AcmeNotSupportedException("profile: " + profile);
380        }
381
382        var hasAncestorDomain = identifierSet.stream()
383                .filter(id -> Identifier.TYPE_DNS.equals(id.getType()))
384                .anyMatch(id -> id.toMap().containsKey(Identifier.KEY_ANCESTOR_DOMAIN));
385        if (hasAncestorDomain && !login.getSession().getMetadata().isSubdomainAuthAllowed()) {
386            throw new AcmeNotSupportedException("ancestor-domain");
387        }
388
389        LOG.debug("create");
390        try (var conn = session.connect()) {
391            var claims = new JSONBuilder();
392            claims.array("identifiers", identifierSet.stream().map(Identifier::toMap).collect(toList()));
393
394            if (notBefore != null) {
395                claims.put("notBefore", notBefore);
396            }
397            if (notAfter != null) {
398                claims.put("notAfter", notAfter);
399            }
400
401            if (autoRenewal) {
402                var arClaims = claims.object("auto-renewal");
403                if (autoRenewalStart != null) {
404                    arClaims.put("start-date", autoRenewalStart);
405                }
406                if (autoRenewalStart != null) {
407                    arClaims.put("end-date", autoRenewalEnd);
408                }
409                if (autoRenewalLifetime != null) {
410                    arClaims.put("lifetime", autoRenewalLifetime);
411                }
412                if (autoRenewalLifetimeAdjust != null) {
413                    arClaims.put("lifetime-adjust", autoRenewalLifetimeAdjust);
414                }
415                if (autoRenewalGet) {
416                    arClaims.put("allow-certificate-get", autoRenewalGet);
417                }
418            }
419
420            if (replaces != null) {
421                claims.put("replaces", replaces);
422            }
423
424            if (profile != null) {
425                claims.put("profile", profile);
426            }
427
428            conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
429
430            var order = new Order(login, conn.getLocation());
431            order.setJSON(conn.readJsonResponse());
432            return order;
433        }
434    }
435
436}