Skip to content

Commit 0f87e96

Browse files
Add canary test (#7328)
Co-authored-by: Amaury Chamayou <[email protected]>
1 parent a9da025 commit 0f87e96

File tree

8 files changed

+380
-2
lines changed

8 files changed

+380
-2
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [7.0.0-dev4]
9+
10+
[7.0.0-dev4]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.0-dev4
11+
12+
### Added
13+
14+
- Added `verify_uvm_attestation_and_endorsements` binary. This tests that the authentication of the startup files during start and join would succeed. Usage on C-ACI: `verify_uvm_attestation_and_endorsements /security-context-xxxx/host-amd-cert-base64 /security-context-xxxx/reference-info-base64 /security-context-xxxx/security-policy-base64`
15+
816
## [7.0.0-dev3]
917

1018
[7.0.0-dev3]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.0-dev3

CMakeLists.txt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,11 @@ install(
222222
)
223223

224224
# CCF platform agnostic library
225-
add_ccf_static_library(ccf_pal SRCS ${CCF_DIR}/src/pal/attestation.cpp)
225+
add_ccf_static_library(
226+
ccf_pal
227+
SRCS ${CCF_DIR}/src/pal/attestation.cpp
228+
LINK_LIBS ccfcrypto
229+
)
226230

227231
# CCF js lib
228232
add_ccf_static_library(
@@ -464,11 +468,36 @@ install(FILES tests/requirements.txt DESTINATION bin)
464468
# Add sample apps
465469
add_subdirectory(${CCF_DIR}/samples)
466470

471+
# UVM canary executable
472+
add_test_bin(
473+
verify_uvm_attestation_and_endorsements
474+
${CMAKE_CURRENT_SOURCE_DIR}/src/pal/test/verify_uvm_attestation_and_endorsements.cpp
475+
${CMAKE_CURRENT_SOURCE_DIR}/src/host/env.cpp
476+
)
477+
target_link_libraries(
478+
verify_uvm_attestation_and_endorsements PRIVATE ccf_pal ccf
479+
)
480+
install(TARGETS verify_uvm_attestation_and_endorsements DESTINATION bin)
481+
467482
if(BUILD_TESTS)
468483
enable_testing()
469484

470485
# Unit tests
471486
if(BUILD_UNIT_TESTS)
487+
add_test(
488+
NAME verify_uvm_attestation_and_endorsements
489+
COMMAND
490+
bash
491+
${CMAKE_SOURCE_DIR}/tests/run_verify_uvm_attestation_and_endorsements.sh
492+
$<TARGET_FILE:verify_uvm_attestation_and_endorsements>
493+
)
494+
set_property(
495+
TEST verify_uvm_attestation_and_endorsements
496+
APPEND
497+
PROPERTY LABELS unit
498+
)
499+
add_san_test_properties(verify_uvm_attestation_and_endorsements)
500+
472501
add_unit_test(
473502
snp_ioctl_test
474503
${CMAKE_CURRENT_SOURCE_DIR}/src/pal/test/snp_ioctl_test.cpp

doc/operations/platforms/snp.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,15 @@ The parsed TCB version mapped to that cpuid in the :ref:`audit/builtin_maps:``no
259259
| FMC | 85 | 0x55 |
260260
+-------------------+-----+------------+
261261

262+
Testing CCF's attestation validation
263+
-----------------------------------------------------
264+
265+
After installing CCF, the lightweight `verify_uvm_attestation_and_endorsements` binary is available in the CCF installation directory.
266+
267+
When `verify_uvm_attestation_and_endorsements host-amd-cert-base64 reference-info-base64 security-policy-base64` is run, this mirrors the authentication of the attestation and endorsements during a CCF node's startup and join, and will return an error if any of the files are invalid.
268+
269+
On C-ACI these files are available in the security context directory.
270+
262271
.. rubric:: Footnotes
263272

264273
.. [#security_policy] A `REGO <https://www.openpolicyagent.org/docs/latest/policy-language/>`_ policy checked by the utility VM (UVM) against the container.

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "ccf"
7-
version = "7.0.0-dev3"
7+
version = "7.0.0-dev4"
88
authors = [
99
{ name="CCF Team", email="[email protected]" },
1010
]

src/pal/quote_generation.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
#pragma once
44

55
#include "ccf/crypto/hash_provider.h"
6+
#include "ccf/ds/quote_info.h"
7+
#include "ccf/pal/attestation.h"
68
#include "ccf/pal/attestation_sev_snp.h"
9+
#include "ccf/pal/platform.h"
710
#include "ccf/pal/snp_ioctl.h"
811
#include "ds/files.h"
912

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the Apache 2.0 License.
3+
4+
#include "verify_uvm_attestation_and_endorsements.h"
5+
6+
#include "ccf/ds/quote_info.h"
7+
#include "node/uvm_endorsements.h"
8+
9+
std::string read_in(const std::string& path)
10+
{
11+
auto expanded_path = ccf::env::expand_envvars_in_path(path);
12+
LOG_TRACE_FMT("Reading from: {}", expanded_path);
13+
auto str_opt = files::try_slurp_string(expanded_path);
14+
if (!str_opt.has_value())
15+
{
16+
throw std::invalid_argument("Could not read from: " + expanded_path);
17+
}
18+
return str_opt.value();
19+
}
20+
21+
void validate_endorsements(
22+
const std::string& snp_endorsements,
23+
const ccf::pal::snp::TcbVersionRaw& attested_tcb,
24+
std::vector<uint8_t>& endorsements_pem)
25+
{
26+
const auto raw_data = ccf::crypto::raw_from_b64(snp_endorsements);
27+
28+
const auto j = nlohmann::json::parse(raw_data);
29+
const auto aci_endorsements = j.get<ccf::pal::snp::ACIReportEndorsements>();
30+
31+
// tcbm is a single hex value, like DB18000000000004. To match
32+
// that with a TcbVersion, reverse the bytes.
33+
const auto* tcb_begin = reinterpret_cast<const uint8_t*>(&attested_tcb);
34+
const std::span<const uint8_t> tcb_bytes{
35+
tcb_begin, tcb_begin + sizeof(attested_tcb)};
36+
auto tcb_as_hex =
37+
fmt::format("{:02x}", fmt::join(tcb_bytes.rbegin(), tcb_bytes.rend(), ""));
38+
ccf::nonstd::to_upper(tcb_as_hex);
39+
40+
if (tcb_as_hex == aci_endorsements.tcbm)
41+
{
42+
LOG_INFO_FMT(
43+
"Using SNP endorsements loaded from file, endorsing TCB {}", tcb_as_hex);
44+
}
45+
else
46+
{
47+
throw std::runtime_error(fmt::format(
48+
"SNP endorsements loaded from disk contained tcbm {}, which does not "
49+
"match reported TCB of current attestation {}. ",
50+
aci_endorsements.tcbm,
51+
tcb_as_hex));
52+
}
53+
54+
endorsements_pem.clear();
55+
endorsements_pem.insert(
56+
endorsements_pem.end(),
57+
aci_endorsements.vcek_cert.begin(),
58+
aci_endorsements.vcek_cert.end());
59+
endorsements_pem.insert(
60+
endorsements_pem.end(),
61+
aci_endorsements.certificate_chain.begin(),
62+
aci_endorsements.certificate_chain.end());
63+
}
64+
65+
// Verify that the security policy matches the quoted digest of the policy
66+
void validate_security_policy(
67+
const ccf::QuoteInfo& quote_info, const std::string& security_policy)
68+
{
69+
auto quoted_digest = ccf::AttestationProvider::get_host_data(quote_info);
70+
if (!quoted_digest.has_value())
71+
{
72+
throw std::logic_error("Unable to find host data in attestation");
73+
}
74+
75+
auto security_policy_digest =
76+
quote_info.format == ccf::QuoteFormat::amd_sev_snp_v1 ?
77+
ccf::crypto::Sha256Hash(ccf::crypto::raw_from_b64(security_policy)) :
78+
ccf::crypto::Sha256Hash(security_policy);
79+
if (security_policy_digest != quoted_digest.value())
80+
{
81+
throw std::logic_error(fmt::format(
82+
"Digest of decoded security policy \"{}\" {} does not match "
83+
"attestation host data {}",
84+
security_policy,
85+
security_policy_digest.hex_str(),
86+
quoted_digest.value().hex_str()));
87+
}
88+
LOG_INFO_FMT(
89+
"Successfully verified attested security policy {}",
90+
security_policy_digest);
91+
}
92+
93+
void validate_uvm_endorsements(
94+
ccf::QuoteInfo& quote_info, const std::string& uvm_endorsements)
95+
{
96+
try
97+
{
98+
auto uvm_endorsements_raw = ccf::crypto::raw_from_b64(uvm_endorsements);
99+
auto snp_uvm_endorsements = ccf::pal::verify_uvm_endorsements_descriptor(
100+
uvm_endorsements_raw,
101+
ccf::AttestationProvider::get_measurement(quote_info).value());
102+
LOG_INFO_FMT(
103+
"Successfully verified attested UVM endorsements: {}",
104+
snp_uvm_endorsements.to_str());
105+
quote_info.uvm_endorsements = uvm_endorsements_raw;
106+
}
107+
catch (const std::exception& e)
108+
{
109+
throw std::logic_error(
110+
fmt::format("Error verifying UVM endorsements: {}", e.what()));
111+
}
112+
}
113+
114+
// CCF's join policy both authenticates the quote and checks it against
115+
// reference values in the CCF store. Here we only authenticate the quote.
116+
// Each step that would check against the store is commented out and replaced
117+
// with the steps which do not require the store.
118+
ccf::QuoteVerificationResult validate_join_policy(
119+
ccf::QuoteInfo quote_info, std::vector<uint8_t> expected_pubk_der)
120+
{
121+
ccf::pal::PlatformAttestationMeasurement measurement = {};
122+
ccf::crypto::Sha256Hash quoted_hash;
123+
ccf::pal::PlatformAttestationReportData report_data;
124+
try
125+
{
126+
ccf::pal::verify_quote(quote_info, measurement, report_data);
127+
quoted_hash = report_data.to_sha256_hash();
128+
}
129+
catch (const std::exception& e)
130+
{
131+
LOG_FAIL_FMT("Failed to verify attestation report: {}", e.what());
132+
return ccf::QuoteVerificationResult::Failed;
133+
}
134+
135+
// auto rc = verify_host_data_against_store(tx, quote_info);
136+
137+
// rc = verify_enclave_measurement_against_store(
138+
ccf::verify_uvm_endorsements_against_roots_of_trust(
139+
quote_info.uvm_endorsements.value(),
140+
measurement,
141+
ccf::default_uvm_roots_of_trust);
142+
143+
// rc = verify_tcb_version_against_store(tx, quote_info);
144+
145+
auto rc = ccf::verify_quoted_node_public_key(expected_pubk_der, quoted_hash);
146+
147+
return rc;
148+
}
149+
150+
int main(int argc, char** argv)
151+
{
152+
if (!ccf::pal::snp::supports_sev_snp())
153+
{
154+
std::cout << "Skipping test as this is not running in SEV-SNP" << std::endl;
155+
return 0;
156+
}
157+
ccf::logger::config::level() = ccf::LoggerLevel::TRACE;
158+
ccf::logger::config::add_text_console_logger();
159+
160+
if (argc < 2 || argc > 4)
161+
{
162+
std::cout << "Usage <program> <snp_endorsements_path> "
163+
"[uvm_endorsements_path] [security_policy_path]"
164+
<< std::endl;
165+
return 1;
166+
}
167+
168+
const std::string endorsements_path = argv[1];
169+
170+
std::optional<std::string> uvm_endorsements_path;
171+
if (argc >= 3)
172+
{
173+
uvm_endorsements_path = argv[2];
174+
}
175+
176+
std::optional<std::string> security_policy_path = std::nullopt;
177+
if (argc >= 4)
178+
{
179+
security_policy_path = argv[3];
180+
}
181+
182+
try
183+
{
184+
LOG_INFO_FMT("Reading SNP endorsements from: {}", endorsements_path);
185+
std::string endorsements = read_in(endorsements_path);
186+
187+
LOG_INFO_FMT(
188+
"Reading SNP UVM endorsements from: {}", uvm_endorsements_path);
189+
std::optional<std::string> uvm_endorsements = std::nullopt;
190+
if (uvm_endorsements_path.has_value())
191+
{
192+
uvm_endorsements = read_in(uvm_endorsements_path.value());
193+
}
194+
else
195+
{
196+
LOG_INFO_FMT("No UVM endorsements provided, skipping");
197+
}
198+
199+
std::optional<std::string> security_policy = std::nullopt;
200+
if (security_policy_path.has_value())
201+
{
202+
LOG_INFO_FMT(
203+
"Reading SNP security policy from: {}", security_policy_path);
204+
security_policy = read_in(security_policy_path.value());
205+
}
206+
else
207+
{
208+
LOG_INFO_FMT("No SNP security policy provided, skipping");
209+
}
210+
211+
LOG_INFO_FMT("Generating attestation");
212+
213+
// generate private key
214+
ccf::crypto::KeyPair_OpenSSL node_sign_kp(ccf::crypto::CurveID::SECP384R1);
215+
ccf::pal::PlatformAttestationReportData report_data =
216+
ccf::crypto::Sha256Hash(node_sign_kp.public_key_der());
217+
ccf::pal::generate_quote(
218+
report_data,
219+
[&](
220+
const ccf::QuoteInfo& qi,
221+
const ccf::pal::snp::EndorsementEndpointsConfiguration&
222+
/*endpoint_config*/) {
223+
ccf::QuoteInfo quote_info = qi;
224+
225+
CCF_ASSERT_FMT(
226+
quote_info.format == ccf::QuoteFormat::amd_sev_snp_v1,
227+
"Expected SNP quote format");
228+
229+
LOG_INFO_FMT("Verifying endorsements");
230+
const auto* attestation_unverified =
231+
reinterpret_cast<const ccf::pal::snp::Attestation*>(
232+
quote_info.quote.data());
233+
validate_endorsements(
234+
endorsements,
235+
attestation_unverified->reported_tcb,
236+
quote_info.endorsements);
237+
238+
LOG_INFO_FMT("Verifying quote");
239+
ccf::pal::PlatformAttestationMeasurement d = {};
240+
ccf::pal::PlatformAttestationReportData r = {};
241+
ccf::pal::verify_quote(quote_info, d, r);
242+
243+
LOG_INFO_FMT("Verifying security policy");
244+
if (security_policy.has_value())
245+
{
246+
validate_security_policy(quote_info, security_policy.value());
247+
}
248+
249+
LOG_INFO_FMT("Verifying UVM endorsements");
250+
if (uvm_endorsements.has_value())
251+
{
252+
validate_uvm_endorsements(quote_info, uvm_endorsements.value());
253+
}
254+
255+
LOG_INFO_FMT("Running join policy validation");
256+
auto rc =
257+
validate_join_policy(quote_info, node_sign_kp.public_key_der());
258+
if (rc != ccf::QuoteVerificationResult::Verified)
259+
{
260+
throw std::logic_error(
261+
fmt::format("Join policy validation failed: {}", (int)rc));
262+
}
263+
},
264+
{});
265+
}
266+
catch (const std::invalid_argument& e)
267+
{
268+
LOG_FAIL_FMT("Error: {}", e.what());
269+
return 2;
270+
}
271+
LOG_INFO_FMT("Successfully verified attestation and endorsements.");
272+
return 0;
273+
}

0 commit comments

Comments
 (0)