All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xipki.ca.gateway.acme.AcmeResponder Maven / Gradle / Ivy

There is a newer version: 6.5.1
Show newest version
// Copyright (c) 2013-2023 xipki. All rights reserved.
// License Apache License 2.0

package org.xipki.ca.gateway.acme;

import org.bouncycastle.asn1.ASN1IA5String;
import org.bouncycastle.asn1.pkcs.CertificationRequest;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.util.Pack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xipki.audit.AuditEvent;
import org.xipki.audit.AuditLevel;
import org.xipki.audit.AuditStatus;
import org.xipki.ca.gateway.GatewayUtil;
import org.xipki.ca.gateway.PopControl;
import org.xipki.ca.gateway.acme.msg.*;
import org.xipki.ca.gateway.acme.type.*;
import org.xipki.ca.sdk.*;
import org.xipki.datasource.DataAccessException;
import org.xipki.datasource.DataSourceFactory;
import org.xipki.datasource.DataSourceWrapper;
import org.xipki.pki.ErrorCode;
import org.xipki.security.CrlReason;
import org.xipki.security.HashAlgo;
import org.xipki.security.SecurityFactory;
import org.xipki.security.SignAlgo;
import org.xipki.security.util.X509Util;
import org.xipki.util.*;
import org.xipki.util.exception.InvalidConfException;
import org.xipki.util.exception.ObjectCreationException;
import org.xipki.util.http.HttpRespContent;
import org.xipki.util.http.HttpResponse;
import org.xipki.util.http.XiHttpRequest;

import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import static org.xipki.ca.gateway.acme.AcmeConstants.*;
import static org.xipki.util.Base64Url.decodeFast;

/**
 * ACME responder.
 *
 * @author Lijun Liao (xipki)
 * @since 6.4.0
 */
public class AcmeResponder {

  private static class HttpRespAuditException extends Exception {

    private final int httpStatus;

    private final String auditMessage;

    private final AuditLevel auditLevel;

    private final AuditStatus auditStatus;

    public HttpRespAuditException(int httpStatus, String auditMessage,
                                  AuditLevel auditLevel, AuditStatus auditStatus) {
      this.httpStatus = httpStatus;
      this.auditMessage = Args.notBlank(auditMessage, "auditMessage");
      this.auditLevel = Args.notNull(auditLevel, "auditLevel");
      this.auditStatus = Args.notNull(auditStatus, "auditStatus");
    }

    public int getHttpStatus() {
      return httpStatus;
    }

    public String getAuditMessage() {
      return auditMessage;
    }

    public AuditLevel getAuditLevel() {
      return auditLevel;
    }

    public AuditStatus getAuditStatus() {
      return auditStatus;
    }

  } // class HttpRespAuditException

  private static class StringContainer {
    String text;
  }

  private static final Logger LOG = LoggerFactory.getLogger(AcmeResponder.class);

  private static final Map joseAlgMap = new HashMap<>();

  private final SdkClient sdk;

  private final PopControl popControl;

  private final SecurityFactory securityFactory;

  private final ContactVerifier contactVerifier;

  private final NonceManager nonceManager;

  private static final Set knownCommands;

  private final boolean termsOfServicePresent;
  private final byte[] directoryBytes;

  private final String directoryHeader;

  private final String host;

  private final String host2;

  private final String baseUrl;

  private final String accountPrefix;

  private final List caProfiles;

  private final Set challengeTypes;

  private final SecureRandom rnd;

  private final AcmeRepo repo;

  private final Map cacertsMap = new ConcurrentHashMap<>();

  private final int tokenNumBytes;

  private final AcmeProxyConf.CleanupOrderConf cleanOrderConf;

  private final ChallengeValidator challengeValidator;

  private final CertEnroller certEnroller;

  private final AtomicLong lastOrdersCleaned = new AtomicLong(0);

  static {
    LOG.info("XiPKI ACME-Gateway version {}", StringUtil.getBundleVersion(AcmeResponder.class));
    knownCommands = CollectionUtil.asUnmodifiableSet(
        CMD_directory, CMD_newNonce, CMD_newAccount, CMD_newOrder, CMD_revokeCert, CMD_keyChange,
        CMD_account, CMD_order, CMD_orders, CMD_authz, CMD_chall, CMD_finalize, CMD_cert);

    // See https://www.rfc-editor.org/rfc/rfc7518.html#section-3.1
    // ECDSA using P-256 and SHA-256
    joseAlgMap.put("ES256", SignAlgo.ECDSA_SHA256);
    // ECDSA using P-234 and SHA-384
    joseAlgMap.put("ES384", SignAlgo.ECDSA_SHA384);
    // ECDSA using P-521 and SHA-512
    joseAlgMap.put("ES512", SignAlgo.ECDSA_SHA512);

    // RSASSA-PKCS1-v1_5 using SHA-256
    joseAlgMap.put("RS256", SignAlgo.RSA_SHA256);
    // RSASSA-PKCS1-v1_5 using SHA-384
    joseAlgMap.put("RS384", SignAlgo.RSA_SHA384);
    // RSASSA-PKCS1-v1_5 using SHA-512
    joseAlgMap.put("RS512", SignAlgo.RSA_SHA512);

    // RSASSA-PSS using SHA-256 and MGF1 with SHA-256
    joseAlgMap.put("PS256", SignAlgo.RSAPSS_SHA256);
    // RSASSA-PSS using SHA-384 and MGF1 with SHA-384
    joseAlgMap.put("PS384", SignAlgo.RSAPSS_SHA384);
    // RSASSA-PSS using SHA-512 and MGF1 with SHA-512
    joseAlgMap.put("PS512", SignAlgo.RSAPSS_SHA512);
  }

