Skip to content

Commit 5a41a95

Browse files
Copilotachamayou
andauthored
On recovery, set UVM descriptor SVN to minimum of existing KV value and startup endorsements (#7717)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com> Co-authored-by: Amaury Chamayou <amchamay@microsoft.com> Co-authored-by: Amaury Chamayou <amaury@xargs.fr>
1 parent a6f44e1 commit 5a41a95

File tree

8 files changed

+346
-26
lines changed

8 files changed

+346
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1818

1919
### Changed
2020

21+
- On recovery, the UVM descriptor SVN is now set to the minimum of the previously stored value in the KV and the value found in the new node's startup endorsements. On start, the behaviour is unchanged (#7716).
2122
- Refactored the user facing surface of self-healing-open and local sealing. The whole feature is now `sealing-recovery` with `self-healing-open` now referred to as the `recovery-decision-protocol`. (#7679)
2223
- Local sealing is enabled by setting the `sealing-recovery` config field (for both the sealing node, and the unsealing recovery node)
2324
- The local sealing identity is under `sealing-recovery.location.name`

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,13 @@ if(BUILD_TESTS)
819819
ccf_tasks
820820
)
821821

822+
add_unit_test(
823+
internal_tables_access_test
824+
${CMAKE_CURRENT_SOURCE_DIR}/src/node/rpc/test/internal_tables_access_test.cpp
825+
${CCF_DIR}/src/node/uvm_endorsements.cpp
826+
)
827+
target_link_libraries(internal_tables_access_test PRIVATE ccfcrypto ccf_kv)
828+
822829
add_unit_test(
823830
merkle_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/merkle_test.cpp
824831
)

src/node/rpc/node_frontend.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1615,7 +1615,7 @@ namespace ccf
16151615
ctx.tx, host_data, in.snp_security_policy);
16161616

16171617
InternalTablesAccess::trust_node_uvm_endorsements(
1618-
ctx.tx, in.snp_uvm_endorsements);
1618+
ctx.tx, in.snp_uvm_endorsements, recovering);
16191619

