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-ietf-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 * @since 3.2.0 303 */ 304 public OrderBuilder replaces(String uniqueId) { 305 this.replaces = Objects.requireNonNull(uniqueId); 306 return this; 307 } 308 309 /** 310 * Notifies the CA that the ordered certificate will replace a previously issued 311 * certificate. 312 * <p> 313 * Optional, only supported if the CA provides renewal information. However, in this 314 * case the client <em>should</em> include this field. 315 * 316 * @param certificate 317 * Certificate to be replaced 318 * @return itself 319 * @since 3.2.0 320 */ 321 public OrderBuilder replaces(X509Certificate certificate) { 322 return replaces(getRenewalUniqueIdentifier(certificate)); 323 } 324 325 /** 326 * Notifies the CA that the ordered certificate will replace a previously issued 327 * certificate. 328 * <p> 329 * Optional, only supported if the CA provides renewal information. However, in this 330 * case the client <em>should</em> include this field. 331 * 332 * @param certificate 333 * Certificate to be replaced 334 * @return itself 335 * @since 3.2.0 336 */ 337 public OrderBuilder replaces(Certificate certificate) { 338 return replaces(certificate.getCertificate()); 339 } 340 341 /** 342 * Sends a new order to the server, and returns an {@link Order} object. 343 * 344 * @return {@link Order} that was created 345 */ 346 public Order create() throws AcmeException { 347 if (identifierSet.isEmpty()) { 348 throw new IllegalArgumentException("At least one identifer is required"); 349 } 350 351 var session = login.getSession(); 352 353 if (autoRenewal && !session.getMetadata().isAutoRenewalEnabled()) { 354 throw new AcmeNotSupportedException("auto-renewal"); 355 } 356 357 if (autoRenewalGet && !session.getMetadata().isAutoRenewalGetAllowed()) { 358 throw new AcmeNotSupportedException("auto-renewal-get"); 359 } 360 361 if (replaces != null && session.resourceUrlOptional(Resource.RENEWAL_INFO).isEmpty()) { 362 throw new AcmeNotSupportedException("renewal-information"); 363 } 364 365 if (profile != null && !session.getMetadata().isProfileAllowed()) { 366 throw new AcmeNotSupportedException("profile"); 367 } 368 369 if (profile != null && !session.getMetadata().isProfileAllowed(profile)) { 370 throw new AcmeNotSupportedException("profile: " + profile); 371 } 372 373 var hasAncestorDomain = identifierSet.stream() 374 .filter(id -> Identifier.TYPE_DNS.equals(id.getType())) 375 .anyMatch(id -> id.toMap().containsKey(Identifier.KEY_ANCESTOR_DOMAIN)); 376 if (hasAncestorDomain && !login.getSession().getMetadata().isSubdomainAuthAllowed()) { 377 throw new AcmeNotSupportedException("ancestor-domain"); 378 } 379 380 LOG.debug("create"); 381 try (var conn = session.connect()) { 382 var claims = new JSONBuilder(); 383 claims.array("identifiers", identifierSet.stream().map(Identifier::toMap).collect(toList())); 384 385 if (notBefore != null) { 386 claims.put("notBefore", notBefore); 387 } 388 if (notAfter != null) { 389 claims.put("notAfter", notAfter); 390 } 391 392 if (autoRenewal) { 393 var arClaims = claims.object("auto-renewal"); 394 if (autoRenewalStart != null) { 395 arClaims.put("start-date", autoRenewalStart); 396 } 397 if (autoRenewalStart != null) { 398 arClaims.put("end-date", autoRenewalEnd); 399 } 400 if (autoRenewalLifetime != null) { 401 arClaims.put("lifetime", autoRenewalLifetime); 402 } 403 if (autoRenewalLifetimeAdjust != null) { 404 arClaims.put("lifetime-adjust", autoRenewalLifetimeAdjust); 405 } 406 if (autoRenewalGet) { 407 arClaims.put("allow-certificate-get", autoRenewalGet); 408 } 409 } 410 411 if (replaces != null) { 412 claims.put("replaces", replaces); 413 } 414 415 if (profile != null) { 416 claims.put("profile", profile); 417 } 418 419 conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login); 420 421 var order = new Order(login, conn.getLocation()); 422 order.setJSON(conn.readJsonResponse()); 423 return order; 424 } 425 } 426 427}