Newer
Older
XinYang_IOS / Pods / OpenVPNAdapter / Sources / OpenVPN3 / openvpn / aws / awspc.hpp
@zhangfeng zhangfeng on 7 Dec 2023 18 KB 1.8.0
//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012-2020 OpenVPN Inc.
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Affero General Public License Version 3
//    as published by the Free Software Foundation.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU Affero General Public License for more details.
//
//    You should have received a copy of the GNU Affero General Public License
//    along with this program in the COPYING file.
//    If not, see <http://www.gnu.org/licenses/>.

// Get AWS info such as instanceId, region, and privateIp.
// Also optionally call AWSPC API with product code to get
// number of licensed concurrent connections.

#pragma once

#include <string>
#include <utility>

#include <openvpn/aws/awscreds.hpp>
#include <openvpn/ws/httpcliset.hpp>
#include <openvpn/common/jsonhelper.hpp>
#include <openvpn/common/hexstr.hpp>
#include <openvpn/common/enumdir.hpp>
#include <openvpn/common/file.hpp>
#include <openvpn/random/devurand.hpp>
#include <openvpn/frame/frame_init.hpp>
#include <openvpn/openssl/sign/verify.hpp>
#include <openvpn/openssl/sign/pkcs7verify.hpp>
#include <openvpn/ssl/sslchoose.hpp>

namespace openvpn {
  namespace AWS {

    class PCQuery : public RC<thread_unsafe_refcount>
    {
    public:
      typedef RCPtr<PCQuery> Ptr;

      OPENVPN_EXCEPTION(awspc_query_error);

      struct Info
      {
	std::string instanceId;
	std::string region;
	std::string privateIp;

	Creds creds;

	int concurrentConnections = -1;
	std::string error;

	bool is_error() const
	{
	  return !error.empty();
	}

	bool instance_data_defined() const
	{
	  return !instanceId.empty() && !region.empty() && !privateIp.empty();
	}

	// example: [instanceId=i-ae91d23e region=us-east-1 privateIp=10.0.0.218 concurrentConnections=10]
	std::string to_string() const
	{
	  std::string ret = "[instanceId=" + instanceId + " region=" + region;
	  if (!privateIp.empty())
	    ret += " privateIp=" + privateIp;
	  if (concurrentConnections >= 0)
	    ret += " concurrentConnections=" + std::to_string(concurrentConnections);
	  if (!error.empty())
	    ret += " error='" + error + '\'';
	  ret += ']';
	  return ret;
	}
      };

      PCQuery(WS::ClientSet::Ptr cs_arg,
	      const bool lookup_product_code_arg,
	      const int debug_level_arg)
	: cs(std::move(cs_arg)),
	  rng(new DevURand()),
	  frame(frame_init_simple(1024)),
	  lookup_product_code(lookup_product_code_arg),
	  debug_level(debug_level_arg)
      {
      }

      PCQuery(WS::ClientSet::Ptr cs_arg,
	      const std::string& role_for_credentials_arg,
	      const std::string& certs_dir_arg)
	: cs(std::move(cs_arg)),
	  rng(new DevURand()),
	  frame(frame_init_simple(1024)),
	  lookup_product_code(false),
	  debug_level(0),
	  role_for_credentials(role_for_credentials_arg),
	  certs_dir(certs_dir_arg)
      {
      }

