diff --git a/examples/BUILD b/examples/BUILD index 2cfe8c8..a8fea54 100644 --- a/examples/BUILD +++ b/examples/BUILD @@ -236,6 +236,8 @@ cc_binary( "htool_tpm.c", "htool_tpm.h", "htool_update_failure_reasons.h", + "htool_update_session.c", + "htool_update_session.h", "htool_usb.c", "htool_usb.h", ], @@ -270,6 +272,7 @@ cc_binary( "//protocol:secure_boot", "//protocol:spi_proxy", "//protocol:statistics", + "//protocol:update_session", "//protocol:util", "//transports:libhoth_device", "//transports:libhoth_mtd", diff --git a/examples/htool.c b/examples/htool.c index ade53de..5b5f993 100644 --- a/examples/htool.c +++ b/examples/htool.c @@ -56,6 +56,7 @@ #include "htool_statistics.h" #include "htool_target_control.h" #include "htool_tpm.h" +#include "htool_update_session.h" #include "htool_usb.h" #include "protocol/authz_record.h" #include "protocol/chipinfo.h" @@ -2176,6 +2177,35 @@ static const struct htool_cmd CMDS[] = { .params = (const struct htool_param[]){{}}, .func = htool_set_tpm_mode, }, + { + .verbs = (const char*[]){"update_session", "start", NULL}, + .desc = "Start a RoT update session.", + .params = + (const struct htool_param + []){{HTOOL_FLAG_VALUE, 't', "timeout", "", + .desc = "Requested timeout in seconds for the update " + "session."}, + {HTOOL_POSITIONAL, .name = "timeout_seconds", + .default_value = "", + .desc = "Requested timeout in seconds for the update " + "session."}, + {}}, + .func = htool_update_session_start, + }, + { + .verbs = (const char*[]){"update_session", "finalize", NULL}, + .alias = (const char*[]){"update_session", "end", NULL}, + .desc = "Finalize the active RoT update session.", + .params = (const struct htool_param[]){{}}, + .func = htool_update_session_finalize, + }, + { + .verbs = (const char*[]){"update_session", "status", NULL}, + .alias = (const char*[]){"update_session", "get_status", NULL}, + .desc = "Get the current status of the RoT update session.", + .params = (const struct htool_param[]){{}}, + .func = htool_update_session_status, + }, {}, }; diff --git a/examples/htool_update_session.c b/examples/htool_update_session.c new file mode 100644 index 0000000..1933006 --- /dev/null +++ b/examples/htool_update_session.c @@ -0,0 +1,114 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "htool_update_session.h" + +#include +#include +#include +#include + +#include "htool.h" +#include "htool_cmd.h" +#include "protocol/update_session.h" + +int htool_update_session_start(const struct htool_invocation* inv) { + struct libhoth_device* dev = htool_libhoth_device(); + if (dev == NULL) { + return -1; + } + + const char* timeout_str = NULL; + if (htool_has_param(inv, "timeout")) { + htool_get_param_string(inv, "timeout", &timeout_str); + } + if ((!timeout_str || timeout_str[0] == '\0') && + htool_has_param(inv, "timeout_seconds")) { + htool_get_param_string(inv, "timeout_seconds", &timeout_str); + } + if (!timeout_str || timeout_str[0] == '\0') { + fprintf( + stderr, + "Missing required timeout (use --timeout= or )\n"); + return -1; + } + + char* endptr = NULL; + unsigned long val = strtoul(timeout_str, &endptr, 0); + uint32_t timeout_seconds = 0; + if (endptr != timeout_str && *endptr == '\0') { + timeout_seconds = (uint32_t)val; + } else { + int64_t time_us = parse_time_string_us(timeout_str); + if (time_us > 0 && time_us % 1000000 == 0) { + timeout_seconds = (uint32_t)(time_us / 1000000); + } + } + + if (timeout_seconds == 0) { + fprintf(stderr, "Invalid or zero timeout: %s\n", timeout_str); + return -1; + } + + libhoth_error err = libhoth_update_session_start(dev, timeout_seconds); + if (err != HOTH_SUCCESS) { + htool_report_error("update_session_start", err); + return -1; + } + + printf("Update session started (timeout: %u seconds)\n", timeout_seconds); + return 0; +} + +int htool_update_session_finalize(const struct htool_invocation* inv) { + (void)inv; + struct libhoth_device* dev = htool_libhoth_device(); + if (dev == NULL) { + return -1; + } + + libhoth_error err = libhoth_update_session_finalize(dev); + if (err != HOTH_SUCCESS) { + htool_report_error("update_session_finalize", err); + return -1; + } + + printf("Update session finalized\n"); + return 0; +} + +int htool_update_session_status(const struct htool_invocation* inv) { + (void)inv; + struct libhoth_device* dev = htool_libhoth_device(); + if (dev == NULL) { + return -1; + } + + struct update_session_status_response status; + memset(&status, 0, sizeof(status)); + + libhoth_error err = libhoth_update_session_get_status(dev, &status); + if (err != HOTH_SUCCESS) { + htool_report_error("update_session_get_status", err); + return -1; + } + + printf("Update session status:\n"); + printf(" State: %s (%u)\n", + libhoth_update_session_state_string( + (enum update_session_state)status.current_state), + status.current_state); + printf(" Timeout seconds left: %u\n", status.timeout_seconds_left); + return 0; +} diff --git a/examples/htool_update_session.h b/examples/htool_update_session.h new file mode 100644 index 0000000..fe510a4 --- /dev/null +++ b/examples/htool_update_session.h @@ -0,0 +1,32 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef LIBHOTH_EXAMPLES_HTOOL_UPDATE_SESSION_H_ +#define LIBHOTH_EXAMPLES_HTOOL_UPDATE_SESSION_H_ + +#include "htool_cmd.h" + +#ifdef __cplusplus +extern "C" { +#endif + +int htool_update_session_start(const struct htool_invocation* inv); +int htool_update_session_finalize(const struct htool_invocation* inv); +int htool_update_session_status(const struct htool_invocation* inv); + +#ifdef __cplusplus +} +#endif + +#endif // LIBHOTH_EXAMPLES_HTOOL_UPDATE_SESSION_H_ diff --git a/examples/meson.build b/examples/meson.build index 3c5e982..2a50baf 100644 --- a/examples/meson.build +++ b/examples/meson.build @@ -64,6 +64,8 @@ executable( 'htool_security_tokens.c', 'htool_tpm.h', 'htool_tpm.c', + 'htool_update_session.h', + 'htool_update_session.c', git_version_h, ], dependencies: [libusb], diff --git a/protocol/BUILD b/protocol/BUILD index 5094ad2..3be5079 100644 --- a/protocol/BUILD +++ b/protocol/BUILD @@ -620,3 +620,28 @@ cc_test( "@googletest//:gtest_main", ], ) + +cc_library( + name = "update_session", + srcs = ["update_session.c"], + hdrs = ["update_session.h"], + deps = [ + ":host_cmd", + ":libhoth_status", + "//transports:libhoth_device", + ], +) + +cc_test( + name = "update_session_test", + srcs = ["update_session_test.cc"], + deps = [ + ":host_cmd", + ":libhoth_status", + ":update_session", + "//protocol/test:libhoth_device_mock", + "//transports:libhoth_device", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) diff --git a/protocol/meson.build b/protocol/meson.build index 5219601..9c1c85d 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -26,6 +26,7 @@ protocol_srcs = [ 'util.c', 'console.c', 'gpio_drive_strength.c', + 'update_session.c', 'status.c' ] diff --git a/protocol/update_session.c b/protocol/update_session.c new file mode 100644 index 0000000..ee2df24 --- /dev/null +++ b/protocol/update_session.c @@ -0,0 +1,95 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "protocol/update_session.h" + +#include +#include + +#include "protocol/host_cmd.h" +#include "protocol/status.h" +#include "transports/libhoth_device.h" + +libhoth_error libhoth_update_session_start(struct libhoth_device* dev, + uint32_t timeout_seconds) { + if (dev == NULL || timeout_seconds == 0) { + return LIBHOTH_ERR_CONSTRUCT(HOTH_CTX_CMD_EXEC, HOTH_HOST_SPACE_LIBHOTH, + LIBHOTH_ERR_INVALID_PARAMETER); + } + + struct update_session_start_request req = { + .timeout_seconds = timeout_seconds, + }; + size_t rlen = 0; + return libhoth_hostcmd_exec_v2( + dev, + HOTH_CMD_BOARD_SPECIFIC_BASE + HOTH_PRV_CMD_HOTH_UPDATE_SESSION_START, 0, + &req, sizeof(req), NULL, 0, &rlen); +} + +libhoth_error libhoth_update_session_finalize(struct libhoth_device* dev) { + if (dev == NULL) { + return LIBHOTH_ERR_CONSTRUCT(HOTH_CTX_CMD_EXEC, HOTH_HOST_SPACE_LIBHOTH, + LIBHOTH_ERR_INVALID_PARAMETER); + } + + size_t rlen = 0; + return libhoth_hostcmd_exec_v2( + dev, + HOTH_CMD_BOARD_SPECIFIC_BASE + HOTH_PRV_CMD_HOTH_UPDATE_SESSION_FINALIZE, + 0, NULL, 0, NULL, 0, &rlen); +} + +libhoth_error libhoth_update_session_get_status( + struct libhoth_device* dev, struct update_session_status_response* status) { + if (dev == NULL || status == NULL) { + return LIBHOTH_ERR_CONSTRUCT(HOTH_CTX_CMD_EXEC, HOTH_HOST_SPACE_LIBHOTH, + LIBHOTH_ERR_INVALID_PARAMETER); + } + + size_t rlen = 0; + libhoth_error err = + libhoth_hostcmd_exec_v2(dev, + HOTH_CMD_BOARD_SPECIFIC_BASE + + HOTH_PRV_CMD_HOTH_UPDATE_SESSION_GET_STATUS, + 0, NULL, 0, status, sizeof(*status), &rlen); + if (err != HOTH_SUCCESS) { + return err; + } + if (rlen != sizeof(*status)) { + return LIBHOTH_ERR_CONSTRUCT(HOTH_CTX_CMD_EXEC, HOTH_HOST_SPACE_LIBHOTH, + LIBHOTH_ERR_FAIL); + } + return HOTH_SUCCESS; +} + +const char* libhoth_update_session_state_string( + enum update_session_state state) { + switch (state) { + case UPDATE_SESSION_NONE: + return "NONE"; + case UPDATE_SESSION_PENDING_START: + return "PENDING_START"; + case UPDATE_SESSION_UPDATING: + return "UPDATING"; + case UPDATE_SESSION_FINALIZED: + return "FINALIZED"; + case UPDATE_SESSION_START_TIMEOUT: + return "START_TIMEOUT"; + case UPDATE_SESSION_UPDATE_TIMEOUT: + return "UPDATE_TIMEOUT"; + default: + return "UNKNOWN"; + } +} diff --git a/protocol/update_session.h b/protocol/update_session.h new file mode 100644 index 0000000..38e54d3 --- /dev/null +++ b/protocol/update_session.h @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef LIBHOTH_PROTOCOL_UPDATE_SESSION_H_ +#define LIBHOTH_PROTOCOL_UPDATE_SESSION_H_ + +#include + +#include "protocol/status.h" +#include "transports/libhoth_device.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define HOTH_PRV_CMD_HOTH_UPDATE_SESSION_START 0x005a +#define HOTH_PRV_CMD_HOTH_UPDATE_SESSION_FINALIZE 0x005b +#define HOTH_PRV_CMD_HOTH_UPDATE_SESSION_GET_STATUS 0x005c + +#define EC_PRV_CMD_UPDATE_SESSION_START 0x005a +#define EC_PRV_CMD_UPDATE_SESSION_FINALIZE 0x005b +#define EC_PRV_CMD_UPDATE_SESSION_GET_STATUS 0x005c + +enum update_session_state { + UPDATE_SESSION_NONE = 0, + UPDATE_SESSION_PENDING_START = 1, + UPDATE_SESSION_UPDATING = 2, + UPDATE_SESSION_FINALIZED = 3, + UPDATE_SESSION_START_TIMEOUT = 4, + UPDATE_SESSION_UPDATE_TIMEOUT = 5, +}; + +struct update_session_start_request { + uint32_t timeout_seconds; +} __attribute__((packed, aligned(4))); + +struct update_session_status_response { + uint32_t current_state; + uint32_t timeout_seconds_left; +} __attribute__((packed, aligned(4))); + +// Start an update session with the requested timeout in seconds. +libhoth_error libhoth_update_session_start(struct libhoth_device* dev, + uint32_t timeout_seconds); + +// Finalize the active update session. +libhoth_error libhoth_update_session_finalize(struct libhoth_device* dev); + +// Get the current update session status. +libhoth_error libhoth_update_session_get_status( + struct libhoth_device* dev, struct update_session_status_response* status); + +// Returns a human-readable string representation of the update session state. +const char* libhoth_update_session_state_string( + enum update_session_state state); + +#ifdef __cplusplus +} +#endif + +#endif // LIBHOTH_PROTOCOL_UPDATE_SESSION_H_ diff --git a/protocol/update_session_test.cc b/protocol/update_session_test.cc new file mode 100644 index 0000000..d7b40d3 --- /dev/null +++ b/protocol/update_session_test.cc @@ -0,0 +1,185 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "protocol/update_session.h" + +#include +#include + +#include +#include + +#include "protocol/host_cmd.h" +#include "protocol/status.h" +#include "protocol/test/libhoth_device_mock.h" + +namespace { + +using ::testing::_; +using ::testing::DoAll; +using ::testing::Return; +using ::testing::StrEq; + +MATCHER_P(UsesStartTimeout, expected_timeout, "") { + const auto* req = static_cast(arg); + if (req->command != + HOTH_CMD_BOARD_SPECIFIC_BASE + HOTH_PRV_CMD_HOTH_UPDATE_SESSION_START) { + return false; + } + if (req->data_len != sizeof(struct update_session_start_request)) { + return false; + } + const auto* payload = + reinterpret_cast( + reinterpret_cast(arg) + + sizeof(struct hoth_host_request)); + return payload->timeout_seconds == expected_timeout; +} + +TEST_F(LibHothTest, UpdateSessionStartSuccess) { + constexpr uint32_t kTimeoutSeconds = 60; + EXPECT_CALL(mock_, send(_, UsesStartTimeout(kTimeoutSeconds), _)) + .WillOnce(Return(LIBHOTH_OK)); + + uint32_t dummy = 0; + EXPECT_CALL(mock_, receive) + .WillOnce(DoAll(CopyResp(&dummy, 0), Return(LIBHOTH_OK))); + + EXPECT_EQ(libhoth_update_session_start(&hoth_dev_, kTimeoutSeconds), + HOTH_SUCCESS); +} + +TEST_F(LibHothTest, UpdateSessionStartZeroTimeout) { + libhoth_error err = libhoth_update_session_start(&hoth_dev_, 0); + EXPECT_NE(err, HOTH_SUCCESS); + EXPECT_EQ(LIBHOTH_ERR_GET_CTX(err), HOTH_CTX_CMD_EXEC); + EXPECT_EQ(LIBHOTH_ERR_GET_SPACE(err), HOTH_HOST_SPACE_LIBHOTH); + EXPECT_EQ(LIBHOTH_ERR_GET_CODE(err), LIBHOTH_ERR_INVALID_PARAMETER); +} + +TEST_F(LibHothTest, UpdateSessionStartNullDevice) { + libhoth_error err = libhoth_update_session_start(nullptr, 60); + EXPECT_NE(err, HOTH_SUCCESS); + EXPECT_EQ(LIBHOTH_ERR_GET_CTX(err), HOTH_CTX_CMD_EXEC); + EXPECT_EQ(LIBHOTH_ERR_GET_SPACE(err), HOTH_HOST_SPACE_LIBHOTH); + EXPECT_EQ(LIBHOTH_ERR_GET_CODE(err), LIBHOTH_ERR_INVALID_PARAMETER); +} + +TEST_F(LibHothTest, UpdateSessionFinalizeSuccess) { + EXPECT_CALL(mock_, + send(_, + UsesCommand(HOTH_CMD_BOARD_SPECIFIC_BASE + + HOTH_PRV_CMD_HOTH_UPDATE_SESSION_FINALIZE), + _)) + .WillOnce(Return(LIBHOTH_OK)); + + uint32_t dummy = 0; + EXPECT_CALL(mock_, receive) + .WillOnce(DoAll(CopyResp(&dummy, 0), Return(LIBHOTH_OK))); + + EXPECT_EQ(libhoth_update_session_finalize(&hoth_dev_), HOTH_SUCCESS); +} + +TEST_F(LibHothTest, UpdateSessionFinalizeNullDevice) { + libhoth_error err = libhoth_update_session_finalize(nullptr); + EXPECT_NE(err, HOTH_SUCCESS); + EXPECT_EQ(LIBHOTH_ERR_GET_CTX(err), HOTH_CTX_CMD_EXEC); + EXPECT_EQ(LIBHOTH_ERR_GET_SPACE(err), HOTH_HOST_SPACE_LIBHOTH); + EXPECT_EQ(LIBHOTH_ERR_GET_CODE(err), LIBHOTH_ERR_INVALID_PARAMETER); +} + +TEST_F(LibHothTest, UpdateSessionGetStatusSuccess) { + struct update_session_status_response expected_status = { + .current_state = UPDATE_SESSION_UPDATING, + .timeout_seconds_left = 42, + }; + + EXPECT_CALL(mock_, + send(_, + UsesCommand(HOTH_CMD_BOARD_SPECIFIC_BASE + + HOTH_PRV_CMD_HOTH_UPDATE_SESSION_GET_STATUS), + _)) + .WillOnce(Return(LIBHOTH_OK)); + + EXPECT_CALL(mock_, receive) + .WillOnce(DoAll(CopyResp(&expected_status, sizeof(expected_status)), + Return(LIBHOTH_OK))); + + struct update_session_status_response actual_status = {}; + EXPECT_EQ(libhoth_update_session_get_status(&hoth_dev_, &actual_status), + HOTH_SUCCESS); + EXPECT_EQ(actual_status.current_state, UPDATE_SESSION_UPDATING); + EXPECT_EQ(actual_status.timeout_seconds_left, 42); +} + +TEST_F(LibHothTest, UpdateSessionGetStatusNullParam) { + libhoth_error err = libhoth_update_session_get_status(&hoth_dev_, nullptr); + EXPECT_NE(err, HOTH_SUCCESS); + EXPECT_EQ(LIBHOTH_ERR_GET_CTX(err), HOTH_CTX_CMD_EXEC); + EXPECT_EQ(LIBHOTH_ERR_GET_SPACE(err), HOTH_HOST_SPACE_LIBHOTH); + EXPECT_EQ(LIBHOTH_ERR_GET_CODE(err), LIBHOTH_ERR_INVALID_PARAMETER); +} + +TEST_F(LibHothTest, UpdateSessionGetStatusNullDevice) { + struct update_session_status_response status = {}; + libhoth_error err = libhoth_update_session_get_status(nullptr, &status); + EXPECT_NE(err, HOTH_SUCCESS); + EXPECT_EQ(LIBHOTH_ERR_GET_CTX(err), HOTH_CTX_CMD_EXEC); + EXPECT_EQ(LIBHOTH_ERR_GET_SPACE(err), HOTH_HOST_SPACE_LIBHOTH); + EXPECT_EQ(LIBHOTH_ERR_GET_CODE(err), LIBHOTH_ERR_INVALID_PARAMETER); +} + +TEST_F(LibHothTest, UpdateSessionGetStatusShortResponse) { + EXPECT_CALL(mock_, + send(_, + UsesCommand(HOTH_CMD_BOARD_SPECIFIC_BASE + + HOTH_PRV_CMD_HOTH_UPDATE_SESSION_GET_STATUS), + _)) + .WillOnce(Return(LIBHOTH_OK)); + + uint32_t short_data = 1; + EXPECT_CALL(mock_, receive) + .WillOnce( + DoAll(CopyResp(&short_data, sizeof(short_data)), Return(LIBHOTH_OK))); + + struct update_session_status_response status = {}; + libhoth_error err = libhoth_update_session_get_status(&hoth_dev_, &status); + EXPECT_NE(err, HOTH_SUCCESS); + EXPECT_EQ(LIBHOTH_ERR_GET_CTX(err), HOTH_CTX_CMD_EXEC); + EXPECT_EQ(LIBHOTH_ERR_GET_SPACE(err), HOTH_HOST_SPACE_LIBHOTH); + EXPECT_EQ(LIBHOTH_ERR_GET_CODE(err), LIBHOTH_ERR_FAIL); +} + +TEST(UpdateSessionTest, StateString) { + EXPECT_STREQ(libhoth_update_session_state_string(UPDATE_SESSION_NONE), + "NONE"); + EXPECT_STREQ( + libhoth_update_session_state_string(UPDATE_SESSION_PENDING_START), + "PENDING_START"); + EXPECT_STREQ(libhoth_update_session_state_string(UPDATE_SESSION_UPDATING), + "UPDATING"); + EXPECT_STREQ(libhoth_update_session_state_string(UPDATE_SESSION_FINALIZED), + "FINALIZED"); + EXPECT_STREQ( + libhoth_update_session_state_string(UPDATE_SESSION_START_TIMEOUT), + "START_TIMEOUT"); + EXPECT_STREQ( + libhoth_update_session_state_string(UPDATE_SESSION_UPDATE_TIMEOUT), + "UPDATE_TIMEOUT"); + EXPECT_STREQ(libhoth_update_session_state_string( + static_cast(99)), + "UNKNOWN"); +} + +} // namespace