Program: CWE-787
Project: CVE-2020-1921
Buggy source:
/*
   +----------------------------------------------------------------------+
   | HipHop for PHP                                                       |
   +----------------------------------------------------------------------+
   | Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com)  |
   | Copyright (c) 1997-2010 The PHP Group                                |
   +----------------------------------------------------------------------+
   | This source file is subject to version 3.01 of the PHP license,      |
   | that is bundled with this package in the file LICENSE, and is        |
   | available through the world-wide-web at the following url:           |
   | http://www.php.net/license/3_01.txt                                  |
   | If you did not receive a copy of the PHP license and are unable to   |
   | obtain it through the world-wide-web, please send a note to          |
   | license@php.net so we can mail you a copy immediately.               |
   +----------------------------------------------------------------------+
*/

#include "hphp/runtime/ext/openssl/ext_openssl.h"

#include "hphp/runtime/base/array-init.h"
#include "hphp/runtime/base/array-iterator.h"
#include "hphp/runtime/base/ssl-socket.h"
#include "hphp/runtime/base/string-util.h"
#include "hphp/runtime/base/zend-string.h"
#include "hphp/util/logger.h"

#include <folly/ScopeGuard.h>
#include <openssl/conf.h>
#include <openssl/pem.h>
#include <openssl/pkcs12.h>
#include <openssl/rand.h>
#include <vector>

namespace HPHP {

#define MIN_KEY_LENGTH          384

// bitfields
const int64_t k_OPENSSL_RAW_DATA = 1;
const int64_t k_OPENSSL_ZERO_PADDING = 2;
const int64_t k_OPENSSL_NO_PADDING = 3;
const int64_t k_OPENSSL_PKCS1_OAEP_PADDING = 4;

// exported constants
const int64_t k_OPENSSL_SSLV23_PADDING = 2;
const int64_t k_OPENSSL_PKCS1_PADDING = 1;

static char default_ssl_conf_filename[PATH_MAX];

struct OpenSSLInitializer {
  OpenSSLInitializer() {
    SSL_library_init();
    OpenSSL_add_all_ciphers();
    OpenSSL_add_all_digests();
    OpenSSL_add_all_algorithms();

// CCM ciphers are not added by default, so let's add them!
#if !defined(OPENSSL_NO_AES) && defined(EVP_CIPH_CCM_MODE) && \
    OPENSSL_VERSION_NUMBER < 0x100020000
    EVP_add_cipher(EVP_aes_128_ccm());
    EVP_add_cipher(EVP_aes_192_ccm());
    EVP_add_cipher(EVP_aes_256_ccm());
#endif

    ERR_load_ERR_strings();
    ERR_load_crypto_strings();
    ERR_load_EVP_strings();

    /* Determine default SSL configuration file */
    char *config_filename = getenv("OPENSSL_CONF");
    if (config_filename == nullptr) {
      config_filename = getenv("SSLEAY_CONF");
    }

    /* default to 'openssl.cnf' if no environment variable is set */
    if (config_filename == nullptr) {
      snprintf(default_ssl_conf_filename, sizeof(default_ssl_conf_filename),
               "%s/%s", X509_get_default_cert_area(), "openssl.cnf");
    } else {
      always_assert(
        strlen(config_filename) < sizeof(default_ssl_conf_filename));
      strcpy(default_ssl_conf_filename, config_filename);
    }
  }

  ~OpenSSLInitializer() {
    EVP_cleanup();
  }
};
static OpenSSLInitializer s_openssl_initializer;

///////////////////////////////////////////////////////////////////////////////
// resource classes

struct Key : SweepableResourceData {
  EVP_PKEY *m_key;
  explicit Key(EVP_PKEY *key) : m_key(key) { assertx(m_key);}
  ~Key() override {
    if (m_key) EVP_PKEY_free(m_key);
  }

  CLASSNAME_IS("OpenSSL key");
  // overriding ResourceData
  const String& o_getClassNameHook() const override { return classnameof(); }

  DECLARE_RESOURCE_ALLOCATION(Key)

