static inline bool php_openssl_config_check_syntax
(const char *section_label, const char *config_filename, const char *section,
 LHASH_OF(CONF_VALUE) *config) {
#else
static inline bool php_openssl_config_check_syntax
(const char *section_label, const char *config_filename, const char *section,
 LHASH *config) {
#endif

  X509V3_CTX ctx;
  X509V3_set_ctx_test(&ctx);
  X509V3_set_conf_lhash(&ctx, config);
  if (!X509V3_EXT_add_conf(config, &ctx, (char*)section, nullptr)) {
    raise_warning("Error loading %s section %s of %s",
                    section_label, section, config_filename);
    return false;
  }
  return true;
}

const StaticString
  s_config("config"),
  s_config_section_name("config_section_name"),
  s_digest_alg("digest_alg"),
  s_x509_extensions("x509_extensions"),
  s_req_extensions("req_extensions"),
  s_private_key_bits("private_key_bits"),
  s_private_key_type("private_key_type"),
  s_encrypt_key("encrypt_key"),
  s_curve_name("curve_name");

static bool php_openssl_parse_config(struct php_x509_request *req,
                                     const Array& args,
                                     std::vector<String> &strings) {
  req->config_filename =
    read_string(args, s_config, default_ssl_conf_filename, strings);
  req->section_name =
    read_string(args, s_config_section_name, "req", strings);
  req->global_config = CONF_load(nullptr, default_ssl_conf_filename, nullptr);
  req->req_config = CONF_load(nullptr, req->config_filename, nullptr);
  if (req->req_config == nullptr) {
    return false;
  }

  /* read in the oids */
  char *str = CONF_get_string(req->req_config, nullptr, "oid_file");
  if (str) {
    BIO *oid_bio = BIO_new_file(str, "r");
    if (oid_bio) {
      OBJ_create_objects(oid_bio);
      BIO_free(oid_bio);
    }
  }
  if (!add_oid_section(req)) {
    return false;
  }

  req->digest_name =
    read_string(args, s_digest_alg,
                CONF_get_string(req->req_config, req->section_name,
                                "default_md"),
                strings);

  req->extensions_section =
    read_string(args, s_x509_extensions,
                CONF_get_string(req->req_config, req->section_name,
                                "x509_extensions"),
                strings);

  req->request_extensions_section =
    read_string(args, s_req_extensions,
                CONF_get_string(req->req_config, req->section_name,
                                "req_extensions"),
                strings);

  req->priv_key_bits =
    read_integer(args, s_private_key_bits,
                 CONF_get_number(req->req_config, req->section_name,
                                 "default_bits"));

  req->priv_key_type =
    read_integer(args, s_private_key_type, OPENSSL_KEYTYPE_DEFAULT);

  if (args.exists(s_encrypt_key)) {
    bool value = args[s_encrypt_key].toBoolean();
    req->priv_key_encrypt = value ? 1 : 0;
  } else {
    str = CONF_get_string(req->req_config, req->section_name,
                          "encrypt_rsa_key");
    if (str == nullptr) {
      str = CONF_get_string(req->req_config, req->section_name, "encrypt_key");
    }
    if (str && strcmp(str, "no") == 0) {
      req->priv_key_encrypt = 0;
    } else {
      req->priv_key_encrypt = 1;
    }
  }

  /* digest alg */
  if (req->digest_name == nullptr) {
    req->digest_name = CONF_get_string(req->req_config, req->section_name,
                                       "default_md");
  }
  if (req->digest_name) {
    req->digest = req->md_alg = EVP_get_digestbyname(req->digest_name);
  }
  if (req->md_alg == nullptr) {
    req->md_alg = req->digest = EVP_sha256();
  }

#ifdef HAVE_EVP_PKEY_EC
  /* set the ec group curve name */
  req->curve_name = NID_undef;
  if (args.exists(s_curve_name)) {
    auto const curve_name = args[s_curve_name].toString();
    req->curve_name = OBJ_sn2nid(curve_name.data());
    if (req->curve_name == NID_undef) {
      raise_warning(
        "Unknown elliptic curve (short) name %s",
        curve_name.data()
      );
      return false;
    }
  }
#endif

  if (req->extensions_section &&
      !php_openssl_config_check_syntax
      ("extensions_section", req->config_filename, req->extensions_section,
       req->req_config)) {
    return false;
  }

  /* set the string mask */
  str = CONF_get_string(req->req_config, req->section_name, "string_mask");
  if (str && !ASN1_STRING_set_default_mask_asc(str)) {
    raise_warning("Invalid global string mask setting %s", str);
    return false;
  }

  if (req->request_extensions_section &&
      !php_openssl_config_check_syntax
      ("request_extensions_section", req->config_filename,
       req->request_extensions_section, req->req_config)) {
    return false;
  }

  return true;
}

static void php_openssl_dispose_config(struct php_x509_request *req) {
  if (req->global_config) {
    CONF_free(req->global_config);
    req->global_config = nullptr;
  }
  if (req->req_config) {
    CONF_free(req->req_config);
    req->req_config = nullptr;
  }
}

static STACK_OF(X509) *load_all_certs_from_file(const char *certfile) {
  STACK_OF(X509_INFO) *sk = nullptr;
  STACK_OF(X509) *stack = nullptr, *ret = nullptr;
  BIO *in = nullptr;
  X509_INFO *xi;

  if (!(stack = sk_X509_new_null())) {
    raise_warning("memory allocation failure");
    goto end;
  }

  if (!(in = BIO_new_file(certfile, "r"))) {
    raise_warning("error opening the file, %s", certfile);
    sk_X509_free(stack);
    goto end;
  }

  /* This loads from a file, a stack of x509/crl/pkey sets */
  if (!(sk = PEM_X509_INFO_read_bio(in, nullptr, nullptr, nullptr))) {
    raise_warning("error reading the file, %s", certfile);
    sk_X509_free(stack);
    goto end;
  }

  /* scan over it and pull out the certs */
  while (sk_X509_INFO_num(sk)) {
    xi = sk_X509_INFO_shift(sk);
    if (xi->x509 != nullptr) {
      sk_X509_push(stack, xi->x509);
      xi->x509 = nullptr;
    }
    X509_INFO_free(xi);
  }
  if (!sk_X509_num(stack)) {
    raise_warning("no certificates in file, %s", certfile);
    sk_X509_free(stack);
    goto end;
  }
  ret = stack;

end:
  BIO_free(in);
  sk_X509_INFO_free(sk);

  return ret;
}

/**
 * calist is an array containing file and directory names.  create a
 * certificate store and add those certs to it for use in verification.
 */
static X509_STORE *setup_verify(const Array& calist) {
  X509_STORE *store = X509_STORE_new();
  if (store == nullptr) {
    return nullptr;
  }

  X509_LOOKUP *dir_lookup, *file_lookup;
  int ndirs = 0, nfiles = 0;
  for (ArrayIter iter(calist); iter; ++iter) {
    String item = iter.second().toString();

    struct stat sb;
    if (stat(item.data(), &sb) == -1) {
      raise_warning("unable to stat %s", item.data());
      continue;
    }

    if ((sb.st_mode & S_IFREG) == S_IFREG) {
      file_lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
      if (file_lookup == nullptr ||
          !X509_LOOKUP_load_file(file_lookup, item.data(),
                                 X509_FILETYPE_PEM)) {
        raise_warning("error loading file %s", item.data());
      } else {
        nfiles++;
      }
      file_lookup = nullptr;
    } else {
      dir_lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir());
      if (dir_lookup == nullptr ||
          !X509_LOOKUP_add_dir(dir_lookup, item.data(), X509_FILETYPE_PEM)) {
        raise_warning("error loading directory %s", item.data());
      } else {
        ndirs++;
      }
      dir_lookup = nullptr;
    }
  }
  if (nfiles == 0) {
    file_lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
    if (file_lookup) {
      X509_LOOKUP_load_file(file_lookup, nullptr, X509_FILETYPE_DEFAULT);
    }
  }
  if (ndirs == 0) {
    dir_lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir());
    if (dir_lookup) {
      X509_LOOKUP_add_dir(dir_lookup, nullptr, X509_FILETYPE_DEFAULT);
    }
  }
  return store;
}

///////////////////////////////////////////////////////////////////////////////

static bool add_entries(X509_NAME *subj, const Array& items) {
  for (ArrayIter iter(items); iter; ++iter) {
    auto const index = iter.first().toString();
    auto const item = iter.second().toString();
    int nid = OBJ_txt2nid(index.data());
    if (nid != NID_undef) {
      if (!X509_NAME_add_entry_by_NID(subj, nid, MBSTRING_ASC,
                                      (unsigned char*)item.data(),
                                      -1, -1, 0)) {
        raise_warning("dn: add_entry_by_NID %d -> %s (failed)",
                        nid, item.data());
        return false;
      }
    } else {
      raise_warning("dn: %s is not a recognized name", index.data());
    }
  }
  return true;
}

static bool php_openssl_make_REQ(struct php_x509_request *req, X509_REQ *csr,
                                 const Array& dn, const Array& attribs) {
  char *dn_sect = CONF_get_string(req->req_config, req->section_name,
                                  "distinguished_name");
  if (dn_sect == nullptr) return false;

  STACK_OF(CONF_VALUE) *dn_sk = CONF_get_section(req->req_config, dn_sect);
  if (dn_sk == nullptr) return false;

  char *attr_sect = CONF_get_string(req->req_config, req->section_name,
                                    "attributes");
  STACK_OF(CONF_VALUE) *attr_sk = nullptr;
  if (attr_sect) {
    attr_sk = CONF_get_section(req->req_config, attr_sect);
    if (attr_sk == nullptr) {
      return false;
    }
  }

  /* setup the version number: version 1 */
  if (X509_REQ_set_version(csr, 0L)) {
    X509_NAME *subj = X509_REQ_get_subject_name(csr);
    if (!add_entries(subj, dn)) return false;

    /* Finally apply defaults from config file */
    for (int i = 0; i < sk_CONF_VALUE_num(dn_sk); i++) {
      CONF_VALUE *v = sk_CONF_VALUE_value(dn_sk, i);
      char *type = v->name;

      int len = strlen(type);
      if (len < (int)sizeof("_default")) {
        continue;
      }
      len -= sizeof("_default") - 1;
      if (strcmp("_default", type + len) != 0) {
        continue;
      }
      if (len > 200) {
        len = 200;
      }
      char buffer[200 + 1]; /*200 + \0 !*/
      memcpy(buffer, type, len);
      buffer[len] = '\0';
      type = buffer;

      /* Skip past any leading X. X: X, etc to allow for multiple instances */
      for (char *str = type; *str; str++) {
        if (*str == ':' || *str == ',' || *str == '.') {
          str++;
          if (*str) {
            type = str;
          }
          break;
        }
      }
      /* if it is already set, skip this */
      int nid = OBJ_txt2nid(type);
      if (X509_NAME_get_index_by_NID(subj, nid, -1) >= 0) {
        continue;
      }
      if (!X509_NAME_add_entry_by_txt(subj, type, MBSTRING_ASC,
                                      (unsigned char*)v->value, -1, -1, 0)) {
        raise_warning("add_entry_by_txt %s -> %s (failed)", type, v->value);
        return false;
      }
      if (!X509_NAME_entry_count(subj)) {
        raise_warning("no objects specified in config file");
        return false;
      }
    }

    if (!add_entries(subj, attribs)) return false;

    if (attr_sk) {
      for (int i = 0; i < sk_CONF_VALUE_num(attr_sk); i++) {
        CONF_VALUE *v = sk_CONF_VALUE_value(attr_sk, i);
        /* if it is already set, skip this */
        int nid = OBJ_txt2nid(v->name);
        if (X509_REQ_get_attr_by_NID(csr, nid, -1) >= 0) {
          continue;
        }
        if (!X509_REQ_add1_attr_by_txt(csr, v->name, MBSTRING_ASC,
                                       (unsigned char*)v->value, -1)) {
          /**
           * hzhao: mismatched version of conf file may have attributes that
           * are not recognizable, and I don't think it should be treated as
           * fatal errors.
           */
          Logger::Verbose("add1_attr_by_txt %s -> %s (failed)", v->name,
                          v->value);
          // return false;
        }
      }
    }
  }

  X509_REQ_set_pubkey(csr, req->priv_key);
  return true;
}

bool HHVM_FUNCTION(openssl_csr_export_to_file, const Variant& csr,
                                               const String& outfilename,
                                               bool notext /* = true */) {
  auto pcsr = CSRequest::Get(csr);
  if (!pcsr) return false;

  BIO *bio_out = BIO_new_file((char*)outfilename.data(), "w");
  if (bio_out == nullptr) {
    raise_warning("error opening file %s", outfilename.data());
    return false;
  }

  if (!notext) {
    X509_REQ_print(bio_out, pcsr->csr());
  }
  PEM_write_bio_X509_REQ(bio_out, pcsr->csr());
  BIO_free(bio_out);
  return true;
}

bool HHVM_FUNCTION(openssl_csr_export, const Variant& csr, Variant& out,
                                       bool notext /* = true */) {
  auto pcsr = CSRequest::Get(csr);
  if (!pcsr) return false;

  BIO *bio_out = BIO_new(BIO_s_mem());
  if (!notext) {
    X509_REQ_print(bio_out, pcsr->csr());
  }

  if (PEM_write_bio_X509_REQ(bio_out, pcsr->csr())) {
    BUF_MEM *bio_buf;
    BIO_get_mem_ptr(bio_out, &bio_buf);
    out = String((char*)bio_buf->data, bio_buf->length, CopyString);
    BIO_free(bio_out);
    return true;
  }

  BIO_free(bio_out);
  return false;
}

Variant HHVM_FUNCTION(openssl_csr_get_public_key, const Variant& csr) {
  auto pcsr = CSRequest::Get(csr);
  if (!pcsr) return false;

  auto input_csr = pcsr->csr();

#if OPENSSL_VERSION_NUMBER >= 0x10100000
  /* Due to changes in OpenSSL 1.1 related to locking when decoding CSR,
   * the pub key is not changed after assigning. It means if we pass
   * a private key, it will be returned including the private part.
   * If we duplicate it, then we get just the public part which is
   * the same behavior as for OpenSSL 1.0 */
  input_csr = X509_REQ_dup(input_csr);
  /* We need to free the CSR as it was duplicated */
  SCOPE_EXIT { X509_REQ_free(input_csr); };
#endif
  auto pubkey = X509_REQ_get_pubkey(input_csr);
  if (!pubkey) return false;
  return Variant(req::make<Key>(pubkey));
}

