diff --git a/doc/operations/platforms/snp.rst b/doc/operations/platforms/snp.rst index 48b277778014..d565b6ce904a 100644 --- a/doc/operations/platforms/snp.rst +++ b/doc/operations/platforms/snp.rst @@ -4,7 +4,7 @@ AMD SEV-SNP How to use the AMD SEV-SNP platform ----------------------------------- -CCF must run on an AMD CPU which supports SEV-SNP, such as `Azure confidential containers `_ or `Azure Kubernetes Service with Confidential Containers `_. +CCF must run on an AMD CPU which supports SEV-SNP, such as `Azure confidential containers `_, `Azure Kubernetes Service with Confidential Containers `_, or AWS EC2 SEV-SNP instances. CCF will use the SEV-SNP platform features automatically on the supported hardware. @@ -48,7 +48,12 @@ AMD VCEK endorsements must be fetched, preferably from the THIM service, but con Non-Azure Deployment ~~~~~~~~~~~~~~~~~~~~ -For non-Azure deployments, the certificate chain for VCEK can be retrieved either from file, if already cached, or from an endorsement server, as specified in the :ref:`operations/configuration:``attestation.snp_endorsements_servers``` configuration section. For example, for the `well-known AMD endorsement server `_, the value should be set to: +For non-Azure deployments, configure an endorsement server in the :ref:`operations/configuration:``attestation.snp_endorsements_servers``` configuration section. CCF selects the collateral flow from the signing key recorded in the attestation report: + +- For VCEK-signed reports, CCF retrieves the chip-specific VCEK and certificate chain. +- For VLEK-signed reports, such as reports from AWS EC2 shared-tenancy SEV-SNP instances, CCF obtains the VLEK leaf certificate from the host through ``SNP_GET_EXT_REPORT`` and retrieves the ASVK/ARK chain from AMD. The leaf is read from the certificate table published by the hypervisor, under either the VLEK or the VCEK GUID and in either big-endian or mixed-endian byte order, since hosts differ in which slot and encoding they use. + +For the `well-known AMD endorsement server `_, the value should be set to: .. code-block:: json @@ -66,7 +71,9 @@ For non-Azure deployments, the certificate chain for VCEK can be retrieved eithe .. tip:: See :ccf_repo:`samples/config/start_config_amd_sev_snp.json` for a sample node configuration for non-Azure deployments. -.. note:: If no local file is available, the CCF node will fetch the AMD VCEK endorsements from the server on startup, which may cause substantial deployment delays (up to tens of seconds) depending on network latency and endpoint throttling. +.. note:: CCF fetches the AMD certificate chain from the server on startup, which may cause substantial deployment delays (up to tens of seconds) depending on network latency and endpoint throttling. + +.. note:: The ``snp_endorsements_file`` option contains VCEK collateral and is not used for VLEK-signed reports. Governance Proposals ~~~~~~~~~~~~~~~~~~~~ diff --git a/include/ccf/pal/attestation_sev_snp.h b/include/ccf/pal/attestation_sev_snp.h index 600ed237a403..b8d9f49536c6 100644 --- a/include/ccf/pal/attestation_sev_snp.h +++ b/include/ccf/pal/attestation_sev_snp.h @@ -366,6 +366,7 @@ pRb21iI1NlNCfOGUPIhVpWECAwEAAQ== "Cannot cast GuestPolicy to uint64_t"); static constexpr uint8_t attestation_flags_signing_key_vcek = 0; + static constexpr uint8_t attestation_flags_signing_key_vlek = 1; #pragma pack(push, 1) struct Flags @@ -489,10 +490,17 @@ pRb21iI1NlNCfOGUPIhVpWECAwEAAQ== EndorsementEndpointsConfiguration config; - auto chip_id_hex = - fmt::format("{:02x}", fmt::join(quote.get_chip_id_for_vcek(), "")); auto reported_tcb = fmt::format( "{:0x}", *reinterpret_cast("e.reported_tcb)); + const bool is_vlek = + quote.flags.signing_key == attestation_flags_signing_key_vlek; + + std::optional chip_id_hex = std::nullopt; + if (!is_vlek) + { + chip_id_hex = + fmt::format("{:02x}", fmt::join(quote.get_chip_id_for_vcek(), "")); + } constexpr size_t default_max_retries_count = 10; static const ds::SizeString default_max_client_response_size = @@ -500,13 +508,26 @@ pRb21iI1NlNCfOGUPIhVpWECAwEAAQ== if (endorsements_servers.empty()) { - // Default to Azure server if no servers are specified - config.servers.emplace_back(make_azure_endorsements_server( - default_azure_endorsements_endpoint, - chip_id_hex, - reported_tcb, - default_max_retries_count, - default_max_client_response_size)); + if (is_vlek) + { + const auto product = + get_sev_snp_product(quote.cpuid_fam_id, quote.cpuid_mod_id); + config.servers.emplace_back(make_amd_vlek_endorsements_server( + default_amd_endorsements_endpoint, + product, + default_max_retries_count, + default_max_client_response_size)); + } + else + { + // Default to Azure server if no servers are specified + config.servers.emplace_back(make_azure_endorsements_server( + default_azure_endorsements_endpoint, + chip_id_hex.value(), + reported_tcb, + default_max_retries_count, + default_max_client_response_size)); + } return config; } @@ -521,11 +542,17 @@ pRb21iI1NlNCfOGUPIhVpWECAwEAAQ== { case EndorsementsEndpointType::Azure: { + if (is_vlek) + { + throw std::logic_error( + "Azure endorsements endpoints do not support VLEK-signed " + "attestation reports"); + } auto loc = get_endpoint_loc(server, default_azure_endorsements_endpoint); config.servers.emplace_back(make_azure_endorsements_server( loc, - chip_id_hex, + chip_id_hex.value(), reported_tcb, max_retries_count, max_client_response_size)); @@ -536,6 +563,15 @@ pRb21iI1NlNCfOGUPIhVpWECAwEAAQ== auto product = get_sev_snp_product(quote.cpuid_fam_id, quote.cpuid_mod_id); + auto loc = + get_endpoint_loc(server, default_amd_endorsements_endpoint); + if (is_vlek) + { + config.servers.emplace_back(make_amd_vlek_endorsements_server( + loc, product, max_retries_count, max_client_response_size)); + break; + } + std::string boot_loader; std::string tee; std::string snp; @@ -570,11 +606,9 @@ pRb21iI1NlNCfOGUPIhVpWECAwEAAQ== } } - auto loc = - get_endpoint_loc(server, default_amd_endorsements_endpoint); config.servers.emplace_back(make_amd_endorsements_server( loc, - chip_id_hex, + chip_id_hex.value(), boot_loader, tee, snp, @@ -587,11 +621,17 @@ pRb21iI1NlNCfOGUPIhVpWECAwEAAQ== } case EndorsementsEndpointType::THIM: { + if (is_vlek) + { + throw std::logic_error( + "THIM endorsements endpoints do not support VLEK-signed " + "attestation reports"); + } auto loc = get_endpoint_loc(server, default_thim_endorsements_endpoint); config.servers.emplace_back(make_thim_endorsements_server( loc, - chip_id_hex, + chip_id_hex.value(), reported_tcb, max_retries_count, max_client_response_size)); @@ -613,6 +653,10 @@ pRb21iI1NlNCfOGUPIhVpWECAwEAAQ== public: [[nodiscard]] virtual const snp::Attestation& get() const = 0; virtual std::vector get_raw() = 0; + virtual std::vector> get_endorsements() + { + return {}; + } virtual ~AttestationInterface() = default; }; diff --git a/include/ccf/pal/attestation_sev_snp_endorsements.h b/include/ccf/pal/attestation_sev_snp_endorsements.h index 607807f9ee29..729a65c4e3bd 100644 --- a/include/ccf/pal/attestation_sev_snp_endorsements.h +++ b/include/ccf/pal/attestation_sev_snp_endorsements.h @@ -171,6 +171,25 @@ namespace ccf::pal::snp return server; } + static EndorsementEndpointsConfiguration::Server + make_amd_vlek_endorsements_server( + const HostPort& endpoint, + const ProductName& product_name, + size_t max_retries_count, + size_t max_client_response_size) + { + EndorsementEndpointsConfiguration::EndpointInfo chain{ + .host = endpoint.host, + .port = endpoint.port, + .uri = fmt::format("/vlek/v1/{}/cert_chain", to_string(product_name)), + .params = {}, + .headers = {}}; + chain.max_retries_count = max_retries_count; + chain.max_client_response_size = max_client_response_size; + + return {chain}; + } + static HostPort default_thim_endorsements_endpoint = { "169.254.169.254", "80"}; diff --git a/include/ccf/pal/snp_ioctl6.h b/include/ccf/pal/snp_ioctl6.h index 344febe028d4..fb3808ab3626 100644 --- a/include/ccf/pal/snp_ioctl6.h +++ b/include/ccf/pal/snp_ioctl6.h @@ -9,11 +9,16 @@ #include #include #include +#include #include +#include #include +#include #include #include #include +#include +#include // Based on the SEV-SNP ABI Spec document at // https://www.amd.com/system/files/TechDocs/56860.pdf @@ -122,6 +127,15 @@ namespace ccf::pal::snp::ioctl6 }; #pragma pack(pop) +#pragma pack(push, 1) + struct ExtendedAttestationReq + { + AttestationReq data; + uint64_t certs_address = 0; + uint32_t certs_len = 0; + }; // snp_ext_report_req in (linux) include/uapi/linux/sev-guest.h +#pragma pack(pop) + // Table 20 of the SEVSNP ABI constexpr uint8_t GUEST_FIELD_SELECT_GUEST_POLICY = 0b00000001; constexpr uint8_t GUEST_FIELD_SELECT_IMAGE_ID = 0b00000010; @@ -190,6 +204,8 @@ namespace ccf::pal::snp::ioctl6 using GuestRequestAttestation = GuestRequest; + using GuestRequestExtendedAttestation = + GuestRequest; using GuestRequestDerivedKey = GuestRequest; @@ -199,6 +215,206 @@ namespace ccf::pal::snp::ioctl6 _IOWR(SEV_GUEST_IOC_TYPE, 0x0, GuestRequestAttestation); constexpr int SEV_SNP_GUEST_MSG_DERIVED_KEY = _IOWR(SEV_GUEST_IOC_TYPE, 0x1, GuestRequestDerivedKey); + constexpr int SEV_SNP_GUEST_MSG_EXT_REPORT = + _IOWR(SEV_GUEST_IOC_TYPE, 0x2, GuestRequestExtendedAttestation); + + static constexpr uint32_t VMM_ERROR_INVALID_CERTIFICATE_PAGE_LENGTH = 1; + static constexpr size_t MAX_CERTIFICATE_TABLE_SIZE = 1024 * 1024; + +#pragma pack(push, 1) + struct CertificateTableEntry + { + std::array guid = {}; + uint32_t offset = 0; + uint32_t length = 0; + }; +#pragma pack(pop) + static_assert(sizeof(CertificateTableEntry) == 24); + + // Certificate table GUIDs are published either in the mixed-endian + // EFI_GUID layout or, as on AWS, in plain big-endian byte order, so both + // encodings of each GUID must be recognised. + static constexpr std::array VLEK_CERTIFICATE_GUID = { + 0xa8, + 0x07, + 0x4b, + 0xc2, + 0xa2, + 0x5a, + 0x48, + 0x3e, + 0xaa, + 0xe6, + 0x39, + 0xc0, + 0x45, + 0xa0, + 0xb8, + 0xa1}; + + static constexpr std::array VLEK_CERTIFICATE_GUID_MIXED_ENDIAN = + {0xc2, + 0x4b, + 0x07, + 0xa8, + 0x5a, + 0xa2, + 0x3e, + 0x48, + 0xaa, + 0xe6, + 0x39, + 0xc0, + 0x45, + 0xa0, + 0xb8, + 0xa1}; + + // Some hosts publish the endorsement key certificate under the VCEK GUID + // even when the report is VLEK-signed, so both slots must be considered. + static constexpr std::array VCEK_CERTIFICATE_GUID = { + 0x63, + 0xda, + 0x75, + 0x8d, + 0xe6, + 0x64, + 0x45, + 0x64, + 0xad, + 0xc5, + 0xf4, + 0xb9, + 0x3b, + 0xe8, + 0xac, + 0xcd}; + + static constexpr std::array VCEK_CERTIFICATE_GUID_MIXED_ENDIAN = + {0x8d, + 0x75, + 0xda, + 0x63, + 0x64, + 0xe6, + 0x64, + 0x45, + 0xad, + 0xc5, + 0xf4, + 0xb9, + 0x3b, + 0xe8, + 0xac, + 0xcd}; + + // Renders the bytes in the order they are stored, which is the canonical + // textual form for big-endian GUIDs. + static std::string format_certificate_guid( + const std::array& guid) + { + return fmt::format( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}" + "{:02x}{:02x}{:02x}{:02x}{:02x}", + guid[0], + guid[1], + guid[2], + guid[3], + guid[4], + guid[5], + guid[6], + guid[7], + guid[8], + guid[9], + guid[10], + guid[11], + guid[12], + guid[13], + guid[14], + guid[15]); + } + + using CertificateTable = + std::vector, std::vector>>; + + static CertificateTable parse_certificate_table( + std::span certificate_table) + { + size_t entry_offset = 0; + CertificateTable entries; + + while (true) + { + if ( + entry_offset > certificate_table.size() || + certificate_table.size() - entry_offset < sizeof(CertificateTableEntry)) + { + throw std::logic_error( + "SEV-SNP certificate table is missing its terminator"); + } + + CertificateTableEntry entry; + memcpy( + &entry, + certificate_table.data() + entry_offset, + sizeof(CertificateTableEntry)); + entry_offset += sizeof(CertificateTableEntry); + + if ( + std::all_of( + entry.guid.begin(), + entry.guid.end(), + [](uint8_t b) { return b == 0; }) && + entry.offset == 0 && entry.length == 0) + { + break; + } + + if ( + entry.offset > certificate_table.size() || + entry.length > certificate_table.size() - entry.offset) + { + throw std::logic_error( + "SEV-SNP certificate table entry is out of bounds"); + } + + entries.emplace_back( + entry.guid, + std::vector( + certificate_table.begin() + entry.offset, + certificate_table.begin() + entry.offset + entry.length)); + } + + return entries; + } + + static std::optional> extract_certificate( + const CertificateTable& entries, const std::array& guid) + { + std::optional> found = std::nullopt; + for (const auto& [entry_guid, certificate] : entries) + { + if (entry_guid != guid) + { + continue; + } + if (found.has_value()) + { + throw std::logic_error(fmt::format( + "SEV-SNP certificate table contains multiple {} certificates", + format_certificate_guid(guid))); + } + found = certificate; + } + return found; + } + + static std::optional> extract_vlek_certificate( + std::span certificate_table) + { + return extract_certificate( + parse_certificate_table(certificate_table), VLEK_CERTIFICATE_GUID); + } static inline bool supports_sev_snp() { @@ -209,6 +425,92 @@ namespace ccf::pal::snp::ioctl6 { IoctlSentinel resp_with_sentinel; PaddedAttestationResp& padded_resp = resp_with_sentinel.data; + std::vector> endorsements; + + void get_extended_report(int fd, const AttestationReq& report_req) + { + ExtendedAttestationReq ext_req = {.data = report_req}; + GuestRequestExtendedAttestation payload = { + .req_data = &ext_req, .resp_wrapper = &padded_resp, .exit_info = {0}}; + + int rc = ioctl(fd, SEV_SNP_GUEST_MSG_EXT_REPORT, &payload); + if ( + payload.exit_info.errors.fw != 0 || + (payload.exit_info.errors.vmm != 0 && + payload.exit_info.errors.vmm != + VMM_ERROR_INVALID_CERTIFICATE_PAGE_LENGTH) || + (rc < 0 && ext_req.certs_len == 0) || + (rc >= 0 && + payload.exit_info.errors.vmm != + VMM_ERROR_INVALID_CERTIFICATE_PAGE_LENGTH)) + { + throw std::logic_error(fmt::format( + "Failed to query SEV_SNP_GUEST_MSG_EXT_REPORT certificate table " + "size: {} fw_error: {} vmm_error: {}", + nonstd::strerror(errno), + payload.exit_info.errors.fw, + payload.exit_info.errors.vmm)); + } + if ( + ext_req.certs_len == 0 || + ext_req.certs_len > MAX_CERTIFICATE_TABLE_SIZE) + { + throw std::logic_error(fmt::format( + "Invalid SEV-SNP certificate table size: {}", ext_req.certs_len)); + } + + std::vector certificate_table(ext_req.certs_len); + ext_req.certs_address = + reinterpret_cast(certificate_table.data()); + payload.exit_info.whole = 0; + + rc = ioctl(fd, SEV_SNP_GUEST_MSG_EXT_REPORT, &payload); + if (rc < 0 || payload.exit_info.whole != 0) + { + throw std::logic_error(fmt::format( + "Failed to issue ioctl SEV_SNP_GUEST_MSG_EXT_REPORT: {} fw_error: " + "{} vmm_error: {}", + nonstd::strerror(errno), + payload.exit_info.errors.fw, + payload.exit_info.errors.vmm)); + } + + auto entries = parse_certificate_table(certificate_table); + std::optional> vek = std::nullopt; + // The report is VLEK-signed, so an endorsement key certificate + // published under the VCEK GUID is still the VLEK. + for (const auto& guid : + {VLEK_CERTIFICATE_GUID, + VLEK_CERTIFICATE_GUID_MIXED_ENDIAN, + VCEK_CERTIFICATE_GUID, + VCEK_CERTIFICATE_GUID_MIXED_ENDIAN}) + { + vek = extract_certificate(entries, guid); + if (vek.has_value()) + { + break; + } + } + if (!vek.has_value()) + { + std::vector guids; + guids.reserve(entries.size()); + for (const auto& [guid, certificate] : entries) + { + guids.emplace_back(fmt::format( + "{} ({} bytes)", + format_certificate_guid(guid), + certificate.size())); + } + throw std::logic_error(fmt::format( + "SEV-SNP certificate table does not contain a VLEK certificate. " + "Table is {} bytes and contains {} entries: [{}]", + certificate_table.size(), + entries.size(), + fmt::join(guids, ", "))); + } + endorsements.emplace_back(std::move(vek.value())); + } public: Attestation(const PlatformAttestationReportData& report_data) @@ -258,6 +560,18 @@ namespace ccf::pal::snp::ioctl6 throw std::logic_error( "SEV_SNP_GUEST_MSG_REPORT IOCTL overwrote safety sentinels."); } + + if ( + padded_resp.report.flags.signing_key == + snp::attestation_flags_signing_key_vlek) + { + get_extended_report(fd, req); + if (!resp_with_sentinel.sentinels_intact()) + { + throw std::logic_error( + "SEV_SNP_GUEST_MSG_EXT_REPORT IOCTL overwrote safety sentinels."); + } + } } [[nodiscard]] const snp::Attestation& get() const override @@ -270,6 +584,11 @@ namespace ccf::pal::snp::ioctl6 auto* quote_bytes = reinterpret_cast(&padded_resp.report); return {quote_bytes, quote_bytes + padded_resp.report_size}; } + + std::vector> get_endorsements() override + { + return endorsements; + } }; class DerivedKey diff --git a/src/node/node_state.h b/src/node/node_state.h index caf4357872a8..db49d83d523f 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -867,8 +867,16 @@ namespace ccf if (quote_info.format == QuoteFormat::amd_sev_snp_v1) { + const auto* quote = + reinterpret_cast( + quote_info.quote.data()); + const bool is_vlek = quote->flags.signing_key == + ccf::pal::snp::attestation_flags_signing_key_vlek; + // Use endorsements retrieved from file, if available - if (config.attestation.environment.snp_endorsements.has_value()) + if ( + config.attestation.environment.snp_endorsements.has_value() && + !is_vlek) { bool loaded_endorsements = false; try @@ -882,9 +890,6 @@ namespace ccf // Check that tcbm in endorsement matches reported TCB in our // retrieved attestation - const auto* quote = - reinterpret_cast( - quote_info.quote.data()); const auto reported_tcb = quote->reported_tcb; // tcbm is a single hex value, like DB18000000000004. To match @@ -960,7 +965,9 @@ namespace ccf } // On SEV-SNP, fetch endorsements from servers if specified quote_endorsements_client = std::make_shared( - endpoint_config, [this](std::vector&& endorsements) { + endpoint_config, + quote_info.endorsements, + [this](std::vector&& endorsements) { std::lock_guard guard(lock); quote_info.endorsements = std::move(endorsements); try diff --git a/src/node/quote_endorsements_client.h b/src/node/quote_endorsements_client.h index f93614bd88ea..7096a39336a0 100644 --- a/src/node/quote_endorsements_client.h +++ b/src/node/quote_endorsements_client.h @@ -338,8 +338,16 @@ namespace ccf QuoteEndorsementsClient( pal::snp::EndorsementEndpointsConfiguration config_, QuoteEndorsementsFetchedCallback cb) : + QuoteEndorsementsClient(std::move(config_), {}, std::move(cb)) + {} + + QuoteEndorsementsClient( + pal::snp::EndorsementEndpointsConfiguration config_, + std::vector initial_endorsements, + QuoteEndorsementsFetchedCallback cb) : config(std::move(config_)), - done_cb(std::move(cb)) + done_cb(std::move(cb)), + endorsements_pem(std::move(initial_endorsements)) {} void fetch_endorsements() diff --git a/src/pal/attestation.cpp b/src/pal/attestation.cpp index 8df0a1fbea34..7e403cb92069 100644 --- a/src/pal/attestation.cpp +++ b/src/pal/attestation.cpp @@ -197,9 +197,9 @@ namespace ccf::pal } std::optional> get_endorsed_chip_id_from_cert( - const crypto::Pem& vcek_leaf_cert) + const crypto::Pem& vek_leaf_cert) { - ccf::crypto::OpenSSL::Unique_BIO mem_bio(vcek_leaf_cert); + ccf::crypto::OpenSSL::Unique_BIO mem_bio(vek_leaf_cert); ccf::crypto::OpenSSL::Unique_X509 x509(mem_bio, true); const std::string chip_id_oid = "1.3.6.1.4.1.3704.1.4"; @@ -210,8 +210,6 @@ namespace ccf::pal int ext_index = X509_get_ext_by_OBJ(x509, chip_id_obj, -1); if (ext_index < 0) { - LOG_FAIL_FMT( - "Chip ID OID {} not present in VCEK certificate", chip_id_oid); return std::nullopt; } @@ -281,8 +279,8 @@ namespace ccf::pal } // ark_cert --signs--> ask_cert - // ask_cert --signs--> vcek_cert - auto vcek_cert = certificates[0]; + // ask_cert --signs--> vek_cert + auto vek_cert = certificates[0]; auto ask_cert = certificates[1]; auto ark_cert = certificates[2]; @@ -328,8 +326,8 @@ namespace ccf::pal "self signed as expected"); } - auto vcek_verifier = ccf::crypto::make_verifier(/* leaf */ vcek_cert); - if (!vcek_verifier->verify_certificate( + auto vek_verifier = ccf::crypto::make_verifier(/* leaf */ vek_cert); + if (!vek_verifier->verify_certificate( /* root */ {&ark_cert}, /* chain */ {&ask_cert})) { throw std::logic_error( @@ -360,22 +358,25 @@ namespace ccf::pal std::span quote_without_signature{ quote_info.quote.data(), quote_info.quote.size() - sizeof(quote.signature)}; - if (!vcek_verifier->verify(quote_without_signature, quote_signature)) + if (!vek_verifier->verify(quote_without_signature, quote_signature)) { throw std::logic_error( - "SEV-SNP: Chip certificate (VCEK) did not sign this attestation"); + "SEV-SNP: VEK certificate did not sign this attestation"); } // ---- Verify attestation report contents ---- - if (quote.flags.signing_key != snp::attestation_flags_signing_key_vcek) + const bool is_vcek = + quote.flags.signing_key == snp::attestation_flags_signing_key_vcek; + const bool is_vlek = + quote.flags.signing_key == snp::attestation_flags_signing_key_vlek; + if (!is_vcek && !is_vlek) { throw std::logic_error(fmt::format( - "SEV-SNP: Attestation report must be signed by VCEK: {}", + "SEV-SNP: Attestation report has unsupported signing key: {}", static_cast(quote.flags.signing_key))); } - // mask_chip_key if set means the operator set the vcek to 0s if (quote.flags.mask_chip_key != 0) { throw std::logic_error( @@ -409,7 +410,7 @@ namespace ccf::pal "enabled"); } - auto endorsed_tcb = get_endorsed_tcb_from_cert(product_family, vcek_cert); + auto endorsed_tcb = get_endorsed_tcb_from_cert(product_family, vek_cert); if (endorsed_tcb.has_value()) { auto endorsed_tcb_policy = endorsed_tcb->to_policy(product_family); @@ -425,21 +426,41 @@ namespace ccf::pal } } - auto endorsed_chip_id = get_endorsed_chip_id_from_cert(vcek_cert); - auto reported_chip_id = quote.get_chip_id_for_vcek(); - if ( - endorsed_chip_id.has_value() && - (endorsed_chip_id->size() != reported_chip_id.size() || - memcmp( - endorsed_chip_id->data(), - reported_chip_id.data(), - reported_chip_id.size()) != 0)) + auto endorsed_chip_id = get_endorsed_chip_id_from_cert(vek_cert); + if (is_vcek) { - throw std::logic_error(fmt::format( - "SEV-SNP: Chip ID in attestation does not match endorsed chip ID: {} " - "!= {}", - ccf::ds::to_hex(endorsed_chip_id.value()), - ccf::ds::to_hex(reported_chip_id))); + auto reported_chip_id = quote.get_chip_id_for_vcek(); + if ( + endorsed_chip_id.has_value() && + (endorsed_chip_id->size() != reported_chip_id.size() || + memcmp( + endorsed_chip_id->data(), + reported_chip_id.data(), + reported_chip_id.size()) != 0)) + { + throw std::logic_error(fmt::format( + "SEV-SNP: Chip ID in attestation does not match endorsed chip ID: " + "{} != {}", + ccf::ds::to_hex(endorsed_chip_id.value()), + ccf::ds::to_hex(reported_chip_id))); + } + } + else + { + if (endorsed_chip_id.has_value()) + { + throw std::logic_error( + "SEV-SNP: VLEK certificate unexpectedly contains a chip ID"); + } + if (!std::all_of( + std::begin(quote.chip_id), std::end(quote.chip_id), [](uint8_t b) { + return b == 0; + })) + { + throw std::logic_error( + "SEV-SNP: VLEK-signed attestation report contains a non-zero chip " + "ID"); + } } if (quote_info.endorsed_tcb.has_value()) diff --git a/src/pal/quote_generation.h b/src/pal/quote_generation.h index 72ec00b8ddea..a5d8b0ab3ee7 100644 --- a/src/pal/quote_generation.h +++ b/src/pal/quote_generation.h @@ -101,6 +101,12 @@ namespace ccf::pal } node_quote_info.quote = attestation->get_raw(); + for (const auto& endorsement : attestation->get_endorsements()) + { + const auto pem = ccf::crypto::cert_der_to_pem(endorsement).raw(); + node_quote_info.endorsements.insert( + node_quote_info.endorsements.end(), pem.begin(), pem.end()); + } if (endorsement_cb != nullptr) { diff --git a/src/pal/test/snp_attestation_validation.cpp b/src/pal/test/snp_attestation_validation.cpp index afc8d026418e..bff62b46dad8 100644 --- a/src/pal/test/snp_attestation_validation.cpp +++ b/src/pal/test/snp_attestation_validation.cpp @@ -12,6 +12,7 @@ #include "ccf/pal/measurement.h" #include "ccf/pal/report_data.h" #include "ccf/pal/sev_snp_cpuid.h" +#include "ccf/pal/snp_ioctl6.h" #include "crypto/openssl/hash.h" #include "pal/test/attestation.h" #include "pal/test/attestation_sev_snp_endorsements.h" @@ -517,6 +518,135 @@ TEST_CASE("Quote endorsements generation for v2 attestation version fails") "version is 3"); } +TEST_CASE("VLEK endorsement endpoint generation") +{ + auto quote = *reinterpret_cast( + ccf::pal::snp::testing::genoa_attestation.data()); + quote.flags.signing_key = ccf::pal::snp::attestation_flags_signing_key_vlek; + std::fill(std::begin(quote.chip_id), std::end(quote.chip_id), 0); + + auto config = ccf::pal::snp::make_endorsement_endpoint_configuration( + quote, + {{ + ccf::pal::snp::EndorsementsEndpointType::AMD, + "invalid.amd.com:12345", + }}); + + REQUIRE(config.servers.size() == 1); + REQUIRE(config.servers.front().size() == 1); + const auto& endpoint = config.servers.front().front(); + CHECK_EQ(endpoint.host, "invalid.amd.com"); + CHECK_EQ(endpoint.port, "12345"); + CHECK_EQ(endpoint.uri, "/vlek/v1/Genoa/cert_chain"); + CHECK(endpoint.params.empty()); + CHECK_FALSE(endpoint.response_is_der); + + CHECK_THROWS_WITH( + ccf::pal::snp::make_endorsement_endpoint_configuration( + quote, {{ccf::pal::snp::EndorsementsEndpointType::Azure}}), + "Azure endorsements endpoints do not support VLEK-signed attestation " + "reports"); + CHECK_THROWS_WITH( + ccf::pal::snp::make_endorsement_endpoint_configuration( + quote, {{ccf::pal::snp::EndorsementsEndpointType::THIM}}), + "THIM endorsements endpoints do not support VLEK-signed attestation " + "reports"); +} + +TEST_CASE("VLEK certificate table parsing") +{ + using namespace ccf::pal::snp::ioctl6; + + static constexpr std::array certificate = {1, 2, 3, 4}; + // Reserve room for two entries plus a terminator, so that tables with a + // duplicated entry remain correctly terminated. + static constexpr uint32_t certificate_offset = + 3 * sizeof(CertificateTableEntry); + CertificateTableEntry vlek_entry = { + .guid = VLEK_CERTIFICATE_GUID, + .offset = certificate_offset, + .length = certificate.size()}; + CertificateTableEntry terminator = {}; + + std::vector table( + certificate_offset + certificate.size(), static_cast(0)); + memcpy(table.data(), &vlek_entry, sizeof(vlek_entry)); + memcpy( + table.data() + 2 * sizeof(CertificateTableEntry), + &terminator, + sizeof(terminator)); + std::copy( + certificate.begin(), certificate.end(), table.begin() + certificate_offset); + + auto extracted = extract_vlek_certificate(table); + REQUIRE(extracted.has_value()); + CHECK(std::equal(certificate.begin(), certificate.end(), extracted->begin())); + + { + INFO("An endorsement key published under the VCEK GUID is still usable"); + auto vcek_slot = table; + auto vcek_entry = vlek_entry; + vcek_entry.guid = VCEK_CERTIFICATE_GUID; + memcpy(vcek_slot.data(), &vcek_entry, sizeof(vcek_entry)); + + auto entries = parse_certificate_table(vcek_slot); + CHECK_FALSE( + extract_certificate(entries, VLEK_CERTIFICATE_GUID).has_value()); + auto from_vcek_slot = extract_certificate(entries, VCEK_CERTIFICATE_GUID); + REQUIRE(from_vcek_slot.has_value()); + CHECK(std::equal( + certificate.begin(), certificate.end(), from_vcek_slot->begin())); + } + + { + INFO("GUIDs published in mixed-endian byte order are also recognised"); + auto mixed_endian = table; + auto mixed_entry = vlek_entry; + mixed_entry.guid = VLEK_CERTIFICATE_GUID_MIXED_ENDIAN; + memcpy(mixed_endian.data(), &mixed_entry, sizeof(mixed_entry)); + + auto entries = parse_certificate_table(mixed_endian); + CHECK_FALSE( + extract_certificate(entries, VLEK_CERTIFICATE_GUID).has_value()); + auto from_mixed_endian = + extract_certificate(entries, VLEK_CERTIFICATE_GUID_MIXED_ENDIAN); + REQUIRE(from_mixed_endian.has_value()); + CHECK(std::equal( + certificate.begin(), certificate.end(), from_mixed_endian->begin())); + } + + { + INFO("Both encodings of a GUID render as the same identifier"); + CHECK_EQ( + format_certificate_guid(VLEK_CERTIFICATE_GUID), + "a8074bc2-a25a-483e-aae6-39c045a0b8a1"); + CHECK_EQ( + format_certificate_guid(VCEK_CERTIFICATE_GUID), + "63da758d-e664-4564-adc5-f4b93be8accd"); + } + + auto duplicate = table; + memcpy( + duplicate.data() + sizeof(vlek_entry), &vlek_entry, sizeof(vlek_entry)); + CHECK_THROWS_WITH( + extract_vlek_certificate(duplicate), + "SEV-SNP certificate table contains multiple " + "a8074bc2-a25a-483e-aae6-39c045a0b8a1 certificates"); + + auto out_of_bounds = vlek_entry; + out_of_bounds.offset = table.size(); + out_of_bounds.length = 1; + memcpy(table.data(), &out_of_bounds, sizeof(out_of_bounds)); + CHECK_THROWS_WITH( + extract_vlek_certificate(table), + "SEV-SNP certificate table entry is out of bounds"); + + std::vector unterminated(sizeof(CertificateTableEntry), 1); + CHECK_THROWS_WITH( + extract_vlek_certificate(unterminated), + "SEV-SNP certificate table entry is out of bounds"); +} + TEST_CASE("Extracting metadata from endorsements") { using namespace ccf;