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}