Variant HHVM_FUNCTION(openssl_csr_get_subject, const Variant& csr,
                      bool use_shortnames /* = true */) {
  auto pcsr = CSRequest::Get(csr);
  if (!pcsr) return false;

  X509_NAME *subject = X509_REQ_get_subject_name(pcsr->csr());
  Array ret = Array::CreateDArray();
  add_assoc_name_entry(ret, nullptr, subject, use_shortnames);
  return ret;
}

Variant HHVM_FUNCTION(openssl_csr_new,
                      const Variant& dn, Variant& privkey,
                      const Variant& configargs /* = uninit_variant */,
                      const Variant& extraattribs /* = uninit_variant */) {
  Variant ret = false;
  struct php_x509_request req;
  memset(&req, 0, sizeof(req));

  req::ptr<Key> okey;
  X509_REQ *csr = nullptr;
  std::vector<String> strings;
  if (php_openssl_parse_config(&req, configargs.toArray(), strings)) {
    /* Generate or use a private key */
    if (!privkey.isNull()) {
      okey = Key::Get(privkey, false);
      if (okey) {
        req.priv_key = okey->m_key;
      }
    }
    if (req.priv_key == nullptr) {
      req.generatePrivateKey();
      if (req.priv_key) {
        okey = req::make<Key>(req.priv_key);
      }
    }
    if (req.priv_key == nullptr) {
      raise_warning("Unable to generate a private key");
    } else {
      csr = X509_REQ_new();
      if (csr && php_openssl_make_REQ(&req, csr, dn.toArray(),
                                      extraattribs.toArray())) {
        X509V3_CTX ext_ctx;
        X509V3_set_ctx(&ext_ctx, nullptr, nullptr, csr, nullptr, 0);
        X509V3_set_conf_lhash(&ext_ctx, req.req_config);

        /* Add extensions */
        if (req.request_extensions_section &&
            !X509V3_EXT_REQ_add_conf(req.req_config, &ext_ctx,
                                     (char*)req.request_extensions_section,
                                     csr)) {
          raise_warning("Error loading extension section %s",
                          req.request_extensions_section);
        } else {
          ret = true;
          if (X509_REQ_sign(csr, req.priv_key, req.digest)) {
            ret = req::make<CSRequest>(csr);
            csr = nullptr;
          } else {
            raise_warning("Error signing request");
          }

          privkey = Variant(okey);
        }
      }
    }
  }
  if (csr) {
    X509_REQ_free(csr);
  }
  php_openssl_dispose_config(&req);
  return ret;
}

Variant HHVM_FUNCTION(openssl_csr_sign, const Variant& csr,
                                        const Variant& cacert,
                                        const Variant& priv_key, int days,
                                        const Variant& configargs /* = null */,
                                        int serial /* = 0 */) {
  auto pcsr = CSRequest::Get(csr);
  if (!pcsr) return false;

  req::ptr<Certificate> ocert;
  if (!cacert.isNull()) {
    ocert = Certificate::Get(cacert);
    if (!ocert) {
      raise_warning("cannot get cert from parameter 2");
      return false;
    }
  }
  auto okey = Key::Get(priv_key, false);
  if (!okey) {
    raise_warning("cannot get private key from parameter 3");
    return false;
  }
  X509 *cert = nullptr;
  if (ocert) {
    cert = ocert->m_cert;
  }
  EVP_PKEY *pkey = okey->m_key;
  if (cert && !X509_check_private_key(cert, pkey)) {
    raise_warning("private key does not correspond to signing cert");
    return false;
  }

  req::ptr<Certificate> onewcert;
  struct php_x509_request req;
  memset(&req, 0, sizeof(req));
  Variant ret = false;
  std::vector<String> strings;
  if (!php_openssl_parse_config(&req, configargs.toArray(), strings)) {
    goto cleanup;
  }

  /* Check that the request matches the signature */
  EVP_PKEY *key;
  key = X509_REQ_get_pubkey(pcsr->csr());
  if (key == nullptr) {
    raise_warning("error unpacking public key");
    goto cleanup;
  }
  int i;
  i = X509_REQ_verify(pcsr->csr(), key);
  if (i < 0) {
    raise_warning("Signature verification problems");
    goto cleanup;
  }
  if (i == 0) {
    raise_warning("Signature did not match the certificate request");
    goto cleanup;
  }

  /* Now we can get on with it */
  X509 *new_cert;
  new_cert = X509_new();
  if (new_cert == nullptr) {
    raise_warning("No memory");
    goto cleanup;
  }
  onewcert = req::make<Certificate>(new_cert);
  /* Version 3 cert */
  if (!X509_set_version(new_cert, 2)) {
    goto cleanup;
  }
  ASN1_INTEGER_set(X509_get_serialNumber(new_cert), serial);
  X509_set_subject_name(new_cert, X509_REQ_get_subject_name(pcsr->csr()));

  if (cert == nullptr) {
    cert = new_cert;
  }
  if (!X509_set_issuer_name(new_cert, X509_get_subject_name(cert))) {
    goto cleanup;
  }
  X509_gmtime_adj(X509_get_notBefore(new_cert), 0);
  X509_gmtime_adj(X509_get_notAfter(new_cert), (long)60 * 60 * 24 * days);
  i = X509_set_pubkey(new_cert, key);
  if (!i) {
    goto cleanup;
  }
  if (req.extensions_section) {
    X509V3_CTX ctx;
    X509V3_set_ctx(&ctx, cert, new_cert, pcsr->csr(), nullptr, 0);
    X509V3_set_conf_lhash(&ctx, req.req_config);
    if (!X509V3_EXT_add_conf(req.req_config, &ctx, (char*)req.extensions_section,
                             new_cert)) {
      goto cleanup;
    }
  }

  /* Now sign it */
  if (!X509_sign(new_cert, pkey, req.digest)) {
    raise_warning("failed to sign it");
    goto cleanup;
  }

  /* Succeeded; lets return the cert */
  ret = onewcert;

 cleanup:
  php_openssl_dispose_config(&req);
  return ret;
}

Variant HHVM_FUNCTION(openssl_error_string) {
  char buf[512];
  unsigned long val = ERR_get_error();
  if (val) {
    return String(ERR_error_string(val, buf), CopyString);
  }
  return false;
}

bool HHVM_FUNCTION(openssl_open, const String& sealed_data, Variant& open_data,
                                 const String& env_key,
                                 const Variant& priv_key_id,
                                 const String& method, /* = null_string */
                                 const String& iv /* = null_string */) {
  const EVP_CIPHER *cipher_type;
  if (method.empty()) {
    cipher_type = EVP_rc4();
  } else {
    cipher_type = EVP_get_cipherbyname(method.c_str());
    if (!cipher_type) {
      raise_warning("Unknown cipher algorithm");
      return false;
    }
  }

  auto okey = Key::Get(priv_key_id, false);
  if (!okey) {
    raise_warning("unable to coerce parameter 4 into a private key");
    return false;
  }
  EVP_PKEY *pkey = okey->m_key;

  const unsigned char *iv_buf = nullptr;
  int iv_len = EVP_CIPHER_iv_length(cipher_type);
  if (iv_len > 0) {
    if (iv.empty()) {
      raise_warning(
        "Cipher algorithm requires an IV to be supplied as a sixth parameter");
      return false;
    }
    if (iv.length() != iv_len) {
      raise_warning("IV length is invalid");
      return false;
    }
    iv_buf = reinterpret_cast<const unsigned char*>(iv.c_str());
  }

  String s = String(sealed_data.size(), ReserveString);
  unsigned char *buf = (unsigned char *)s.mutableData();

  EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
  if (ctx == nullptr) {
    raise_warning("Failed to allocate an EVP_CIPHER_CTX object");
    return false;
  }
  SCOPE_EXIT {
    EVP_CIPHER_CTX_free(ctx);
  };
  int len1, len2;
  if (!EVP_OpenInit(
          ctx,
          cipher_type,
          (unsigned char*)env_key.data(),
          env_key.size(),
          iv_buf,
          pkey) ||
      !EVP_OpenUpdate(
          ctx,
          buf,
          &len1,
          (unsigned char*)sealed_data.data(),
          sealed_data.size()) ||
      !EVP_OpenFinal(ctx, buf + len1, &len2) || len1 + len2 == 0) {
    return false;
  }
  open_data = s.setSize(len1 + len2);
  return true;
}

static STACK_OF(X509) *php_array_to_X509_sk(const Variant& certs) {
  STACK_OF(X509) *pcerts = sk_X509_new_null();
  Array arrCerts;
  if (certs.isArray()) {
    arrCerts = certs.toArray();
  } else {
    arrCerts.append(certs);
  }
  for (ArrayIter iter(arrCerts); iter; ++iter) {
    auto ocert = Certificate::Get(iter.second());
    if (!ocert) {
      break;
    }
    sk_X509_push(pcerts, ocert->m_cert);
  }
  return pcerts;
}

const StaticString
  s_friendly_name("friendly_name"),
  s_extracerts("extracerts");

static bool
openssl_pkcs12_export_impl(const Variant& x509, BIO *bio_out,
                           const Variant& priv_key, const String& pass,
                           const Variant& args /* = uninit_variant */) {
  auto ocert = Certificate::Get(x509);
  if (!ocert) {
    raise_warning("cannot get cert from parameter 1");
    return false;
  }
  auto okey = Key::Get(priv_key, false);
  if (!okey) {
    raise_warning("cannot get private key from parameter 3");
    return false;
  }
  X509 *cert = ocert->m_cert;
  EVP_PKEY *key = okey->m_key;
  if (cert && !X509_check_private_key(cert, key)) {
    raise_warning("private key does not correspond to cert");
    return false;
  }

  Array arrArgs = args.toArray();

  String friendly_name;
  if (arrArgs.exists(s_friendly_name)) {
    friendly_name = arrArgs[s_friendly_name].toString();
  }

  STACK_OF(X509) *ca = nullptr;
  if (arrArgs.exists(s_extracerts)) {
    ca = php_array_to_X509_sk(arrArgs[s_extracerts]);
  }

  PKCS12 *p12 = PKCS12_create
    ((char*)pass.data(),
     (char*)(friendly_name.empty() ? nullptr : friendly_name.data()),
     key, cert, ca, 0, 0, 0, 0, 0);

  assertx(bio_out);
  bool ret = i2d_PKCS12_bio(bio_out, p12);
  PKCS12_free(p12);
  sk_X509_free(ca);
  return ret;
}

bool HHVM_FUNCTION(openssl_pkcs12_export_to_file, const Variant& x509,
                                                  const String& filename,
                                                  const Variant& priv_key,
                                                  const String& pass,
                                  const Variant& args /* = uninit_variant */) {
  BIO *bio_out = BIO_new_file(filename.data(), "w");
  if (bio_out == nullptr) {
    raise_warning("error opening file %s", filename.data());
    return false;
  }
  bool ret = openssl_pkcs12_export_impl(x509, bio_out, priv_key, pass, args);
  BIO_free(bio_out);
  return ret;
}

bool HHVM_FUNCTION(openssl_pkcs12_export, const Variant& x509, Variant& out,
                                          const Variant& priv_key,
                                          const String& pass,
                                  const Variant& args /* = uninit_variant */) {
  BIO *bio_out = BIO_new(BIO_s_mem());
  bool ret = openssl_pkcs12_export_impl(x509, bio_out, priv_key, pass, args);
  if (ret) {
    BUF_MEM *bio_buf;
    BIO_get_mem_ptr(bio_out, &bio_buf);
    out = String((char*)bio_buf->data, bio_buf->length, CopyString);
  }
  BIO_free(bio_out);
  return ret;
}

const StaticString
  s_cert("cert"),
  s_pkey("pkey");

bool HHVM_FUNCTION(openssl_pkcs12_read, const String& pkcs12, Variant& certs,
                                        const String& pass) {
  bool ret = false;
  PKCS12 *p12 = nullptr;

  BIO *bio_in = BIO_new(BIO_s_mem());
  if (!BIO_write(bio_in, pkcs12.data(), pkcs12.size())) {
    goto cleanup;
  }

  if (d2i_PKCS12_bio(bio_in, &p12)) {
    EVP_PKEY *pkey = nullptr;
    X509 *cert = nullptr;
    STACK_OF(X509) *ca = nullptr;
    if (PKCS12_parse(p12, pass.data(), &pkey, &cert, &ca)) {
      Variant vcerts = Array::CreateDArray();
      SCOPE_EXIT {
        certs = vcerts;
      };
      BIO *bio_out = nullptr;
      if (cert) {
        bio_out = BIO_new(BIO_s_mem());
        if (PEM_write_bio_X509(bio_out, cert)) {
          BUF_MEM *bio_buf;
          BIO_get_mem_ptr(bio_out, &bio_buf);
          vcerts.asArrRef().set(s_cert,
            String((char*)bio_buf->data, bio_buf->length, CopyString));
        }
        BIO_free(bio_out);
      }

      if (pkey) {
        bio_out = BIO_new(BIO_s_mem());
        if (PEM_write_bio_PrivateKey(bio_out, pkey, nullptr, nullptr, 0, 0, nullptr)) {
          BUF_MEM *bio_buf;
          BIO_get_mem_ptr(bio_out, &bio_buf);
          vcerts.asArrRef().set(s_pkey,
            String((char*)bio_buf->data, bio_buf->length, CopyString));
        }
        BIO_free(bio_out);
      }

      if (ca) {
        Array extracerts;
        for (X509 *aCA = sk_X509_pop(ca); aCA; aCA = sk_X509_pop(ca)) {
          bio_out = BIO_new(BIO_s_mem());
          if (PEM_write_bio_X509(bio_out, aCA)) {
            BUF_MEM *bio_buf;
            BIO_get_mem_ptr(bio_out, &bio_buf);
            extracerts.append(String((char*)bio_buf->data, bio_buf->length,
                                     CopyString));
          }
          BIO_free(bio_out);
          X509_free(aCA);
        }
        sk_X509_free(ca);
        vcerts.asArrRef().set(s_extracerts, extracerts);
      }
      ret = true;
      PKCS12_free(p12);
    }
  }

 cleanup:
  if (bio_in) {
    BIO_free(bio_in);
  }

  return ret;
}

