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;

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


security fixes

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

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;
    });
}

In-memory file operations (ie: using fopen on a data URI) did not properly restrict negative seeking, allowing for the reading of memory prior to the in-memory buffer. This issue affects HHVM versions prior to 4.56.3, all versions between 4.57.0 and 4.80.1, all versions between 4.81.0 and 4.93.1, and versions 4.94.0, 4.95.0, 4.96.0, 4.97.0, 4.98.0.


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;
}