16201620
auto attestation =
16211621
AttestationProvider::get_snp_attestation(in.quote_info).value();
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the Apache 2.0 License.
3+
4+
#include "ccf/app_interface.h"
5+
#include "ccf/service/tables/host_data.h"
6+
#include "ccf/service/tables/service.h"
7+
#include "service/tables/config.h"
8+
#include "service/tables/signatures.h"
9+
10+
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
11+
12+
#include "kv/store.h"
13+
#include "kv/test/null_encryptor.h"
14+
#include "service/internal_tables_access.h"
15+
16+
#include <doctest/doctest.h>
17+
18+
using namespace ccf;
19+
20+
TEST_CASE("trust_node_uvm_endorsements - not recovering, empty map")
21+
{
22+
ccf::kv::Store kv_store;
23+
auto encryptor = std::make_shared<ccf::kv::NullTxEncryptor>();
24+
kv_store.set_encryptor(encryptor);
25+
26+
SNPUVMEndorsements table(Tables::NODE_SNP_UVM_ENDORSEMENTS);
27+
28+
pal::UVMEndorsements endorsement{"did:x509:test", "test-feed", "42"};
29+
30+
{
31+
auto tx = kv_store.create_tx();
32+
InternalTablesAccess::trust_node_uvm_endorsements(
33+
tx, endorsement, false /* recovering */);
34+
REQUIRE(tx.commit() == ccf::kv::CommitResult::SUCCESS);
35+
}
36+
37+
{
38+
auto tx = kv_store.create_read_only_tx();
39+
auto handle = tx.ro(table);
40+
auto result = handle->get("did:x509:test");
41+
REQUIRE(result.has_value());
42+
REQUIRE(result->size() == 1);
43+
auto it = result->find("test-feed");
44+
REQUIRE(it != result->end());
45+
REQUIRE(it->second.svn == "42");
46+
}
47+
}
48+
49+
TEST_CASE("trust_node_uvm_endorsements - recovering, new DID not in map")
50+
{
51+
ccf::kv::Store kv_store;
52+
auto encryptor = std::make_shared<ccf::kv::NullTxEncryptor>();
53+
kv_store.set_encryptor(encryptor);
54+
55+
SNPUVMEndorsements table(Tables::NODE_SNP_UVM_ENDORSEMENTS);
56+
57+
// Pre-populate with an existing DID
58+
{
59+
auto tx = kv_store.create_tx();
60+
auto handle = tx.rw(table);
61+
FeedToEndorsementsDataMap existing;
62+
existing["existing-feed"] = {"100"};
63+
handle->put("did:x509:existing", existing);
64+
REQUIRE(tx.commit() == ccf::kv::CommitResult::SUCCESS);
65+
}
66+
67+
// Call with a different DID while recovering
68+
pal::UVMEndorsements endorsement{"did:x509:new", "new-feed", "50"};
69+
70+
{
71+
auto tx = kv_store.create_tx();
72+
InternalTablesAccess::trust_node_uvm_endorsements(
73+
tx, endorsement, true /* recovering */);
74+
REQUIRE(tx.commit() == ccf::kv::CommitResult::SUCCESS);
75+
}
76+
77+
// Verify new DID was written
78+
{
79+
auto tx = kv_store.create_read_only_tx();
80+
auto handle = tx.ro(table);
81+
82+
auto new_result = handle->get("did:x509:new");
83+
REQUIRE(new_result.has_value());
84+
REQUIRE(new_result->size() == 1);
85+
auto it = new_result->find("new-feed");
86+
REQUIRE(it != new_result->end());
87+
REQUIRE(it->second.svn == "50");
88+
89+
// Prior contents unchanged
90+
auto existing_result = handle->get("did:x509:existing");
91+
REQUIRE(existing_result.has_value());
92+
REQUIRE(existing_result->size() == 1);
93+
auto eit = existing_result->find("existing-feed");
94+
REQUIRE(eit != existing_result->end());
95+
REQUIRE(eit->second.svn == "100");
96+
}
97+
}
98+
99+
TEST_CASE("trust_node_uvm_endorsements - recovering, existing DID, new feed")
100+
{
101+
ccf::kv::Store kv_store;
102+
auto encryptor = std::make_shared<ccf::kv::NullTxEncryptor>();
103+
kv_store.set_encryptor(encryptor);
104+
105+
SNPUVMEndorsements table(Tables::NODE_SNP_UVM_ENDORSEMENTS);
106+
107+
// Pre-populate with an existing DID and feed
108+
{
109+
auto tx = kv_store.create_tx();
110+
auto handle = tx.rw(table);
111+
FeedToEndorsementsDataMap existing;
112+
existing["feed-A"] = {"100"};
113+
handle->put("did:x509:shared", existing);
114+
REQUIRE(tx.commit() == ccf::kv::CommitResult::SUCCESS);
115+
}
116+
117+
// Call with the same DID but a different feed while recovering
118+
pal::UVMEndorsements endorsement{"did:x509:shared", "feed-B", "75"};
119+
120+
{
121+
auto tx = kv_store.create_tx();
122+
InternalTablesAccess::trust_node_uvm_endorsements(
123+
tx, endorsement, true /* recovering */);
124+
REQUIRE(tx.commit() == ccf::kv::CommitResult::SUCCESS);
125+
}
126+
127+
// Verify both feeds are present
128+
{
129+
auto tx = kv_store.create_read_only_tx();
130+
auto handle = tx.ro(table);
131+
132+
auto result = handle->get("did:x509:shared");
133+
REQUIRE(result.has_value());
134+
REQUIRE(result->size() == 2);
135+
136+
auto it_a = result->find("feed-A");
137+
REQUIRE(it_a != result->end());
138+
REQUIRE(it_a->second.svn == "100");
139+
140+
auto it_b = result->find("feed-B");
141+
REQUIRE(it_b != result->end());
142+
REQUIRE(it_b->second.svn == "75");
143+
}
144+
}
145+
146+
TEST_CASE(
147+
"trust_node_uvm_endorsements - recovering, existing DID and feed, lower SVN")
148+
{
149+
ccf::kv::Store kv_store;
150+
auto encryptor = std::make_shared<ccf::kv::NullTxEncryptor>();
151+
kv_store.set_encryptor(encryptor);
152+
153+
SNPUVMEndorsements table(Tables::NODE_SNP_UVM_ENDORSEMENTS);
154+
155+
// Pre-populate with SVN 100, plus a separate unrelated DID
156+
{
157+
auto tx = kv_store.create_tx();
158+
auto handle = tx.rw(table);
159+
FeedToEndorsementsDataMap existing;
160+
existing["the-feed"] = {"100"};
161+
handle->put("did:x509:the-did", existing);
162+
163+
FeedToEndorsementsDataMap other;
164+
other["other-feed"] = {"999"};
165+
handle->put("did:x509:other-did", other);
166+
REQUIRE(tx.commit() == ccf::kv::CommitResult::SUCCESS);
167+
}
168+
169+
// Call with strictly lower SVN while recovering
170+
pal::UVMEndorsements endorsement{"did:x509:the-did", "the-feed", "42"};
171+
172+
{
173+
auto tx = kv_store.create_tx();
174+
InternalTablesAccess::trust_node_uvm_endorsements(
175+
tx, endorsement, true /* recovering */);
176+
REQUIRE(tx.commit() == ccf::kv::CommitResult::SUCCESS);
177+
}
178+
179+
// SVN should be updated to the lower value
180+
{
181+
auto tx = kv_store.create_read_only_tx();
182+
auto handle = tx.ro(table);
183+
184+
auto result = handle->get("did:x509:the-did");
185+
REQUIRE(result.has_value());
186+
REQUIRE(result->size() == 1);
187+
auto it = result->find("the-feed");
188+
REQUIRE(it != result->end());
189+
REQUIRE(it->second.svn == "42");
190+
191+
// Pre-existing unrelated DID is unchanged
192+
auto other_result = handle->get("did:x509:other-did");
193+
REQUIRE(other_result.has_value());
194+
REQUIRE(other_result->size() == 1);
195+
auto oit = other_result->find("other-feed");
196+
REQUIRE(oit != other_result->end());
197+
REQUIRE(oit->second.svn == "999");
198+
}
199+
}
200+
201+
TEST_CASE(
202+
"trust_node_uvm_endorsements - recovering, existing DID and feed, higher "
203+
"SVN")
204+
{
205+
ccf::kv::Store kv_store;
206+
auto encryptor = std::make_shared<ccf::kv::NullTxEncryptor>();
207+
kv_store.set_encryptor(encryptor);
208+
209+
SNPUVMEndorsements table(Tables::NODE_SNP_UVM_ENDORSEMENTS);
210+
211+
// Pre-populate with SVN 42
212+
{
213+
auto tx = kv_store.create_tx();
214+
auto handle = tx.rw(table);
215+
FeedToEndorsementsDataMap existing;
216+
existing["the-feed"] = {"42"};
217+
handle->put("did:x509:the-did", existing);
218+
REQUIRE(tx.commit() == ccf::kv::CommitResult::SUCCESS);
219+
}
220+
221+
// Call with strictly higher SVN while recovering
222+
pal::UVMEndorsements endorsement{"did:x509:the-did", "the-feed", "100"};
223+
224+
{
225+
auto tx = kv_store.create_tx();
226+
InternalTablesAccess::trust_node_uvm_endorsements(
227+
tx, endorsement, true /* recovering */);
228+
REQUIRE(tx.commit() == ccf::kv::CommitResult::SUCCESS);
229+
}
230+
231+
// Map should be unchanged - SVN stays at 42
232+
{
233+
auto tx = kv_store.create_read_only_tx();
234+
auto handle = tx.ro(table);
235+
236+
auto result = handle->get("did:x509:the-did");
237+
REQUIRE(result.has_value());
238+
REQUIRE(result->size() == 1);
239+
auto it = result->find("the-feed");
240+
REQUIRE(it != result->end());
241+
REQUIRE(it->second.svn == "42");
242+
}
243+
}