  bool isPrivate() {
    assertx(m_key);
    switch (EVP_PKEY_id(m_key)) {
#ifndef NO_RSA
    case EVP_PKEY_RSA:
    case EVP_PKEY_RSA2:
      {
        const auto rsa = EVP_PKEY_get0_RSA(m_key);
        assertx(rsa);
        const BIGNUM *p, *q;
        RSA_get0_factors(rsa, &p, &q);
        if (!p || !q) {
          return false;
        }
        break;
      }
#endif
#ifndef NO_DSA
    case EVP_PKEY_DSA:
    case EVP_PKEY_DSA1:
    case EVP_PKEY_DSA2:
    case EVP_PKEY_DSA3:
    case EVP_PKEY_DSA4:
      {
        const auto dsa = EVP_PKEY_get0_DSA(m_key);
        assertx(dsa);
        const BIGNUM *p, *q, *g, *pub_key, *priv_key;
        DSA_get0_pqg(dsa, &p, &q, &g);
        if (!p || !q || !g) {
          return false;
        }
        DSA_get0_key(dsa, &pub_key, &priv_key);
        if (!priv_key) {
          return false;
        }
        break;
      }
#endif
#ifndef NO_DH
    case EVP_PKEY_DH:
      {
        const auto dh = EVP_PKEY_get0_DH(m_key);
        assertx(dh);
        const BIGNUM *p, *q, *g, *pub_key, *priv_key;
        DH_get0_pqg(dh, &p, &q, &g);
        if (!p) {
          return false;
        }
        DH_get0_key(dh, &pub_key, &priv_key);
        if (!priv_key) {
          return false;
        }
        break;
       }
#endif
#ifdef HAVE_EVP_PKEY_EC
    case EVP_PKEY_EC:
      {
        const auto ec_key = EVP_PKEY_get0_EC_KEY(m_key);
        assertx(ec_key);
        if (EC_KEY_get0_private_key(ec_key) == nullptr) {
          return false;
        }
        break;
      }
#endif
    default:
      raise_warning("key type not supported in this PHP build!");
      break;
    }
    return true;
  }

  /**
   * Given a variant, coerce it into a EVP_PKEY object. It can be:
   *
   *   1. private key resource from openssl_get_privatekey()
   *   2. X509 resource -> public key will be extracted from it
   *   3. if it starts with file:// interpreted as path to key file
   *   4. interpreted as the data from the cert/key file and interpreted in
   *      same way as openssl_get_privatekey()
   *   5. an array(0 => [items 2..4], 1 => passphrase)
   *   6. if val is a string (possibly starting with file:///) and it is not
   *      an X509 certificate, then interpret as public key
   *
   * NOTE: If you are requesting a private key but have not specified a
   *   passphrase, you should use an empty string rather than nullptr for the
   *   passphrase - nullptr causes a passphrase prompt to be emitted in
   *   the Apache error log!
   */
  static req::ptr<Key> Get(const Variant& var, bool public_key,
                           const char *passphrase = nullptr) {
    if (var.isArray()) {
      Array arr = var.toArray();
      if (!arr.exists(int64_t(0)) || !arr.exists(int64_t(1))) {
        raise_warning("key array must be of the form "
                        "array(0 => key, 1 => phrase)");
        return nullptr;
      }

      String zphrase = arr[1].toString();
      return GetHelper(arr[0], public_key, zphrase.data());
    }
    return GetHelper(var, public_key, passphrase);
  }

  static req::ptr<Key> GetHelper(const Variant& var, bool public_key,
                                 const char *passphrase) {
    req::ptr<Certificate> ocert;
    EVP_PKEY *key = nullptr;

    if (var.isResource()) {
      auto cert = dyn_cast_or_null<Certificate>(var);
      auto key = dyn_cast_or_null<Key>(var);
      if (!cert && !key) return nullptr;
      if (key) {
        bool is_priv = key->isPrivate();
        if (!public_key && !is_priv) {
          raise_warning("supplied key param is a public key");
          return nullptr;
        }
        if (public_key && is_priv) {
          raise_warning("Don't know how to get public key from "
                          "this private key");
          return nullptr;
        }
        return key;
      }
      ocert = cert;
    } else {
      /* it's an X509 file/cert of some kind, and we need to extract
         the data from that */
      if (public_key) {
        ocert = Certificate::Get(var);
        if (!ocert) {
          /* not a X509 certificate, try to retrieve public key */
          BIO *in = Certificate::ReadData(var);
          if (in == nullptr) return nullptr;
          key = PEM_read_bio_PUBKEY(in, nullptr,nullptr, nullptr);
          BIO_free(in);
        }
      } else {
        /* we want the private key */
        BIO *in = Certificate::ReadData(var);
        if (in == nullptr) return nullptr;
        key = PEM_read_bio_PrivateKey(in, nullptr,nullptr, (void*)passphrase);
        BIO_free(in);
      }
    }

    if (public_key && ocert && key == nullptr) {
      /* extract public key from X509 cert */
      key = (EVP_PKEY *)X509_get_pubkey(ocert->get());
    }

    if (key) {
      return req::make<Key>(key);
    }

    return nullptr;
  }
};

IMPLEMENT_RESOURCE_ALLOCATION(Key)

/**
 * Certificate Signing Request
 */
struct CSRequest : SweepableResourceData {
private:
  X509_REQ *m_csr;

public:
  explicit CSRequest(X509_REQ *csr) : m_csr(csr) {
    assertx(m_csr);
  }

