Add ACME client for globally endorsed TLS certificates (#3877)

Co-authored-by: Amaury Chamayou <amaury@xargs.fr>
Co-authored-by: Maik Riechert <maik.riechert@arcor.de>
This commit is contained in:
Christoph M. Wintersteiger 2022-06-15 18:06:32 +01:00 коммит произвёл GitHub
Родитель f225caac95
Коммит c734789723
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
48 изменённых файлов: 2837 добавлений и 44 удалений

Просмотреть файл

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- The node-to-node interface configuration now supports a `published_address` to enable networks with nodes running in different (virtual) subnets (#3867).
- Added a `GET /node/service/previous_identity` endpoint, which can be used during a recovery to look up the identity of the service before the catastrophic failure (#3880).
- Added an automatic certificate management environment (ACME) client to automatically manage TLS certificates that are globally endorsed by an external authority, e.g. Let's Encrypt (#3877).
### Changed

Просмотреть файл

@ -804,6 +804,13 @@ if(BUILD_TESTS)
CONSENSUS ${CONSENSUS_FILTER}
)
add_e2e_test(
NAME acme_endorsement_test
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/acme_endorsement.py
LABEL ACME
CONSENSUS cft
)
foreach(CONSENSUS ${CONSENSUSES})
add_e2e_test(
NAME vegeta_stress_${CONSENSUS}

Просмотреть файл

@ -279,6 +279,7 @@ target_link_libraries(
${CMAKE_THREAD_LIBS_INIT}
${LINK_LIBCXX}
ccfcrypto.host
http_parser.host
)
if("sgx" IN_LIST COMPILE_TARGETS)
target_link_libraries(cchost PRIVATE openenclave::oehost)

Просмотреть файл

@ -76,9 +76,14 @@
"properties": {
"authority": {
"type": "string",
"enum": ["Node", "Service"],
"enum": ["Node", "Service", "ACME"],
"default": "Service",
"description": "The type of endorsement for the TLS certificate used in client sessions. If the endorsement is not available, client sessions will be terminated, before the TLS handshake is complete. 'Node' means self-signed, 'Service' means service-endorsed."
},
"acme_configuration": {
"type": "string",
"default": "",
"description": "Name of the ACME configuration defined in the network.acme.configurations section"
}
},
"required": ["authority"],
@ -88,6 +93,64 @@
"required": ["bind_address"]
},
"description": "Interfaces to listen on for incoming client TLS connections, as a dictionary from unique interface name to RPC interface information"
},
"acme": {
"type": "object",
"properties": {
"configurations": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"ca_certs": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "Root certificate(s) of the CA to connect to in PEM format (for TLS connections to the CA, e.g. Let's Encrypt's ISRG Root X1)"
},
"directory_url": {
"type": "string",
"default": "",
"description": "URL of the ACME server's directory"
},
"service_dns_name": {
"type": "string",
"default": "",
"description": "DNS name of the service we represent"
},
"contact": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "Contact addresses (see RFC8555 7.3, e.g. mailto:john@example.com)"
},
"terms_of_service_agreed": {
"type": "boolean",
"default": false,
"description": "Indication that the user/operator is aware of the latest terms and conditions for the CA"
},
"challenge_type": {
"type": "string",
"default": "http-01",
"description": "Type of the ACME challenge (currently only http-01 supported)"
}
},
"description": "ACME Configurations",
"additionalProperties": false
}
},
"challenge_server_interface": {
"type": "string",
"default": "0.0.0.0:80",
"description": "Interface for the (http) challenge server to listen on"
}
},
"description": "Configuration for the ACME client(s) to obtain globally valid TLS certificates, e.g. from Let's Encrypt",
"additionalProperties": false
}
},
"description": "This section includes configuration for the interfaces a node listens on (for both client and node-to-node communications)",
@ -111,7 +174,11 @@
"allOf": [
{
"if": {
"properties": { "type": { "const": "Start" } }
"properties": {
"type": {
"const": "Start"
}
}
},
"then": {
"properties": {
@ -193,7 +260,11 @@
},
{
"if": {
"properties": { "type": { "const": "Join" } }
"properties": {
"type": {
"const": "Join"
}
}
},
"then": {
"properties": {
@ -219,7 +290,11 @@
},
{
"if": {
"properties": { "type": { "const": "Recover" } }
"properties": {
"type": {
"const": "Recover"
}
}
},
"then": {
"properties": {

Просмотреть файл

@ -78,3 +78,49 @@ The procedure that operators and members should follow is summarised in the foll
section Service <br> Certificate
Initial Validity Period (24h default): done, 01-01/00:00, 1d
Post Service Open Validity Period : 01-01/15:00, 5d
ACME-endorsed TLS certificates
==================================
Unendorsed, self-signed (CA) service certificates are a complication for clients as they need to be given a copy of the certificate before they can establish TLS connections to the service, or the service certificate is permanently installed in their trust store. To alleviate this, CCF provides an `ACME <https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment>`_ client, which is used to obtain TLS certificates that are endorsed by external certificate authorities. For instance, the `Let's Encrypt <https://letsencrypt.org/>`_ CA is endorsed by a root certificate that is pre-installed on most current operating systems, which means that clients usually have all required certificates to establish TLS connections without further configuration, if the service certificate is endorsed by Let's Encrypt. CCF handles the creation and renewal of ACME certificates, but it requires some configuration:
1. Get a globally reachable DNS name for your CCF network, e.g. ``my-ccf.example.com``, which resolves to the address of at least one node in the network. Multiple nodes or a load balancer address are fine too.
2. ACME `http-01 <https://letsencrypt.org/docs/challenge-types/>`_ challenges require a challenge server to be reachable on port 80 (non-negotiable).
To be able to bind to that port, the ``cchost`` binary may need to be given special permission, e.g. by running ``sudo setcap CAP_NET_BIND_SERVICE=+eip cchost``. Alternatively, port 80 can be redirected to a non-privileged port that ``cchost`` may bind to without special permission.
3. Each interface defined in the ``cchost`` configuration file can be given the name of an ACME configuration to use. The settings of each ACME configuration are defined in ``network.acme``. Note that this information is required by *all* nodes as they might have to renew the certificate(s) later.
The various options are as follows:
.. code-block:: json
"network": {
"rpc_interfaces": {
... ,
"acme_endorsed_interface": { ... , "endorsement": { ... , "acme_configuration": "my-acme-cfg" } }
},
"acme": {
"my-acme-cfg": {
"ca_certs": [ "-----BEGIN CERTIFICATE-----\nMIIBg ..." ],
"directory_url": "https://...",
"service_dns_name": "my-ccf.example.com",
"contact": ["mailto:john@example.com"],
"terms_of_service_agreed": true,
"challenge_type": "http-01",
"challenge_server_interface": "0.0.0.0:80"
}
}
}
- ``ca_certs``: CCF will need to establish https connections with the CA, but does not come with root certificates by default and therefore will fail to establish connections. This setting is populated with one or more such certificates; e.g. for Let's Encrypt this would be their ISRG Root X1 certificate (see `here <https://letsencrypt.org/certificates/>`_) in PEM format.
- ``directory_url``: This is the main entry point for the ACME protocol. For Let's Encrypt's `staging environment <https://letsencrypt.org/docs/staging-environment/>`_, this is ``https://acme-staging-v02.api.letsencrypt.org/directory``; minus the ``-staging`` for their production environment).
- ``service_dns_name``: The DNS name for the network from step 1.
- ``contact``: A list of contact addresses, usually e-mail addresses, which must be prefixed with ``mailto:``. These contacts may receive notifications about service changes, e.g. certificate revocation or expiry.
- ``terms_of_service_agreed``: A Boolean confirming that the operator accepts the terms of service for the CA. RFC8555 requires this to be set explicitly by the operator.
- ``challenge_type``: Currently only `http-01 <https://letsencrypt.org/docs/challenge-types/>`_ is supported.
- ``challenge_server_interface``: Interface for the ACME challenge server to listen on. For http-01 challenges, this must run on port 80.
4. CCF nodes periodically check for certificate expiry and trigger renewal when 66% of the validity period has elapsed. The resulting certificates are stored in the ``ccf.gov.service.acme_certificates`` table and upon an update to this table, nodes will automatically install the corresponding certificate on their interfaces. If necessary, renewal can also be triggered manually by submitting a ``trigger_acme_refresh`` governance proposal.

Просмотреть файл

@ -4,7 +4,8 @@
"Authority": {
"enum": [
"Node",
"Service"
"Service",
"ACME"
],
"type": "string"
},
@ -142,6 +143,9 @@
},
"Endorsement": {
"properties": {
"acme_configuration": {
"$ref": "#/components/schemas/string"
},
"authority": {
"$ref": "#/components/schemas/Authority"
}

Просмотреть файл

@ -14,4 +14,10 @@ namespace crypto
std::string b64_from_raw(const uint8_t* data, size_t size);
std::string b64_from_raw(const std::vector<uint8_t>& data);
std::string b64url_from_raw(
const uint8_t* data, size_t size, bool with_padding = true);
std::string b64url_from_raw(
const std::vector<uint8_t>& data, bool with_padding = true);
}

Просмотреть файл

@ -56,6 +56,11 @@ namespace crypto
return create_csr(subject_name, {});
}
virtual std::vector<uint8_t> create_csr_der(
const std::string& subject_name,
const std::vector<SubjectAltName>& subject_alt_names,
const std::optional<Pem>& public_key = std::nullopt) const = 0;
// Note about the signed_by_issuer parameter to sign_csr: when issuing a new
// certificate for an old subject, which does not exist anymore, we cannot
// sign the CSR with that old subject's private key. Instead, the issuer
@ -128,6 +133,8 @@ namespace crypto
virtual std::vector<uint8_t> public_key_raw() const = 0;
virtual CurveID get_curve_id() const = 0;
virtual PublicKey::Coordinates coordinates() const = 0;
};
using PublicKeyPtr = std::shared_ptr<PublicKey>;

Просмотреть файл

@ -136,5 +136,16 @@ namespace crypto
* The curve ID
*/
virtual CurveID get_curve_id() const = 0;
struct Coordinates
{
std::vector<uint8_t> x;
std::vector<uint8_t> y;
};
/**
* The x/y coordinates of the public key
*/
virtual Coordinates coordinates() const = 0;
};
}

Просмотреть файл

@ -6,6 +6,8 @@
#include "ccf/crypto/pem.h"
#include "ccf/crypto/public_key.h"
#include <chrono>
namespace crypto
{
class Verifier
@ -202,6 +204,15 @@ namespace crypto
/** The validity period of the certificate */
virtual std::pair<std::string, std::string> validity_period() const = 0;
/** The number of seconds of the validity period of the
* certificate remaining */
virtual size_t remaining_seconds(
const std::chrono::system_clock::time_point& now) const = 0;
/** The percentage of the validity period of the certificate remaining */
virtual double remaining_percentage(
const std::chrono::system_clock::time_point& now) const = 0;
/** The subject name of the certificate */
virtual std::string subject() const = 0;
};

Просмотреть файл

@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once
#include "ccf/ds/json.h"
#include <optional>
#include <string>
#include <vector>
namespace ccf
{
struct ACMEClientConfig
{
// Root certificate(s) of the CA to connect to in PEM format (for TLS
// connections to the CA, e.g. Let's Encrypt's ISRG Root X1)
std::vector<std::string> ca_certs;
// URL of the ACME server's directory
std::string directory_url;
// DNS name of the service we represent
std::string service_dns_name;
// Contact addresses (see RFC8555 7.3, e.g. mailto:john@example.com)
std::vector<std::string> contact;
// Indication that the user/operator is aware of the latest terms and
// conditions for the CA
bool terms_of_service_agreed = false;
// Type of the ACME challenge (currently only http-01 supported)
std::string challenge_type = "http-01";
// Validity range (Note: not supported by Let's Encrypt)
std::optional<std::string> not_before;
std::optional<std::string> not_after;
bool operator==(const ACMEClientConfig& other) const = default;
};
DECLARE_JSON_TYPE(ACMEClientConfig);
DECLARE_JSON_REQUIRED_FIELDS(
ACMEClientConfig,
ca_certs,
directory_url,
service_dns_name,
contact,
terms_of_service_agreed,
challenge_type);
DECLARE_JSON_OPTIONAL_FIELDS(ACMEClientConfig, not_before, not_after);
}

Просмотреть файл

@ -5,6 +5,7 @@
#include "ccf/ds/json.h"
#include "ccf/ds/nonstd.h"
#include "ccf/service/acme_client_config.h"
#include <string>
@ -13,22 +14,30 @@ namespace ccf
enum class Authority
{
NODE,
SERVICE
SERVICE,
ACME
};
DECLARE_JSON_ENUM(
Authority, {{Authority::NODE, "Node"}, {Authority::SERVICE, "Service"}});
Authority,
{{Authority::NODE, "Node"},
{Authority::SERVICE, "Service"},
{Authority::ACME, "ACME"}});
struct Endorsement
{
Authority authority;
std::optional<std::string> acme_configuration;
bool operator==(const Endorsement& other) const
{
return authority == other.authority;
return authority == other.authority &&
acme_configuration == other.acme_configuration;
}
};
DECLARE_JSON_TYPE(Endorsement);
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Endorsement);
DECLARE_JSON_REQUIRED_FIELDS(Endorsement, authority);
DECLARE_JSON_OPTIONAL_FIELDS(Endorsement, acme_configuration);
struct NodeInfoNetwork_v1
{
@ -77,6 +86,16 @@ namespace ccf
NetInterface node_to_node_interface;
RpcInterfaces rpc_interfaces;
struct ACME
{
std::map<std::string, ccf::ACMEClientConfig> configurations;
std::string challenge_server_interface;
bool operator==(const ACME&) const = default;
};
std::optional<ACME> acme;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(NodeInfoNetwork_v2::NetInterface);
DECLARE_JSON_REQUIRED_FIELDS(NodeInfoNetwork_v2::NetInterface, bind_address);
@ -87,9 +106,13 @@ namespace ccf
max_open_sessions_hard,
published_address,
protocol);
DECLARE_JSON_TYPE(NodeInfoNetwork_v2);
DECLARE_JSON_TYPE(NodeInfoNetwork_v2::ACME);
DECLARE_JSON_REQUIRED_FIELDS(
NodeInfoNetwork_v2::ACME, configurations, challenge_server_interface);
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(NodeInfoNetwork_v2);
DECLARE_JSON_REQUIRED_FIELDS(
NodeInfoNetwork_v2, node_to_node_interface, rpc_interfaces);
DECLARE_JSON_OPTIONAL_FIELDS(NodeInfoNetwork_v2, acme);
struct NodeInfoNetwork : public NodeInfoNetwork_v2
{
@ -192,6 +215,10 @@ struct formatter<ccf::Authority>
{
return format_to(ctx.out(), "Service");
}
case (ccf::Authority::ACME):
{
return format_to(ctx.out(), "ACME");
}
}
}
};

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once
#include "ccf/crypto/pem.h"
#include "ccf/ds/json.h"
#include "ccf/service/map.h"
namespace ccf
{
// Maps each interface name to a certificate
using ACMECertificates = ServiceMap<std::string, crypto::Pem>;
namespace Tables
{
static constexpr auto ACME_CERTIFICATES =
"public:ccf.gov.service.acme_certificates";
}
}