      void start(std::function<void(Info info)> completion_arg)
      {
	// make sure we are not in a pending state
	if (pending)
	  throw awspc_query_error("request pending");
	pending = true;

	// save completion method
	completion = std::move(completion_arg);

	// init return object
	info = Info();

	try {
	  // make HTTP context
	  WS::Client::Config::Ptr http_config(new WS::Client::Config());
	  http_config->frame = frame;
	  http_config->connect_timeout = 15;
	  http_config->general_timeout = 30;

	  // make transaction set for initial local query
	  WS::ClientSet::TransactionSet::Ptr ts = new WS::ClientSet::TransactionSet;
	  ts->host.host = "169.254.169.254";
	  ts->host.port = "80";
	  ts->http_config = http_config;
	  ts->max_retries = 3;
	  ts->debug_level = debug_level;

	  // transaction #1
	  {
	    std::unique_ptr<WS::ClientSet::Transaction> t(new WS::ClientSet::Transaction);
	    t->req.method = "GET";
	    t->req.uri = "/latest/dynamic/instance-identity/document";
	    ts->transactions.push_back(std::move(t));
	  }

	  // transaction #2
	  {
	    std::unique_ptr<WS::ClientSet::Transaction> t(new WS::ClientSet::Transaction);
	    t->req.method = "GET";
	    t->req.uri = "/latest/dynamic/instance-identity/pkcs7";
	    ts->transactions.push_back(std::move(t));
	  }

	  // transaction #3
	  if (lookup_product_code)
	    {
	      std::unique_ptr<WS::ClientSet::Transaction> t(new WS::ClientSet::Transaction);
	      t->req.method = "GET";
	      t->req.uri = "/latest/meta-data/product-codes";
	      ts->transactions.push_back(std::move(t));
	    }

	  // transaction #4
	  if (!role_for_credentials.empty())
	    {
	      std::unique_ptr<WS::ClientSet::Transaction> t(new WS::ClientSet::Transaction);
	      t->req.method = "GET";
	      t->req.uri = "/latest/meta-data/iam/security-credentials/" + role_for_credentials;
	      ts->transactions.push_back(std::move(t));
	    }

	  // completion handler
	  ts->completion = [self=Ptr(this)](WS::ClientSet::TransactionSet& ts) {
	    self->local_query_complete(ts);
	  };

	  // do the request
	  cs->new_request(ts);
	}
	catch (const std::exception& e)
	  {
	    done(e.what());
	  }
      }

      void stop()
      {
	if (cs)
	  cs->stop();
      }

    private:
      void done(std::string error)
      {
	pending = false;
	info.error = std::move(error);
	if (completion)
	  completion(std::move(info));
      }

      void local_query_complete(WS::ClientSet::TransactionSet& lts)
      {
	try {
	  // get transactions and check that they succeeded
	  WS::ClientSet::Transaction& ident_trans = *lts.transactions.at(0);
	  if (!ident_trans.request_status_success())
	    {
	      done("could not fetch AWS identity document: " + ident_trans.format_status(lts));
	      return;
	    }

	  WS::ClientSet::Transaction& sig_trans = *lts.transactions.at(1);
	  if (!sig_trans.request_status_success())
	    {
	      done("could not fetch AWS identity document signature: " + sig_trans.format_status(lts));
	      return;
	    }

	  // get identity document and signature
	  const std::string ident = ident_trans.content_in.to_string();
	  const std::string sig = "-----BEGIN PKCS7-----\n"
	    + sig_trans.content_in.to_string()
	    + "\n-----END PKCS7-----\n";

	  if (debug_level >= 3)
	    {
	      OPENVPN_LOG("IDENT\n" << ident);
	      OPENVPN_LOG("SIG\n" << sig);
	    }

	  // verify signature on identity document
	  {
	    std::list<OpenSSLPKI::X509> certs;
	    if (certs_dir.empty())
	      certs.emplace_back(awscert(), "AWS Cert");
	    else
	      {
		enum_dir(certs_dir, [&certs, certs_dir=certs_dir](const std::string& file) {
		  certs.emplace_back(read_text(certs_dir + "/" + file), "AWS Cert");
		});
	      }
	    OpenSSLSign::verify_pkcs7(certs, sig, ident);
	  }

	  // parse the identity document (JSON)
	  {
	    const std::string title = "identity-document";
	    const Json::Value root = json::parse(ident, title);
	    info.region = json::get_string(root, "region", title);
	    info.instanceId = json::get_string(root, "instanceId", title);
	    info.privateIp = json::get_string(root, "privateIp", title);
	  }

	  if (lookup_product_code)
	    {
	      WS::ClientSet::Transaction& pc_trans = *lts.transactions.at(2);
	      if (pc_trans.request_status_success())
		{
		  const std::string pc = pc_trans.content_in.to_string();
		  queue_pc_validation(pc);
		}
	      else
		done("could not fetch AWS product code: " + pc_trans.format_status(lts));
	    }

	  if (!role_for_credentials.empty())
	    {
	      WS::ClientSet::Transaction& cred_trans = *lts.transactions.at(lookup_product_code ? 3 : 2);
	      if (cred_trans.request_status_success())
		{
		  const std::string creds = cred_trans.content_in.to_string();
		  const Json::Value root = json::parse(creds);
		  info.creds.access_key = json::get_string(root, "AccessKeyId");
		  info.creds.secret_key = json::get_string(root, "SecretAccessKey");
		  info.creds.token = json::get_string(root, "Token");
		  done("");
		}
	      else
		done("could not fetch role credentials: " + cred_trans.format_status(lts));
	    }
	  else
	    done("");
	}
	catch (const std::exception& e)
	  {
	    done(e.what());
	  }
      }