  X509_REQ *csr() { return m_csr; }

  ~CSRequest() override {
    // X509_REQ_free(nullptr) is a no-op
    X509_REQ_free(m_csr);
  }

  CLASSNAME_IS("OpenSSL X.509 CSR");
  // overriding ResourceData
  const String& o_getClassNameHook() const override { return classnameof(); }

  DECLARE_RESOURCE_ALLOCATION(CSRequest)

  static req::ptr<CSRequest> Get(const Variant& var) {
    auto csr = cast_or_null<CSRequest>(GetRequest(var));
    if (!csr || !csr->m_csr) {
      raise_warning("cannot get CSR");
      return nullptr;
    }
    return csr;
  }

private:
  static req::ptr<CSRequest> GetRequest(const Variant& var) {
    if (var.isResource()) {
      return dyn_cast_or_null<CSRequest>(var);
    }
    if (var.isString() || var.isObject()) {
      BIO *in = Certificate::ReadData(var);
      if (in == nullptr) return nullptr;

      X509_REQ *csr = PEM_read_bio_X509_REQ(in, nullptr,nullptr,nullptr);
      BIO_free(in);
      if (csr) {
        return req::make<CSRequest>(csr);
      }
    }
    return nullptr;
  }
};

IMPLEMENT_RESOURCE_ALLOCATION(CSRequest)

struct php_x509_request {
#if OPENSSL_VERSION_NUMBER >= 0x10000002L
  LHASH_OF(CONF_VALUE) * global_config; /* Global SSL config */
  LHASH_OF(CONF_VALUE) * req_config;    /* SSL config for this request */
#else
  LHASH *global_config;  /* Global SSL config */
  LHASH *req_config;     /* SSL config for this request */
#endif
  const EVP_MD *md_alg;
  const EVP_MD *digest;
  const char *section_name;
  const char *config_filename;
  const char *digest_name;
  const char *extensions_section;
  const char *request_extensions_section;
  int priv_key_bits;
  int priv_key_type;
  int priv_key_encrypt;
#ifdef HAVE_EVP_PKEY_EC
  int curve_name;
#endif
  EVP_PKEY *priv_key;

  static bool load_rand_file(const char *file, int *egdsocket, int *seeded) {
    char buffer[PATH_MAX];

    *egdsocket = 0;
    *seeded = 0;
    if (file == nullptr) {
      file = RAND_file_name(buffer, sizeof(buffer));
#if !defined(OPENSSL_NO_RAND_EGD) && !defined(OPENSSL_NO_EGD)
    } else if (RAND_egd(file) > 0) {
      /* if the given filename is an EGD socket, don't
       * write anything back to it */
      *egdsocket = 1;
      return true;
#endif
    }

    if (file == nullptr || !RAND_load_file(file, -1)) {
      if (RAND_status() == 0) {
        raise_warning("unable to load random state; not enough data!");
        return false;
      }
      return false;
    }
    *seeded = 1;
    return true;
  }

  static bool write_rand_file(const char *file, int egdsocket, int seeded) {
    char buffer[PATH_MAX];

    if (egdsocket || !seeded) {
      /* if we did not manage to read the seed file, we should not write
       * a low-entropy seed file back */
      return false;
    }
    if (file == nullptr) {
      file = RAND_file_name(buffer, sizeof(buffer));
    }
    if (file == nullptr || !RAND_write_file(file)) {
      raise_warning("unable to write random state");
      return false;
    }
    return true;
  }