Просмотреть файл

@ -1203,4 +1203,19 @@ const actions = new Map([
}
),
],
[
"trigger_acme_refresh",
new Action(
function (args) {
checkType(
args.interfaces,
"array?",
"interfaces to refresh the certificates for"
);
},
function (args, proposalId) {
ccf.node.triggerACMERefresh(args.interfaces);
}
),
],
]);

Просмотреть файл

@ -44,4 +44,39 @@ namespace crypto
{
return b64_from_raw(data.data(), data.size());
}
std::string b64url_from_raw(
const uint8_t* data, size_t size, bool with_padding)
{
auto r = Base64Impl::b64_from_raw(data, size);
for (size_t i = 0; i < r.size(); i++)
{
switch (r[i])
{
case '+':
r[i] = '-';
break;
case '/':
r[i] = '_';
break;
}
}
if (!with_padding)
{
while (r.ends_with('='))
{
r.pop_back();
}
}
return r;
}
std::string b64url_from_raw(
const std::vector<uint8_t>& data, bool with_padding)
{
return b64url_from_raw(data.data(), data.size(), with_padding);
}
}

Просмотреть файл

@ -48,9 +48,13 @@ namespace crypto
EVP_PKEY_paramgen_init(pkctx) < 0 ||
EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pkctx, curve_nid) < 0 ||
EVP_PKEY_CTX_set_ec_param_enc(pkctx, OPENSSL_EC_NAMED_CURVE) < 0)
{
throw std::runtime_error("could not initialize PK context");
}
if (EVP_PKEY_keygen_init(pkctx) < 0 || EVP_PKEY_keygen(pkctx, &key) < 0)
{
throw std::runtime_error("could not generate new EC key");
}
}
KeyPair_OpenSSL::KeyPair_OpenSSL(const Pem& pem)
@ -161,7 +165,7 @@ namespace crypto
return 0;
}
Pem KeyPair_OpenSSL::create_csr(
Unique_X509_REQ KeyPair_OpenSSL::create_req(
const std::string& subject_name,
const std::vector<SubjectAltName>& subject_alt_names,
const std::optional<Pem>& public_key) const
@ -215,6 +219,17 @@ namespace crypto
if (key)
OpenSSL::CHECK1(X509_REQ_sign(req, key, EVP_sha512()));
return req;
}
Pem KeyPair_OpenSSL::create_csr(
const std::string& subject_name,
const std::vector<SubjectAltName>& subject_alt_names,
const std::optional<Pem>& public_key) const
{
Unique_X509_REQ req =
create_req(subject_name, subject_alt_names, public_key);
Unique_BIO mem;
OpenSSL::CHECK1(PEM_write_bio_X509_REQ(mem, req));
@ -225,6 +240,25 @@ namespace crypto
return result;
}
std::vector<uint8_t> KeyPair_OpenSSL::create_csr_der(
const std::string& subject_name,
const std::vector<SubjectAltName>& subject_alt_names,
const std::optional<Pem>& public_key) const
{
Unique_X509_REQ req =
create_req(subject_name, subject_alt_names, public_key);
Unique_BIO mem;
CHECK1(i2d_X509_REQ_bio(mem, req));
BUF_MEM* bptr;
BIO_get_mem_ptr(mem, &bptr);
std::vector<uint8_t> result(
(uint8_t*)bptr->data, (uint8_t*)bptr->data + bptr->length);
return result;
}
Pem KeyPair_OpenSSL::sign_csr_impl(
const std::optional<Pem>& issuer_cert,
const Pem& signing_request,
@ -393,4 +427,9 @@ namespace crypto
return shared_secret;
}
PublicKey::Coordinates KeyPair_OpenSSL::coordinates() const
{
return PublicKey_OpenSSL::coordinates();
}
}

