diff --git a/.snpcc_canary b/.snpcc_canary index 0c4a3d2ca9ce..935635465f8c 100644 --- a/.snpcc_canary +++ b/.snpcc_canary @@ -1,5 +1,5 @@ - ___ ___ ___ \/ - (. =) Y (0 0) (x X) Y (vv) - O \ o | / | + ___ ___ ___ \_/ + (. =) Y (0 0) (x X) Y (___) + O \ o | / | /-xXx--//-----x=x--/-xXx--/---x-/--->>>--/ .... diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 9b42828f8c83..ac548410cabd 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -2,7 +2,8 @@ "configurations": [ { "name": "Linux", - "compileCommands": "${workspaceFolder}/build/compile_commands.json" + "compileCommands": "${workspaceFolder}/build/compile_commands.json", + "includePath": ["${workspaceFolder}/include", "${workspaceFolder}/src"] } ], "version": 4 diff --git a/CHANGELOG.md b/CHANGELOG.md index c36f3e99cb1f..4bc4fe03e8f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `ccf-devel` RPM to support building CCF applications (#6904) - `ccf` to support running pre-built CCF applications (#6909) +### Added + +- Attestations of new SNP nodes must be from a trusted TCB version higher than the minimum TCB version stored for that CPU model in `public:ccf.gov.nodes.snp.tcb_versions`. Added `set_snp_minimum_tcb_version(cpuid, tcb_version)` and `remove_snp_minimum_tcb_version(cpuid)` governance actions. New networks will automatically populate the TCB version, pre-existing networks must set a TCB version when upgrading. (#6837) +- Expose `AttestationProvider::get_snp_attestation` to extract snp attestations from a quote. (#6837) + ## [6.0.0-rc1] [6.0.0-rc1]: https://github.com/microsoft/CCF/releases/tag/6.0.0-rc1 diff --git a/doc/audit/builtin_maps.rst b/doc/audit/builtin_maps.rst index 632c06d41aa1..ef2a7c1695dc 100644 --- a/doc/audit/builtin_maps.rst +++ b/doc/audit/builtin_maps.rst @@ -197,6 +197,26 @@ For Confidential Azure Container Instance (ACI) deployments, trusted endorsement **Value** Map of issuer feed to Security Version Number (SVN) represented as JSON. See https://ietf-wg-scitt.github.io/draft-ietf-scitt-architecture/draft-ietf-scitt-architecture.html#name-issuer-identity. +``nodes.snp.tcb_versions`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The minimum trusted TCB version for new nodes allowed to join the network (:doc`SNP <../operations/platforms/snp>` only). + +.. note:: For improved serviceability on confidential ACI deployments, see :ref:`audit/builtin_maps:``nodes.snp.tcb_versions``` map. + +**Key** AMD CPUID, represented as a lowercase hex string without an '0x' prefix. + +**Value** The minimum TCB version for that CPUID. + +**Example** +.. list-table:: + :header-rows: 1 + + * - CPUID + - TCB Version + * - ``00a00f11`` + - ``{boot_loader: 4, tee: 0, snp: 24, microcode: 219}`` + ``service.info`` ~~~~~~~~~~~~~~~~ diff --git a/doc/operations/platforms/snp.rst b/doc/operations/platforms/snp.rst index 1f253b1e2344..7721cbe58c3e 100644 --- a/doc/operations/platforms/snp.rst +++ b/doc/operations/platforms/snp.rst @@ -75,6 +75,7 @@ The following governance proposals can be issued to add/remove these trusted val - ``add_snp_host_data``/``remove_snp_host_data``: To add/remove a trusted security policy, e.g. when adding a new trusted container image as part of the code upgrade procedure. - ``add_snp_uvm_endorsement``/``add_snp_uvm_endorsement``: To add remove a trusted UVM endorsement (Azure deployment only). - ``add_snp_measurement``/``remove_snp_measurement``: To add/remove a trusted measurement. +- ``set_snp_minimum_tcb_version``/``remove_snp_minimum_tcb_version``: To add/remove a minimum trusted TCB version. .. rubric:: Footnotes diff --git a/doc/schemas/gov/2024-07-01/gov.json b/doc/schemas/gov/2024-07-01/gov.json index 67c24410430c..a85975ae623b 100644 --- a/doc/schemas/gov/2024-07-01/gov.json +++ b/doc/schemas/gov/2024-07-01/gov.json @@ -2260,12 +2260,20 @@ "additionalProperties": { "$ref": "#/definitions/ServiceState.UvmEndorsementFeeds" } + }, + "tcbVersions": { + "type": "object", + "description": "Collection of minimum TCB versions. Keyed by corresponding CPUID.", + "additionalProperties": { + "$ref": "#/definitions/ServiceState.MinimumTcbVersion" + } } }, "required": [ "measurements", "hostData", - "uvmEndorsements" + "uvmEndorsements", + "tcbVersions" ] }, "ServiceState.SnpQuoteInfo": { @@ -2301,6 +2309,35 @@ "$ref": "#/definitions/ServiceState.FeedInfo" } }, + "ServiceState.TcbVersion": { + "type": "object", + "description": "Minimum TCB version for a specific CPUID.", + "properties": { + "boot_loader": { + "type": "integer", + "description": "" + }, + "tee": { + "type": "integer", + "description": "" + }, + "snp": { + "type": "integer", + "description": "" + }, + "microcode": { + "type": "integer", + "description": "" + } + } + }, + "ServiceState.MinimumTcbVersion": { + "type": "object", + "description": "Collection of minimum TCB versions. Keyed by corresponding CPUID.", + "additionalProperties": { + "$ref": "#/definitions/ServiceState.TcbVersion" + } + }, "ServiceState.caCertBundle": { "type": "string", "description": "Chain of endorsed certificates (PEM format) leading to a CA." @@ -2454,4 +2491,4 @@ "x-ms-parameter-location": "method" } } -} +} \ No newline at end of file diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json index db48581f67af..ef1334edfa4f 100644 --- a/doc/schemas/gov_openapi.json +++ b/doc/schemas/gov_openapi.json @@ -1172,6 +1172,29 @@ ], "type": "object" }, + "TcbVersion": { + "properties": { + "boot_loader": { + "$ref": "#/components/schemas/uint8" + }, + "microcode": { + "$ref": "#/components/schemas/uint8" + }, + "snp": { + "$ref": "#/components/schemas/uint8" + }, + "tee": { + "$ref": "#/components/schemas/uint8" + } + }, + "required": [ + "boot_loader", + "tee", + "snp", + "microcode" + ], + "type": "object" + }, "TransactionId": { "pattern": "^[0-9]+\\.[0-9]+$", "type": "string" @@ -1284,6 +1307,12 @@ }, "type": "object" }, + "string_to_TcbVersion": { + "additionalProperties": { + "$ref": "#/components/schemas/TcbVersion" + }, + "type": "object" + }, "string_to_UVMEndorsementsData": { "additionalProperties": { "$ref": "#/components/schemas/UVMEndorsementsData" @@ -1312,6 +1341,11 @@ "maximum": 18446744073709551615, "minimum": 0, "type": "integer" + }, + "uint8": { + "maximum": 255, + "minimum": 0, + "type": "integer" } }, "securitySchemes": { @@ -1339,7 +1373,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.6.1" + "version": "4.7.2" }, "openapi": "3.0.0", "paths": { @@ -2136,6 +2170,31 @@ } } }, + "/gov/kv/nodes/snp/tcb_versions": { + "get": { + "deprecated": true, + "operationId": "GetGovKvNodesSnpTcbVersions", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/string_to_TcbVersion" + } + } + }, + "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/snp/uvm_endorsements": { "get": { "deprecated": true, diff --git a/include/ccf/node/quote.h b/include/ccf/node/quote.h index f499857d7ead..f5b2d73e55ef 100644 --- a/include/ccf/node/quote.h +++ b/include/ccf/node/quote.h @@ -4,6 +4,7 @@ #include "ccf/ccf_deprecated.h" #include "ccf/ds/quote_info.h" +#include "ccf/pal/attestation_sev_snp.h" #include "ccf/pal/measurement.h" #include "ccf/service/tables/host_data.h" #include "ccf/tx.h" @@ -22,6 +23,8 @@ namespace ccf FailedInvalidHostData, FailedInvalidQuotedPublicKey, FailedUVMEndorsementsNotFound, + FailedInvalidCPUID, + FailedInvalidTcbVersion }; class AttestationProvider @@ -35,10 +38,16 @@ namespace ccf static std::optional get_host_data(const QuoteInfo& quote_info); + static std::optional get_snp_attestation( + const QuoteInfo& quote_info); + static QuoteVerificationResult verify_quote_against_store( ccf::kv::ReadOnlyTx& tx, const QuoteInfo& quote_info, const std::vector& expected_node_public_key_der, pal::PlatformAttestationMeasurement& measurement); }; + QuoteVerificationResult verify_tcb_version_against_store( + ccf::kv::ReadOnlyTx& tx, const QuoteInfo& quote_info); + } \ No newline at end of file diff --git a/include/ccf/pal/attestation_sev_snp.h b/include/ccf/pal/attestation_sev_snp.h index f9c71facfaa5..fa2a9b4ffda8 100644 --- a/include/ccf/pal/attestation_sev_snp.h +++ b/include/ccf/pal/attestation_sev_snp.h @@ -52,6 +52,8 @@ QPHfbkH0CyPfhl1jWhJFZasCAwEAAQ== static_assert( sizeof(TcbVersion) == sizeof(uint64_t), "Can't cast TcbVersion to uint64_t"); + DECLARE_JSON_TYPE(TcbVersion); + DECLARE_JSON_REQUIRED_FIELDS(TcbVersion, boot_loader, tee, snp, microcode); #pragma pack(push, 1) struct Signature @@ -141,7 +143,10 @@ QPHfbkH0CyPfhl1jWhJFZasCAwEAAQ== uint8_t report_id[32]; /* 0x140 */ uint8_t report_id_ma[32]; /* 0x160 */ struct TcbVersion reported_tcb; /* 0x180 */ - uint8_t reserved1[24]; /* 0x188 */ + uint8_t cpuid_fam_id; /* 0x188*/ + uint8_t cpuid_mod_id; /* 0x189 */ + uint8_t cpuid_step; /* 0x18A */ + uint8_t reserved1[21]; /* 0x18B */ uint8_t chip_id[64]; /* 0x1A0 */ struct TcbVersion committed_tcb; /* 0x1E0 */ uint8_t current_minor; /* 0x1E8 */ @@ -261,4 +266,96 @@ QPHfbkH0CyPfhl1jWhJFZasCAwEAAQ== virtual ~AttestationInterface() = default; }; + + static uint8_t MIN_TCB_VERIF_VERSION = 3; +#pragma pack(push, 1) + // AMD CPUID specification. Chapter 2 Fn0000_0001_EAX + // Milan: 0x00A00F11 + // Genoa: 0X00A10F11 + // Note: The CPUID is little-endian so the hex_string is reversed + struct CPUID + { + uint8_t stepping : 4; + uint8_t base_model : 4; + uint8_t base_family : 4; + uint8_t reserved : 4; + uint8_t extended_model : 4; + uint8_t extended_family : 8; + uint8_t reserved2 : 4; + + bool operator==(const CPUID&) const = default; + std::string hex_str() const + { + CPUID buf = *this; + auto buf_ptr = reinterpret_cast(&buf); + const std::span tcb_bytes{ + buf_ptr, buf_ptr + sizeof(CPUID)}; + return fmt::format( + "{:02x}", fmt::join(tcb_bytes.rbegin(), tcb_bytes.rend(), "")); + } + inline uint8_t get_family_id() const + { + return this->base_family + this->extended_family; + } + inline uint8_t get_model_id() const + { + return (this->extended_model << 4) | this->base_model; + } + }; +#pragma pack(pop) + DECLARE_JSON_TYPE(CPUID); + DECLARE_JSON_REQUIRED_FIELDS( + CPUID, stepping, base_model, base_family, extended_model, extended_family); + static_assert( + sizeof(CPUID) == sizeof(uint32_t), "Can't cast CPUID to uint32_t"); + static CPUID cpuid_from_hex(const std::string& hex_str) + { + CPUID ret; + auto buf_ptr = reinterpret_cast(&ret); + ccf::ds::from_hex(hex_str, buf_ptr, buf_ptr + sizeof(CPUID)); + std::reverse( + buf_ptr, buf_ptr + sizeof(CPUID)); // fix little endianness of AMD + return ret; + } + + // On SEVSNP cpuid cannot be trusted and must be validated against an + // attestation. + static CPUID get_cpuid_untrusted() + { + uint32_t ieax = 1; + uint64_t iebx = 0; + uint64_t iecx = 0; + uint64_t iedx = 0; + uint32_t oeax = 0; + uint64_t oebx = 0; + uint64_t oecx = 0; + uint64_t oedx = 0; + // pass in e{b,c,d}x to prevent cpuid from blatting other registers + asm volatile("cpuid" + : "=a"(oeax), "=b"(oebx), "=c"(oecx), "=d"(oedx) + : "a"(ieax), "b"(iebx), "c"(iecx), "d"(iedx)); + auto cpuid = *reinterpret_cast(&oeax); + return cpuid; + } } + +namespace ccf::kv::serialisers +{ + // Use hex string to ensure uniformity between the endpoint perspective and + // the kv's key + template <> + struct BlitSerialiser + { + static SerialisedEntry to_serialised(const ccf::pal::snp::CPUID& chip) + { + auto hex_str = chip.hex_str(); + return SerialisedEntry(hex_str.begin(), hex_str.end()); + } + + static ccf::pal::snp::CPUID from_serialised(const SerialisedEntry& data) + { + return ccf::pal::snp::cpuid_from_hex( + std::string(data.data(), data.end())); + } + }; +} \ No newline at end of file diff --git a/include/ccf/pal/snp_ioctl5.h b/include/ccf/pal/snp_ioctl5.h index a40251bb5f25..2c546b471212 100644 --- a/include/ccf/pal/snp_ioctl5.h +++ b/include/ccf/pal/snp_ioctl5.h @@ -124,10 +124,11 @@ namespace ccf::pal::snp::ioctl5 int rc = ioctl(fd, SEV_SNP_GUEST_MSG_REPORT, &payload); if (rc < 0) { - CCF_APP_FAIL("IOCTL call failed: {}", strerror(errno)); - CCF_APP_FAIL("Payload error: {}", payload.error); - throw std::logic_error( - "Failed to issue ioctl SEV_SNP_GUEST_MSG_REPORT"); + const auto msg = fmt::format( + "Failed to issue ioctl SEV_SNP: {} payload error: {}", + strerror(errno), + payload.error); + throw std::logic_error(msg); } } diff --git a/include/ccf/service/tables/tcb_verification.h b/include/ccf/service/tables/tcb_verification.h new file mode 100644 index 000000000000..2e01f53ff1e6 --- /dev/null +++ b/include/ccf/service/tables/tcb_verification.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "ccf/ds/json.h" +#include "ccf/pal/attestation_sev_snp.h" +#include "ccf/service/map.h" + +namespace ccf +{ + using SnpTcbVersionMap = ServiceMap; + + namespace Tables + { + static constexpr auto SNP_TCB_VERSIONS = + "public:ccf.gov.nodes.snp.tcb_versions"; + } +} \ No newline at end of file diff --git a/js/ccf-app/src/global.ts b/js/ccf-app/src/global.ts index c60439f9f23c..9497cb9ee6d6 100644 --- a/js/ccf-app/src/global.ts +++ b/js/ccf-app/src/global.ts @@ -789,6 +789,9 @@ export interface SnpAttestationResult { report_id: ArrayBuffer; report_id_ma: ArrayBuffer; reported_tcb: TcbVersion; + cpuid_fam_id: number; + cpuid_mod_id: number; + cpuid_step: number; chip_id: ArrayBuffer; committed_tcb: TcbVersion; current_minor: number; diff --git a/samples/constitutions/default/actions.js b/samples/constitutions/default/actions.js index 0dd2e8ada4c9..c13df4a11a6f 100644 --- a/samples/constitutions/default/actions.js +++ b/samples/constitutions/default/actions.js @@ -1096,6 +1096,44 @@ const actions = new Map([ }, ), ], + [ + "set_snp_minimum_tcb_version", + new Action( + function (args) { + checkType(args.cpuid, "string", "cpuid"); + checkLength(ccf.strToBuf(args.cpuid), 8, 8, "cpuid"); + checkLength(hexStrToBuf(args.cpuid), 4, 4, "cpuid"); + if (args.cpuid !== args.cpuid.toLowerCase()) { + throw new Error( + `CPUID must be an lowercaqse hex string, ${args.cpuid}`, + ); + } + + checkType(args.tcb_version, "object", "tcb_version"); + checkType( + args.tcb_version?.boot_loader, + "number", + "tcb_version.boot_loader", + ); + checkType(args.tcb_version?.tee, "number", "tcb_version.tee"); + checkType(args.tcb_version?.snp, "number", "tcb_version.snp"); + checkType( + args.tcb_version?.microcode, + "number", + "tcb_version.microcode", + ); + }, + function (args, proposalId) { + // ensure cpuid is uppercase to prevent aliasing + ccf.kv["public:ccf.gov.nodes.snp.tcb_versions"].set( + ccf.strToBuf(args.cpuid), + ccf.jsonCompatibleToBuf(args.tcb_version), + ); + + invalidateOtherOpenProposals(proposalId); + }, + ), + ], [ "remove_snp_host_data", new Action( @@ -1151,6 +1189,23 @@ const actions = new Map([ }, ), ], + [ + "remove_snp_minimum_tcb_version", + new Action( + function (args) { + checkType(args.cpuid, "string", "cpuid"); + checkLength(ccf.strToBuf(args.cpuid), 8, 8, "cpuid"); + }, + function (args) { + const cpuid = ccf.strToBuf(args.cpuid); + if (ccf.kv["public:ccf.gov.nodes.snp.tcb_versions"].has(cpuid)) { + ccf.kv["public:ccf.gov.nodes.snp.tcb_versions"].delete(cpuid); + } else { + throw new Error(`CPUID ${args.cpuid} not found`); + } + }, + ), + ], [ "set_node_data", new Action( diff --git a/scripts/ci-checks.sh b/scripts/ci-checks.sh index b1597bbdce40..6d69a5cdcfbd 100755 --- a/scripts/ci-checks.sh +++ b/scripts/ci-checks.sh @@ -94,6 +94,7 @@ endgroup group "OpenAPI" npm install --loglevel=error --no-save @apidevtools/swagger-cli 1>/dev/null find doc/schemas/*.json -exec npx swagger-cli validate {} \; +find doc/schemas/gov/*/*.json -exec npx swagger-cli validate {} \; endgroup group "Copyright notice headers" diff --git a/src/js/extensions/ccf/converters.cpp b/src/js/extensions/ccf/converters.cpp index b53649e9e7bf..f497c86811e6 100644 --- a/src/js/extensions/ccf/converters.cpp +++ b/src/js/extensions/ccf/converters.cpp @@ -7,10 +7,12 @@ #include "ccf/js/extensions/ccf/converters.h" #include "ccf/js/core/context.h" +#include "ccf/pal/attestation_sev_snp.h" #include "ccf/version.h" #include "js/checks.h" #include "node/rpc/jwt_management.h" +#include #include namespace ccf::js::extensions diff --git a/src/js/extensions/snp_attestation.cpp b/src/js/extensions/snp_attestation.cpp index 4e2365cf2464..8dafd1a59963 100644 --- a/src/js/extensions/snp_attestation.cpp +++ b/src/js/extensions/snp_attestation.cpp @@ -214,6 +214,10 @@ namespace ccf::js::extensions JS_CHECK_EXC(reported_tcb); JS_CHECK_SET(a.set("reported_tcb", std::move(reported_tcb))); + JS_CHECK_SET(a.set_uint32("cpuid_fam_id", attestation.cpuid_fam_id)); + JS_CHECK_SET(a.set_uint32("cpuid_mod_id", attestation.cpuid_mod_id)); + JS_CHECK_SET(a.set_uint32("cpuid_step", attestation.cpuid_step)); + auto attestation_chip_id = jsctx.new_array_buffer_copy(attestation.chip_id); JS_CHECK_EXC(attestation_chip_id); JS_CHECK_SET(a.set("chip_id", std::move(attestation_chip_id))); diff --git a/src/node/gov/handlers/service_state.h b/src/node/gov/handlers/service_state.h index 7fe0e3f6a636..f03b816fb196 100644 --- a/src/node/gov/handlers/service_state.h +++ b/src/node/gov/handlers/service_state.h @@ -615,6 +615,20 @@ namespace ccf::gov::endpoints }); snp_policy["uvmEndorsements"] = snp_endorsements; + auto snp_tcb_versions = nlohmann::json::object(); + auto tcb_versions_handle = + ctx.tx.template ro( + ccf::Tables::SNP_TCB_VERSIONS); + + tcb_versions_handle->foreach( + [&snp_tcb_versions]( + const std::string& cpuid, + const pal::snp::TcbVersion& tcb_version) { + snp_tcb_versions[cpuid] = tcb_version; + return true; + }); + snp_policy["tcbVersions"] = snp_tcb_versions; + response_body["snp"] = snp_policy; } diff --git a/src/node/quote.cpp b/src/node/quote.cpp index f821bab473a0..65d09d1dfd51 100644 --- a/src/node/quote.cpp +++ b/src/node/quote.cpp @@ -6,6 +6,7 @@ #include "ccf/pal/attestation.h" #include "ccf/service/tables/code_id.h" #include "ccf/service/tables/snp_measurements.h" +#include "ccf/service/tables/tcb_verification.h" #include "ccf/service/tables/uvm_endorsements.h" #include "ccf/service/tables/virtual_measurements.h" #include "node/uvm_endorsements.h" @@ -138,6 +139,29 @@ namespace ccf return measurement; } + std::optional AttestationProvider::get_snp_attestation( + const QuoteInfo& quote_info) + { + if (quote_info.format != QuoteFormat::amd_sev_snp_v1) + { + return std::nullopt; + } + try + { + pal::PlatformAttestationMeasurement d = {}; + pal::PlatformAttestationReportData r = {}; + pal::verify_quote(quote_info, d, r); + auto attestation = *reinterpret_cast( + quote_info.quote.data()); + return attestation; + } + catch (const std::exception& e) + { + LOG_FAIL_FMT("Failed to verify local attestation report: {}", e.what()); + return std::nullopt; + } + } + std::optional AttestationProvider::get_host_data( const QuoteInfo& quote_info) { @@ -231,6 +255,62 @@ namespace ccf return QuoteVerificationResult::Verified; } + QuoteVerificationResult verify_tcb_version_against_store( + ccf::kv::ReadOnlyTx& tx, const QuoteInfo& quote_info) + { + if (quote_info.format != QuoteFormat::amd_sev_snp_v1) + { + return QuoteVerificationResult::Verified; + } + + pal::PlatformAttestationMeasurement d = {}; + pal::PlatformAttestationReportData r = {}; + pal::verify_quote(quote_info, d, r); + auto attestation = + *reinterpret_cast(quote_info.quote.data()); + + if (attestation.version < pal::snp::MIN_TCB_VERIF_VERSION) + { + // Necessary until all C-ACI servers are updated + return QuoteVerificationResult::Verified; + } + + std::optional min_tcb_opt = std::nullopt; + + auto h = tx.ro(Tables::SNP_TCB_VERSIONS); + // expensive but there should not be many entries + h->foreach([&min_tcb_opt, &attestation]( + const std::string cpuid_hex, const pal::snp::TcbVersion& v) { + auto cpuid = pal::snp::cpuid_from_hex(cpuid_hex); + if ( + cpuid.get_family_id() == attestation.cpuid_fam_id && + cpuid.get_model_id() == attestation.cpuid_mod_id && + cpuid.stepping == attestation.cpuid_step) + { + min_tcb_opt = v; + return false; + } + return true; + }); + + if (!min_tcb_opt.has_value()) + { + return QuoteVerificationResult::FailedInvalidCPUID; + } + + auto min_tcb = min_tcb_opt.value(); + + // only check snp and microcode as these are AMD controlled + if ( + min_tcb.snp > attestation.reported_tcb.snp || + min_tcb.microcode > attestation.reported_tcb.microcode) + { + return QuoteVerificationResult::FailedInvalidTcbVersion; + } + + return QuoteVerificationResult::Verified; + } + QuoteVerificationResult AttestationProvider::verify_quote_against_store( ccf::kv::ReadOnlyTx& tx, const QuoteInfo& quote_info, @@ -263,6 +343,12 @@ namespace ccf return rc; } + rc = verify_tcb_version_against_store(tx, quote_info); + if (rc != QuoteVerificationResult::Verified) + { + return rc; + } + return verify_quoted_node_public_key( expected_node_public_key_der, quoted_hash); } diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index caddcf3a659a..cd59e1afc24a 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -15,6 +15,7 @@ #include "ccf/service/tables/jwt.h" #include "ccf/service/tables/members.h" #include "ccf/service/tables/nodes.h" +#include "ccf/service/tables/tcb_verification.h" #include "frontend.h" #include "js/extensions/ccf/network.h" #include "js/extensions/ccf/node.h" @@ -508,7 +509,8 @@ namespace ccf handle->foreach([&response_body](const auto& k, const auto& v) { if constexpr ( std::is_same_v || - pal::is_attestation_measurement::value) + pal::is_attestation_measurement::value || + std::is_same_v) { response_body[k.hex_str()] = v; } @@ -610,7 +612,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.6.1"; + openapi_info.document_version = "4.7.2"; } static std::optional get_caller_member_id( diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 7c962f225858..1ec9a0499280 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1635,6 +1635,11 @@ namespace ccf InternalTablesAccess::trust_node_uvm_endorsements( ctx.tx, in.snp_uvm_endorsements); + + auto attestation = + AttestationProvider::get_snp_attestation(in.quote_info).value(); + InternalTablesAccess::trust_node_snp_tcb_version( + ctx.tx, attestation); break; } diff --git a/src/pal/quote_generation.h b/src/pal/quote_generation.h index 347da0fff7c3..f5a7aa6ce06d 100644 --- a/src/pal/quote_generation.h +++ b/src/pal/quote_generation.h @@ -3,6 +3,7 @@ #pragma once #include "ccf/crypto/hash_provider.h" +#include "ccf/pal/attestation.h" #include "ds/files.h" #include diff --git a/src/pal/test/snp_ioctl_test.cpp b/src/pal/test/snp_ioctl_test.cpp index bfb4cf8b1f75..9711ea5a3390 100644 --- a/src/pal/test/snp_ioctl_test.cpp +++ b/src/pal/test/snp_ioctl_test.cpp @@ -2,7 +2,6 @@ // Licensed under the Apache 2.0 License. #include "ccf/crypto/symmetric_key.h" -#include "ccf/ds/logger.h" #include "ccf/pal/snp_ioctl.h" #include "crypto/openssl/hash.h" diff --git a/src/service/internal_tables_access.h b/src/service/internal_tables_access.h index 72dbd998edd8..c68e263f0b4d 100644 --- a/src/service/internal_tables_access.h +++ b/src/service/internal_tables_access.h @@ -9,6 +9,7 @@ #include "ccf/service/tables/members.h" #include "ccf/service/tables/nodes.h" #include "ccf/service/tables/snp_measurements.h" +#include "ccf/service/tables/tcb_verification.h" #include "ccf/service/tables/users.h" #include "ccf/service/tables/virtual_measurements.h" #include "ccf/tx.h" @@ -822,6 +823,112 @@ namespace ccf {{uvm_endorsements->feed, {uvm_endorsements->svn}}}); } + static void trust_static_snp_tcb_version(ccf::kv::Tx& tx) + { + auto h = tx.wo(Tables::SNP_TCB_VERSIONS); + + constexpr pal::snp::CPUID milan_chip_id{ + .stepping = 0x1, + .base_model = 0x1, + .base_family = 0xF, + .reserved = 0, + .extended_model = 0x0, + .extended_family = 0x0A, + .reserved2 = 0}; + constexpr pal::snp::TcbVersion milan_tcb_version = { + .boot_loader = 0, + .tee = 0, + .reserved = {0}, + .snp = 0x18, + .microcode = 0xDB}; + h->put(milan_chip_id.hex_str(), milan_tcb_version); + + constexpr pal::snp::CPUID milan_x_chip_id{ + .stepping = 0x2, + .base_model = 0x1, + .base_family = 0xF, + .reserved = 0, + .extended_model = 0x0, + .extended_family = 0x0A, + .reserved2 = 0}; + constexpr pal::snp::TcbVersion milan_x_tcb_version = { + .boot_loader = 0, + .tee = 0, + .reserved = {0}, + .snp = 0x18, + .microcode = 0x44}; + h->put(milan_x_chip_id.hex_str(), milan_x_tcb_version); + + constexpr pal::snp::CPUID genoa_chip_id{ + .stepping = 0x1, + .base_model = 0x1, + .base_family = 0xF, + .reserved = 0, + .extended_model = 0x1, + .extended_family = 0x0A, + .reserved2 = 0}; + constexpr pal::snp::TcbVersion genoa_tcb_version = { + .boot_loader = 0, + .tee = 0, + .reserved = {0}, + .snp = 0x17, + .microcode = 0x54}; + h->put(genoa_chip_id.hex_str(), genoa_tcb_version); + + constexpr pal::snp::CPUID genoa_x_chip_id{ + .stepping = 0x2, + .base_model = 0x1, + .base_family = 0xF, + .reserved = 0, + .extended_model = 0x1, + .extended_family = 0x0A, + .reserved2 = 0}; + constexpr pal::snp::TcbVersion genoa_x_tcb_version = { + .boot_loader = 0, + .tee = 0, + .reserved = {0}, + .snp = 0x17, + .microcode = 0x4F}; + h->put(genoa_x_chip_id.hex_str(), genoa_x_tcb_version); + } + + static void trust_node_snp_tcb_version( + ccf::kv::Tx& tx, pal::snp::Attestation& attestation) + { + // Fall back to statically configured tcb versions + if (attestation.version < pal::snp::MIN_TCB_VERIF_VERSION) + { + LOG_FAIL_FMT( + "SNP attestation version {} older than {}, falling back to static " + "minimum TCB values", + attestation.version, + pal::snp::MIN_TCB_VERIF_VERSION); + trust_static_snp_tcb_version(tx); + return; + } + + // As cpuid -> attestation cpuid is surjective, we must use the local + // cpuid and validate it against the attestation's cpuid + auto cpuid = pal::snp::get_cpuid_untrusted(); + if ( + cpuid.get_family_id() != attestation.cpuid_fam_id || + cpuid.get_model_id() != attestation.cpuid_mod_id || + cpuid.stepping != attestation.cpuid_step) + { + LOG_FAIL_FMT( + "CPU-sourced cpuid does not match attestation cpuid ({} != {}, {}, " + "{})", + cpuid.hex_str(), + attestation.cpuid_fam_id, + attestation.cpuid_mod_id, + attestation.cpuid_step); + trust_static_snp_tcb_version(tx); + return; + } + auto h = tx.wo(Tables::SNP_TCB_VERSIONS); + h->put(cpuid.hex_str(), attestation.reported_tcb); + } + static void init_configuration( ccf::kv::Tx& tx, const ServiceConfiguration& configuration) { diff --git a/src/service/network_tables.h b/src/service/network_tables.h index a5576516a227..a52cd5dc2c72 100644 --- a/src/service/network_tables.h +++ b/src/service/network_tables.h @@ -18,6 +18,7 @@ #include "ccf/service/tables/proposals.h" #include "ccf/service/tables/service.h" #include "ccf/service/tables/snp_measurements.h" +#include "ccf/service/tables/tcb_verification.h" #include "ccf/service/tables/users.h" #include "ccf/service/tables/uvm_endorsements.h" #include "ccf/service/tables/virtual_measurements.h" @@ -96,6 +97,7 @@ namespace ccf const SnpMeasurements snp_measurements = {Tables::NODE_SNP_MEASUREMENTS}; const SNPUVMEndorsements snp_uvm_endorsements = { Tables::NODE_SNP_UVM_ENDORSEMENTS}; + const SnpTcbVersionMap snp_tcb_versions = {Tables::SNP_TCB_VERSIONS}; inline auto get_all_node_tables() const { @@ -108,7 +110,8 @@ namespace ccf virtual_measurements, host_data, snp_measurements, - snp_uvm_endorsements); + snp_uvm_endorsements, + snp_tcb_versions); } // diff --git a/tests/code_update.py b/tests/code_update.py index 806aabf23562..2a8712978927 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -273,6 +273,66 @@ def get_trusted_host_data(node): return network +@reqs.description("Test tcb version tables") +@reqs.snp_only() +def test_tcb_version_tables(network, args): + primary, _ = network.find_nodes() + LOG.info("Checking that the TCB versions is correctly populated") + cpuid, tcb_version = None, None + 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 + versions = r.body.json()["snp"]["tcbVersions"] + assert len(versions) == 1, f"Expected one TCB version, {versions}" + cpuid, tcb_version = next(iter(versions.items())) + + LOG.info("CPUID should be lowercase") + assert cpuid.lower() == cpuid, f"Expected lowercase CPUID, {cpuid}" + + LOG.info("Change current cpuid's TCB version") + test_tcb_version = {"boot_loader": 0, "microcode": 0, "snp": 0, "tee": 0} + network.consortium.set_snp_minimum_tcb_version(primary, cpuid, test_tcb_version) + 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 + versions = r.body.json()["snp"]["tcbVersions"] + assert cpuid in versions, f"Expected {cpuid} in TCB versions, {versions}" + assert ( + versions[cpuid] == test_tcb_version + ), f"TCB version does not match, {versions} != {test_tcb_version}" + + LOG.info("Removing current cpuid's TCB version") + network.consortium.remove_snp_minimum_tcb_version(primary, cpuid) + 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 + versions = r.body.json()["snp"]["tcbVersions"] + assert len(versions) == 0, f"Expected no TCB versions, {versions}" + + LOG.info("Checking new nodes are prevented from joining") + thrown_exception = None + try: + new_node = network.create_node("local://localhost") + network.join_node(new_node, args.package, args, timeout=3) + network.trust_node(new_node, args) + except TimeoutError as e: + thrown_exception = e + assert thrown_exception is not None, "New node should not have been able to join" + + LOG.info("Adding new cpuid's TCB version") + network.consortium.set_snp_minimum_tcb_version(primary, cpuid, tcb_version) + 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 + versions = r.body.json()["snp"]["tcbVersions"] + assert len(versions) == 1, f"Expected one TCB version, {versions}" + + LOG.info("Checking new nodes are allowed to join") + new_node = network.create_node("local://localhost") + network.join_node(new_node, args.package, args, timeout=3) + network.trust_node(new_node, args) + + @reqs.description("Join node with no security policy") @reqs.snp_only() def test_add_node_without_security_policy(network, args): @@ -739,6 +799,7 @@ def run(args): test_add_node_with_stubbed_security_policy(network, args) test_start_node_with_mismatched_host_data(network, args) test_add_node_without_security_policy(network, args) + test_tcb_version_tables(network, args) # Endorsements test_endorsements_tables(network, args) diff --git a/tests/e2e_operations.py b/tests/e2e_operations.py index 0605e7247031..979b8bea8091 100644 --- a/tests/e2e_operations.py +++ b/tests/e2e_operations.py @@ -1040,6 +1040,79 @@ def run_initial_uvm_descriptor_checks(args): assert False, "No UVM endorsement found in recovery ledger" +def run_initial_tcb_version_checks(args): + with infra.network.network( + args.nodes, + args.binary_dir, + args.debug_nodes, + args.perf_nodes, + pdb=args.pdb, + ) as network: + LOG.info("Start a network and stop it") + network.start_and_open(args) + primary, _ = network.find_primary() + old_common = infra.network.get_common_folder_name(args.workspace, args.label) + snapshots_dir = network.get_committed_snapshots(primary) + network.stop_all_nodes() + + LOG.info("Check that the a SNP tcb_version is present") + ledger_dirs = primary.remote.ledger_paths() + ledger = ccf.ledger.Ledger(ledger_dirs) + first_chunk = next(iter(ledger)) + first_tx = next(first_chunk) + tables = first_tx.get_public_domain().get_tables() + tcb_versions = tables["public:ccf.gov.nodes.snp.tcb_versions"] + assert len(tcb_versions) == 1, tcb_versions + LOG.info(f"Initial TCB_version found in ledger: {tcb_versions}") + + LOG.info("Start a recovery network and stop it") + current_ledger_dir, committed_ledger_dirs = primary.get_ledger() + recovered_network = infra.network.Network( + args.nodes, + args.binary_dir, + args.debug_nodes, + args.perf_nodes, + existing_network=network, + ) + args.previous_service_identity_file = os.path.join( + old_common, "service_cert.pem" + ) + recovered_network.start_in_recovery( + args, + ledger_dir=current_ledger_dir, + committed_ledger_dirs=committed_ledger_dirs, + snapshots_dir=snapshots_dir, + ) + recovered_primary, _ = recovered_network.find_primary() + LOG.info("Check that the TCB_version is present in the recovery tx") + recovery_seqno = None + with recovered_primary.client() as c: + r = c.get("/node/network").body.json() + recovery_seqno = int(r["current_service_create_txid"].split(".")[1]) + network.stop_all_nodes() + ledger = ccf.ledger.Ledger( + recovered_primary.remote.ledger_paths(), + committed_only=False, + read_recovery_files=True, + ) + for chunk in ledger: + _, chunk_end_seqno = chunk.get_seqnos() + if chunk_end_seqno < recovery_seqno: + continue + for tx in chunk: + tables = tx.get_public_domain().get_tables() + seqno = tx.get_public_domain().get_seqno() + if seqno < recovery_seqno: + continue + else: + tables = tx.get_public_domain().get_tables() + tcb_versions = tables["public:ccf.gov.nodes.snp.tcb_versions"] + assert len(tcb_versions) == 1, tcb_versions + LOG.info(f"Recovery TCB_version found in ledger: {tcb_versions}") + return + assert False, "No TCB_version found in recovery ledger" + + def run(args): run_max_uncommitted_tx_count(args) run_file_operations(args) @@ -1055,3 +1128,4 @@ def run(args): run_empty_ledger_dir_check(args) if infra.snp.is_snp(): run_initial_uvm_descriptor_checks(args) + run_initial_tcb_version_checks(args) diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 19c4c2dccda8..c205615383a7 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -890,6 +890,15 @@ def add_snp_host_data( proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) + def set_snp_minimum_tcb_version(self, remote_node, cpuid, new_tcb_version): + proposal_body, careful_vote = self.make_proposal( + "set_snp_minimum_tcb_version", + cpuid=cpuid, + tcb_version=new_tcb_version, + ) + 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) @@ -914,6 +923,14 @@ def remove_snp_host_data(self, remote_node, host_data): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) + def remove_snp_minimum_tcb_version(self, remote_node, cpuid): + proposal_body, careful_vote = self.make_proposal( + "remove_snp_minimum_tcb_version", + cpuid=cpuid, + ) + proposal = self.get_any_active_member().propose(remote_node, proposal_body) + return self.vote_using_majority(remote_node, proposal, careful_vote) + def set_node_data(self, remote_node, node_service_id, node_data): proposal, careful_vote = self.make_proposal( "set_node_data", diff --git a/tests/npm-app/src/endpoints/snp_attestation.ts b/tests/npm-app/src/endpoints/snp_attestation.ts index 339511f2e412..ea20d66fe39b 100644 --- a/tests/npm-app/src/endpoints/snp_attestation.ts +++ b/tests/npm-app/src/endpoints/snp_attestation.ts @@ -57,6 +57,9 @@ interface SnpAttestationResult { report_id: string; report_id_ma: string; reported_tcb: TcbVersion; + cpuid_fam_id: number; + cpuid_mod_id: number; + cpuid_step: number; chip_id: string; committed_tcb: TcbVersion; current_minor: number; diff --git a/tests/schema.py b/tests/schema.py index 4e9da2dab971..02df82949e9e 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -165,8 +165,7 @@ def fetch_schema(api_response, target_file_path): for method in sorted(set(all_methods)): LOG.info(f" {method}") - if made_changes or not documents_valid: - assert False + assert not (made_changes or not documents_valid) def run_nobuiltins(args):