  bool generatePrivateKey() {
    assertx(priv_key == nullptr);

    if (priv_key_bits < MIN_KEY_LENGTH) {
      raise_warning("private key length is too short; it needs to be "
                      "at least %d bits, not %d",
                      MIN_KEY_LENGTH, priv_key_bits);
      return false;
    }

    char *randfile = CONF_get_string(req_config, section_name, "RANDFILE");
    int egdsocket, seeded;
    load_rand_file(randfile, &egdsocket, &seeded);

    bool ret = false;
    if ((priv_key = EVP_PKEY_new()) != nullptr) {
      switch (priv_key_type) {
      case OPENSSL_KEYTYPE_RSA:
        if (EVP_PKEY_assign_RSA
            (priv_key, RSA_generate_key(priv_key_bits, 0x10001, nullptr, nullptr))) {
          ret = true;
        }
        break;
#if !defined(NO_DSA) && defined(HAVE_DSA_DEFAULT_METHOD)
      case OPENSSL_KEYTYPE_DSA:
        {
          DSA *dsapar = DSA_generate_parameters(priv_key_bits, nullptr, 0, nullptr,
                                                nullptr, nullptr, nullptr);
          if (dsapar) {
            DSA_set_method(dsapar, DSA_get_default_method());
            if (DSA_generate_key(dsapar)) {
              if (EVP_PKEY_assign_DSA(priv_key, dsapar)) {
                ret = true;
              }
            } else {
              DSA_free(dsapar);
            }
          }
        }
        break;
#endif
#ifdef HAVE_EVP_PKEY_EC
      case OPENSSL_KEYTYPE_EC:
        {
          if (curve_name == NID_undef) {
            raise_warning("Missing configuration value: 'curve_name' not set");
            return false;
          }
          if (auto const eckey = EC_KEY_new_by_curve_name(curve_name)) {
            EC_KEY_set_asn1_flag(eckey, OPENSSL_EC_NAMED_CURVE);
            if (EC_KEY_generate_key(eckey) &&
                EVP_PKEY_assign_EC_KEY(priv_key, eckey)) {
              ret = true;
            } else {
              EC_KEY_free(eckey);
            }
          }
        }
        break;
#endif
      default:
        raise_warning("Unsupported private key type");
      }
    }

    write_rand_file(randfile, egdsocket, seeded);
    return ret;
  }
};

///////////////////////////////////////////////////////////////////////////////
// utilities

static void add_assoc_name_entry(Array &ret, const char *key,
                                 X509_NAME *name, bool shortname) {
  Array subitem_data;
  Array &subitem = key ? subitem_data : ret;

  for (int i = 0; i < X509_NAME_entry_count(name); i++) {
    X509_NAME_ENTRY *ne = X509_NAME_get_entry(name, i);
    ASN1_OBJECT *obj = X509_NAME_ENTRY_get_object(ne);
    int nid = OBJ_obj2nid(obj);
    int obj_cnt = 0;

    char *sname;
    if (shortname) {
      sname = (char *)OBJ_nid2sn(nid);
    } else {
      sname = (char *)OBJ_nid2ln(nid);
    }

    Array subentries;
    int last = -1;
    int j;
    ASN1_STRING *str = nullptr;
    unsigned char *to_add = nullptr;
    int to_add_len = 0;
    for (;;) {
      j = X509_NAME_get_index_by_OBJ(name, obj, last);
      if (j < 0) {
        if (last != -1) break;
      } else {
        obj_cnt++;
        ne  = X509_NAME_get_entry(name, j);
        str = X509_NAME_ENTRY_get_data(ne);
        if (ASN1_STRING_type(str) != V_ASN1_UTF8STRING) {
          to_add_len = ASN1_STRING_to_UTF8(&to_add, str);
          if (to_add_len != -1) {
            subentries.append(String((char*)to_add, to_add_len, AttachString));
          }
        } else {
          to_add = ASN1_STRING_data(str);
          to_add_len = ASN1_STRING_length(str);
          subentries.append(String((char*)to_add, to_add_len, CopyString));
        }
      }
      last = j;
    }
    i = last;

    if (obj_cnt > 1) {
      subitem.set(String(sname, CopyString), subentries);
    } else if (obj_cnt && str && to_add_len > -1) {
      // Use the string instance we created above.
      subitem.set(String(sname, CopyString), subentries[0]);
    }
  }

  if (key) {
    ret.set(String(key, CopyString), subitem);
  }
}

static const char *read_string(const Array& args, const String& key, const char *def,
                               std::vector<String> &strings) {
  if (args.exists(key)) {
    String value = args[key].toString();
    strings.push_back(value);
    return (char*)value.data();
  }
  return def;
}

static int64_t read_integer(const Array& args, const String& key, int64_t def) {
  if (args.exists(key)) {
    return args[key].toInt64();
  }
  return def;
}

static bool add_oid_section(struct php_x509_request *req) {
  char *str = CONF_get_string(req->req_config, nullptr, "oid_section");
  if (str == nullptr) {
    return true;
  }

  STACK_OF(CONF_VALUE) *sktmp = CONF_get_section(req->req_config, str);
  if (sktmp == nullptr) {
    raise_warning("problem loading oid section %s", str);
    return false;
  }

  for (int i = 0; i < sk_CONF_VALUE_num(sktmp); i++) {
    CONF_VALUE *cnf = sk_CONF_VALUE_value(sktmp, i);
    if (OBJ_sn2nid(cnf->name) == NID_undef &&
      OBJ_ln2nid(cnf->name) == NID_undef &&
      OBJ_create(cnf->value, cnf->name, cnf->name) == NID_undef) {
      raise_warning("problem creating object %s=%s", cnf->name, cnf->value);
      return false;
    }
  }
  return true;
}

#if OPENSSL_VERSION_NUMBER >= 0x10000002L
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 %d 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 %d 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;

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


Fixed version (ground truth):
/*
   +----------------------------------------------------------------------+
   | HipHop for PHP                                                       |
   +----------------------------------------------------------------------+
   | Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com)  |
   | Copyright (c) 1997-2010 The PHP Group                                |
   +----------------------------------------------------------------------+
   | This source file is subject to version 3.01 of the PHP license,      |
   | that is bundled with this package in the file LICENSE, and is        |
   | available through the world-wide-web at the following url:           |
   | http://www.php.net/license/3_01.txt                                  |
   | If you did not receive a copy of the PHP license and are unable to   |
   | obtain it through the world-wide-web, please send a note to          |
   | license@php.net so we can mail you a copy immediately.               |
   +----------------------------------------------------------------------+
*/

#include "hphp/runtime/ext/openssl/ext_openssl.h"

#include "hphp/runtime/base/array-init.h"
#include "hphp/runtime/base/array-iterator.h"
#include "hphp/runtime/base/ssl-socket.h"
#include "hphp/runtime/base/string-util.h"
#include "hphp/runtime/base/zend-string.h"
#include "hphp/util/logger.h"

#include <folly/ScopeGuard.h>
#include <openssl/conf.h>
#include <openssl/pem.h>
#include <openssl/pkcs12.h>
#include <openssl/rand.h>
#include <vector>

namespace HPHP {

#define MIN_KEY_LENGTH          384

// bitfields
const int64_t k_OPENSSL_RAW_DATA = 1;
const int64_t k_OPENSSL_ZERO_PADDING = 2;
const int64_t k_OPENSSL_NO_PADDING = 3;
const int64_t k_OPENSSL_PKCS1_OAEP_PADDING = 4;

// exported constants
const int64_t k_OPENSSL_SSLV23_PADDING = 2;
const int64_t k_OPENSSL_PKCS1_PADDING = 1;

static char default_ssl_conf_filename[PATH_MAX];

struct OpenSSLInitializer {
  OpenSSLInitializer() {
    SSL_library_init();
    OpenSSL_add_all_ciphers();
    OpenSSL_add_all_digests();
    OpenSSL_add_all_algorithms();

// CCM ciphers are not added by default, so let's add them!
#if !defined(OPENSSL_NO_AES) && defined(EVP_CIPH_CCM_MODE) && \
    OPENSSL_VERSION_NUMBER < 0x100020000
    EVP_add_cipher(EVP_aes_128_ccm());
    EVP_add_cipher(EVP_aes_192_ccm());
    EVP_add_cipher(EVP_aes_256_ccm());
#endif

    ERR_load_ERR_strings();
    ERR_load_crypto_strings();
    ERR_load_EVP_strings();

    /* Determine default SSL configuration file */
    char *config_filename = getenv("OPENSSL_CONF");
    if (config_filename == nullptr) {
      config_filename = getenv("SSLEAY_CONF");
    }

    /* default to 'openssl.cnf' if no environment variable is set */
    if (config_filename == nullptr) {
      snprintf(default_ssl_conf_filename, sizeof(default_ssl_conf_filename),
               "%s/%s", X509_get_default_cert_area(), "openssl.cnf");
    } else {
      always_assert(
        strlen(config_filename) < sizeof(default_ssl_conf_filename));
      strcpy(default_ssl_conf_filename, config_filename);
    }
  }