bool HHVM_FUNCTION(openssl_pkcs7_decrypt, const String& infilename,
                                          const String& outfilename,
                                          const Variant& recipcert,
                              const Variant& recipkey /* = uninit_variant */) {
  bool ret = false;
  BIO *in = nullptr, *out = nullptr, *datain = nullptr;
  PKCS7 *p7 = nullptr;
  req::ptr<Key> okey;

  auto ocert = Certificate::Get(recipcert);
  if (!ocert) {
    raise_warning("unable to coerce parameter 3 to x509 cert");
    goto clean_exit;
  }

  okey = Key::Get(recipkey.isNull() ? recipcert : recipkey, false);
  if (!okey) {
    raise_warning("unable to get private key");
    goto clean_exit;
  }

  in = BIO_new_file(infilename.data(), "r");
  if (in == nullptr) {
    raise_warning("error opening the file, %s", infilename.data());
    goto clean_exit;
  }
  out = BIO_new_file(outfilename.data(), "w");
  if (out == nullptr) {
    raise_warning("error opening the file, %s", outfilename.data());
    goto clean_exit;
  }

  p7 = SMIME_read_PKCS7(in, &datain);
  if (p7 == nullptr) {
    goto clean_exit;
  }
  assertx(okey->m_key);
  assertx(ocert->m_cert);
  if (PKCS7_decrypt(p7, okey->m_key, ocert->m_cert, out, PKCS7_DETACHED)) {
    ret = true;
  }

 clean_exit:
  PKCS7_free(p7);
  BIO_free(datain);
  BIO_free(in);
  BIO_free(out);

  return ret;
}

static void print_headers(BIO *outfile, const Array& headers) {
  if (!headers.isNull()) {
    if (headers->isVectorData()) {
      for (ArrayIter iter(headers); iter; ++iter) {
        BIO_printf(outfile, "%s\n", iter.second().toString().data());
      }
    } else {
      for (ArrayIter iter(headers); iter; ++iter) {
        BIO_printf(outfile, "%s: %s\n", iter.first().toString().data(),
                   iter.second().toString().data());
      }
    }
  }
}

bool HHVM_FUNCTION(openssl_pkcs7_encrypt, const String& infilename,
                                          const String& outfilename,
                                          const Variant& recipcerts,
                                          const Array& headers,
                                          int flags /* = 0 */,
                                int cipherid /* = k_OPENSSL_CIPHER_RC2_40 */) {
  bool ret = false;
  BIO *infile = nullptr, *outfile = nullptr;
  STACK_OF(X509) *precipcerts = nullptr;
  PKCS7 *p7 = nullptr;
  const EVP_CIPHER *cipher = nullptr;

  infile = BIO_new_file(infilename.data(), (flags & PKCS7_BINARY) ? "rb" : "r");
  if (infile == nullptr) {
    raise_warning("error opening the file, %s", infilename.data());
    goto clean_exit;
  }
  outfile = BIO_new_file(outfilename.data(), "w");
  if (outfile == nullptr) {
    raise_warning("error opening the file, %s", outfilename.data());
    goto clean_exit;
  }

  precipcerts = php_array_to_X509_sk(recipcerts);

  /* sanity check the cipher */
  switch (cipherid) {
#ifndef OPENSSL_NO_RC2
  case PHP_OPENSSL_CIPHER_RC2_40:  cipher = EVP_rc2_40_cbc();   break;
  case PHP_OPENSSL_CIPHER_RC2_64:  cipher = EVP_rc2_64_cbc();   break;
  case PHP_OPENSSL_CIPHER_RC2_128: cipher = EVP_rc2_cbc();      break;
#endif
#ifndef OPENSSL_NO_DES
  case PHP_OPENSSL_CIPHER_DES:     cipher = EVP_des_cbc();      break;
  case PHP_OPENSSL_CIPHER_3DES:    cipher = EVP_des_ede3_cbc(); break;
#endif
  default:
    raise_warning("Invalid cipher type `%d'", cipherid);
    goto clean_exit;
  }
  if (cipher == nullptr) {
    raise_warning("Failed to get cipher");
    goto clean_exit;
  }

  p7 = PKCS7_encrypt(precipcerts, infile, (EVP_CIPHER*)cipher, flags);
  if (p7 == nullptr) goto clean_exit;

  print_headers(outfile, headers);
  (void)BIO_reset(infile);
  SMIME_write_PKCS7(outfile, p7, infile, flags);
  ret = true;

 clean_exit:
  PKCS7_free(p7);
  BIO_free(infile);
  BIO_free(outfile);
  sk_X509_free(precipcerts);
  return ret;
}

bool HHVM_FUNCTION(openssl_pkcs7_sign, const String& infilename,
                                       const String& outfilename,
                                       const Variant& signcert,
                                       const Variant& privkey,
                                       const Variant& headers,
                                       int flags /* = k_PKCS7_DETACHED */,
                                const String& extracerts /* = null_string */) {
  bool ret = false;
  STACK_OF(X509) *others = nullptr;
  BIO *infile = nullptr, *outfile = nullptr;
  PKCS7 *p7 = nullptr;
  req::ptr<Key> okey;
  req::ptr<Certificate> ocert;

  if (!extracerts.empty()) {
    others = load_all_certs_from_file(extracerts.data());
    if (others == nullptr) {
      goto clean_exit;
    }
  }

  okey = Key::Get(privkey, false);
  if (!okey) {
    raise_warning("error getting private key");
    goto clean_exit;
  }
  EVP_PKEY *key;
  key = okey->m_key;

  ocert = Certificate::Get(signcert);
  if (!ocert) {
    raise_warning("error getting cert");
    goto clean_exit;
  }
  X509 *cert;
  cert = ocert->m_cert;

  infile = BIO_new_file(infilename.data(), (flags & PKCS7_BINARY) ? "rb" : "r");
  if (infile == nullptr) {
    raise_warning("error opening input file %s!", infilename.data());
    goto clean_exit;
  }

  outfile = BIO_new_file(outfilename.data(), "w");
  if (outfile == nullptr) {
    raise_warning("error opening output file %s!", outfilename.data());
    goto clean_exit;
  }

  p7 = PKCS7_sign(cert, key, others, infile, flags);
  if (p7 == nullptr) {
    raise_warning("error creating PKCS7 structure!");
    goto clean_exit;
  }

  print_headers(outfile, headers.toArray());
  (void)BIO_reset(infile);
  SMIME_write_PKCS7(outfile, p7, infile, flags);
  ret = true;

 clean_exit:
  PKCS7_free(p7);
  BIO_free(infile);
  BIO_free(outfile);
  if (others) {
    sk_X509_pop_free(others, X509_free);
  }

  return ret;
}

static int pkcs7_ignore_expiration(int ok, X509_STORE_CTX *ctx) {
  if (ok) {
    return ok;
  }
  int error = X509_STORE_CTX_get_error(ctx);
  if (error == X509_V_ERR_CERT_HAS_EXPIRED) {
    // ignore cert expirations
    Logger::Verbose("Ignoring cert expiration");
    return 1;
  }
  return ok;
}

/**
 *  NOTE: when ignore_cert_expiration is true, a custom certificate validation
 *  callback is set up. Please be aware of this if you modify the function to
 *  allow other certificate validation behaviors
 */
Variant openssl_pkcs7_verify_core(
  const String& filename,
  int flags,
  const Variant& voutfilename /* = null_string */,
  const Variant& vcainfo /* = null_array */,
  const Variant& vextracerts /* = null_string */,
  const Variant& vcontent /* = null_string */,
  bool ignore_cert_expiration
) {
  Variant ret = -1;
  X509_STORE *store = nullptr;
  BIO *in = nullptr;
  PKCS7 *p7 = nullptr;
  BIO *datain = nullptr;
  BIO *dataout = nullptr;

  auto cainfo = vcainfo.toArray();
  auto extracerts = vextracerts.toString();
  auto content = vcontent.toString();

  STACK_OF(X509) *others = nullptr;
  if (!extracerts.empty()) {
    others = load_all_certs_from_file(extracerts.data());
    if (others == nullptr) {
      goto clean_exit;
    }
  }

  flags = flags & ~PKCS7_DETACHED;

  store = setup_verify(cainfo);
  if (!store) {
    goto clean_exit;
  }
  if (ignore_cert_expiration) {
#if (OPENSSL_VERSION_NUMBER >= 0x10000000)
    // make sure no other callback is specified
  #if OPENSSL_VERSION_NUMBER >= 0x10100000L
    assertx(!X509_STORE_get_verify_cb(store));
  #else
    assertx(!store->verify_cb);
  #endif
    // ignore expired certs
    X509_STORE_set_verify_cb(store, pkcs7_ignore_expiration);
#else
    always_assert(false);
#endif
  }
  in = BIO_new_file(filename.data(), (flags & PKCS7_BINARY) ? "rb" : "r");
  if (in == nullptr) {
    raise_warning("error opening the file, %s", filename.data());
    goto clean_exit;
  }

  p7 = SMIME_read_PKCS7(in, &datain);
  if (p7 == nullptr) {
    goto clean_exit;
  }

  if (!content.empty()) {
    dataout = BIO_new_file(content.data(), "w");
    if (dataout == nullptr) {
      raise_warning("error opening the file, %s", content.data());
      goto clean_exit;
    }
  }

  if (PKCS7_verify(p7, others, store, datain, dataout, flags)) {
    ret = true;
    auto outfilename = voutfilename.toString();
    if (!outfilename.empty()) {
      BIO *certout = BIO_new_file(outfilename.data(), "w");
      if (certout) {
        STACK_OF(X509) *signers = PKCS7_get0_signers(p7, nullptr, flags);
        for (int i = 0; i < sk_X509_num(signers); i++) {
          PEM_write_bio_X509(certout, sk_X509_value(signers, i));
        }
        BIO_free(certout);
        sk_X509_free(signers);
      } else {
        raise_warning("signature OK, but cannot open %s for writing",
                        outfilename.data());
        ret = -1;
      }
    }
    goto clean_exit;
  } else {
    ret = false;
  }

 clean_exit:
  X509_STORE_free(store);
  BIO_free(datain);
  BIO_free(in);
  BIO_free(dataout);
  PKCS7_free(p7);
  sk_X509_pop_free(others, X509_free);

  return ret;
}

Variant HHVM_FUNCTION(openssl_pkcs7_verify, const String& filename, int flags,
                               const Variant& voutfilename /* = null_string */,
                               const Variant& vcainfo /* = null_array */,
                               const Variant& vextracerts /* = null_string */,
                               const Variant& vcontent /* = null_string */) {
  return openssl_pkcs7_verify_core(filename, flags, voutfilename, vcainfo,
                                   vextracerts, vcontent, false);
}

static bool
openssl_pkey_export_impl(const Variant& key, BIO *bio_out,
                         const String& passphrase /* = null_string */,
                         const Variant& configargs /* = uninit_variant */) {
  auto okey = Key::Get(key, false, passphrase.data());
  if (!okey) {
    raise_warning("cannot get key from parameter 1");
    return false;
  }
  EVP_PKEY *pkey = okey->m_key;

  struct php_x509_request req;
  memset(&req, 0, sizeof(req));
  std::vector<String> strings;
  bool ret = false;
  if (php_openssl_parse_config(&req, configargs.toArray(), strings)) {
    const EVP_CIPHER *cipher;
    if (!passphrase.empty() && req.priv_key_encrypt) {
      cipher = (EVP_CIPHER *)EVP_des_ede3_cbc();
    } else {
      cipher = nullptr;
    }
    assertx(bio_out);

    switch (EVP_PKEY_id(pkey)) {
#ifdef HAVE_EVP_PKEY_EC
      case EVP_PKEY_EC:
        ret = PEM_write_bio_ECPrivateKey(bio_out, EVP_PKEY_get0_EC_KEY(pkey),
                                         cipher,
                                         (unsigned char *)passphrase.data(),
                                         passphrase.size(),
                                         nullptr,
                                         nullptr);
        break;
#endif
      default:
        ret = PEM_write_bio_PrivateKey(bio_out, pkey, cipher,
                                       (unsigned char *)passphrase.data(),
                                       passphrase.size(), nullptr, nullptr);
        break;
    }
  }
  php_openssl_dispose_config(&req);
  return ret;
}

bool HHVM_FUNCTION(openssl_pkey_export_to_file, const Variant& key,
                                                const String& outfilename,
                                   const String& passphrase /* = null_string */,
                             const Variant& configargs /* = uninit_variant */) {
  BIO *bio_out = BIO_new_file(outfilename.data(), "w");
  if (bio_out == nullptr) {
    raise_warning("error opening the file, %s", outfilename.data());
    return false;
  }
  bool ret = openssl_pkey_export_impl(key, bio_out, passphrase, configargs);
  BIO_free(bio_out);
  return ret;
}

bool HHVM_FUNCTION(openssl_pkey_export, const Variant& key, Variant& out,
                                   const String& passphrase /* = null_string */,
                            const Variant& configargs /* = uninit_variant */) {
  BIO *bio_out = BIO_new(BIO_s_mem());
  bool ret = openssl_pkey_export_impl(key, bio_out, passphrase, configargs);
  if (ret) {
    char *bio_mem_ptr;
    long bio_mem_len = BIO_get_mem_data(bio_out, &bio_mem_ptr);
    out = String(bio_mem_ptr, bio_mem_len, CopyString);
  }
  BIO_free(bio_out);
  return ret;
}

