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}