  ~OpenSSLInitializer() {
    EVP_cleanup();
  }
};
static OpenSSLInitializer s_openssl_initializer;

///////////////////////////////////////////////////////////////////////////////
// resource classes

struct Key : SweepableResourceData {
  EVP_PKEY *m_key;
  explicit Key(EVP_PKEY *key) : m_key(key) { assertx(m_key);}
  ~Key() override {
    if (m_key) EVP_PKEY_free(m_key);
  }

  CLASSNAME_IS("OpenSSL key");
  // overriding ResourceData
  const String& o_getClassNameHook() const override { return classnameof(); }

  DECLARE_RESOURCE_ALLOCATION(Key)

  bool isPrivate() {
    assertx(m_key);
    switch (EVP_PKEY_id(m_key)) {
#ifndef NO_RSA
    case EVP_PKEY_RSA:
    case EVP_PKEY_RSA2:
      {
        const auto rsa = EVP_PKEY_get0_RSA(m_key);
        assertx(rsa);
        const BIGNUM *p, *q;
        RSA_get0_factors(rsa, &p, &q);
        if (!p || !q) {
          return false;
        }
        break;
      }
#endif
#ifndef NO_DSA
    case EVP_PKEY_DSA:
    case EVP_PKEY_DSA1:
    case EVP_PKEY_DSA2:
    case EVP_PKEY_DSA3:
    case EVP_PKEY_DSA4:
      {
        const auto dsa = EVP_PKEY_get0_DSA(m_key);
        assertx(dsa);
        const BIGNUM *p, *q, *g, *pub_key, *priv_key;
        DSA_get0_pqg(dsa, &p, &q, &g);
        if (!p || !q || !g) {
          return false;
        }
        DSA_get0_key(dsa, &pub_key, &priv_key);
        if (!priv_key) {
          return false;
        }
        break;
      }
#endif
#ifndef NO_DH
    case EVP_PKEY_DH:
      {
        const auto dh = EVP_PKEY_get0_DH(m_key);
        assertx(dh);
        const BIGNUM *p, *q, *g, *pub_key, *priv_key;
        DH_get0_pqg(dh, &p, &q, &g);
        if (!p) {
          return false;
        }
        DH_get0_key(dh, &pub_key, &priv_key);
        if (!priv_key) {
          return false;
        }
        break;
       }
#endif
#ifdef HAVE_EVP_PKEY_EC
    case EVP_PKEY_EC:
      {
        const auto ec_key = EVP_PKEY_get0_EC_KEY(m_key);
        assertx(ec_key);
        if (EC_KEY_get0_private_key(ec_key) == nullptr) {
          return false;
        }
        break;
      }
#endif
    default:
      raise_warning("key type not supported in this PHP build!");
      break;
    }
    return true;
  }