const StaticString
  s_bits("bits"),
  s_key("key"),
  s_type("type"),
  s_name("name"),
  s_hash("hash"),
  s_version("version"),
  s_serialNumber("serialNumber"),
  s_signatureAlgorithm("signatureAlgorithm"),
  s_validFrom("validFrom"),
  s_validTo("validTo"),
  s_validFrom_time_t("validFrom_time_t"),
  s_validTo_time_t("validTo_time_t"),
  s_alias("alias"),
  s_purposes("purposes"),
  s_extensions("extensions"),
  s_rsa("rsa"),
  s_dsa("dsa"),
  s_dh("dh"),
  s_ec("ec"),
  s_n("n"),
  s_e("e"),
  s_d("d"),
  s_p("p"),
  s_q("q"),
  s_g("g"),
  s_x("x"),
  s_y("y"),
  s_dmp1("dmp1"),
  s_dmq1("dmq1"),
  s_iqmp("iqmp"),
  s_priv_key("priv_key"),
  s_pub_key("pub_key"),
  s_curve_oid("curve_oid");

static void add_bignum_as_string(Array &arr,
                                 StaticString key,
                                 const BIGNUM *bn) {
  if (!bn) {
    return;
  }
  int num_bytes = BN_num_bytes(bn);
  String str{size_t(num_bytes), ReserveString};
  BN_bn2bin(bn, (unsigned char*)str.mutableData());
  str.setSize(num_bytes);
  arr.set(key, std::move(str));
}

Array HHVM_FUNCTION(openssl_pkey_get_details, const Resource& key) {
  EVP_PKEY *pkey = cast<Key>(key)->m_key;
  BIO *out = BIO_new(BIO_s_mem());
  PEM_write_bio_PUBKEY(out, pkey);
  char *pbio;
  unsigned int pbio_len = BIO_get_mem_data(out, &pbio);

  auto ret = make_darray(
    s_bits, EVP_PKEY_bits(pkey),
    s_key, String(pbio, pbio_len, CopyString)
  );
  long ktype = -1;

  auto details = Array::CreateDArray();
  switch (EVP_PKEY_id(pkey)) {
  case EVP_PKEY_RSA:
  case EVP_PKEY_RSA2:
    {
      ktype = OPENSSL_KEYTYPE_RSA;
      RSA *rsa = EVP_PKEY_get0_RSA(pkey);
      assertx(rsa);
      const BIGNUM *n, *e, *d, *p, *q, *dmp1, *dmq1, *iqmp;
      RSA_get0_key(rsa, &n, &e, &d);
      RSA_get0_factors(rsa, &p, &q);
      RSA_get0_crt_params(rsa, &dmp1, &dmq1, &iqmp);
      add_bignum_as_string(details, s_n, n);
      add_bignum_as_string(details, s_e, e);
      add_bignum_as_string(details, s_d, d);
      add_bignum_as_string(details, s_p, p);
      add_bignum_as_string(details, s_q, q);
      add_bignum_as_string(details, s_dmp1, dmp1);
      add_bignum_as_string(details, s_dmq1, dmq1);
      add_bignum_as_string(details, s_iqmp, iqmp);
      ret.set(s_rsa, details);
      break;
  }
  case EVP_PKEY_DSA:
  case EVP_PKEY_DSA2:
  case EVP_PKEY_DSA3:
  case EVP_PKEY_DSA4:
    {
      ktype = OPENSSL_KEYTYPE_DSA;
      DSA *dsa = EVP_PKEY_get0_DSA(pkey);
      assertx(dsa);
      const BIGNUM *p, *q, *g, *pub_key, *priv_key;
      DSA_get0_pqg(dsa, &p, &q, &g);
      DSA_get0_key(dsa, &pub_key, &priv_key);
      add_bignum_as_string(details, s_p, p);
      add_bignum_as_string(details, s_q, q);
      add_bignum_as_string(details, s_g, g);
      add_bignum_as_string(details, s_priv_key, priv_key);
      add_bignum_as_string(details, s_pub_key, pub_key);
      ret.set(s_dsa, details);
      break;
    }
  case EVP_PKEY_DH:
    {
      ktype = OPENSSL_KEYTYPE_DH;
      DH *dh = EVP_PKEY_get0_DH(pkey);
      assertx(dh);
      const BIGNUM *p, *q, *g, *pub_key, *priv_key;
      DH_get0_pqg(dh, &p, &q, &g);
      DH_get0_key(dh, &pub_key, &priv_key);
      add_bignum_as_string(details, s_p, p);
      add_bignum_as_string(details, s_g, g);
      add_bignum_as_string(details, s_priv_key, priv_key);
      add_bignum_as_string(details, s_pub_key, pub_key);
      ret.set(s_dh, details);
      break;
    }
#ifdef HAVE_EVP_PKEY_EC
  case EVP_PKEY_EC:
    {
      ktype = OPENSSL_KEYTYPE_EC;
      auto const ec = EVP_PKEY_get0_EC_KEY(pkey);
      assertx(ec);

      auto const ec_group = EC_KEY_get0_group(ec);
      auto const nid = EC_GROUP_get_curve_name(ec_group);
      if (nid == NID_undef) {
        break;
      }

      auto const crv_sn = OBJ_nid2sn(nid);
      if (crv_sn != nullptr) {
        details.set(s_curve_name, String(crv_sn, CopyString));
      }

      auto const obj = OBJ_nid2obj(nid);
      if (obj != nullptr) {
        SCOPE_EXIT {
          ASN1_OBJECT_free(obj);
        };
        char oir_buf[256];
        OBJ_obj2txt(oir_buf, sizeof(oir_buf) - 1, obj, 1);
        details.set(s_curve_oid, String(oir_buf, CopyString));
      }

      auto x = BN_new();
      auto y = BN_new();
      SCOPE_EXIT {
        BN_free(x);
        BN_free(y);
      };
      auto const pub = EC_KEY_get0_public_key(ec);
      if (EC_POINT_get_affine_coordinates_GFp(ec_group, pub, x, y, nullptr)) {
        add_bignum_as_string(details, s_x, x);
        add_bignum_as_string(details, s_y, y);
      }

      auto d = BN_dup(EC_KEY_get0_private_key(ec));
      SCOPE_EXIT {
        BN_free(d);
      };
      if (d != nullptr) {
        add_bignum_as_string(details, s_d, d);
      }

      ret.set(s_ec, details);
    }
    break;
#endif
  }
  ret.set(s_type, ktype);
  BIO_free(out);
  return ret;
}

Variant HHVM_FUNCTION(openssl_pkey_get_private, const Variant& key,
                                 const String& passphrase /* = null_string */) {
  return toVariant(Key::Get(key, false, passphrase.data()));
}

Variant HHVM_FUNCTION(openssl_pkey_get_public, const Variant& certificate) {
  return toVariant(Key::Get(certificate, true));
}

Variant HHVM_FUNCTION(openssl_pkey_new,
                       const Variant& configargs /* = uninit_variant */) {
  struct php_x509_request req;
  memset(&req, 0, sizeof(req));
  SCOPE_EXIT {
    php_openssl_dispose_config(&req);
  };

  std::vector<String> strings;
  if (php_openssl_parse_config(&req, configargs.toArray(), strings) &&
      req.generatePrivateKey()) {
    return Resource(req::make<Key>(req.priv_key));
  } else {
    return false;
  }
}

bool HHVM_FUNCTION(openssl_private_decrypt, const String& data,
                                            Variant& decrypted,
                                            const Variant& key,
                                  int padding /* = k_OPENSSL_PKCS1_PADDING */) {
  auto okey = Key::Get(key, false);
  if (!okey) {
    raise_warning("key parameter is not a valid private key");
    return false;
  }
  EVP_PKEY *pkey = okey->m_key;
  int cryptedlen = EVP_PKEY_size(pkey);
  String s = String(cryptedlen, ReserveString);
  unsigned char *cryptedbuf = (unsigned char *)s.mutableData();

  int successful = 0;
  switch (EVP_PKEY_id(pkey)) {
  case EVP_PKEY_RSA:
  case EVP_PKEY_RSA2:
    cryptedlen = RSA_private_decrypt(data.size(),
                                     (unsigned char *)data.data(),
                                     cryptedbuf,
                                     EVP_PKEY_get0_RSA(pkey),
                                     padding);
    if (cryptedlen != -1) {
      successful = 1;
    }
    break;

  default:
    raise_warning("key type not supported");
  }

  if (successful) {
    decrypted = s.setSize(cryptedlen);
    return true;
  }

  return false;
}

bool HHVM_FUNCTION(openssl_private_encrypt, const String& data,
                                            Variant& crypted,
                                            const Variant& key,
                                  int padding /* = k_OPENSSL_PKCS1_PADDING */) {
  auto okey = Key::Get(key, false);
  if (!okey) {
    raise_warning("key param is not a valid private key");
    return false;
  }
  EVP_PKEY *pkey = okey->m_key;
  int cryptedlen = EVP_PKEY_size(pkey);
  String s = String(cryptedlen, ReserveString);
  unsigned char *cryptedbuf = (unsigned char *)s.mutableData();

  int successful = 0;
  switch (EVP_PKEY_id(pkey)) {
  case EVP_PKEY_RSA:
  case EVP_PKEY_RSA2:
    successful = (RSA_private_encrypt(data.size(),
                                      (unsigned char *)data.data(),
                                      cryptedbuf,
                                      EVP_PKEY_get0_RSA(pkey),
                                      padding) == cryptedlen);
    break;
  default:
    raise_warning("key type not supported");
  }

  if (successful) {
    crypted = s.setSize(cryptedlen);
    return true;
  }

  return false;
}

bool HHVM_FUNCTION(openssl_public_decrypt, const String& data,
                                           Variant& decrypted,
                                           const Variant& key,
                                  int padding /* = k_OPENSSL_PKCS1_PADDING */) {
  auto okey = Key::Get(key, true);
  if (!okey) {
    raise_warning("key parameter is not a valid public key");
    return false;
  }
  EVP_PKEY *pkey = okey->m_key;
  int cryptedlen = EVP_PKEY_size(pkey);
  String s = String(cryptedlen, ReserveString);
  unsigned char *cryptedbuf = (unsigned char *)s.mutableData();

  int successful = 0;
  switch (EVP_PKEY_id(pkey)) {
  case EVP_PKEY_RSA:
  case EVP_PKEY_RSA2:
    cryptedlen = RSA_public_decrypt(data.size(),
                                    (unsigned char *)data.data(),
                                    cryptedbuf,
                                    EVP_PKEY_get0_RSA(pkey),
                                    padding);
    if (cryptedlen != -1) {
      successful = 1;
    }
    break;

  default:
    raise_warning("key type not supported");
  }

  if (successful) {
    decrypted = s.setSize(cryptedlen);
    return true;
  }

  return false;
}

bool HHVM_FUNCTION(openssl_public_encrypt, const String& data,
                                           Variant& crypted,
                                           const Variant& key,
                                  int padding /* = k_OPENSSL_PKCS1_PADDING */) {
  auto okey = Key::Get(key, true);
  if (!okey) {
    raise_warning("key parameter is not a valid public key");
    return false;
  }
  EVP_PKEY *pkey = okey->m_key;
  int cryptedlen = EVP_PKEY_size(pkey);
  String s = String(cryptedlen, ReserveString);
  unsigned char *cryptedbuf = (unsigned char *)s.mutableData();

  int successful = 0;
  switch (EVP_PKEY_id(pkey)) {
  case EVP_PKEY_RSA:
  case EVP_PKEY_RSA2:
    successful = (RSA_public_encrypt(data.size(),
                                     (unsigned char *)data.data(),
                                     cryptedbuf,
                                     EVP_PKEY_get0_RSA(pkey),
                                     padding) == cryptedlen);
    break;
  default:
    raise_warning("key type not supported");
  }

  if (successful) {
    crypted = s.setSize(cryptedlen);
    return true;
  }

  return false;
}

Variant HHVM_FUNCTION(openssl_seal, const String& data, Variant& sealed_data,
                                    Variant& env_keys,
                                    const Array& pub_key_ids,
                                    const String& method,
                                    Variant& iv) {
  int nkeys = pub_key_ids.size();
  if (nkeys == 0) {
    raise_warning("Fourth argument to openssl_seal() must be "
                    "a non-empty array");
    return false;
  }

  const EVP_CIPHER *cipher_type;
  if (method.empty()) {
    cipher_type = EVP_rc4();
  } else {
    cipher_type = EVP_get_cipherbyname(method.c_str());
    if (!cipher_type) {
      raise_warning("Unknown cipher algorithm");
      return false;
    }
  }

  int iv_len = EVP_CIPHER_iv_length(cipher_type);
  unsigned char *iv_buf = nullptr;
  String iv_s;
  if (iv_len > 0) {
    iv_s = String(iv_len, ReserveString);
    iv_buf = (unsigned char*)iv_s.mutableData();

    if (!RAND_bytes(iv_buf, iv_len)) {
      raise_warning("Could not generate an IV.");
      return false;
    }
  }

  EVP_PKEY **pkeys = (EVP_PKEY**)malloc(nkeys * sizeof(*pkeys));
  int *eksl = (int*)malloc(nkeys * sizeof(*eksl));
  unsigned char **eks = (unsigned char **)malloc(nkeys * sizeof(*eks));
  memset(eks, 0, sizeof(*eks) * nkeys);

  // holder is needed to make sure none of the Keys get deleted prematurely.
  // The pkeys array points to elements inside of Keys returned from Key::Get()
  // which may be newly allocated and have no other owners.
  std::vector<req::ptr<Key>> holder;

  /* get the public keys we are using to seal this data */
  bool ret = true;
  int i = 0;
  String s;
  unsigned char* buf = nullptr;
  EVP_CIPHER_CTX* ctx = nullptr;
  for (ArrayIter iter(pub_key_ids); iter; ++iter, ++i) {
    auto okey = Key::Get(iter.second(), true);
    if (!okey) {
      raise_warning("not a public key (%dth member of pubkeys)", i + 1);
      ret = false;
      goto clean_exit;
    }
    holder.push_back(okey);
    pkeys[i] = okey->m_key;
    eks[i] = (unsigned char *)malloc(EVP_PKEY_size(pkeys[i]) + 1);
  }

  ctx = EVP_CIPHER_CTX_new();
  if (ctx == nullptr) {
    raise_warning("Failed to allocate an EVP_CIPHER_CTX object");
    ret = false;
    goto clean_exit;
  }
  if (!EVP_EncryptInit_ex(ctx, cipher_type, nullptr, nullptr, nullptr)) {
    ret = false;
    goto clean_exit;
  }

  int len1, len2;

  s = String(data.size() + EVP_CIPHER_CTX_block_size(ctx), ReserveString);
  buf = (unsigned char *)s.mutableData();
  if (EVP_SealInit(ctx, cipher_type, eks, eksl, iv_buf, pkeys, nkeys) <= 0 ||
      !EVP_SealUpdate(ctx, buf, &len1, (unsigned char*)data.data(), data.size()) ||
      !EVP_SealFinal(ctx, buf + len1, &len2)) {
    ret = false;
    goto clean_exit;
  }

  if (len1 + len2 > 0) {
    sealed_data = s.setSize(len1 + len2);

    auto ekeys = Array::CreateVArray();
    for (i = 0; i < nkeys; i++) {
      eks[i][eksl[i]] = '\0';
      ekeys.append(String((char*)eks[i], eksl[i], AttachString));
      eks[i] = nullptr;
    }
    env_keys = ekeys;
  }

 clean_exit:
  for (i = 0; i < nkeys; i++) {
    if (eks[i]) free(eks[i]);
  }
  free(eks);
  free(eksl);
  free(pkeys);

  if (iv_buf != nullptr) {
    if (ret) {
      iv = iv_s.setSize(iv_len);
    }
  }
  if (ctx != nullptr) {
    EVP_CIPHER_CTX_free(ctx);
  }

  if (ret) return len1 + len2;
  return false;
}