  public AcmeResponder(SdkClient sdk, SecurityFactory securityFactory, PopControl popControl, AcmeProxyConf.Acme conf)
      throws InvalidConfException {
    this.sdk = Args.notNull(sdk, "sdk");
    this.popControl = Args.notNull(popControl, "popControl");
    this.securityFactory = Args.notNull(securityFactory, "securityFactory");

    this.baseUrl = Args.notBlank(conf.getBaseUrl(), "baseUrl");
    this.accountPrefix = this.baseUrl + "acct/";
    this.cleanOrderConf = new AcmeProxyConf.CleanupOrderConf();
    AcmeProxyConf.CleanupOrderConf cleanOrder = conf.getCleanupOrder();
    if (cleanOrder == null) {
      this.cleanOrderConf.setExpiredOrderDays(365);
      this.cleanOrderConf.setExpiredCertDays(365);
    } else {
      // minimal 10 days
      this.cleanOrderConf.setExpiredCertDays(Math.max(10, cleanOrder.getExpiredCertDays()));
      this.cleanOrderConf.setExpiredOrderDays(Math.max(10, cleanOrder.getExpiredOrderDays()));
    }

    try {
      URL url = new URL(baseUrl);
      String host0 = url.getHost();
      int port = url.getPort();
      int dfltPort = url.getDefaultPort();

      if (port == -1) {
        this.host  = host0;
        this.host2 = host0 + ":" + dfltPort;
      } else {
        this.host  = host0 + ":" + port;
        this.host2 = (port == dfltPort) ? host0 : host;
      }
    } catch (MalformedURLException e) {
      throw new InvalidConfException("invalid baseUrl '" + baseUrl + "'");
    }

    this.nonceManager = new NonceManager(conf.getNonceNumBytes());
    this.tokenNumBytes = conf.getTokenNumBytes();
    this.directoryHeader = "<" + baseUrl + "directory>;rel=\"index\"";
    this.caProfiles = conf.getCaProfiles();
    if (conf.getChallengeTypes() != null) {
      List types = conf.getChallengeTypes();
      if (!(types.contains(DNS_01) || types.contains(HTTP_01) || types.contains(TLS_ALPN_01))) {
        throw new InvalidConfException("invalid challengeTypes '" + types + "'");
      }
      challengeTypes = new HashSet<>(types);
    } else {
      challengeTypes = new HashSet<>(4);
      challengeTypes.add(DNS_01);
      challengeTypes.add(HTTP_01);
      challengeTypes.add(TLS_ALPN_01);
    }
    LOG.info("challenge types: {}", challengeTypes);

    StringBuilder sb = new StringBuilder();
    sb.append("{");
    addJsonField(sb, "newNonce",   baseUrl + CMD_newNonce);
    addJsonField(sb, "newAccount", baseUrl + CMD_newAccount);
    addJsonField(sb, "newOrder",   baseUrl + CMD_newOrder);
    // newAuthz is not supported
    //addJsonField(sb, "newAuthz", baseUrl + CMD_newAuthz);
    addJsonField(sb, "revokeCert", baseUrl + CMD_revokeCert);
    addJsonField(sb, "keyChange",  baseUrl + CMD_keyChange);

    sb.append(addQuoteSign("meta")).append(":{");
    if (StringUtil.isNotBlank(conf.getWebsite())) {
      addJsonField(sb, "website", conf.getWebsite());
    }

    this.termsOfServicePresent = StringUtil.isNotBlank(conf.getTermsOfService());
    if (termsOfServicePresent) {
      addJsonField(sb, "termsOfService", conf.getTermsOfService());
    }

    if (CollectionUtil.isNotEmpty(conf.getCaaIdentities())) {
      sb.append(addQuoteSign("caaIdentities")).append(":[");
      for (String caIdentity : conf.getCaaIdentities()) {
        sb.append(addQuoteSign(caIdentity)).append(",");
      }

      // remove the last ','
      sb.deleteCharAt(sb.length() - 1);
      sb.append("],");
    }

    sb.append(addQuoteSign("externalAccountRequired")).append(":false");
    sb.append("}}");
    this.directoryBytes = sb.toString().getBytes(StandardCharsets.UTF_8);

    String str = conf.getContactVerifier();
    if (str != null) {
      str = str.trim();
    }

    if (str == null || str.isEmpty()) {
      this.contactVerifier = new ContactVerifier.DfltContactVerifier();
    } else {
      try {
        this.contactVerifier = ReflectiveUtil.newInstance(str);
      } catch (ObjectCreationException ex) {
        throw new InvalidConfException("invalid contactVerifier '" + str + "'", ex);
      }
    }

    rnd = new SecureRandom();

    if (conf.getDbConf() == null) {
      throw new InvalidConfException("dbConf is not specified");
    }

    try {
      FileOrValue fileOrValue = new FileOrValue();
      fileOrValue.setFile(conf.getDbConf());
      DataSourceWrapper dataSource0 = new DataSourceFactory().createDataSource("acme-db", fileOrValue);
      repo = new AcmeRepo(new AcmeDataSource(dataSource0), conf.getCacheSize(), conf.getSyncDbSeconds());
    } catch (Exception ex) {
      throw new InvalidConfException("could not initialize database", ex);
    }

    this.challengeValidator = new ChallengeValidator(repo);
    this.certEnroller = new CertEnroller(repo, sdk);
  }

  private static String addQuoteSign(String text) {
    return "\"" + text + "\"";
  }

  private static void addJsonField(StringBuilder sb, String name, String value) {
    sb.append(addQuoteSign(name)).append(":").append(addQuoteSign(value)).append(",");
  }

  public void start() {
    Thread t = new Thread(challengeValidator);
    t.setName("challengeValidator");
    t.setDaemon(true);
    t.start();

    t = new Thread(certEnroller);
    t.setName("certEnroller");
    t.setDaemon(true);
    t.start();

    repo.start();
  }

  public void close() {
    challengeValidator.close();
    certEnroller.close();
    nonceManager.close();

    repo.close();
  }

