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; 018import static java.util.stream.Collectors.toUnmodifiableList; 019 020import java.io.IOException; 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 private static final long serialVersionUID = 5435808648658292177L; 047 private static final Logger LOG = LoggerFactory.getLogger(Order.class); 048 049 private transient @Nullable Certificate certificate = null; 050 private transient @Nullable Certificate autoRenewalCertificate = 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 .collect(toUnmodifiableList()); 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 * Gets the STAR extension's {@link Certificate} if it is available. 158 * 159 * @since 2.6 160 * @throws IllegalStateException 161 * if the order is not ready yet. You must finalize the order first, and wait 162 * for the status to become {@link Status#VALID}. It is also thrown if the 163 * order has been {@link Status#CANCELED}. 164 * @deprecated Use {@link #getCertificate()} for STAR certificates as well. 165 */ 166 @Deprecated 167 @SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended 168 public Certificate getAutoRenewalCertificate() { 169 if (autoRenewalCertificate == null) { 170 autoRenewalCertificate = getJSON().get("star-certificate") 171 .optional() 172 .map(Value::asURL) 173 .map(getLogin()::bindCertificate) 174 .orElseThrow(() -> new IllegalStateException("Order is in an invalid state")); 175 } 176 return autoRenewalCertificate; 177 } 178 179 /** 180 * Returns whether this is a STAR certificate ({@code true}) or a standard certificate 181 * ({@code false}). 182 * 183 * @since 3.5.0 184 */ 185 public boolean isAutoRenewalCertificate() { 186 return getJSON().contains("star-certificate"); 187 } 188 189 /** 190 * Finalizes the order. 191 * <p> 192 * If the finalization was successful, the certificate is provided via 193 * {@link #getCertificate()}. 194 * <p> 195 * Even though the ACME protocol uses the term "finalize an order", this method is 196 * called {@link #execute(KeyPair)} to avoid confusion with the problematic 197 * {@link Object#finalize()} method. 198 * 199 * @param domainKeyPair 200 * The {@link KeyPair} that is going to be certified. This is <em>not</em> 201 * your account's keypair! 202 * @see #execute(KeyPair, Consumer) 203 * @see #execute(PKCS10CertificationRequest) 204 * @see #execute(byte[]) 205 * @see #waitUntilReady(Duration) 206 * @see #waitForCompletion(Duration) 207 * @since 3.0.0 208 */ 209 public void execute(KeyPair domainKeyPair) throws AcmeException { 210 execute(domainKeyPair, csrBuilder -> {}); 211 } 212 213 /** 214 * Finalizes the order (see {@link #execute(KeyPair)}). 215 * <p> 216 * This method also accepts a builderConsumer that can be used to add further details 217 * to the CSR (e.g. your organization). The identifiers (IPs, domain names, etc.) are 218 * automatically added to the CSR. 219 * 220 * @param domainKeyPair 221 * The {@link KeyPair} that is going to be used together with the certificate. 222 * This is not your account's keypair! 223 * @param builderConsumer 224 * {@link Consumer} that adds further details to the provided 225 * {@link CSRBuilder}. 226 * @see #execute(KeyPair) 227 * @see #execute(PKCS10CertificationRequest) 228 * @see #execute(byte[]) 229 * @see #waitUntilReady(Duration) 230 * @see #waitForCompletion(Duration) 231 * @since 3.0.0 232 */ 233 public void execute(KeyPair domainKeyPair, Consumer<CSRBuilder> builderConsumer) throws AcmeException { 234 try { 235 var csrBuilder = new CSRBuilder(); 236 csrBuilder.addIdentifiers(getIdentifiers()); 237 builderConsumer.accept(csrBuilder); 238 csrBuilder.sign(domainKeyPair); 239 execute(csrBuilder.getCSR()); 240 } catch (IOException ex) { 241 throw new AcmeException("Failed to create CSR", ex); 242 } 243 } 244 245 /** 246 * Finalizes the order (see {@link #execute(KeyPair)}). 247 * <p> 248 * This method receives a {@link PKCS10CertificationRequest} instance of a CSR that 249 * was generated externally. Use this method to gain full control over the content of 250 * the CSR. The CSR is not checked by acme4j, but just transported to the CA. It is 251 * your responsibility that it matches to the order. 252 * 253 * @param csr 254 * {@link PKCS10CertificationRequest} to be used for this order. 255 * @see #execute(KeyPair) 256 * @see #execute(KeyPair, Consumer) 257 * @see #execute(byte[]) 258 * @see #waitUntilReady(Duration) 259 * @see #waitForCompletion(Duration) 260 * @since 3.0.0 261 */ 262 public void execute(PKCS10CertificationRequest csr) throws AcmeException { 263 try { 264 execute(csr.getEncoded()); 265 } catch (IOException ex) { 266 throw new AcmeException("Invalid CSR", ex); 267 } 268 } 269 270 /** 271 * Finalizes the order (see {@link #execute(KeyPair)}). 272 * <p> 273 * This method receives a byte array containing an encoded CSR that was generated 274 * externally. Use this method to gain full control over the content of the CSR. The 275 * CSR is not checked by acme4j, but just transported to the CA. It is your 276 * responsibility that it matches to the order. 277 * 278 * @param csr 279 * Binary representation of a CSR containing the parameters for the 280 * certificate being requested, in DER format 281 * @see #waitUntilReady(Duration) 282 * @see #waitForCompletion(Duration) 283 */ 284 public void execute(byte[] csr) throws AcmeException { 285 LOG.debug("finalize"); 286 try (var conn = getSession().connect()) { 287 var claims = new JSONBuilder(); 288 claims.putBase64("csr", csr); 289 290 conn.sendSignedRequest(getFinalizeLocation(), claims, getLogin()); 291 } 292 invalidate(); 293 } 294 295 /** 296 * Waits until the order is ready for finalization. 297 * <p> 298 * Is is ready if it reaches {@link Status#READY}. The method will also return if the 299 * order already has another terminal state, which is either {@link Status#VALID} or 300 * {@link Status#INVALID}. 301 * <p> 302 * This method is synchronous and blocks the current thread. 303 * 304 * @param timeout 305 * Timeout until a terminal status must have been reached 306 * @return Status that was reached 307 * @since 3.4.0 308 */ 309 public Status waitUntilReady(Duration timeout) 310 throws AcmeException, InterruptedException { 311 return waitForStatus(EnumSet.of(Status.READY, Status.VALID, Status.INVALID), timeout); 312 } 313 314 /** 315 * Waits until the order finalization is completed. 316 * <p> 317 * Is is completed if it reaches either {@link Status#VALID} or 318 * {@link Status#INVALID}. 319 * <p> 320 * This method is synchronous and blocks the current thread. 321 * 322 * @param timeout 323 * Timeout until a terminal status must have been reached 324 * @return Status that was reached 325 * @since 3.4.0 326 */ 327 public Status waitForCompletion(Duration timeout) 328 throws AcmeException, InterruptedException { 329 return waitForStatus(EnumSet.of(Status.VALID, Status.INVALID), timeout); 330 } 331 332 /** 333 * Checks if this order is auto-renewing, according to the ACME STAR specifications. 334 * 335 * @since 2.3 336 */ 337 public boolean isAutoRenewing() { 338 return getJSON().get("auto-renewal") 339 .optional() 340 .isPresent(); 341 } 342 343 /** 344 * Returns the earliest date of validity of the first certificate issued. 345 * 346 * @since 2.3 347 * @throws AcmeNotSupportedException if auto-renewal is not supported 348 */ 349 public Optional<Instant> getAutoRenewalStartDate() { 350 return getJSON().getFeature("auto-renewal") 351 .map(Value::asObject) 352 .orElseGet(JSON::empty) 353 .get("start-date") 354 .optional() 355 .map(Value::asInstant); 356 } 357 358 /** 359 * Returns the latest date of validity of the last certificate issued. 360 * 361 * @since 2.3 362 * @throws AcmeNotSupportedException if auto-renewal is not supported 363 */ 364 public Instant getAutoRenewalEndDate() { 365 return getJSON().getFeature("auto-renewal") 366 .map(Value::asObject) 367 .orElseGet(JSON::empty) 368 .get("end-date") 369 .asInstant(); 370 } 371 372 /** 373 * Returns the maximum lifetime of each certificate. 374 * 375 * @since 2.3 376 * @throws AcmeNotSupportedException if auto-renewal is not supported 377 */ 378 public Duration getAutoRenewalLifetime() { 379 return getJSON().getFeature("auto-renewal") 380 .optional() 381 .map(Value::asObject) 382 .orElseGet(JSON::empty) 383 .get("lifetime") 384 .asDuration(); 385 } 386 387 /** 388 * Returns the pre-date period of each certificate. 389 * 390 * @since 2.7 391 * @throws AcmeNotSupportedException if auto-renewal is not supported 392 */ 393 public Optional<Duration> getAutoRenewalLifetimeAdjust() { 394 return getJSON().getFeature("auto-renewal") 395 .optional() 396 .map(Value::asObject) 397 .orElseGet(JSON::empty) 398 .get("lifetime-adjust") 399 .optional() 400 .map(Value::asDuration); 401 } 402 403 /** 404 * Returns {@code true} if STAR certificates from this order can also be fetched via 405 * GET requests. 406 * 407 * @since 2.6 408 */ 409 public boolean isAutoRenewalGetEnabled() { 410 return getJSON().getFeature("auto-renewal") 411 .optional() 412 .map(Value::asObject) 413 .orElseGet(JSON::empty) 414 .get("allow-certificate-get") 415 .optional() 416 .map(Value::asBoolean) 417 .orElse(false); 418 } 419 420 /** 421 * Cancels an auto-renewing order. 422 * 423 * @since 2.3 424 */ 425 public void cancelAutoRenewal() throws AcmeException { 426 if (!getSession().getMetadata().isAutoRenewalEnabled()) { 427 throw new AcmeNotSupportedException("auto-renewal"); 428 } 429 430 LOG.debug("cancel"); 431 try (var conn = getSession().connect()) { 432 var claims = new JSONBuilder(); 433 claims.put("status", "canceled"); 434 435 conn.sendSignedRequest(getLocation(), claims, getLogin()); 436 setJSON(conn.readJsonResponse()); 437 } 438 } 439 440 /** 441 * Returns the selected profile. 442 * 443 * @since 3.5.0 444 * @throws AcmeNotSupportedException if profile is not supported 445 */ 446 public String getProfile() { 447 return getJSON().getFeature("profile").asString(); 448 } 449 450 @Override 451 protected void invalidate() { 452 super.invalidate(); 453 certificate = null; 454 autoRenewalCertificate = null; 455 authorizations = null; 456 } 457}