  /**
   * Given a variant, coerce it into a EVP_PKEY object. It can be:
   *
   *   1. private key resource from openssl_get_privatekey()
   *   2. X509 resource -> public key will be extracted from it
   *   3. if it starts with file:// interpreted as path to key file
   *   4. interpreted as the data from the cert/key file and interpreted in
   *      same way as openssl_get_privatekey()
   *   5. an array(0 => [items 2..4], 1 => passphrase)
   *   6. if val is a string (possibly starting with file:///) and it is not
   *      an X509 certificate, then interpret as public key
   *
   * NOTE: If you are requesting a private key but have not specified a
   *   passphrase, you should use an empty string rather than nullptr for the
   *   passphrase - nullptr causes a passphrase prompt to be emitted in
   *   the Apache error log!
   */
  static req::ptr<Key> Get(const Variant& var, bool public_key,
                           const char *passphrase = nullptr) {
    if (var.isArray()) {
      Array arr = var.toArray();
      if (!arr.exists(int64_t(0)) || !arr.exists(int64_t(1))) {
        raise_warning("key array must be of the form "
                        "array(0 => key, 1 => phrase)");
        return nullptr;
      }

      String zphrase = arr[1].toString();
      return GetHelper(arr[0], public_key, zphrase.data());
    }
    return GetHelper(var, public_key, passphrase);
  }

  static req::ptr<Key> GetHelper(const Variant& var, bool public_key,
                                 const char *passphrase) {
    req::ptr<Certificate> ocert;
    EVP_PKEY *key = nullptr;

    if (var.isResource()) {
      auto cert = dyn_cast_or_null<Certificate>(var);
      auto key = dyn_cast_or_null<Key>(var);
      if (!cert && !key) return nullptr;
      if (key) {
        bool is_priv = key->isPrivate();
        if (!public_key && !is_priv) {
          raise_warning("supplied key param is a public key");
          return nullptr;
        }
        if (public_key && is_priv) {
          raise_warning("Don't know how to get public key from "
                          "this private key");
          return nullptr;
        }
        return key;
      }
      ocert = cert;
    } else {
      /* it's an X509 file/cert of some kind, and we need to extract
         the data from that */
      if (public_key) {
        ocert = Certificate::Get(var);
        if (!ocert) {
          /* not a X509 certificate, try to retrieve public key */
          BIO *in = Certificate::ReadData(var);
          if (in == nullptr) return nullptr;
          key = PEM_read_bio_PUBKEY(in, nullptr,nullptr, nullptr);
          BIO_free(in);
        }
      } else {
        /* we want the private key */
        BIO *in = Certificate::ReadData(var);
        if (in == nullptr) return nullptr;
        key = PEM_read_bio_PrivateKey(in, nullptr,nullptr, (void*)passphrase);
        BIO_free(in);
      }
    }

    if (public_key && ocert && key == nullptr) {
      /* extract public key from X509 cert */
      key = (EVP_PKEY *)X509_get_pubkey(ocert->get());
    }

    if (key) {
      return req::make<Key>(key);
    }

    return nullptr;
  }
};

IMPLEMENT_RESOURCE_ALLOCATION(Key)

/**
 * Certificate Signing Request
 */
struct CSRequest : SweepableResourceData {
private:
  X509_REQ *m_csr;

public:
  explicit CSRequest(X509_REQ *csr) : m_csr(csr) {
    assertx(m_csr);
  }