      void queue_pc_validation(const std::string& pc)
      {
	if (debug_level >= 3)
	  OPENVPN_LOG("PRODUCT CODE: " << pc);

	// SSL flags
	unsigned int ssl_flags = SSLConst::ENABLE_CLIENT_SNI;
	if (debug_level >= 1)
	  ssl_flags |= SSLConst::LOG_VERIFY_STATUS;

	// make SSL context using awspc_web_cert() as our CA bundle
	SSLLib::SSLAPI::Config::Ptr ssl(new SSLLib::SSLAPI::Config);
	ssl->set_mode(Mode(Mode::CLIENT));
	ssl->load_ca(awspc_web_cert(), false);
	ssl->set_local_cert_enabled(false);
	ssl->set_tls_version_min(TLSVersion::V1_2);
	ssl->set_remote_cert_tls(KUParse::TLS_WEB_SERVER);
	ssl->set_flags(ssl_flags);
	ssl->set_frame(frame);
	ssl->set_rng(rng);

	// make HTTP context
	WS::Client::Config::Ptr hc(new WS::Client::Config());
	hc->frame = frame;
	hc->ssl_factory = ssl->new_factory();
	hc->user_agent = "PG";
	hc->connect_timeout = 30;
	hc->general_timeout = 60;

	// make host list
	WS::ClientSet::HostRetry::Ptr hr(new WS::ClientSet::HostRetry(
	  "awspc1.openvpn.net",
	  "awspc2.openvpn.net"
	));

	// make transaction set
	WS::ClientSet::TransactionSet::Ptr ts = new WS::ClientSet::TransactionSet;
	ts->host.host = hr->next_host();
	ts->host.port = "443";
	ts->http_config = hc;
	ts->error_recovery = hr;
	ts->max_retries = 5;
	ts->retry_duration = Time::Duration::seconds(5);
	ts->debug_level = debug_level;

	// transaction #1
	{
	  std::unique_ptr<WS::ClientSet::Transaction> t(new WS::ClientSet::Transaction);
	  t->req.uri = "/prod/AwsPC";
	  t->req.method = "POST";
	  t->ci.type = "application/json";
	  t->randomize_resolver_results = true;

	  Json::Value root(Json::objectValue);
	  root["region"] = Json::Value(info.region);
	  root["identityIp"] = Json::Value(info.privateIp);
	  root["host"] = Json::Value(openvpn_io::ip::host_name());
	  root["instanceId"] = Json::Value(info.instanceId);
	  root["productCode"] = Json::Value(pc);
	  root["nonce"] = Json::Value(nonce());
	  const std::string jreq = root.toStyledString();
	  t->content_out.push_back(buf_from_string(jreq));
	  awspc_req = std::move(root);

	  ts->transactions.push_back(std::move(t));

	  if (debug_level >= 3)
	    OPENVPN_LOG("AWSPC REQ\n" << jreq);
	}

	// completion handler
	ts->completion = [self=Ptr(this)](WS::ClientSet::TransactionSet& ts) {
	  self->awspc_query_complete(ts);
	};

	// do the request
	cs->new_request(ts);
      }