static const EVP_MD *php_openssl_get_evp_md_from_algo(long algo) {
  switch (algo) {
  case OPENSSL_ALGO_SHA1: return EVP_sha1();
  case OPENSSL_ALGO_MD5:  return EVP_md5();
  case OPENSSL_ALGO_MD4:  return EVP_md4();
#ifdef HAVE_OPENSSL_MD2_H
  case OPENSSL_ALGO_MD2:  return EVP_md2();
#endif
#if OPENSSL_VERSION_NUMBER < 0x10100000L
  case OPENSSL_ALGO_DSS1: return EVP_dss1();
#endif
#if OPENSSL_VERSION_NUMBER >= 0x0090708fL
  case OPENSSL_ALGO_SHA224: return EVP_sha224();
  case OPENSSL_ALGO_SHA256: return EVP_sha256();
  case OPENSSL_ALGO_SHA384: return EVP_sha384();
  case OPENSSL_ALGO_SHA512: return EVP_sha512();
  case OPENSSL_ALGO_RMD160: return EVP_ripemd160();
#endif
  }
  return nullptr;
}

bool HHVM_FUNCTION(openssl_sign, const String& data, Variant& signature,
                                 const Variant& priv_key_id,
                     const Variant& signature_alg /* = k_OPENSSL_ALGO_SHA1 */) {
  auto okey = Key::Get(priv_key_id, false);
  if (!okey) {
    raise_warning("supplied key param cannot be coerced into a private key");
    return false;
  }

  const EVP_MD *mdtype = nullptr;
  if (signature_alg.isInteger()) {
    mdtype = php_openssl_get_evp_md_from_algo(signature_alg.toInt64Val());
  } else if (signature_alg.isString()) {
    mdtype = EVP_get_digestbyname(signature_alg.toString().data());
  }

  if (!mdtype) {
    raise_warning("Unknown signature algorithm.");
    return false;
  }

  EVP_PKEY *pkey = okey->m_key;
  int siglen = EVP_PKEY_size(pkey);
  String s = String(siglen, ReserveString);
  unsigned char *sigbuf = (unsigned char *)s.mutableData();

  EVP_MD_CTX *md_ctx = EVP_MD_CTX_new();
  SCOPE_EXIT {
    EVP_MD_CTX_free(md_ctx);
  };
  EVP_SignInit(md_ctx, mdtype);
  EVP_SignUpdate(md_ctx, (unsigned char *)data.data(), data.size());
  if (EVP_SignFinal(md_ctx, sigbuf, (unsigned int *)&siglen, pkey)) {
    signature = s.setSize(siglen);
    return true;
  }
  return false;
}

Variant HHVM_FUNCTION(openssl_verify, const String& data,
                                      const String& signature,
                                      const Variant& pub_key_id,
                     const Variant& signature_alg /* = k_OPENSSL_ALGO_SHA1 */) {
  int err;
  const EVP_MD *mdtype = nullptr;

  if (signature_alg.isInteger()) {
    mdtype = php_openssl_get_evp_md_from_algo(signature_alg.toInt64Val());
  } else if (signature_alg.isString()) {
    mdtype = EVP_get_digestbyname(signature_alg.toString().data());
  }

  if (!mdtype) {
    raise_warning("Unknown signature algorithm.");
    return false;
  }

  auto okey = Key::Get(pub_key_id, true);
  if (!okey) {
    raise_warning("supplied key param cannot be coerced into a public key");
    return false;
  }

  EVP_MD_CTX *md_ctx = EVP_MD_CTX_new();
  SCOPE_EXIT {
    EVP_MD_CTX_free(md_ctx);
  };
  EVP_VerifyInit(md_ctx, mdtype);
  EVP_VerifyUpdate(md_ctx, (unsigned char*)data.data(), data.size());
  err = EVP_VerifyFinal(md_ctx, (unsigned char *)signature.data(),
                         signature.size(), okey->m_key);
  return err;
}

bool HHVM_FUNCTION(openssl_x509_check_private_key, const Variant& cert,
                                                   const Variant& key) {
  auto ocert = Certificate::Get(cert);
  if (!ocert) {
    return false;
  }
  auto okey = Key::Get(key, false);
  if (!okey) {
    return false;
  }
  return X509_check_private_key(ocert->m_cert, okey->m_key);
}

static int check_cert(X509_STORE *ctx, X509 *x, STACK_OF(X509) *untrustedchain,
                      int purpose) {
  X509_STORE_CTX *csc = X509_STORE_CTX_new();
  if (csc == nullptr) {
    raise_warning("memory allocation failure");
    return 0;
  }
  X509_STORE_CTX_init(csc, ctx, x, untrustedchain);

  if (purpose >= 0) {
    X509_STORE_CTX_set_purpose(csc, purpose);
  }

  int ret = X509_verify_cert(csc);
  X509_STORE_CTX_free(csc);
  return ret;
}

Variant HHVM_FUNCTION(openssl_x509_checkpurpose, const Variant& x509cert,
                      int purpose,
                      const Array& cainfo /* = null_array */,
                      const String& untrustedfile /* = null_string */) {
  int ret = -1;
  STACK_OF(X509) *untrustedchain = nullptr;
  X509_STORE *pcainfo = nullptr;
  req::ptr<Certificate> ocert;

  if (!untrustedfile.empty()) {
    untrustedchain = load_all_certs_from_file(untrustedfile.data());
    if (untrustedchain == nullptr) {
      goto clean_exit;
    }
  }

  pcainfo = setup_verify(cainfo);
  if (pcainfo == nullptr) {
    goto clean_exit;
  }

  ocert = Certificate::Get(x509cert);
  if (!ocert) {
    raise_warning("cannot get cert from parameter 1");
    return false;
  }
  X509 *cert;
  cert = ocert->m_cert;
  assertx(cert);

  ret = check_cert(pcainfo, cert, untrustedchain, purpose);

 clean_exit:
  if (pcainfo) {
    X509_STORE_free(pcainfo);
  }
  if (untrustedchain) {
    sk_X509_pop_free(untrustedchain, X509_free);
  }
  return ret == 1 ? true : ret == 0 ? false : -1;
}

static bool openssl_x509_export_impl(const Variant& x509, BIO *bio_out,
                                     bool notext /* = true */) {
  auto ocert = Certificate::Get(x509);
  if (!ocert) {
    raise_warning("cannot get cert from parameter 1");
    return false;
  }
  X509 *cert = ocert->m_cert;
  assertx(cert);

  assertx(bio_out);
  if (!notext) {
    X509_print(bio_out, cert);
  }
  return PEM_write_bio_X509(bio_out, cert);
}

bool HHVM_FUNCTION(openssl_x509_export_to_file, const Variant& x509,
                                                const String& outfilename,
                                                bool notext /* = true */) {
  BIO *bio_out = BIO_new_file((char*)outfilename.data(), "w");
  if (bio_out == nullptr) {
    raise_warning("error opening file %s", outfilename.data());
    return false;
  }
  bool ret = openssl_x509_export_impl(x509, bio_out, notext);
  BIO_free(bio_out);
  return ret;
}

bool HHVM_FUNCTION(openssl_x509_export, const Variant& x509, Variant& output,
                                        bool notext /* = true */) {
  BIO *bio_out = BIO_new(BIO_s_mem());
  bool ret = openssl_x509_export_impl(x509, bio_out, notext);
  if (ret) {
    BUF_MEM *bio_buf;
    BIO_get_mem_ptr(bio_out, &bio_buf);
    output = String(bio_buf->data, bio_buf->length, CopyString);
  }
  BIO_free(bio_out);
  return ret;
}

/**
 * This is how the time string is formatted:
 *
 * snprintf(p, sizeof(p), "%02d%02d%02d%02d%02d%02dZ",ts->tm_year%100,
 *          ts->tm_mon+1,ts->tm_mday,ts->tm_hour,ts->tm_min,ts->tm_sec);
 */
static time_t asn1_time_to_time_t(ASN1_UTCTIME *timestr) {

  auto const timestr_type = ASN1_STRING_type(timestr);

  if (timestr_type != V_ASN1_UTCTIME && timestr_type != V_ASN1_GENERALIZEDTIME) {
    raise_warning("illegal ASN1 data type for timestamp");
    return (time_t)-1;
  }

  auto const timestr_len = (size_t)ASN1_STRING_length(timestr);

  // Binary safety
  if (timestr_len != strlen((char*)ASN1_STRING_data(timestr))) {
    raise_warning("illegal length in timestamp");
    return (time_t)-1;
  }

  if (timestr_len < 13 && timestr_len != 11) {
    raise_warning("unable to parse time string %s correctly",
                    timestr->data);
    return (time_t)-1;
  }

  if (timestr_type == V_ASN1_GENERALIZEDTIME && timestr_len < 15) {
    raise_warning("unable to parse time string %s correctly", timestr->data);
    return (time_t)-1;
  }

  char *strbuf = strdup((char*)timestr->data);

  struct tm thetime;
  memset(&thetime, 0, sizeof(thetime));

  /* we work backwards so that we can use atoi more easily */
  char *thestr = strbuf + ASN1_STRING_length(timestr) - 3;
  if (ASN1_STRING_length(timestr) == 11) {
    thetime.tm_sec = 0;
  } else {
    thetime.tm_sec  = atoi(thestr);   *thestr = '\0';  thestr -= 2;
  }
  thetime.tm_min  = atoi(thestr);   *thestr = '\0';  thestr -= 2;
  thetime.tm_hour = atoi(thestr);   *thestr = '\0';  thestr -= 2;
  thetime.tm_mday = atoi(thestr);   *thestr = '\0';  thestr -= 2;
  thetime.tm_mon  = atoi(thestr)-1; *thestr = '\0';

  if (ASN1_STRING_type(timestr) == V_ASN1_UTCTIME) {
    thestr -= 2;
    thetime.tm_year = atoi(thestr);
    if (thetime.tm_year < 68) {
      thetime.tm_year += 100;
    }
  } else if (ASN1_STRING_type(timestr) == V_ASN1_GENERALIZEDTIME) {
    thestr -= 4;
    thetime.tm_year = atoi(thestr) - 1900;
  }

  thetime.tm_isdst = -1;
  time_t ret = mktime(&thetime);

  long gmadjust = 0;
#if HAVE_TM_GMTOFF
  gmadjust = thetime.tm_gmtoff;
#elif defined(_MSC_VER)
  TIME_ZONE_INFORMATION inf;
  GetTimeZoneInformation(&inf);
  gmadjust = thetime.tm_isdst ? inf.DaylightBias : inf.StandardBias;
#else
  /**
   * If correcting for daylight savings time, we set the adjustment to
   * the value of timezone - 3600 seconds. Otherwise, we need to overcorrect
   * and set the adjustment to the main timezone + 3600 seconds.
   */
  gmadjust = -(thetime.tm_isdst ?
               (long)timezone - 3600 : (long)timezone);
#endif
  /* no adjustment for UTC */
  if (timezone) ret += gmadjust;
  free(strbuf);
  return ret;
}

/* Special handling of subjectAltName, see CVE-2013-4073
 * Christian Heimes
 */

