diff --git a/CHANGELOG.md b/CHANGELOG.md index 698f86de9bd6..6c4c1b5ff97b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added `ccf.enableUntrustedDateTime` to JS API. After calling `ccf.enableUntrustedDateTime(true)`, the `Date` global object will use the untrusted host time to retrieve the current time. - Add new `ccf.crypto.jwkToPem`, `ccf.crypto.pubJwkToPem`, `ccf.crypto.rsaJwkToPem`, `ccf.crypto.pubRsaJwkToPem`, `ccf.crypto.eddsaJwkToPem`, `ccf.crypto.pubEddsaJwkToPem` to JavaScript/TypesScript API to convert EC/RSA/EdDSA keys from PEM to Json Web Key (#4876). - Add new constructors to cryptography C++ API to generate EC/RSA/EdDSA keys from Json Web Key (#4876). +- Endorsement certificates for SEV-SNP attestation report can now be retrieved via an environment variable, as specified by `attestation.environment.report_endorsements` configuration entry (#4940). ## [4.0.0-dev3] diff --git a/CMakeLists.txt b/CMakeLists.txt index 1876f071b20c..ec17ee05d1a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,6 +103,8 @@ if(COMPILE_TARGET STREQUAL "sgx") ccf.enclave ${CCF_IMPL_SOURCE} ${CCF_GENERATED_DIR}/ccf_t.cpp ) + target_compile_definitions(ccf.enclave PUBLIC PLATFORM_SGX) + add_warning_checks(ccf.enclave) target_include_directories( diff --git a/doc/host_config_schema/cchost_config.json b/doc/host_config_schema/cchost_config.json index b9a924252204..4dfe7701e080 100644 --- a/doc/host_config_schema/cchost_config.json +++ b/doc/host_config_schema/cchost_config.json @@ -440,6 +440,25 @@ "attestation": { "type": "object", "properties": { + "environment": { + "type": "object", + "properties": { + "report_endorsements": { + "type": ["string", "null"], + "description": "Name of environment variable (e.g. ``UVM_HOST_AMD_CERTIFICATE``) containing base64-encoded attestation report endorsements as JSON object (Azure Container Instance SEV-SNP only). If this is set, specified ``snp_endorsements_servers`` are ignored" + }, + "security_policy": { + "type": ["string", "null"], + "description": "Name of environment variable (e.g. ``UVM_SECURITY_POLICY``) containing base64-encoded security policy (Azure Container Instance SEV-SNP only)" + }, + "uvm_endorsements": { + "type": ["string", "null"], + "description": "Name of environment variable (e.g. ``UVM_REFERENCE_INFO``) containing base64-encoded UVM endorsements as COSE Sign1 document (Azure Container Instance SEV-SNP only)" + } + }, + "description": "Environment variables required to provide best auditability and serviceability for Azure Container Instance deployments (SEV-SNP only)", + "additionalProperties": false + }, "snp_endorsements_servers": { "type": "array", "items": { @@ -459,22 +478,7 @@ "required": ["url"], "additionalProperties": false }, - "description": "List of servers used to retrieve attestation report endorsement certificates (SEV-SNP only). The first server in the list is always used and other servers are only specified as fallback" - }, - "environment": { - "type": "object", - "properties": { - "security_policy": { - "type": ["string", "null"], - "description": "Name of environment variable (e.g. ``UVM_SECURITY_POLICY``) containing base64-encoded security policy (Azure Container Instance SEV-SNP only)" - }, - "uvm_endorsements": { - "type": ["string", "null"], - "description": "Name of environment variable (e.g. ``UVM_REFERENCE_INFO``) containing base64-encoded UVM endorsements (Azure Container Instance SEV-SNP only)" - } - }, - "description": "Environment variables required to provide best auditability and serviceability for Azure Container Instance deployments (SEV-SNP only)", - "additionalProperties": false + "description": "List of servers used to retrieve attestation report endorsement certificates (SEV-SNP only). The first server in the list is always used and other servers are only specified as fallback. Ignored if ``environment.report_endorsements`` is set" } }, "description": "This section includes configuration for the attestation for AMD SEV-SNP platform (ignored for SGX)", diff --git a/include/ccf/node/startup_config.h b/include/ccf/node/startup_config.h index ba15877d26d2..4ea433416ca3 100644 --- a/include/ccf/node/startup_config.h +++ b/include/ccf/node/startup_config.h @@ -57,6 +57,7 @@ struct CCFConfig { std::optional security_policy = std::nullopt; std::optional uvm_endorsements = std::nullopt; + std::optional report_endorsements = std::nullopt; bool operator==(const Environment&) const = default; }; diff --git a/include/ccf/pal/attestation.h b/include/ccf/pal/attestation.h index d349b0e9ae9d..0043d5fe7a57 100644 --- a/include/ccf/pal/attestation.h +++ b/include/ccf/pal/attestation.h @@ -65,6 +65,11 @@ namespace ccf::pal auto certificates = crypto::split_x509_cert_bundle(std::string_view( reinterpret_cast(quote_info.endorsements.data()), quote_info.endorsements.size())); + if (certificates.size() != 3) + { + throw std::logic_error(fmt::format( + "Expected 3 endorsement certificates but got {}", certificates.size())); + } auto chip_certificate = certificates[0]; auto sev_version_certificate = certificates[1]; auto root_certificate = certificates[2]; diff --git a/include/ccf/pal/attestation_sev_snp_endorsements.h b/include/ccf/pal/attestation_sev_snp_endorsements.h index 46ca503ccce1..33d62afa938c 100644 --- a/include/ccf/pal/attestation_sev_snp_endorsements.h +++ b/include/ccf/pal/attestation_sev_snp_endorsements.h @@ -16,6 +16,25 @@ namespace ccf::pal::snp { constexpr auto product_name = "Milan"; + struct ACIReportEndorsements + { + std::string cache_control; + std::string vcek_cert; + std::string certificate_chain; + std::string tcbm; + }; + DECLARE_JSON_TYPE(ACIReportEndorsements); + DECLARE_JSON_REQUIRED_FIELDS_WITH_RENAMES( + ACIReportEndorsements, + cache_control, + "cacheControl", + vcek_cert, + "vcekCert", + certificate_chain, + "certificateChain", + tcbm, + "tcbm"); + struct EndorsementEndpointsConfiguration { struct EndpointInfo diff --git a/include/ccf/pal/platform.h b/include/ccf/pal/platform.h new file mode 100644 index 000000000000..be71cad67f19 --- /dev/null +++ b/include/ccf/pal/platform.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "ccf/ds/json.h" + +namespace ccf::pal +{ + enum class Platform + { + SGX = 0, + SNP = 1, + Virtual = 2, + Unknown = 3, + }; + DECLARE_JSON_ENUM( + Platform, + {{Platform::SGX, "SGX"}, + {Platform::SNP, "SNP"}, + {Platform::Virtual, "Virtual"}, + {Platform::Unknown, "Unknown"}}); + + constexpr static auto platform = +#if defined(PLATFORM_SGX) + Platform::SGX +#elif defined(PLATFORM_SNP) + Platform::SNP +#elif defined(PLATFORM_VIRTUAL) + Platform::Virtual +#else + Platform::Unknown +#endif + ; +} \ No newline at end of file diff --git a/scripts/azure_deployment/arm_aci.py b/scripts/azure_deployment/arm_aci.py index 2753c46b9893..9d3c133bf4c3 100644 --- a/scripts/azure_deployment/arm_aci.py +++ b/scripts/azure_deployment/arm_aci.py @@ -43,6 +43,7 @@ def append_envvar_to_well_known_file(envvar): return [ append_envvar_to_well_known_file("UVM_SECURITY_POLICY"), append_envvar_to_well_known_file("UVM_REFERENCE_INFO"), + append_envvar_to_well_known_file("UVM_HOST_AMD_CERTIFICATE"), ] diff --git a/src/common/configuration.h b/src/common/configuration.h index d41ca30c26f7..de15b7eae7ec 100644 --- a/src/common/configuration.h +++ b/src/common/configuration.h @@ -74,7 +74,10 @@ DECLARE_JSON_OPTIONAL_FIELDS(CCFConfig::JWT, key_refresh_interval); DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(CCFConfig::Attestation::Environment); DECLARE_JSON_REQUIRED_FIELDS(CCFConfig::Attestation::Environment); DECLARE_JSON_OPTIONAL_FIELDS( - CCFConfig::Attestation::Environment, security_policy, uvm_endorsements); + CCFConfig::Attestation::Environment, + security_policy, + uvm_endorsements, + report_endorsements); DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(CCFConfig::Attestation); DECLARE_JSON_REQUIRED_FIELDS(CCFConfig::Attestation); diff --git a/src/host/main.cpp b/src/host/main.cpp index 6f17cc7f02b1..15a39ca1c492 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -3,6 +3,7 @@ #include "ccf/ds/logger.h" #include "ccf/pal/attestation.h" +#include "ccf/pal/platform.h" #include "ccf/version.h" #include "config_schema.h" #include "configuration.h" @@ -52,14 +53,7 @@ void print_version(size_t) { std::cout << "CCF host: " << ccf::ccf_version << std::endl; std::cout << "Platform: " - << -#if defined(PLATFORM_SGX) - "SGX" -#elif defined(PLATFORM_SNP) - "SNP" -#elif defined(PLATFORM_VIRTUAL) - "Virtual" -#endif + << nlohmann::json(ccf::pal::platform).get() << std::endl; exit(0); } @@ -448,6 +442,22 @@ int main(int argc, char** argv) "UVM endorsements"); } + if (config.attestation.environment.report_endorsements.has_value()) + { + startup_config.attestation.environment.report_endorsements = + read_required_environment_variable( + config.attestation.environment.report_endorsements.value(), + "attestation report endorsements"); + } + else if ( + ccf::pal::platform == ccf::pal::Platform::SNP && + config.attestation.snp_endorsements_servers.empty()) + { + LOG_FATAL_FMT( + "On SEV-SNP, either one of report endorsements environment variable or " + "endorsements server should be set"); + } + if (config.node_data_json_file.has_value()) { startup_config.node_data = diff --git a/src/node/node_state.h b/src/node/node_state.h index a68046cf73d2..932c91495a54 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -9,6 +9,7 @@ #include "ccf/ds/logger.h" #include "ccf/pal/attestation.h" #include "ccf/pal/locking.h" +#include "ccf/pal/platform.h" #include "ccf/serdes.h" #include "ccf/service/node_info_network.h" #include "ccf/service/tables/acme_certificates.h" @@ -374,42 +375,74 @@ namespace ccf { auto fetch_endorsements = [this]( - const QuoteInfo& quote_info_, + const QuoteInfo& qi, const pal::snp::EndorsementEndpointsConfiguration& endpoint_config) { - if (quote_info_.format != QuoteFormat::amd_sev_snp_v1) + // Note: Node lock is already taken here as this is called back + // synchronously with the call to pal::generate_quote + + if ( + qi.format == QuoteFormat::amd_sev_snp_v1 && + !config.attestation.environment.report_endorsements.has_value()) { - // Note: Node lock is already taken here as this is called back - // synchronously with the call to pal::generate_quote - CCF_ASSERT_FMT( - quote_info_.format == QuoteFormat::insecure_virtual || - !quote_info_.endorsements.empty(), - "SGX quote generation should have already fetched endorsements"); - quote_info = quote_info_; - launch_node(); + // On SEV-SNP, if no attestation report endorsements are set via + // environment, those need to be fetched + quote_endorsements_client = + std::make_shared( + rpcsessions, + endpoint_config, + [this, qi](std::vector&& endorsements) { + std::lock_guard guard(lock); + quote_info = qi; + quote_info.endorsements = std::move(endorsements); + try + { + launch_node(); + } + catch (const std::exception& e) + { + LOG_FAIL_FMT("{}", e.what()); + throw; + } + quote_endorsements_client.reset(); + }); + + quote_endorsements_client->fetch_endorsements(); return; } - quote_endorsements_client = std::make_shared( - rpcsessions, - endpoint_config, - [this, quote_info_](std::vector&& endorsements) { - // Note: Only called for SEV-SNP - std::lock_guard guard(lock); - quote_info = quote_info_; - quote_info.endorsements = std::move(endorsements); - try - { - launch_node(); - } - catch (const std::exception& e) - { - LOG_FAIL_FMT("{}", e.what()); - throw; - } - quote_endorsements_client.reset(); - }); + CCF_ASSERT_FMT( + (qi.format == QuoteFormat::oe_sgx_v1 && !qi.endorsements.empty()) || + (qi.format != QuoteFormat::oe_sgx_v1 && qi.endorsements.empty()), + "SGX quote generation should have already fetched endorsements"); - quote_endorsements_client->fetch_endorsements(); + quote_info = qi; + + if ( + quote_info.format == QuoteFormat::amd_sev_snp_v1 && + config.attestation.environment.report_endorsements.has_value()) + { + // On SEV-SNP, if reports endorsements are passed via + // environment, read those rather than fetching them from + // endorsement server + pal::snp::ACIReportEndorsements endorsements = + nlohmann::json::parse(crypto::raw_from_b64( + config.attestation.environment.report_endorsements.value())); + + CCF_ASSERT_FMT( + quote_info.endorsements.empty(), + "No endorsements should be set by quote generation"); + + quote_info.endorsements.insert( + quote_info.endorsements.end(), + endorsements.vcek_cert.begin(), + endorsements.vcek_cert.end()); + quote_info.endorsements.insert( + quote_info.endorsements.end(), + endorsements.certificate_chain.begin(), + endorsements.certificate_chain.end()); + } + + launch_node(); }; pal::attestation_report_data report_data = {}; @@ -418,6 +451,7 @@ namespace ccf node_pub_key_hash.h.begin(), node_pub_key_hash.h.end(), report_data.begin()); + pal::generate_quote( report_data, fetch_endorsements, diff --git a/src/node/quote_endorsements_client.h b/src/node/quote_endorsements_client.h index ef3f413a42ee..cf7d8b7c994c 100644 --- a/src/node/quote_endorsements_client.h +++ b/src/node/quote_endorsements_client.h @@ -25,7 +25,7 @@ namespace ccf // Maximum number of retries per remote server before giving up and moving // on to the next server. - static constexpr size_t max_server_retries_count = 2; + static constexpr size_t max_server_retries_count = 3; std::shared_ptr rpcsessions; diff --git a/tests/code_update.py b/tests/code_update.py index 9bce50bfc595..291fec5bb710 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -112,7 +112,7 @@ def test_add_node_without_security_policy(network, args): args.package, args, timeout=3, - security_policy_envvar=None, + set_snp_security_policy_envvar=True, ) network.trust_node(new_node, args) return network @@ -162,7 +162,7 @@ def test_start_node_with_mismatched_host_data(network, args): timeout=3, snp_security_policy=b64encode(b"invalid_security_policy").decode(), ) - except TimeoutError: + except (TimeoutError, RuntimeError): LOG.info("As expected, node with invalid security policy failed to startup") else: raise AssertionError("Node startup unexpectedly succeeded") diff --git a/tests/config.jinja b/tests/config.jinja index 4b8717168beb..876cfe3e1800 100644 --- a/tests/config.jinja +++ b/tests/config.jinja @@ -25,7 +25,8 @@ "environment": { "security_policy": {{ snp_security_policy_envvar|tojson }}, - "uvm_endorsements": {{ snp_uvm_endorsements_envvar|tojson }} + "uvm_endorsements": {{ snp_uvm_endorsements_envvar|tojson }}, + "report_endorsements": {{ snp_report_endorsements_envvar|tojson }} } }, "service_data_json_file": {{ service_data_json_file|tojson }}, diff --git a/tests/infra/remote.py b/tests/infra/remote.py index 5cbce2618c74..7a75fead9409 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -612,16 +612,21 @@ def __init__( snp_endorsements_servers=None, node_pid_file="node.pid", enclave_platform="sgx", - snp_security_policy_envvar=None, + set_snp_security_policy_envvar=True, snp_security_policy=None, - snp_uvm_endorsements_envvar=None, + set_snp_uvm_endorsements_envvar=True, snp_uvm_endorsements=None, + set_snp_report_endorsements_envvar=True, **kwargs, ): """ Run a ccf binary on a remote host. """ + snp_security_policy_envvar = None + snp_uvm_endorsements_envvar = None + snp_report_endorsements_envvar = None + if "env" in kwargs: env = kwargs["env"] else: @@ -634,11 +639,19 @@ def __init__( elif enclave_platform == "snp": env = snp.get_aci_env() snp_security_policy_envvar = ( - snp_security_policy_envvar or snp.ACI_SEV_SNP_ENVVAR_SECURITY_POLICY + snp.ACI_SEV_SNP_ENVVAR_SECURITY_POLICY + if set_snp_security_policy_envvar + else None ) snp_uvm_endorsements_envvar = ( - snp_uvm_endorsements_envvar - or snp.ACI_SEV_SNP_ENVVAR_UVM_ENDORSEMENTS + snp.ACI_SEV_SNP_ENVVAR_UVM_ENDORSEMENTS + if set_snp_uvm_endorsements_envvar + else None + ) + snp_report_endorsements_envvar = ( + snp.ACI_SEV_SNP_ENVVAR_REPORT_ENDORSEMENTS + if set_snp_report_endorsements_envvar + else None ) if snp_security_policy is not None: env[snp_security_policy_envvar] = snp_security_policy @@ -781,6 +794,7 @@ def __init__( node_pid_file=node_pid_file, snp_security_policy_envvar=snp_security_policy_envvar, snp_uvm_endorsements_envvar=snp_uvm_endorsements_envvar, + snp_report_endorsements_envvar=snp_report_endorsements_envvar, **kwargs, ) diff --git a/tests/infra/snp.py b/tests/infra/snp.py index 7f7b57bb7702..9d9c3afe8186 100644 --- a/tests/infra/snp.py +++ b/tests/infra/snp.py @@ -13,6 +13,7 @@ ACI_SEV_SNP_ENVVAR_SECURITY_POLICY = "UVM_SECURITY_POLICY" ACI_SEV_SNP_ENVVAR_UVM_ENDORSEMENTS = "UVM_REFERENCE_INFO" +ACI_SEV_SNP_ENVVAR_REPORT_ENDORSEMENTS = "UVM_HOST_AMD_CERTIFICATE" # Specifying the full security policy is not mandatory for security guarantees # (it's only useful for auditing/debugging) and so this may not be recorded in diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index e5a57b18d5f5..b79f38f7a9d2 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -209,8 +209,11 @@ def test_add_node_from_backup(network, args): return network -@reqs.description("Adding a node with AMD endorsements endpoint") -def test_add_node_amd_endorsements_endpoint(network, args): +@reqs.description("Adding a node with endorsements retrieved from remote server") +def test_add_node_endorsements_endpoints(network, args): + # By default, SEV-SNP endorsements are retrieved from the environment on ACI. + # However, we still want to support fetching those from a remote server, which is + # tested here primary, _ = network.find_primary() if not IS_SNP: LOG.warning("Skipping test as running on non SEV-SNP") @@ -218,6 +221,7 @@ def test_add_node_amd_endorsements_endpoint(network, args): args_copy = deepcopy(args) test_vectors = [ + (["Azure:global.acccache.azure.net"], True), (["AMD:kdsintf.amd.com"], True), (["AMD:invalid.amd.com"], False), (["Azure:invalid.azure.com", "AMD:kdsintf.amd.com"], True), # Fallback server @@ -230,16 +234,22 @@ def test_add_node_amd_endorsements_endpoint(network, args): new_node = network.create_node("local://localhost") args_copy.snp_endorsements_servers = servers try: - network.join_node(new_node, args.package, args_copy, timeout=15) + network.join_node( + new_node, + args.package, + args_copy, + set_snp_report_endorsements_envvar=None, + timeout=15, + ) except TimeoutError: assert not expected_result LOG.info( - "Node with invalid quote endorsement server could not join as expected" + f"Node with invalid quote endorsement servers {servers} could not join as expected" ) else: assert ( expected_result - ), "Node with invalid quote endorsement server joined unexpectedly" + ), f"Node with invalid quote endorsement servers joined unexpectedly: {servers}" network.retire_node(primary, new_node) new_node.stop() @@ -789,7 +799,7 @@ def run_all(args): test_join_straddling_primary_replacement(network, args) test_node_replacement(network, args) test_add_node_from_backup(network, args) - test_add_node_amd_endorsements_endpoint(network, args) + test_add_node_endorsements_endpoints(network, args) test_add_node_on_other_curve(network, args) test_retire_backup(network, args) test_add_node(network, args)