      void awspc_query_complete(WS::ClientSet::TransactionSet& ats)
      {
	try {
	  const std::string title = "awspc-reply";

	  // get transactions and check that they succeeded
	  WS::ClientSet::Transaction& trans = *ats.transactions.at(0);
	  if (!trans.request_status_success())
	    {
	      done("awspc server error: " + trans.format_status(ats));
	      return;
	    }

	  // check content-type
	  if (trans.reply.headers.get_value_trim("content-type") != "application/json")
	    {
	      done("expected application/json reply from awspc server");
	      return;
	    }

	  // parse JSON reply
	  const std::string jtxt = trans.content_in.to_string();
	  const Json::Value root = json::parse(jtxt, title);
	  if (debug_level >= 3)
	    OPENVPN_LOG("AWSPC REPLY\n" << root.toStyledString());

	  // check for errors
	  if (json::exists(root, "errorMessage"))
	    {
	      const std::string em = json::get_string(root, "errorMessage", title);
	      const std::string et = json::get_string_optional(root, "errorType", "unspecified-error", title);
	      done(et + " : " + em);
	      return;
	    }

	  // verify consistency of region, instanceId, productCode, and nonce
	  if (!awspc_req_verify_consistency(root))
	    {
	      done("awspc request/reply consistency");
	      return;
	    }

	  // verify reply signature
	  {
	    const std::string line_to_sign = to_string_sig(root);
	    if (debug_level >= 3)
	      OPENVPN_LOG("LINE TO SIGN: " << line_to_sign);
	    const std::string sig = json::get_string(root, "signature", title);
	    const OpenSSLPKI::X509 cert(awspc_signing_cert(), "awspc-cert");
	    OpenSSLSign::verify(cert, sig, line_to_sign, "sha256");
	  }

	  // get concurrent connections
	  info.concurrentConnections = json::get_int(root, "concurrentConnections", title);
	  done("");
	}
	catch (const std::exception& e)
	  {
	    done(e.what());
	  }
      }

      bool awspc_req_verify_consistency(const Json::Value& reply,
					const std::string& key) const
      {
	return json::get_string(reply, key, "awspc-verify-reply") == json::get_string(awspc_req, key, "awspc-verify-request");
      }

      bool awspc_req_verify_consistency(const Json::Value& reply) const
      {
	return awspc_req_verify_consistency(reply, "region")
	  && awspc_req_verify_consistency(reply, "instanceId")
	  && awspc_req_verify_consistency(reply, "productCode")
	  && awspc_req_verify_consistency(reply, "nonce");
      }

      static std::string to_string_sig(const Json::Value& reply)
      {
	const std::string title = "to-string-sig";
	return json::get_string(reply, "region", title)
	  + '/' + json::get_string(reply, "instanceId", title)
	  + '/' + json::get_string(reply, "productCode", title)
	  + '/' + json::get_string(reply, "nonce", title)
	  + '/' + std::to_string(json::get_int(reply, "concurrentConnections", title));
      }

      std::string nonce() const
      {
	unsigned char data[16];
	rng->assert_crypto();
	rng->rand_fill(data);
	return render_hex(data, sizeof(data));
      }

      // The AWS cert for PKCS#7 validation of AWS identity document
      static std::string awscert()
      {
	return std::string(
          "-----BEGIN CERTIFICATE-----\n"
	  "MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw\n"
	  "FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD\n"
	  "VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z\n"
	  "ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u\n"
	  "IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl\n"
	  "cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e\n"
	  "ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3\n"
	  "VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P\n"
	  "hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j\n"
	  "k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U\n"
	  "hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF\n"
	  "lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf\n"
	  "MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW\n"
	  "MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw\n"
	  "vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw\n"
	  "7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K\n"
	  "-----END CERTIFICATE-----\n");
      }