static int openssl_x509v3_subjectAltName(BIO *bio, X509_EXTENSION *extension)
{
  GENERAL_NAMES *names;
  const X509V3_EXT_METHOD *method = nullptr;
  long i, length, num;
  const unsigned char *p;

  method = X509V3_EXT_get(extension);
  if (method == nullptr) {
    return -1;
  }

  const auto data = X509_EXTENSION_get_data(extension);
  p = data->data;
  length = data->length;
  if (method->it) {
    names = (GENERAL_NAMES*)(ASN1_item_d2i(nullptr, &p, length,
                                          ASN1_ITEM_ptr(method->it)));
  } else {
    names = (GENERAL_NAMES*)(method->d2i(nullptr, &p, length));
  }
  if (names == nullptr) {
    return -1;
  }

  num = sk_GENERAL_NAME_num(names);
  for (i = 0; i < num; i++) {
    GENERAL_NAME *name;
    ASN1_STRING *as;
    name = sk_GENERAL_NAME_value(names, i);
    switch (name->type) {
      case GEN_EMAIL:
             BIO_puts(bio, "email:");
             as = name->d.rfc822Name;
             BIO_write(bio, ASN1_STRING_data(as),
                       ASN1_STRING_length(as));
             break;
      case GEN_DNS:
             BIO_puts(bio, "DNS:");
             as = name->d.dNSName;
             BIO_write(bio, ASN1_STRING_data(as),
                       ASN1_STRING_length(as));
             break;
      case GEN_URI:
             BIO_puts(bio, "URI:");
             as = name->d.uniformResourceIdentifier;
             BIO_write(bio, ASN1_STRING_data(as),
                       ASN1_STRING_length(as));
             break;
      default:
             /* use builtin print for GEN_OTHERNAME, GEN_X400,
              * GEN_EDIPARTY, GEN_DIRNAME, GEN_IPADD and GEN_RID
              */
             GENERAL_NAME_print(bio, name);
    }
    /* trailing ', ' except for last element */
    if (i < (num - 1)) {
           BIO_puts(bio, ", ");
    }
  }
  sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);

  return 0;
}
Variant HHVM_FUNCTION(openssl_x509_parse, const Variant& x509cert,
                                          bool shortnames /* = true */) {
  auto ocert = Certificate::Get(x509cert);
  if (!ocert) {
    return false;
  }
  X509 *cert = ocert->m_cert;
  assertx(cert);

  auto ret = Array::CreateDArray();
  const auto sn = X509_get_subject_name(cert);
  if (sn) {
    ret.set(s_name, String(X509_NAME_oneline(sn, nullptr, 0), CopyString));
  }
  add_assoc_name_entry(ret, "subject", sn, shortnames);
  /* hash as used in CA directories to lookup cert by subject name */
  {
    char buf[32];
    snprintf(buf, sizeof(buf), "%08lx", X509_subject_name_hash(cert));
    ret.set(s_hash, String(buf, CopyString));
  }

  add_assoc_name_entry(ret, "issuer", X509_get_issuer_name(cert), shortnames);
  ret.set(s_version, X509_get_version(cert));

  ret.set(s_serialNumber, String
          (i2s_ASN1_INTEGER(nullptr, X509_get_serialNumber(cert)), AttachString));
  // Adding Signature Algorithm
  BIO *bio_out = BIO_new(BIO_s_mem());
  SCOPE_EXIT { BIO_free(bio_out); };
  if (i2a_ASN1_OBJECT(bio_out, X509_get0_tbs_sigalg(cert)->algorithm) > 0) {
    BUF_MEM *bio_buf;
    BIO_get_mem_ptr(bio_out, &bio_buf);
    ret.set(s_signatureAlgorithm,
            String((char*)bio_buf->data, bio_buf->length, CopyString));
  }

  ASN1_STRING *str = X509_get_notBefore(cert);
  ret.set(s_validFrom, String((char*)str->data, str->length, CopyString));
  str = X509_get_notAfter(cert);
  ret.set(s_validTo, String((char*)str->data, str->length, CopyString));
  ret.set(s_validFrom_time_t, asn1_time_to_time_t(X509_get_notBefore(cert)));
  ret.set(s_validTo_time_t, asn1_time_to_time_t(X509_get_notAfter(cert)));

  char *tmpstr = (char *)X509_alias_get0(cert, nullptr);
  if (tmpstr) {
    ret.set(s_alias, String(tmpstr, CopyString));
  }

  /* NOTE: the purposes are added as integer keys - the keys match up to
     the X509_PURPOSE_SSL_XXX defines in x509v3.h */
  {
    Array subitem;
    for (int i = 0; i < X509_PURPOSE_get_count(); i++) {
      X509_PURPOSE *purp = X509_PURPOSE_get0(i);
      int id = X509_PURPOSE_get_id(purp);
      char * pname = shortnames ? X509_PURPOSE_get0_sname(purp) :
        X509_PURPOSE_get0_name(purp);
      auto subsub = make_varray(
        (bool)X509_check_purpose(cert, id, 0),
        (bool)X509_check_purpose(cert, id, 1),
        String(pname, CopyString)
      );
      subitem.set(id, std::move(subsub));
    }
    ret.set(s_purposes, subitem);
  }
  {
    auto subitem = Array::CreateDArray();
    for (int i = 0; i < X509_get_ext_count(cert); i++) {
      int nid;
      X509_EXTENSION *extension = X509_get_ext(cert, i);
      char *extname;
      char buf[256];
      nid = OBJ_obj2nid(X509_EXTENSION_get_object(extension));
      if (nid != NID_undef) {
        extname = (char*)OBJ_nid2sn(OBJ_obj2nid
                                    (X509_EXTENSION_get_object(extension)));
      } else {
        OBJ_obj2txt(buf, sizeof(buf)-1, X509_EXTENSION_get_object(extension),
                    1);
        extname = buf;
      }
      BIO *bio_out = BIO_new(BIO_s_mem());
      if (nid == NID_subject_alt_name) {
        if (openssl_x509v3_subjectAltName(bio_out, extension) == 0) {
          BUF_MEM *bio_buf;
          BIO_get_mem_ptr(bio_out, &bio_buf);
          subitem.set(String(extname, CopyString),
                      String((char*)bio_buf->data,
                             bio_buf->length,
                             CopyString));
        } else {
          BIO_free(bio_out);
          return false;
        }
      } else if (X509V3_EXT_print(bio_out, extension, 0, 0)) {
        BUF_MEM *bio_buf;
        BIO_get_mem_ptr(bio_out, &bio_buf);
        subitem.set(String(extname, CopyString),
                    String((char*)bio_buf->data, bio_buf->length, CopyString));
      } else {
        str = X509_EXTENSION_get_data(extension);
        subitem.set(String(extname, CopyString),
                    String((char*)str->data, str->length, CopyString));
      }
      BIO_free(bio_out);
    }
    ret.set(s_extensions, subitem);
  }

  return ret;
}

Variant HHVM_FUNCTION(openssl_x509_read, const Variant& x509certdata) {
  auto ocert = Certificate::Get(x509certdata);
  if (!ocert) {
    raise_warning("supplied parameter cannot be coerced into "
                  "an X509 certificate!");
    return false;
  }
  return Variant(ocert);
}

Variant HHVM_FUNCTION(openssl_random_pseudo_bytes, int length,
                      bool& crypto_strong) {
  if (length <= 0) {
    return false;
  }

  unsigned char *buffer = nullptr;

  String s = String(length, ReserveString);
  buffer = (unsigned char *)s.mutableData();

  if (RAND_bytes(buffer, length) <= 0) {
    crypto_strong = false;
    return false;
  } else {
    crypto_strong = true;
    s.setSize(length);
    return s;
  }
}

Variant HHVM_FUNCTION(openssl_cipher_iv_length, const String& method) {
  if (method.empty()) {
    raise_warning("Unknown cipher algorithm");
    return false;
  }

  const EVP_CIPHER *cipher_type = EVP_get_cipherbyname(method.c_str());
  if (!cipher_type) {
    raise_warning("Unknown cipher algorithm");
    return false;
  }

  return EVP_CIPHER_iv_length(cipher_type);
}

/* Cipher mode info */
struct php_openssl_cipher_mode {
  /* Whether this mode uses authenticated encryption. True, for example, with
     the GCM and CCM modes */
  bool is_aead;
  /* Whether this mode is a 'single run aead', meaning that DecryptFinal doesn't
     get called. For example, CCM mode is a single run aead mode. */
  bool is_single_run_aead;
  /* The OpenSSL flag to get the computed tag, if this mode is aead. */
  int aead_get_tag_flag;
  /* The OpenSSL flag to set the computed tag, if this mode is aead. */
  int aead_set_tag_flag;
  /* The OpenSSL flag to set the IV length, if this mode is aead */
  int aead_ivlen_flag;
};

// initialize a php_openssl_cipher_mode corresponding to an EVP_CIPHER.
static php_openssl_cipher_mode php_openssl_load_cipher_mode(
    const EVP_CIPHER* cipher_type) {
  php_openssl_cipher_mode mode = {};
  switch (EVP_CIPHER_mode(cipher_type)) {
#ifdef EVP_CIPH_GCM_MODE
    case EVP_CIPH_GCM_MODE:
      mode.is_aead = true;
      mode.is_single_run_aead = false;
      mode.aead_get_tag_flag = EVP_CTRL_GCM_GET_TAG;
      mode.aead_set_tag_flag = EVP_CTRL_GCM_SET_TAG;
      mode.aead_ivlen_flag = EVP_CTRL_GCM_SET_IVLEN;
      break;
#endif
#ifdef EVP_CIPH_CCM_MODE
    case EVP_CIPH_CCM_MODE:
      mode.is_aead = true;
      mode.is_single_run_aead = true;
      mode.aead_get_tag_flag = EVP_CTRL_CCM_GET_TAG;
      mode.aead_set_tag_flag = EVP_CTRL_CCM_SET_TAG;
      mode.aead_ivlen_flag = EVP_CTRL_CCM_SET_IVLEN;
      break;
#endif
    default:
      break;
  }
  return mode;
}

static bool php_openssl_validate_iv(
    String piv,
    int iv_required_len,
    String& out,
    EVP_CIPHER_CTX* cipher_ctx,
    const php_openssl_cipher_mode* mode) {
  if (cipher_ctx == nullptr || mode == nullptr) {
    return false;
  }

  /* Best case scenario, user behaved */
  if (piv.size() == iv_required_len) {
    out = std::move(piv);
    return true;
  }

  if (mode->is_aead) {
    if (EVP_CIPHER_CTX_ctrl(
            cipher_ctx, mode->aead_ivlen_flag, piv.size(), nullptr) != 1) {
      raise_warning(
          "Setting of IV length for AEAD mode failed, the expected length is "
          "%d bytes",
          iv_required_len);
      return false;
    }
    out = std::move(piv);
    return true;
  }

  String s = String(iv_required_len, ReserveString);
  char* iv_new = s.mutableData();
  memset(iv_new, 0, iv_required_len);

  if (piv.size() <= 0) {
    /* BC behavior */
    s.setSize(iv_required_len);
    out = std::move(s);
    return true;
  }

  if (piv.size() < iv_required_len) {
    raise_warning("IV passed is only %ld bytes long, cipher "
                  "expects an IV of precisely %d bytes, padding with \\0",
                  piv.size(), iv_required_len);
    memcpy(iv_new, piv.data(), piv.size());
    s.setSize(iv_required_len);
    out = std::move(s);
    return true;
  }

  raise_warning("IV passed is %ld bytes long which is longer than the %d "
                "expected by selected cipher, truncating", piv.size(),
                iv_required_len);
  memcpy(iv_new, piv.data(), iv_required_len);
  s.setSize(iv_required_len);
  out = std::move(s);
  return true;
}

namespace {

Variant openssl_encrypt_impl(const String& data,
                                    const String& method,
                                    const String& password,
                                    int options,
                                    const String& iv,
                                    Variant* tag_out,
                                    const String& aad,
                                    int tag_length) {
  const EVP_CIPHER *cipher_type = EVP_get_cipherbyname(method.c_str());
  if (!cipher_type) {
    raise_warning("Unknown cipher algorithm");
    return false;
  }

  EVP_CIPHER_CTX* cipher_ctx = EVP_CIPHER_CTX_new();
  if (!cipher_ctx) {
    raise_warning("Failed to create cipher context");
    return false;
  }

  SCOPE_EXIT {
    EVP_CIPHER_CTX_free(cipher_ctx);
  };

  php_openssl_cipher_mode mode = php_openssl_load_cipher_mode(cipher_type);

  if (mode.is_aead && !tag_out) {
    raise_warning("Must call openssl_encrypt_with_tag when using an AEAD cipher");
    return false;
  }

  int keylen = EVP_CIPHER_key_length(cipher_type);
  String key = password;

  /*
   * older openssl libraries can assert if the passed in password length is
   * less than keylen
   */
  if (keylen > password.size()) {
    String s = String(keylen, ReserveString);
    char *keybuf = s.mutableData();
    memset(keybuf, 0, keylen);
    memcpy(keybuf, password.data(), password.size());
    key = s.setSize(keylen);
  }

  int max_iv_len = EVP_CIPHER_iv_length(cipher_type);
  if (iv.size() <= 0 && max_iv_len > 0 && !mode.is_aead) {
    raise_warning("Using an empty Initialization Vector (iv) is potentially "
                  "insecure and not recommended");
  }

  int result_len = 0;
  int outlen = data.size() + EVP_CIPHER_block_size(cipher_type);
  String rv = String(outlen, ReserveString);
  unsigned char *outbuf = (unsigned char*)rv.mutableData();

  EVP_EncryptInit_ex(cipher_ctx, cipher_type, nullptr, nullptr, nullptr);

  String new_iv;
  // we do this after EncryptInit because validate_iv changes cipher_ctx for
  // aead modes (must be initialized first).
  if (!php_openssl_validate_iv(
          std::move(iv), max_iv_len, new_iv, cipher_ctx, &mode)) {
    return false;
  }

  // set the tag length for CCM mode/other modes that require tag lengths to
  // be set.
  if (mode.is_single_run_aead &&
      !EVP_CIPHER_CTX_ctrl(
          cipher_ctx, mode.aead_set_tag_flag, tag_length, nullptr)) {
    raise_warning("Setting tag length failed");
    return false;
  }
  if (password.size() > keylen) {
    EVP_CIPHER_CTX_set_key_length(cipher_ctx, password.size());
  }
  EVP_EncryptInit_ex(
      cipher_ctx,
      nullptr,
      nullptr,
      (unsigned char*)key.data(),
      (unsigned char*)new_iv.data());
  if (options & k_OPENSSL_ZERO_PADDING) {
    EVP_CIPHER_CTX_set_padding(cipher_ctx, 0);
  }

  // for single run aeads like CCM, we need to provide the length of the
  // plaintext before providing AAD or ciphertext.
  if (mode.is_single_run_aead &&
      !EVP_EncryptUpdate(
          cipher_ctx, nullptr, &result_len, nullptr, data.size())) {
    raise_warning("Setting of data length failed");
    return false;
  }

  // set up aad:
  if (mode.is_aead &&
      !EVP_EncryptUpdate(
          cipher_ctx,
          nullptr,
          &result_len,
          (unsigned char*)aad.data(),
          aad.size())) {
    raise_warning("Setting of additional application data failed");
    return false;
  }

  // OpenSSL before 0.9.8i asserts with size < 0
  if (data.size() >= 0) {
    EVP_EncryptUpdate(cipher_ctx, outbuf, &result_len,
                      (unsigned char *)data.data(), data.size());
  }

  outlen = result_len;

  if (EVP_EncryptFinal_ex(
          cipher_ctx, (unsigned char*)outbuf + result_len, &result_len)) {
    outlen += result_len;
    rv.setSize(outlen);
    // Get tag if possible
    if (mode.is_aead) {
      String tagrv = String(tag_length, ReserveString);
      if (EVP_CIPHER_CTX_ctrl(
              cipher_ctx,
              mode.aead_get_tag_flag,
              tag_length,
              tagrv.mutableData()) == 1) {
        tagrv.setSize(tag_length);
        assertx(tag_out);
        *tag_out = tagrv;
      } else {
        raise_warning("Retrieving authentication tag failed");
        return false;
      }
    } else if (tag_out) {
      raise_warning(
          "The authenticated tag cannot be provided for cipher that does not"
          " support AEAD");
    }
    // Return encrypted data
    if (options & k_OPENSSL_RAW_DATA) {
      return rv;
    } else {
      return StringUtil::Base64Encode(rv);
    }
  }
  return false;
}

} // anonymous namespace