Просмотреть файл

@ -61,6 +61,11 @@ namespace crypto
const std::vector<SubjectAltName>& subject_alt_names,
const std::optional<Pem>& public_key = std::nullopt) const override;
virtual std::vector<uint8_t> create_csr_der(
const std::string& subject_name,
const std::vector<SubjectAltName>& subject_alt_names,
const std::optional<Pem>& public_key = std::nullopt) const override;
virtual Pem sign_csr_impl(
const std::optional<Pem>& issuer_cert,
const Pem& signing_request,
@ -75,5 +80,13 @@ namespace crypto
virtual CurveID get_curve_id() const override;
virtual std::vector<uint8_t> public_key_raw() const override;
virtual PublicKey::Coordinates coordinates() const override;
protected:
OpenSSL::Unique_X509_REQ create_req(
const std::string& subject_name,
const std::vector<SubjectAltName>& subject_alt_names,
const std::optional<Pem>& public_key) const;
};
}

Просмотреть файл

@ -318,10 +318,13 @@ namespace crypto
struct Unique_EC_POINT
: public Unique_SSL_OBJECT<EC_POINT, nullptr, nullptr>
{
Unique_EC_POINT(EC_GROUP* group) :
Unique_EC_POINT(const EC_GROUP* group) :
Unique_SSL_OBJECT(
EC_POINT_new(group), EC_POINT_free, /*check_null=*/true)
{}
Unique_EC_POINT(EC_POINT* point) :
Unique_SSL_OBJECT(point, EC_POINT_free, /*check_null=*/true)
{}
};
struct Unique_EC_KEY : public Unique_SSL_OBJECT<EC_KEY, nullptr, nullptr>
@ -330,6 +333,9 @@ namespace crypto
Unique_SSL_OBJECT(
EC_KEY_new_by_curve_name(nid), EC_KEY_free, /*check_null=*/true)
{}
Unique_EC_KEY(EC_KEY* key) :
Unique_SSL_OBJECT(key, EC_KEY_free, /*check_null=*/true)
{}
};
struct Unique_EVP_ENCODE_CTX : public Unique_SSL_OBJECT<

Просмотреть файл

@ -29,7 +29,9 @@ namespace crypto
Unique_BIO mem(pem);
key = PEM_read_bio_PUBKEY(mem, NULL, NULL, NULL);
if (!key)
{
throw std::runtime_error("could not parse PEM");
}
}
PublicKey_OpenSSL::PublicKey_OpenSSL(const std::vector<uint8_t>& der)
@ -47,7 +49,9 @@ namespace crypto
PublicKey_OpenSSL::~PublicKey_OpenSSL()
{
if (key)
{
EVP_PKEY_free(key);
}
}
CurveID PublicKey_OpenSSL::get_curve_id() const
@ -195,4 +199,21 @@ namespace crypto
EVP_PKEY_up_ref(pk);
return pk;
}
PublicKey::Coordinates PublicKey_OpenSSL::coordinates() const
{
Unique_EC_KEY eckey(EVP_PKEY_get1_EC_KEY(key));
const EC_POINT* p = EC_KEY_get0_public_key(eckey);
Unique_EC_GROUP group(get_openssl_group_id());
Unique_BN_CTX bn_ctx;
Unique_BIGNUM x, y;
CHECK1(EC_POINT_get_affine_coordinates(group, p, x, y, bn_ctx));
Coordinates r;
int sz = EC_GROUP_get_degree(group) / 8;
r.x.resize(sz);
r.y.resize(sz);
BN_bn2binpad(x, r.x.data(), sz);
BN_bn2binpad(y, r.y.data(), sz);
return r;
}
}

Просмотреть файл

@ -56,6 +56,8 @@ namespace crypto
{
return key;
}
virtual Coordinates coordinates() const override;
};
OpenSSL::Unique_PKEY key_from_raw_ec_point(

Просмотреть файл

@ -180,4 +180,29 @@ namespace crypto
BIO_get_mem_ptr(mem, &bptr);
return std::string(bptr->data, bptr->length);
}
size_t Verifier_OpenSSL::remaining_seconds(
const std::chrono::system_clock::time_point& now) const
{
auto [from, to] = validity_period();
auto tp_to = ds::time_point_from_string(to);
return std::chrono::duration_cast<std::chrono::seconds>(tp_to - now)
.count() +
1;
}
double Verifier_OpenSSL::remaining_percentage(
const std::chrono::system_clock::time_point& now) const
{
auto [from, to] = validity_period();
auto tp_from = ds::time_point_from_string(from);
auto tp_to = ds::time_point_from_string(to);
auto total_sec =
std::chrono::duration_cast<std::chrono::seconds>(tp_to - tp_from)
.count() +
1;
auto rem_sec =
std::chrono::duration_cast<std::chrono::seconds>(tp_to - now).count() + 1;
return rem_sec / (double)total_sec;
}
}

Просмотреть файл

@ -5,6 +5,7 @@
#include "ccf/crypto/verifier.h"
#include "crypto/openssl/openssl_wrappers.h"
#include <chrono>
#include <openssl/x509.h>
namespace crypto
@ -37,6 +38,12 @@ namespace crypto
virtual std::pair<std::string, std::string> validity_period()
const override;
virtual size_t remaining_seconds(
const std::chrono::system_clock::time_point& now) const override;
virtual double remaining_percentage(
const std::chrono::system_clock::time_point& now) const override;
virtual std::string subject() const override;
};
}

Просмотреть файл