      // The OpenVPN Tech. lambda web cert
      static std::string awspc_web_cert()
      {
	// Go Daddy Root Certificate Authority - G2
	return std::string(
	  "-----BEGIN CERTIFICATE-----\n"
	  "MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT\n"
	  "B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu\n"
	  "MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5\n"
	  "MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6\n"
	  "b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G\n"
	  "A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI\n"
	  "hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq\n"
	  "9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD\n"
	  "+qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd\n"
	  "fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl\n"
	  "NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC\n"
	  "MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9\n"
	  "BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac\n"
	  "vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r\n"
	  "5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV\n"
	  "N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO\n"
	  "LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1\n"
	  "-----END CERTIFICATE-----\n");
      }

      // The OpenVPN Tech. lambda response signing cert
      static std::string awspc_signing_cert()
      {
	return std::string(
	  "-----BEGIN CERTIFICATE-----\n"
	  "MIIDSDCCAjCgAwIBAgIQYadxADonNbu3mPeXR0yYVTANBgkqhkiG9w0BAQsFADAW\n"
	  "MRQwEgYDVQQDEwtBV1MgUEMgUm9vdDAeFw0xNjAzMDExOTU2NTZaFw0yNjAyMjcx\n"
	  "OTU2NTZaMBAxDjAMBgNVBAMTBWF3c3BjMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n"
	  "MIIBCgKCAQEA0ggZoYroOMwDHKCngVOdUKiF6y65LDWmbAwZVqwVI7WYpvOELV34\n"
	  "04ZYtSqPq6IoGFuH6zl0P5rCi674T0oBPSUTmlLwLks+1zrGznboApkr67Mf2dCd\n"
	  "snlyaNPuwrjWHJBa6Pi9dv/YMoJgDxOxk9mslAlcl5xOFgXbfSj1pAA0KVzwwbzz\n"
	  "dnznJL67wCnuiAeqBxbkyarfOL414tepsI24kHoAddAVDdhWQ2WkhrT/vK2IRdGZ\n"
	  "kU5hAAz/qPKkJxebw5uc+cL2TBii2r0Hvg7tEXI9eIEWeoghftsE5YEuaQHP4EVL\n"
	  "JU+21OQzz0lT9L2rrvffTR7cF89Nbn2KMQIDAQABo4GXMIGUMAkGA1UdEwQCMAAw\n"
	  "HQYDVR0OBBYEFAMy6uiElCGZVP/wwJeqvXL7QHTSMEYGA1UdIwQ/MD2AFLDKS6Dk\n"
	  "NtTpQoOPxJi+DRS+GD2CoRqkGDAWMRQwEgYDVQQDEwtBV1MgUEMgUm9vdIIJAOu5\n"
	  "NqrIe040MBMGA1UdJQQMMAoGCCsGAQUFBwMCMAsGA1UdDwQEAwIHgDANBgkqhkiG\n"
	  "9w0BAQsFAAOCAQEAsFhhC9wwybTS2yTYiStATbxHWqnHJRrbMBpqX8FJweS1MM/j\n"
	  "pwr1suTllwTHpqXpqgN6SDzdeG2ZKx8pvJr/dlmD9e+cHguIMTo6TcqPv1MPl3MZ\n"
	  "ugOmDPlgmFYwAWBwzujiGR9bgdGfzw+94KK06iO8MrFLtkz9EbeoJol68mi98CEz\n"
	  "kmOb2BM6tVzkvB9fIYyNkW66ZJs2gXwb6RZTyE9HMMGR67nWKYo9SxpB6f+6hlyU\n"
	  "q7ptxP2Rwmz0u1pRaZdfHmJFOJnPniB7UmMx/t3ftqYWYDXuobr3LVvg7+33WUk0\n"
	  "HfSdbAEkzzC82UTHj0xVH/uZZt8ORChRxuIWZQ==\n"
	  "-----END CERTIFICATE-----\n");
      }

      WS::ClientSet::Ptr cs;
      RandomAPI::Ptr rng;
      Frame::Ptr frame;
      const bool lookup_product_code;
      const int debug_level;
      std::string role_for_credentials;
      std::string certs_dir;

      std::function<void(Info info)> completion;
      Info info;
      Json::Value awspc_req;
      bool pending = false;
    };
  }
}