Variant HHVM_FUNCTION(openssl_encrypt,
                      const String& data,
                      const String& method,
                      const String& password,
                      int options /* = 0 */,
                      const String& iv /* = null_string */,
                      const String& aad /* = null_string */,
                      int tag_length /* = 16 */) {
  return openssl_encrypt_impl(data, method, password, options, iv,
                              nullptr, aad, tag_length);
}

Variant HHVM_FUNCTION(openssl_encrypt_with_tag,
                      const String& data,
                      const String& method,
                      const String& password,
                      int options,
                      const String& iv,
                      Variant& tag_out,
                      const String& aad /* = null_string */,
                      int tag_length /* = 16 */) {
  return openssl_encrypt_impl(data, method, password, options, iv,
                              &tag_out, aad, tag_length);
}

Variant HHVM_FUNCTION(openssl_decrypt, const String& data, const String& method,
                                       const String& password,
                                       int options /* = 0 */,
                                       const String& iv /* = null_string */,
                                       const String& tag /* = null_string */,
                                       const String& aad /* = null_string */) {
  const EVP_CIPHER *cipher_type = EVP_get_cipherbyname(method.c_str());
  if (!cipher_type) {
    raise_warning("Unknown cipher algorithm");
    return false;
  }

  EVP_CIPHER_CTX* cipher_ctx = EVP_CIPHER_CTX_new();
  if (!cipher_ctx) {
    raise_warning("Failed to create cipher context");
    return false;
  }

  SCOPE_EXIT {
    EVP_CIPHER_CTX_free(cipher_ctx);
  };

  php_openssl_cipher_mode mode = php_openssl_load_cipher_mode(cipher_type);

  String decoded_data = data;

  if (!(options & k_OPENSSL_RAW_DATA)) {
    decoded_data = StringUtil::Base64Decode(data);
  }

  int keylen = EVP_CIPHER_key_length(cipher_type);
  String key = password;

  /*
   * older openssl libraries can assert if the passed in password length is
   * less than keylen
   */
   if (keylen > password.size()) {
    String s = String(keylen, ReserveString);
    char *keybuf = s.mutableData();
    memset(keybuf, 0, keylen);
    memcpy(keybuf, password.data(), password.size());
    key = s.setSize(keylen);
  }

  int result_len = 0;
  int outlen = decoded_data.size() + EVP_CIPHER_block_size(cipher_type);
  String rv = String(outlen, ReserveString);
  unsigned char *outbuf = (unsigned char*)rv.mutableData();

  EVP_DecryptInit_ex(cipher_ctx, cipher_type, nullptr, nullptr, nullptr);

  String new_iv;
  // we do this after DecryptInit because validate_iv changes cipher_ctx for
  // aead modes (must be initialized first).
  if (!php_openssl_validate_iv(
          std::move(iv),
          EVP_CIPHER_iv_length(cipher_type),
          new_iv,
          cipher_ctx,
          &mode)) {
    return false;
  }

  // set the tag if required:
  if (tag.size() > 0) {
    if (!mode.is_aead) {
      raise_warning(
          "The tag is being ignored because the cipher method does not"
          " support AEAD");
    } else if (!EVP_CIPHER_CTX_ctrl(
                   cipher_ctx,
                   mode.aead_set_tag_flag,
                   tag.size(),
                   (unsigned char*)tag.data())) {
      raise_warning("Setting tag for AEAD cipher decryption failed");
      return false;
    }
  } else {
    if (mode.is_aead) {
      raise_warning("A tag should be provided when using AEAD mode");
      return false;
    }
  }
  if (password.size() > keylen) {
    EVP_CIPHER_CTX_set_key_length(cipher_ctx, password.size());
  }
  EVP_DecryptInit_ex(
      cipher_ctx,
      nullptr,
      nullptr,
      (unsigned char*)key.data(),
      (unsigned char*)new_iv.data());
  if (options & k_OPENSSL_ZERO_PADDING) {
    EVP_CIPHER_CTX_set_padding(cipher_ctx, 0);
  }

  // for single run aeads like CCM, we need to provide the length of the
  // ciphertext before providing AAD or ciphertext.
  if (mode.is_single_run_aead &&
      !EVP_DecryptUpdate(
          cipher_ctx, nullptr, &result_len, nullptr, decoded_data.size())) {
    raise_warning("Setting of data length failed");
    return false;
  }

  // set up aad:
  if (mode.is_aead &&
      !EVP_DecryptUpdate(
          cipher_ctx,
          nullptr,
          &result_len,
          (unsigned char*)aad.data(),
          aad.size())) {
    raise_warning("Setting of additional application data failed");
    return false;
  }

  if (!EVP_DecryptUpdate(
          cipher_ctx,
          outbuf,
          &result_len,
          (unsigned char*)decoded_data.data(),
          decoded_data.size())) {
    return false;
  }
  outlen = result_len;

  // if is_single_run_aead is enabled, DecryptFinal shouldn't be called.
  // if something went wrong in this case, we would've caught it at
  // DecryptUpdate.
  if (mode.is_single_run_aead ||
      EVP_DecryptFinal_ex(
          cipher_ctx, (unsigned char*)outbuf + result_len, &result_len)) {
    // don't want to do this if is_single_run_aead was enabled, since we didn't
    // make a call to EVP_DecryptFinal.
    if (!mode.is_single_run_aead) {
      outlen += result_len;
    }
    rv.setSize(outlen);
    return rv;
  } else {
    return false;
  }
}

Variant HHVM_FUNCTION(openssl_digest, const String& data, const String& method,
                                      bool raw_output /* = false */) {
  const EVP_MD *mdtype = EVP_get_digestbyname(method.c_str());

  if (!mdtype) {
    raise_warning("Unknown signature algorithm");
    return false;
  }
  int siglen = EVP_MD_size(mdtype);
  String rv = String(siglen, ReserveString);
  unsigned char *sigbuf = (unsigned char *)rv.mutableData();
  EVP_MD_CTX *md_ctx = EVP_MD_CTX_new();
  SCOPE_EXIT {
    EVP_MD_CTX_free(md_ctx);
  };

  EVP_DigestInit(md_ctx, mdtype);

  EVP_DigestUpdate(md_ctx, (unsigned char *)data.data(), data.size());
  if (EVP_DigestFinal(md_ctx, (unsigned char *)sigbuf, (unsigned int *)&siglen)) {
    if (raw_output) {
      rv.setSize(siglen);
      return rv;
    } else {
      char* digest_str = string_bin2hex((char*)sigbuf, siglen);
      return String(digest_str, AttachString);
    }
  } else {
    return false;
  }
}

static void openssl_add_method_or_alias(const OBJ_NAME *name, void *arg)
{
  Array *ret = (Array*)arg;
  ret->append(String((char *)name->name, CopyString));
}

static void openssl_add_method(const OBJ_NAME *name, void *arg)
{
  if (name->alias == 0) {
    Array *ret = (Array*)arg;
    ret->append(String((char *)name->name, CopyString));
  }
}

Array HHVM_FUNCTION(openssl_get_cipher_methods, bool aliases /* = false */) {
  Array ret = Array::CreateVArray();
  OBJ_NAME_do_all_sorted(OBJ_NAME_TYPE_CIPHER_METH,
    aliases ? openssl_add_method_or_alias: openssl_add_method,
    &ret);
  return ret;
}

Variant HHVM_FUNCTION(openssl_get_curve_names) {
#ifdef HAVE_EVP_PKEY_EC
  const size_t len = EC_get_builtin_curves(nullptr, 0);
  std::unique_ptr<EC_builtin_curve[]> curves(new EC_builtin_curve[len]);
  if (!EC_get_builtin_curves(curves.get(), len)) {
    return false;
  }

  VArrayInit ret(len);
  for (size_t i = 0; i < len; ++i) {
    auto const sname = OBJ_nid2sn(curves[i].nid);
    if (sname != nullptr) {
      ret.append(String(sname, CopyString));
    }
  }

  return ret.toArray();
#else
  return false;
#endif
}

Array HHVM_FUNCTION(openssl_get_md_methods, bool aliases /* = false */) {
  Array ret = Array::CreateVArray();
  OBJ_NAME_do_all_sorted(OBJ_NAME_TYPE_MD_METH,
    aliases ? openssl_add_method_or_alias: openssl_add_method,
    &ret);
  return ret;
}

/////////////////////////////////////////////////////////////////////////////

const StaticString s_OPENSSL_VERSION_TEXT("OPENSSL_VERSION_TEXT");

struct opensslExtension final : Extension {
  opensslExtension() : Extension("openssl") {}
  void moduleInit() override {
    HHVM_RC_INT(OPENSSL_RAW_DATA, k_OPENSSL_RAW_DATA);
    HHVM_RC_INT(OPENSSL_ZERO_PADDING, k_OPENSSL_ZERO_PADDING);
    HHVM_RC_INT(OPENSSL_NO_PADDING, k_OPENSSL_NO_PADDING);
    HHVM_RC_INT(OPENSSL_PKCS1_OAEP_PADDING, k_OPENSSL_PKCS1_OAEP_PADDING);
    HHVM_RC_INT(OPENSSL_SSLV23_PADDING, k_OPENSSL_SSLV23_PADDING);
    HHVM_RC_INT(OPENSSL_PKCS1_PADDING, k_OPENSSL_PKCS1_PADDING);

    HHVM_RC_INT_SAME(OPENSSL_ALGO_SHA1);
    HHVM_RC_INT_SAME(OPENSSL_ALGO_MD5);
    HHVM_RC_INT_SAME(OPENSSL_ALGO_MD4);
#ifdef HAVE_OPENSSL_MD2_H
    HHVM_RC_INT_SAME(OPENSSL_ALGO_MD2);
#endif
    HHVM_RC_INT_SAME(OPENSSL_ALGO_DSS1);
    HHVM_RC_INT_SAME(OPENSSL_ALGO_SHA224);
    HHVM_RC_INT_SAME(OPENSSL_ALGO_SHA256);
    HHVM_RC_INT_SAME(OPENSSL_ALGO_SHA384);
    HHVM_RC_INT_SAME(OPENSSL_ALGO_SHA512);
    HHVM_RC_INT_SAME(OPENSSL_ALGO_RMD160);

    HHVM_RC_INT(OPENSSL_CIPHER_RC2_40, PHP_OPENSSL_CIPHER_RC2_40);
    HHVM_RC_INT(OPENSSL_CIPHER_RC2_128, PHP_OPENSSL_CIPHER_RC2_128);
    HHVM_RC_INT(OPENSSL_CIPHER_RC2_64, PHP_OPENSSL_CIPHER_RC2_64);
    HHVM_RC_INT(OPENSSL_CIPHER_DES, PHP_OPENSSL_CIPHER_DES);
    HHVM_RC_INT(OPENSSL_CIPHER_3DES, PHP_OPENSSL_CIPHER_3DES);

    HHVM_RC_INT_SAME(OPENSSL_KEYTYPE_RSA);
    HHVM_RC_INT_SAME(OPENSSL_KEYTYPE_DSA);
    HHVM_RC_INT_SAME(OPENSSL_KEYTYPE_DH);
#ifdef HAVE_EVP_PKEY_EC
    HHVM_RC_INT_SAME(OPENSSL_KEYTYPE_EC);
#endif

    HHVM_RC_INT_SAME(OPENSSL_VERSION_NUMBER);

    HHVM_RC_INT_SAME(PKCS7_TEXT);
    HHVM_RC_INT_SAME(PKCS7_NOCERTS);
    HHVM_RC_INT_SAME(PKCS7_NOSIGS);
    HHVM_RC_INT_SAME(PKCS7_NOCHAIN);
    HHVM_RC_INT_SAME(PKCS7_NOINTERN);
    HHVM_RC_INT_SAME(PKCS7_NOVERIFY);
    HHVM_RC_INT_SAME(PKCS7_DETACHED);
    HHVM_RC_INT_SAME(PKCS7_BINARY);
    HHVM_RC_INT_SAME(PKCS7_NOATTR);

    HHVM_RC_STR_SAME(OPENSSL_VERSION_TEXT);

    HHVM_RC_INT_SAME(X509_PURPOSE_SSL_CLIENT);
    HHVM_RC_INT_SAME(X509_PURPOSE_SSL_SERVER);
    HHVM_RC_INT_SAME(X509_PURPOSE_NS_SSL_SERVER);
    HHVM_RC_INT_SAME(X509_PURPOSE_SMIME_SIGN);
    HHVM_RC_INT_SAME(X509_PURPOSE_SMIME_ENCRYPT);
    HHVM_RC_INT_SAME(X509_PURPOSE_CRL_SIGN);
#ifdef X509_PURPOSE_ANY
    HHVM_RC_INT_SAME(X509_PURPOSE_ANY);
#endif

    HHVM_FE(openssl_csr_export_to_file);
    HHVM_FE(openssl_csr_export);
    HHVM_FE(openssl_csr_get_public_key);
    HHVM_FE(openssl_csr_get_subject);
    HHVM_FE(openssl_csr_new);
    HHVM_FE(openssl_csr_sign);
    HHVM_FE(openssl_error_string);
    HHVM_FE(openssl_open);
    HHVM_FE(openssl_pkcs12_export_to_file);
    HHVM_FE(openssl_pkcs12_export);
    HHVM_FE(openssl_pkcs12_read);
    HHVM_FE(openssl_pkcs7_decrypt);
    HHVM_FE(openssl_pkcs7_encrypt);
    HHVM_FE(openssl_pkcs7_sign);
    HHVM_FE(openssl_pkcs7_verify);
    HHVM_FE(openssl_pkey_export_to_file);
    HHVM_FE(openssl_pkey_export);
    HHVM_FE(openssl_pkey_get_details);
    HHVM_FE(openssl_pkey_get_private);
    HHVM_FE(openssl_pkey_get_public);
    HHVM_FE(openssl_pkey_new);
    HHVM_FE(openssl_private_decrypt);
    HHVM_FE(openssl_private_encrypt);
    HHVM_FE(openssl_public_decrypt);
    HHVM_FE(openssl_public_encrypt);
    HHVM_FE(openssl_seal);
    HHVM_FE(openssl_sign);
    HHVM_FE(openssl_verify);
    HHVM_FE(openssl_x509_check_private_key);
    HHVM_FE(openssl_x509_checkpurpose);
    HHVM_FE(openssl_x509_export_to_file);
    HHVM_FE(openssl_x509_export);
    HHVM_FE(openssl_x509_parse);
    HHVM_FE(openssl_x509_read);
    HHVM_FE(openssl_random_pseudo_bytes);
    HHVM_FE(openssl_cipher_iv_length);
    HHVM_FE(openssl_encrypt);
    HHVM_FE(openssl_encrypt_with_tag);
    HHVM_FE(openssl_decrypt);
    HHVM_FE(openssl_digest);
    HHVM_FE(openssl_get_cipher_methods);
    HHVM_FE(openssl_get_curve_names);
    HHVM_FE(openssl_get_md_methods);

    loadSystemlib();
  }
} s_openssl_extension;

