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.Collections.unmodifiableList; 017import static java.util.stream.Collectors.toList; 018 019import java.io.IOException; 020import java.io.Serial; 021import java.net.URL; 022import java.security.KeyPair; 023import java.time.Duration; 024import java.time.Instant; 025import java.util.EnumSet; 026import java.util.List; 027import java.util.Optional; 028import java.util.function.Consumer; 029 030import edu.umd.cs.findbugs.annotations.Nullable; 031import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 032import org.bouncycastle.pkcs.PKCS10CertificationRequest; 033import org.shredzone.acme4j.exception.AcmeException; 034import org.shredzone.acme4j.exception.AcmeNotSupportedException; 035import org.shredzone.acme4j.toolbox.JSON; 036import org.shredzone.acme4j.toolbox.JSON.Value; 037import org.shredzone.acme4j.toolbox.JSONBuilder; 038import org.shredzone.acme4j.util.CSRBuilder; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042/** 043 * A representation of a certificate order at the CA. 044 */ 045public class Order extends AcmeJsonResource implements PollableResource { 046 @Serial 047 private static final long serialVersionUID = 5435808648658292177L; 048 private static final Logger LOG = LoggerFactory.getLogger(Order.class); 049 050 private transient @Nullable Certificate certificate = null; 051 private transient @Nullable List<Authorization> authorizations = null; 052 053 protected Order(Login login, URL location) { 054 super(login, location); 055 } 056 057 /** 058 * Returns the current status of the order. 059 * <p> 060 * Possible values are: {@link Status#PENDING}, {@link Status#READY}, 061 * {@link Status#PROCESSING}, {@link Status#VALID}, {@link Status#INVALID}. 062 * If the server supports STAR, another possible value is {@link Status#CANCELED}. 063 */ 064 @Override 065 public Status getStatus() { 066 return getJSON().get("status").asStatus(); 067 } 068 069 /** 070 * Returns a {@link Problem} document with the reason if the order has failed. 071 */ 072 public Optional<Problem> getError() { 073 return getJSON().get("error").map(v -> v.asProblem(getLocation())); 074 } 075 076 /** 077 * Gets the expiry date of the authorization, if set by the server. 078 */ 079 public Optional<Instant> getExpires() { 080 return getJSON().get("expires").map(Value::asInstant); 081 } 082 083 /** 084 * Gets a list of {@link Identifier} that are connected to this order. 085 * 086 * @since 2.3 087 */ 088 public List<Identifier> getIdentifiers() { 089 return getJSON().get("identifiers") 090 .asArray() 091 .stream() 092 .map(Value::asIdentifier) 093 .toList(); 094 } 095 096 /** 097 * Gets the "not before" date that was used for the order. 098 */ 099 public Optional<Instant> getNotBefore() { 100 return getJSON().get("notBefore").map(Value::asInstant); 101 } 102 103 /** 104 * Gets the "not after" date that was used for the order. 105 */ 106 public Optional<Instant> getNotAfter() { 107 return getJSON().get("notAfter").map(Value::asInstant); 108 } 109 110 /** 111 * Gets the {@link Authorization} that are required to fulfil this order, in no 112 * specific order. 113 */ 114 public List<Authorization> getAuthorizations() { 115 if (authorizations == null) { 116 var login = getLogin(); 117 authorizations = getJSON().get("authorizations") 118 .asArray() 119 .stream() 120 .map(Value::asURL) 121 .map(login::bindAuthorization) 122 .collect(toList()); 123 } 124 return unmodifiableList(authorizations); 125 } 126 127 /** 128 * Gets the location {@link URL} of where to send the finalization call to. 129 * <p> 130 * For internal purposes. Use {@link #execute(byte[])} to finalize an order. 131 */ 132 public URL getFinalizeLocation() { 133 return getJSON().get("finalize").asURL(); 134 } 135 136 /** 137 * Gets the {@link Certificate}. 138 * 139 * @throws IllegalStateException 140 * if the order is not ready yet. You must finalize the order first, and wait 141 * for the status to become {@link Status#VALID}. 142 */ 143 @SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended 144 public Certificate getCertificate() { 145 if (certificate == null) { 146 certificate = getJSON().get("star-certificate") 147 .optional() 148 .or(() -> getJSON().get("certificate").optional()) 149 .map(Value::asURL) 150 .map(getLogin()::bindCertificate) 151 .orElseThrow(() -> new IllegalStateException("Order is not completed")); 152 } 153 return certificate; 154 } 155 156 /** 157 * Returns whether this is a STAR certificate ({@code true}) or a standard certificate 158 * ({@code false}). 159 * 160 * @since 3.5.0 161 */ 162 public boolean isAutoRenewalCertificate() { 163 return getJSON().contains("star-certificate"); 164 } 165 166 /** 167 * Finalizes the order. 168 * <p> 169 * If the finalization was successful, the certificate is provided via 170 * {@link #getCertificate()}. 171 * <p> 172 * Even though the ACME protocol uses the term "finalize an order", this method is 173 * called {@link #execute(KeyPair)} to avoid confusion with the problematic 174 * {@link Object#finalize()} method. 175 * 176 * @param domainKeyPair 177 * The {@link KeyPair} that is going to be certified. This is <em>not</em> 178 * your account's keypair! 179 * @see #execute(KeyPair, Consumer) 180 * @see #execute(PKCS10CertificationRequest) 181 * @see #execute(byte[]) 182 * @see #waitUntilReady(Duration) 183 * @see #waitForCompletion(Duration) 184 * @since 3.0.0 185 */ 186 public void execute(KeyPair domainKeyPair) throws AcmeException { 187 execute(domainKeyPair, csrBuilder -> {}); 188 } 189 190 /** 191 * Finalizes the order (see {@link #execute(KeyPair)}). 192 * <p> 193 * This method also accepts a builderConsumer that can be used to add further details 194 * to the CSR (e.g. your organization). The identifiers (IPs, domain names, etc.) are 195 * automatically added to the CSR. 196 * 197 * @param domainKeyPair 198 * The {@link KeyPair} that is going to be used together with the certificate. 199 * This is not your account's keypair! 200 * @param builderConsumer 201 * {@link Consumer} that adds further details to the provided 202 * {@link CSRBuilder}. 203 * @see #execute(KeyPair) 204 * @see #execute(PKCS10CertificationRequest) 205 * @see #execute(byte[]) 206 * @see #waitUntilReady(Duration) 207 * @see #waitForCompletion(Duration) 208 * @since 3.0.0 209 */ 210 public void execute(KeyPair domainKeyPair, Consumer<CSRBuilder> builderConsumer) throws AcmeException { 211 try { 212 var csrBuilder = new CSRBuilder(); 213 csrBuilder.addIdentifiers(getIdentifiers()); 214 builderConsumer.accept(csrBuilder); 215 csrBuilder.sign(domainKeyPair); 216 execute(csrBuilder.getCSR()); 217 } catch (IOException ex) { 218 throw new AcmeException("Failed to create CSR", ex); 219 } 220 } 221 222 /** 223 * Finalizes the order (see {@link #execute(KeyPair)}). 224 * <p> 225 * This method receives a {@link PKCS10CertificationRequest} instance of a CSR that 226 * was generated externally. Use this method to gain full control over the content of 227 * the CSR. The CSR is not checked by acme4j, but just transported to the CA. It is 228 * your responsibility that it matches to the order. 229 * 230 * @param csr 231 * {@link PKCS10CertificationRequest} to be used for this order. 232 * @see #execute(KeyPair) 233 * @see #execute(KeyPair, Consumer) 234 * @see #execute(byte[]) 235 * @see #waitUntilReady(Duration) 236 * @see #waitForCompletion(Duration) 237 * @since 3.0.0 238 */ 239 public void execute(PKCS10CertificationRequest csr) throws AcmeException { 240 try { 241 execute(csr.getEncoded()); 242 } catch (IOException ex) { 243 throw new AcmeException("Invalid CSR", ex); 244 } 245 } 246 247 /** 248 * Finalizes the order (see {@link #execute(KeyPair)}). 249 * <p> 250 * This method receives a byte array containing an encoded CSR that was generated 251 * externally. Use this method to gain full control over the content of the CSR. The 252 * CSR is not checked by acme4j, but just transported to the CA. It is your 253 * responsibility that it matches to the order. 254 * 255 * @param csr 256 * Binary representation of a CSR containing the parameters for the 257 * certificate being requested, in DER format 258 * @see #waitUntilReady(Duration) 259 * @see #waitForCompletion(Duration) 260 */ 261 public void execute(byte[] csr) throws AcmeException { 262 LOG.debug("finalize"); 263 try (var conn = getSession().connect()) { 264 var claims = new JSONBuilder(); 265 claims.putBase64("csr", csr); 266 267 conn.sendSignedRequest(getFinalizeLocation(), claims, getLogin()); 268 } 269 invalidate(); 270 } 271 272 /** 273 * Waits until the order is ready for finalization. 274 * <p> 275 * Is is ready if it reaches {@link Status#READY}. The method will also return if the 276 * order already has another terminal state, which is either {@link Status#VALID} or 277 * {@link Status#INVALID}. 278 * <p> 279 * This method is synchronous and blocks the current thread. 280 * 281 * @param timeout 282 * Timeout until a terminal status must have been reached 283 * @return Status that was reached 284 * @since 3.4.0 285 */ 286 public Status waitUntilReady(Duration timeout) 287 throws AcmeException, InterruptedException { 288 return waitForStatus(EnumSet.of(Status.READY, Status.VALID, Status.INVALID), timeout); 289 } 290 291 /** 292 * Waits until the order finalization is completed. 293 * <p> 294 * Is is completed if it reaches either {@link Status#VALID} or 295 * {@link Status#INVALID}. 296 * <p> 297 * This method is synchronous and blocks the current thread. 298 * 299 * @param timeout 300 * Timeout until a terminal status must have been reached 301 * @return Status that was reached 302 * @since 3.4.0 303 */ 304 public Status waitForCompletion(Duration timeout) 305 throws AcmeException, InterruptedException { 306 return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout); 307 } 308 309 /** 310 * Checks if this order is auto-renewing, according to the ACME STAR specifications. 311 * 312 * @since 2.3 313 */ 314 public boolean isAutoRenewing() { 315 return getJSON().get("auto-renewal") 316 .optional() 317 .isPresent(); 318 } 319 320 /** 321 * Returns the earliest date of validity of the first certificate issued. 322 * 323 * @since 2.3 324 * @throws AcmeNotSupportedException if auto-renewal is not supported 325 */ 326 public Optional<Instant> getAutoRenewalStartDate() { 327 return getJSON().getFeature("auto-renewal") 328 .map(Value::asObject) 329 .orElseGet(JSON::empty) 330 .get("start-date") 331 .optional() 332 .map(Value::asInstant); 333 } 334 335 /** 336 * Returns the latest date of validity of the last certificate issued. 337 * 338 * @since 2.3 339 * @throws AcmeNotSupportedException if auto-renewal is not supported 340 */ 341 public Instant getAutoRenewalEndDate() { 342 return getJSON().getFeature("auto-renewal") 343 .map(Value::asObject) 344 .orElseGet(JSON::empty) 345 .get("end-date") 346 .asInstant(); 347 } 348 349 /** 350 * Returns the maximum lifetime of each certificate. 351 * 352 * @since 2.3 353 * @throws AcmeNotSupportedException if auto-renewal is not supported 354 */ 355 public Duration getAutoRenewalLifetime() { 356 return getJSON().getFeature("auto-renewal") 357 .optional() 358 .map(Value::asObject) 359 .orElseGet(JSON::empty) 360 .get("lifetime") 361 .asDuration(); 362 } 363 364 /** 365 * Returns the pre-date period of each certificate. 366 * 367 * @since 2.7 368 * @throws AcmeNotSupportedException if auto-renewal is not supported 369 */ 370 public Optional<Duration> getAutoRenewalLifetimeAdjust() { 371 return getJSON().getFeature("auto-renewal") 372 .optional() 373 .map(Value::asObject) 374 .orElseGet(JSON::empty) 375 .get("lifetime-adjust") 376 .optional() 377 .map(Value::asDuration); 378 } 379 380 /** 381 * Returns {@code true} if STAR certificates from this order can also be fetched via 382 * GET requests. 383 * 384 * @since 2.6 385 */ 386 public boolean isAutoRenewalGetEnabled() { 387 return getJSON().getFeature("auto-renewal") 388 .optional() 389 .map(Value::asObject) 390 .orElseGet(JSON::empty) 391 .get("allow-certificate-get") 392 .optional() 393 .map(Value::asBoolean) 394 .orElse(false); 395 } 396 397 /** 398 * Cancels an auto-renewing order. 399 * 400 * @since 2.3 401 */ 402 public void cancelAutoRenewal() throws AcmeException { 403 if (!getSession().getMetadata().isAutoRenewalEnabled()) { 404 throw new AcmeNotSupportedException("auto-renewal"); 405 } 406 407 LOG.debug("cancel"); 408 try (var conn = getSession().connect()) { 409 var claims = new JSONBuilder(); 410 claims.put("status", "canceled"); 411 412 conn.sendSignedRequest(getLocation(), claims, getLogin()); 413 setJSON(conn.readJsonResponse()); 414 } 415 } 416 417 /** 418 * Returns the selected profile. 419 * 420 * @since 3.5.0 421 * @throws AcmeNotSupportedException if profile is not supported 422 */ 423 public String getProfile() { 424 return getJSON().getFeature("profile").asString(); 425 } 426 427 @Override 428 protected void invalidate() { 429 super.invalidate(); 430 certificate = null; 431 authorizations = null; 432 } 433}