  public HttpResponse service(XiHttpRequest servletReq, byte[] request, AuditEvent event) {
    StringContainer command = new StringContainer();

    AuditStatus auditStatus = AuditStatus.SUCCESSFUL;
    AuditLevel auditLevel = AuditLevel.INFO;
    String auditMessage = null;

    HttpResponse resp;
    try {
      resp = doService(servletReq, request, event, command);
      int sc = resp.getStatusCode();
      if (sc >= 300 || sc < 200) {
        auditStatus = AuditStatus.FAILED;
        auditLevel = AuditLevel.ERROR;
      }
    } catch (HttpRespAuditException ex) {
      auditStatus = ex.getAuditStatus();
      auditLevel = ex.getAuditLevel();
      auditMessage = ex.getAuditMessage();

      return new HttpResponse(ex.getHttpStatus(), null, null, null);
    } catch (AcmeProtocolException ex) {
      auditLevel = AuditLevel.WARN;
      auditStatus = AuditStatus.FAILED;
      auditMessage = ex.getMessage();
      Problem problem = new Problem();
      problem.setType(ex.getAcmeError().getQualifiedCode());
      problem.setDetail(ex.getAcmeDetail());
      return buildProblemResp(ex.getHttpError(), problem);
    } catch (DataAccessException | AcmeSystemException ex) {
      LogUtil.error(LOG, ex, null);
      auditLevel = AuditLevel.ERROR;
      auditStatus = AuditStatus.FAILED;
      if (ex instanceof DataAccessException) {
        auditMessage = "database error";
      } else {
        auditMessage = "ACME system exception";
      }
      return new HttpResponse(SC_INTERNAL_SERVER_ERROR, null, null, null);
    } catch (Throwable th) {
      LOG.error("Throwable thrown, this should not happen!", th);
      auditLevel = AuditLevel.ERROR;
      auditStatus = AuditStatus.FAILED;
      auditMessage = "internal error";
      return new HttpResponse(SC_INTERNAL_SERVER_ERROR, null, null, null);
    } finally {
      event.setStatus(auditStatus);
      event.setLevel(auditLevel);
      if (auditMessage != null) {
        event.addEventData(CaAuditConstants.NAME_message, auditMessage);
      }
    }

    if (command.text != null && !"directory".equals(command.text)) {
      String nonce = nonceManager.newNonce();
      resp.putHeader("Replay-Nonce", nonce)
              .putHeader(HDR_LINK, directoryHeader);
    }

    return resp;
  }