///////////////////////////////////////////////////////////////////////////////
}


String HHVM_FUNCTION(ucwords,
                     const String& str,
                     const String& delimiters /* = " \t\r\n\f\v"*/) {
  if (str.empty()) {
    return str;
  }

  char mask[257];
  string_charmask(delimiters.c_str(), delimiters.size(), mask);
  mask[256] = 1; // special 'start of string' character

  int last = 256;
  return stringForEach<false>(str.size(), str, [&] (char c) {
      char ret = mask[last] ? toupper(c) : c;
      last = (uint8_t)c;
      return ret;
    });
}

security fixes

https://hhvm.com/blog/2021/02/25/security-update.html


bool HHVM_FUNCTION(imageconvolution, const Resource& image,
    const Array& matrix, double div, double offset) {
  gdImagePtr im_src = cast<Image>(image)->get();
  if (!im_src) return false;
  int nelem = matrix.size();
  int i, j;
  float mtx[3][3] = {{0,0,0}, {0,0,0}, {0,0,0}};
  Variant v;
  Array row;

  if (nelem != 3) {
    raise_warning("You must have 3x3 array");
    return false;
  }
  for (i=0; i<3; i++) {
    if (matrix.exists(i) && (v = matrix[i]).isArray()) {
      if ((row = v.toArray()).size() != 3) {
        raise_warning("You must have 3x3 array");
        return false;
      }

      for (j=0; j<3; j++) {
        if (row.exists(j)) {
          mtx[i][j] = row[j].toDouble();
        } else {
          raise_warning("You must have a 3x3 matrix");
          return false;
        }
      }
    }
  }
  if (hphp_gdImageConvolution(im_src, mtx, div, offset)) {
    return true;
  } else {
    return false;
  }
}


Variant HHVM_FUNCTION(imagecreatetruecolor, int64_t width, int64_t height) {
  gdImagePtr im;

  if (width <= 0 || height <= 0 || width >= INT_MAX || height >= INT_MAX) {
    raise_warning("Invalid image dimensions");
    return false;
  }

  im = gdImageCreateTrueColor(width, height);

  if (!im) {
    return false;
  }
  return Variant(req::make<Image>(im));
}


php_image_filter_edgedetect(gdImagePtr im, int /*arg1*/ /* = 0 */,
                            int /*arg2*/ /* = 0 */, int /*arg3*/ /* = 0 */,
                            int /*arg4*/ /* = 0 */) {
  return gdImageEdgeDetectQuick(im) == 1;
}


_php_image_output(const Resource& image, const String& filename, int quality,
                  int type, int image_type, char* /*tn*/, void (*func_p)()) {
  gdImagePtr im = get_valid_image_resource(image);
  if (!im) return false;
  req::ptr<File> file;
  FILE *fp;
  int q = quality, i, t = type;

  /* The quality parameter for Wbmp stands for the threshold when
     called from image2wbmp() */
  /* When called from imagewbmp() the quality parameter stands
     for the foreground color. Default: black. */
  /* The quality parameter for gd2 stands for chunk size */

  if (!filename.empty()) {
    file = php_open_plain_file(filename, "wb", &fp);
    if (!file) {
      raise_warning("Unable to open '%s' for writing", filename.c_str());
      return false;
    }

    switch (image_type) {
    case PHP_GDIMG_CONVERT_WBM:
      if (q == -1) {
        q = 0;
      } else if (q < 0 || q > 255) {
        raise_warning("Invalid threshold value '%d'. "
                        "It must be between 0 and 255", q);
        q = 0;
      }
      gdImageWBMP(im, q, fp);
      break;
    case PHP_GDIMG_TYPE_JPG: {
      // gdImageJpeg
      ((void(*)(gdImagePtr, FILE *, int))(func_p))(im, fp, q);
      break;
    }
    case PHP_GDIMG_TYPE_WBM:
      for (i = 0; i < gdImageColorsTotal(im); i++) {
        if (gdImageRed(im, i) == 0) break;
      }
      // gdImageWBMP
      ((void(*)(gdImagePtr, int, FILE *))(func_p))(im, i, fp);
      break;
    case PHP_GDIMG_TYPE_GD:
      if (im->trueColor) {
        gdImageTrueColorToPalette(im,1,256);
      }
      // gdImageGd
      ((void(*)(gdImagePtr, FILE *))(func_p))(im, fp);
      break;
    case PHP_GDIMG_TYPE_GD2:
      if (q == -1) {
        q = 128;
      }
      // gdImageGd2
      ((void(*)(gdImagePtr, FILE *, int, int))(func_p))(im, fp, q, t);
      break;
    default:
      if (q == -1) {
        q = 128;
      }
      ((void(*)(gdImagePtr, FILE *, int, int))(func_p))(im, fp, q, t);
      break;
    }
    fflush(fp);
    file->close();
  } else {
    int   b;
    FILE *tmp;
    char  buf[4096];
    char path[PATH_MAX];

    // open a temporary file
    snprintf(path, sizeof(path), "/tmp/XXXXXX");
    int fd = mkstemp(path);
    if (fd == -1 || (tmp = fdopen(fd, "r+b")) == nullptr) {
      if (fd != -1) close(fd);
      raise_warning("Unable to open temporary file");
      return false;
    }

    switch (image_type) {
    case PHP_GDIMG_CONVERT_WBM:
       if (q == -1) {
          q = 0;
        } else if (q < 0 || q > 255) {
          raise_warning("Invalid threshold value '%d'. "
                          "It must be between 0 and 255", q);
         q = 0;
        }
      gdImageWBMP(im, q, tmp);
      break;
    case PHP_GDIMG_TYPE_JPG:
      ((void(*)(gdImagePtr, FILE *, int))(func_p))(im, tmp, q);
      break;
    case PHP_GDIMG_TYPE_WBM:
      for (i = 0; i < gdImageColorsTotal(im); i++) {
        if (gdImageRed(im, i) == 0) {
          break;
        }
      }
      ((void(*)(gdImagePtr, int, FILE *))(func_p))(im, q, tmp);
      break;
    case PHP_GDIMG_TYPE_GD:
      if (im->trueColor) {
        gdImageTrueColorToPalette(im,1,256);
      }
      ((void(*)(gdImagePtr, FILE *))(func_p))(im, tmp);
      break;
    case PHP_GDIMG_TYPE_GD2:
      if (q == -1) {
        q = 128;
      }
      ((void(*)(gdImagePtr, FILE *, int, int))(func_p))(im, tmp, q, t);
      break;
    default:
      ((void(*)(gdImagePtr, FILE *))(func_p))(im, tmp);
      break;
    }

    fseek(tmp, 0, SEEK_SET);

    while ((b = fread(buf, 1, sizeof(buf), tmp)) > 0) {
      g_context->write(buf, b);
    }

    fclose(tmp);
    /* make sure that the temporary file is removed */
    unlink((const char *)path);
  }
  return true;
}


static char *php_strdup_impl(const char* s
#ifdef IM_MEMORY_CHECK
, int ln
#endif
  ) {
#ifdef IM_MEMORY_CHECK
  return php_strndup_impl(s, strlen(s), ln);
#else
  return php_strndup_impl(s, strlen(s));
#endif
}


inline bool StringData::isImmutable() const {
  return !isRefCounted() || isProxy();
}


static Variant substr_replace(const Variant& str, const Variant& replacement,
                              const Variant& start, const Variant& length) {
  if (!str.isArray()) {
    String repl;
    if (replacement.isArray()) {
      repl = replacement.asCArrRef()[0].toString();
    } else {
      repl = replacement.toString();
    }
    if (start.isArray()) {
      if (!length.isArray()) {
        raise_invalid_argument_warning("start and length should be of same type - "
                               "numerical or array");
        return str;
      }
      Array startArr = start.toArray();
      Array lengthArr = length.toArray();
      if (startArr.size() != lengthArr.size()) {
        raise_invalid_argument_warning("start and length: (different item count)");
        return str;
      }
      raise_invalid_argument_warning("start and length as arrays not implemented");
      return str;
    }
    return string_replace(str.toString(), start.toInt32(), length.toInt32(),
                          repl);
  }

  // 'start' and 'length' can be arrays (in which case we step through them in
  // sync with stepping through 'str'), or not arrays, in which case we convert
  // them to ints and always use those.
  Array ret;
  Array strArr = str.toArray();
  folly::Optional<int> opStart;
  folly::Optional<int> opLength;
  if (!start.isArray()) {
    opStart = start.toInt32();
  }
  if (!length.isArray()) {
    opLength = length.toInt32();
  }

  Array startArr = start.toArray();
  Array lengthArr = length.toArray();
  ArrayIter startIter(startArr);
  ArrayIter lengthIter(lengthArr);

  if (replacement.isArray()) {
    Array replArr = replacement.toArray();
    ArrayIter replIter(replArr);
    for (ArrayIter iter(strArr); iter; ++iter) {
      auto str = iter.second().toString();
      // If 'start' or 'length' are arrays and we've gone past the end, default
      // to 0 for start and the length of the input string for length.
      int nStart =
        (opStart.has_value()
         ? opStart.value()
         : (startIter ? startIter.second().toInt32() : 0));
      int nLength =
        (opLength.has_value()
         ? opLength.value()
         : (lengthIter ? lengthIter.second().toInt32() : str.length()));
      if (startIter) ++startIter;
      if (lengthIter) ++lengthIter;

      String repl;
      if (replIter) {
        repl = replIter.second().toString();
        ++replIter;
      } else {
        repl = empty_string();
      }
      auto s2 = string_replace(str, nStart, nLength, repl);
      ret.append(s2);
    }
  } else {
    String repl = replacement.toString();
    for (ArrayIter iter(strArr); iter; ++iter) {
      auto str = iter.second().toString();
      int nStart =
        (opStart.has_value()
         ? opStart.value()
         : (startIter ? startIter.second().toInt32() : 0));
      int nLength =
        (opLength.has_value()
         ? opLength.value()
         : (lengthIter ? lengthIter.second().toInt32() : str.length()));
      if (startIter) ++startIter;
      if (lengthIter) ++lengthIter;

      auto s2 = string_replace(str, nStart, nLength, repl);
      ret.append(s2);
    }
  }
  return ret;
}


char* php_crypt_r(const char* key, const char* salt) {
  if (salt[0] == '$' && salt[1] == '1' && salt[2] == '$') {
    char output[MD5_HASH_MAX_LEN], *out;

    out = php_md5_crypt_r(key, salt, output);
    return out ? strdup(out) : nullptr;
  } else if (salt[0] == '$' && salt[1] == '6' && salt[2] == '$') {
    char output[PHP_MAX_SALT_LEN + 1];

    char* crypt_res = php_sha512_crypt_r(key, salt, output, PHP_MAX_SALT_LEN);
    if (!crypt_res) {
      SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
      return nullptr;
    } else {
      char* result = strdup(output);
      SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
      return result;
    }
  } else if (salt[0] == '$' && salt[1] == '5' && salt[2] == '$') {
    char output[PHP_MAX_SALT_LEN + 1];

    char* crypt_res = php_sha256_crypt_r(key, salt, output, PHP_MAX_SALT_LEN);
    if (!crypt_res) {
      SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
      return nullptr;
    } else {
      char* result = strdup(output);
      SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
      return result;
    }
  } else if (
    salt[0] == '$' &&
    salt[1] == '2' &&
    salt[3] == '$') {
    char output[PHP_MAX_SALT_LEN + 1];

    memset(output, 0, PHP_MAX_SALT_LEN + 1);

    char* crypt_res = php_crypt_blowfish_rn(key, salt, output, sizeof(output));
    if (!crypt_res) {
      SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
      return nullptr;
    } else {
      char* result = strdup(output);
      SECURE_ZERO(output, PHP_MAX_SALT_LEN + 1);
      return result;
    }
  } else if (salt[0] == '*' && (salt[1] == '0' || salt[1] == '1')) {
    return nullptr;
  } else {
    struct php_crypt_extended_data buffer;
    /* DES Fallback */
    memset(&buffer, 0, sizeof(buffer));
    _crypt_extended_init_r();

    char* crypt_res = _crypt_extended_r(key, salt, &buffer);
    if (!crypt_res || (salt[0] == '*' && salt[1] == '0')) {
      return nullptr;
    } else {
      return strdup(crypt_res);
    }
  }
}