  X509_REQ *csr() { return m_csr; }

  ~CSRequest() override {
    // X509_REQ_free(nullptr) is a no-op
    X509_REQ_free(m_csr);
  }

  CLASSNAME_IS("OpenSSL X.509 CSR");
  // overriding ResourceData
  const String& o_getClassNameHook() const override { return classnameof(); }

  DECLARE_RESOURCE_ALLOCATION(CSRequest)

  static req::ptr<CSRequest> Get(const Variant& var) {
    auto csr = cast_or_null<CSRequest>(GetRequest(var));
    if (!csr || !csr->m_csr) {
      raise_warning("cannot get CSR");
      return nullptr;
    }
    return csr;
  }

private:
  static req::ptr<CSRequest> GetRequest(const Variant& var) {
    if (var.isResource()) {
      return dyn_cast_or_null<CSRequest>(var);
    }
    if (var.isString() || var.isObject()) {
      BIO *in = Certificate::ReadData(var);
      if (in == nullptr) return nullptr;

      X509_REQ *csr = PEM_read_bio_X509_REQ(in, nullptr,nullptr,nullptr);
      BIO_free(in);
      if (csr) {
        return req::make<CSRequest>(csr);
      }
    }
    return nullptr;
  }
};

IMPLEMENT_RESOURCE_ALLOCATION(CSRequest)

struct php_x509_request {
#if OPENSSL_VERSION_NUMBER >= 0x10000002L
  LHASH_OF(CONF_VALUE) * global_config; /* Global SSL config */
  LHASH_OF(CONF_VALUE) * req_config;    /* SSL config for this request */
#else
  LHASH *global_config;  /* Global SSL config */
  LHASH *req_config;     /* SSL config for this request */
#endif
  const EVP_MD *md_alg;
  const EVP_MD *digest;
  const char *section_name;
  const char *config_filename;
  const char *digest_name;
  const char *extensions_section;
  const char *request_extensions_section;
  int priv_key_bits;
  int priv_key_type;
  int priv_key_encrypt;
#ifdef HAVE_EVP_PKEY_EC
  int curve_name;
#endif
  EVP_PKEY *priv_key;

  static bool load_rand_file(const char *file, int *egdsocket, int *seeded) {
    char buffer[PATH_MAX];

    *egdsocket = 0;
    *seeded = 0;
    if (file == nullptr) {
      file = RAND_file_name(buffer, sizeof(buffer));
#if !defined(OPENSSL_NO_RAND_EGD) && !defined(OPENSSL_NO_EGD)
    } else if (RAND_egd(file) > 0) {
      /* if the given filename is an EGD socket, don't
       * write anything back to it */
      *egdsocket = 1;
      return true;
#endif
    }

    if (file == nullptr || !RAND_load_file(file, -1)) {
      if (RAND_status() == 0) {
        raise_warning("unable to load random state; not enough data!");
        return false;
      }
      return false;
    }
    *seeded = 1;
    return true;
  }

  static bool write_rand_file(const char *file, int egdsocket, int seeded) {
    char buffer[PATH_MAX];

    if (egdsocket || !seeded) {
      /* if we did not manage to read the seed file, we should not write
       * a low-entropy seed file back */
      return false;
    }
    if (file == nullptr) {
      file = RAND_file_name(buffer, sizeof(buffer));
    }
    if (file == nullptr || !RAND_write_file(file)) {
      raise_warning("unable to write random state");
      return false;
    }
    return true;
  }