@ -2,6 +2,8 @@
// Licensed under the Apache 2.0 License.
#pragma once
#include <sys/socket.h>
/**
* @brief Pending writes on both host and enclave, with data, length and
* destination address.

Просмотреть файл

@ -9,7 +9,7 @@ namespace ccf
{
class ClientEndpoint
{
protected:
public:
using HandleDataCallback = std::function<void(
http_status status,
http::HeaderMap&& headers,
@ -18,6 +18,7 @@ namespace ccf
using HandleErrorCallback =
std::function<void(const std::string& error_msg)>;
protected:
HandleDataCallback handle_data_cb;
HandleErrorCallback handle_error_cb;

Просмотреть файл

@ -389,6 +389,24 @@ namespace ccf
}
});
DISPATCHER_SET_MESSAGE_HANDLER(
bp.get_dispatcher(),
ACMEMessage::acme_challenge_response_ack,
[this](const uint8_t* data, size_t size) {
try
{
auto [token] = ringbuffer::read_message<
ACMEMessage::acme_challenge_response_ack>(data, size);
node->acme_challenge_response_ack(token);
}
catch (const std::exception& ex)
{
LOG_FAIL_FMT(
"ACME: acme_challenge_response_ack handler failed: {}",
ex.what());
}
});
rpcsessions->register_message_handlers(bp.get_dispatcher());
if (start_type == StartType::Join)

Просмотреть файл

@ -61,3 +61,17 @@ struct LaunchHostProcessMessage
DECLARE_JSON_TYPE(LaunchHostProcessMessage);
DECLARE_JSON_REQUIRED_FIELDS(LaunchHostProcessMessage, args);
// ACME
enum ACMEMessage : ringbuffer::Message
{
DEFINE_RINGBUFFER_MSG_TYPE(acme_challenge_response),
DEFINE_RINGBUFFER_MSG_TYPE(acme_challenge_response_ack),
DEFINE_RINGBUFFER_MSG_TYPE(acme_challenge_server_stop),
};
DECLARE_RINGBUFFER_MESSAGE_PAYLOAD(
ACMEMessage::acme_challenge_response, std::string);
DECLARE_RINGBUFFER_MESSAGE_PAYLOAD(
ACMEMessage::acme_challenge_response_ack, std::string);
DECLARE_RINGBUFFER_MESSAGE_NO_PAYLOAD(ACMEMessage::acme_challenge_server_stop);

Просмотреть файл

@ -29,8 +29,8 @@ namespace ccf
static constexpr size_t max_open_sessions_soft_default = 1000;
static constexpr size_t max_open_sessions_hard_default = 1010;
static constexpr ccf::Endorsement endorsement_default =
ccf::Endorsement{ccf::Authority::SERVICE};
static const ccf::Endorsement endorsement_default = {
ccf::Authority::SERVICE, std::nullopt};
class RPCSessions : public std::enable_shared_from_this<RPCSessions>,
public AbstractRPCResponder,
@ -231,7 +231,10 @@ namespace ccf
}
void set_cert(
ccf::Authority authority, const crypto::Pem& cert_, const crypto::Pem& pk)
ccf::Authority authority,
const crypto::Pem& cert_,
const crypto::Pem& pk,
const std::string& acme_configuration = "")
{
// Caller authentication is done by each frontend by looking up
// the caller's certificate in the relevant store table. The caller
@ -246,7 +249,13 @@ namespace ccf
{
if (interface.endorsement.authority == authority)
{
certs.insert_or_assign(listen_interface_id, cert);
if (
interface.endorsement.authority != Authority::ACME ||
(interface.endorsement.acme_configuration &&
*interface.endorsement.acme_configuration == acme_configuration))
{
certs.insert_or_assign(listen_interface_id, cert);
}
}
}
}

Просмотреть файл

@ -0,0 +1,282 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once
#include "ccf/ds/hex.h"
#include "ccf/ds/logger.h"
#include "ccf/http_status.h"
#include "ccf/json_handler.h"
#include "ds/cli_helper.h"
#include "ds/ring_buffer_types.h"
#include "enclave/interface.h"
#include "host/socket.h"
#include "http/http_builder.h"
#include "http/http_endpoint.h"
#include "http/http_parser.h"
#include "tcp.h"
#include <cstddef>
#include <fmt/format.h>
#include <memory>
#include <stdexcept>
#include <uv.h>
class ACMEConnectionTracker
{
public:
virtual void add(asynchost::TCP& peer) = 0;
};
class ACMEServerBehaviour : public asynchost::SocketBehaviour<asynchost::TCP>
{
protected:
class ClientBehaviour : public asynchost::SocketBehaviour<asynchost::TCP>
{
public:
ClientBehaviour(
asynchost::TCP socket,
std::mutex& lock,
const std::map<std::string, std::string>& prepared_responses,
ringbuffer::WriterPtr to_enclave) :
asynchost::SocketBehaviour<asynchost::TCP>("", ""),
parser(sp),
socket(socket),
lock(lock),
prepared_responses(prepared_responses),
to_enclave(to_enclave)
{}
virtual ~ClientBehaviour() = default;
void reply(http::Response& r, const std::string& body)
{
if (!socket.is_null())
{
r.set_body(body);
auto bytes = r.build_response();
socket->write(bytes.size(), bytes.data());
}
}
virtual void on_read(size_t len, uint8_t*& incoming, sockaddr sa) override
{
try
{
parser.execute(incoming, len);
while (!sp.received.empty())
{
auto req = sp.received.front();
sp.received.pop();
// We serve only
// http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>
if (req.url.find("/.well-known/acme-challenge/") != 0)
{
std::string content((char*)incoming, len);
LOG_INFO_FMT(
"ACME: invalid request from {} for url={} with following "
"body:\n{}",
socket->get_peer_name(),
req.url,
content);
http::Response r(HTTP_STATUS_NOT_FOUND);
reply(r, "Not found");
}
else
{
auto token = req.url.substr(req.url.rfind('/') + 1);
if (token.empty())
{
throw std::runtime_error(fmt::format(
"Missing ACME token in {} (requested by {})",
req.url,
socket->get_peer_name()));
}
{
std::unique_lock<std::mutex> guard(lock);
std::string response;
auto tit = prepared_responses.find(token);
if (tit == prepared_responses.end())
{
auto prit = prepared_responses.find("");
if (prit != prepared_responses.end())
{
LOG_TRACE_FMT("ACME: using blanket response");
response = token + "." + prit->second;
}
else
{
LOG_DEBUG_FMT(
"ACME: challenge response for token '{}' not found "
"(requested "
"by {})",
token,
socket->get_peer_name());
http::Response r(HTTP_STATUS_NOT_FOUND);
reply(
r, fmt::format("No response for token '{}' found", token));
}
}
else
{
response = tit->second;
}
auto rbody = fmt::format("{}.{}", token, response);
http::Response r(HTTP_STATUS_OK);
r.set_header("Content-Type", "application/octet-stream");
reply(r, rbody);
LOG_DEBUG_FMT(
"ACME: challenge response for token '{}' provided to {}",
token,
socket->get_peer_name());
}
}
}
}
catch (const std::exception& ex)
{
LOG_INFO_FMT("HTTP handling failed with: {}", ex.what());
http::Response r(HTTP_STATUS_BAD_REQUEST);
reply(r, "Bad request");
}
}
protected:
http::SimpleRequestProcessor sp;
http::RequestParser parser;
asynchost::TCP socket;
std::mutex& lock;
const std::map<std::string, std::string>& prepared_responses;
ringbuffer::WriterPtr to_enclave;
};
public:
ACMEServerBehaviour(
ACMEConnectionTracker& tracker,
std::mutex& lock,
const std::map<std::string, std::string>& prepared_responses,
ringbuffer::WriterPtr to_enclave) :
asynchost::SocketBehaviour<asynchost::TCP>("", ""),
tracker(tracker),
lock(lock),
prepared_responses(prepared_responses),
to_enclave(to_enclave)
{}
void on_listening(
const std::string& host, const std::string& service) override
{
LOG_DEBUG_FMT("ACME: challenge server listening on {}:{}", host, service);
}
void on_accept(asynchost::TCP& peer) override
{
peer->set_behaviour(std::make_unique<ClientBehaviour>(
peer, lock, prepared_responses, to_enclave));
tracker.add(peer);
}
ACMEConnectionTracker& tracker;
std::mutex& lock;
const std::map<std::string, std::string>& prepared_responses;
ringbuffer::WriterPtr to_enclave;
};
class ACMEChallengeServer : public ACMEConnectionTracker
{
public:
ACMEChallengeServer(
const std::string& interface,
messaging::Dispatcher<ringbuffer::Message>& disp,
ringbuffer::AbstractWriterFactory& writer_factory) :
listener(nullptr),
to_enclave(writer_factory.create_writer_to_inside())
{
auto iface = cli::validate_address(interface, "80");
host = iface.first;
port = iface.second;
DISPATCHER_SET_MESSAGE_HANDLER(
disp,
ACMEMessage::acme_challenge_response,
[this](const uint8_t* data, size_t size) {
try
{
auto [response] =
ringbuffer::read_message<ACMEMessage::acme_challenge_response>(
data, size);
auto dotidx = response.find(".");
std::string token, token_response;
if (dotidx != std::string::npos)
{
token = response.substr(0, dotidx);
token_response = response.substr(dotidx + 1);
}
else
{
token = "";
token_response = response;
}
LOG_TRACE_FMT(
"ACME: challenge server received response for token '{}' ({})",
token,
token_response);
{
std::unique_lock<std::mutex> guard(lock);
prepared_responses.emplace(token, token_response);
}
if (listener.is_null())
{
listener = asynchost::TCP();
listener->set_behaviour(std::make_unique<ACMEServerBehaviour>(
*this, lock, prepared_responses, to_enclave));
listener->listen(host, port);
}
RINGBUFFER_WRITE_MESSAGE(
ACMEMessage::acme_challenge_response_ack, to_enclave, token);
}
catch (const std::exception& ex)
{
LOG_FAIL_FMT(
"ACME: acme_challenge_response message handler failed: {}",
ex.what());
}
});
DISPATCHER_SET_MESSAGE_HANDLER(
disp,
ACMEMessage::acme_challenge_server_stop,
[this](const uint8_t* data, size_t size) {
listener = nullptr;
prepared_responses.clear();
LOG_DEBUG_FMT("ACME: challenge server stopped");
});
}
virtual ~ACMEChallengeServer() = default;
virtual void add(asynchost::TCP& peer) override
{
sockets.emplace(sockets.size(), peer);
}
protected:
std::string host, port;
asynchost::TCP listener;
std::unordered_map<size_t, asynchost::TCP> sockets;
std::mutex lock;
std::map<std::string, std::string> prepared_responses;
ringbuffer::WriterPtr to_enclave;
};

Просмотреть файл

@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#include "acme_challenge_server.h"
#include "ccf/ds/logger.h"
#include "ccf/version.h"
#include "config_schema.h"
@ -526,6 +527,11 @@ int main(int argc, char** argv)
#endif
}
if (config.network.acme)
{
startup_config.network.acme = config.network.acme;
}
LOG_INFO_FMT("Initialising enclave: enclave_create_node");
std::atomic<bool> ecall_completed = false;
auto flush_outbound = [&]() {
@ -606,6 +612,17 @@ int main(int argc, char** argv)
threads.emplace_back(std::thread(enclave_thread_start));
}
std::unique_ptr<ACMEChallengeServer> acs;
if (
config.network.acme &&
!config.network.acme->challenge_server_interface.empty())
{
acs = std::make_unique<ACMEChallengeServer>(
config.network.acme->challenge_server_interface,
bp.get_dispatcher(),
writer_factory);
}
LOG_INFO_FMT("Entering event loop");
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
LOG_INFO_FMT("Exited event loop");

Просмотреть файл

@ -50,7 +50,7 @@ namespace asynchost
}
virtual void on_disconnect()
{
LOG_INFO_FMT("{} {} disconnected", conn_name, name);
LOG_TRACE_FMT("{} {} disconnected", conn_name, name);
}
/// Failure loggers for when things go wrong, but not fatal

Просмотреть файл

@ -9,6 +9,7 @@
#include "proxy.h"
#include "socket.h"
#include <netinet/in.h>
#include <optional>
namespace asynchost
@ -142,6 +143,36 @@ namespace asynchost
return port;
}
std::string get_peer_name() const
{
sockaddr_storage sa = {};
int name_len = sizeof(sa);
if (uv_tcp_getpeername(&uv_handle, (sockaddr*)&sa, &name_len) < 0)
{
LOG_FAIL_FMT("uv_tcp_getpeername failed");
return "";
}
switch (sa.ss_family)
{
case AF_INET:
{
char tmp[INET_ADDRSTRLEN];
sockaddr_in* sa4 = (sockaddr_in*)&sa;
uv_ip4_name(sa4, tmp, sizeof(tmp));
return tmp;
}
case AF_INET6:
{
char tmp[INET6_ADDRSTRLEN];
sockaddr_in6* sa6 = (sockaddr_in6*)&sa;
uv_ip6_name(sa6, tmp, sizeof(tmp));
return tmp;
}
default:
return fmt::format("unknown family: {}", sa.ss_family);
}
}
std::optional<std::string> get_listen_name() const
{
return listen_name;

Просмотреть файл

@ -91,6 +91,15 @@ namespace http
headers[headers::CONTENT_LENGTH] =
fmt::format("{}", get_content_length());
}
void set_body(const std::string& s)
{
body = (uint8_t*)s.data();
body_size = s.size();
headers[headers::CONTENT_LENGTH] =
fmt::format("{}", get_content_length());
}
};
class Request : public Message

Просмотреть файл

@ -1076,25 +1076,17 @@ namespace ccf::js
return JS_UNDEFINED;
}
JSValue js_node_trigger_host_process_launch(
JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv)
JSValue get_string_array(
JSContext* ctx, JSValueConst& argv, std::vector<std::string>& out)
{
js::Context& jsctx = *(js::Context*)JS_GetContextOpaque(ctx);
if (argc != 1)
{
return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 1", argc);
}
auto args = JSWrappedValue(ctx, argv[0]);
auto args = JSWrappedValue(ctx, argv);
if (!JS_IsArray(ctx, args))
{
return JS_ThrowTypeError(ctx, "First argument must be an array");
}
std::vector<std::string> process_args;
auto len_atom = JS_NewAtom(ctx, "length");
auto len_val = args.get_property(len_atom);
JS_FreeAtom(ctx, len_atom);
@ -1115,8 +1107,76 @@ namespace ccf::js
return JS_ThrowTypeError(
ctx, "First argument must be an array of strings, found non-string");
}
auto arg = jsctx.to_str(arg_val);
process_args.push_back(*arg);
out.push_back(*jsctx.to_str(arg_val));
}
return JS_UNDEFINED;
}
JSValue js_trigger_acme_refresh(
JSContext* ctx,
JSValueConst this_val,
[[maybe_unused]] int argc,
[[maybe_unused]] JSValueConst* argv)
{
js::Context& jsctx = *(js::Context*)JS_GetContextOpaque(ctx);
auto gov_effects = static_cast<ccf::AbstractGovernanceEffects*>(
JS_GetOpaque(this_val, node_class_id));
auto global_obj = jsctx.get_global_obj();
auto ccf = global_obj["ccf"];
auto kv = ccf["kv"];
auto tx_ctx_ptr = static_cast<TxContext*>(JS_GetOpaque(kv, kv_class_id));
if (tx_ctx_ptr->tx == nullptr)
{
return JS_ThrowInternalError(ctx, "No transaction available");
}
try
{
std::optional<std::vector<std::string>> opt_interfaces = std::nullopt;
if (argc > 0)
{
std::vector<std::string> interfaces;
JSValue r = get_string_array(ctx, argv[0], interfaces);
if (!JS_IsUndefined(r))
{
return r;
}
opt_interfaces = interfaces;
}
gov_effects->trigger_acme_refresh(*tx_ctx_ptr->tx, opt_interfaces);
}
catch (const std::exception& e)
{
LOG_FAIL_FMT("Unable to request snapshot: {}", e.what());
}
return JS_UNDEFINED;
}
JSValue js_node_trigger_host_process_launch(
JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv)
{
js::Context& jsctx = *(js::Context*)JS_GetContextOpaque(ctx);
if (argc != 1)
{
return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 1", argc);
}
std::vector<std::string> process_args;
JSValue r = get_string_array(ctx, argv[0], process_args);
if (!JS_IsUndefined(r))
{
return r;
}
auto host_processes = static_cast<ccf::AbstractHostProcesses*>(
@ -1672,6 +1732,11 @@ namespace ccf::js
node,
"triggerSnapshot",
JS_NewCFunction(ctx, js_trigger_snapshot, "triggerSnapshot", 0));
JS_SetPropertyStr(
ctx,
node,
"triggerACMERefresh",
JS_NewCFunction(ctx, js_trigger_acme_refresh, "triggerACMERefresh", 0));
}
if (host_processes != nullptr)

1184
src/node/acme_client.h Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

142
src/node/ccf_acme_client.h Normal file
Просмотреть файл

@ -0,0 +1,142 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once
#include "ccf/ds/json.h"
#include "ccf/service/acme_client_config.h"
#include "ccf/service/tables/acme_certificates.h"
#include "enclave/interface.h"
#include "enclave/rpc_sessions.h"
#include "node/acme_client.h"
#include "service/network_tables.h"
#include <chrono>
namespace ccf
{
namespace
{
static inline ACME::ClientConfig get_client_config(
const ACMEClientConfig& cfg)
{
return {
cfg.ca_certs,
cfg.directory_url,
cfg.service_dns_name,
cfg.contact,
cfg.terms_of_service_agreed,
cfg.challenge_type,
cfg.not_before,
cfg.not_after};
}
}
class ACMEClient : public ACME::Client
{
public:
ACMEClient(
const std::string& config_name,
const ACMEClientConfig& config,
std::shared_ptr<RPCSessions> rpc_sessions,
std::shared_ptr<kv::Store> store,
ringbuffer::WriterPtr to_host,
std::shared_ptr<crypto::KeyPair> account_key_pair = nullptr) :
ACME::Client(get_client_config(config), account_key_pair),
config_name(config_name),
rpc_sessions(rpc_sessions),
store(store),
to_host(to_host)
{}
virtual ~ACMEClient() {}
virtual void set_account_key(
std::shared_ptr<crypto::KeyPair> new_account_key_pair) override
{
ACME::Client::set_account_key(new_account_key_pair);
install_wildcard_response();
}
virtual void check_expiry(
std::shared_ptr<kv::Store> tables,
std::unique_ptr<NetworkIdentity>& identity)
{
auto now = std::chrono::system_clock::now();
bool renew = false;
auto tx = tables->create_read_only_tx();
auto certs = tx.ro<ACMECertificates>(Tables::ACME_CERTIFICATES);
auto cert = certs->get(config_name);
if (cert)
{
auto v = crypto::make_verifier(*cert);
double rem_pct = v->remaining_percentage(now);
LOG_TRACE_FMT(
"ACME: remaining certificate for '{}' validity: {}%, "
"{} "
"seconds",
config_name,
100.0 * rem_pct,
v->remaining_seconds(now));
renew = rem_pct < 0.33;
}
if (renew || !cert)
{
get_certificate(make_key_pair(identity->priv_key));
}
}
protected:
std::string config_name;
std::shared_ptr<RPCSessions> rpc_sessions;
std::shared_ptr<kv::Store> store;
ringbuffer::WriterPtr to_host;
void install_wildcard_response()
{
// Register a wildcard-response for all challenge tokens. If we use a
// shared account key, we can use this response on all nodes without
// further communication.
on_challenge(make_challenge_response());
}
virtual void on_http_request(
const http::URL& url,
std::vector<uint8_t>&& req,
std::function<
bool(http_status status, http::HeaderMap&&, std::vector<uint8_t>&&)>
callback) override
{
auto ca = std::make_shared<tls::CA>(config.ca_certs, true);
auto ca_cert = std::make_shared<tls::Cert>(ca);
auto client = rpc_sessions->create_client(ca_cert);
client->connect(
url.host,
url.port,
[callback](
http_status status,
http::HeaderMap&& headers,
std::vector<uint8_t>&& data) {
return callback(status, std::move(headers), std::move(data));
});
client->send_request(std::move(req));
}
virtual void on_challenge(const std::string& key_authorization) override
{
RINGBUFFER_WRITE_MESSAGE(
ACMEMessage::acme_challenge_response, to_host, key_authorization);
}
virtual void on_certificate(const std::string& certificate) override
{
// Write the endorsed certificate to the certificate table; all nodes
// will install it later, in the global hook on that table.
auto tx = store->create_tx();
auto certs = tx.rw<ACMECertificates>(Tables::ACME_CERTIFICATES);
certs->put(config_name, crypto::Pem(certificate));
tx.commit();
}
};
}

Просмотреть файл

@ -5,9 +5,12 @@
#include "ccf/crypto/entropy.h"
#include "ccf/crypto/pem.h"
#include "ccf/crypto/symmetric_key.h"
#include "ccf/crypto/verifier.h"
#include "ccf/ds/logger.h"
#include "ccf/serdes.h"
#include "ccf/service/tables/acme_certificates.h"
#include "ccf/service/tables/service.h"
#include "ccf_acme_client.h"
#include "consensus/aft/raft.h"
#include "consensus/ledger_enclave.h"
#include "crypto/certs.h"
@ -196,6 +199,10 @@ namespace ccf
// the lifetime of the node
kv::Version startup_seqno = 0;
// ACME certificate endorsement client
std::map<std::string, std::shared_ptr<ACMEClient>> acme_clients;
size_t num_acme_interfaces = 0;
std::shared_ptr<kv::AbstractTxEncryptor> make_encryptor()
{
#ifdef USE_NULL_ENCRYPTOR
@ -360,6 +367,8 @@ namespace ccf
setup_snapshotter();
setup_encryptor();
setup_acme_clients();
switch (start_type)
{
case StartType::Start:
@ -469,6 +478,7 @@ namespace ccf
auto network_ca = std::make_shared<tls::CA>(std::string(
config.join.service_cert.begin(), config.join.service_cert.end()));
auto join_client_cert = std::make_unique<tls::Cert>(
network_ca,
self_signed_node_cert,
@ -1377,6 +1387,68 @@ namespace ccf
kv::CommittableTx::Flag::SNAPSHOT_AT_NEXT_SIGNATURE);
}
void trigger_acme_refresh(
kv::Tx& tx,
const std::optional<std::vector<std::string>>& interfaces =
std::nullopt) override
{
if (!network.identity)
{
return;
}
num_acme_interfaces = 0;
for (const auto& [iname, interface] : config.network.rpc_interfaces)
{
if (
interface.endorsement->authority != Authority::ACME ||
!interface.endorsement->acme_configuration)
{
continue;
}
num_acme_interfaces++;
if (
!interfaces ||
std::find(interfaces->begin(), interfaces->end(), iname) !=
interfaces->end())
{
const std::string& cfg_name =
*interface.endorsement->acme_configuration;
auto cit = config.network.acme->configurations.find(cfg_name);
if (cit == config.network.acme->configurations.end())
{
LOG_INFO_FMT("Unknown ACME configuration '{}'", cfg_name);
continue;
}
if (acme_clients.find(cfg_name) == acme_clients.end())
{
const auto& aconfig = cit->second;
acme_clients.emplace(
cfg_name,
std::make_shared<ccf::ACMEClient>(
cfg_name,
aconfig,
rpcsessions,
network.tables,
to_host,
node_sign_kp));
}
auto client = acme_clients[cfg_name];
if (!client->has_active_orders())
{
acme_clients[cfg_name]->get_certificate(
make_key_pair(network.identity->priv_key), true);
}
}
}
}
void trigger_host_process_launch(
const std::vector<std::string>& args) override
{
@ -2188,6 +2260,34 @@ namespace ccf
LOG_INFO_FMT("Service open at seqno {}", hook_version);
}
}));
network.tables->set_global_hook(
network.acme_certificates.get_name(),
network.acme_certificates.wrap_commit_hook(
[this](
kv::Version hook_version, const ccf::ACMECertificates::Write& w) {
for (auto const& [interface_id, interface] :
config.network.rpc_interfaces)
{
if (interface.endorsement->acme_configuration)
{
auto cit = w.find(*interface.endorsement->acme_configuration);
if (cit != w.end())
{
LOG_INFO_FMT(
"ACME: new certificate for interface '{}' with "
"configuration '{}'",
interface_id,
*interface.endorsement->acme_configuration);
rpcsessions->set_cert(
Authority::ACME,
*cit->second,
network.identity->priv_key,
cit->first);
}
}
}
}));
}
kv::Version get_last_recovered_signed_idx() override
@ -2419,5 +2519,67 @@ namespace ccf
RINGBUFFER_WRITE_MESSAGE(
consensus::ledger_truncate, to_host, idx, recovery_mode);
}
void setup_acme_clients()
{
if (!config.network.acme || config.network.acme->configurations.empty())
{
return;
}
const auto& ifaces = config.network.rpc_interfaces;
num_acme_interfaces =
std::count_if(ifaces.begin(), ifaces.end(), [](const auto& id_iface) {
return id_iface.second.endorsement->authority == Authority::ACME;
});
if (num_acme_interfaces > 0)
{
using namespace threading;
// Start task to periodically check whether any of the certs are
// expired.
auto msg = std::make_unique<threading::Tmsg<NodeStateMsg>>(
[](std::unique_ptr<threading::Tmsg<NodeStateMsg>> msg) {
auto& state = msg->data.self;
auto& config = state.config;
if (state.consensus && state.consensus->can_replicate())
{
if (state.acme_clients.size() != state.num_acme_interfaces)
{
auto tx = state.network.tables->create_tx();
state.trigger_acme_refresh(tx);
tx.commit();
}
else
{
for (auto& [cfg_name, client] : state.acme_clients)
{
client->check_expiry(
state.network.tables, state.network.identity);
}
}
}
auto delay = std::chrono::minutes(1);
ThreadMessaging::thread_messaging.add_task_after(
std::move(msg), delay);
},
*this);
ThreadMessaging::thread_messaging.add_task_after(
std::move(msg), std::chrono::seconds(2));
}
}
public:
void acme_challenge_response_ack(const std::string& token)
{
for (auto& [_, client] : acme_clients)
{
client->start_challenge(token);
}
}
};
}

Просмотреть файл

@ -40,5 +40,13 @@ namespace ccf
{
impl.trigger_snapshot(tx);
}
void trigger_acme_refresh(
kv::Tx& tx,
const std::optional<std::vector<std::string>>& interfaces =
std::nullopt) override
{
impl.trigger_acme_refresh(tx, interfaces);
}
};
}

Просмотреть файл

@ -29,5 +29,9 @@ namespace ccf
virtual void trigger_recovery_shares_refresh(kv::Tx& tx) = 0;
virtual void trigger_ledger_chunk(kv::Tx& tx) = 0;
virtual void trigger_snapshot(kv::Tx& tx) = 0;
virtual void trigger_acme_refresh(
kv::Tx& tx,
const std::optional<std::vector<std::string>>& interfaces =
std::nullopt) = 0;
};
}

Просмотреть файл

@ -25,6 +25,10 @@ namespace ccf
virtual void trigger_snapshot(kv::Tx& tx) = 0;
virtual void trigger_host_process_launch(
const std::vector<std::string>& args) = 0;
virtual void trigger_acme_refresh(
kv::Tx& tx,
const std::optional<std::vector<std::string>>& interfaces =
std::nullopt) = 0;
virtual bool is_in_initialised_state() const = 0;
virtual bool is_part_of_public_network() const = 0;
virtual bool is_primary() const = 0;

Просмотреть файл

@ -119,6 +119,14 @@ namespace ccf
{
return;
}
void trigger_acme_refresh(
kv::Tx& tx,
const std::optional<std::vector<std::string>>& interfaces =
std::nullopt) override
{
return;
}
};
class StubHostProcesses : public ccf::AbstractHostProcesses

Просмотреть файл

@ -3,6 +3,7 @@
#pragma once
#include "ccf/service/signed_req.h"
#include "ccf/service/tables/acme_certificates.h"
#include "ccf/service/tables/cert_bundles.h"
#include "ccf/service/tables/code_id.h"
#include "ccf/service/tables/constitution.h"
@ -80,6 +81,7 @@ namespace ccf
//
Nodes nodes;
NodeEndorsedCertificates node_endorsed_certificates;
ACMECertificates acme_certificates;
//
// Internal CCF tables
@ -125,6 +127,7 @@ namespace ccf
user_info(Tables::USER_INFO),
nodes(Tables::NODES),
node_endorsed_certificates(Tables::NODE_ENDORSED_CERTIFICATES),
acme_certificates(Tables::ACME_CERTIFICATES),
service(Tables::SERVICE),
secrets(Tables::ENCRYPTED_LEDGER_SECRETS),
snapshot_evidence(Tables::SNAPSHOT_EVIDENCE),

Просмотреть файл

@ -14,18 +14,31 @@ namespace tls
class CA
{
private:
Unique_X509 ca;
std::vector<Unique_X509> cas;
bool partial_ok = false;
public:
CA(const std::string& ca_ = "")
CA(const std::string& ca_ = "", bool partial_ok = false) :
CA(std::vector<std::string>({ca_}), partial_ok)
{}
CA(
const std::vector<std::string>& ca_strings = {},
bool partial_ok = false) :
partial_ok(partial_ok)
{
if (!ca_.empty())
for (const auto& ca_string : ca_strings)
{
Unique_BIO bio(ca_.data(), ca_.size());
if (!(ca = Unique_X509(bio, true)))
if (!ca_string.empty())
{
throw std::logic_error(
"Could not parse CA: " + error_string(ERR_get_error()));
Unique_BIO bio(ca_string.data(), ca_string.size());
Unique_X509 ca;
if (!(ca = Unique_X509(bio, true)))
{
throw std::logic_error(
"Could not parse CA: " + error_string(ERR_get_error()));
}
cas.push_back(std::move(ca));
}
}
}
@ -35,7 +48,14 @@ namespace tls
void use(SSL_CTX* ssl_ctx)
{
X509_STORE* store = X509_STORE_new();
CHECK1(X509_STORE_add_cert(store, ca));
if (partial_ok)
{
CHECK1(X509_STORE_set_flags(store, X509_V_FLAG_PARTIAL_CHAIN));
}
for (const auto& ca : cas)
{
CHECK1(X509_STORE_add_cert(store, ca));
}
SSL_CTX_set_cert_store(ssl_ctx, store);
}
};

356
tests/acme_endorsement.py Normal file
Просмотреть файл

@ -0,0 +1,356 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the Apache 2.0 License.
import subprocess
import os
import time
import urllib.request
import json
import infra.network
import infra.path
import infra.proc
import infra.interfaces
import infra.net
import infra.e2e_args
import infra.crypto
import suite.test_requirements as reqs
import socket
import ssl
from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from loguru import logger as LOG
def get_network_public_key(network):
cert_data = open(os.path.join(network.common_dir, "service_cert.pem"), "rb").read()
network_cert = load_pem_x509_certificate(cert_data, default_backend())
return network_cert.public_key().public_bytes(
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
)
def wait_for_port_to_listen(host, port, timeout=10):
end_time = time.time() + timeout
while time.time() < end_time:
try:
socket.create_connection((host, int(port)), timeout=0.1)
return
except Exception as ex:
LOG.trace(f"Likely expected exception: {ex}")
time.sleep(0.5)
raise TimeoutError(f"port did not start listening within {timeout} seconds")
@reqs.description("Start network and wait for ACME certificates")
def wait_for_certificates(args, network_name, ca_certs, timeout=5 * 60):
with infra.network.network(
args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
) as network:
network.start_and_open(args)
# NB: Pebble non-deterministically injects delays and failures,
# so the following checks may take a significant amount of time.
network_public_key = get_network_public_key(network)
start_time = time.time()
end_time = start_time + timeout
num_ok = 0
while num_ok != len(args.nodes):
if time.time() > end_time:
raise TimeoutError(
f"Not all nodes had the correct ACME-endorsed TLS certificate installed after {timeout} seconds"
)
num_ok = 0
for node in network.nodes:
iface = node.host.rpc_interfaces["acme_endorsed_interface"]
try:
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True
for crt in ca_certs:
context.load_verify_locations(cadata=crt)
s = socket.socket()
s.settimeout(1)
c = context.wrap_socket(s, server_hostname=network_name)
c.connect((iface.host, iface.port))
cert_der = c.getpeercert(binary_form=True)
cert = load_der_x509_certificate(cert_der, default_backend())
if cert.subject.rfc4514_string() != "CN=" + network_name:
raise Exception("Common name mismatch")
cert_public_key = cert.public_key().public_bytes(
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
)
if network_public_key != cert_public_key:
raise Exception("Public key mismatch")
num_ok += 1
except Exception as ex:
LOG.trace(f"Likely expected exception: {ex}")
if num_ok != len(args.nodes):
time.sleep(1)
LOG.info(
f"Success: all nodes had correct certificates installed after {int(time.time() - start_time)} seconds"
)
def get_binary(url, filename):
if not os.path.isfile(filename):
urllib.request.urlretrieve(
url,
filename,
)
os.chmod(filename, 0o744)
def start_mock_dns(filename, listen_address, mgmt_address, out, err, env=None):
p = subprocess.Popen(
[
filename,
"-http01",
"",
"-https01",
"",
"-dns01",
listen_address,
"-tlsalpn01",
"",
"-management",
mgmt_address,
],
stdout=out,
stderr=err,
close_fds=True,
env=env,
)
host, port = listen_address.split(":")
wait_for_port_to_listen(host, port, 5)
return p
def register_endorsed_hosts(args, network_name, dns_mgmt_address):
endorsed_hosts = [
node.rpc_interfaces["acme_endorsed_interface"].host for node in args.nodes
]
data = str(
json.dumps(
{
"host": network_name,
"addresses": endorsed_hosts,
}
)
).encode("utf-8")
urllib.request.urlopen(f"http://{dns_mgmt_address}/add-a", data=data)
# Disable the default IPv6 entry
data = str(json.dumps({"ip": ""})).encode("utf-8")
urllib.request.urlopen(
"http://" + dns_mgmt_address + "/set-default-ipv6", data=data
)
def start_pebble(filename, config_filename, dns_address, listen_address, out, err, env):
p = subprocess.Popen(
[
filename,
"--config",
config_filename,
"-dnsserver",
dns_address,
],
stdout=out,
stderr=err,
close_fds=True,
env=env,
)
host, port = listen_address.split(":")
wait_for_port_to_listen(host, port, 5)
return p
def get_without_cert_check(url):
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return urllib.request.urlopen(url, context=ctx).read().decode("utf-8")
def get_pebble_ca_certs(mgmt_address):
ca = get_without_cert_check("https://" + mgmt_address + "/roots/0")
intermediate = get_without_cert_check(
"https://" + mgmt_address + "/intermediates/0"
)
return ca, intermediate
@reqs.description("Test against a local pebble CA")
def run_pebble(args):
binary_filename = "/opt/pebble/pebble_linux-amd64"
config_filename = "pebble.config.json"
ca_key_filename = "pebble-key.pem"
ca_cert_filename = "pebble-ca-cert.pem"
output_filename = "pebble.out"
error_filename = "pebble.err"
listen_address = "127.0.0.1:1024"
mgmt_address = "127.0.0.1:1025"
tls_port = 1026
http_port = 1027
mock_dns_filename = "/opt/pebble/pebble-challtestsrv_linux-amd64"
mock_dns_listen_address = "127.0.0.1:1028"
mock_dns_mgmt_address = "127.0.0.1:1029"
network_name = "my-network.ccf.dev"
if not os.path.exists(binary_filename) or not os.path.exists(mock_dns_filename):
raise Exception("pebble not found; run playbooks to install it")
config = {
"pebble": {
"listenAddress": listen_address,
"managementListenAddress": mgmt_address,
"certificate": ca_cert_filename,
"privateKey": ca_key_filename,
"httpPort": http_port,
"tlsPort": tls_port,
"ocspResponderURL": "",
"externalAccountBindingRequired": False,
}
}
with open(config_filename, "w", encoding="ascii") as f:
json.dump(config, f)
ca_key, _ = infra.crypto.generate_ec_keypair("secp384r1")
with open(ca_key_filename, "w", encoding="ascii") as f:
f.write(ca_key)
ca_cert = infra.crypto.generate_cert(ca_key, ca=True, cn="Pebble Test CA")
with open(ca_cert_filename, "w", encoding="ascii") as f:
f.write(ca_cert)
args.acme = {
"configurations": {
"pebble": {
"ca_certs": [ca_cert],
"directory_url": f"https://{listen_address}/dir",
"service_dns_name": network_name,
"contact": ["mailto:nobody@example.com"],
"terms_of_service_agreed": True,
"challenge_type": "http-01",
}
}
}
for node in args.nodes:
endorsed_interface = infra.interfaces.RPCInterface(
host=infra.net.expand_localhost(),
endorsement=infra.interfaces.Endorsement(
authority=infra.interfaces.EndorsementAuthority.ACME,
acme_configuration="pebble",
),
)
endorsed_interface.public_host = network_name
node.rpc_interfaces["acme_endorsed_interface"] = endorsed_interface
node.acme_challenge_server_interface = (
endorsed_interface.host + ":" + str(http_port)
)
exception_seen = None
with open(output_filename, "w", encoding="ascii") as out:
with open(error_filename, "w", encoding="ascii") as err:
mock_dns_proc = start_mock_dns(
mock_dns_filename,
mock_dns_listen_address,
mock_dns_mgmt_address,
out,
err,
)
register_endorsed_hosts(args, network_name, mock_dns_mgmt_address)
pebble_proc = start_pebble(
binary_filename,
config_filename,
mock_dns_listen_address,
listen_address,
out,
err,
env={"PEBBLE_WFE_NONCEREJECT": "0", "PEBBLE_VA_NOSLEEP": "1"},
)
try:
ca_certs = get_pebble_ca_certs(mgmt_address)
wait_for_certificates(args, network_name, ca_certs)
except Exception as ex:
exception_seen = ex
finally:
if pebble_proc:
pebble_proc.kill()
if mock_dns_proc:
mock_dns_proc.kill()
if exception_seen:
LOG.info("Pebble out:")
LOG.info(open(output_filename, "r", encoding="ascii").read())
LOG.info("Pebble err:")
LOG.info(open(error_filename, "r", encoding="ascii").read())
raise exception_seen
@reqs.description("Test against Let's Encrypt's staging environment")
def run_lets_encrypt(args):
# This requires a DNS name by which the network is reachable (which we don't have in the CI), e.g. your dev VM name.
# On the interface of that name, we also need port 80 to be reachable from the internet for the challenge responses (see Network on the Azure portal panel of your VM).
# The Let's Encrypt staging environment is described here: https://letsencrypt.org/docs/staging-environment/ (we need the CA certs for the staging environment, which are not globally endorsed).
# Further, to connect to Let's Encrypt, we also need their public root cert, which can be found at https://letsencrypt.org/certificates/
# (Clients won't need this as they usually have ISRG Root X1 installed, but our enclaves don't.)
service_dns_name = "..." # Set to the DNS name of your machine
ca_certs = [
"-----BEGIN CERTIFICATE-----\nMIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw\nWhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg\nRW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP\nR5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx\nsxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm\nNHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg\nZ3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG\n/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB\nAf8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA\nFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw\nAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw\nOi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB\ngt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W\nPTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl\nikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz\nCkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm\nlJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4\navAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2\nyJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O\nyK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids\nhCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+\nHlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv\nMldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX\nnLRbwHOoq7hHwg==\n-----END CERTIFICATE-----\n",
"-----BEGIN CERTIFICATE-----\nMIIDCzCCApGgAwIBAgIRALRY4992FVxZJKOJ3bpffWIwCgYIKoZIzj0EAwMwaDEL\nMAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0\neSBSZXNlYXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Nj\nb2xpIFgyMB4XDTIwMDkwNDAwMDAwMFoXDTI1MDkxNTE2MDAwMFowVTELMAkGA1UE\nBhMCVVMxIDAeBgNVBAoTFyhTVEFHSU5HKSBMZXQncyBFbmNyeXB0MSQwIgYDVQQD\nExsoU1RBR0lORykgRXJzYXR6IEVkYW1hbWUgRTEwdjAQBgcqhkjOPQIBBgUrgQQA\nIgNiAAT9v/PJUtHOTk28nXCXrpP665vI4Z094h8o7R+5E6yNajZa0UubqjpZFoGq\nu785/vGXj6mdfIzc9boITGusZCSWeMj5ySMZGZkS+VSvf8VQqj+3YdEu4PLZEjBA\nivRFpEejggEQMIIBDDAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUH\nAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFOv5JcKA\nKGbibQiSMvPC4a3D/zVFMB8GA1UdIwQYMBaAFN7Ro1lkDsGaNqNG7rAQdu+ul5Vm\nMDYGCCsGAQUFBwEBBCowKDAmBggrBgEFBQcwAoYaaHR0cDovL3N0Zy14Mi5pLmxl\nbmNyLm9yZy8wKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3N0Zy14Mi5jLmxlbmNy\nLm9yZy8wIgYDVR0gBBswGTAIBgZngQwBAgEwDQYLKwYBBAGC3xMBAQEwCgYIKoZI\nzj0EAwMDaAAwZQIwXcZbdgxcGH9rTErfSTkXfBKKygU0yO7OpbuNeY1id0FZ/hRY\nN5fdLOGuc+aHfCsMAjEA0P/xwKr6NQ9MN7vrfGAzO397PApdqfM7VdFK18aEu1xm\n3HMFKzIR8eEPsMx4smMl\n-----END CERTIFICATE-----\n",
"-----BEGIN CERTIFICATE-----\nMIICTjCCAdSgAwIBAgIRAIPgc3k5LlLVLtUUvs4K/QcwCgYIKoZIzj0EAwMwaDEL\nMAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0\neSBSZXNlYXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Nj\nb2xpIFgyMB4XDTIwMDkwNDAwMDAwMFoXDTQwMDkxNzE2MDAwMFowaDELMAkGA1UE\nBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl\nYXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Njb2xpIFgy\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOvS+w1kCzAxYOJbA06Aw0HFP2tLBLKPo\nFQqR9AMskl1nC2975eQqycR+ACvYelA8rfwFXObMHYXJ23XLB+dAjPJVOJ2OcsjT\nVqO4dcDWu+rQ2VILdnJRYypnV1MMThVxo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD\nVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU3tGjWWQOwZo2o0busBB2766XlWYwCgYI\nKoZIzj0EAwMDaAAwZQIwRcp4ZKBsq9XkUuN8wfX+GEbY1N5nmCRc8e80kUkuAefo\nuc2j3cICeXo1cOybQ1iWAjEA3Ooawl8eQyR4wrjCofUE8h44p0j7Yl/kBlJZT8+9\nvbtH7QiVzeKCOTQPINyRql6P\n-----END CERTIFICATE-----\n",
]
args.acme = {
"configurations": {
"letsencrypt": {
"ca_certs": ca_certs,
"directory_url": "https://acme-staging-v02.api.letsencrypt.org/directory",
"service_dns_name": service_dns_name,
"contact": ["mailto:admin@ccf.dev"],
"terms_of_service_agreed": True,
"challenge_type": "http-01",
}
},
"challenge_server_interface": "0.0.0.0:80",
}
for node in args.nodes:
endorsed_interface = infra.interfaces.RPCInterface(
host="0.0.0.0",
endorsement=infra.interfaces.Endorsement(
authority=infra.interfaces.EndorsementAuthority.ACME
),
)
endorsed_interface.public_host = service_dns_name
endorsed_interface.endorsement.acme_configuration = "letsencrypt"
node.rpc_interfaces["acme_endorsed_interface"] = endorsed_interface
wait_for_certificates(args, service_dns_name, ca_certs)
if __name__ == "__main__":
args = infra.e2e_args.cli_args()
args.package = "samples/apps/logging/liblogging"
args.nodes = infra.e2e_args.max_nodes(args, f=1)
run_pebble(args)
# run_lets_encrypt(args)

Просмотреть файл

@ -6,6 +6,9 @@
"network": {
"node_to_node_interface": { "bind_address": "{{ node_address }}" },
"rpc_interfaces": {{ rpc_interfaces|tojson }}
{% if acme %}
, "acme": {{ acme|tojson }}
{% endif %}
},
"node_certificate":
{
@ -87,5 +90,5 @@
"circuit_size": "4MB",
"max_msg_size": "16MB",
"max_fragment_size": "64KB"
}
}
}

Просмотреть файл

@ -85,6 +85,8 @@ def generate_rsa_keypair(key_size: int) -> Tuple[str, str]:
def generate_ec_keypair(curve_name: str) -> Tuple[str, str]:
if curve_name == "secp256r1":
curve = ec.SECP256R1()
elif curve_name == "secp384r1":
curve = ec.SECP384R1()
else:
raise ValueError("unsupported curve")
priv = ec.generate_private_key(

Просмотреть файл

@ -28,22 +28,27 @@ NODE_TO_NODE_INTERFACE_NAME = "node_to_node_interface"
class EndorsementAuthority(Enum):
Service = auto()
Node = auto()
ACME = auto()
@dataclass
class Endorsement:
authority: EndorsementAuthority = EndorsementAuthority.Service
acme_configuration: Optional[str] = None
@staticmethod
def to_json(endorsement):
return {
"authority": endorsement.authority.name,
}
r = {"authority": endorsement.authority.name}
if endorsement.acme_configuration:
r["acme_configuration"] = endorsement.acme_configuration
return r
@staticmethod
def from_json(json):
endorsement = Endorsement()
endorsement.authority = json["authority"]
endorsement.acme_configuration = json["acme_configuration"]
return endorsement
@ -66,10 +71,11 @@ class RPCInterface(Interface):
max_open_sessions_soft: Optional[int] = DEFAULT_MAX_OPEN_SESSIONS_SOFT
max_open_sessions_hard: Optional[int] = DEFAULT_MAX_OPEN_SESSIONS_HARD
endorsement: Optional[Endorsement] = Endorsement()
acme_configuration: Optional[str] = None
@staticmethod
def to_json(interface):
return {
r = {
"bind_address": f"{interface.host}:{interface.port}",
"protocol": f"{interface.transport}",
"published_address": f"{interface.public_host}:{interface.public_port or 0}",
@ -77,6 +83,9 @@ class RPCInterface(Interface):
"max_open_sessions_hard": interface.max_open_sessions_hard,
"endorsement": Endorsement.to_json(interface.endorsement),
}
if interface.acme_configuration:
r["acme_configuration"] = interface.acme_configuration
return r
@staticmethod
def from_json(json):
@ -96,6 +105,8 @@ class RPCInterface(Interface):
)
if "endorsement" in json:
interface.endorsement = Endorsement.from_json(json["endorsement"])
if "acme_configuration" in json:
interface.acme_configuration = json.get("acme_configuration")
return interface
@ -110,6 +121,7 @@ def make_secondary_interface(transport="tcp", interface_name=SECONDARY_RPC_INTER
@dataclass
class HostSpec:
rpc_interfaces: Dict[str, RPCInterface] = RPCInterface()
acme_challenge_server_interface: Optional[str] = None
def get_primary_interface(self):
return self.rpc_interfaces[PRIMARY_RPC_INTERFACE]

Просмотреть файл

@ -125,6 +125,7 @@ class Network:
"config_file",
"ubsan_options",
"previous_service_identity_file",
"acme",
]
# Maximum delay (seconds) for updates to propagate from the primary to backups

Просмотреть файл

@ -651,6 +651,12 @@ class CCFRemote(object):
os.path.join(self.common_dir, os.path.basename(f)) for f in constitution
]
# ACME
if "acme" in kwargs and host.acme_challenge_server_interface:
kwargs["acme"][
"challenge_server_interface"
] = host.acme_challenge_server_interface
# Configuration file
if config_file:
LOG.info(