src/node/uvm_endorsements.cpp

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,27 @@
99

1010
namespace ccf
1111
{
12+
size_t parse_svn(const std::string& svn_str)
13+
{
14+
size_t svn = 0;
15+
auto result =
16+
std::from_chars(svn_str.data(), svn_str.data() + svn_str.size(), svn);
17+
if (result.ec != std::errc())
18+
{
19+
throw std::runtime_error(
20+
fmt::format("Unable to parse svn value {} to unsigned", svn_str));
21+
}
22+
return svn;
23+
}
24+
1225
bool matches_uvm_roots_of_trust(
1326
const pal::UVMEndorsements& endorsements,
1427
const std::vector<pal::UVMEndorsements>& uvm_roots_of_trust)
1528
{
1629
return std::ranges::any_of(
1730
uvm_roots_of_trust, [&](const auto& uvm_root_of_trust) {
18-
size_t root_of_trust_svn = 0;
19-
auto result = std::from_chars(
20-
uvm_root_of_trust.svn.data(),
21-
uvm_root_of_trust.svn.data() + uvm_root_of_trust.svn.size(),
22-
root_of_trust_svn);
23-
if (result.ec != std::errc())
24-
{
25-
throw std::runtime_error(fmt::format(
26-
"Unable to parse svn value {} to unsigned in UVM root of trust",
27-
uvm_root_of_trust.svn));
28-
}
29-
size_t endorsement_svn = 0;
30-
result = std::from_chars(
31-
endorsements.svn.data(),
32-
endorsements.svn.data() + endorsements.svn.size(),
33-
endorsement_svn);
34-
if (result.ec != std::errc())
35-
{
36-
throw std::runtime_error(fmt::format(
37-
"Unable to parse svn value {} to unsigned in UVM endorsements",
38-
endorsements.svn));
39-
}
31+
auto root_of_trust_svn = parse_svn(uvm_root_of_trust.svn);
32+
auto endorsement_svn = parse_svn(endorsements.svn);
4033

4134
return uvm_root_of_trust.did == endorsements.did &&
4235
uvm_root_of_trust.feed == endorsements.feed &&

src/node/uvm_endorsements.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ namespace ccf
4545
const pal::PlatformAttestationMeasurement& uvm_measurement,
4646
const std::vector<pal::UVMEndorsements>& uvm_roots_of_trust);
4747

48+
size_t parse_svn(const std::string& svn_str);
49+
4850
bool matches_uvm_roots_of_trust(
4951
const pal::UVMEndorsements& endorsements,
5052
const std::vector<pal::UVMEndorsements>& uvm_roots_of_trust);

0 commit comments

Comments
 (0)