From a931ec507cc3d007c8e9d56ba41d9a3ba7acd934 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Wed, 15 Jan 2025 13:39:43 +0000 Subject: [PATCH 01/36] Remove unused SGX block --- include/ccf/pal/attestation.h | 167 ---------------------------------- 1 file changed, 167 deletions(-) diff --git a/include/ccf/pal/attestation.h b/include/ccf/pal/attestation.h index d0128f5fe4f9..f787ea8bae0f 100644 --- a/include/ccf/pal/attestation.h +++ b/include/ccf/pal/attestation.h @@ -291,173 +291,6 @@ namespace ccf::pal } } -#else // SGX - - static void generate_quote( - PlatformAttestationReportData& report_data, - RetrieveEndorsementCallback endorsement_cb, - const snp::EndorsementsServers& endorsements_servers = {}) - { - QuoteInfo node_quote_info = {}; - node_quote_info.format = QuoteFormat::oe_sgx_v1; - - sgx::Evidence evidence; - sgx::Endorsements endorsements; - sgx::SerialisedClaims serialised_custom_claims; - - const size_t custom_claim_length = 1; - oe_claim_t custom_claim; - custom_claim.name = const_cast(sgx::report_data_claim_name); - custom_claim.value = report_data.data.data(); - custom_claim.value_size = report_data.data.size(); - - auto rc = oe_serialize_custom_claims( - &custom_claim, - custom_claim_length, - &serialised_custom_claims.buffer, - &serialised_custom_claims.size); - if (rc != OE_OK) - { - throw std::logic_error(fmt::format( - "Could not serialise node's public key as quote custom claim: {}", - oe_result_str(rc))); - } - - rc = oe_get_evidence( - &sgx::oe_quote_format, - 0, - serialised_custom_claims.buffer, - serialised_custom_claims.size, - nullptr, - 0, - &evidence.buffer, - &evidence.size, - &endorsements.buffer, - &endorsements.size); - if (rc != OE_OK) - { - throw std::logic_error( - fmt::format("Failed to get evidence: {}", oe_result_str(rc))); - } - - node_quote_info.quote.assign( - evidence.buffer, evidence.buffer + evidence.size); - node_quote_info.endorsements.assign( - endorsements.buffer, endorsements.buffer + endorsements.size); - - if (endorsement_cb != nullptr) - { - endorsement_cb(node_quote_info, {}); - } - } - - static void verify_quote( - const QuoteInfo& quote_info, - PlatformAttestationMeasurement& measurement, - PlatformAttestationReportData& report_data) - { - if (quote_info.format == QuoteFormat::insecure_virtual) - { - throw std::logic_error(fmt::format( - "Cannot verify virtual insecure attestation report on SGX platform")); - } - else if (quote_info.format == QuoteFormat::amd_sev_snp_v1) - { - verify_snp_attestation_report(quote_info, measurement, report_data); - return; - } - - sgx::Claims claims; - - auto rc = oe_verify_evidence( - &sgx::oe_quote_format, - quote_info.quote.data(), - quote_info.quote.size(), - quote_info.endorsements.data(), - quote_info.endorsements.size(), - nullptr, - 0, - &claims.data, - &claims.length); - if (rc != OE_OK) - { - throw std::logic_error(fmt::format( - "Failed to verify evidence in SGX attestation report: {}", - oe_result_str(rc))); - } - - std::optional claim_measurement = std::nullopt; - std::optional custom_claim_report_data = - std::nullopt; - for (size_t i = 0; i < claims.length; i++) - { - auto& claim = claims.data[i]; - auto claim_name = std::string(claim.name); - if (claim_name == OE_CLAIM_UNIQUE_ID) - { - if (claim.value_size != SgxAttestationMeasurement::size()) - { - throw std::logic_error( - fmt::format("SGX measurement claim is not of expected size")); - } - - claim_measurement = - SgxAttestationMeasurement({claim.value, claim.value_size}); - } - else if (claim_name == OE_CLAIM_CUSTOM_CLAIMS_BUFFER) - { - // Find sgx report data in custom claims - sgx::CustomClaims custom_claims; - rc = oe_deserialize_custom_claims( - claim.value, - claim.value_size, - &custom_claims.data, - &custom_claims.length); - if (rc != OE_OK) - { - throw std::logic_error(fmt::format( - "Failed to deserialise custom claims in SGX attestation report", - oe_result_str(rc))); - } - - for (size_t j = 0; j < custom_claims.length; j++) - { - auto& custom_claim = custom_claims.data[j]; - if (std::string(custom_claim.name) == sgx::report_data_claim_name) - { - if (custom_claim.value_size != SgxAttestationReportData::size()) - { - throw std::logic_error(fmt::format( - "Expected claim {} of size {}, had size {}", - sgx::report_data_claim_name, - SgxAttestationReportData::size(), - custom_claim.value_size)); - } - - custom_claim_report_data = SgxAttestationReportData( - {custom_claim.value, custom_claim.value_size}); - - break; - } - } - } - } - - if (!claim_measurement.has_value()) - { - throw std::logic_error( - "Could not find measurement in SGX attestation report"); - } - - if (!custom_claim_report_data.has_value()) - { - throw std::logic_error( - "Could not find report data in SGX attestation report"); - } - - measurement = claim_measurement.value(); - report_data = custom_claim_report_data.value(); - } #endif class AttestationCollateralFetchingTimeout : public std::exception From deb614f2e0cd82e30b98aa26f0cb8a119a99b0a9 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Thu, 16 Jan 2025 10:11:02 +0000 Subject: [PATCH 02/36] Plausible SNP-style virtual attestation --- doc/schemas/gov_openapi.json | 75 ++++++++++- include/ccf/ds/quote_info.h | 2 +- include/ccf/pal/attestation.h | 69 +++++----- include/ccf/pal/measurement.h | 47 ++++--- include/ccf/pal/report_data.h | 5 + include/ccf/service/tables/host_data.h | 3 + .../ccf/service/tables/virtual_measurements.h | 19 +++ src/ds/files.h | 19 +-- src/host/main.cpp | 11 ++ src/node/gov/handlers/service_state.h | 10 ++ src/node/quote.cpp | 118 ++++++++++++------ src/node/rpc/member_frontend.h | 2 +- src/node/rpc/node_frontend.h | 41 ++++-- src/service/internal_tables_access.h | 24 +++- src/service/network_tables.h | 8 ++ 15 files changed, 344 insertions(+), 109 deletions(-) create mode 100644 include/ccf/service/tables/virtual_measurements.h diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json index 290b503c64b2..09fc01ad4612 100644 --- a/doc/schemas/gov_openapi.json +++ b/doc/schemas/gov_openapi.json @@ -1223,6 +1223,29 @@ }, "type": "object" }, + "VirtualAttestationMeasurement": { + "format": "hex", + "pattern": "^[a-f0-9]64$", + "type": "string" + }, + "VirtualAttestationMeasurement_to_CodeStatus": { + "items": { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/VirtualAttestationMeasurement" + }, + { + "$ref": "#/components/schemas/CodeStatus" + } + ] + }, + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "type": "array" + }, "base64string": { "format": "base64", "type": "string" @@ -1331,7 +1354,7 @@ "info": { "description": "This API is used to submit and query proposals which affect CCF's public governance tables.", "title": "CCF Governance API", - "version": "4.5.0" + "version": "4.5.1" }, "openapi": "3.0.0", "paths": { @@ -2153,6 +2176,56 @@ } } }, + "/gov/kv/nodes/virtual/host_data": { + "get": { + "deprecated": true, + "operationId": "GetGovKvNodesVirtualHostData", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sha256Digest_to_string" + } + } + }, + "description": "Default response description" + }, + "default": { + "$ref": "#/components/responses/default" + } + }, + "summary": "This route is auto-generated from the KV schema.", + "x-ccf-forwarding": { + "$ref": "#/components/x-ccf-forwarding/sometimes" + } + } + }, + "/gov/kv/nodes/virtual/measurements": { + "get": { + "deprecated": true, + "operationId": "GetGovKvNodesVirtualMeasurements", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VirtualAttestationMeasurement_to_CodeStatus" + } + } + }, + "description": "Default response description" + }, + "default": { + "$ref": "#/components/responses/default" + } + }, + "summary": "This route is auto-generated from the KV schema.", + "x-ccf-forwarding": { + "$ref": "#/components/x-ccf-forwarding/sometimes" + } + } + }, "/gov/kv/proposals": { "get": { "deprecated": true, diff --git a/include/ccf/ds/quote_info.h b/include/ccf/ds/quote_info.h index 2c11668db695..d61e08da0674 100644 --- a/include/ccf/ds/quote_info.h +++ b/include/ccf/ds/quote_info.h @@ -32,7 +32,7 @@ namespace ccf std::vector endorsements; /// UVM endorsements (SNP-only) std::optional> uvm_endorsements; - /// Endorsed TCB (hex-encoded) + /// Endorsed TCB (hex-encoded) (SNP-only) std::optional endorsed_tcb = std::nullopt; }; diff --git a/include/ccf/pal/attestation.h b/include/ccf/pal/attestation.h index f787ea8bae0f..5fb81fdb61e7 100644 --- a/include/ccf/pal/attestation.h +++ b/include/ccf/pal/attestation.h @@ -11,6 +11,9 @@ #include "ccf/pal/measurement.h" #include "ccf/pal/snp_ioctl.h" +// TODO: Public->private +#include "ds/files.h" + #include #include @@ -29,6 +32,19 @@ namespace ccf::pal const QuoteInfo& quote_info, const snp::EndorsementEndpointsConfiguration& config)>; + static void verify_virtual_attestation_report( + const QuoteInfo& quote_info, + PlatformAttestationMeasurement& measurement, + PlatformAttestationReportData& report_data) + { + auto j = nlohmann::json::parse(quote_info.quote); + + measurement = + VirtualAttestationMeasurement(j["measurement"].get()); + report_data = VirtualAttestationReportData( + j["report_data"].get>()); + } + // Verifying SNP attestation report is available on all platforms as unlike // SGX, this does not require external dependencies (Open Enclave for SGX). static void verify_snp_attestation_report( @@ -207,6 +223,19 @@ namespace ccf::pal } } + static void emit_virtual_measurement(const std::string& package_path) + { + auto package = files::slurp(package_path); + + auto package_hash = ccf::crypto::Sha256Hash(package); + + auto j = nlohmann::json::object(); + j["measurement"] = package_hash.hex_str(); + j["host_data"] = "TODO"; + + files::dump(j.dump(), "MY_VIRTUAL_ATTESTATION.measurement"); + } + #if defined(PLATFORM_VIRTUAL) static void generate_quote( @@ -214,9 +243,17 @@ namespace ccf::pal RetrieveEndorsementCallback endorsement_cb, const snp::EndorsementsServers& endorsements_servers = {}) { + auto quote = files::slurp_json("MY_VIRTUAL_ATTESTATION.measurement"); + quote["report_data"] = ccf::crypto::b64_from_raw(report_data.data); + + files::dump(quote.dump(), "MY_VIRTUAL_ATTESTATION.attestation"); + + auto dumped_quote = quote.dump(); + std::vector quote_vec(dumped_quote.begin(), dumped_quote.end()); + endorsement_cb( {.format = QuoteFormat::insecure_virtual, - .quote = {}, + .quote = quote_vec, .endorsements = {}, .uvm_endorsements = {}, .endorsed_tcb = {}}, @@ -253,44 +290,20 @@ namespace ccf::pal PlatformAttestationMeasurement& measurement, PlatformAttestationReportData& report_data) { - auto is_sev_snp = snp::is_sev_snp(); - if (quote_info.format == QuoteFormat::insecure_virtual) { - if (is_sev_snp) - { - throw std::logic_error( - "Cannot verify virtual attestation report if node is SEV-SNP"); - } - // For now, virtual resembles SGX (mostly for historical reasons) - measurement = SgxAttestationMeasurement(); - report_data = SgxAttestationReportData(); + verify_virtual_attestation_report(quote_info, measurement, report_data); } else if (quote_info.format == QuoteFormat::amd_sev_snp_v1) { - if (!is_sev_snp) - { - throw std::logic_error( - "Cannot verify SEV-SNP attestation report if node is virtual"); - } - verify_snp_attestation_report(quote_info, measurement, report_data); } else { - if (is_sev_snp) - { - throw std::logic_error( - "Cannot verify SGX attestation report if node is SEV-SNP"); - } - else - { - throw std::logic_error( - "Cannot verify SGX attestation report if node is virtual"); - } + throw std::logic_error( + "Cannot verify SGX attestation report in this build"); } } - #endif class AttestationCollateralFetchingTimeout : public std::exception diff --git a/include/ccf/pal/measurement.h b/include/ccf/pal/measurement.h index 3c3a8c609c6f..bea5fac47a9e 100644 --- a/include/ccf/pal/measurement.h +++ b/include/ccf/pal/measurement.h @@ -12,7 +12,7 @@ namespace ccf::pal { - template + template struct AttestationMeasurement { std::array measurement; @@ -51,20 +51,21 @@ namespace ccf::pal struct is_attestation_measurement : std::false_type {}; - template - struct is_attestation_measurement> : std::true_type + template + struct is_attestation_measurement> + : std::true_type {}; - template + template inline void to_json( - nlohmann::json& j, const AttestationMeasurement& measurement) + nlohmann::json& j, const AttestationMeasurement& measurement) { j = measurement.hex_str(); } - template + template inline void from_json( - const nlohmann::json& j, AttestationMeasurement& measurement) + const nlohmann::json& j, AttestationMeasurement& measurement) { if (j.is_string()) { @@ -77,9 +78,9 @@ namespace ccf::pal } } - template + template inline void fill_json_schema( - nlohmann::json& schema, const AttestationMeasurement*) + nlohmann::json& schema, const AttestationMeasurement*) { schema["type"] = "string"; @@ -88,7 +89,19 @@ namespace ccf::pal // https://swagger.io/docs/specification/data-models/data-types/#format schema["format"] = "hex"; schema["pattern"] = - fmt::format("^[a-f0-9]{}$", AttestationMeasurement::size() * 2); + fmt::format("^[a-f0-9]{}$", AttestationMeasurement::size() * 2); + } + + // Virtual + static constexpr size_t virtual_attestation_measurement_size = 32; + struct VirtualTag + {}; + using VirtualAttestationMeasurement = + AttestationMeasurement; + + inline std::string schema_name(const VirtualAttestationMeasurement*) + { + return "VirtualAttestationMeasurement"; } // SGX @@ -120,9 +133,9 @@ namespace ccf::pal PlatformAttestationMeasurement(const PlatformAttestationMeasurement&) = default; - template + template PlatformAttestationMeasurement( - const AttestationMeasurement& measurement) : + const AttestationMeasurement& measurement) : data(measurement.measurement.begin(), measurement.measurement.end()) {} @@ -145,20 +158,20 @@ namespace ccf::pal namespace ccf::kv::serialisers { - template - struct BlitSerialiser> + template + struct BlitSerialiser> { static SerialisedEntry to_serialised( - const ccf::pal::AttestationMeasurement& measurement) + const ccf::pal::AttestationMeasurement& measurement) { auto hex_str = measurement.hex_str(); return SerialisedEntry(hex_str.begin(), hex_str.end()); } - static ccf::pal::AttestationMeasurement from_serialised( + static ccf::pal::AttestationMeasurement from_serialised( const SerialisedEntry& data) { - ccf::pal::AttestationMeasurement ret; + ccf::pal::AttestationMeasurement ret; ccf::ds::from_hex(std::string(data.data(), data.end()), ret.measurement); return ret; } diff --git a/include/ccf/pal/report_data.h b/include/ccf/pal/report_data.h index 6fe8a7e6e087..7db197efb35d 100644 --- a/include/ccf/pal/report_data.h +++ b/include/ccf/pal/report_data.h @@ -36,6 +36,11 @@ namespace ccf::pal } }; + // Virtual + static constexpr size_t virtual_attestation_report_data_size = 32; + using VirtualAttestationReportData = + AttestationReportData; + // SGX static constexpr size_t sgx_attestation_report_data_size = 32; using SgxAttestationReportData = diff --git a/include/ccf/service/tables/host_data.h b/include/ccf/service/tables/host_data.h index 98c915417a2c..1fab20a36419 100644 --- a/include/ccf/service/tables/host_data.h +++ b/include/ccf/service/tables/host_data.h @@ -12,8 +12,11 @@ using HostDataMetadata = namespace ccf { using SnpHostDataMap = ServiceMap; + using VirtualHostDataMap = ServiceMap; namespace Tables { static constexpr auto HOST_DATA = "public:ccf.gov.nodes.snp.host_data"; + static constexpr auto VIRTUAL_HOST_DATA = + "public:ccf.gov.nodes.virtual.host_data"; } } \ No newline at end of file diff --git a/include/ccf/service/tables/virtual_measurements.h b/include/ccf/service/tables/virtual_measurements.h new file mode 100644 index 000000000000..9c0be45330a5 --- /dev/null +++ b/include/ccf/service/tables/virtual_measurements.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "ccf/pal/measurement.h" +#include "ccf/service/code_status.h" +#include "ccf/service/map.h" + +namespace ccf +{ + using VirtualMeasurements = + ServiceMap; + + namespace Tables + { + static constexpr auto NODE_VIRTUAL_MEASUREMENTS = + "public:ccf.gov.nodes.virtual.measurements"; + } +} \ No newline at end of file diff --git a/src/ds/files.h b/src/ds/files.h index 7b3e058535d7..694c6841d867 100644 --- a/src/ds/files.h +++ b/src/ds/files.h @@ -26,7 +26,7 @@ namespace files * @param file file to check * @return true if the file exists. */ - bool exists(const std::string& file) + static bool exists(const std::string& file) { std::ifstream f(file.c_str()); return f.good(); @@ -40,7 +40,8 @@ namespace files * exist. If true, an empty vector is returned. If false, the process exits * @return vector the file contents as bytes. */ - std::vector slurp(const std::string& file, bool optional = false) + static std::vector slurp( + const std::string& file, bool optional = false) { std::ifstream f(file, std::ios::binary | std::ios::ate); @@ -79,13 +80,14 @@ namespace files * exist. If true, an empty vector is returned. If false, the process exits * @return std::string the file contents as a string. */ - std::string slurp_string(const std::string& file, bool optional = false) + static std::string slurp_string( + const std::string& file, bool optional = false) { auto v = slurp(file, optional); return {v.begin(), v.end()}; } - std::optional try_slurp_string(const std::string& file) + static std::optional try_slurp_string(const std::string& file) { if (!fs::exists(file)) { @@ -103,7 +105,8 @@ namespace files * exits * @return nlohmann::json JSON object containing the parsed file */ - nlohmann::json slurp_json(const std::string& file, bool optional = false) + static nlohmann::json slurp_json( + const std::string& file, bool optional = false) { auto v = slurp(file, optional); if (v.size() == 0) @@ -118,7 +121,7 @@ namespace files * @param data vector to write * @param file the path */ - void dump(const std::vector& data, const std::string& file) + static void dump(const std::vector& data, const std::string& file) { using namespace std; ofstream f(file, ios::binary | ios::trunc); @@ -133,12 +136,12 @@ namespace files * @param data string to write * @param file the path */ - void dump(const std::string& data, const std::string& file) + static void dump(const std::string& data, const std::string& file) { return dump(std::vector(data.begin(), data.end()), file); } - void rename(const fs::path& src, const fs::path& dst) + static void rename(const fs::path& src, const fs::path& dst) { std::error_code ec; fs::rename(src, dst, ec); diff --git a/src/host/main.cpp b/src/host/main.cpp index e4a7a43b5122..ca833f70a866 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -9,6 +9,7 @@ #include "ccf/version.h" #include "config_schema.h" #include "configuration.h" +#include "crypto/openssl/hash.h" #include "ds/cli_helper.h" #include "ds/files.h" #include "ds/non_blocking.h" @@ -72,6 +73,9 @@ int main(int argc, char** argv) { // ignore SIGPIPE signal(SIGPIPE, SIG_IGN); + + ccf::crypto::openssl_sha256_init(); + CLI::App app{ "CCF Host launcher. Runs a single CCF node, based on the given " "configuration file.\n" @@ -292,6 +296,11 @@ int main(int argc, char** argv) return static_cast(CLI::ExitCodes::ValidationError); } + if (config.enclave.platform == host::EnclavePlatform::VIRTUAL) + { + ccf::pal::emit_virtual_measurement(enclave_file_path); + } + host::Enclave enclave( enclave_file_path, config.enclave.type, config.enclave.platform); @@ -825,5 +834,7 @@ int main(int argc, char** argv) if (rc) LOG_FAIL_FMT("Failed to close uv loop cleanly: {}", uv_err_name(rc)); + ccf::crypto::openssl_sha256_shutdown(); + return rc; } diff --git a/src/node/gov/handlers/service_state.h b/src/node/gov/handlers/service_state.h index 6992fabb3f1c..5c40b6897fcc 100644 --- a/src/node/gov/handlers/service_state.h +++ b/src/node/gov/handlers/service_state.h @@ -93,7 +93,12 @@ namespace ccf::gov::endpoints } case ccf::QuoteFormat::insecure_virtual: { + // TODO quote_info["format"] = "Insecure_Virtual"; + quote_info["quote"] = + ccf::crypto::b64_from_raw(node_info.quote_info.quote); + quote_info["endorsements"] = + ccf::crypto::b64_from_raw(node_info.quote_info.endorsements); break; } case ccf::QuoteFormat::amd_sev_snp_v1: @@ -499,6 +504,11 @@ namespace ccf::gov::endpoints response_body["sgx"] = sgx_policy; } + // Describe Virtual join policy + { + // TODO + } + // Describe SNP join policy { auto snp_policy = nlohmann::json::object(); diff --git a/src/node/quote.cpp b/src/node/quote.cpp index 68afc95f733c..74d3943646de 100644 --- a/src/node/quote.cpp +++ b/src/node/quote.cpp @@ -7,6 +7,7 @@ #include "ccf/service/tables/code_id.h" #include "ccf/service/tables/snp_measurements.h" #include "ccf/service/tables/uvm_endorsements.h" +#include "ccf/service/tables/virtual_measurements.h" #include "node/uvm_endorsements.h" namespace ccf @@ -58,8 +59,16 @@ namespace ccf case QuoteFormat::oe_sgx_v1: { if (!tx.ro(Tables::NODE_CODE_IDS) - ->get(pal::SgxAttestationMeasurement(quote_measurement)) - .has_value()) + ->has(pal::SgxAttestationMeasurement(quote_measurement))) + { + return QuoteVerificationResult::FailedMeasurementNotFound; + } + break; + } + case QuoteFormat::insecure_virtual: + { + if (!tx.ro(Tables::NODE_VIRTUAL_MEASUREMENTS) + ->has(pal::VirtualAttestationMeasurement(quote_measurement))) { return QuoteVerificationResult::FailedMeasurementNotFound; } @@ -80,8 +89,7 @@ namespace ccf else { if (!tx.ro(Tables::NODE_SNP_MEASUREMENTS) - ->get(pal::SnpAttestationMeasurement(quote_measurement)) - .has_value()) + ->has(pal::SnpAttestationMeasurement(quote_measurement))) { return QuoteVerificationResult::FailedMeasurementNotFound; } @@ -132,36 +140,60 @@ namespace ccf std::optional AttestationProvider::get_host_data( const QuoteInfo& quote_info) { - if (quote_info.format != QuoteFormat::amd_sev_snp_v1) + switch (quote_info.format) { - return std::nullopt; - } + case QuoteFormat::insecure_virtual: + { + auto j = nlohmann::json::parse(quote_info.quote); + auto it = j.find("host_data"); + if (it != j.end()) + { + const auto raw_host_data = it->get(); + return ccf::crypto::Sha256Hash(raw_host_data); + } - HostData digest{}; - HostData::Representation rep{}; - pal::PlatformAttestationMeasurement d = {}; - pal::PlatformAttestationReportData r = {}; - try - { - pal::verify_quote(quote_info, d, r); - auto quote = *reinterpret_cast( - quote_info.quote.data()); - std::copy( - std::begin(quote.host_data), std::end(quote.host_data), rep.begin()); - } - catch (const std::exception& e) - { - LOG_FAIL_FMT("Failed to verify attestation report: {}", e.what()); - return std::nullopt; - } + LOG_FAIL_FMT("No host data in virtual attestation"); + return std::nullopt; + } + + case QuoteFormat::amd_sev_snp_v1: + { + HostData digest{}; + HostData::Representation rep{}; + pal::PlatformAttestationMeasurement d = {}; + pal::PlatformAttestationReportData r = {}; + try + { + pal::verify_quote(quote_info, d, r); + auto quote = *reinterpret_cast( + quote_info.quote.data()); + std::copy( + std::begin(quote.host_data), + std::end(quote.host_data), + rep.begin()); + } + catch (const std::exception& e) + { + LOG_FAIL_FMT("Failed to verify attestation report: {}", e.what()); + return std::nullopt; + } - return digest.from_representation(rep); + return digest.from_representation(rep); + } + + default: + { + return std::nullopt; + } + } } QuoteVerificationResult verify_host_data_against_store( ccf::kv::ReadOnlyTx& tx, const QuoteInfo& quote_info) { - if (quote_info.format != QuoteFormat::amd_sev_snp_v1) + if ( + quote_info.format != QuoteFormat::amd_sev_snp_v1 && + quote_info.format != QuoteFormat::insecure_virtual) { throw std::logic_error( "Attempted to verify host data for an unsupported platform"); @@ -173,9 +205,21 @@ namespace ccf return QuoteVerificationResult::FailedHostDataDigestNotFound; } - auto accepted_policies_table = tx.ro(Tables::HOST_DATA); - auto accepted_policy = accepted_policies_table->get(host_data.value()); - if (!accepted_policy.has_value()) + bool accepted_policy = false; + + if (quote_info.format == QuoteFormat::insecure_virtual) + { + auto accepted_policies_table = + tx.ro(Tables::VIRTUAL_HOST_DATA); + accepted_policy = accepted_policies_table->has(host_data.value()); + } + else if (quote_info.format == QuoteFormat::amd_sev_snp_v1) + { + auto accepted_policies_table = tx.ro(Tables::HOST_DATA); + accepted_policy = accepted_policies_table->has(host_data.value()); + } + + if (!accepted_policy) { return QuoteVerificationResult::FailedInvalidHostData; } @@ -202,21 +246,13 @@ namespace ccf return QuoteVerificationResult::Failed; } - if (quote_info.format == QuoteFormat::insecure_virtual) - { - LOG_INFO_FMT("Skipped attestation report verification"); - return QuoteVerificationResult::Verified; - } - else if (quote_info.format == QuoteFormat::amd_sev_snp_v1) + auto rc = verify_host_data_against_store(tx, quote_info); + if (rc != QuoteVerificationResult::Verified) { - auto rc = verify_host_data_against_store(tx, quote_info); - if (rc != QuoteVerificationResult::Verified) - { - return rc; - } + return rc; } - auto rc = verify_enclave_measurement_against_store( + rc = verify_enclave_measurement_against_store( tx, measurement, quote_info.format, quote_info.uvm_endorsements); if (rc != QuoteVerificationResult::Verified) { diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index c4f4a5da3a70..883f923ac8fe 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -600,7 +600,7 @@ namespace ccf openapi_info.description = "This API is used to submit and query proposals which affect CCF's " "public governance tables."; - openapi_info.document_version = "4.5.0"; + openapi_info.document_version = "4.5.1"; } static std::optional get_caller_member_id( diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index d28dfa36bd45..2f90b48ae1b5 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1558,6 +1558,7 @@ namespace ccf in.public_key, in.node_data}; InternalTablesAccess::add_node(ctx.tx, in.node_id, node_info); + if ( in.quote_info.format != QuoteFormat::amd_sev_snp_v1 || !in.snp_uvm_endorsements.has_value()) @@ -1567,14 +1568,40 @@ namespace ccf InternalTablesAccess::trust_node_measurement( ctx.tx, in.measurement, in.quote_info.format); } - if (in.quote_info.format == QuoteFormat::amd_sev_snp_v1) + + switch (in.quote_info.format) { - auto host_data = - AttestationProvider::get_host_data(in.quote_info).value(); - InternalTablesAccess::trust_node_host_data( - ctx.tx, host_data, in.snp_security_policy); - InternalTablesAccess::trust_node_uvm_endorsements( - ctx.tx, in.snp_uvm_endorsements); + case QuoteFormat::insecure_virtual: + { + auto host_data = AttestationProvider::get_host_data(in.quote_info); + if (host_data.has_value()) + { + InternalTablesAccess::trust_node_virtual_host_data( + ctx.tx, host_data.value()); + } + else + { + LOG_FAIL_FMT("Unable to extract host data from virtual quote"); + } + break; + } + + case QuoteFormat::amd_sev_snp_v1: + { + auto host_data = + AttestationProvider::get_host_data(in.quote_info).value(); + InternalTablesAccess::trust_node_host_data( + ctx.tx, host_data, in.snp_security_policy); + + InternalTablesAccess::trust_node_uvm_endorsements( + ctx.tx, in.snp_uvm_endorsements); + break; + } + + default: + { + break; + } } std::optional digest = diff --git a/src/service/internal_tables_access.h b/src/service/internal_tables_access.h index 4ce2300be3c8..f247527b1bff 100644 --- a/src/service/internal_tables_access.h +++ b/src/service/internal_tables_access.h @@ -10,6 +10,7 @@ #include "ccf/service/tables/nodes.h" #include "ccf/service/tables/snp_measurements.h" #include "ccf/service/tables/users.h" +#include "ccf/service/tables/virtual_measurements.h" #include "ccf/tx.h" #include "consensus/aft/raft_types.h" #include "crypto/openssl/cose_sign.h" @@ -587,12 +588,17 @@ namespace ccf { switch (platform) { - // For now, record null code id for virtual platform in SGX code id - // table case QuoteFormat::insecure_virtual: + { + tx.wo(Tables::NODE_VIRTUAL_MEASUREMENTS) + ->put( + pal::VirtualAttestationMeasurement(node_measurement), + CodeStatus::ALLOWED_TO_JOIN); + break; + } case QuoteFormat::oe_sgx_v1: { - tx.rw(Tables::NODE_CODE_IDS) + tx.wo(Tables::NODE_CODE_IDS) ->put( pal::SgxAttestationMeasurement(node_measurement), CodeStatus::ALLOWED_TO_JOIN); @@ -600,7 +606,7 @@ namespace ccf } case QuoteFormat::amd_sev_snp_v1: { - tx.rw(Tables::NODE_SNP_MEASUREMENTS) + tx.wo(Tables::NODE_SNP_MEASUREMENTS) ->put( pal::SnpAttestationMeasurement(node_measurement), CodeStatus::ALLOWED_TO_JOIN); @@ -614,12 +620,20 @@ namespace ccf } } + static void trust_node_virtual_host_data( + ccf::kv::Tx& tx, const HostData& host_data) + { + auto host_data_table = + tx.wo(Tables::VIRTUAL_HOST_DATA); + host_data_table->put(host_data, ""); + } + static void trust_node_host_data( ccf::kv::Tx& tx, const HostData& host_data, const std::optional& security_policy = std::nullopt) { - auto host_data_table = tx.rw(Tables::HOST_DATA); + auto host_data_table = tx.wo(Tables::HOST_DATA); if (security_policy.has_value()) { auto raw_security_policy = diff --git a/src/service/network_tables.h b/src/service/network_tables.h index 9a1df3a7348e..a5576516a227 100644 --- a/src/service/network_tables.h +++ b/src/service/network_tables.h @@ -20,6 +20,7 @@ #include "ccf/service/tables/snp_measurements.h" #include "ccf/service/tables/users.h" #include "ccf/service/tables/uvm_endorsements.h" +#include "ccf/service/tables/virtual_measurements.h" #include "kv/store.h" #include "tables/config.h" #include "tables/governance_history.h" @@ -86,6 +87,11 @@ namespace ccf const NodeEndorsedCertificates node_endorsed_certificates = { Tables::NODE_ENDORSED_CERTIFICATES}; const ACMECertificates acme_certificates = {Tables::ACME_CERTIFICATES}; + + const VirtualHostDataMap virtual_host_data = {Tables::VIRTUAL_HOST_DATA}; + const VirtualMeasurements virtual_measurements = { + Tables::NODE_VIRTUAL_MEASUREMENTS}; + const SnpHostDataMap host_data = {Tables::HOST_DATA}; const SnpMeasurements snp_measurements = {Tables::NODE_SNP_MEASUREMENTS}; const SNPUVMEndorsements snp_uvm_endorsements = { @@ -98,6 +104,8 @@ namespace ccf nodes, node_endorsed_certificates, acme_certificates, + virtual_host_data, + virtual_measurements, host_data, snp_measurements, snp_uvm_endorsements); From 4cafc933e776b06b46a723fecd9a91b0533d44e8 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Thu, 16 Jan 2025 15:43:55 +0000 Subject: [PATCH 03/36] Rename response's mrenclave to measurement --- src/node/rpc/node_frontend.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 2f90b48ae1b5..316bdb427cfb 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -35,7 +35,7 @@ namespace ccf std::vector endorsements; QuoteFormat format; - std::string mrenclave = {}; // < Hex-encoded + std::string measurement = {}; // < Hex-encoded std::optional> uvm_endorsements = std::nullopt; // SNP only @@ -43,7 +43,7 @@ namespace ccf DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Quote); DECLARE_JSON_REQUIRED_FIELDS(Quote, node_id, raw, endorsements, format); - DECLARE_JSON_OPTIONAL_FIELDS(Quote, mrenclave, uvm_endorsements); + DECLARE_JSON_OPTIONAL_FIELDS(Quote, measurement, uvm_endorsements); struct GetQuotes { @@ -725,7 +725,7 @@ namespace ccf auto node_info = nodes->get(context.get_node_id()); if (node_info.has_value() && node_info->code_digest.has_value()) { - q.mrenclave = node_info->code_digest.value(); + q.measurement = node_info->code_digest.value(); } else { @@ -733,7 +733,7 @@ namespace ccf AttestationProvider::get_measurement(node_quote_info); if (measurement.has_value()) { - q.mrenclave = measurement.value().hex_str(); + q.measurement = measurement.value().hex_str(); } else { @@ -792,7 +792,7 @@ namespace ccf // unreliable get_measurement otherwise. if (node_info.code_digest.has_value()) { - q.mrenclave = node_info.code_digest.value(); + q.measurement = node_info.code_digest.value(); } else { @@ -800,7 +800,7 @@ namespace ccf AttestationProvider::get_measurement(node_info.quote_info); if (measurement.has_value()) { - q.mrenclave = measurement.value().hex_str(); + q.measurement = measurement.value().hex_str(); } } quotes.emplace_back(q); From a85285053a20120c022651506f73be2a8401ca6b Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Thu, 16 Jan 2025 15:44:47 +0000 Subject: [PATCH 04/36] Comment tweak --- tests/npm_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/npm_tests.py b/tests/npm_tests.py index 703476106963..c20791e89b34 100644 --- a/tests/npm_tests.py +++ b/tests/npm_tests.py @@ -904,7 +904,7 @@ def corrupt_value(value: str): ) assert r.status_code == http.HTTPStatus.BAD_REQUEST, r.status_code - # Test that static attestation report format is still valid on both SNP and virtual + # Test that static attestation report format can be verified on both SNP and virtual if args.enclave_platform != "sgx": LOG.info( "Virtual: Test verifySnpAttestation with a static attestation report" From 76bb8b73aaac1a2cd9a30dbbf67ee54a46d525b3 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Thu, 16 Jan 2025 15:45:55 +0000 Subject: [PATCH 05/36] Baby steps - Python code to verify virtual quotes --- tests/code_update.py | 55 +++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/tests/code_update.py b/tests/code_update.py index 8c8186b27e81..dcf16fd0a03a 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -1,11 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache 2.0 License. -from base64 import b64encode +from base64 import b64encode, b64decode import infra.e2e_args import infra.network import infra.path import infra.proc import infra.utils +import infra.crypto import suite.test_requirements as reqs import os from infra.checker import check_can_progress @@ -13,41 +14,47 @@ import tempfile import shutil import http +import hashlib +import json -from loguru import logger as LOG -# Dummy code id used by virtual nodes -VIRTUAL_CODE_ID = "0" * 64 +from loguru import logger as LOG @reqs.description("Verify node evidence") def test_verify_quotes(network, args): - if args.enclave_platform == "virtual": - LOG.warning("Skipping quote test with virtual enclave") - return network - elif snp.IS_SNP: - LOG.warning( - "Skipping quote test until there is a separate utility to verify SNP quotes" - ) - return network - LOG.info("Check the network is stable") primary, _ = network.find_primary() check_can_progress(primary) for node in network.get_joined_nodes(): LOG.info(f"Verifying quote for node {node.node_id}") - cafile = os.path.join(network.common_dir, "service_cert.pem") - assert ( - infra.proc.ccall( - "verify_quote.sh", - f"https://{node.get_public_rpc_host()}:{node.get_public_rpc_port()}", - "--cacert", - f"{cafile}", - log_output=True, - ).returncode - == 0 - ), f"Quote verification for node {node.node_id} failed" + with node.client() as c: + r = c.get("/node/quotes/self") + assert r.status_code == http.HTTPStatus.OK, r + + j = r.body.json() + if j["format"] == "Insecure_Virtual": + # A virtual attestation makes 2 claims: + # - The measurement (equal to any equivalent node) is the sha256 of the package (library) it loaded + package_path = infra.path.build_lib_path( + args.package, args.enclave_type, args.enclave_platform + ) + digest = hashlib.sha256(open(package_path, "rb").read()) + assert j["measurement"] == digest.hexdigest() + + raw = json.loads(b64decode(j["raw"])) + assert raw["measurement"] == j["measurement"] + + # - The report_data (unique to this node) is the sha256 of the node's public key, in DER encoding + # That is the same value we use as the node's ID, though that is usually represented as a hex string + report_data = b64decode(raw["report_data"]) + assert report_data.hex() == node.node_id + + elif j["format"] == "AMD_SEV_SNP_v1": + LOG.warning( + "Skipping client-side verification of SNP node's quote until there is a separate utility to verify SNP quotes" + ) return network From d8e8aa4f8fcca38b68de385bf71e4c33abf642c9 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 17 Jan 2025 11:01:58 +0000 Subject: [PATCH 06/36] Working virtual code update --- samples/constitutions/default/actions.js | 31 ++++++++ src/node/gov/handlers/service_state.h | 31 +++++++- tests/code_update.py | 93 +++++++++++++----------- tests/infra/consortium.py | 34 +++++++++ tests/infra/utils.py | 11 ++- 5 files changed, 153 insertions(+), 47 deletions(-) diff --git a/samples/constitutions/default/actions.js b/samples/constitutions/default/actions.js index 98f0ed2f7c0d..aa9813c6f80b 100644 --- a/samples/constitutions/default/actions.js +++ b/samples/constitutions/default/actions.js @@ -1004,6 +1004,25 @@ const actions = new Map([ }, ), ], + [ + "add_virtual_measurement", + new Action( + function (args) { + checkType(args.measurement, "string", "measurement"); + }, + function (args, proposalId) { + const measurement = ccf.strToBuf(args.measurement); + const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToJoin"); + ccf.kv["public:ccf.gov.nodes.virtual.measurements"].set( + measurement, + ALLOWED, + ); + + // Adding a new allowed measurement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification + invalidateOtherOpenProposals(proposalId); + }, + ), + ], [ "add_snp_measurement", new Action( @@ -1106,6 +1125,18 @@ const actions = new Map([ }, ), ], + [ + "remove_virtual_measurement", + new Action( + function (args) { + checkType(args.measurement, "string", "measurement"); + }, + function (args) { + const measurement = ccf.strToBuf(args.measurement); + ccf.kv["public:ccf.gov.nodes.virtual.measurements"].delete(measurement); + }, + ), + ], [ "remove_snp_measurement", new Action( diff --git a/src/node/gov/handlers/service_state.h b/src/node/gov/handlers/service_state.h index 5c40b6897fcc..3ea2a5f31354 100644 --- a/src/node/gov/handlers/service_state.h +++ b/src/node/gov/handlers/service_state.h @@ -506,7 +506,36 @@ namespace ccf::gov::endpoints // Describe Virtual join policy { - // TODO + auto virtual_policy = nlohmann::json::object(); + + auto virtual_measurements = nlohmann::json::array(); + auto measurements_handle = + ctx.tx.template ro( + ccf::Tables::NODE_VIRTUAL_MEASUREMENTS); + measurements_handle->foreach( + [&virtual_measurements]( + const pal::VirtualAttestationMeasurement& measurement, + const ccf::CodeStatus& status) { + if (status == ccf::CodeStatus::ALLOWED_TO_JOIN) + { + virtual_measurements.push_back(measurement.hex_str()); + } + return true; + }); + virtual_policy["measurements"] = virtual_measurements; + + auto virtual_host_data = nlohmann::json::object(); + auto host_data_handle = ctx.tx.template ro( + ccf::Tables::VIRTUAL_HOST_DATA); + host_data_handle->foreach( + [&virtual_host_data]( + const HostData& host_data, const HostDataMetadata& metadata) { + virtual_host_data[host_data.hex_str()] = metadata; + return true; + }); + virtual_policy["hostData"] = virtual_host_data; + + response_body["virtual"] = virtual_policy; } // Describe SNP join policy diff --git a/tests/code_update.py b/tests/code_update.py index dcf16fd0a03a..60447d47ba20 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -14,7 +14,6 @@ import tempfile import shutil import http -import hashlib import json @@ -27,6 +26,11 @@ def test_verify_quotes(network, args): primary, _ = network.find_primary() check_can_progress(primary) + with primary.api_versioned_client(api_version=args.gov_api_version) as uc: + r = uc.get("/gov/service/join-policy") + assert r.status_code == http.HTTPStatus.OK, r + trusted_measurements = r.body.json()[args.enclave_platform]["measurements"] + for node in network.get_joined_nodes(): LOG.info(f"Verifying quote for node {node.node_id}") with node.client() as c: @@ -37,20 +41,29 @@ def test_verify_quotes(network, args): if j["format"] == "Insecure_Virtual": # A virtual attestation makes 2 claims: # - The measurement (equal to any equivalent node) is the sha256 of the package (library) it loaded - package_path = infra.path.build_lib_path( - args.package, args.enclave_type, args.enclave_platform + claimed_measurement = j["measurement"] + expected_measurement = infra.utils.get_measurement( + args.enclave_type, args.enclave_platform, "", args.package ) - digest = hashlib.sha256(open(package_path, "rb").read()) - assert j["measurement"] == digest.hexdigest() + assert ( + claimed_measurement == expected_measurement + ), f"{claimed_measurement} != {expected_measurement}" raw = json.loads(b64decode(j["raw"])) - assert raw["measurement"] == j["measurement"] + assert raw["measurement"] == claimed_measurement # - The report_data (unique to this node) is the sha256 of the node's public key, in DER encoding # That is the same value we use as the node's ID, though that is usually represented as a hex string report_data = b64decode(raw["report_data"]) assert report_data.hex() == node.node_id + # Additionally, we check that the measurement is one of the service's currently trusted measurements. + # Note this might not always be true - a node may be added while its measurement is trusted, and persist past the point that its measurement is retired! + # But it _is_ true in this test, and a sensible thing to check most of the time + assert ( + claimed_measurement in trusted_measurements + ), f"This node's measurement ({claimed_measurement}) is not one of the currently trusted measurements ({trusted_measurements})" + elif j["format"] == "AMD_SEV_SNP_v1": LOG.warning( "Skipping client-side verification of SNP node's quote until there is a separate utility to verify SNP quotes" @@ -317,7 +330,7 @@ def test_add_node_with_no_uvm_endorsements(network, args): primary, _ = network.find_nodes() with primary.client() as client: r = client.get("/node/quotes/self") - measurement = r.body.json()["mrenclave"] + measurement = r.body.json()["measurement"] network.consortium.add_snp_measurement(primary, measurement) LOG.info("Add new node without UVM endorsements (expect success)") @@ -339,21 +352,13 @@ def test_add_node_with_no_uvm_endorsements(network, args): @reqs.description("Node with bad code fails to join") def test_add_node_with_bad_code(network, args): - if args.enclave_platform != "sgx": - LOG.warning("Skipping test_add_node_with_bad_code with non-sgx enclave") + if args.enclave_platform == "snp": + LOG.warning("Skipping test_add_node_with_bad_code with SNP enclave") return network - replacement_package = ( - "samples/apps/logging/liblogging" - if args.package == "libjs_generic" - else "libjs_generic" - ) - - new_code_id = infra.utils.get_code_id( - args.enclave_type, args.enclave_platform, args.oe_binary, replacement_package - ) + replacement_package = get_replacement_package(args) - LOG.info(f"Adding a node with unsupported code id {new_code_id}") + LOG.info(f"Adding unsupported node running {replacement_package}") code_not_found_exception = None try: new_node = network.create_node("local://localhost") @@ -363,7 +368,7 @@ def test_add_node_with_bad_code(network, args): assert ( code_not_found_exception is not None - ), f"Adding a node with unsupported code id {new_code_id} should fail" + ), f"Adding a node with {replacement_package} should fail" return network @@ -385,41 +390,37 @@ def test_update_all_nodes(network, args): primary, _ = network.find_nodes() - first_code_id = infra.utils.get_code_id( + first_measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, args.oe_binary, args.package ) - new_code_id = infra.utils.get_code_id( + new_measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, args.oe_binary, replacement_package ) - if args.enclave_platform == "virtual": - # Pretend this was already present - network.consortium.add_new_code(primary, first_code_id) - - LOG.info("Add new code id") - network.consortium.add_new_code(primary, new_code_id) + LOG.info("Add new measurement") + network.consortium.add_measurement(primary, args.enclave_platform, new_measurement) with primary.api_versioned_client(api_version=args.gov_api_version) as uc: LOG.info("Check reported trusted measurements") r = uc.get("/gov/service/join-policy") assert r.status_code == http.HTTPStatus.OK, r - versions: list = r.body.json()["sgx"]["measurements"] + versions: list = r.body.json()[args.enclave_platform]["measurements"] - expected = [first_code_id, new_code_id] - if args.enclave_platform == "virtual": - expected.append(VIRTUAL_CODE_ID) + expected = [first_measurement, new_measurement] versions.sort() expected.sort() assert versions == expected, f"{versions} != {expected}" - LOG.info("Remove old code id") - network.consortium.retire_code(primary, first_code_id) + LOG.info("Remove old measurement") + network.consortium.remove_measurement( + primary, args.enclave_platform, first_measurement + ) r = uc.get("/gov/service/join-policy") assert r.status_code == http.HTTPStatus.OK, r - versions = r.body.json()["sgx"]["measurements"] + versions = r.body.json()[args.enclave_platform]["measurements"] - expected.remove(first_code_id) + expected.remove(first_measurement) versions.sort() assert versions == expected, f"{versions} != {expected}" @@ -445,12 +446,14 @@ def test_update_all_nodes(network, args): check_can_progress(new_primary) node.stop() + args.package = replacement_package + LOG.info("Check the network is still functional") check_can_progress(new_node) return network -@reqs.description("Adding a new code ID invalidates open proposals") +@reqs.description("Adding a new measurement invalidates open proposals") def test_proposal_invalidation(network, args): primary, _ = network.find_nodes() @@ -462,14 +465,16 @@ def test_proposal_invalidation(network, args): ) pending_proposals.append(new_member_proposal.proposal_id) - LOG.info("Add temporary code ID") - temp_code_id = infra.utils.get_code_id( + LOG.info("Add temporary measurement") + temporary_measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, args.oe_binary, get_replacement_package(args), ) - network.consortium.add_new_code(primary, temp_code_id) + network.consortium.add_measurement( + primary, args.enclave_platform, temporary_measurement + ) LOG.info("Confirm open proposals are dropped") with primary.api_versioned_client( @@ -480,8 +485,10 @@ def test_proposal_invalidation(network, args): assert r.status_code == 200, r.body.text() assert r.body.json()["proposalState"] == "Dropped", r.body.json() - LOG.info("Remove temporary code ID") - network.consortium.retire_code(primary, temp_code_id) + LOG.info("Remove temporary measurement") + network.consortium.remove_measurement( + primary, args.enclave_platform, temporary_measurement + ) return network @@ -530,7 +537,7 @@ def run(args): test_start_node_with_mismatched_host_data(network, args) test_add_node_with_bad_host_data(network, args) test_add_node_with_bad_code(network, args) - # NB: Assumes the current nodes are still using args.package, so must run before test_proposal_invalidation + # NB: Assumes the current nodes are still using args.package, so must run before test_update_all_nodes test_proposal_invalidation(network, args) if not snp.IS_SNP: diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 759be70bea64..6b1baeae44e7 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -732,6 +732,16 @@ def set_recovery_threshold(self, remote_node, recovery_threshold): self.recovery_threshold = recovery_threshold return r + def add_measurement(self, remote_node, platform, measurement): + if platform == "sgx": + return self.add_new_code(remote_node, measurement) + elif platform == "virtual": + return self.add_virtual_measurement(remote_node, measurement) + elif platform == "snp": + return self.add_snp_measurement(remote_node, measurement) + else: + raise ValueError(f"Unsupported platform {platform}") + def add_new_code(self, remote_node, new_code_id): proposal_body, careful_vote = self.make_proposal( "add_node_code", code_id=new_code_id @@ -739,6 +749,13 @@ def add_new_code(self, remote_node, new_code_id): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) + def add_virtual_measurement(self, remote_node, measurement): + proposal_body, careful_vote = self.make_proposal( + "add_virtual_measurement", measurement=measurement + ) + proposal = self.get_any_active_member().propose(remote_node, proposal_body) + return self.vote_using_majority(remote_node, proposal, careful_vote) + def add_snp_measurement(self, remote_node, measurement): proposal_body, careful_vote = self.make_proposal( "add_snp_measurement", measurement=measurement @@ -753,6 +770,16 @@ def add_snp_uvm_endorsement(self, remote_node, did, feed, svn): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) + def remove_measurement(self, remote_node, platform, measurement): + if platform == "sgx": + return self.retire_code(remote_node, measurement) + elif platform == "virtual": + return self.remove_virtual_measurement(remote_node, measurement) + elif platform == "snp": + return self.remove_snp_measurement(remote_node, measurement) + else: + raise ValueError(f"Unsupported platform {platform}") + def retire_code(self, remote_node, code_id): proposal_body, careful_vote = self.make_proposal( "remove_node_code", code_id=code_id @@ -760,6 +787,13 @@ def retire_code(self, remote_node, code_id): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) + def remove_virtual_measurement(self, remote_node, measurement): + proposal_body, careful_vote = self.make_proposal( + "remove_virtual_measurement", measurement=measurement + ) + proposal = self.get_any_active_member().propose(remote_node, proposal_body) + return self.vote_using_majority(remote_node, proposal, careful_vote) + def remove_snp_measurement(self, remote_node, measurement): proposal_body, careful_vote = self.make_proposal( "remove_snp_measurement", measurement=measurement diff --git a/tests/infra/utils.py b/tests/infra/utils.py index db2b5c58fc96..987a5d82dae7 100644 --- a/tests/infra/utils.py +++ b/tests/infra/utils.py @@ -6,7 +6,8 @@ import subprocess -def get_code_id( +# TODO: Remove oe_binary_dir +def get_measurement( enclave_type, enclave_platform, oe_binary_dir, package, library_dir="." ): lib_path = infra.path.build_lib_path( @@ -26,6 +27,10 @@ def get_code_id( ] return lines[0].split("=")[1] + + elif enclave_platform == "virtual": + hash = hashlib.sha256(open(lib_path, "rb").read()) + return hash.hexdigest() + else: - # Virtual and SNP - return hashlib.sha256(lib_path.encode()).hexdigest() + raise ValueError(f"Cannot get code ID on {enclave_platform}") From 844dc566ce0d1b1150a2bc8c25b7a33ee34a32ce Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 17 Jan 2025 11:04:15 +0000 Subject: [PATCH 07/36] Remove oe_binary arg --- tests/code_update.py | 7 +++---- tests/infra/utils.py | 23 +++-------------------- tests/lts_compatibility.py | 6 ++---- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/tests/code_update.py b/tests/code_update.py index 60447d47ba20..b2068a2a63a2 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -43,7 +43,7 @@ def test_verify_quotes(network, args): # - The measurement (equal to any equivalent node) is the sha256 of the package (library) it loaded claimed_measurement = j["measurement"] expected_measurement = infra.utils.get_measurement( - args.enclave_type, args.enclave_platform, "", args.package + args.enclave_type, args.enclave_platform, args.package ) assert ( claimed_measurement == expected_measurement @@ -391,10 +391,10 @@ def test_update_all_nodes(network, args): primary, _ = network.find_nodes() first_measurement = infra.utils.get_measurement( - args.enclave_type, args.enclave_platform, args.oe_binary, args.package + args.enclave_type, args.enclave_platform, args.package ) new_measurement = infra.utils.get_measurement( - args.enclave_type, args.enclave_platform, args.oe_binary, replacement_package + args.enclave_type, args.enclave_platform, replacement_package ) LOG.info("Add new measurement") @@ -469,7 +469,6 @@ def test_proposal_invalidation(network, args): temporary_measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, - args.oe_binary, get_replacement_package(args), ) network.consortium.add_measurement( diff --git a/tests/infra/utils.py b/tests/infra/utils.py index 987a5d82dae7..f5ee8fc54f2e 100644 --- a/tests/infra/utils.py +++ b/tests/infra/utils.py @@ -6,31 +6,14 @@ import subprocess -# TODO: Remove oe_binary_dir -def get_measurement( - enclave_type, enclave_platform, oe_binary_dir, package, library_dir="." -): +def get_measurement(enclave_type, enclave_platform, package, library_dir="."): lib_path = infra.path.build_lib_path( package, enclave_type, enclave_platform, library_dir ) - if enclave_platform == "sgx": - res = subprocess.run( - [os.path.join(oe_binary_dir, "oesign"), "dump", "-e", lib_path], - capture_output=True, - check=True, - ) - lines = [ - line - for line in res.stdout.decode().split(os.linesep) - if line.startswith("mrenclave=") - ] - - return lines[0].split("=")[1] - - elif enclave_platform == "virtual": + if enclave_platform == "virtual": hash = hashlib.sha256(open(lib_path, "rb").read()) return hash.hexdigest() else: - raise ValueError(f"Cannot get code ID on {enclave_platform}") + raise ValueError(f"Cannot get measurement on {enclave_platform}") diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index fa6c898d27cc..dc6a52560b38 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -251,10 +251,9 @@ def run_code_upgrade_from( LOG.info("Apply transactions to old service") issue_activity_on_live_service(network, args) - new_code_id = infra.utils.get_code_id( + new_code_id = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, - args.oe_binary, args.package, library_dir=to_library_dir, ) @@ -327,10 +326,9 @@ def run_code_upgrade_from( LOG.info("Apply transactions to hybrid network, with primary as old node") issue_activity_on_live_service(network, args) - old_code_id = infra.utils.get_code_id( + old_code_id = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, - args.oe_binary, args.package, library_dir=from_library_dir, ) From aa3f34f9a06308ff673c4e3820f4ea0228af0ff1 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 17 Jan 2025 14:49:36 +0000 Subject: [PATCH 08/36] Virtual security policy and host data tests, where possible --- include/ccf/pal/attestation.h | 16 ++- samples/constitutions/default/actions.js | 30 ++++ src/host/main.cpp | 27 +++- src/node/node_state.h | 62 +++++---- src/node/quote.cpp | 13 +- src/node/rpc/node_frontend.h | 2 +- src/service/internal_tables_access.h | 10 +- tests/code_update.py | 170 +++++++++++++++-------- tests/infra/consortium.py | 48 ++++++- 9 files changed, 272 insertions(+), 106 deletions(-) diff --git a/include/ccf/pal/attestation.h b/include/ccf/pal/attestation.h index 5fb81fdb61e7..cd6eabc52327 100644 --- a/include/ccf/pal/attestation.h +++ b/include/ccf/pal/attestation.h @@ -223,7 +223,13 @@ namespace ccf::pal } } - static void emit_virtual_measurement(const std::string& package_path) + static std::string virtual_attestation_path(const std::string& suffix) + { + return fmt::format("ccf_virtual_attestation.{}.{}", ::getpid(), suffix); + }; + + static void emit_virtual_measurement( + const std::string& package_path, const std::string& security_policy) { auto package = files::slurp(package_path); @@ -231,9 +237,9 @@ namespace ccf::pal auto j = nlohmann::json::object(); j["measurement"] = package_hash.hex_str(); - j["host_data"] = "TODO"; + j["security_policy"] = security_policy; - files::dump(j.dump(), "MY_VIRTUAL_ATTESTATION.measurement"); + files::dump(j.dump(2), virtual_attestation_path("measurement")); } #if defined(PLATFORM_VIRTUAL) @@ -243,10 +249,10 @@ namespace ccf::pal RetrieveEndorsementCallback endorsement_cb, const snp::EndorsementsServers& endorsements_servers = {}) { - auto quote = files::slurp_json("MY_VIRTUAL_ATTESTATION.measurement"); + auto quote = files::slurp_json(virtual_attestation_path("measurement")); quote["report_data"] = ccf::crypto::b64_from_raw(report_data.data); - files::dump(quote.dump(), "MY_VIRTUAL_ATTESTATION.attestation"); + files::dump(quote.dump(2), virtual_attestation_path("attestation")); auto dumped_quote = quote.dump(); std::vector quote_vec(dumped_quote.begin(), dumped_quote.end()); diff --git a/samples/constitutions/default/actions.js b/samples/constitutions/default/actions.js index aa9813c6f80b..828970d1f7f6 100644 --- a/samples/constitutions/default/actions.js +++ b/samples/constitutions/default/actions.js @@ -1081,6 +1081,24 @@ const actions = new Map([ }, ), ], + [ + "add_virtual_host_data", + new Action( + function (args) { + checkType(args.host_data, "string", "host_data"); + checkType(args.metadata, "string", "metadata"); + }, + function (args, proposalId) { + ccf.kv["public:ccf.gov.nodes.virtual.host_data"].set( + ccf.strToBuf(args.host_data), + ccf.jsonCompatibleToBuf(args.metadata), + ); + + // Adding a new allowed host data changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification + invalidateOtherOpenProposals(proposalId); + }, + ), + ], [ "add_snp_host_data", new Action( @@ -1113,6 +1131,18 @@ const actions = new Map([ }, ), ], + [ + "remove_virtual_host_data", + new Action( + function (args) { + checkType(args.host_data, "string", "host_data"); + }, + function (args) { + const hostData = ccf.strToBuf(args.host_data); + ccf.kv["public:ccf.gov.nodes.virtual.host_data"].delete(hostData); + }, + ), + ], [ "remove_snp_host_data", new Action( diff --git a/src/host/main.cpp b/src/host/main.cpp index ca833f70a866..083e6ef05e0a 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -60,6 +60,9 @@ size_t asynchost::UDPImpl::remaining_read_quota = std::chrono::microseconds asynchost::TimeBoundLogger::default_max_time(10'000); +static constexpr char const* default_virtual_security_policy = + "Default CCF virtual security policy"; + void print_version(size_t) { std::cout << "CCF host: " << ccf::ccf_version << std::endl; @@ -296,11 +299,6 @@ int main(int argc, char** argv) return static_cast(CLI::ExitCodes::ValidationError); } - if (config.enclave.platform == host::EnclavePlatform::VIRTUAL) - { - ccf::pal::emit_virtual_measurement(enclave_file_path); - } - host::Enclave enclave( enclave_file_path, config.enclave.type, config.enclave.platform); @@ -533,6 +531,25 @@ int main(int argc, char** argv) files::try_slurp_string(security_policy_file); } + if (config.enclave.platform == host::EnclavePlatform::VIRTUAL) + { + // Hard-coded here and repeated in the relevant tests. Can be made dynamic + // (eg - from an env var or file) when the tests are able to run SNP nodes + // with distinct policies + startup_config.attestation.environment.security_policy = + default_virtual_security_policy; + } + LOG_INFO_FMT( + "!!!! startup_config.attestation.environment.security_policy = {}", + startup_config.attestation.environment.security_policy.value_or("\"\"")); + + if (config.enclave.platform == host::EnclavePlatform::VIRTUAL) + { + ccf::pal::emit_virtual_measurement( + enclave_file_path, + startup_config.attestation.environment.security_policy.value_or("")); + } + if (startup_config.attestation.snp_uvm_endorsements_file.has_value()) { auto snp_uvm_endorsements_file = diff --git a/src/node/node_state.h b/src/node/node_state.h index 2e2249efd75b..22b842607b47 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -298,41 +298,43 @@ namespace ccf } // Verify that the security policy matches the quoted digest of the policy - if (quote_info.format == QuoteFormat::amd_sev_snp_v1) + if (!config.attestation.environment.security_policy.has_value()) + { + LOG_INFO_FMT( + "Security policy not set, skipping check against attestation host " + "data"); + } + else { - if (!config.attestation.environment.security_policy.has_value()) + auto quoted_digest = AttestationProvider::get_host_data(quote_info); + if (!quoted_digest.has_value()) { - LOG_INFO_FMT( - "Security policy not set, skipping check against attestation host " - "data"); + throw std::logic_error("Unable to find host data in attestation"); } - else - { - auto quoted_digest = AttestationProvider::get_host_data(quote_info); - if (!quoted_digest.has_value()) - { - throw std::logic_error("Unable to find host data in attestation"); - } - auto const& security_policy = - config.attestation.environment.security_policy.value(); + auto const& security_policy = + config.attestation.environment.security_policy.value(); - auto security_policy_digest = - ccf::crypto::Sha256Hash(ccf::crypto::raw_from_b64(security_policy)); - if (security_policy_digest != quoted_digest.value()) - { - throw std::logic_error(fmt::format( - "Digest of decoded security policy \"{}\" {} does not match " - "attestation host data {}", - security_policy, - security_policy_digest.hex_str(), - quoted_digest.value().hex_str())); - } - LOG_INFO_FMT( - "Successfully verified attested security policy {}", - security_policy_digest); + auto security_policy_digest = + quote_info.format == QuoteFormat::amd_sev_snp_v1 ? + ccf::crypto::Sha256Hash(ccf::crypto::raw_from_b64(security_policy)) : + ccf::crypto::Sha256Hash(security_policy); + if (security_policy_digest != quoted_digest.value()) + { + throw std::logic_error(fmt::format( + "Digest of decoded security policy \"{}\" {} does not match " + "attestation host data {}", + security_policy, + security_policy_digest.hex_str(), + quoted_digest.value().hex_str())); } + LOG_INFO_FMT( + "Successfully verified attested security policy {}", + security_policy_digest); + } + if (quote_info.format == QuoteFormat::amd_sev_snp_v1) + { if (!config.attestation.environment.uvm_endorsements.has_value()) { LOG_INFO_FMT( @@ -1976,6 +1978,10 @@ namespace ccf create_params.snp_security_policy = config.attestation.environment.security_policy; + LOG_INFO_FMT( + "!!!! create_params.snp_security_policy = {}", + create_params.snp_security_policy.value_or("\"\"")); + create_params.node_info_network = config.network; create_params.node_data = config.node_data; create_params.service_data = config.service_data; diff --git a/src/node/quote.cpp b/src/node/quote.cpp index 74d3943646de..38deb0440d81 100644 --- a/src/node/quote.cpp +++ b/src/node/quote.cpp @@ -145,14 +145,19 @@ namespace ccf case QuoteFormat::insecure_virtual: { auto j = nlohmann::json::parse(quote_info.quote); - auto it = j.find("host_data"); + + // To simulate SNP attestation metadata, associate this "security + // policy" with a host_data value containing its digest + auto it = j.find("security_policy"); if (it != j.end()) { - const auto raw_host_data = it->get(); - return ccf::crypto::Sha256Hash(raw_host_data); + const auto security_policy = it->get(); + return ccf::crypto::Sha256Hash(security_policy); } - LOG_FAIL_FMT("No host data in virtual attestation"); + LOG_FAIL_FMT( + "No security policy in virtual attestation from which to derive host " + "data"); return std::nullopt; } diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 316bdb427cfb..a59208a869d8 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1577,7 +1577,7 @@ namespace ccf if (host_data.has_value()) { InternalTablesAccess::trust_node_virtual_host_data( - ctx.tx, host_data.value()); + ctx.tx, host_data.value(), in.snp_security_policy); } else { diff --git a/src/service/internal_tables_access.h b/src/service/internal_tables_access.h index f247527b1bff..180d002704a7 100644 --- a/src/service/internal_tables_access.h +++ b/src/service/internal_tables_access.h @@ -621,11 +621,17 @@ namespace ccf } static void trust_node_virtual_host_data( - ccf::kv::Tx& tx, const HostData& host_data) + ccf::kv::Tx& tx, + const HostData& host_data, + const std::optional& metadata) { + LOG_INFO_FMT( + "!!!! trust_node_virtual_host_data({}, {})", + host_data.hex_str(), + metadata.value_or("\"\"")); auto host_data_table = tx.wo(Tables::VIRTUAL_HOST_DATA); - host_data_table->put(host_data, ""); + host_data_table->put(host_data, metadata.value_or("")); } static void trust_node_host_data( diff --git a/tests/code_update.py b/tests/code_update.py index b2068a2a63a2..9628b0a0b38a 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -15,10 +15,22 @@ import shutil import http import json +from hashlib import sha256 from loguru import logger as LOG +DEFAULT_VIRTUAL_SECURITY_POLICY = "Default CCF virtual security policy" + + +def get_host_data_and_security_policy(): + if snp.IS_SNP: + security_policy = snp.get_container_group_security_policy() + else: + security_policy = DEFAULT_VIRTUAL_SECURITY_POLICY + host_data = sha256(security_policy.encode()).hexdigest() + return host_data, security_policy + @reqs.description("Verify node evidence") def test_verify_quotes(network, args): @@ -79,39 +91,51 @@ def get_trusted_uvm_endorsements(node): return r.body.json()["snp"]["uvmEndorsements"] -@reqs.description("Test the SNP measurements table") -@reqs.snp_only() -def test_snp_measurements_tables(network, args): +@reqs.description("Test the measurements tables") +def test_measurements_tables(network, args): primary, _ = network.find_nodes() - LOG.info("SNP measurements table") - def get_trusted_measurements(node): with node.api_versioned_client(api_version=args.gov_api_version) as client: r = client.get("/gov/service/join-policy") assert r.status_code == http.HTTPStatus.OK, r - return r.body.json()["snp"]["measurements"] + return sorted(r.body.json()[args.enclave_platform]["measurements"]) - measurements = get_trusted_measurements(primary) - assert ( - len(measurements) == 0 - ), "Expected no measurement as UVM endorsements are used by default" + original_measurements = get_trusted_measurements(primary) + + if snp.IS_SNP: + assert ( + len(original_measurements) == 0 + ), "Expected no measurement as UVM endorsements are used by default" LOG.debug("Add dummy measurement") - dummy_snp_measurement = "a" * 96 - network.consortium.add_snp_measurement(primary, dummy_snp_measurement) + measurement_length = 96 if snp.IS_SNP else 64 + dummy_measurement = "a" * measurement_length + network.consortium.add_measurement( + primary, args.enclave_platform, dummy_measurement + ) measurements = get_trusted_measurements(primary) - expected_measurements = [dummy_snp_measurement] + expected_measurements = sorted(original_measurements + [dummy_measurement]) assert ( measurements == expected_measurements ), f"One of the measurements should match the dummy that was populated, expected={expected_measurements}, actual={measurements}" LOG.debug("Remove dummy measurement") - network.consortium.remove_snp_measurement(primary, dummy_snp_measurement) + network.consortium.remove_measurement( + primary, args.enclave_platform, dummy_measurement + ) measurements = get_trusted_measurements(primary) assert ( - len(measurements) == 0 - ), "Expected no measurement as UVM endorsements are used by default" + measurements == original_measurements + ), f"Did not restore original measurements after removing dummy, expected={original_measurements}, actual={measurements}" + + return network + + +@reqs.description("Test the endorsements tables") +@reqs.snp_only() +def test_endorsements_tables(network, args): + primary, _ = network.find_nodes() LOG.info("SNP UVM endorsement table") @@ -172,20 +196,46 @@ def get_trusted_measurements(node): return network -@reqs.description("Test that the security policies table is correctly populated") -@reqs.snp_only() -def test_host_data_table(network, args): +@reqs.description("Test that the host data tables are correctly populated") +def test_host_data_tables(network, args): primary, _ = network.find_nodes() - with primary.api_versioned_client(api_version=args.gov_api_version) as client: - r = client.get("/gov/service/join-policy") - assert r.status_code == http.HTTPStatus.OK, r - host_data = r.body.json()["snp"]["hostData"] - expected = { - snp.get_container_group_security_policy_digest(): snp.get_container_group_security_policy(), + def get_trusted_host_data(node): + with node.api_versioned_client(api_version=args.gov_api_version) as client: + r = client.get("/gov/service/join-policy") + assert r.status_code == http.HTTPStatus.OK, r + return r.body.json()[args.enclave_platform]["hostData"] + + original_host_data = get_trusted_host_data(primary) + + host_data, security_policy = get_host_data_and_security_policy() + expected = {host_data: security_policy} + + assert original_host_data == expected, f"{original_host_data} != {expected}" + + LOG.debug("Add dummy host data") + dummy_host_data_value = "Open Season" + # For SNP compatibility, the host_data key must be the digest of the content/metadata + dummy_host_data_key = sha256(dummy_host_data_value.encode()).hexdigest() + network.consortium.add_host_data( + primary, args.enclave_platform, dummy_host_data_key, dummy_host_data_value + ) + host_data = get_trusted_host_data(primary) + expected_host_data = { + **original_host_data, + dummy_host_data_key: dummy_host_data_value, } + assert host_data == expected_host_data, f"{host_data} != {expected_host_data}" + + LOG.debug("Remove dummy host data") + network.consortium.remove_host_data( + primary, args.enclave_platform, dummy_host_data_key + ) + host_data = get_trusted_host_data(primary) + assert ( + host_data == original_host_data + ), f"Did not restore original host data after removing dummy, expected={original_host_data}, actual={host_data}" - assert host_data == expected, f"{host_data} != {expected}" return network @@ -211,17 +261,18 @@ def test_add_node_without_security_policy(network, args): @reqs.description("Remove raw security policy from trusted host data and join new node") -@reqs.snp_only() -def test_add_node_remove_trusted_security_policy(network, args): +def test_add_node_with_stubbed_security_policy(network, args): LOG.info("Remove raw security policy from trusted host data") primary, _ = network.find_nodes() - network.consortium.retire_host_data( - primary, snp.get_container_group_security_policy_digest() - ) - network.consortium.add_new_host_data( + + host_data, security_policy = get_host_data_and_security_policy() + + network.consortium.remove_host_data(primary, args.enclave_platform, host_data) + network.consortium.add_host_data( primary, - snp.EMPTY_SNP_SECURITY_POLICY, - snp.get_container_group_security_policy_digest(), + args.enclave_platform, + host_data, + "", # Remove the raw security policy metadata, while retaining the host_data key ) # If we don't throw an exception, joining was successful @@ -230,21 +281,16 @@ def test_add_node_remove_trusted_security_policy(network, args): network.trust_node(new_node, args) # Revert to original state - network.consortium.retire_host_data( - primary, - snp.get_container_group_security_policy_digest(), - ) - network.consortium.add_new_host_data( - primary, - snp.get_container_group_security_policy(), - snp.get_container_group_security_policy_digest(), + network.consortium.remove_host_data(primary, args.enclave_platform, host_data) + network.consortium.add_host_data( + primary, args.enclave_platform, host_data, security_policy ) return network @reqs.description("Start node with mismatching security policy") @reqs.snp_only() -def test_start_node_with_mismatched_host_data(network, args): +def test_add_node_with_bad_security_policy(network, args): try: security_context_dir = snp.get_security_context_dir() with tempfile.TemporaryDirectory() as snp_dir: @@ -274,16 +320,15 @@ def test_start_node_with_mismatched_host_data(network, args): @reqs.description("Node with bad host data fails to join") -@reqs.snp_only() def test_add_node_with_bad_host_data(network, args): primary, _ = network.find_nodes() + host_data, security_policy = get_host_data_and_security_policy() + LOG.info( "Removing trusted security policy so that a new joiner is seen as an unmatching policy" ) - network.consortium.retire_host_data( - primary, snp.get_container_group_security_policy_digest() - ) + network.consortium.remove_host_data(primary, args.enclave_platform, host_data) new_node = network.create_node("local://localhost") try: @@ -293,10 +338,11 @@ def test_add_node_with_bad_host_data(network, args): else: raise AssertionError("Node join unexpectedly succeeded") - network.consortium.add_new_host_data( + network.consortium.add_host_data( primary, - snp.get_container_group_security_policy(), - snp.get_container_group_security_policy_digest(), + args.enclave_platform, + host_data, + security_policy, ) return network @@ -527,15 +573,25 @@ def run(args): network.start_and_open(args) test_verify_quotes(network, args) + + # Measurements + test_measurements_tables(network, args) + test_add_node_with_bad_code(network, args) + + # Host data/security policy + test_host_data_tables(network, args) + test_add_node_with_bad_host_data(network, args) + test_add_node_with_stubbed_security_policy(network, args) + if snp.IS_SNP: - test_snp_measurements_tables(network, args) - test_add_node_with_no_uvm_endorsements(network, args) - test_host_data_table(network, args) test_add_node_without_security_policy(network, args) - test_add_node_remove_trusted_security_policy(network, args) - test_start_node_with_mismatched_host_data(network, args) - test_add_node_with_bad_host_data(network, args) - test_add_node_with_bad_code(network, args) + test_add_node_with_bad_security_policy(network, args) + + # Endorsements + if snp.IS_SNP: + test_endorsements_tables(network, args) + test_add_node_with_no_uvm_endorsements(network, args) + # NB: Assumes the current nodes are still using args.package, so must run before test_update_all_nodes test_proposal_invalidation(network, args) diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 6b1baeae44e7..0df43a1926d8 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -808,21 +808,61 @@ def remove_snp_uvm_endorsement(self, remote_node, did, feed): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) - def add_new_host_data( + def add_host_data(self, remote_node, platform, host_data_key, host_data_value): + if platform == "virtual": + return self.add_virtual_host_data( + remote_node, host_data_key, host_data_value + ) + elif platform == "snp": + return self.add_snp_host_data(remote_node, host_data_key, host_data_value) + else: + raise ValueError(f"Unsupported platform {platform}") + + def add_virtual_host_data( + self, + remote_node, + host_data_key, + metadata, + ): + proposal_body, careful_vote = self.make_proposal( + "add_virtual_host_data", + host_data=host_data_key, + metadata=metadata, + ) + proposal = self.get_any_active_member().propose(remote_node, proposal_body) + return self.vote_using_majority(remote_node, proposal, careful_vote) + + def add_snp_host_data( self, remote_node, - new_security_policy, new_host_data, + new_security_policy, ): proposal_body, careful_vote = self.make_proposal( "add_snp_host_data", - security_policy=new_security_policy, host_data=new_host_data, + security_policy=new_security_policy, + ) + proposal = self.get_any_active_member().propose(remote_node, proposal_body) + return self.vote_using_majority(remote_node, proposal, careful_vote) + + def remove_host_data(self, remote_node, platform, host_data_key): + if platform == "virtual": + return self.remove_virtual_host_data(remote_node, host_data_key) + elif platform == "snp": + return self.remove_snp_host_data(remote_node, host_data_key) + else: + raise ValueError(f"Unsupported platform {platform}") + + def remove_virtual_host_data(self, remote_node, host_data): + proposal_body, careful_vote = self.make_proposal( + "remove_virtual_host_data", + host_data=host_data, ) proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) - def retire_host_data(self, remote_node, host_data): + def remove_snp_host_data(self, remote_node, host_data): proposal_body, careful_vote = self.make_proposal( "remove_snp_host_data", host_data=host_data, From 12f000dbcb84c77e770751961965114e06cc18e8 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 17 Jan 2025 14:56:01 +0000 Subject: [PATCH 09/36] Remove redundant test_quote --- tests/code_update.py | 7 ++++++ tests/governance.py | 60 -------------------------------------------- 2 files changed, 7 insertions(+), 60 deletions(-) diff --git a/tests/code_update.py b/tests/code_update.py index 9628b0a0b38a..0c8de1561138 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -43,6 +43,10 @@ def test_verify_quotes(network, args): assert r.status_code == http.HTTPStatus.OK, r trusted_measurements = r.body.json()[args.enclave_platform]["measurements"] + r = uc.get("/node/quotes") + all_quotes = r.body.json()["quotes"] + assert len(all_quotes) == len(network.get_joined_nodes()) + for node in network.get_joined_nodes(): LOG.info(f"Verifying quote for node {node.node_id}") with node.client() as c: @@ -81,6 +85,9 @@ def test_verify_quotes(network, args): "Skipping client-side verification of SNP node's quote until there is a separate utility to verify SNP quotes" ) + # Quick API validation - confirm that all of these /quotes/self entries match the collection returned from /quotes + assert j in all_quotes + return network diff --git a/tests/governance.py b/tests/governance.py index 737321fa374a..cd5e04f3a7e2 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -48,66 +48,6 @@ def test_consensus_status(network, args): return network -@reqs.description("Test quotes") -@reqs.supports_methods("/node/quotes/self", "/node/quotes") -def test_quote(network, args): - if args.enclave_platform != "sgx": - LOG.warning("Quote test can only run in real enclaves, skipping") - return network - - primary, _ = network.find_nodes() - with primary.client() as c: - oed = subprocess.run( - [ - os.path.join(args.oe_binary, "oesign"), - "dump", - "-e", - infra.path.build_lib_path( - args.package, args.enclave_type, args.enclave_platform - ), - ], - capture_output=True, - check=True, - ) - lines = [ - line - for line in oed.stdout.decode().split(os.linesep) - if line.startswith("mrenclave=") - ] - expected_mrenclave = lines[0].strip().split("=")[1] - - r = c.get("/node/quotes/self") - primary_quote_info = r.body.json() - assert primary_quote_info["node_id"] == primary.node_id - primary_mrenclave = primary_quote_info["mrenclave"] - assert primary_mrenclave == expected_mrenclave, ( - primary_mrenclave, - expected_mrenclave, - ) - - r = c.get("/node/quotes") - quotes = r.body.json()["quotes"] - assert len(quotes) == len(network.get_joined_nodes()) - - for quote in quotes: - mrenclave = quote["mrenclave"] - assert mrenclave == expected_mrenclave, (mrenclave, expected_mrenclave) - - cafile = os.path.join(network.common_dir, "service_cert.pem") - assert ( - infra.proc.ccall( - "verify_quote.sh", - f"https://{primary.get_public_rpc_host()}:{primary.get_public_rpc_port()}", - "--cacert", - f"{cafile}", - log_output=True, - ).returncode - == 0 - ), f"Quote verification for node {quote['node_id']} failed" - - return network - - @reqs.description("Add user, remove user") @reqs.supports_methods("/app/log/private") def test_user(network, args, verify=True): From 767ae419e67875db0f36c18fd6cface4faf44ef6 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 17 Jan 2025 15:27:12 +0000 Subject: [PATCH 10/36] Update new nodes endpoints to describe virtual quotes --- src/node/gov/handlers/service_state.h | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/node/gov/handlers/service_state.h b/src/node/gov/handlers/service_state.h index 3ea2a5f31354..a4191f56ea0c 100644 --- a/src/node/gov/handlers/service_state.h +++ b/src/node/gov/handlers/service_state.h @@ -93,12 +93,23 @@ namespace ccf::gov::endpoints } case ccf::QuoteFormat::insecure_virtual: { - // TODO quote_info["format"] = "Insecure_Virtual"; - quote_info["quote"] = - ccf::crypto::b64_from_raw(node_info.quote_info.quote); - quote_info["endorsements"] = - ccf::crypto::b64_from_raw(node_info.quote_info.endorsements); + quote_info["rawQuote"] = node_info.quote_info.quote; + + { + const auto details = + nlohmann::json::parse(node_info.quote_info.quote); + auto j_details = nlohmann::json::object(); + j_details["measurement"] = details["measurement"]; + j_details["reportData"] = details["report_data"]; + const auto security_policy = + details["security_policy"].get(); + j_details["securityPolicy"] = security_policy; + j_details["hostData"] = + ccf::crypto::Sha256Hash(security_policy).hex_str(); + quote_info["details"] = j_details; + } + break; } case ccf::QuoteFormat::amd_sev_snp_v1: From 6a17b1858599529e5db0a96b49a762bbed4a7530 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 17 Jan 2025 15:50:28 +0000 Subject: [PATCH 11/36] LTS compat working with new virtual attestations --- tests/code_update.py | 23 ++++++++------------ tests/infra/node.py | 1 + tests/infra/utils.py | 19 ++++++++++++---- tests/lts_compatibility.py | 44 +++++++++++++++++++++++++++++++------- 4 files changed, 61 insertions(+), 26 deletions(-) diff --git a/tests/code_update.py b/tests/code_update.py index 0c8de1561138..08e620e04708 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -20,17 +20,6 @@ from loguru import logger as LOG -DEFAULT_VIRTUAL_SECURITY_POLICY = "Default CCF virtual security policy" - - -def get_host_data_and_security_policy(): - if snp.IS_SNP: - security_policy = snp.get_container_group_security_policy() - else: - security_policy = DEFAULT_VIRTUAL_SECURITY_POLICY - host_data = sha256(security_policy.encode()).hexdigest() - return host_data, security_policy - @reqs.description("Verify node evidence") def test_verify_quotes(network, args): @@ -215,7 +204,9 @@ def get_trusted_host_data(node): original_host_data = get_trusted_host_data(primary) - host_data, security_policy = get_host_data_and_security_policy() + host_data, security_policy = infra.utils.get_host_data_and_security_policy( + args.enclave_platform + ) expected = {host_data: security_policy} assert original_host_data == expected, f"{original_host_data} != {expected}" @@ -272,7 +263,9 @@ def test_add_node_with_stubbed_security_policy(network, args): LOG.info("Remove raw security policy from trusted host data") primary, _ = network.find_nodes() - host_data, security_policy = get_host_data_and_security_policy() + host_data, security_policy = infra.utils.get_host_data_and_security_policy( + args.enclave_platform + ) network.consortium.remove_host_data(primary, args.enclave_platform, host_data) network.consortium.add_host_data( @@ -330,7 +323,9 @@ def test_add_node_with_bad_security_policy(network, args): def test_add_node_with_bad_host_data(network, args): primary, _ = network.find_nodes() - host_data, security_policy = get_host_data_and_security_policy() + host_data, security_policy = infra.utils.get_host_data_and_security_policy( + args.enclave_platform + ) LOG.info( "Removing trusted security policy so that a new joiner is seen as an unmatching policy" diff --git a/tests/infra/node.py b/tests/infra/node.py index f44dd753e2b1..953f10717d29 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -284,6 +284,7 @@ def _setup( self.common_dir = common_dir members_info = members_info or [] self.label = label + self.enclave_platform = enclave_platform self.certificate_validity_days = kwargs.get("initial_node_cert_validity_days") self.remote = infra.remote.CCFRemote( diff --git a/tests/infra/utils.py b/tests/infra/utils.py index f5ee8fc54f2e..99d759fd73fc 100644 --- a/tests/infra/utils.py +++ b/tests/infra/utils.py @@ -1,9 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache 2.0 License. import infra.path -import hashlib -import os -import subprocess +from hashlib import sha256 +import infra.snp as snp def get_measurement(enclave_type, enclave_platform, package, library_dir="."): @@ -12,8 +11,20 @@ def get_measurement(enclave_type, enclave_platform, package, library_dir="."): ) if enclave_platform == "virtual": - hash = hashlib.sha256(open(lib_path, "rb").read()) + hash = sha256(open(lib_path, "rb").read()) return hash.hexdigest() else: raise ValueError(f"Cannot get measurement on {enclave_platform}") + + +def get_host_data_and_security_policy(enclave_platform): + DEFAULT_VIRTUAL_SECURITY_POLICY = "Default CCF virtual security policy" + if enclave_platform == "snp": + security_policy = snp.get_container_group_security_policy() + elif enclave_platform == "virtual": + security_policy = DEFAULT_VIRTUAL_SECURITY_POLICY + else: + raise ValueError(f"Cannot get security policy on {enclave_platform}") + host_data = sha256(security_policy.encode()).hexdigest() + return host_data, security_policy diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index dc6a52560b38..64b9d4f5b895 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -251,17 +251,30 @@ def run_code_upgrade_from( LOG.info("Apply transactions to old service") issue_activity_on_live_service(network, args) - new_code_id = infra.utils.get_measurement( + LOG.info("Update constitution") + new_constitution = get_new_constitution_for_install(args, to_install_path) + network.consortium.set_constitution(primary, new_constitution) + + new_measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, args.package, library_dir=to_library_dir, ) - network.consortium.add_new_code(primary, new_code_id) + network.consortium.add_measurement( + primary, args.enclave_platform, new_measurement + ) - LOG.info("Update constitution") - new_constitution = get_new_constitution_for_install(args, to_install_path) - network.consortium.set_constitution(primary, new_constitution) + new_host_data = None + try: + new_host_data, new_security_policy = ( + infra.utils.get_host_data_and_security_policy(args.enclave_platform) + ) + network.consortium.add_host_data( + primary, args.enclave_platform, new_host_data, new_security_policy + ) + except ValueError as e: + LOG.warning(f"Not setting host data/security policy for new nodes: {e}") # Note: alternate between joining from snapshot and replaying entire ledger new_nodes = [] @@ -326,14 +339,29 @@ def run_code_upgrade_from( LOG.info("Apply transactions to hybrid network, with primary as old node") issue_activity_on_live_service(network, args) - old_code_id = infra.utils.get_measurement( + primary, _ = network.find_primary() + + old_measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, args.package, library_dir=from_library_dir, ) - primary, _ = network.find_primary() - network.consortium.retire_code(primary, old_code_id) + if old_measurement != new_measurement: + network.consortium.remove_measurement( + primary, args.enclave_platform, old_measurement + ) + + # If host_data was found for original nodes, check if it's different on new nodes, in which case old should be removed + if new_host_data is not None: + old_host_data, old_security_policy = ( + infra.utils.get_host_data_and_security_policy(args.enclave_platform) + ) + + if old_host_data != new_host_data: + network.consortium.remove_host_data( + primary, args.enclave_platform, old_host_data + ) for index, node in enumerate(old_nodes): network.retire_node(primary, node) From 663b8b0376ad6bdefbba4b056ef2186ea35f3d38 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 17 Jan 2025 16:06:08 +0000 Subject: [PATCH 12/36] Include juggling --- include/ccf/pal/attestation.h | 69 ------------------------------- src/host/main.cpp | 1 + src/node/node_state.h | 2 +- src/pal/quote_generation.h | 77 +++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 70 deletions(-) create mode 100644 src/pal/quote_generation.h diff --git a/include/ccf/pal/attestation.h b/include/ccf/pal/attestation.h index cd6eabc52327..eeae9da853f5 100644 --- a/include/ccf/pal/attestation.h +++ b/include/ccf/pal/attestation.h @@ -11,9 +11,6 @@ #include "ccf/pal/measurement.h" #include "ccf/pal/snp_ioctl.h" -// TODO: Public->private -#include "ds/files.h" - #include #include @@ -223,72 +220,6 @@ namespace ccf::pal } } - static std::string virtual_attestation_path(const std::string& suffix) - { - return fmt::format("ccf_virtual_attestation.{}.{}", ::getpid(), suffix); - }; - - static void emit_virtual_measurement( - const std::string& package_path, const std::string& security_policy) - { - auto package = files::slurp(package_path); - - auto package_hash = ccf::crypto::Sha256Hash(package); - - auto j = nlohmann::json::object(); - j["measurement"] = package_hash.hex_str(); - j["security_policy"] = security_policy; - - files::dump(j.dump(2), virtual_attestation_path("measurement")); - } - -#if defined(PLATFORM_VIRTUAL) - - static void generate_quote( - PlatformAttestationReportData& report_data, - RetrieveEndorsementCallback endorsement_cb, - const snp::EndorsementsServers& endorsements_servers = {}) - { - auto quote = files::slurp_json(virtual_attestation_path("measurement")); - quote["report_data"] = ccf::crypto::b64_from_raw(report_data.data); - - files::dump(quote.dump(2), virtual_attestation_path("attestation")); - - auto dumped_quote = quote.dump(); - std::vector quote_vec(dumped_quote.begin(), dumped_quote.end()); - - endorsement_cb( - {.format = QuoteFormat::insecure_virtual, - .quote = quote_vec, - .endorsements = {}, - .uvm_endorsements = {}, - .endorsed_tcb = {}}, - {}); - } - -#elif defined(PLATFORM_SNP) - - static void generate_quote( - PlatformAttestationReportData& report_data, - RetrieveEndorsementCallback endorsement_cb, - const snp::EndorsementsServers& endorsements_servers = {}) - { - QuoteInfo node_quote_info = {}; - node_quote_info.format = QuoteFormat::amd_sev_snp_v1; - auto attestation = snp::get_attestation(report_data); - - node_quote_info.quote = attestation->get_raw(); - - if (endorsement_cb != nullptr) - { - endorsement_cb( - node_quote_info, - snp::make_endorsement_endpoint_configuration( - attestation->get(), endorsements_servers)); - } - } -#endif - #if !defined(INSIDE_ENCLAVE) || defined(VIRTUAL_ENCLAVE) static void verify_quote( diff --git a/src/host/main.cpp b/src/host/main.cpp index db280a41acef..8449c91929ee 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -22,6 +22,7 @@ #include "lfs_file_handler.h" #include "load_monitor.h" #include "node_connections.h" +#include "pal/quote_generation.h" #include "process_launcher.h" #include "rpc_connections.h" #include "sig_term.h" diff --git a/src/node/node_state.h b/src/node/node_state.h index 20429affa274..d9db48ce8953 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -9,7 +9,6 @@ #include "ccf/ds/logger.h" #include "ccf/js/core/context.h" #include "ccf/node/cose_signatures_config.h" -#include "ccf/pal/attestation.h" #include "ccf/pal/locking.h" #include "ccf/pal/platform.h" #include "ccf/service/node_info_network.h" @@ -34,6 +33,7 @@ #include "node/node_to_node_channel_manager.h" #include "node/snapshotter.h" #include "node_to_node.h" +#include "pal/quote_generation.h" #include "quote_endorsements_client.h" #include "rpc/frontend.h" #include "rpc/serialization.h" diff --git a/src/pal/quote_generation.h b/src/pal/quote_generation.h new file mode 100644 index 000000000000..0264f731d6c0 --- /dev/null +++ b/src/pal/quote_generation.h @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "ds/files.h" + +#include +#include + +namespace ccf::pal +{ + static std::string virtual_attestation_path(const std::string& suffix) + { + return fmt::format("ccf_virtual_attestation.{}.{}", ::getpid(), suffix); + }; + + static void emit_virtual_measurement( + const std::string& package_path, const std::string& security_policy) + { + auto package = files::slurp(package_path); + + auto package_hash = ccf::crypto::Sha256Hash(package); + + auto j = nlohmann::json::object(); + j["measurement"] = package_hash.hex_str(); + j["security_policy"] = security_policy; + + files::dump(j.dump(2), virtual_attestation_path("measurement")); + } + +#if defined(PLATFORM_VIRTUAL) + + static void generate_quote( + PlatformAttestationReportData& report_data, + RetrieveEndorsementCallback endorsement_cb, + const snp::EndorsementsServers& endorsements_servers = {}) + { + auto quote = files::slurp_json(virtual_attestation_path("measurement")); + quote["report_data"] = ccf::crypto::b64_from_raw(report_data.data); + + files::dump(quote.dump(2), virtual_attestation_path("attestation")); + + auto dumped_quote = quote.dump(); + std::vector quote_vec(dumped_quote.begin(), dumped_quote.end()); + + endorsement_cb( + {.format = QuoteFormat::insecure_virtual, + .quote = quote_vec, + .endorsements = {}, + .uvm_endorsements = {}, + .endorsed_tcb = {}}, + {}); + } + +#elif defined(PLATFORM_SNP) + + static void generate_quote( + PlatformAttestationReportData& report_data, + RetrieveEndorsementCallback endorsement_cb, + const snp::EndorsementsServers& endorsements_servers = {}) + { + QuoteInfo node_quote_info = {}; + node_quote_info.format = QuoteFormat::amd_sev_snp_v1; + auto attestation = snp::get_attestation(report_data); + + node_quote_info.quote = attestation->get_raw(); + + if (endorsement_cb != nullptr) + { + endorsement_cb( + node_quote_info, + snp::make_endorsement_endpoint_configuration( + attestation->get(), endorsements_servers)); + } + } +#endif +} From 1ac798a33056332d3ed149dd29901fd6e8d9bf49 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 17 Jan 2025 16:09:18 +0000 Subject: [PATCH 13/36] Lint --- samples/constitutions/default/actions.js | 285 +++++++++++------------ tests/governance.py | 2 - 2 files changed, 140 insertions(+), 147 deletions(-) diff --git a/samples/constitutions/default/actions.js b/samples/constitutions/default/actions.js index 6ef0bd16e46d..08cba468231c 100644 --- a/samples/constitutions/default/actions.js +++ b/samples/constitutions/default/actions.js @@ -8,7 +8,7 @@ class Action { function parseUrl(url) { // From https://tools.ietf.org/html/rfc3986#appendix-B const re = new RegExp( - "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?" + "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?", ); const groups = url.match(re); if (!groups) { @@ -150,7 +150,7 @@ function checkJwks(value, field) { checkType(jwk.crv, "string", `${field}.keys[${i}].crv`); } else { throw new Error( - "JWK must contain either x5c, or n/e for RSA key type, or x/y/crv for EC key type" + "JWK must contain either x5c, or n/e for RSA key type, or x/y/crv for EC key type", ); } } @@ -159,7 +159,7 @@ function checkJwks(value, field) { function checkX509CertBundle(value, field) { if (!ccf.crypto.isValidX509CertBundle(value)) { throw new Error( - `${field} must be a valid X509 certificate (bundle) in PEM format` + `${field} must be a valid X509 certificate (bundle) in PEM format`, ); } } @@ -179,9 +179,8 @@ function invalidateOtherOpenProposals(proposalIdToRetain) { } function setServiceCertificateValidityPeriod(validFrom, validityPeriodDays) { - const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( - getSingletonKvKey() - ); + const rawConfig = + ccf.kv["public:ccf.gov.service.config"].get(getSingletonKvKey()); if (rawConfig === undefined) { throw new Error("Service configuration could not be found"); } @@ -197,13 +196,13 @@ function setServiceCertificateValidityPeriod(validFrom, validityPeriodDays) { validityPeriodDays > max_allowed_cert_validity_period_days ) { throw new Error( - `Validity period ${validityPeriodDays} (days) is not allowed: service max allowed is ${max_allowed_cert_validity_period_days} (days)` + `Validity period ${validityPeriodDays} (days) is not allowed: service max allowed is ${max_allowed_cert_validity_period_days} (days)`, ); } const renewed_service_certificate = ccf.network.generateNetworkCertificate( validFrom, - validityPeriodDays ?? max_allowed_cert_validity_period_days + validityPeriodDays ?? max_allowed_cert_validity_period_days, ); const serviceInfoTable = "public:ccf.gov.service.info"; @@ -216,7 +215,7 @@ function setServiceCertificateValidityPeriod(validFrom, validityPeriodDays) { serviceInfo.cert = renewed_service_certificate; ccf.kv[serviceInfoTable].set( getSingletonKvKey(), - ccf.jsonCompatibleToBuf(serviceInfo) + ccf.jsonCompatibleToBuf(serviceInfo), ); } @@ -224,15 +223,14 @@ function setNodeCertificateValidityPeriod( nodeId, nodeInfo, validFrom, - validityPeriodDays + validityPeriodDays, ) { if (nodeInfo.certificate_signing_request === undefined) { throw new Error(`Node ${nodeId} has no certificate signing request`); } - const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( - getSingletonKvKey() - ); + const rawConfig = + ccf.kv["public:ccf.gov.service.config"].get(getSingletonKvKey()); if (rawConfig === undefined) { throw new Error("Service configuration could not be found"); } @@ -248,18 +246,18 @@ function setNodeCertificateValidityPeriod( validityPeriodDays > max_allowed_cert_validity_period_days ) { throw new Error( - `Validity period ${validityPeriodDays} (days) is not allowed: service max allowed is ${max_allowed_cert_validity_period_days} (days)` + `Validity period ${validityPeriodDays} (days) is not allowed: service max allowed is ${max_allowed_cert_validity_period_days} (days)`, ); } const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, validFrom, - validityPeriodDays ?? max_allowed_cert_validity_period_days + validityPeriodDays ?? max_allowed_cert_validity_period_days, ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(nodeId), - ccf.strToBuf(endorsed_node_cert) + ccf.strToBuf(endorsed_node_cert), ); } @@ -280,13 +278,13 @@ function checkRecoveryThreshold(config, new_config) { if (service.status === "WaitingForRecoveryShares") { throw new Error( - `Cannot set recovery threshold if service is ${service.status}` + `Cannot set recovery threshold if service is ${service.status}`, ); } else if (service.status === "Open") { let activeRecoveryMembersCount = getActiveRecoveryMembersCount(); if (new_config.recovery_threshold > activeRecoveryMembersCount) { throw new Error( - `Cannot set recovery threshold to ${new_config.recovery_threshold}: recovery threshold would be greater than the number of recovery members ${activeRecoveryMembersCount}` + `Cannot set recovery threshold to ${new_config.recovery_threshold}: recovery threshold would be greater than the number of recovery members ${activeRecoveryMembersCount}`, ); } } @@ -303,7 +301,7 @@ function checkReconfigurationType(config, new_config) { ) ) { throw new Error( - `Cannot change reconfiguration type from ${from} to ${to}.` + `Cannot change reconfiguration type from ${from} to ${to}.`, ); } } @@ -342,7 +340,7 @@ function updateServiceConfig(new_config) { ccf.kv[service_config_table].set( getSingletonKvKey(), - ccf.jsonCompatibleToBuf(config) + ccf.jsonCompatibleToBuf(config), ); if (need_recovery_threshold_refresh) { @@ -360,12 +358,12 @@ const actions = new Map([ function (args, proposalId) { ccf.kv["public:ccf.gov.constitution"].set( getSingletonKvKey(), - ccf.jsonCompatibleToBuf(args.constitution) + ccf.jsonCompatibleToBuf(args.constitution), ); // Changing the constitution changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } + }, ), ], [ @@ -387,17 +385,17 @@ const actions = new Map([ ccf.kv["public:ccf.gov.members.certs"].set( rawMemberId, - ccf.strToBuf(args.cert) + ccf.strToBuf(args.cert), ); if (args.encryption_pub_key == null) { ccf.kv["public:ccf.gov.members.encryption_public_keys"].delete( - rawMemberId + rawMemberId, ); } else { ccf.kv["public:ccf.gov.members.encryption_public_keys"].set( rawMemberId, - ccf.strToBuf(args.encryption_pub_key) + ccf.strToBuf(args.encryption_pub_key), ); } @@ -406,12 +404,11 @@ const actions = new Map([ member_info.status = "Accepted"; ccf.kv["public:ccf.gov.members.info"].set( rawMemberId, - ccf.jsonCompatibleToBuf(member_info) + ccf.jsonCompatibleToBuf(member_info), ); - const rawSignature = ccf.kv["public:ccf.internal.signatures"].get( - getSingletonKvKey() - ); + const rawSignature = + ccf.kv["public:ccf.internal.signatures"].get(getSingletonKvKey()); if (rawSignature === undefined) { ccf.kv["public:ccf.gov.members.acks"].set(rawMemberId); } else { @@ -420,10 +417,10 @@ const actions = new Map([ ack.state_digest = signature.root; ccf.kv["public:ccf.gov.members.acks"].set( rawMemberId, - ccf.jsonCompatibleToBuf(ack) + ccf.jsonCompatibleToBuf(ack), ); } - } + }, ), ], [ @@ -453,9 +450,8 @@ const actions = new Map([ // would still be a sufficient number of recovery members left // to recover the service if (isActiveMember && isRecoveryMember) { - const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( - getSingletonKvKey() - ); + const rawConfig = + ccf.kv["public:ccf.gov.service.config"].get(getSingletonKvKey()); if (rawConfig === undefined) { throw new Error("Service configuration could not be found"); } @@ -465,14 +461,14 @@ const actions = new Map([ getActiveRecoveryMembersCount() - 1; if (activeRecoveryMembersCountAfter < config.recovery_threshold) { throw new Error( - `Number of active recovery members (${activeRecoveryMembersCountAfter}) would be less than recovery threshold (${config.recovery_threshold})` + `Number of active recovery members (${activeRecoveryMembersCountAfter}) would be less than recovery threshold (${config.recovery_threshold})`, ); } } ccf.kv["public:ccf.gov.members.info"].delete(rawMemberId); ccf.kv["public:ccf.gov.members.encryption_public_keys"].delete( - rawMemberId + rawMemberId, ); ccf.kv["public:ccf.gov.members.certs"].delete(rawMemberId); ccf.kv["public:ccf.gov.members.acks"].delete(rawMemberId); @@ -484,7 +480,7 @@ const actions = new Map([ // remaining active recovery members ccf.node.triggerLedgerRekey(); } - } + }, ), ], [ @@ -505,7 +501,7 @@ const actions = new Map([ let mi = ccf.bufToJsonCompatible(member_info); mi.member_data = args.member_data; members_info.set(member_id, ccf.jsonCompatibleToBuf(mi)); - } + }, ), ], [ @@ -521,7 +517,7 @@ const actions = new Map([ ccf.kv["public:ccf.gov.users.certs"].set( rawUserId, - ccf.strToBuf(args.cert) + ccf.strToBuf(args.cert), ); if (args.user_data !== null && args.user_data !== undefined) { @@ -529,12 +525,12 @@ const actions = new Map([ userInfo.user_data = args.user_data; ccf.kv["public:ccf.gov.users.info"].set( rawUserId, - ccf.jsonCompatibleToBuf(userInfo) + ccf.jsonCompatibleToBuf(userInfo), ); } else { ccf.kv["public:ccf.gov.users.info"].delete(rawUserId); } - } + }, ), ], [ @@ -547,7 +543,7 @@ const actions = new Map([ const user_id = ccf.strToBuf(args.user_id); ccf.kv["public:ccf.gov.users.certs"].delete(user_id); ccf.kv["public:ccf.gov.users.info"].delete(user_id); - } + }, ), ], [ @@ -565,12 +561,12 @@ const actions = new Map([ userInfo.user_data = args.user_data; ccf.kv["public:ccf.gov.users.info"].set( userId, - ccf.jsonCompatibleToBuf(userInfo) + ccf.jsonCompatibleToBuf(userInfo), ); } else { ccf.kv["public:ccf.gov.users.info"].delete(userId); } - } + }, ), ], [ @@ -582,7 +578,7 @@ const actions = new Map([ }, function (args) { updateServiceConfig(args); - } + }, ), ], [ @@ -593,7 +589,7 @@ const actions = new Map([ }, function (args) { ccf.node.triggerRecoverySharesRefresh(); - } + }, ), ], [ @@ -605,7 +601,7 @@ const actions = new Map([ function (args) { ccf.node.triggerLedgerRekey(); - } + }, ), ], [ @@ -615,22 +611,22 @@ const actions = new Map([ checkType( args.next_service_identity, "string", - "next service identity (PEM certificate)" + "next service identity (PEM certificate)", ); checkX509CertBundle( args.next_service_identity, - "next_service_identity" + "next_service_identity", ); checkType( args.previous_service_identity, "string?", - "previous service identity (PEM certificate)" + "previous service identity (PEM certificate)", ); if (args.previous_service_identity !== undefined) { checkX509CertBundle( args.previous_service_identity, - "previous_service_identity" + "previous_service_identity", ); } }, @@ -650,7 +646,7 @@ const actions = new Map([ args.next_service_identity === undefined) ) { throw new Error( - `Opening a recovering network requires both, the previous and the next service identity` + `Opening a recovering network requires both, the previous and the next service identity`, ); } @@ -660,7 +656,7 @@ const actions = new Map([ : undefined; const next_identity = ccf.strToBuf(args.next_service_identity); ccf.node.transitionServiceToOpen(previous_identity, next_identity); - } + }, ), ], [ @@ -682,7 +678,7 @@ const actions = new Map([ checkType(bundle.metadata, "object", prefix); checkType(bundle.metadata.endpoints, "object", `${prefix}.endpoints`); for (const [url, endpoint] of Object.entries( - bundle.metadata.endpoints + bundle.metadata.endpoints, )) { checkType(endpoint, "object", `${prefix}.endpoints["${url}"]`); for (const [method, info] of Object.entries(endpoint)) { @@ -693,12 +689,12 @@ const actions = new Map([ checkEnum( info.mode, ["readwrite", "readonly", "historical"], - `${prefix2}.mode` + `${prefix2}.mode`, ); checkEnum( info.forwarding_required, ["sometimes", "always", "never"], - `${prefix2}.forwarding_required` + `${prefix2}.forwarding_required`, ); const redirection_strategy = info.redirection_strategy; @@ -706,7 +702,7 @@ const actions = new Map([ checkEnum( info.redirection_strategy, ["none", "to_primary", "to_backup"], - `${prefix2}.redirection_strategy` + `${prefix2}.redirection_strategy`, ); } @@ -714,12 +710,12 @@ const actions = new Map([ checkType( info.openapi_hidden, "boolean?", - `${prefix2}.openapi_hidden` + `${prefix2}.openapi_hidden`, ); checkType( info.authn_policies, "array", - `${prefix2}.authn_policies` + `${prefix2}.authn_policies`, ); for (const [i, policy] of info.authn_policies.entries()) { if (typeof policy === "string") { @@ -730,18 +726,18 @@ const actions = new Map([ checkType( constituents, "array", - `${prefix2}.authn_policies[${i}].all_of` + `${prefix2}.authn_policies[${i}].all_of`, ); for (const [j, sub_policy] of constituents.entries()) { checkType( sub_policy, "string", - `${prefix2}.authn_policies[${i}].all_of[${j}]` + `${prefix2}.authn_policies[${i}].all_of[${j}]`, ); } } else { throw new Error( - `${prefix2}.authn_policies[${i}] must be of type string or object but is ${typeof policy}` + `${prefix2}.authn_policies[${i}] must be of type string or object but is ${typeof policy}`, ); } } @@ -754,7 +750,7 @@ const actions = new Map([ checkType( args.disable_bytecode_cache, "boolean?", - "disable_bytecode_cache" + "disable_bytecode_cache", ); }, function (args) { @@ -785,11 +781,11 @@ const actions = new Map([ interpreterFlushVal.set( getSingletonKvKey(), - ccf.jsonCompatibleToBuf(true) + ccf.jsonCompatibleToBuf(true), ); for (const [url, endpoint] of Object.entries( - bundle.metadata.endpoints + bundle.metadata.endpoints, )) { for (const [method, info] of Object.entries(endpoint)) { const key = `${method.toUpperCase()} ${url}`; @@ -800,7 +796,7 @@ const actions = new Map([ endpointsMap.set(keyBuf, infoBuf); } } - } + }, ), ], [ @@ -820,7 +816,7 @@ const actions = new Map([ modulesQuickJsVersionVal.clear(); interpreterFlushVal.clear(); endpointsMap.clear(); - } + }, ), ], [ @@ -832,28 +828,28 @@ const actions = new Map([ checkType( args.max_execution_time_ms, "integer", - "max_execution_time_ms" + "max_execution_time_ms", ); checkType( args.log_exception_details, "boolean?", - "log_exception_details" + "log_exception_details", ); checkType( args.return_exception_details, "boolean?", - "return_exception_details" + "return_exception_details", ); checkType( args.max_cached_interpreters, "integer?", - "max_cached_interpreters" + "max_cached_interpreters", ); }, function (args) { const js_engine_map = ccf.kv["public:ccf.gov.js_runtime_options"]; js_engine_map.set(getSingletonKvKey(), ccf.jsonCompatibleToBuf(args)); - } + }, ), ], [ @@ -862,7 +858,7 @@ const actions = new Map([ function (args) {}, function (args) { ccf.refreshAppBytecodeCache(); - } + }, ), ], [ @@ -878,7 +874,7 @@ const actions = new Map([ const nameBuf = ccf.strToBuf(name); const bundleBuf = ccf.jsonCompatibleToBuf(bundle); ccf.kv["public:ccf.gov.tls.ca_cert_bundles"].set(nameBuf, bundleBuf); - } + }, ), ], [ @@ -891,7 +887,7 @@ const actions = new Map([ const name = args.name; const nameBuf = ccf.strToBuf(name); ccf.kv["public:ccf.gov.tls.ca_cert_bundles"].delete(nameBuf); - } + }, ), ], [ @@ -908,7 +904,7 @@ const actions = new Map([ if (args.auto_refresh) { if (!args.ca_cert_bundle_name) { throw new Error( - "ca_cert_bundle_name is missing but required if auto_refresh is true" + "ca_cert_bundle_name is missing but required if auto_refresh is true", ); } let url; @@ -919,12 +915,12 @@ const actions = new Map([ } if (url.scheme != "https") { throw new Error( - "issuer must be a URL starting with https:// if auto_refresh is true" + "issuer must be a URL starting with https:// if auto_refresh is true", ); } if (url.query || url.fragment) { throw new Error( - "issuer must be a URL without query/fragment if auto_refresh is true" + "issuer must be a URL without query/fragment if auto_refresh is true", ); } } @@ -935,11 +931,11 @@ const actions = new Map([ const caCertBundleNameBuf = ccf.strToBuf(args.ca_cert_bundle_name); if ( !ccf.kv["public:ccf.gov.tls.ca_cert_bundles"].has( - caCertBundleNameBuf + caCertBundleNameBuf, ) ) { throw new Error( - `No CA cert bundle found with name '${caCertBundleName}'` + `No CA cert bundle found with name '${caCertBundleName}'`, ); } } @@ -953,7 +949,7 @@ const actions = new Map([ const issuerBuf = ccf.strToBuf(issuer); const metadataBuf = ccf.jsonCompatibleToBuf(metadata); ccf.kv["public:ccf.gov.jwt.issuers"].set(issuerBuf, metadataBuf); - } + }, ), ], [ @@ -973,7 +969,7 @@ const actions = new Map([ const metadata = ccf.bufToJsonCompatible(metadataBuf); const jwks = args.jwks; ccf.setJwtPublicSigningKeys(issuer, metadata, jwks); - } + }, ), ], [ @@ -989,7 +985,7 @@ const actions = new Map([ } ccf.kv["public:ccf.gov.jwt.issuers"].delete(issuerBuf); ccf.removeJwtPublicSigningKeys(args.issuer); - } + }, ), ], [ @@ -1005,7 +1001,7 @@ const actions = new Map([ // Adding a new allowed code ID changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } + }, ), ], [ @@ -1019,12 +1015,12 @@ const actions = new Map([ const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToJoin"); ccf.kv["public:ccf.gov.nodes.virtual.measurements"].set( measurement, - ALLOWED + ALLOWED, ); // Adding a new allowed measurement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } + }, ), ], [ @@ -1038,12 +1034,12 @@ const actions = new Map([ const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToJoin"); ccf.kv["public:ccf.gov.nodes.snp.measurements"].set( measurement, - ALLOWED + ALLOWED, ); // Adding a new allowed measurement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } + }, ), ], [ @@ -1065,11 +1061,11 @@ const actions = new Map([ uvme[args.feed] = { svn: args.svn }; ccf.kv["public:ccf.gov.nodes.snp.uvm_endorsements"].set( ccf.strToBuf(args.did), - ccf.jsonCompatibleToBuf(uvme) + ccf.jsonCompatibleToBuf(uvme), ); // Adding a new allowed UVM endorsement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } + }, ), ], [ @@ -1082,12 +1078,12 @@ const actions = new Map([ function (args, proposalId) { ccf.kv["public:ccf.gov.nodes.virtual.host_data"].set( ccf.strToBuf(args.host_data), - ccf.jsonCompatibleToBuf(args.metadata) + ccf.jsonCompatibleToBuf(args.metadata), ); // Adding a new allowed host data changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } + }, ), ], [ @@ -1101,12 +1097,12 @@ const actions = new Map([ // SHA-256 digest is the specified host data if (args.security_policy != "") { const securityPolicyDigest = ccf.bufToStr( - ccf.crypto.digest("SHA-256", ccf.strToBuf(args.security_policy)) + ccf.crypto.digest("SHA-256", ccf.strToBuf(args.security_policy)), ); const hostData = ccf.bufToStr(hexStrToBuf(args.host_data)); if (securityPolicyDigest != hostData) { throw new Error( - `The hash of raw policy ${securityPolicyDigest} does not match digest ${hostData}` + `The hash of raw policy ${securityPolicyDigest} does not match digest ${hostData}`, ); } } @@ -1114,12 +1110,12 @@ const actions = new Map([ function (args, proposalId) { ccf.kv["public:ccf.gov.nodes.snp.host_data"].set( ccf.strToBuf(args.host_data), - ccf.jsonCompatibleToBuf(args.security_policy) + ccf.jsonCompatibleToBuf(args.security_policy), ); // Adding a new allowed host data changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } + }, ), ], [ @@ -1131,7 +1127,7 @@ const actions = new Map([ function (args) { const hostData = ccf.strToBuf(args.host_data); ccf.kv["public:ccf.gov.nodes.virtual.host_data"].delete(hostData); - } + }, ), ], [ @@ -1143,7 +1139,7 @@ const actions = new Map([ function (args) { const hostData = ccf.strToBuf(args.host_data); ccf.kv["public:ccf.gov.nodes.snp.host_data"].delete(hostData); - } + }, ), ], [ @@ -1155,7 +1151,7 @@ const actions = new Map([ function (args) { const measurement = ccf.strToBuf(args.measurement); ccf.kv["public:ccf.gov.nodes.virtual.measurements"].delete(measurement); - } + }, ), ], [ @@ -1167,7 +1163,7 @@ const actions = new Map([ function (args) { const measurement = ccf.strToBuf(args.measurement); ccf.kv["public:ccf.gov.nodes.snp.measurements"].delete(measurement); - } + }, ), ], [ @@ -1190,15 +1186,15 @@ const actions = new Map([ if (Object.keys(uvme).length === 0) { // Delete DID if no feed are left ccf.kv["public:ccf.gov.nodes.snp.uvm_endorsements"].delete( - ccf.strToBuf(args.did) + ccf.strToBuf(args.did), ); } else { ccf.kv["public:ccf.gov.nodes.snp.uvm_endorsements"].set( ccf.strToBuf(args.did), - ccf.jsonCompatibleToBuf(uvme) + ccf.jsonCompatibleToBuf(uvme), ); } - } + }, ), ], [ @@ -1217,7 +1213,7 @@ const actions = new Map([ let ni = ccf.bufToJsonCompatible(node_info); ni.node_data = args.node_data; nodes_info.set(node_id, ccf.jsonCompatibleToBuf(ni)); - } + }, ), ], [ @@ -1230,26 +1226,25 @@ const actions = new Map([ checkType( args.validity_period_days, "integer", - "validity_period_days" + "validity_period_days", ); checkBounds( args.validity_period_days, 1, null, - "validity_period_days" + "validity_period_days", ); } }, function (args) { - const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( - getSingletonKvKey() - ); + const rawConfig = + ccf.kv["public:ccf.gov.service.config"].get(getSingletonKvKey()); if (rawConfig === undefined) { throw new Error("Service configuration could not be found"); } const serviceConfig = ccf.bufToJsonCompatible(rawConfig); const node = ccf.kv["public:ccf.gov.nodes.info"].get( - ccf.strToBuf(args.node_id) + ccf.strToBuf(args.node_id), ); if (node === undefined) { throw new Error(`No such node: ${args.node_id}`); @@ -1261,7 +1256,7 @@ const actions = new Map([ ccf.network.getLatestLedgerSecretSeqno(); ccf.kv["public:ccf.gov.nodes.info"].set( ccf.strToBuf(args.node_id), - ccf.jsonCompatibleToBuf(nodeInfo) + ccf.jsonCompatibleToBuf(nodeInfo), ); // Also generate and record service-endorsed node certificate from node CSR @@ -1276,22 +1271,23 @@ const actions = new Map([ args.validity_period_days > max_allowed_cert_validity_period_days ) { throw new Error( - `Validity period ${args.validity_period_days} is not allowed: max allowed is ${max_allowed_cert_validity_period_days}` + `Validity period ${args.validity_period_days} is not allowed: max allowed is ${max_allowed_cert_validity_period_days}`, ); } const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, args.valid_from, - args.validity_period_days ?? max_allowed_cert_validity_period_days + args.validity_period_days ?? + max_allowed_cert_validity_period_days, ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), - ccf.strToBuf(endorsed_node_cert) + ccf.strToBuf(endorsed_node_cert), ); } } - } + }, ), ], [ @@ -1303,7 +1299,7 @@ const actions = new Map([ function (args) { const codeId = ccf.strToBuf(args.code_id); ccf.kv["public:ccf.gov.nodes.code_ids"].delete(codeId); - } + }, ), ], [ @@ -1313,15 +1309,14 @@ const actions = new Map([ checkEntityId(args.node_id, "node_id"); }, function (args) { - const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( - getSingletonKvKey() - ); + const rawConfig = + ccf.kv["public:ccf.gov.service.config"].get(getSingletonKvKey()); if (rawConfig === undefined) { throw new Error("Service configuration could not be found"); } const serviceConfig = ccf.bufToJsonCompatible(rawConfig); const node = ccf.kv["public:ccf.gov.nodes.info"].get( - ccf.strToBuf(args.node_id) + ccf.strToBuf(args.node_id), ); if (node === undefined) { return; @@ -1329,16 +1324,16 @@ const actions = new Map([ const node_obj = ccf.bufToJsonCompatible(node); if (node_obj.status === "Pending") { ccf.kv["public:ccf.gov.nodes.info"].delete( - ccf.strToBuf(args.node_id) + ccf.strToBuf(args.node_id), ); } else { node_obj.status = "Retired"; ccf.kv["public:ccf.gov.nodes.info"].set( ccf.strToBuf(args.node_id), - ccf.jsonCompatibleToBuf(node_obj) + ccf.jsonCompatibleToBuf(node_obj), ); } - } + }, ), ], [ @@ -1351,19 +1346,19 @@ const actions = new Map([ checkType( args.validity_period_days, "integer", - "validity_period_days" + "validity_period_days", ); checkBounds( args.validity_period_days, 1, null, - "validity_period_days" + "validity_period_days", ); } }, function (args) { const node = ccf.kv["public:ccf.gov.nodes.info"].get( - ccf.strToBuf(args.node_id) + ccf.strToBuf(args.node_id), ); if (node === undefined) { throw new Error(`No such node: ${args.node_id}`); @@ -1377,9 +1372,9 @@ const actions = new Map([ args.node_id, nodeInfo, args.valid_from, - args.validity_period_days + args.validity_period_days, ); - } + }, ), ], [ @@ -1391,13 +1386,13 @@ const actions = new Map([ checkType( args.validity_period_days, "integer", - "validity_period_days" + "validity_period_days", ); checkBounds( args.validity_period_days, 1, null, - "validity_period_days" + "validity_period_days", ); } }, @@ -1410,11 +1405,11 @@ const actions = new Map([ nodeId, nodeInfo, args.valid_from, - args.validity_period_days + args.validity_period_days, ); } }); - } + }, ), ], [ @@ -1426,22 +1421,22 @@ const actions = new Map([ checkType( args.validity_period_days, "integer", - "validity_period_days" + "validity_period_days", ); checkBounds( args.validity_period_days, 1, null, - "validity_period_days" + "validity_period_days", ); } }, function (args) { setServiceCertificateValidityPeriod( args.valid_from, - args.validity_period_days + args.validity_period_days, ); - } + }, ), ], [ @@ -1457,7 +1452,7 @@ const actions = new Map([ ].includes(key) ) { throw new Error( - `Cannot change ${key} via set_service_configuration.` + `Cannot change ${key} via set_service_configuration.`, ); } } @@ -1467,18 +1462,18 @@ const actions = new Map([ checkType( args.recent_cose_proposals_window_size, "integer?", - "recent cose proposals window size" + "recent cose proposals window size", ); checkBounds( args.recent_cose_proposals_window_size, 1, 10000, - "recent cose proposals window size" + "recent cose proposals window size", ); }, function (args) { updateServiceConfig(args); - } + }, ), ], [ @@ -1487,7 +1482,7 @@ const actions = new Map([ function (args) {}, function (args, proposalId) { ccf.node.triggerLedgerChunk(); - } + }, ), ], [ @@ -1496,7 +1491,7 @@ const actions = new Map([ function (args) {}, function (args, proposalId) { ccf.node.triggerSnapshot(); - } + }, ), ], [ @@ -1506,12 +1501,12 @@ const actions = new Map([ checkType( args.interfaces, "array?", - "interfaces to refresh the certificates for" + "interfaces to refresh the certificates for", ); }, function (args, proposalId) { ccf.node.triggerACMERefresh(args.interfaces); - } + }, ), ], [ @@ -1529,7 +1524,7 @@ const actions = new Map([ throw new Error("Service identity certificate mismatch"); } }, - function (args) {} + function (args) {}, ), ], ]); diff --git a/tests/governance.py b/tests/governance.py index cd5e04f3a7e2..e778d9a3f4ff 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -2,7 +2,6 @@ # Licensed under the Apache 2.0 License. import os import http -import subprocess import infra.network import infra.path import infra.proc @@ -545,7 +544,6 @@ def gov(args): test_consensus_status(network, args) test_member_data(network, args) network = test_all_members(network, args) - test_quote(network, args) test_user(network, args) test_jinja_templates(network, args) test_no_quote(network, args) From 75f1dbf874dbad283f4a7c9d81fb05e969870988 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 17 Jan 2025 16:20:28 +0000 Subject: [PATCH 14/36] Schema bump --- doc/schemas/node_openapi.json | 4 ++-- src/node/rpc/node_frontend.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/schemas/node_openapi.json b/doc/schemas/node_openapi.json index ebce76f54057..c75662f7dc06 100644 --- a/doc/schemas/node_openapi.json +++ b/doc/schemas/node_openapi.json @@ -576,7 +576,7 @@ "format": { "$ref": "#/components/schemas/QuoteFormat" }, - "mrenclave": { + "measurement": { "$ref": "#/components/schemas/string" }, "node_id": { @@ -858,7 +858,7 @@ "info": { "description": "This API provides public, uncredentialed access to service and node state.", "title": "CCF Public Node API", - "version": "4.11.0" + "version": "4.12.0" }, "openapi": "3.0.0", "paths": { diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 285af2317e2f..7fb160c8cb11 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -406,7 +406,7 @@ namespace ccf openapi_info.description = "This API provides public, uncredentialed access to service and node " "state."; - openapi_info.document_version = "4.11.0"; + openapi_info.document_version = "4.12.0"; } void init_handlers() override From d4a0461b0e8dd4043e991f511af84bf92e17bce9 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 17 Jan 2025 16:27:13 +0000 Subject: [PATCH 15/36] Remove debug logging --- src/node/node_state.h | 4 ---- src/service/internal_tables_access.h | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index d9db48ce8953..59251cf9ede9 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1978,10 +1978,6 @@ namespace ccf create_params.snp_security_policy = config.attestation.environment.security_policy; - LOG_INFO_FMT( - "!!!! create_params.snp_security_policy = {}", - create_params.snp_security_policy.value_or("\"\"")); - create_params.node_info_network = config.network; create_params.node_data = config.node_data; create_params.service_data = config.service_data; diff --git a/src/service/internal_tables_access.h b/src/service/internal_tables_access.h index 180d002704a7..cbab0132c19e 100644 --- a/src/service/internal_tables_access.h +++ b/src/service/internal_tables_access.h @@ -625,10 +625,6 @@ namespace ccf const HostData& host_data, const std::optional& metadata) { - LOG_INFO_FMT( - "!!!! trust_node_virtual_host_data({}, {})", - host_data.hex_str(), - metadata.value_or("\"\"")); auto host_data_table = tx.wo(Tables::VIRTUAL_HOST_DATA); host_data_table->put(host_data, metadata.value_or("")); From 488d573f2c6f22f19d56f56ffbe2f7c301bcf16d Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Tue, 21 Jan 2025 09:53:39 +0000 Subject: [PATCH 16/36] Document new tables --- doc/audit/builtin_maps.rst | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/doc/audit/builtin_maps.rst b/doc/audit/builtin_maps.rst index 5b9fda374787..6d6ba9b13edc 100644 --- a/doc/audit/builtin_maps.rst +++ b/doc/audit/builtin_maps.rst @@ -138,10 +138,30 @@ DEPRECATED. Previously contained versions of the code allowed to join the curren * - ``cae46d1...bb908b64e`` - ``ALLOWED_TO_JOIN`` +``nodes.virtual.host_data`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Virtual table mimicking SNP host_data, restricting which host_data values may be presented by new nodes joining the network. + +**Key** Host data: The host data. + +**Value** Metadata: The platform specific meaning of the host data. + +``nodes.virtual.measurements`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Trusted virtual measurements for new nodes allowed to join the network. Virtual measurements are constructed by CCF to test and debug code update flows on hardware without TEE protections. + +.. warning:: Since virtual nodes provide no protection, this should be empty on production instances. + +**Key** Measurement, represented as a base64 hex-encoded string (length: 64). + +**Value** Status represented as JSON. + ``nodes.snp.host_data`` ~~~~~~~~~~~~~~~~~~~~~~~ -Trusted attestation report host data field for new nodes allowed to join the network (:doc:`SNP <../operations/platforms/snp>` only). +Trusted attestation report host data field for new nodes allowed to join the network (:doc:`SNP <../operations/platforms/snp>` only). Only the presence of the joiner's host data key is checked, so the metadata is optional and may be empty for space-saving or privacy reasons. **Key** Host data: The host data. @@ -150,7 +170,7 @@ Trusted attestation report host data field for new nodes allowed to join the net ``nodes.snp.measurements`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ -Trusted measurements for new nodes allowed to join the network (:doc:`SNP <../operations/platforms/snp>` only). +Trusted SNP measurements for new nodes allowed to join the network (:doc:`SNP <../operations/platforms/snp>` only). .. note:: For improved serviceability on confidential ACI deployments, see :ref:`audit/builtin_maps:``nodes.snp.uvm_endorsements``` map. @@ -387,7 +407,7 @@ JWT signing keys, used until 6.0. **Value** List of (DER-encoded certificate, issuer, constraint), represented as JSON. ``jwt.public_signing_keys_metadata_v2`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JWT signing keys, from 6.0.0 onwards. From d402708479628402fc0870aca1817410e62c309c Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Tue, 21 Jan 2025 10:34:07 +0000 Subject: [PATCH 17/36] Update reconfig test --- tests/reconfiguration.py | 66 +++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 24962140b5e0..7751ef6ab123 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -576,40 +576,38 @@ def test_issue_fake_join(network, args): == "Quote report data does not contain node's public key hash" ) - LOG.info("Join with AMD SEV-SNP quote") - req["quote_info"] = { - "format": "AMD_SEV_SNP_v1", - "quote": own_quote["raw"], - "endorsements": own_quote["endorsements"], - } - if args.enclave_platform == "snp": - req["quote_info"]["uvm_endorsements"] = own_quote["uvm_endorsements"] - r = c.post("/node/join", body=req) - if args.enclave_platform != "snp": - assert r.status_code == http.HTTPStatus.UNAUTHORIZED - assert r.body.json()["error"]["code"] == "InvalidQuote" - assert r.body.json()["error"]["message"] == "Quote could not be verified" - else: - assert ( - r.body.json()["error"]["message"] - == "Quote report data does not contain node's public key hash" - ) - - LOG.info("Join with virtual quote") - req["quote_info"] = { - "format": "Insecure_Virtual", - "quote": "", - "endorsements": "", - } - r = c.post("/node/join", body=req) - if args.enclave_platform == "virtual": - assert r.status_code == http.HTTPStatus.OK - assert r.body.json()["node_status"] == ccf.ledger.NodeStatus.PENDING.value - else: - assert r.status_code == http.HTTPStatus.UNAUTHORIZED - assert ( - r.body.json()["error"]["code"] == "InvalidQuote" - ), "Virtual node must never join non-virtual network" + for platform, info, format in ( + ( + "snp", + "Join with AMD SEV-SNP quote", + "AMD_SEV_SNP_v1", + ), + ( + "virtual", + "Join with virtual quote", + "Insecure_Virtual", + ), + ): + LOG.info(info) + req["quote_info"] = { + "format": format, + "quote": own_quote["raw"], + "endorsements": own_quote["endorsements"], + } + if args.enclave_platform == "snp": + req["quote_info"]["uvm_endorsements"] = own_quote["uvm_endorsements"] + r = c.post("/node/join", body=req) + if args.enclave_platform != platform: + assert r.status_code == http.HTTPStatus.UNAUTHORIZED + assert r.body.json()["error"]["code"] == "InvalidQuote" + assert ( + r.body.json()["error"]["message"] == "Quote could not be verified" + ) + else: + assert ( + r.body.json()["error"]["message"] + == "Quote report data does not contain node's public key hash" + ) return network From e032125a4ef24dee22516c9768b1eb7ab01b1a0a Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Tue, 21 Jan 2025 14:46:47 +0000 Subject: [PATCH 18/36] Update API descriptions --- doc/schemas/gov/2024-07-01/gov.json | 33 ++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/doc/schemas/gov/2024-07-01/gov.json b/doc/schemas/gov/2024-07-01/gov.json index 83162c351d40..b15915fd5509 100644 --- a/doc/schemas/gov/2024-07-01/gov.json +++ b/doc/schemas/gov/2024-07-01/gov.json @@ -1708,11 +1708,16 @@ "snp": { "$ref": "#/definitions/ServiceState.SnpJoinPolicy", "description": "Policy applied to nodes running in AMD SEV-SNP containers." + }, + "virtual": { + "$ref": "#/definitions/ServiceState.VirtualJoinPolicy", + "description": "Policy applied to virtual nodes (insecure, intended for debugging)." } }, "required": [ "sgx", - "snp" + "snp", + "virtual" ] }, "ServiceState.JoinPolicy": { @@ -2232,6 +2237,32 @@ ], "x-ms-discriminator-value": "OE_SGX_v1" }, + "ServiceState.VirtualJoinPolicy": { + "type": "object", + "description": "Join policy fields specific to nodes running on virtual nodes with no hardware protection.", + "properties": { + "measurements": { + "type": "array", + "description": "Code measurements of acceptable enclaves.", + "items": { + "type": "string", + "format": "byte" + } + }, + "hostData": { + "type": "object", + "description": "Collection of acceptable host data values.", + "additionalProperties": { + "format": "byte", + "type": "string" + } + } + }, + "required": [ + "measurements", + "hostData" + ] + }, "ServiceState.SnpJoinPolicy": { "type": "object", "description": "Join policy fields specific to nodes running on AMD SEV-SNP hardware.", From c0ff3efb130bf15952c2aaf0ceeda1cad9ab1352 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Tue, 21 Jan 2025 15:37:25 +0000 Subject: [PATCH 19/36] Minimal plumbing to enable test_add_node_with_bad_security_policy --- src/host/main.cpp | 11 +++++----- tests/code_update.py | 31 ++++++++++++++++----------- tests/infra/remote.py | 49 +++++++++++++++++++++---------------------- 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/host/main.cpp b/src/host/main.cpp index 8449c91929ee..13604336dd35 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -536,12 +536,13 @@ int main(int argc, char** argv) // Hard-coded here and repeated in the relevant tests. Can be made dynamic // (eg - from an env var or file) when the tests are able to run SNP nodes // with distinct policies - startup_config.attestation.environment.security_policy = - default_virtual_security_policy; + char const* policy = std::getenv("CCF_VIRTUAL_SECURITY_POLICY"); + if (policy == nullptr) + { + policy = default_virtual_security_policy; + } + startup_config.attestation.environment.security_policy = policy; } - LOG_INFO_FMT( - "!!!! startup_config.attestation.environment.security_policy = {}", - startup_config.attestation.environment.security_policy.value_or("\"\"")); if (config.enclave.platform == host::EnclavePlatform::VIRTUAL) { diff --git a/tests/code_update.py b/tests/code_update.py index 08e620e04708..e051b1b4824b 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -289,19 +289,20 @@ def test_add_node_with_stubbed_security_policy(network, args): @reqs.description("Start node with mismatching security policy") -@reqs.snp_only() def test_add_node_with_bad_security_policy(network, args): try: - security_context_dir = snp.get_security_context_dir() with tempfile.TemporaryDirectory() as snp_dir: - if security_context_dir is not None: - shutil.copytree(security_context_dir, snp_dir, dirs_exist_ok=True) - with open( - os.path.join(snp_dir, snp.ACI_SEV_SNP_FILENAME_SECURITY_POLICY), - "w", - encoding="utf-8", - ) as f: - f.write(b64encode(b"invalid_security_policy").decode()) + security_context_dir = None + if snp.IS_SNP: + security_context_dir = snp.get_security_context_dir() + if security_context_dir is not None: + shutil.copytree(security_context_dir, snp_dir, dirs_exist_ok=True) + with open( + os.path.join(snp_dir, snp.ACI_SEV_SNP_FILENAME_SECURITY_POLICY), + "w", + encoding="utf-8", + ) as f: + f.write(b64encode(b"invalid_security_policy").decode()) new_node = network.create_node("local://localhost") network.join_node( @@ -310,6 +311,7 @@ def test_add_node_with_bad_security_policy(network, args): args, timeout=3, snp_uvm_security_context_dir=snp_dir if security_context_dir else None, + env={"CCF_VIRTUAL_SECURITY_POLICY": "invalid_security_policy"}, ) except (TimeoutError, RuntimeError): LOG.info("As expected, node with invalid security policy failed to startup") @@ -410,7 +412,12 @@ def test_add_node_with_bad_code(network, args): code_not_found_exception = None try: new_node = network.create_node("local://localhost") - network.join_node(new_node, replacement_package, args, timeout=3) + network.join_node( + new_node, + replacement_package, + args, + timeout=3, + ) except infra.network.CodeIdNotFound as err: code_not_found_exception = err @@ -584,10 +591,10 @@ def run(args): test_host_data_tables(network, args) test_add_node_with_bad_host_data(network, args) test_add_node_with_stubbed_security_policy(network, args) + test_add_node_with_bad_security_policy(network, args) if snp.IS_SNP: test_add_node_without_security_policy(network, args) - test_add_node_with_bad_security_policy(network, args) # Endorsements if snp.IS_SNP: diff --git a/tests/infra/remote.py b/tests/infra/remote.py index 2bf52563363f..e07fd8f618c0 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -352,32 +352,31 @@ def __init__( snp_security_context_directory_envvar = None - if "env" in kwargs: - env = kwargs["env"] - else: - env = {} - if enclave_platform == "virtual": - env["UBSAN_OPTIONS"] = "print_stacktrace=1" - ubsan_opts = kwargs.get("ubsan_options") - if ubsan_opts: - env["UBSAN_OPTIONS"] += ":" + ubsan_opts - env["TSAN_OPTIONS"] = os.environ.get("TSAN_OPTIONS", "") - # https://github.com/microsoft/CCF/issues/5198 - env["ASAN_OPTIONS"] = os.environ.get( - "ASAN_OPTIONS", "alloc_dealloc_mismatch=0" - ) - elif enclave_platform == "snp": - env = snp.get_aci_env() - snp_security_context_directory_envvar = ( - snp.ACI_SEV_SNP_ENVVAR_UVM_SECURITY_CONTEXT_DIR - if set_snp_uvm_security_context_dir_envvar - and snp.ACI_SEV_SNP_ENVVAR_UVM_SECURITY_CONTEXT_DIR in env - else None + env = kwargs.get("env", {}) + if enclave_platform == "snp": + env.update(snp.get_aci_env()) + + if enclave_platform == "virtual": + env["UBSAN_OPTIONS"] = "print_stacktrace=1" + ubsan_opts = kwargs.get("ubsan_options") + if ubsan_opts: + env["UBSAN_OPTIONS"] += ":" + ubsan_opts + env["TSAN_OPTIONS"] = os.environ.get("TSAN_OPTIONS", "") + # https://github.com/microsoft/CCF/issues/5198 + env["ASAN_OPTIONS"] = os.environ.get( + "ASAN_OPTIONS", "alloc_dealloc_mismatch=0" + ) + elif enclave_platform == "snp": + snp_security_context_directory_envvar = ( + snp.ACI_SEV_SNP_ENVVAR_UVM_SECURITY_CONTEXT_DIR + if set_snp_uvm_security_context_dir_envvar + and snp.ACI_SEV_SNP_ENVVAR_UVM_SECURITY_CONTEXT_DIR in env + else None + ) + if snp_uvm_security_context_dir is not None: + env[snp_security_context_directory_envvar] = ( + snp_uvm_security_context_dir ) - if snp_uvm_security_context_dir is not None: - env[snp_security_context_directory_envvar] = ( - snp_uvm_security_context_dir - ) oe_log_level = CCF_TO_OE_LOG_LEVEL.get(kwargs.get("host_log_level")) if oe_log_level: From e6342e2dc571a2a7336d402656c96797257ebde8 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Tue, 21 Jan 2025 15:53:31 +0000 Subject: [PATCH 20/36] Justifying comment --- tests/code_update.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/code_update.py b/tests/code_update.py index e051b1b4824b..40ccc6bc714a 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -594,6 +594,12 @@ def run(args): test_add_node_with_bad_security_policy(network, args) if snp.IS_SNP: + # Not tested on virtual, as this relies on the fact that the local security + # policy can be ignored on SNP. It has been applied already, and its digest + # is available as a host_data claim, so the raw policy is merely an audit + # nicety and nodes will launch without it. This is not true on virtual, where + # an actual "security policy" value is always digested at launch time to + # produce a host_data value. test_add_node_without_security_policy(network, args) # Endorsements From 998e0b8ee2ba6ed2a94a0dd991f3f4568bafa723 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Wed, 22 Jan 2025 16:05:08 +0000 Subject: [PATCH 21/36] Update doc/audit/builtin_maps.rst Co-authored-by: Amaury Chamayou --- doc/audit/builtin_maps.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/audit/builtin_maps.rst b/doc/audit/builtin_maps.rst index 6d6ba9b13edc..5e6da134b70b 100644 --- a/doc/audit/builtin_maps.rst +++ b/doc/audit/builtin_maps.rst @@ -141,7 +141,7 @@ DEPRECATED. Previously contained versions of the code allowed to join the curren ``nodes.virtual.host_data`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Virtual table mimicking SNP host_data, restricting which host_data values may be presented by new nodes joining the network. +Map mimicking SNP host_data for virtual nodes, restricting which host_data values may be presented by new nodes joining the network. **Key** Host data: The host data. From 7c362f686cc60cd50e39bf99653e1e5a9b841904 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Wed, 22 Jan 2025 16:05:26 +0000 Subject: [PATCH 22/36] Update include/ccf/pal/attestation.h Co-authored-by: Amaury Chamayou --- include/ccf/pal/attestation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/ccf/pal/attestation.h b/include/ccf/pal/attestation.h index eeae9da853f5..14f15167ed9c 100644 --- a/include/ccf/pal/attestation.h +++ b/include/ccf/pal/attestation.h @@ -238,7 +238,7 @@ namespace ccf::pal else { throw std::logic_error( - "Cannot verify SGX attestation report in this build"); + "SGX attestation reports are no longer supported from 6.0.0 onwards"); } } #endif From 551ea7e347126be65faf22f71abe3cbb79127b1c Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Wed, 22 Jan 2025 16:52:31 +0000 Subject: [PATCH 23/36] Remove virtual attestation actions from default constitution --- CMakeLists.txt | 14 +++- samples/constitutions/default/actions.js | 61 ------------------ .../virtual/virtual_attestation_actions.js | 64 +++++++++++++++++++ 3 files changed, 75 insertions(+), 64 deletions(-) create mode 100644 samples/constitutions/virtual/virtual_attestation_actions.js diff --git a/CMakeLists.txt b/CMakeLists.txt index d81dc186bcb3..256d908906f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1120,8 +1120,13 @@ if(BUILD_TESTS) add_e2e_test( NAME code_update_test PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/code_update.py - ADDITIONAL_ARGS --oe-binary ${OE_BINDIR} --js-app-bundle - ${CMAKE_SOURCE_DIR}/samples/apps/logging/js + ADDITIONAL_ARGS + --oe-binary + ${OE_BINDIR} + --js-app-bundle + ${CMAKE_SOURCE_DIR}/samples/apps/logging/js + --constitution + ${CMAKE_SOURCE_DIR}/samples/constitutions/virtual/virtual_attestation_actions.js ) if(BUILD_TPCC) @@ -1268,7 +1273,10 @@ if(BUILD_TESTS) NAME lts_compatibility PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/lts_compatibility.py LABEL e2e - ADDITIONAL_ARGS ${LTS_TEST_ARGS} + ADDITIONAL_ARGS + ${LTS_TEST_ARGS} + --constitution + ${CMAKE_SOURCE_DIR}/samples/constitutions/virtual/virtual_attestation_actions.js ) set_property( TEST lts_compatibility diff --git a/samples/constitutions/default/actions.js b/samples/constitutions/default/actions.js index 08cba468231c..7a41bc0a8e06 100644 --- a/samples/constitutions/default/actions.js +++ b/samples/constitutions/default/actions.js @@ -1004,25 +1004,6 @@ const actions = new Map([ }, ), ], - [ - "add_virtual_measurement", - new Action( - function (args) { - checkType(args.measurement, "string", "measurement"); - }, - function (args, proposalId) { - const measurement = ccf.strToBuf(args.measurement); - const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToJoin"); - ccf.kv["public:ccf.gov.nodes.virtual.measurements"].set( - measurement, - ALLOWED, - ); - - // Adding a new allowed measurement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification - invalidateOtherOpenProposals(proposalId); - }, - ), - ], [ "add_snp_measurement", new Action( @@ -1068,24 +1049,6 @@ const actions = new Map([ }, ), ], - [ - "add_virtual_host_data", - new Action( - function (args) { - checkType(args.host_data, "string", "host_data"); - checkType(args.metadata, "string", "metadata"); - }, - function (args, proposalId) { - ccf.kv["public:ccf.gov.nodes.virtual.host_data"].set( - ccf.strToBuf(args.host_data), - ccf.jsonCompatibleToBuf(args.metadata), - ); - - // Adding a new allowed host data changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification - invalidateOtherOpenProposals(proposalId); - }, - ), - ], [ "add_snp_host_data", new Action( @@ -1118,18 +1081,6 @@ const actions = new Map([ }, ), ], - [ - "remove_virtual_host_data", - new Action( - function (args) { - checkType(args.host_data, "string", "host_data"); - }, - function (args) { - const hostData = ccf.strToBuf(args.host_data); - ccf.kv["public:ccf.gov.nodes.virtual.host_data"].delete(hostData); - }, - ), - ], [ "remove_snp_host_data", new Action( @@ -1142,18 +1093,6 @@ const actions = new Map([ }, ), ], - [ - "remove_virtual_measurement", - new Action( - function (args) { - checkType(args.measurement, "string", "measurement"); - }, - function (args) { - const measurement = ccf.strToBuf(args.measurement); - ccf.kv["public:ccf.gov.nodes.virtual.measurements"].delete(measurement); - }, - ), - ], [ "remove_snp_measurement", new Action( diff --git a/samples/constitutions/virtual/virtual_attestation_actions.js b/samples/constitutions/virtual/virtual_attestation_actions.js new file mode 100644 index 000000000000..d8116d861749 --- /dev/null +++ b/samples/constitutions/virtual/virtual_attestation_actions.js @@ -0,0 +1,64 @@ +actions.set( + "add_virtual_measurement", + new Action( + function (args) { + checkType(args.measurement, "string", "measurement"); + }, + function (args, proposalId) { + const measurement = ccf.strToBuf(args.measurement); + const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToJoin"); + ccf.kv["public:ccf.gov.nodes.virtual.measurements"].set( + measurement, + ALLOWED + ); + + // Adding a new allowed measurement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification + invalidateOtherOpenProposals(proposalId); + } + ) +); + +actions.set( + "add_virtual_host_data", + new Action( + function (args) { + checkType(args.host_data, "string", "host_data"); + checkType(args.metadata, "string", "metadata"); + }, + function (args, proposalId) { + ccf.kv["public:ccf.gov.nodes.virtual.host_data"].set( + ccf.strToBuf(args.host_data), + ccf.jsonCompatibleToBuf(args.metadata) + ); + + // Adding a new allowed host data changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification + invalidateOtherOpenProposals(proposalId); + } + ) +); + +actions.set( + "remove_virtual_host_data", + new Action( + function (args) { + checkType(args.host_data, "string", "host_data"); + }, + function (args) { + const hostData = ccf.strToBuf(args.host_data); + ccf.kv["public:ccf.gov.nodes.virtual.host_data"].delete(hostData); + } + ) +); + +actions.set( + "remove_virtual_measurement", + new Action( + function (args) { + checkType(args.measurement, "string", "measurement"); + }, + function (args) { + const measurement = ccf.strToBuf(args.measurement); + ccf.kv["public:ccf.gov.nodes.virtual.measurements"].delete(measurement); + } + ) +); From cde04dbf9bda17be79bde625a979f96410d7240f Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Wed, 22 Jan 2025 16:56:26 +0000 Subject: [PATCH 24/36] A helpful error for future travellers --- tests/infra/remote.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/infra/remote.py b/tests/infra/remote.py index e07fd8f618c0..4a777d266ea0 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -437,6 +437,9 @@ def __init__( # Constitution constitution = [os.path.basename(f) for f in constitution] + assert len(set(constitution)) == len( + constitution + ), f"Constitution contains files with duplicate names, which is not going to do what you want. Recommend renaming one of them, or improving this infra to copy them to unique names." # ACME if "acme" in kwargs and host.acme_challenge_server_interface: From 41e8efa272d9b7e48fa08e31919bf9bc18d685a9 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Wed, 22 Jan 2025 16:58:08 +0000 Subject: [PATCH 25/36] Remove --oe-binary, celebrate --- CMakeLists.txt | 15 +++------------ tests/infra/e2e_args.py | 7 ------- tests/jwt_test.py | 28 ---------------------------- 3 files changed, 3 insertions(+), 47 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 256d908906f2..4ca2f2ffeefc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -301,8 +301,6 @@ set(CCF_JS_SOURCES ${CCF_DIR}/src/js/registry.cpp ) -set(OE_BINDIR "") - if(COMPILE_TARGET STREQUAL "snp") add_host_library(ccf_js.snp "${CCF_JS_SOURCES}") add_san(ccf_js.snp) @@ -1027,8 +1025,6 @@ if(BUILD_TESTS) PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/e2e_suite.py LABEL suite ADDITIONAL_ARGS - --oe-binary - ${OE_BINDIR} --ledger-recovery-timeout 20 --test-duration @@ -1106,9 +1102,8 @@ if(BUILD_TESTS) NAME governance_test PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/governance.py CONSTITUTION ${CONSTITUTION_ARGS} - ADDITIONAL_ARGS - --oe-binary ${OE_BINDIR} --initial-operator-count 1 - --jinja-templates-path ${CMAKE_SOURCE_DIR}/samples/templates + ADDITIONAL_ARGS --initial-operator-count 1 --jinja-templates-path + ${CMAKE_SOURCE_DIR}/samples/templates ) add_e2e_test( @@ -1121,8 +1116,6 @@ if(BUILD_TESTS) NAME code_update_test PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/code_update.py ADDITIONAL_ARGS - --oe-binary - ${OE_BINDIR} --js-app-bundle ${CMAKE_SOURCE_DIR}/samples/apps/logging/js --constitution @@ -1261,9 +1254,7 @@ if(BUILD_TESTS) ${CMAKE_SOURCE_DIR}/python/config_1_x.ini ) - list(APPEND LTS_TEST_ARGS --oe-binary ${OE_BINDIR} --ccf-version - ${CCF_VERSION} - ) + list(APPEND LTS_TEST_ARGS --ccf-version ${CCF_VERSION}) if(LONG_TESTS) list(APPEND LTS_TEST_ARGS --check-ledger-compatibility) endif() diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index 14782b40dabe..b6123217c8b3 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -80,13 +80,6 @@ def cli_args( help="Path to CCF binaries (cchost, scurl, keygenerator)", default=".", ) - parser.add_argument( - "--oe-binary", - help="Path to Open Enclave binary folder", - type=str, - nargs="?", - default="/opt/openenclave/bin/", - ) parser.add_argument( "--library-dir", help="Path to CCF libraries (enclave images)", diff --git a/tests/jwt_test.py b/tests/jwt_test.py index bca66e1e9b96..c4511af2c758 100644 --- a/tests/jwt_test.py +++ b/tests/jwt_test.py @@ -282,34 +282,6 @@ def test_jwt_without_key_policy(network, args): return network -def make_attested_cert(network, args): - keygen = os.path.join(args.binary_dir, "keygenerator.sh") - oeutil = os.path.join(args.oe_binary, "oeutil") - infra.proc.ccall( - keygen, "--name", "attested", "--gen-enc-key", path=network.common_dir - ).check_returncode() - privk = os.path.join(network.common_dir, "attested_enc_privk.pem") - pubk = os.path.join(network.common_dir, "attested_enc_pubk.pem") - der = os.path.join(network.common_dir, "oe_cert.der") - infra.proc.ccall( - oeutil, - "generate-evidence", - "-f", - "cert", - privk, - pubk, - "-o", - der, - # To ensure in-process attestation is always used, clear the env to unset the SGX_AESM_ADDR variable - env={}, - ).check_returncode() - pem = os.path.join(network.common_dir, "oe_cert.pem") - infra.proc.ccall( - "openssl", "x509", "-inform", "der", "-in", der, "-out", pem - ).check_returncode() - return pem - - def check_kv_jwt_key_matches(args, network, kid, key_pem): primary, _ = network.find_nodes() latest_jwt_signing_keys = get_jwt_keys(args, primary) From 1a54d1ef116934c6ccfc97635fcd826e3dbe41f8 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Wed, 22 Jan 2025 17:17:36 +0000 Subject: [PATCH 26/36] Subtleties --- tests/lts_compatibility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index 64b9d4f5b895..ffe70e168e69 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -78,7 +78,7 @@ def replace_constitution_fragment(args, fragment_name): args.constitution[:] = [ ( os.path.join(constitution_directory, fragment_name) - if fragment_name in f + if os.path.basename(f) == fragment_name else f ) for f in args.constitution From fdb1095a6ba7d1d73fa6926eafd65f7a349326b0 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Wed, 22 Jan 2025 17:23:57 +0000 Subject: [PATCH 27/36] artforms --- .../virtual/virtual_attestation_actions.js | 20 +++++++++---------- tests/infra/remote.py | 2 +- tests/jwt_test.py | 1 - 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/samples/constitutions/virtual/virtual_attestation_actions.js b/samples/constitutions/virtual/virtual_attestation_actions.js index d8116d861749..9756a9bcaf0d 100644 --- a/samples/constitutions/virtual/virtual_attestation_actions.js +++ b/samples/constitutions/virtual/virtual_attestation_actions.js @@ -9,13 +9,13 @@ actions.set( const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToJoin"); ccf.kv["public:ccf.gov.nodes.virtual.measurements"].set( measurement, - ALLOWED + ALLOWED, ); // Adding a new allowed measurement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } - ) + }, + ), ); actions.set( @@ -28,13 +28,13 @@ actions.set( function (args, proposalId) { ccf.kv["public:ccf.gov.nodes.virtual.host_data"].set( ccf.strToBuf(args.host_data), - ccf.jsonCompatibleToBuf(args.metadata) + ccf.jsonCompatibleToBuf(args.metadata), ); // Adding a new allowed host data changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } - ) + }, + ), ); actions.set( @@ -46,8 +46,8 @@ actions.set( function (args) { const hostData = ccf.strToBuf(args.host_data); ccf.kv["public:ccf.gov.nodes.virtual.host_data"].delete(hostData); - } - ) + }, + ), ); actions.set( @@ -59,6 +59,6 @@ actions.set( function (args) { const measurement = ccf.strToBuf(args.measurement); ccf.kv["public:ccf.gov.nodes.virtual.measurements"].delete(measurement); - } - ) + }, + ), ); diff --git a/tests/infra/remote.py b/tests/infra/remote.py index 4a777d266ea0..7de694710563 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -439,7 +439,7 @@ def __init__( constitution = [os.path.basename(f) for f in constitution] assert len(set(constitution)) == len( constitution - ), f"Constitution contains files with duplicate names, which is not going to do what you want. Recommend renaming one of them, or improving this infra to copy them to unique names." + ), f"Constitution contains files with duplicate names, which is not going to do what you want. Recommend renaming one of them, or improving this infra to copy them to unique names. {constitution=}" # ACME if "acme" in kwargs and host.acme_challenge_server_interface: diff --git a/tests/jwt_test.py b/tests/jwt_test.py index c4511af2c758..71cee7ff0b35 100644 --- a/tests/jwt_test.py +++ b/tests/jwt_test.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache 2.0 License. -import os import tempfile import json import time From 00757321defa1ffefb51f411e9e658582c4d1f7e Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Thu, 23 Jan 2025 11:43:53 +0000 Subject: [PATCH 28/36] Rename CodeID to measurement in some Python errors/tests --- tests/code_update.py | 20 +++++++++++--------- tests/infra/network.py | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/code_update.py b/tests/code_update.py index 40ccc6bc714a..96ea484767b7 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -371,7 +371,7 @@ def test_add_node_with_no_uvm_endorsements(network, args): timeout=3, snp_uvm_security_context_dir=snp_dir if security_context_dir else None, ) - except infra.network.CodeIdNotFound: + except infra.network.MeasurementNotFound: LOG.info("As expected, node with no UVM endorsements failed to join") else: raise AssertionError("Node join unexpectedly succeeded") @@ -400,16 +400,18 @@ def test_add_node_with_no_uvm_endorsements(network, args): return network -@reqs.description("Node with bad code fails to join") -def test_add_node_with_bad_code(network, args): +@reqs.description("Node with bad measurement fails to join") +def test_add_node_with_bad_measurement(network, args): if args.enclave_platform == "snp": - LOG.warning("Skipping test_add_node_with_bad_code with SNP enclave") + LOG.warning( + "Skipping test_add_node_with_bad_measurement with SNP - cannot affect measurement on SNP" + ) return network replacement_package = get_replacement_package(args) LOG.info(f"Adding unsupported node running {replacement_package}") - code_not_found_exception = None + measurement_not_found_exception = None try: new_node = network.create_node("local://localhost") network.join_node( @@ -418,11 +420,11 @@ def test_add_node_with_bad_code(network, args): args, timeout=3, ) - except infra.network.CodeIdNotFound as err: - code_not_found_exception = err + except infra.network.MeasurementNotFound as err: + measurement_not_found_exception = err assert ( - code_not_found_exception is not None + measurement_not_found_exception is not None ), f"Adding a node with {replacement_package} should fail" return network @@ -585,7 +587,7 @@ def run(args): # Measurements test_measurements_tables(network, args) - test_add_node_with_bad_code(network, args) + test_add_node_with_bad_measurement(network, args) # Host data/security policy test_host_data_tables(network, args) diff --git a/tests/infra/network.py b/tests/infra/network.py index 3bd6241afc80..cb3ba6669ec0 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -67,7 +67,7 @@ class PrimaryNotFound(Exception): pass -class CodeIdNotFound(Exception): +class MeasurementNotFound(Exception): pass @@ -926,7 +926,7 @@ def run_join_node( # Throw accurate exceptions if known errors found in for error in errors: if "Quote does not contain known enclave measurement" in error: - raise CodeIdNotFound from e + raise MeasurementNotFound from e if "UVM endorsements are not authorised" in error: raise UVMEndorsementsNotAuthorised from e if "StartupSeqnoIsOld" in error: From c76a61cd86476741da799983b06692abd8f7de89 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Thu, 23 Jan 2025 16:57:27 +0000 Subject: [PATCH 29/36] WIP: Move digest to host_data --- doc/schemas/gov/2024-07-01/gov.json | 8 +- include/ccf/pal/attestation.h | 5 +- include/ccf/pal/measurement.h | 12 +- include/ccf/service/tables/host_data.h | 2 +- .../virtual/virtual_attestation_actions.js | 41 ++-- src/host/main.cpp | 20 +- src/node/gov/handlers/service_state.h | 15 +- src/node/quote.cpp | 13 +- src/node/rpc/member_frontend.h | 12 +- src/node/rpc/node_frontend.h | 4 +- src/pal/quote_generation.h | 7 +- src/service/internal_tables_access.h | 11 +- tests/code_update.py | 201 +++++++++++++----- tests/infra/consortium.py | 8 +- tests/infra/utils.py | 18 +- tests/lts_compatibility.py | 14 +- 16 files changed, 236 insertions(+), 155 deletions(-) diff --git a/doc/schemas/gov/2024-07-01/gov.json b/doc/schemas/gov/2024-07-01/gov.json index b15915fd5509..e7979b6639d7 100644 --- a/doc/schemas/gov/2024-07-01/gov.json +++ b/doc/schemas/gov/2024-07-01/gov.json @@ -2250,11 +2250,11 @@ } }, "hostData": { - "type": "object", - "description": "Collection of acceptable host data values.", + "type": "array", + "items": "Collection of acceptable host data values.", "additionalProperties": { - "format": "byte", - "type": "string" + "type": "string", + "format": "byte" } } }, diff --git a/include/ccf/pal/attestation.h b/include/ccf/pal/attestation.h index 14f15167ed9c..bc440119ca79 100644 --- a/include/ccf/pal/attestation.h +++ b/include/ccf/pal/attestation.h @@ -36,8 +36,9 @@ namespace ccf::pal { auto j = nlohmann::json::parse(quote_info.quote); - measurement = - VirtualAttestationMeasurement(j["measurement"].get()); + const auto s_measurement = j["measurement"].get(); + measurement.data = + std::vector(s_measurement.begin(), s_measurement.end()); report_data = VirtualAttestationReportData( j["report_data"].get>()); } diff --git a/include/ccf/pal/measurement.h b/include/ccf/pal/measurement.h index bea5fac47a9e..253933dbdf35 100644 --- a/include/ccf/pal/measurement.h +++ b/include/ccf/pal/measurement.h @@ -93,16 +93,8 @@ namespace ccf::pal } // Virtual - static constexpr size_t virtual_attestation_measurement_size = 32; - struct VirtualTag - {}; - using VirtualAttestationMeasurement = - AttestationMeasurement; - - inline std::string schema_name(const VirtualAttestationMeasurement*) - { - return "VirtualAttestationMeasurement"; - } + // TODO: Struct _wrapping_ a string, for schema generation? + using VirtualAttestationMeasurement = std::string; // SGX static constexpr size_t sgx_attestation_measurement_size = 32; diff --git a/include/ccf/service/tables/host_data.h b/include/ccf/service/tables/host_data.h index 1fab20a36419..e65b3ca0e7d2 100644 --- a/include/ccf/service/tables/host_data.h +++ b/include/ccf/service/tables/host_data.h @@ -12,7 +12,7 @@ using HostDataMetadata = namespace ccf { using SnpHostDataMap = ServiceMap; - using VirtualHostDataMap = ServiceMap; + using VirtualHostDataMap = ServiceSet; namespace Tables { static constexpr auto HOST_DATA = "public:ccf.gov.nodes.snp.host_data"; diff --git a/samples/constitutions/virtual/virtual_attestation_actions.js b/samples/constitutions/virtual/virtual_attestation_actions.js index 9756a9bcaf0d..5a3eb18eaf1e 100644 --- a/samples/constitutions/virtual/virtual_attestation_actions.js +++ b/samples/constitutions/virtual/virtual_attestation_actions.js @@ -9,13 +9,26 @@ actions.set( const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToJoin"); ccf.kv["public:ccf.gov.nodes.virtual.measurements"].set( measurement, - ALLOWED, + ALLOWED ); // Adding a new allowed measurement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); + } + ) +); + +actions.set( + "remove_virtual_measurement", + new Action( + function (args) { + checkType(args.measurement, "string", "measurement"); }, - ), + function (args) { + const measurement = ccf.strToBuf(args.measurement); + ccf.kv["public:ccf.gov.nodes.virtual.measurements"].delete(measurement); + } + ) ); actions.set( @@ -23,18 +36,17 @@ actions.set( new Action( function (args) { checkType(args.host_data, "string", "host_data"); - checkType(args.metadata, "string", "metadata"); }, function (args, proposalId) { ccf.kv["public:ccf.gov.nodes.virtual.host_data"].set( ccf.strToBuf(args.host_data), - ccf.jsonCompatibleToBuf(args.metadata), + getSingletonKvKey() ); // Adding a new allowed host data changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - }, - ), + } + ) ); actions.set( @@ -46,19 +58,6 @@ actions.set( function (args) { const hostData = ccf.strToBuf(args.host_data); ccf.kv["public:ccf.gov.nodes.virtual.host_data"].delete(hostData); - }, - ), -); - -actions.set( - "remove_virtual_measurement", - new Action( - function (args) { - checkType(args.measurement, "string", "measurement"); - }, - function (args) { - const measurement = ccf.strToBuf(args.measurement); - ccf.kv["public:ccf.gov.nodes.virtual.measurements"].delete(measurement); - }, - ), + } + ) ); diff --git a/src/host/main.cpp b/src/host/main.cpp index 13604336dd35..f20873b41c3f 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -62,9 +62,6 @@ size_t asynchost::UDPImpl::remaining_read_quota = std::chrono::microseconds asynchost::TimeBoundLogger::default_max_time(10'000); -static constexpr char const* default_virtual_security_policy = - "Default CCF virtual security policy"; - void print_version(size_t) { std::cout << "CCF host: " << ccf::ccf_version << std::endl; @@ -533,22 +530,7 @@ int main(int argc, char** argv) if (config.enclave.platform == host::EnclavePlatform::VIRTUAL) { - // Hard-coded here and repeated in the relevant tests. Can be made dynamic - // (eg - from an env var or file) when the tests are able to run SNP nodes - // with distinct policies - char const* policy = std::getenv("CCF_VIRTUAL_SECURITY_POLICY"); - if (policy == nullptr) - { - policy = default_virtual_security_policy; - } - startup_config.attestation.environment.security_policy = policy; - } - - if (config.enclave.platform == host::EnclavePlatform::VIRTUAL) - { - ccf::pal::emit_virtual_measurement( - enclave_file_path, - startup_config.attestation.environment.security_policy.value_or("")); + ccf::pal::emit_virtual_measurement(enclave_file_path); } if (startup_config.attestation.snp_uvm_endorsements_file.has_value()) diff --git a/src/node/gov/handlers/service_state.h b/src/node/gov/handlers/service_state.h index a4191f56ea0c..6b010334e669 100644 --- a/src/node/gov/handlers/service_state.h +++ b/src/node/gov/handlers/service_state.h @@ -102,11 +102,7 @@ namespace ccf::gov::endpoints auto j_details = nlohmann::json::object(); j_details["measurement"] = details["measurement"]; j_details["reportData"] = details["report_data"]; - const auto security_policy = - details["security_policy"].get(); - j_details["securityPolicy"] = security_policy; - j_details["hostData"] = - ccf::crypto::Sha256Hash(security_policy).hex_str(); + j_details["hostData"] = details["host_data"]; quote_info["details"] = j_details; } @@ -529,19 +525,18 @@ namespace ccf::gov::endpoints const ccf::CodeStatus& status) { if (status == ccf::CodeStatus::ALLOWED_TO_JOIN) { - virtual_measurements.push_back(measurement.hex_str()); + virtual_measurements.push_back(measurement); } return true; }); virtual_policy["measurements"] = virtual_measurements; - auto virtual_host_data = nlohmann::json::object(); + auto virtual_host_data = nlohmann::json::array(); auto host_data_handle = ctx.tx.template ro( ccf::Tables::VIRTUAL_HOST_DATA); host_data_handle->foreach( - [&virtual_host_data]( - const HostData& host_data, const HostDataMetadata& metadata) { - virtual_host_data[host_data.hex_str()] = metadata; + [&virtual_host_data](const HostData& host_data) { + virtual_host_data.push_back(host_data.hex_str()); return true; }); virtual_policy["hostData"] = virtual_host_data; diff --git a/src/node/quote.cpp b/src/node/quote.cpp index 38deb0440d81..f821bab473a0 100644 --- a/src/node/quote.cpp +++ b/src/node/quote.cpp @@ -68,7 +68,8 @@ namespace ccf case QuoteFormat::insecure_virtual: { if (!tx.ro(Tables::NODE_VIRTUAL_MEASUREMENTS) - ->has(pal::VirtualAttestationMeasurement(quote_measurement))) + ->has(pal::VirtualAttestationMeasurement( + quote_measurement.data.begin(), quote_measurement.data.end()))) { return QuoteVerificationResult::FailedMeasurementNotFound; } @@ -146,13 +147,11 @@ namespace ccf { auto j = nlohmann::json::parse(quote_info.quote); - // To simulate SNP attestation metadata, associate this "security - // policy" with a host_data value containing its digest - auto it = j.find("security_policy"); + auto it = j.find("host_data"); if (it != j.end()) { - const auto security_policy = it->get(); - return ccf::crypto::Sha256Hash(security_policy); + const auto host_data = it->get(); + return ccf::crypto::Sha256Hash::from_hex_string(host_data); } LOG_FAIL_FMT( @@ -216,7 +215,7 @@ namespace ccf { auto accepted_policies_table = tx.ro(Tables::VIRTUAL_HOST_DATA); - accepted_policy = accepted_policies_table->has(host_data.value()); + accepted_policy = accepted_policies_table->contains(host_data.value()); } else if (quote_info.format == QuoteFormat::amd_sev_snp_v1) { diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 883f923ac8fe..c2498f1f8b98 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -488,8 +488,10 @@ namespace ccf ccf::nonstd::is_specialization::value; constexpr bool is_value = ccf::nonstd::is_specialization::value; + constexpr bool is_set = + ccf::nonstd::is_specialization::value; - if constexpr (!(is_map || is_value)) + if constexpr (!(is_map || is_value || is_set)) { static_assert( ccf::nonstd::dependent_false_v, "Unsupported table type"); @@ -527,6 +529,14 @@ namespace ccf { response_body = handle->get(); } + else if constexpr (is_set) + { + response_body = nlohmann::json::array(); + handle->foreach([&response_body](const auto& k) { + response_body.push_back(k); + return true; + }); + } return ccf::make_success(response_body); }; diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 7fb160c8cb11..1f6fbb7583a8 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1580,7 +1580,7 @@ namespace ccf if (host_data.has_value()) { InternalTablesAccess::trust_node_virtual_host_data( - ctx.tx, host_data.value(), in.snp_security_policy); + ctx.tx, host_data.value()); } else { @@ -1593,7 +1593,7 @@ namespace ccf { auto host_data = AttestationProvider::get_host_data(in.quote_info).value(); - InternalTablesAccess::trust_node_host_data( + InternalTablesAccess::trust_node_snp_host_data( ctx.tx, host_data, in.snp_security_policy); InternalTablesAccess::trust_node_uvm_endorsements( diff --git a/src/pal/quote_generation.h b/src/pal/quote_generation.h index 0264f731d6c0..b205704a8369 100644 --- a/src/pal/quote_generation.h +++ b/src/pal/quote_generation.h @@ -14,16 +14,15 @@ namespace ccf::pal return fmt::format("ccf_virtual_attestation.{}.{}", ::getpid(), suffix); }; - static void emit_virtual_measurement( - const std::string& package_path, const std::string& security_policy) + static void emit_virtual_measurement(const std::string& package_path) { auto package = files::slurp(package_path); auto package_hash = ccf::crypto::Sha256Hash(package); auto j = nlohmann::json::object(); - j["measurement"] = package_hash.hex_str(); - j["security_policy"] = security_policy; + j["measurement"] = "TODO: Call uname"; + j["host_data"] = package_hash.hex_str(); files::dump(j.dump(2), virtual_attestation_path("measurement")); } diff --git a/src/service/internal_tables_access.h b/src/service/internal_tables_access.h index cbab0132c19e..a4733f18dddd 100644 --- a/src/service/internal_tables_access.h +++ b/src/service/internal_tables_access.h @@ -592,7 +592,8 @@ namespace ccf { tx.wo(Tables::NODE_VIRTUAL_MEASUREMENTS) ->put( - pal::VirtualAttestationMeasurement(node_measurement), + pal::VirtualAttestationMeasurement( + node_measurement.data.begin(), node_measurement.data.end()), CodeStatus::ALLOWED_TO_JOIN); break; } @@ -621,16 +622,14 @@ namespace ccf } static void trust_node_virtual_host_data( - ccf::kv::Tx& tx, - const HostData& host_data, - const std::optional& metadata) + ccf::kv::Tx& tx, const HostData& host_data) { auto host_data_table = tx.wo(Tables::VIRTUAL_HOST_DATA); - host_data_table->put(host_data, metadata.value_or("")); + host_data_table->insert(host_data); } - static void trust_node_host_data( + static void trust_node_snp_host_data( ccf::kv::Tx& tx, const HostData& host_data, const std::optional& security_policy = std::nullopt) diff --git a/tests/code_update.py b/tests/code_update.py index 96ea484767b7..49b2f1dd3a03 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -44,9 +44,13 @@ def test_verify_quotes(network, args): j = r.body.json() if j["format"] == "Insecure_Virtual": + # TODO: Update comments here # A virtual attestation makes 2 claims: # - The measurement (equal to any equivalent node) is the sha256 of the package (library) it loaded claimed_measurement = j["measurement"] + if args.enclave_platform == "virtual": + # For consistency with other platforms, this endpoint always returns a hex-string. But for virtual, it's encoding some ASCII string, not a digest, so decode it for readability + claimed_measurement = bytes.fromhex(claimed_measurement).decode() expected_measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, args.package ) @@ -205,9 +209,15 @@ def get_trusted_host_data(node): original_host_data = get_trusted_host_data(primary) host_data, security_policy = infra.utils.get_host_data_and_security_policy( - args.enclave_platform + args.enclave_type, args.enclave_platform, args.package ) - expected = {host_data: security_policy} + + if args.enclave_platform == "snp": + expected = {host_data: security_policy} + elif args.enclave_platform == "virtual": + expected = [host_data] + else: + raise ValueError(f"Unsupported platform: {args.enclave_platform}") assert original_host_data == expected, f"{original_host_data} != {expected}" @@ -219,10 +229,17 @@ def get_trusted_host_data(node): primary, args.enclave_platform, dummy_host_data_key, dummy_host_data_value ) host_data = get_trusted_host_data(primary) - expected_host_data = { - **original_host_data, - dummy_host_data_key: dummy_host_data_value, - } + if args.enclave_platform == "snp": + expected_host_data = { + **original_host_data, + dummy_host_data_key: dummy_host_data_value, + } + elif args.enclave_platform == "virtual": + host_data = sorted(host_data) + expected_host_data = sorted([*original_host_data, dummy_host_data_key]) + else: + raise ValueError(f"Unsupported platform: {args.enclave_platform}") + assert host_data == expected_host_data, f"{host_data} != {expected_host_data}" LOG.debug("Remove dummy host data") @@ -264,7 +281,7 @@ def test_add_node_with_stubbed_security_policy(network, args): primary, _ = network.find_nodes() host_data, security_policy = infra.utils.get_host_data_and_security_policy( - args.enclave_platform + args.enclave_type, args.enclave_platform, args.package ) network.consortium.remove_host_data(primary, args.enclave_platform, host_data) @@ -290,6 +307,10 @@ def test_add_node_with_stubbed_security_policy(network, args): @reqs.description("Start node with mismatching security policy") def test_add_node_with_bad_security_policy(network, args): + if args.enclave_platform == "virtual": + LOG.warning(f"TODO: Skipping this test for now") + return network + try: with tempfile.TemporaryDirectory() as snp_dir: security_context_dir = None @@ -311,7 +332,6 @@ def test_add_node_with_bad_security_policy(network, args): args, timeout=3, snp_uvm_security_context_dir=snp_dir if security_context_dir else None, - env={"CCF_VIRTUAL_SECURITY_POLICY": "invalid_security_policy"}, ) except (TimeoutError, RuntimeError): LOG.info("As expected, node with invalid security policy failed to startup") @@ -326,7 +346,7 @@ def test_add_node_with_bad_host_data(network, args): primary, _ = network.find_nodes() host_data, security_policy = infra.utils.get_host_data_and_security_policy( - args.enclave_platform + args.enclave_type, args.enclave_platform, args.package ) LOG.info( @@ -408,6 +428,10 @@ def test_add_node_with_bad_measurement(network, args): ) return network + if args.enclave_platform == "virtual": + LOG.warning(f"TODO: Restore this on virtual") + return network + replacement_package = get_replacement_package(args) LOG.info(f"Adding unsupported node running {replacement_package}") @@ -420,6 +444,7 @@ def test_add_node_with_bad_measurement(network, args): args, timeout=3, ) + except infra.network.MeasurementNotFound as err: measurement_not_found_exception = err @@ -440,47 +465,117 @@ def get_replacement_package(args): @reqs.description("Update all nodes code") @reqs.not_snp( - "Not yet supported as all nodes run the same measurement/security policy in SNP CI" + "Not yet supported as all nodes run the same measurement AND security policy in SNP CI" ) def test_update_all_nodes(network, args): replacement_package = get_replacement_package(args) primary, _ = network.find_nodes() - first_measurement = infra.utils.get_measurement( + initial_measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, args.package ) + initial_host_data, initial_security_policy = ( + infra.utils.get_host_data_and_security_policy( + args.enclave_type, args.enclave_platform, args.package + ) + ) new_measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, replacement_package ) + new_host_data, new_security_policy = infra.utils.get_host_data_and_security_policy( + args.enclave_type, args.enclave_platform, replacement_package + ) + + measurement_changed = initial_measurement != new_measurement + host_data_changed = initial_host_data != new_host_data + assert ( + measurement_changed or host_data_changed + ), f"Cannot test code update, as new package produced identical measurement and host_data as original" - LOG.info("Add new measurement") + LOG.info("Add new measurement and host_data") network.consortium.add_measurement(primary, args.enclave_platform, new_measurement) + network.consortium.add_host_data( + primary, args.enclave_platform, new_host_data, new_security_policy + ) with primary.api_versioned_client(api_version=args.gov_api_version) as uc: - LOG.info("Check reported trusted measurements") r = uc.get("/gov/service/join-policy") assert r.status_code == http.HTTPStatus.OK, r - versions: list = r.body.json()[args.enclave_platform]["measurements"] + platform_policy = r.body.json()[args.enclave_platform] - expected = [first_measurement, new_measurement] + if measurement_changed: + LOG.info("Check reported trusted measurements") + actual_measurements: list = platform_policy["measurements"] - versions.sort() - expected.sort() - assert versions == expected, f"{versions} != {expected}" + expected_measurements = [initial_measurement, new_measurement] - LOG.info("Remove old measurement") - network.consortium.remove_measurement( - primary, args.enclave_platform, first_measurement - ) - r = uc.get("/gov/service/join-policy") - assert r.status_code == http.HTTPStatus.OK, r - versions = r.body.json()[args.enclave_platform]["measurements"] + actual_measurements.sort() + expected_measurements.sort() + assert ( + actual_measurements == expected_measurements + ), f"{actual_measurements} != {expected_measurements}" - expected.remove(first_measurement) + LOG.info("Remove old measurement") + network.consortium.remove_measurement( + primary, args.enclave_platform, initial_measurement + ) - versions.sort() - assert versions == expected, f"{versions} != {expected}" + r = uc.get("/gov/service/join-policy") + assert r.status_code == http.HTTPStatus.OK, r + actual_measurements = r.body.json()[args.enclave_platform]["measurements"] + + expected_measurements.remove(initial_measurement) + + actual_measurements.sort() + expected_measurements.sort() + assert ( + actual_measurements == expected_measurements + ), f"{actual_measurements} != {expected_measurements}" + + if initial_host_data != new_host_data: + + def format_expected_host_data(entries): + if args.enclave_platform == "snp": + return { + host_data: security_policy + for host_data, security_policy in entries + } + elif args.enclave_platform == "virtual": + return set(host_data for host_data, _ in entries) + else: + raise ValueError(f"Unsupported platform: {args.enclave_platform}") + + LOG.info("Check reported trusted host datas") + actual_host_datas = platform_policy["hostData"] + if args.enclave_platform == "virtual": + actual_host_datas = set(actual_host_datas) + expected_host_datas = format_expected_host_data( + [ + (initial_host_data, initial_security_policy), + (new_host_data, new_security_policy), + ] + ) + assert ( + actual_host_datas == expected_host_datas + ), f"{actual_host_datas} != {expected_host_datas}" + + LOG.info("Remove old host_data") + network.consortium.remove_host_data( + primary, args.enclave_platform, initial_host_data + ) + + r = uc.get("/gov/service/join-policy") + assert r.status_code == http.HTTPStatus.OK, r + actual_host_datas = r.body.json()[args.enclave_platform]["hostData"] + if args.enclave_platform == "virtual": + actual_host_datas = set(actual_host_datas) + expected_host_datas = format_expected_host_data( + [(new_host_data, new_security_policy)] + ) + assert ( + actual_host_datas == expected_host_datas + ), f"{actual_host_datas} != {expected_host_datas}" old_nodes = network.nodes.copy() @@ -586,31 +681,31 @@ def run(args): test_verify_quotes(network, args) # Measurements - test_measurements_tables(network, args) - test_add_node_with_bad_measurement(network, args) - - # Host data/security policy - test_host_data_tables(network, args) - test_add_node_with_bad_host_data(network, args) - test_add_node_with_stubbed_security_policy(network, args) - test_add_node_with_bad_security_policy(network, args) - - if snp.IS_SNP: - # Not tested on virtual, as this relies on the fact that the local security - # policy can be ignored on SNP. It has been applied already, and its digest - # is available as a host_data claim, so the raw policy is merely an audit - # nicety and nodes will launch without it. This is not true on virtual, where - # an actual "security policy" value is always digested at launch time to - # produce a host_data value. - test_add_node_without_security_policy(network, args) - - # Endorsements - if snp.IS_SNP: - test_endorsements_tables(network, args) - test_add_node_with_no_uvm_endorsements(network, args) - - # NB: Assumes the current nodes are still using args.package, so must run before test_update_all_nodes - test_proposal_invalidation(network, args) + # test_measurements_tables(network, args) + # test_add_node_with_bad_measurement(network, args) + + # # Host data/security policy + # test_host_data_tables(network, args) + # test_add_node_with_bad_host_data(network, args) + # test_add_node_with_stubbed_security_policy(network, args) + # test_add_node_with_bad_security_policy(network, args) + + # if snp.IS_SNP: + # # Not tested on virtual, as this relies on the fact that the local security + # # policy can be ignored on SNP. It has been applied already, and its digest + # # is available as a host_data claim, so the raw policy is merely an audit + # # nicety and nodes will launch without it. This is not true on virtual, where + # # an actual "security policy" value is always digested at launch time to + # # produce a host_data value. + # test_add_node_without_security_policy(network, args) + + # # Endorsements + # if snp.IS_SNP: + # test_endorsements_tables(network, args) + # test_add_node_with_no_uvm_endorsements(network, args) + + # # NB: Assumes the current nodes are still using args.package, so must run before test_update_all_nodes + # test_proposal_invalidation(network, args) if not snp.IS_SNP: test_update_all_nodes(network, args) diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 0df43a1926d8..14fafffea668 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -808,11 +808,9 @@ def remove_snp_uvm_endorsement(self, remote_node, did, feed): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) - def add_host_data(self, remote_node, platform, host_data_key, host_data_value): + def add_host_data(self, remote_node, platform, host_data_key, host_data_value=""): if platform == "virtual": - return self.add_virtual_host_data( - remote_node, host_data_key, host_data_value - ) + return self.add_virtual_host_data(remote_node, host_data_key) elif platform == "snp": return self.add_snp_host_data(remote_node, host_data_key, host_data_value) else: @@ -822,12 +820,10 @@ def add_virtual_host_data( self, remote_node, host_data_key, - metadata, ): proposal_body, careful_vote = self.make_proposal( "add_virtual_host_data", host_data=host_data_key, - metadata=metadata, ) proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) diff --git a/tests/infra/utils.py b/tests/infra/utils.py index 99d759fd73fc..debe71ac1e30 100644 --- a/tests/infra/utils.py +++ b/tests/infra/utils.py @@ -11,20 +11,24 @@ def get_measurement(enclave_type, enclave_platform, package, library_dir="."): ) if enclave_platform == "virtual": - hash = sha256(open(lib_path, "rb").read()) - return hash.hexdigest() + return "TODO: Call uname" else: raise ValueError(f"Cannot get measurement on {enclave_platform}") -def get_host_data_and_security_policy(enclave_platform): - DEFAULT_VIRTUAL_SECURITY_POLICY = "Default CCF virtual security policy" +def get_host_data_and_security_policy( + enclave_type, enclave_platform, package, library_dir="." +): + lib_path = infra.path.build_lib_path( + package, enclave_type, enclave_platform, library_dir + ) if enclave_platform == "snp": security_policy = snp.get_container_group_security_policy() + host_data = sha256(security_policy.encode()).hexdigest() + return host_data, security_policy elif enclave_platform == "virtual": - security_policy = DEFAULT_VIRTUAL_SECURITY_POLICY + hash = sha256(open(lib_path, "rb").read()) + return hash.hexdigest(), None else: raise ValueError(f"Cannot get security policy on {enclave_platform}") - host_data = sha256(security_policy.encode()).hexdigest() - return host_data, security_policy diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index ffe70e168e69..d012967d04ce 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -268,7 +268,12 @@ def run_code_upgrade_from( new_host_data = None try: new_host_data, new_security_policy = ( - infra.utils.get_host_data_and_security_policy(args.enclave_platform) + infra.utils.get_host_data_and_security_policy( + args.enclave_type, + args.enclave_platform, + args.package, + library_dir=to_library_dir, + ) ) network.consortium.add_host_data( primary, args.enclave_platform, new_host_data, new_security_policy @@ -355,7 +360,12 @@ def run_code_upgrade_from( # If host_data was found for original nodes, check if it's different on new nodes, in which case old should be removed if new_host_data is not None: old_host_data, old_security_policy = ( - infra.utils.get_host_data_and_security_policy(args.enclave_platform) + infra.utils.get_host_data_and_security_policy( + args.enclave_type, + args.enclave_platform, + args.package, + library_dir=from_library_dir, + ) ) if old_host_data != new_host_data: From 4c04a2c3f0f49321f6ed2bbd0cab9b0675535af5 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 24 Jan 2025 10:37:58 +0000 Subject: [PATCH 30/36] Call `uname -a` for virtual measurement --- src/ds/system.h | 46 ++++++++++++++++++++++++++++++++++++++ src/pal/quote_generation.h | 10 ++++++++- tests/infra/utils.py | 4 +++- 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/ds/system.h diff --git a/src/ds/system.h b/src/ds/system.h new file mode 100644 index 000000000000..e5afd28485c8 --- /dev/null +++ b/src/ds/system.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "ccf/ds/logger.h" +#include "ccf/ds/nonstd.h" + +#include +#include +#include +#include +#include + +namespace ccf::ds::system +{ + std::optional exec(const std::string& cmd) + { + std::array buffer; + std::string result; + + LOG_TRACE_FMT("Opening pipe to execute command: {}", cmd); + FILE* pipe = popen(cmd.c_str(), "r"); + if (!pipe) + { + LOG_FAIL_FMT("Error opening pipe: {}", strerror(errno)); + return std::nullopt; + } + + while (fgets(buffer.data(), buffer.size(), pipe) != NULL) + { + result += buffer.data(); + } + + auto return_code = pclose(pipe); + if (return_code != 0) + { + LOG_TRACE_FMT("Command returned error: {}", return_code); + } + + result = ccf::nonstd::trim(result); + + LOG_TRACE_FMT("Result is: {}", result); + + return result; + } +} diff --git a/src/pal/quote_generation.h b/src/pal/quote_generation.h index b205704a8369..db55b4bdea08 100644 --- a/src/pal/quote_generation.h +++ b/src/pal/quote_generation.h @@ -3,6 +3,7 @@ #pragma once #include "ds/files.h" +#include "ds/system.h" #include #include @@ -21,7 +22,14 @@ namespace ccf::pal auto package_hash = ccf::crypto::Sha256Hash(package); auto j = nlohmann::json::object(); - j["measurement"] = "TODO: Call uname"; + + const auto uname = ccf::ds::system::exec("uname -a"); + if (!uname.has_value()) + { + throw std::runtime_error("Error calling uname"); + } + + j["measurement"] = uname.value(); j["host_data"] = package_hash.hex_str(); files::dump(j.dump(2), virtual_attestation_path("measurement")); diff --git a/tests/infra/utils.py b/tests/infra/utils.py index debe71ac1e30..ffa8a20fe29a 100644 --- a/tests/infra/utils.py +++ b/tests/infra/utils.py @@ -3,6 +3,7 @@ import infra.path from hashlib import sha256 import infra.snp as snp +import infra.proc def get_measurement(enclave_type, enclave_platform, package, library_dir="."): @@ -11,7 +12,8 @@ def get_measurement(enclave_type, enclave_platform, package, library_dir="."): ) if enclave_platform == "virtual": - return "TODO: Call uname" + result = infra.proc.ccall("uname", "-a") + return result.stdout.decode().strip() else: raise ValueError(f"Cannot get measurement on {enclave_platform}") From cf4b4d547f1fca4f6adb9c73894c37e9ad956e8e Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 24 Jan 2025 10:56:43 +0000 Subject: [PATCH 31/36] Test juggling, explicitly test omitted measurement and host_data, independent of package-update --- tests/code_update.py | 116 ++++++++++++++++++++++++----------------- tests/infra/network.py | 6 +++ tests/infra/utils.py | 4 -- 3 files changed, 75 insertions(+), 51 deletions(-) diff --git a/tests/code_update.py b/tests/code_update.py index 49b2f1dd3a03..2d299c4640f5 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -307,10 +307,6 @@ def test_add_node_with_stubbed_security_policy(network, args): @reqs.description("Start node with mismatching security policy") def test_add_node_with_bad_security_policy(network, args): - if args.enclave_platform == "virtual": - LOG.warning(f"TODO: Skipping this test for now") - return network - try: with tempfile.TemporaryDirectory() as snp_dir: security_context_dir = None @@ -341,24 +337,49 @@ def test_add_node_with_bad_security_policy(network, args): return network -@reqs.description("Node with bad host data fails to join") -def test_add_node_with_bad_host_data(network, args): +@reqs.description("Node with untrusted measurement fails to join") +def test_add_node_with_untrusted_measurement(network, args): primary, _ = network.find_nodes() - host_data, security_policy = infra.utils.get_host_data_and_security_policy( + measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, args.package ) - LOG.info( - "Removing trusted security policy so that a new joiner is seen as an unmatching policy" + LOG.info("Removing this measurement so that a new joiner is refused") + network.consortium.remove_measurement(primary, args.enclave_platform, measurement) + + new_node = network.create_node("local://localhost") + try: + network.join_node(new_node, args.package, args, timeout=3) + except infra.network.MeasurementNotFound: + LOG.info("As expected, node with untrusted measurement failed to join") + else: + raise AssertionError("Node join unexpectedly succeeded") + + network.consortium.add_measurement( + primary, + args.enclave_platform, + measurement, ) + return network + + +@reqs.description("Node with untrusted host data fails to join") +def test_add_node_with_untrusted_host_data(network, args): + primary, _ = network.find_nodes() + + host_data, security_policy = infra.utils.get_host_data_and_security_policy( + args.enclave_type, args.enclave_platform, args.package + ) + + LOG.info("Removing this host data value so that a new joiner is refused") network.consortium.remove_host_data(primary, args.enclave_platform, host_data) new_node = network.create_node("local://localhost") try: network.join_node(new_node, args.package, args, timeout=3) - except TimeoutError: - LOG.info("As expected, node with untrusted security policy failed to join") + except infra.network.HostDataNotFound: + LOG.info("As expected, node with untrusted host data failed to join") else: raise AssertionError("Node join unexpectedly succeeded") @@ -420,22 +441,18 @@ def test_add_node_with_no_uvm_endorsements(network, args): return network -@reqs.description("Node with bad measurement fails to join") -def test_add_node_with_bad_measurement(network, args): +@reqs.description("Node running other package (binary) fails to join") +def test_add_node_with_different_package(network, args): if args.enclave_platform == "snp": LOG.warning( - "Skipping test_add_node_with_bad_measurement with SNP - cannot affect measurement on SNP" + "Skipping test_add_node_with_different_package with SNP - policy does not currently restrict packages" ) return network - if args.enclave_platform == "virtual": - LOG.warning(f"TODO: Restore this on virtual") - return network - replacement_package = get_replacement_package(args) LOG.info(f"Adding unsupported node running {replacement_package}") - measurement_not_found_exception = None + exception_thrown = None try: new_node = network.create_node("local://localhost") network.join_node( @@ -445,12 +462,18 @@ def test_add_node_with_bad_measurement(network, args): timeout=3, ) - except infra.network.MeasurementNotFound as err: - measurement_not_found_exception = err + except (infra.network.MeasurementNotFound, infra.network.HostDataNotFound) as err: + exception_thrown = err assert ( - measurement_not_found_exception is not None + exception_thrown is not None ), f"Adding a node with {replacement_package} should fail" + if args.enclave_platform == "virtual": + assert isinstance( + exception_thrown, infra.network.HostDataNotFound + ), f"Virtual node package should affect host data" + else: + raise ValueError(f"Unchecked platform") return network @@ -681,31 +704,30 @@ def run(args): test_verify_quotes(network, args) # Measurements - # test_measurements_tables(network, args) - # test_add_node_with_bad_measurement(network, args) - - # # Host data/security policy - # test_host_data_tables(network, args) - # test_add_node_with_bad_host_data(network, args) - # test_add_node_with_stubbed_security_policy(network, args) - # test_add_node_with_bad_security_policy(network, args) - - # if snp.IS_SNP: - # # Not tested on virtual, as this relies on the fact that the local security - # # policy can be ignored on SNP. It has been applied already, and its digest - # # is available as a host_data claim, so the raw policy is merely an audit - # # nicety and nodes will launch without it. This is not true on virtual, where - # # an actual "security policy" value is always digested at launch time to - # # produce a host_data value. - # test_add_node_without_security_policy(network, args) - - # # Endorsements - # if snp.IS_SNP: - # test_endorsements_tables(network, args) - # test_add_node_with_no_uvm_endorsements(network, args) - - # # NB: Assumes the current nodes are still using args.package, so must run before test_update_all_nodes - # test_proposal_invalidation(network, args) + test_measurements_tables(network, args) + test_add_node_with_untrusted_measurement(network, args) + + # Host data/security policy + test_host_data_tables(network, args) + test_add_node_with_untrusted_host_data(network, args) + + if snp.IS_SNP: + # Virtual has no security policy, _only_ host data (unassociated with anything) + test_add_node_with_stubbed_security_policy(network, args) + test_add_node_with_bad_security_policy(network, args) + test_add_node_without_security_policy(network, args) + + # Endorsements + test_endorsements_tables(network, args) + test_add_node_with_no_uvm_endorsements(network, args) + + # This is in practice equivalent to either "bad measurement" or "bad host data", but is explicitly + # testing that (without artifically removing/corrupting those values) a replacement package differs + # in one of these values + test_add_node_with_different_package(network, args) + + # NB: Assumes the current nodes are still using args.package, so must run before test_update_all_nodes + test_proposal_invalidation(network, args) if not snp.IS_SNP: test_update_all_nodes(network, args) diff --git a/tests/infra/network.py b/tests/infra/network.py index cb3ba6669ec0..24e98bf3ed61 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -71,6 +71,10 @@ class MeasurementNotFound(Exception): pass +class HostDataNotFound(Exception): + pass + + class UVMEndorsementsNotAuthorised(Exception): pass @@ -927,6 +931,8 @@ def run_join_node( for error in errors: if "Quote does not contain known enclave measurement" in error: raise MeasurementNotFound from e + if "Quote host data is not authorised" in error: + raise HostDataNotFound from e if "UVM endorsements are not authorised" in error: raise UVMEndorsementsNotAuthorised from e if "StartupSeqnoIsOld" in error: diff --git a/tests/infra/utils.py b/tests/infra/utils.py index ffa8a20fe29a..041dc574658a 100644 --- a/tests/infra/utils.py +++ b/tests/infra/utils.py @@ -7,10 +7,6 @@ def get_measurement(enclave_type, enclave_platform, package, library_dir="."): - lib_path = infra.path.build_lib_path( - package, enclave_type, enclave_platform, library_dir - ) - if enclave_platform == "virtual": result = infra.proc.ccall("uname", "-a") return result.stdout.decode().strip() From 43e995eb801f1fb7f4d129d1b4de90f088cbcaef Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 24 Jan 2025 11:24:11 +0000 Subject: [PATCH 32/36] More fiddling --- tests/code_update.py | 74 +++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/tests/code_update.py b/tests/code_update.py index 2d299c4640f5..af244f29d1c3 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -30,7 +30,11 @@ def test_verify_quotes(network, args): with primary.api_versioned_client(api_version=args.gov_api_version) as uc: r = uc.get("/gov/service/join-policy") assert r.status_code == http.HTTPStatus.OK, r - trusted_measurements = r.body.json()[args.enclave_platform]["measurements"] + + policy = r.body.json() + + trusted_virtual_measurements = policy["virtual"]["measurements"] + trusted_virtual_host_data = policy["virtual"]["hostData"] r = uc.get("/node/quotes") all_quotes = r.body.json()["quotes"] @@ -44,13 +48,12 @@ def test_verify_quotes(network, args): j = r.body.json() if j["format"] == "Insecure_Virtual": - # TODO: Update comments here - # A virtual attestation makes 2 claims: - # - The measurement (equal to any equivalent node) is the sha256 of the package (library) it loaded + # A virtual attestation makes 3 claims: + # - The measurement (same on many nodes) is the result of calling `uname -a` claimed_measurement = j["measurement"] - if args.enclave_platform == "virtual": - # For consistency with other platforms, this endpoint always returns a hex-string. But for virtual, it's encoding some ASCII string, not a digest, so decode it for readability - claimed_measurement = bytes.fromhex(claimed_measurement).decode() + # For consistency with other platforms, this endpoint always returns a hex-string. + # But for virtual, it's encoding some ASCII string, not a digest, so decode it for readability + claimed_measurement = bytes.fromhex(claimed_measurement).decode() expected_measurement = infra.utils.get_measurement( args.enclave_type, args.enclave_platform, args.package ) @@ -61,17 +64,29 @@ def test_verify_quotes(network, args): raw = json.loads(b64decode(j["raw"])) assert raw["measurement"] == claimed_measurement + # - The host_data (equal to any equivalent node) is the sha256 of the package (library) it loaded + host_data = raw["host_data"] + expected_host_data, _ = infra.utils.get_host_data_and_security_policy( + args.enclave_type, args.enclave_platform, args.package + ) + assert ( + host_data == expected_host_data + ), f"{host_data} != {expected_host_data}" + # - The report_data (unique to this node) is the sha256 of the node's public key, in DER encoding # That is the same value we use as the node's ID, though that is usually represented as a hex string report_data = b64decode(raw["report_data"]) assert report_data.hex() == node.node_id - # Additionally, we check that the measurement is one of the service's currently trusted measurements. - # Note this might not always be true - a node may be added while its measurement is trusted, and persist past the point that its measurement is retired! + # Additionally, we check that the measurement and host_data are in the service's currently trusted values. + # Note this might not always be true - a node may be added while it was trusted, and persist past the point its values become untrusted! # But it _is_ true in this test, and a sensible thing to check most of the time assert ( - claimed_measurement in trusted_measurements - ), f"This node's measurement ({claimed_measurement}) is not one of the currently trusted measurements ({trusted_measurements})" + claimed_measurement in trusted_virtual_measurements + ), f"This node's measurement ({claimed_measurement}) is not one of the currently trusted measurements ({trusted_virtual_measurements})" + assert ( + host_data in trusted_virtual_host_data + ), f"This node's host data ({host_data}) is not one of the currently trusted values ({trusted_virtual_host_data})" elif j["format"] == "AMD_SEV_SNP_v1": LOG.warning( @@ -306,20 +321,19 @@ def test_add_node_with_stubbed_security_policy(network, args): @reqs.description("Start node with mismatching security policy") -def test_add_node_with_bad_security_policy(network, args): +@reqs.snp_only() +def test_start_node_with_mismatched_host_data(network, args): try: + security_context_dir = snp.get_security_context_dir() with tempfile.TemporaryDirectory() as snp_dir: - security_context_dir = None - if snp.IS_SNP: - security_context_dir = snp.get_security_context_dir() - if security_context_dir is not None: - shutil.copytree(security_context_dir, snp_dir, dirs_exist_ok=True) - with open( - os.path.join(snp_dir, snp.ACI_SEV_SNP_FILENAME_SECURITY_POLICY), - "w", - encoding="utf-8", - ) as f: - f.write(b64encode(b"invalid_security_policy").decode()) + if security_context_dir is not None: + shutil.copytree(security_context_dir, snp_dir, dirs_exist_ok=True) + with open( + os.path.join(snp_dir, snp.ACI_SEV_SNP_FILENAME_SECURITY_POLICY), + "w", + encoding="utf-8", + ) as f: + f.write(b64encode(b"invalid_security_policy").decode()) new_node = network.create_node("local://localhost") network.join_node( @@ -442,6 +456,9 @@ def test_add_node_with_no_uvm_endorsements(network, args): @reqs.description("Node running other package (binary) fails to join") +@reqs.not_snp( + "Not yet supported as all nodes run the same measurement AND security policy in SNP CI" +) def test_add_node_with_different_package(network, args): if args.enclave_platform == "snp": LOG.warning( @@ -714,22 +731,21 @@ def run(args): if snp.IS_SNP: # Virtual has no security policy, _only_ host data (unassociated with anything) test_add_node_with_stubbed_security_policy(network, args) - test_add_node_with_bad_security_policy(network, args) + test_start_node_with_mismatched_host_data(network, args) test_add_node_without_security_policy(network, args) # Endorsements test_endorsements_tables(network, args) test_add_node_with_no_uvm_endorsements(network, args) - # This is in practice equivalent to either "bad measurement" or "bad host data", but is explicitly - # testing that (without artifically removing/corrupting those values) a replacement package differs - # in one of these values - test_add_node_with_different_package(network, args) - # NB: Assumes the current nodes are still using args.package, so must run before test_update_all_nodes test_proposal_invalidation(network, args) if not snp.IS_SNP: + # This is in practice equivalent to either "bad measurement" or "bad host data", but is explicitly + # testing that (without artifically removing/corrupting those values) a replacement package differs + # in one of these values + test_add_node_with_different_package(network, args) test_update_all_nodes(network, args) # Run again at the end to confirm current nodes are acceptable From 9c196626b003e5be7ef9f5a1c37478c0f14824fd Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 24 Jan 2025 11:41:23 +0000 Subject: [PATCH 33/36] Schema --- doc/schemas/gov/2024-07-01/gov.json | 4 +-- doc/schemas/gov_openapi.json | 38 ++++++----------------------- tests/schema.py | 2 +- 3 files changed, 10 insertions(+), 34 deletions(-) diff --git a/doc/schemas/gov/2024-07-01/gov.json b/doc/schemas/gov/2024-07-01/gov.json index e7979b6639d7..c365359ca19d 100644 --- a/doc/schemas/gov/2024-07-01/gov.json +++ b/doc/schemas/gov/2024-07-01/gov.json @@ -2251,8 +2251,8 @@ }, "hostData": { "type": "array", - "items": "Collection of acceptable host data values.", - "additionalProperties": { + "description": "Collection of acceptable host data values.", + "items": { "type": "string", "format": "byte" } diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json index 09fc01ad4612..857b3f5ec25f 100644 --- a/doc/schemas/gov_openapi.json +++ b/doc/schemas/gov_openapi.json @@ -1223,29 +1223,6 @@ }, "type": "object" }, - "VirtualAttestationMeasurement": { - "format": "hex", - "pattern": "^[a-f0-9]64$", - "type": "string" - }, - "VirtualAttestationMeasurement_to_CodeStatus": { - "items": { - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/VirtualAttestationMeasurement" - }, - { - "$ref": "#/components/schemas/CodeStatus" - } - ] - }, - "maxItems": 2, - "minItems": 2, - "type": "array" - }, - "type": "array" - }, "base64string": { "format": "base64", "type": "string" @@ -1263,6 +1240,12 @@ "string": { "type": "string" }, + "string_to_CodeStatus": { + "additionalProperties": { + "$ref": "#/components/schemas/CodeStatus" + }, + "type": "object" + }, "string_to_JwtIssuerMetadata": { "additionalProperties": { "$ref": "#/components/schemas/JwtIssuerMetadata" @@ -2182,13 +2165,6 @@ "operationId": "GetGovKvNodesVirtualHostData", "responses": { "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Sha256Digest_to_string" - } - } - }, "description": "Default response description" }, "default": { @@ -2210,7 +2186,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VirtualAttestationMeasurement_to_CodeStatus" + "$ref": "#/components/schemas/string_to_CodeStatus" } } }, diff --git a/tests/schema.py b/tests/schema.py index 0627e7353f6a..92e10a2accbd 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -50,6 +50,7 @@ def fetch_schema(api_response, target_file_path): pass with open(openapi_target_file, "a+", encoding="utf-8") as f: + prefix, ext = os.path.splitext(openapi_target_file) f.seek(0) previous = f.read().strip() if previous != formatted_schema: @@ -81,7 +82,6 @@ def fetch_schema(api_response, target_file_path): LOG.error( f"Found differences in {openapi_target_file}, but not overwriting as retrieved version is not newer ({fetched_version} <= {file_version})" ) - prefix, ext = os.path.splitext(openapi_target_file) alt_file = f"{prefix}_{fetched_version}{ext}" LOG.error(f"Writing to {alt_file} for comparison") with open(alt_file, "w", encoding="utf-8") as f2: From 750308b72db2ff0033f019064d41a4d968b50db5 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 24 Jan 2025 13:01:23 +0000 Subject: [PATCH 34/36] Formatting --- include/ccf/pal/measurement.h | 1 - .../virtual/virtual_attestation_actions.js | 20 +++++++++---------- tests/code_update.py | 6 +++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/include/ccf/pal/measurement.h b/include/ccf/pal/measurement.h index 253933dbdf35..56c0ac22f11f 100644 --- a/include/ccf/pal/measurement.h +++ b/include/ccf/pal/measurement.h @@ -93,7 +93,6 @@ namespace ccf::pal } // Virtual - // TODO: Struct _wrapping_ a string, for schema generation? using VirtualAttestationMeasurement = std::string; // SGX diff --git a/samples/constitutions/virtual/virtual_attestation_actions.js b/samples/constitutions/virtual/virtual_attestation_actions.js index 5a3eb18eaf1e..c545b8784cbd 100644 --- a/samples/constitutions/virtual/virtual_attestation_actions.js +++ b/samples/constitutions/virtual/virtual_attestation_actions.js @@ -9,13 +9,13 @@ actions.set( const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToJoin"); ccf.kv["public:ccf.gov.nodes.virtual.measurements"].set( measurement, - ALLOWED + ALLOWED, ); // Adding a new allowed measurement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } - ) + }, + ), ); actions.set( @@ -27,8 +27,8 @@ actions.set( function (args) { const measurement = ccf.strToBuf(args.measurement); ccf.kv["public:ccf.gov.nodes.virtual.measurements"].delete(measurement); - } - ) + }, + ), ); actions.set( @@ -40,13 +40,13 @@ actions.set( function (args, proposalId) { ccf.kv["public:ccf.gov.nodes.virtual.host_data"].set( ccf.strToBuf(args.host_data), - getSingletonKvKey() + getSingletonKvKey(), ); // Adding a new allowed host data changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification invalidateOtherOpenProposals(proposalId); - } - ) + }, + ), ); actions.set( @@ -58,6 +58,6 @@ actions.set( function (args) { const hostData = ccf.strToBuf(args.host_data); ccf.kv["public:ccf.gov.nodes.virtual.host_data"].delete(hostData); - } - ) + }, + ), ); diff --git a/tests/code_update.py b/tests/code_update.py index af244f29d1c3..007273775613 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -488,9 +488,9 @@ def test_add_node_with_different_package(network, args): if args.enclave_platform == "virtual": assert isinstance( exception_thrown, infra.network.HostDataNotFound - ), f"Virtual node package should affect host data" + ), "Virtual node package should affect host data" else: - raise ValueError(f"Unchecked platform") + raise ValueError("Unchecked platform") return network @@ -531,7 +531,7 @@ def test_update_all_nodes(network, args): host_data_changed = initial_host_data != new_host_data assert ( measurement_changed or host_data_changed - ), f"Cannot test code update, as new package produced identical measurement and host_data as original" + ), "Cannot test code update, as new package produced identical measurement and host_data as original" LOG.info("Add new measurement and host_data") network.consortium.add_measurement(primary, args.enclave_platform, new_measurement) From 815adce2a2efdfc4b8872b166a07fd28dddc09ea Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 24 Jan 2025 14:22:46 +0000 Subject: [PATCH 35/36] Test was renamed --- tests/code_update.py | 2 +- tests/suite/test_suite.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/code_update.py b/tests/code_update.py index 007273775613..954aa7177037 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -742,7 +742,7 @@ def run(args): test_proposal_invalidation(network, args) if not snp.IS_SNP: - # This is in practice equivalent to either "bad measurement" or "bad host data", but is explicitly + # This is in practice equivalent to either "unknown measurement" or "unknown host data", but is explicitly # testing that (without artifically removing/corrupting those values) a replacement package differs # in one of these values test_add_node_with_different_package(network, args) diff --git a/tests/suite/test_suite.py b/tests/suite/test_suite.py index 1016f8498b9d..afa154ecd3ba 100644 --- a/tests/suite/test_suite.py +++ b/tests/suite/test_suite.py @@ -117,7 +117,7 @@ election.test_kill_primary, # code update: code_update.test_verify_quotes, - code_update.test_add_node_with_bad_code, + code_update.test_add_node_with_different_package, # curve migration: reconfiguration.test_change_curve, recovery.test_recover_service, From efefc509691d1b777d982c10d80cf3eb98eda600 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 24 Jan 2025 16:39:11 +0000 Subject: [PATCH 36/36] Our suites like to mess things up --- tests/code_update.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/code_update.py b/tests/code_update.py index 954aa7177037..5d5bc884ae78 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -38,7 +38,9 @@ def test_verify_quotes(network, args): r = uc.get("/node/quotes") all_quotes = r.body.json()["quotes"] - assert len(all_quotes) == len(network.get_joined_nodes()) + assert len(all_quotes) >= len( + network.get_joined_nodes() + ), f"There are {len(network.get_joined_nodes())} joined nodes, yet got only {len(all_quotes)} quotes: {json.dumps(all_quotes, indent=2)}" for node in network.get_joined_nodes(): LOG.info(f"Verifying quote for node {node.node_id}")