Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add C++ API to get untrusted host time #2550

Merged
merged 5 commits into from
May 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions include/ccf/base_endpoint_registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,9 @@ namespace ccf
kv::ReadOnlyTx& tx,
const MemberId& member_id,
crypto::Pem& member_cert_pem);

/** Get untrusted time from the host of the currently executing node.
*/
ApiResult get_untrusted_host_time_v1(::timespec& time);
};
}
52 changes: 52 additions & 0 deletions samples/apps/nobuiltins/nobuiltins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ namespace nobuiltins
DECLARE_JSON_TYPE(TransactionIDResponse)
DECLARE_JSON_REQUIRED_FIELDS(TransactionIDResponse, transaction_id)

struct TimeResponse
{
std::string timestamp;
};

DECLARE_JSON_TYPE(TimeResponse)
DECLARE_JSON_REQUIRED_FIELDS(TimeResponse, timestamp)

// SNIPPET: registry_inheritance
class NoBuiltinsRegistry : public ccf::BaseEndpointRegistry
{
Expand Down Expand Up @@ -227,6 +235,50 @@ namespace nobuiltins
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.set_auto_schema<void, TransactionIDResponse>()
.install();

auto get_time = [this](auto& ctx, nlohmann::json&&) {
::timespec time;
ccf::ApiResult result = get_untrusted_host_time_v1(time);
if (result != ccf::ApiResult::OK)
{
return ccf::make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
fmt::format(
"Unable to get time: {}", ccf::api_result_to_str(result)));
}

std::tm calendar_time;
gmtime_r(&time.tv_sec, &calendar_time);

// 20 characters for a (timezoneless) ISO 8601 datetime, plus a
// terminating null
constexpr size_t buf_size = 21;
char buf[buf_size];
if (strftime(buf, buf_size, "%Y-%m-%dT%H:%M:%S", &calendar_time) == 0)
{
return ccf::make_error(
HTTP_STATUS_INTERNAL_SERVER_ERROR,
ccf::errors::InternalError,
fmt::format("Unable to format timestamp"));
}

// Build full time, with 6 decimals of sub-second precision, and a
// Python-friendly +00:00 UTC offset
TimeResponse response;
response.timestamp =
fmt::format("{}.{:06}+00:00", buf, time.tv_nsec / 1'000);
return ccf::make_success(response);
};
make_command_endpoint(
"current_time",
HTTP_GET,
ccf::json_command_adapter(get_time),
ccf::no_auth_required)
.set_execute_outside_consensus(
ccf::endpoints::ExecuteOutsideConsensus::Locally)
.set_auto_schema<void, TimeResponse>()
.install();
}
};

Expand Down
12 changes: 12 additions & 0 deletions src/endpoints/base_endpoint_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "ccf/base_endpoint_registry.h"

#include "enclave/enclave_time.h"
#include "node/members.h"
#include "node/users.h"

Expand Down Expand Up @@ -243,4 +244,15 @@ namespace ccf
return ApiResult::InternalError;
}
}

ApiResult BaseEndpointRegistry::get_untrusted_host_time_v1(::timespec& time)
{
const std::chrono::microseconds now_us = enclave::get_enclave_time();

constexpr auto us_per_s = 1'000'000;
time.tv_sec = now_us.count() / us_per_s;
time.tv_nsec = (now_us.count() % us_per_s) * 1'000;

return ApiResult::OK;
}
}
18 changes: 18 additions & 0 deletions tests/nobuiltins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from ccf.tx_id import TxID
from http import HTTPStatus
import openapi_spec_validator
from datetime import datetime, timezone
import time


def test_nobuiltins_endpoints(network, args):
Expand Down Expand Up @@ -32,6 +34,22 @@ def test_nobuiltins_endpoints(network, args):
body_j = r.body.json()
assert body_j["transaction_id"] == f"{tx_id}"

for i in range(3):
if i != 0:
time.sleep(1.5)
r = c.get("/app/current_time")
local_time = datetime.now(timezone.utc)
assert r.status_code == HTTPStatus.OK
body_j = r.body.json()
service_time = datetime.fromisoformat(body_j["timestamp"])
diff = (local_time - service_time).total_seconds()
# This intends to test that the reported time is "close enough"
# to the real current time. This is dependent on the skew between
# clocks on this executor and the target node, and the request
# latency (including Python IO and parsing). It may need to be
# more lenient
assert abs(diff) < 1, diff


def run(args):
with infra.network.network(
Expand Down