  private HttpResponse doService(XiHttpRequest servletReq, byte[] request,
                                 AuditEvent event, StringContainer commandContainer)
      throws HttpRespAuditException, AcmeProtocolException, AcmeSystemException, DataAccessException {
    String method = servletReq.getMethod();
    String path = servletReq.getServletPath();

    String[] tokens;
    String hdrHost = servletReq.getHeader(HDR_HOST);
    if (!host.equals(hdrHost) && !host2.equals(hdrHost)) {
      String message = "invalid header host '" + hdrHost + "'";
      LOG.error(message);
      throw new HttpRespAuditException(SC_BAD_REQUEST, message, AuditLevel.ERROR, AuditStatus.FAILED);
    }

    if (path.isEmpty()) {
      path = "/";
    }

    // the first char is always '/'
    String coreUri = path.substring("/".length());
    tokens = coreUri.split("/");

    if (tokens.length < 1) {
      String message = "invalid path " + path;
      LOG.error(message);
      throw new HttpRespAuditException(SC_NOT_FOUND, message, AuditLevel.ERROR, AuditStatus.FAILED);
    }

    String command = tokens[0].toLowerCase(Locale.ROOT);
    commandContainer.text = command;

    if (StringUtil.isBlank(command)) {
      command = CMD_directory;
    }

    event.addEventType(command);

    if (!knownCommands.contains(command)) {
      String message = "invalid command '" + command + "'";
      LOG.error(message);
      throw new HttpRespAuditException(SC_NOT_FOUND, message, AuditLevel.INFO, AuditStatus.FAILED);
    }

    if (CMD_newNonce.equalsIgnoreCase(command)) {
      int sc = "HEAD".equals(method) ? SC_OK
          : "GET" .equals(method) ? SC_NO_CONTENT : 0;
      if (sc == 0) {
        throw new HttpRespAuditException(SC_METHOD_NOT_ALLOWED, "HTTP method not allowed: " + method,
            AuditLevel.INFO, AuditStatus.FAILED);
      }

      return new HttpResponse(sc, null, null, null)
          .putHeader("Cache-Control", "no-store");
    } else if (CMD_directory.equalsIgnoreCase(command)) {
      if (!"GET".equals(method)) {
        throw new HttpRespAuditException(SC_METHOD_NOT_ALLOWED, "HTTP method not allowed: " + method,
            AuditLevel.INFO, AuditStatus.FAILED);
      }

      HttpRespContent respContent = HttpRespContent.ofOk(CT_JSON, false, directoryBytes);
      return new HttpResponse(SC_OK, respContent.getContentType(), null,
          respContent.isBase64(), respContent.getContent());
    }

    if (!"POST".equals(method)) {
      throw new HttpRespAuditException(SC_METHOD_NOT_ALLOWED, "HTTP method not allowed: " + method,
          AuditLevel.INFO, AuditStatus.FAILED);
    }

    String contentType = servletReq.getContentType();
    if (!CT_JOSE_JSON.equals(contentType)) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed,
          "invalid Content-Type '" + contentType + "'");
    }

    JoseMessage body = JSON.parseObject(request, JoseMessage.class);
    Map protected_= JSON.parseObject(decodeFast(body.getProtected()), Map.class);

    String protectedUrl = (String) protected_.get("url");
    if (protectedUrl == null) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "url is not present");
    }

    if (!protectedUrl.equals(baseUrl + path.substring(1))) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "url is not valid: '" + protectedUrl + "'");
    }

    String nonce = (String) protected_.get("nonce");
    if (nonce == null) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badNonce, "nonce is not present");
    }

    if (!nonceManager.removeNonce(nonce)) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badNonce, null);
    }

    final int MASK_JWK = 1; // jwk is allowed
    final int MASK_KID = 2; // kid is allowed

    int verificationKeyRequirement = CMD_newAccount.equals(command) ? MASK_JWK
        : CMD_revokeCert.equals(command) ? MASK_JWK | MASK_KID : MASK_KID;

    Map jwk = toStringMap((Map) protected_.get("jwk"));
    String kid = (String) protected_.get("kid");

    AcmeAccount account = null;
    PublicKey pubKey;

    if (kid != null && jwk != null) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed,
          "Both jwk and kid are specified, but exactly one of them is allowed");
    } else if (jwk != null) {
      if ((verificationKeyRequirement & MASK_JWK) == 0) {
        throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "kid is specified, but jwk is allowed");
      }

      try {
        pubKey = AcmeUtils.jwkPublicKey(jwk);
      } catch (Exception ex) {
        LogUtil.error(LOG, ex, "jwkPublicKey");
        throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badPublicKey, null);
      }
    } else if (kid != null) {
      if ((verificationKeyRequirement & MASK_KID) == 0) {
        throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed,
            "jwk is specified, but only kid is allowed");
      }

      // extract the location
      if (kid.startsWith(accountPrefix)) {
        Long id = toLongId(kid.substring(accountPrefix.length()));
        if (id != null) {
          account = repo.getAccount(id);
        }
      }

      if (account == null) {
        throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.accountDoesNotExist, null);
      }

      try {
        pubKey = account.getPublicKey();
      } catch (InvalidKeySpecException e) {
        throw new AcmeProtocolException(SC_INTERNAL_SERVER_ERROR, AcmeError.badPublicKey, null);
      }
    } else {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed,
          "None of jwk and kid is specified, but one of them is required");
    }

    // pre-check
    if (CMD_account.equals(command)) {
      if (!protectedUrl.equals(kid)) {
        throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "kid and url do not match");
      }
    }

    // assert the account is valid
    if (account != null) {
      if (account.getStatus() != AccountStatus.valid) {
        throw new AcmeProtocolException(SC_UNAUTHORIZED, AcmeError.unauthorized, "account is not valid");
      }
    }

    HttpResponse verifyRes = verifySignature((String) protected_.get("alg"), pubKey, body);
    if (verifyRes != null) {
      return verifyRes;
    }

    switch (command) {
      case CMD_newAccount: {
        NewAccountPayload reqPayload = JSON.parseObject(decodeFast(body.getPayload()), NewAccountPayload.class);

        AcmeAccount existingAccount = repo.getAccountForJwk(jwk);
        if (existingAccount != null) {
          return buildSuccJsonResp(SC_OK, existingAccount.toResponse(baseUrl))
              .putHeader(HDR_LOCATION, existingAccount.getLocation(baseUrl));
        }

        Boolean onlyReturnExisting = reqPayload.getOnlyReturnExisting();
        if (onlyReturnExisting != null && onlyReturnExisting) {
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.accountDoesNotExist, null);
        }

        // create a new account
        Boolean b = reqPayload.getTermsOfServiceAgreed();
        boolean tosAgreed = (b != null) ? b : !termsOfServicePresent;

        if (!tosAgreed) {
          throw new AcmeProtocolException(SC_UNAUTHORIZED, AcmeError.userActionRequired,
              "terms of service has not been agreed");
        }

        AcmeAccount newAccount = repo.newAcmeAccount();
        List contacts = reqPayload.getContact();
        if (contacts != null && !contacts.isEmpty()) {
          HttpResponse verifyErrorResp = verifyContacts(contacts);
          if (verifyErrorResp != null) {
            return verifyErrorResp;
          }
          newAccount.setContact(contacts);
        }
        newAccount.setExternalAccountBinding(reqPayload.getExternalAccountBinding());
        if (b != null) {
          newAccount.setTermsOfServiceAgreed(true);
        }

        newAccount.setJwk(jwk);
        newAccount.setStatus(AccountStatus.valid);
        repo.addAccount(newAccount);

        AccountResponse resp = newAccount.toResponse(baseUrl);

        LOG.info("created new account {}", newAccount.idText());
        return buildSuccJsonResp(SC_CREATED, resp)
            .putHeader(HDR_LOCATION, newAccount.getLocation(baseUrl));
      }
      case CMD_keyChange: {
        JoseMessage reqPayload = JSON.parseObject(decodeFast(body.getPayload()), JoseMessage.class);
        Map innerProtected = JSON.parseObject(
            decodeFast(reqPayload.getProtected()), Map.class);

        Map newJwk = toStringMap((Map) innerProtected.get("jwk"));
        AcmeAccount accountForNewJwk = repo.getAccountForJwk(newJwk);
        if (accountForNewJwk != null) {
          // jwk not exists.
          return toHttpResponse(HttpRespContent.of(SC_CONFLICT, null, null))
              .putHeader(HDR_LOCATION, accountForNewJwk.getLocation(baseUrl));
        }

        // check payload.account, and payload.oldKey
        Map innerPayload = JSON.parseObject(
            decodeFast(reqPayload.getPayload()), Map.class);
        String innerAccount = (String) innerPayload.get("account");
        if (!innerAccount.equals(kid)) {
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "invalid payload.account");
        }

        Map oldKey = toStringMap((Map) innerPayload.get("oldKey"));
        if (!account.hasJwk(oldKey)) {
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "oldKey does not match the account");
        }

        // check inner signature (by the new key)
        PublicKey newPubKey;
        try {
          newPubKey = AcmeUtils.jwkPublicKey(newJwk);
        } catch (InvalidKeySpecException e) {
          LogUtil.error(LOG, e, "jwkPublicKey");
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badPublicKey, null);
        }

        verifyRes = verifySignature((String) protected_.get("alg"), newPubKey, reqPayload);
        if (verifyRes != null) {
          return verifyRes;
        }

        account.setJwk(newJwk);

        LOG.info("changed key of account {}", account.idText());
        return buildSuccJsonResp(SC_OK, account.toResponse(baseUrl))
            .putHeader(HDR_LOCATION, account.getLocation(baseUrl));
      }
      case CMD_account: {
        AccountResponse reqPayload = JSON.parseObject(decodeFast(body.getPayload()), AccountResponse.class);
        AccountStatus status = reqPayload.getStatus();

        if (status == AccountStatus.revoked) {
          throw new AcmeProtocolException(SC_UNAUTHORIZED, AcmeError.unauthorized, "status revoked is not allowed");
        }

        if (status == AccountStatus.deactivated) {
          // 7.3.6.  Account Deactivation
          account.setStatus(AccountStatus.deactivated);
        }

        // 7.3.2.  Account Update
        List contacts = reqPayload.getContact();
        if (contacts != null && !contacts.isEmpty()) {
          HttpResponse errResp = verifyContacts(contacts);
          if (errResp != null) {
            return errResp;
          }

          account.setContact(contacts);
        }

        LOG.info("updated account {}", account.idText());
        return buildSuccJsonResp(SC_OK, account.toResponse(baseUrl));
      }
      case CMD_revokeCert: {
        RevokeCertPayload reqPayload = JSON.parseObject(decodeFast(body.getPayload()), RevokeCertPayload.class);
        Integer reasonCode = reqPayload.getReason();
        CrlReason reason;
        try {
          reason = reasonCode == null ? CrlReason.UNSPECIFIED : CrlReason.forReasonCode(reasonCode);
        } catch (Exception e) {
          reason = null;
        }

        if (reason == null || !CrlReason.PERMITTED_CLIENT_CRLREASONS.contains(reason)) {
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badRevocationReason,
              "bad revocation reason " + reasonCode);
        }

        byte[] certBytes = decodeFast(reqPayload.getCertificate());

        Certificate cert;
        byte[] encodedIssuer;
        try {
          cert = Certificate.getInstance(certBytes);
          encodedIssuer = cert.getIssuer().getEncoded();
        } catch (Exception e) {
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "malformed certificate");
        }

        LOG.info("try to revoke certificate with (subject={}, issuer={}, serialNumber={})",
            cert.getSubject(), cert.getIssuer(), cert.getSerialNumber());

        if (jwk != null) {
          // request is signed with the private paired with the certificate.
          boolean jwkAndCertMatch;
          try {
            jwkAndCertMatch = AcmeUtils.matchKey(jwk, cert.getSubjectPublicKeyInfo());
          } catch (InvalidKeySpecException e) {
            LogUtil.error(LOG, e, "matchKey");
            throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badPublicKey, "bad jwk");
          }
          if (!jwkAndCertMatch) {
            throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.unauthorized, "jwk and certificate do not match");
          }
        }

        AcmeOrder order = Optional.ofNullable(repo.getOrderForCert(certBytes)).orElseThrow(
            () -> new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.unauthorized,
              "certificate not enrolled through this ACME server"));

        if (jwk == null) {
          // account is non-null here.
          // request is signed with the account keypair.
          // assert the certificate is owned by the account
          if (order.getAccountId() != account.getId()) {
            // certificate has not been issued to the given account.
            throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.unauthorized,
                "account and certificate do not match");
          }
        }

        RevokeCertRequestEntry sdkEntry = new RevokeCertRequestEntry(
            cert.getSerialNumber().getPositiveValue(), reason, null);

        RevokeCertsRequest sdkReq = new RevokeCertsRequest(null, new X500NameType(encodedIssuer),
            null, new RevokeCertRequestEntry[]{sdkEntry});

        RevokeCertsResponse sdkResp;
        try {
          sdkResp = sdk.revokeCerts(sdkReq);
          LOG.info("revoked certificate");
        } catch (SdkErrorResponseException e) {
          LogUtil.error(LOG, e, "sdk.revokeCerts");
          throw new AcmeProtocolException(SC_INTERNAL_SERVER_ERROR, AcmeError.serverInternal,
              "error revoking the certificate");
        }

        ErrorEntry errorEntry = sdkResp.getEntries()[0].getError();
        if (errorEntry == null) {
          return toHttpResponse(HttpRespContent.of(SC_OK, null, null));
        } else {
          int errCode = errorEntry.getCode();
          if (errCode == ErrorCode.CERT_REVOKED.getCode()) {
            throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.alreadyRevoked, null);
          } else if (errCode == ErrorCode.UNKNOWN_CERT.getCode()) {
            throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "certificate is unknown");
          } else {
            throw new AcmeProtocolException(SC_FORBIDDEN, AcmeError.unauthorized, null);
          }
        }
      }
      case CMD_orders: {
        // clean the orders.
        cleanOrders();

        Long id = toLongId(tokens[1]);
        if (id == null || id != account.getId()) {
          throw new AcmeProtocolException(SC_NOT_FOUND, AcmeError.accountDoesNotExist, null);
        }

        List orderIds = repo.getOrderIds(id);
        int size = orderIds == null ? 0 : orderIds.size();
        List urls = new ArrayList<>(size);
        if (orderIds != null) {
          for (Long orderId : orderIds) {
            urls.add(baseUrl + "order/" + AcmeUtils.toBase64(orderId));
          }
        }
        OrdersResponse resp = new OrdersResponse();
        resp.setOrders(urls);
        return buildSuccJsonResp(SC_OK, resp);
      }
      case CMD_newOrder: {
        NewOrderPayload newOrderReq = JSON.parseObject(decodeFast(body.getPayload()), NewOrderPayload.class);
        List identifiers = newOrderReq.getIdentifiers();
        int size = identifiers == null ? 0 : identifiers.size();

        if (size == 0) {
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "no identifier is specified");
        }

        int numChalls = 0;
        for (Identifier identifier : identifiers) {
          String type = identifier.getType();
          String value = identifier.getValue();

          if ("dns".equals(type)) {
            if (!value.startsWith("*.")) {
              if (challengeTypes.contains(HTTP_01)) {
                numChalls++;
              }

              if (challengeTypes.contains(TLS_ALPN_01)) {
                numChalls++;
              }
            }

            if (challengeTypes.contains(DNS_01)) {
              numChalls++;
            }

            if (numChalls == 0) {
              throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.unsupportedIdentifier,
                  "unsupported identifier '" + type + "/" + value + "'");
            }
          } else {
            throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.unsupportedIdentifier,
                "unsupported identifier type '" + type + "'");
          }
        }

        // 7 days validity
        Instant expires = Instant.now().truncatedTo(ChronoUnit.SECONDS).plus(7, ChronoUnit.DAYS);

        List authzs = new ArrayList<>(size);

        AcmeRepo.IdsForOrder ids = repo.newIdsForOrder(size, numChalls);
        int[] authzIds = ids.getAuthzSubIds();
        int[] challIds = ids.getChallSubIds();

        int authzIdOffset = 0;
        int challIdOffset = 0;

        for (Identifier identifier : identifiers) {
          AcmeAuthz authz = new AcmeAuthz(authzIds[authzIdOffset++], identifier.toAcmeIdentifier());
          authzs.add(authz);

          authz.setStatus(AuthzStatus.pending);
          authz.setExpires(expires);

          String type = identifier.getType();
          String value = identifier.getValue();
          String token = rndToken();

          if ("dns".equals(type)) {
            String v = value;
            if (v.startsWith("*.")) {
              v = v.substring(2);
            }

            if (v.indexOf('*') != -1) {
              throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.unsupportedIdentifier,
                  "unsupported identifier '" + value + "'");
            }

            String jwkSha256 = account.getJwkSha256();

            String authorization = token + "." + jwkSha256;
            String authorizationSha256 = Base64Url.encodeToStringNoPadding(
                HashAlgo.SHA256.hash(authorization.getBytes(StandardCharsets.UTF_8)));

            List challenges = new ArrayList<>(3);
            if (!value.startsWith("*.")) {
              if (challengeTypes.contains(HTTP_01)) {
                challenges.add(newChall(challIds[challIdOffset++], HTTP_01, token, authorization));
              }

              if (challengeTypes.contains(TLS_ALPN_01)) {
                challenges.add(newChall(challIds[challIdOffset++], TLS_ALPN_01, token, authorizationSha256));
              }
            }

            if (challengeTypes.contains(DNS_01)) {
              challenges.add(newChall(challIds[challIdOffset++], DNS_01, token, authorizationSha256));
            }
            authz.setChallenges(challenges);
          } else {
            throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.unsupportedIdentifier,
                "unsupported identifier type '" + type + "'");
          }
        }

        AcmeOrder order = repo.newAcmeOrder(account.getId(), ids.getOrderId());
        order.setAuthzs(authzs);

        Instant notBefore = null;
        Instant notAfter = null;
        if (newOrderReq.getNotBefore() != null) {
          notBefore = AcmeUtils.parseTimestamp(newOrderReq.getNotBefore());
        }

        if (newOrderReq.getNotAfter() != null) {
          notAfter = AcmeUtils.parseTimestamp(newOrderReq.getNotAfter());
        }

        order.setExpires(expires);

        if (notBefore != null || notAfter != null)  {
          CertReqMeta certReqMeta = new CertReqMeta();
          certReqMeta.setNotBefore(notBefore);
          certReqMeta.setNotAfter(notAfter);
          order.setCertReqMeta(certReqMeta);
        }

        repo.addOrder(order);
        OrderResponse orderResp = order.toResponse(baseUrl);

        if (LOG.isInfoEnabled()) {
          LOG.info("added new order {} for identifiers {}: {}", order.idText(), identifiers,
              JSON.toJson(orderResp));
        }

        return buildSuccJsonResp(SC_CREATED, orderResp)
            .putHeader(HDR_LOCATION, order.getLocation(baseUrl));
      }
      case CMD_order: {
        String id = tokens[1];
        AcmeOrder order = getOrder(id);
        return buildSuccJsonResp(SC_OK, order.toResponse(baseUrl))
            .putHeader(HDR_LOCATION, order.getLocation(baseUrl));
      }
      case CMD_finalize: {
        String id = tokens[1];
        AcmeOrder order = getOrder(id);
        order.updateStatus();

        // check whether all authorizations have been finished
        switch (order.getStatus()) {
          case ready:
            break;
          case pending:
            throw new AcmeProtocolException(SC_FORBIDDEN, AcmeError.orderNotReady, "Order is not ready");
          case invalid:
            throw new AcmeProtocolException(SC_FORBIDDEN, AcmeError.unauthorized, "Order is invalid");
          case processing:
            throw new AcmeProtocolException(SC_FORBIDDEN, AcmeError.orderNotReady,
                "Enrolling certificate is processing");
          case valid:
            throw new AcmeProtocolException(SC_FORBIDDEN, AcmeError.orderNotReady, "Certificate has been issued");
          default:
            throw new RuntimeException("should not reach here, invalid order status " + order.getStatus());
        }

        FinalizeOrderPayload finalizeOrderReq = JSON.parseObject(decodeFast(body.getPayload()),
            FinalizeOrderPayload.class);

        byte[] csrBytes;
        CertificationRequest csr;
        try {
          csrBytes = decodeFast(finalizeOrderReq.getCsr());
          csr = GatewayUtil.parseCsrInRequest(csrBytes);
        } catch (Exception e) {
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badCSR, "could not parse CSR");
        }

        String keyAlgOid =
            csr.getCertificationRequestInfo().getSubjectPublicKeyInfo().getAlgorithm().getAlgorithm().getId();
        AcmeProxyConf.CaProfile caProfile = Optional.ofNullable(getCaProfile(keyAlgOid))
            .orElseThrow(() ->
                new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badCSR, "unsupported key type " + keyAlgOid));

        // verify the CSR
        Set identifiers = new HashSet<>();
        for (AcmeAuthz authz : order.getAuthzs()) {
          identifiers.add(authz.getIdentifier().toIdentifier());
        }

        X500Name csrSubject = csr.getCertificationRequestInfo().getSubject();
        String cn = X509Util.getCommonName(csrSubject);
        if (cn != null && !cn.isEmpty()) {
          boolean match = false;
          for (Identifier identifier : identifiers) {
            if (identifier.getValue().equals(cn)) {
              match = true;
              break;
            }
          }

          if (!match) {
            throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badCSR, "invalid commonName in CSR");
          }
        }

        Extensions csrExtensions = X509Util.getExtensions(csr.getCertificationRequestInfo());
        byte[] sanExtnValue = csrExtensions == null ? null
            : X509Util.getCoreExtValue(csrExtensions, Extension.subjectAlternativeName);
        if (sanExtnValue == null) {
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badCSR,
              "no extension subjectAlternativeName in CSR");
        }

        GeneralNames generalNames = GeneralNames.getInstance(sanExtnValue);
        String firstSanValue = null;
        for (GeneralName gn : generalNames.getNames()) {
          int tagNo = gn.getTagNo();
          if (tagNo == GeneralName.dNSName) {
            String value = ASN1IA5String.getInstance(gn.getName()).getString();
            if (firstSanValue == null) {
              firstSanValue = value;
            }

            Identifier matchedId = null;
            for (Identifier identifier : identifiers) {
              if ("dns".equalsIgnoreCase(identifier.getType()) && value.equals(identifier.getValue())) {
                matchedId = identifier;
                break;
              }
            }

            if (matchedId != null) {
              identifiers.remove(matchedId);
            } else {
              throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badCSR,
                  "invalid DNS identifier in the extension subjectAlternativeName in CSR: " + value);
            }
          } else {
            throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badCSR,
                "unsupported name in the extension subjectAlternativeName in CSR.");
          }
        }

        if (!identifiers.isEmpty()) {
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badCSR,
              "missing identifier in the extension subjectAlternativeName in CSR: " + identifiers);
        }

        try {
          if (!GatewayUtil.verifyCsr(csr, securityFactory, popControl)) {
            throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badCSR, "could not verify signature of CSR");
          }
        } catch (Exception ex) {
          LogUtil.error(LOG, ex, "error verifying CSR");
          throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badCSR, null);
        }

        CertReqMeta certReqMeta = order.getCertReqMeta();
        if (certReqMeta == null) {
          certReqMeta = new CertReqMeta();
          order.setCertReqMeta(certReqMeta);
        }

        if (cn == null || cn.isEmpty()) {
          // DNS
          certReqMeta.setSubject("CN=" + firstSanValue);
        }

        certReqMeta.setCa(caProfile.getCa());
        certReqMeta.setCertProfile(caProfile.getTlsProfile());

        order.setCsr(csrBytes);
        order.setStatus(OrderStatus.processing);

        LOG.info("finalized order {}", order.idText());

        return buildSuccJsonResp(SC_OK, order.toResponse(baseUrl))
            .putHeader(HDR_LOCATION, order.getLocation(baseUrl));
      }
      case CMD_cert: {
        String id = tokens[1];
        AcmeOrder order = getOrder(id);
        byte[] certBytes = Optional.ofNullable(order.getCert()).orElseThrow(
            () -> new AcmeProtocolException(SC_NOT_FOUND, AcmeError.orderNotReady, "found no certificate"));

        byte[] encodedIssuer = X509Util.extractCertIssuer(certBytes);
        String hexIssuer = Hex.encode(encodedIssuer);
        byte[][] cacerts = cacertsMap.get(hexIssuer);

        if (cacerts == null) {
          try {
            cacerts = sdk.cacertsBySubject(encodedIssuer);
          } catch (SdkErrorResponseException e) {
            throw new AcmeProtocolException(SC_INTERNAL_SERVER_ERROR, AcmeError.serverInternal,
                "could not retrieve CA certificate chain");
          }

          String hexCaSubject = Hex.encode(X509Util.extractCertSubject(cacerts[0]));
          if (!hexIssuer.equals(hexCaSubject)) {
            throw new AcmeProtocolException(SC_INTERNAL_SERVER_ERROR, AcmeError.serverInternal,
                "could not retrieve CA certificate chain");
          }
          cacertsMap.put(hexCaSubject, cacerts);
        }

        byte[][] certchain = new byte[1 + cacerts.length][];
        certchain[0] = certBytes;
        System.arraycopy(cacerts, 0, certchain, 1, cacerts.length);

        byte[] respBytes = StringUtil.toUtf8Bytes(X509Util.encodeCertificates(certchain));

        LOG.info("downloaded certificate of order {}", order.idText());
        return toHttpResponse(HttpRespContent.ofOk(CT_PEM_CERTIFICATE_CHAIN, respBytes));
      }
      case CMD_authz: {
        if (tokens.length != 2) {
          throw new HttpRespAuditException(SC_NOT_FOUND, "unknown authz", AuditLevel.ERROR, AuditStatus.FAILED);
        }

        AuthzId id = new AuthzId(decodeFast(tokens[1]));
        AcmeAuthz authz = Optional.ofNullable(repo.getAuthz(id)).orElseThrow(
            () -> new HttpRespAuditException(SC_NOT_FOUND, "unknown authz", AuditLevel.ERROR, AuditStatus.FAILED));

        if (LOG.isInfoEnabled()) {
          LOG.info("downloaded authz {}: {}", id, JSON.toJson(authz.toResponse(baseUrl, id.getOrderId())));
        }
        return buildSuccJsonResp(SC_OK, authz.toResponse(baseUrl, id.getOrderId()));
      }
      case CMD_chall: {
        if (tokens.length != 2) {
          throw new HttpRespAuditException(SC_NOT_FOUND, "unknown challenge", AuditLevel.ERROR, AuditStatus.FAILED);
        }

        ChallId challId = new ChallId(decodeFast(tokens[1]));
        AcmeChallenge2 chall2 = Optional.ofNullable(repo.getChallenge(challId)).orElseThrow(
            () -> new HttpRespAuditException(SC_NOT_FOUND, "unknown challenge", AuditLevel.ERROR, AuditStatus.FAILED));

        AcmeChallenge chall = chall2.getChallenge();

        ChallengeStatus status = chall.getStatus();
        if (status == ChallengeStatus.pending) {
          chall.setStatus(ChallengeStatus.processing);
        }
        ChallengeResponse resp = chall.toChallengeResponse(baseUrl, challId.getOrderId(), challId.getAuthzId());

        LOG.info("Received ready for challenge {} of order {}", challId, challId.getOrderId());
        HttpResponse ret = buildSuccJsonResp(SC_OK, resp);//.putHeader(HDR_RETRY_AFTER, "2"); // wait for 2 seconds
        String authzUrl = chall2.getChallenge().getAuthz().getUrl(baseUrl);
        ret.putHeader(HDR_LINK, "<" + authzUrl + ">;rel=\"up\"");
        return ret;
      }
      default: {
        throw new HttpRespAuditException(SC_NOT_FOUND, "unknown command " + command,
            AuditLevel.ERROR, AuditStatus.FAILED);
      }
    }
  } // method service

  private HttpResponse toHttpResponse(HttpRespContent respContent) {
    return (respContent == null)
        ? new HttpResponse(SC_OK)
        : new HttpResponse(respContent.getStatusCode(), respContent.getContentType(), null,
            respContent.isBase64(), respContent.getContent());
  }

  private AcmeOrder getOrder(String id) throws HttpRespAuditException, AcmeSystemException {
    Long lLabel = toLongId(id);
    return Optional.ofNullable(lLabel == null ? null : repo.getOrder(lLabel)).orElseThrow(
        () -> new HttpRespAuditException(SC_NOT_FOUND, "unknown order", AuditLevel.ERROR, AuditStatus.FAILED));
  }

  private HttpResponse buildSuccJsonResp(int statusCode, Object body) {
    return toHttpResponse(HttpRespContent.of(statusCode, CT_JSON, JSON.toJSONBytes(body)));
  }

  private HttpResponse buildProblemResp(int statusCode, Problem problem) {
    byte[] bytes = JSON.toJSONBytes(problem);
    return toHttpResponse(HttpRespContent.of(statusCode, CT_PROBLEM_JSON, bytes));
  }

  private HttpResponse verifySignature(String sigAlg, PublicKey pubKey, JoseMessage joseMessage)
      throws AcmeProtocolException {
    sigAlg = sigAlg.toUpperCase(Locale.ROOT);
    SignAlgo signAlgo = joseAlgMap.get(sigAlg);
    if (signAlgo == null) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badSignatureAlgorithm,
          "unsupported signature algorrihm " + sigAlg);
    }

    try {
      Signature sig = signAlgo.newSignature();
      sig.initVerify(pubKey);
      sig.update(joseMessage.getProtected().getBytes(StandardCharsets.UTF_8));
      sig.update((byte) 0x2e); // 0x2e = '.'
      sig.update(joseMessage.getPayload().getBytes(StandardCharsets.UTF_8));
      boolean sigValid = sig.verify(decodeFast(joseMessage.getSignature()));
      if (!sigValid) {
        throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "signature is not valid");
      }
      return null;
    } catch (NoSuchAlgorithmException e) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badSignatureAlgorithm, e.getMessage());
    } catch (InvalidKeyException e) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.badPublicKey, "public key is bad");
    } catch (SignatureException e) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.malformed, "signature is not valid");
    }
  }

  private HttpResponse verifyContacts(List contacts) throws AcmeProtocolException {
    if (contacts == null || contacts.isEmpty()) {
      throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.invalidContact, "no contact is specified");
    }

    for (String contact : contacts) {
      int rc = contactVerifier.verfifyContact(contact);
      if (rc == ContactVerifier.unsupportedContact) {
        throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.unsupportedContact,
            "unsupported contact '" + contact + "'");
      } else if (rc == ContactVerifier.invalidContact) {
        throw new AcmeProtocolException(SC_BAD_REQUEST, AcmeError.invalidContact,
            "invalid contact '" + contact + "'");
      }
    }
    return null;
  }

  private String rndToken() {
    byte[] token = new byte[tokenNumBytes];
    rnd.nextBytes(token);
    return Base64Url.encodeToStringNoPadding(token);
  }

  private AcmeChallenge newChall(int subId, String type, String token, String expectedAuthorization) {
    return new AcmeChallenge(type, subId, token, expectedAuthorization, ChallengeStatus.pending);
  }

  private void cleanOrders() {
    synchronized (lastOrdersCleaned) {
      Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS);
      Instant last = Instant.ofEpochSecond(lastOrdersCleaned.get());
      if (Duration.between(last, now).compareTo(Duration.ofDays(1)) < 0) {
        // last cleanup was still within 1 day
        return;
      }

      lastOrdersCleaned.set(now.getEpochSecond());
      Instant certExpired = now.minus(cleanOrderConf.getExpiredCertDays(), ChronoUnit.DAYS);
      Instant notFinishedOrderExpires = now.minus(cleanOrderConf.getExpiredOrderDays(), ChronoUnit.DAYS);

      Thread thread = new Thread(() -> {
        try {
          int num = repo.cleanOrders(certExpired, notFinishedOrderExpires);
          LOG.info("removed {} orders with cert.notAfter < {} or not-finished-order.expires < {}",
              num, certExpired, notFinishedOrderExpires);
        } catch (Exception e) {
          LogUtil.error(LOG, e, "error cleaning orders");
        }
      });
      thread.setDaemon(true);
      thread.start();
    }
  }

  private static Map toStringMap(Map map) {
    if (map == null) {
      return null;
    }
    Map newMap = new HashMap<>(map.size() * 5 / 4);
    for (Map.Entry m : map.entrySet()) {
      newMap.put(m.getKey(), (String) m.getValue());
    }
    return newMap;
  }

  private static Long toLongId(String id) {
    return (id.length() != 11) ? null : Pack.littleEndianToLong(decodeFast(id), 0);
  }

  private AcmeProxyConf.CaProfile getCaProfile(String keyAlgId) {
    for (AcmeProxyConf.CaProfile caProfile : caProfiles) {
      if (caProfile.getKeyTypes().contains(keyAlgId)) {
        return caProfile;
      }
    }

    return null;
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy