From a931ec507cc3d007c8e9d56ba41d9a3ba7acd934 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Wed, 15 Jan 2025 13:39:43 +0000 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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(""));