Skip to content

Commit

Permalink
Only the KV-defined set of UVM roots of trust should be used to accep…
Browse files Browse the repository at this point in the history
…t joining nodes (#6489)
  • Loading branch information
achamayou authored Sep 23, 2024
1 parent 3fb252b commit 4b16707
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 29 deletions.
1 change: 1 addition & 0 deletions .snpcc_canary
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
/-xXx--//-----x=x--/-xXx--/---x---->>>--/
...
/\/\d(-_-)b/\/\
--
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Changed

- The `set_jwt_issuer` governance action has been updated, and no longer accepts `key_filter` or `key_policy` arguments (#6450).
- Nodes started in `Join` mode will shut down if they receive and unrecoverable condition such as `StartupSeqnoIsOld` when attempting to join (#6471).
- Nodes started in `Join` mode will shut down if they receive an unrecoverable condition such as `StartupSeqnoIsOld` or `InvalidQuote` when attempting to join (#6471, #6489).
- In configuration, `attestation.snp_endorsements_servers` can specify a `max_retries_count`. If the count has been exhausted without success for all configured servers, the node will shut down (#6478).
- When deciding which nodes are allowed to join, only UVM roots of trust defined in `public:ccf.gov.nodes.snp.uvm_endorsements` are considered (#6489).

### Removed

Expand Down
5 changes: 5 additions & 0 deletions include/ccf/http_status.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ using http_status = llhttp_status;
static inline const char* http_status_str(http_status s)
{
return llhttp_status_name(s);
}

static inline bool is_http_status_client_error(http_status s)
{
return s >= 400 && s < 500;
}
5 changes: 3 additions & 2 deletions src/node/node_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -595,12 +595,13 @@ namespace ccf
return;
}

if (status == HTTP_STATUS_BAD_REQUEST)
if (is_http_status_client_error(status))
{
auto error_msg = fmt::format(
"Join request to {} returned 400 Bad Request: {}. Shutting "
"Join request to {} returned {} Bad Request: {}. Shutting "
"down node gracefully.",
config.join.target_rpc_address,
status,
std::string(data.begin(), data.end()));
LOG_FAIL_FMT("{}", error_msg);
RINGBUFFER_WRITE_MESSAGE(
Expand Down
44 changes: 24 additions & 20 deletions src/node/quote.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,35 @@ namespace ccf
const pal::PlatformAttestationMeasurement& quote_measurement,
const std::vector<uint8_t>& uvm_endorsements)
{
auto uvm_endorsements_data =
verify_uvm_endorsements(uvm_endorsements, quote_measurement);
// Uses KV-defined roots of trust (did -> (feed, svn)) to verify the
// UVM measurement against endorsements in the quote.
std::vector<UVMEndorsements> uvm_roots_of_trust_from_kv;
auto uvmes = tx.ro<SNPUVMEndorsements>(Tables::NODE_SNP_UVM_ENDORSEMENTS);
if (uvmes == nullptr)
if (uvmes)
{
// No recorded trusted UVM endorsements
return false;
uvmes->foreach(
[&uvm_roots_of_trust_from_kv](
const DID& did, const FeedToEndorsementsDataMap& endorsements_map) {
for (const auto& [feed, data] : endorsements_map)
{
uvm_roots_of_trust_from_kv.push_back(
UVMEndorsements{did, feed, data.svn});
}
return true;
});
}

bool match = false;
uvmes->foreach([&match, &uvm_endorsements_data](
const DID& did, const FeedToEndorsementsDataMap& value) {
if (uvm_endorsements_data.did == did)
{
auto search = value.find(uvm_endorsements_data.feed);
if (search != value.end())
{
match = true;
return false;
}
}
try
{
auto uvm_endorsements_data = verify_uvm_endorsements(
uvm_endorsements, quote_measurement, uvm_roots_of_trust_from_kv);
return true;
});

return match;
}
catch (const std::logic_error& e)
{
LOG_FAIL_FMT("Failed to verify UVM endorsements: {}", e.what());
return false;
}
}

QuoteVerificationResult verify_enclave_measurement_against_store(
Expand Down
44 changes: 38 additions & 6 deletions tests/code_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ def test_verify_quotes(network, args):
return network


def get_trusted_uvm_endorsements(node):
with node.api_versioned_client(api_version=args.gov_api_version) as client:
r = client.get("/gov/service/join-policy")
assert r.status_code == http.HTTPStatus.OK, r
return r.body.json()["snp"]["uvmEndorsements"]


@reqs.description("Test the SNP measurements table")
@reqs.snp_only()
def test_snp_measurements_tables(network, args):
Expand Down Expand Up @@ -88,12 +95,6 @@ def get_trusted_measurements(node):

LOG.info("SNP UVM endorsement table")

def get_trusted_uvm_endorsements(node):
with node.api_versioned_client(api_version=args.gov_api_version) as client:
r = client.get("/gov/service/join-policy")
assert r.status_code == http.HTTPStatus.OK, r
return r.body.json()["snp"]["uvmEndorsements"]

uvm_endorsements = get_trusted_uvm_endorsements(primary)
assert (
len(uvm_endorsements) == 1
Expand Down Expand Up @@ -478,6 +479,34 @@ def test_proposal_invalidation(network, args):
return network


@reqs.description(
"Node fails to join if KV contains no UVM endorsements roots of trust"
)
@reqs.snp_only()
def test_add_node_with_no_uvm_endorsements_in_kv(network, args):
LOG.info("Remove KV endorsements roots of trust (expect failure)")
primary, _ = network.find_nodes()

uvm_endorsements = get_trusted_uvm_endorsements(primary)
assert (
len(uvm_endorsements) == 1
), f"Expected one UVM endorsement, {uvm_endorsements}"
did, value = next(iter(uvm_endorsements.items()))
feed, data = next(iter(value.items()))

network.consortium.remove_snp_uvm_endorsement(primary, did, feed)

try:
new_node = network.create_node("local://localhost")
network.join_node(new_node, args.package, args, timeout=3)
except infra.network.UVMEndorsementsNotAuthorised:
LOG.info("As expected, node with no UVM endorsements failed to join")
else:
raise AssertionError("Node join unexpectedly succeeded")

return network


def run(args):
with infra.network.network(
args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
Expand All @@ -503,6 +532,9 @@ def run(args):
# Run again at the end to confirm current nodes are acceptable
test_verify_quotes(network, args)

if snp.IS_SNP:
test_add_node_with_no_uvm_endorsements_in_kv(network, args)


if __name__ == "__main__":
args = infra.e2e_args.cli_args()
Expand Down
6 changes: 6 additions & 0 deletions tests/infra/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ class CodeIdNotFound(Exception):
pass


class UVMEndorsementsNotAuthorised(Exception):
pass


class StartupSeqnoIsOld(Exception):
pass

Expand Down Expand Up @@ -928,6 +932,8 @@ def run_join_node(
for error in errors:
if "Quote does not contain known enclave measurement" in error:
raise CodeIdNotFound from e
if "UVM endorsements are not authorised" in error:
raise UVMEndorsementsNotAuthorised from e
if "StartupSeqnoIsOld" in error:
raise StartupSeqnoIsOld(has_stopped) from e
if "invalid cert on handshake" in error:
Expand Down

0 comments on commit 4b16707

Please sign in to comment.