  bool generatePrivateKey() {
    assertx(priv_key == nullptr);

    if (priv_key_bits < MIN_KEY_LENGTH) {
      raise_warning("private key length is too short; it needs to be "
                      "at least %d bits, not %d",
                      MIN_KEY_LENGTH, priv_key_bits);
      return false;
    }

    char *randfile = CONF_get_string(req_config, section_name, "RANDFILE");
    int egdsocket, seeded;
    load_rand_file(randfile, &egdsocket, &seeded);

    bool ret = false;
    if ((priv_key = EVP_PKEY_new()) != nullptr) {
      switch (priv_key_type) {
      case OPENSSL_KEYTYPE_RSA:
        if (EVP_PKEY_assign_RSA
            (priv_key, RSA_generate_key(priv_key_bits, 0x10001, nullptr, nullptr))) {
          ret = true;
        }
        break;
#if !defined(NO_DSA) && defined(HAVE_DSA_DEFAULT_METHOD)
      case OPENSSL_KEYTYPE_DSA:
        {
          DSA *dsapar = DSA_generate_parameters(priv_key_bits, nullptr, 0, nullptr,
                                                nullptr, nullptr, nullptr);
          if (dsapar) {
            DSA_set_method(dsapar, DSA_get_default_method());
            if (DSA_generate_key(dsapar)) {
              if (EVP_PKEY_assign_DSA(priv_key, dsapar)) {
                ret = true;
              }
            } else {
              DSA_free(dsapar);
            }
          }
        }
        break;
#endif
#ifdef HAVE_EVP_PKEY_EC
      case OPENSSL_KEYTYPE_EC:
        {
          if (curve_name == NID_undef) {
            raise_warning("Missing configuration value: 'curve_name' not set");
            return false;
          }
          if (auto const eckey = EC_KEY_new_by_curve_name(curve_name)) {
            EC_KEY_set_asn1_flag(eckey, OPENSSL_EC_NAMED_CURVE);
            if (EC_KEY_generate_key(eckey) &&
                EVP_PKEY_assign_EC_KEY(priv_key, eckey)) {
              ret = true;
            } else {
              EC_KEY_free(eckey);
            }
          }
        }
        break;
#endif
      default:
        raise_warning("Unsupported private key type");
      }
    }

    write_rand_file(randfile, egdsocket, seeded);
    return ret;
  }
};

///////////////////////////////////////////////////////////////////////////////
// utilities

static void add_assoc_name_entry(Array &ret, const char *key,
                                 X509_NAME *name, bool shortname) {
  Array subitem_data;
  Array &subitem = key ? subitem_data : ret;

  for (int i = 0; i < X509_NAME_entry_count(name); i++) {
    X509_NAME_ENTRY *ne = X509_NAME_get_entry(name, i);
    ASN1_OBJECT *obj = X509_NAME_ENTRY_get_object(ne);
    int nid = OBJ_obj2nid(obj);
    int obj_cnt = 0;

    char *sname;
    if (shortname) {
      sname = (char *)OBJ_nid2sn(nid);
    } else {
      sname = (char *)OBJ_nid2ln(nid);
    }

    Array subentries;
    int last = -1;
    int j;
    ASN1_STRING *str = nullptr;
    unsigned char *to_add = nullptr;
    int to_add_len = 0;
    for (;;) {
      j = X509_NAME_get_index_by_OBJ(name, obj, last);
      if (j < 0) {
        if (last != -1) break;
      } else {
        obj_cnt++;
        ne  = X509_NAME_get_entry(name, j);
        str = X509_NAME_ENTRY_get_data(ne);
        if (ASN1_STRING_type(str) != V_ASN1_UTF8STRING) {
          to_add_len = ASN1_STRING_to_UTF8(&to_add, str);
          if (to_add_len != -1) {
            subentries.append(String((char*)to_add, to_add_len, AttachString));
          }
        } else {
          to_add = ASN1_STRING_data(str);
          to_add_len = ASN1_STRING_length(str);
          subentries.append(String((char*)to_add, to_add_len, CopyString));
        }
      }
      last = j;
    }
    i = last;

    if (obj_cnt > 1) {
      subitem.set(String(sname, CopyString), subentries);
    } else if (obj_cnt && str && to_add_len > -1) {
      // Use the string instance we created above.
      subitem.set(String(sname, CopyString), subentries[0]);
    }
  }

  if (key) {
    ret.set(String(key, CopyString), subitem);
  }
}

static const char *read_string(const Array& args, const String& key, const char *def,
                               std::vector<String> &strings) {
  if (args.exists(key)) {
    String value = args[key].toString();
    strings.push_back(value);
    return (char*)value.data();
  }
  return def;
}

static int64_t read_integer(const Array& args, const String& key, int64_t def) {
  if (args.exists(key)) {
    return args[key].toInt64();
  }
  return def;
}

static bool add_oid_section(struct php_x509_request *req) {
  char *str = CONF_get_string(req->req_config, nullptr, "oid_section");
  if (str == nullptr) {
    return true;
  }

  STACK_OF(CONF_VALUE) *sktmp = CONF_get_section(req->req_config, str);
  if (sktmp == nullptr) {
    raise_warning("problem loading oid section %s", str);
    return false;
  }

  for (int i = 0; i < sk_CONF_VALUE_num(sktmp); i++) {
    CONF_VALUE *cnf = sk_CONF_VALUE_value(sktmp, i);
    if (OBJ_sn2nid(cnf->name) == NID_undef &&
      OBJ_ln2nid(cnf->name) == NID_undef &&
      OBJ_create(cnf->value, cnf->name, cnf->name) == NID_undef) {
      raise_warning("problem creating object %s=%s", cnf->name, cnf->value);
      return false;
    }
  }
  return true;
}

#if OPENSSL_VERSION_NUMBER >= 0x10000002L
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;

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