Move hoth-ipmi-blobs to gbmc public

Tested: Presubmit is successful
Google-Bug-Id: 340634286
Change-Id: Ia5d53239b07d97bcd328242d2109d40377c1b643
Signed-off-by: Vamsy Krishna Nooney <vamsykn@google.com>
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ac9f720
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+Hoth IPMI Blob Handler
diff --git a/command/dbus_command.cpp b/command/dbus_command.cpp
new file mode 100644
index 0000000..c61910a
--- /dev/null
+++ b/command/dbus_command.cpp
@@ -0,0 +1,69 @@
+// Copyright 2024 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 "dbus_command.hpp"
+
+#include "dbus_command_impl.hpp"
+
+#include <fmt/format.h>
+
+#include <stdplus/cancel.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+stdplus::Cancel DbusCommandImpl::SendHostCommand(
+    std::string_view hothId, const std::vector<uint8_t>& cmd, Cb&& cb)
+{
+    auto req = newHothdCall(hothId, "SendHostCommand");
+    req.append(cmd);
+    auto cHothId = std::string(hothId);
+    return stdplus::Cancel(new BusCall(req.call_async(
+        [cb = std::move(cb), hothId = std::move(cHothId)](
+            sdbusplus::message::message m) mutable noexcept {
+            if (m.is_method_error())
+            {
+                auto err = m.get_error();
+                fmt::print(stderr, "SendHostCommand failed on `{}`: {}: {}\n",
+                           hothId, err->name, err->message);
+                cb(std::nullopt);
+                return;
+            }
+            try
+            {
+                std::vector<uint8_t> rsp;
+                m.read(rsp);
+                cb(std::move(rsp));
+            }
+            catch (const std::exception& e)
+            {
+                fmt::print(stderr,
+                           "SendHostCommand failed unpacking on `{}`: {}\n",
+                           hothId, e.what());
+                cb(std::nullopt);
+            }
+        },
+        asyncCallTimeout)));
+}
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/command/dbus_command.hpp b/command/dbus_command.hpp
new file mode 100644
index 0000000..cafbae7
--- /dev/null
+++ b/command/dbus_command.hpp
@@ -0,0 +1,53 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus.hpp"
+
+#include <function2/function2.hpp>
+#include <stdplus/cancel.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class DbusCommand
+ *  @brief Overridable D-Bus interface for command handler
+ */
+class DbusCommand : public virtual Dbus
+{
+  public:
+    using Cb = fu2::unique_function<void(std::optional<std::vector<uint8_t>>)>;
+
+    /** @brief Implementation for asynchronous SendHostCommand
+     *  Send a host command to Hoth and run a callback when it responds.
+     *
+     *  @param[in] hothId - The identifier of the targeted hoth instance.
+     *  @param[in] command - Data to write to Hoth SPI host command offset.
+     *  @param[in] cb      - The callback to execute.
+     *  @return cancelable[stdplus::Cancel] - Drop to cancel the command early.
+     */
+    virtual stdplus::Cancel SendHostCommand(
+        std::string_view hothId, const std::vector<uint8_t>&, Cb&&) = 0;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/command/dbus_command_impl.hpp b/command/dbus_command_impl.hpp
new file mode 100644
index 0000000..cf25106
--- /dev/null
+++ b/command/dbus_command_impl.hpp
@@ -0,0 +1,44 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus_command.hpp"
+#include "dbus_impl.hpp"
+
+#include <sdbusplus/bus.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class DbusImpl
+ *  @brief D-Bus concrete implementation
+ *  @details Pass through all calls to the default D-Bus instance
+ */
+class DbusCommandImpl : public DbusCommand, public DbusImpl
+{
+  public:
+    stdplus::Cancel SendHostCommand(std::string_view hothId,
+                                    const std::vector<uint8_t>&, Cb&&) override;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/command/hoth_command.cpp b/command/hoth_command.cpp
new file mode 100644
index 0000000..931d5a8
--- /dev/null
+++ b/command/hoth_command.cpp
@@ -0,0 +1,94 @@
+// Copyright 2024 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 "hoth_command.hpp"
+
+#include <phosphor-logging/log.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <vector>
+
+using phosphor::logging::entry;
+using phosphor::logging::log;
+
+using level = phosphor::logging::level;
+using SdBusError = sdbusplus::exception::SdBusError;
+
+namespace ipmi_hoth
+{
+
+bool HothCommandBlobHandler::stat(const std::string&, blobs::BlobMeta*)
+{
+    /* Hoth command handler does not support a global blob stat. */
+    return false;
+}
+
+bool HothCommandBlobHandler::commit(uint16_t session,
+                                    const std::vector<uint8_t>& data)
+{
+    if (!data.empty())
+    {
+        log<level::ERR>("Unexpected data provided to commit call");
+        return false;
+    }
+
+    HothBlob* sess = getSession(session);
+    if (!sess)
+    {
+        return false;
+    }
+
+    // If commit is called multiple times, return the same result as last time
+    if (sess->state &
+        (blobs::StateFlags::committing | blobs::StateFlags::committed))
+    {
+        return true;
+    }
+
+    sess->state &= ~blobs::StateFlags::commit_error;
+    sess->state |= blobs::StateFlags::committing;
+    sess->outstanding = dbus_->SendHostCommand(
+        sess->hothId, sess->buffer,
+        [sess](std::optional<std::vector<uint8_t>> rsp) {
+            auto outstanding = std::move(sess->outstanding);
+            sess->state &= ~blobs::StateFlags::committing;
+            if (!rsp)
+            {
+                sess->state |= blobs::StateFlags::commit_error;
+                return;
+            }
+            sess->buffer = std::move(*rsp);
+            sess->state |= blobs::StateFlags::committed;
+        });
+    return true;
+}
+
+bool HothCommandBlobHandler::stat(uint16_t session, blobs::BlobMeta* meta)
+{
+    HothBlob* sess = getSession(session);
+    if (!sess)
+    {
+        return false;
+    }
+
+    meta->size = sess->buffer.size();
+    meta->blobState = sess->state;
+    return true;
+}
+
+} // namespace ipmi_hoth
diff --git a/command/hoth_command.hpp b/command/hoth_command.hpp
new file mode 100644
index 0000000..64e9a43
--- /dev/null
+++ b/command/hoth_command.hpp
@@ -0,0 +1,67 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus_command.hpp"
+#include "hoth.hpp"
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+
+class HothCommandBlobHandler : public HothBlobHandler
+{
+  public:
+    explicit HothCommandBlobHandler(internal::DbusCommand* dbus) : dbus_(dbus)
+    {}
+
+    /* Our callbacks require pinned memory */
+    HothCommandBlobHandler(HothCommandBlobHandler&&) = delete;
+    HothCommandBlobHandler& operator=(HothCommandBlobHandler&&) = delete;
+
+    bool stat(const std::string& path, blobs::BlobMeta* meta) override;
+    bool commit(uint16_t session, const std::vector<uint8_t>& data) override;
+    bool stat(uint16_t session, blobs::BlobMeta* meta) override;
+
+    internal::Dbus& dbus() override
+    {
+        return *dbus_;
+    }
+    std::string_view pathSuffix() const override
+    {
+        return "command_passthru";
+    }
+    uint16_t requiredFlags() const override
+    {
+        return blobs::OpenFlags::read | blobs::OpenFlags::write;
+    }
+    uint16_t maxSessions() const override
+    {
+        return 10;
+    }
+    uint32_t maxBufferSize() const override
+    {
+        return 1024;
+    }
+
+  private:
+    internal::DbusCommand* dbus_;
+};
+
+} // namespace ipmi_hoth
diff --git a/command/main_command.cpp b/command/main_command.cpp
new file mode 100644
index 0000000..f7dfdee
--- /dev/null
+++ b/command/main_command.cpp
@@ -0,0 +1,29 @@
+// Copyright 2024 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 "dbus_command_impl.hpp"
+#include "hoth_command.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <memory>
+
+extern "C" std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+    /** @brief Default instantiation of Dbus */
+    static ipmi_hoth::internal::DbusCommandImpl dbusCommand_impl;
+
+    return std::make_unique<ipmi_hoth::HothCommandBlobHandler>(
+        &dbusCommand_impl);
+}
diff --git a/command/meson.build b/command/meson.build
new file mode 100644
index 0000000..63360ca
--- /dev/null
+++ b/command/meson.build
@@ -0,0 +1,40 @@
+# Function2 might not have a pkg-config. It is header only so just make
+# sure we can access the needed symbols from the header.
+function2_dep = dependency('function2', required: false)
+has_function2 = meson.get_compiler('cpp').has_header_symbol(
+  'function2/function2.hpp',
+  'fu2::unique_function',
+  dependencies: function2_dep)
+
+hothcommand_pre = declare_dependency(
+  include_directories: include_directories('.'),
+  dependencies: [
+    function2_dep,
+    hothblob_dep,
+    phosphor_logging_dep,
+    sdbusplus_dep,
+    stdplus_dep,
+  ])
+
+hothcommand_lib = static_library(
+  'hothcommand',
+  'dbus_command.cpp',
+  'hoth_command.cpp',
+  implicit_include_directories: false,
+  dependencies: hothcommand_pre)
+
+hothcommand_dep = declare_dependency(
+  link_with: hothcommand_lib,
+  dependencies: hothcommand_pre)
+
+shared_module(
+  'hothcommand',
+  'main_command.cpp',
+  implicit_include_directories: false,
+  dependencies: hothcommand_dep,
+  install: true,
+  install_dir: get_option('libdir') / 'blob-ipmid')
+
+if not get_option('tests').disabled()
+  subdir('test')
+endif
diff --git a/command/test/dbus_command_mock.hpp b/command/test/dbus_command_mock.hpp
new file mode 100644
index 0000000..5231ca3
--- /dev/null
+++ b/command/test/dbus_command_mock.hpp
@@ -0,0 +1,41 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus_command.hpp"
+#include "dbus_mock.hpp"
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+class DbusCommandMock : public DbusCommand, public DbusMock
+{
+  public:
+    MOCK_METHOD(stdplus::Cancel, SendHostCommand,
+                (std::string_view, const std::vector<uint8_t>&, Cb&&),
+                (override));
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_close_unittest.cpp b/command/test/hoth_command_close_unittest.cpp
new file mode 100644
index 0000000..841ff39
--- /dev/null
+++ b/command/test/hoth_command_close_unittest.cpp
@@ -0,0 +1,46 @@
+// Copyright 2024 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 "hoth_command_unittest.hpp"
+
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothCommandCloseTest : public HothCommandTest
+{};
+
+TEST_F(HothCommandCloseTest, CloseWithInvalidSessionFails)
+{
+    // Verify you cannot close an invalid session.
+
+    EXPECT_FALSE(hvn.close(session_));
+}
+
+TEST_F(HothCommandCloseTest, CloseWithValidSessionSuccess)
+{
+    // Verify you can close a valid session.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.close(session_));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_commit_unittest.cpp b/command/test/hoth_command_commit_unittest.cpp
new file mode 100644
index 0000000..495aa59
--- /dev/null
+++ b/command/test/hoth_command_commit_unittest.cpp
@@ -0,0 +1,241 @@
+// Copyright 2024 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 "hoth_command_unittest.hpp"
+
+#include <sdbusplus/exception.hpp>
+#include <sdbusplus/test/sdbus_mock.hpp>
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::IsEmpty;
+using ::testing::NotNull;
+using ::testing::Return;
+
+using namespace std::literals;
+
+namespace ipmi_hoth
+{
+
+const auto static test_str = "Hello, world!"s;
+const std::vector<uint8_t> static test_buf(test_str.begin(), test_str.end());
+const auto static test2_str = "Good morning, world!"s;
+const std::vector<uint8_t> static test2_buf(test2_str.begin(), test2_str.end());
+
+sdbusplus::SdBusMock sdbusIntf;
+
+ACTION(SdbusThrow)
+{
+    EXPECT_CALL(sdbusIntf, sd_bus_error_set_errno(NotNull(), _))
+        .WillOnce(Return(0));
+    EXPECT_CALL(sdbusIntf, sd_bus_error_is_set(NotNull())).WillOnce(Return(1));
+    EXPECT_CALL(sdbusIntf, sd_bus_error_free(NotNull())).Times(1);
+
+    throw sdbusplus::exception::SdBusError(0, "", &sdbusIntf);
+}
+
+class HothCommandCommitTest : public HothCommandTest
+{
+  protected:
+    void expectValidCommand(std::string_view name,
+                            const std::vector<uint8_t>& input)
+    {
+        EXPECT_CALL(dbus, SendHostCommand(name, ContainerEq(input), _))
+            .WillOnce([&](auto, const auto&, auto cb) {
+                this->cb = std::move(cb);
+                return stdplus::Cancel(std::nullopt);
+            });
+    }
+    internal::DbusCommand::Cb cb;
+};
+
+TEST_F(HothCommandCommitTest, InvalidSessionCommitIsRejected)
+{
+    // Verify the hoth command handler checks for a valid session.
+
+    EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>()));
+}
+
+TEST_F(HothCommandCommitTest, UnexpectedDataParam)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>({1, 2, 3})));
+}
+
+TEST_F(HothCommandCommitTest, DbusCallFail)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    expectValidCommand("", test_buf);
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+
+    cb(std::nullopt);
+
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(blobs::StateFlags::commit_error | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+}
+
+TEST_F(HothCommandCommitTest, EmptyLegacyHoth)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    expectValidCommand("", {});
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+
+    cb(std::vector<uint8_t>{});
+
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(0, meta.size);
+    EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+}
+
+TEST_F(HothCommandCommitTest, EmptyNamedHoth)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view(name))).WillOnce(Return(true));
+    expectValidCommand(name, {});
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), namedPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+
+    cb(std::vector<uint8_t>{});
+
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(0, meta.size);
+    EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+}
+
+// Tests the full commit process with example data
+TEST_F(HothCommandCommitTest, HappyPath)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    expectValidCommand("", test_buf);
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    cb(test2_buf);
+
+    std::vector<uint8_t> result = hvn.read(session_, 0, test2_buf.size());
+    EXPECT_THAT(result, ContainerEq(test2_buf));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+
+    EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+}
+
+// Tests that repeated commits only result in one D-Bus call
+TEST_F(HothCommandCommitTest, IdempotentSuccess)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    expectValidCommand("", {});
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+
+    cb(std::vector<uint8_t>{});
+
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+}
+
+// Tests that repeated commits will retry DBus calls if there is an error
+TEST_F(HothCommandCommitTest, ErrorRetry)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    expectValidCommand("", {});
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+
+    cb(std::nullopt);
+
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(blobs::StateFlags::commit_error | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+
+    expectValidCommand("", {});
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+                  blobs::StateFlags::open_write,
+              meta.blobState);
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_delete_unittest.cpp b/command/test/hoth_command_delete_unittest.cpp
new file mode 100644
index 0000000..5af5d49
--- /dev/null
+++ b/command/test/hoth_command_delete_unittest.cpp
@@ -0,0 +1,32 @@
+// Copyright 2024 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 "hoth_command_unittest.hpp"
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+class HothCommandDeleteTest : public HothCommandTest
+{};
+
+TEST_F(HothCommandDeleteTest, VerifyHothDeleteFails)
+{
+    // The hoth command handler does not support delete.
+
+    EXPECT_FALSE(hvn.deleteBlob(legacyPath));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_open_unittest.cpp b/command/test/hoth_command_open_unittest.cpp
new file mode 100644
index 0000000..4420b9a
--- /dev/null
+++ b/command/test/hoth_command_open_unittest.cpp
@@ -0,0 +1,96 @@
+// Copyright 2024 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 "hoth_command_unittest.hpp"
+
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothCommandOpenTest : public HothCommandTest
+{};
+
+TEST_F(HothCommandOpenTest, OpenWithBadFlagsFails)
+{
+    // Hoth command handler open requires both read & write set.
+
+    EXPECT_FALSE(hvn.open(session_, blobs::OpenFlags::read, legacyPath));
+}
+
+TEST_F(HothCommandOpenTest, OpenWithNoHothd)
+{
+    // Hoth command handler open without a backing hoth daemon present.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(false));
+    EXPECT_FALSE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+}
+
+TEST_F(HothCommandOpenTest, OpenEverythingSucceeds)
+{
+    // Hoth command handler open with everything correct.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+}
+
+TEST_F(HothCommandOpenTest, OpenEleventhSessionFails)
+{
+    // Hoth command handler only allows ten open sessions, verifies the 11th
+    // fails.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+        .WillRepeatedly(Return(true));
+    EXPECT_CALL(dbus, pingHothd(std::string_view(name)))
+        .WillRepeatedly(Return(true));
+    size_t sessId = 0;
+
+    for (int i = 0; i < hvn.maxSessions(); i++)
+    {
+        EXPECT_TRUE(hvn.open(sessId++, hvn.requiredFlags(), legacyPath));
+    }
+
+    for (int i = 0; i < hvn.maxSessions() - 3; i++)
+    {
+        EXPECT_TRUE(hvn.open(sessId++, hvn.requiredFlags(), namedPath));
+    }
+
+    EXPECT_FALSE(hvn.open(sessId++, hvn.requiredFlags(), legacyPath));
+
+    for (int i = hvn.maxSessions() - 3; i < hvn.maxSessions(); i++)
+    {
+        EXPECT_TRUE(hvn.open(sessId++, hvn.requiredFlags(), namedPath));
+    }
+
+    EXPECT_FALSE(hvn.open(sessId++, hvn.requiredFlags(), namedPath));
+}
+
+TEST_F(HothCommandOpenTest, CannotOpenSameSessionTwice)
+{
+    // Verify the hoth command handler won't let you open the same session
+    // twice.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+        .WillRepeatedly(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_read_unittest.cpp b/command/test/hoth_command_read_unittest.cpp
new file mode 100644
index 0000000..bb9c2b7
--- /dev/null
+++ b/command/test/hoth_command_read_unittest.cpp
@@ -0,0 +1,122 @@
+// Copyright 2024 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 "hoth_command_unittest.hpp"
+
+#include <ipmid/handler.hpp>
+
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+using ::testing::Return;
+
+namespace ipmi_hoth
+{
+
+class HothCommandReadTest : public HothCommandTest
+{
+  protected:
+    const uint32_t testOffset_ = 0;
+    const std::vector<uint8_t> testData_ = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+    void openAndWriteTestData()
+    {
+        EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+            .WillOnce(Return(true));
+        EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+        EXPECT_TRUE(hvn.write(session_, testOffset_, testData_));
+    }
+};
+
+TEST_F(HothCommandReadTest, InvalidSessionReadIsRejected)
+{
+    // Verify that read checks for a valid session, returns empty buffer for
+    // failed check.
+
+    openAndWriteTestData();
+
+    uint16_t wrongSession = session_ + 1;
+    EXPECT_THROW(hvn.read(wrongSession, testOffset_, testData_.size()),
+                 ipmi::HandlerCompletion);
+}
+
+TEST_F(HothCommandReadTest, ReadOffsetBeyondBufferSizeReturnsEmpty)
+{
+    // Verify that read with offset beyond buffer size returns empty buffer.
+
+    openAndWriteTestData();
+
+    uint32_t offsetBeyondBuffer = testData_.size();
+    EXPECT_THAT(hvn.read(session_, offsetBeyondBuffer, testData_.size()),
+                IsEmpty());
+}
+
+TEST_F(HothCommandReadTest, ReadFullWrittenData)
+{
+    // Verify that the read successfully reads back the written data.
+
+    openAndWriteTestData();
+
+    EXPECT_EQ(testData_, hvn.read(session_, testOffset_, testData_.size()));
+}
+
+TEST_F(HothCommandReadTest, ReadWrittenDataAtOffset)
+{
+    // Verify that the read with offset returns the expected data.
+
+    openAndWriteTestData();
+
+    EXPECT_EQ(testData_, hvn.read(session_, testOffset_, testData_.size()));
+
+    // Try reading the written data byte by byte at each offset
+    for (size_t i = 0; i < testData_.size(); ++i)
+    {
+        EXPECT_THAT(hvn.read(session_, i, 1), ElementsAre(testData_[i]));
+    }
+}
+
+TEST_F(HothCommandReadTest, ReadFullWrittenDataWithBiggerRequestedSize)
+{
+    // Verify that read with requested size bigger than the written data will
+    // return a response buffer up to the end of the written buffer.
+
+    openAndWriteTestData();
+
+    uint32_t requestedSizeBeyondBuffer = testData_.size() + 1;
+    EXPECT_EQ(testData_,
+              hvn.read(session_, testOffset_, requestedSizeBeyondBuffer));
+}
+
+TEST_F(HothCommandReadTest, ReadWrittenDataAtOffsetWithBiggerRequestedSize)
+{
+    // Verify that read with requested size bigger than the written data at an
+    // offset will return a response buffer up to the end of the written buffer.
+
+    openAndWriteTestData();
+
+    uint32_t newOffset = testData_.size() / 2;
+    uint32_t requestedSizeBeyondBuffer = testData_.size() + 1;
+    std::vector<uint8_t> expectedData =
+        std::vector<uint8_t>(testData_.begin() + newOffset, testData_.end());
+
+    EXPECT_EQ(expectedData,
+              hvn.read(session_, newOffset, requestedSizeBeyondBuffer));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_sessionstat_unittest.cpp b/command/test/hoth_command_sessionstat_unittest.cpp
new file mode 100644
index 0000000..7f6876f
--- /dev/null
+++ b/command/test/hoth_command_sessionstat_unittest.cpp
@@ -0,0 +1,78 @@
+// Copyright 2024 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 "hoth_command_unittest.hpp"
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothCommandSessionStatTest : public HothCommandTest
+{
+  protected:
+    blobs::BlobMeta meta_;
+    // Initialize expected_meta_ with empty members
+    blobs::BlobMeta expected_meta_ = {};
+};
+
+TEST_F(HothCommandSessionStatTest, InvalidSessionStatIsRejected)
+{
+    // Verify the hoth command handler checks for a valid session.
+
+    EXPECT_FALSE(hvn.stat(session_, &meta_));
+}
+
+TEST_F(HothCommandSessionStatTest, SessionStatAlwaysInitialReadAndWrite)
+{
+    // Verify the session stat returns the information for a session.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+    expected_meta_.blobState =
+        blobs::StateFlags::open_read | blobs::StateFlags::open_write;
+    EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothCommandSessionStatTest, AfterWriteMetadataLengthMatches)
+{
+    // Verify that after writes, the length returned matches.
+
+    std::vector<uint8_t> data = {0x01};
+    EXPECT_EQ(1, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.write(session_, (hvn.maxBufferSize() - 1), data));
+
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+    // We wrote one byte to the last index, making the length the buffer size.
+    expected_meta_.size = hvn.maxBufferSize();
+    expected_meta_.blobState =
+        blobs::StateFlags::open_read | blobs::StateFlags::open_write;
+    EXPECT_EQ(meta_, expected_meta_);
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_unittest.cpp b/command/test/hoth_command_unittest.cpp
new file mode 100644
index 0000000..97e1bf3
--- /dev/null
+++ b/command/test/hoth_command_unittest.cpp
@@ -0,0 +1,84 @@
+// Copyright 2024 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 "hoth_command_unittest.hpp"
+
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+namespace ipmi_hoth
+{
+
+class HothCommandBasicTest : public HothCommandTest
+{};
+
+TEST_F(HothCommandBasicTest, PathToHothId)
+{
+    EXPECT_EQ("", hvn.pathToHothId("/dev/hoth/command_passthru"));
+    EXPECT_EQ("hoth0", hvn.pathToHothId("/dev/hoth/hoth0/command_passthru"));
+    EXPECT_EQ("", hvn.pathToHothId("/dev/hoth//command_passthru"));
+    EXPECT_EQ("", hvn.pathToHothId("/dev/hoth////command_passthru"));
+}
+
+TEST_F(HothCommandBasicTest, HothIdToPath)
+{
+    EXPECT_EQ("/dev/hoth/command_passthru", hvn.hothIdToPath(""));
+    EXPECT_EQ("/dev/hoth/hoth0/command_passthru", hvn.hothIdToPath("hoth0"));
+}
+
+TEST_F(HothCommandBasicTest, CanHandleBlobChecksNameInvalid)
+{
+    // Verify canHandleBlob checks and returns false on an invalid name.
+
+    EXPECT_FALSE(hvn.canHandleBlob("asdf"));
+    EXPECT_FALSE(hvn.canHandleBlob("dev/hoth/command_passthru"));
+    EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/command_passthru2"));
+    EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/hoth0/t/command_passthru"));
+    EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/firmware_update"));
+}
+
+TEST_F(HothCommandBasicTest, CanHandleBlobChecksNameValid)
+{
+    // Verify canHandleBlob checks and returns true on the valid name.
+
+    EXPECT_TRUE(hvn.canHandleBlob("/dev/hoth/command_passthru"));
+    EXPECT_TRUE(hvn.canHandleBlob("/dev/hoth/hoth0/command_passthru"));
+}
+
+TEST_F(HothCommandBasicTest, VerifyBehaviorOfBlobIds)
+{
+    // Verify the correct BlobIds is returned from the hoth command handler.
+    internal::Dbus::SubTreeMapping mapping = {
+        {"/xyz/openbmc_project/Control", {}},
+        {"/xyz/openbmc_project/Control/Hoth2nologue", {}},
+        {"/xyz/openbmc_project/Control/Hoth/nologue/2", {}},
+        {"/xyz/openbmc_project/Control/Hoth", {}},
+        {"/xyz/openbmc_project/Control/Hoth/hoth0", {}},
+        {"/xyz/openbmc_project/Control/Hoth/hoth1", {}},
+    };
+    EXPECT_CALL(dbus, getHothdMapping()).WillOnce(Return(mapping));
+    std::vector<std::string> expected = {
+        "/dev/hoth/command_passthru",
+        "/dev/hoth/hoth0/command_passthru",
+        "/dev/hoth/hoth1/command_passthru",
+    };
+    EXPECT_THAT(hvn.getBlobIds(), UnorderedElementsAreArray(expected));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_unittest.hpp b/command/test/hoth_command_unittest.hpp
new file mode 100644
index 0000000..bf64731
--- /dev/null
+++ b/command/test/hoth_command_unittest.hpp
@@ -0,0 +1,48 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus_command_mock.hpp"
+#include "hoth_command.hpp"
+
+#include <stdplus/util/string.hpp>
+
+#include <cstdint>
+#include <string>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+class HothCommandTest : public ::testing::Test
+{
+  protected:
+    explicit HothCommandTest() : hvn(&dbus) {}
+
+    // dbus mock object
+    testing::StrictMock<internal::DbusCommandMock> dbus;
+
+    HothCommandBlobHandler hvn;
+
+    const uint16_t session_ = 0;
+    const std::string legacyPath = stdplus::util::strCat(
+        HothCommandBlobHandler::hothPathPrefix, hvn.pathSuffix());
+    const std::string name = "hoth0";
+    const std::string namedPath = stdplus::util::strCat(
+        HothCommandBlobHandler::hothPathPrefix, name, "/", hvn.pathSuffix());
+};
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_write_unittest.cpp b/command/test/hoth_command_write_unittest.cpp
new file mode 100644
index 0000000..702e633
--- /dev/null
+++ b/command/test/hoth_command_write_unittest.cpp
@@ -0,0 +1,133 @@
+// Copyright 2024 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 "hoth_command_unittest.hpp"
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothCommandWriteTest : public HothCommandTest
+{};
+
+TEST_F(HothCommandWriteTest, InvalidSessionWriteIsRejected)
+{
+    // Verify the hoth command handler checks for a valid session.
+
+    std::vector<uint8_t> data = {0x1, 0x2};
+
+    EXPECT_FALSE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothCommandWriteTest, WritingTooMuchByOneByteFails)
+{
+    // Test the edge case of writing 1 byte too much with an offset of 0.
+    // writing 1025 at offset 0 is invalid.
+
+    int bytes = hvn.maxBufferSize() + 1;
+    std::vector<uint8_t> data(0x11);
+    data.resize(bytes);
+    ASSERT_EQ(bytes, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothCommandWriteTest, WritingTooMuchByOffsetOfOne)
+{
+    // Test the edge case of writing 1024 bytes (which is fine) but at the
+    // offset 1, which makes it go over by 1 byte.
+    // writing 1024 at offset 1 is invalid.
+
+    int bytes = hvn.maxBufferSize();
+    std::vector<uint8_t> data(0x11);
+    data.resize(bytes);
+    ASSERT_EQ(bytes, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    // offset of 1.
+    EXPECT_FALSE(hvn.write(session_, 1, data));
+}
+
+TEST_F(HothCommandWriteTest, WritingOneByteBeyondEndFromOffsetFails)
+{
+    // Test the edge case of writing to the last byte offset but trying to
+    // write two bytes.
+    // writing 2 bytes at 1023 is invalid.
+
+    std::vector<uint8_t> data = {0x01, 0x02};
+    ASSERT_EQ(2, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.write(session_, (hvn.maxBufferSize() - 1), data));
+}
+
+TEST_F(HothCommandWriteTest, WritingOneByteAtOffsetBeyondEndFails)
+{
+    // Test the edge case of writing one byte but exactly one byte beyond the
+    // buffer.
+    // writing 1 byte at 1024 is invalid.
+
+    std::vector<uint8_t> data = {0x01};
+    ASSERT_EQ(1, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.write(session_, hvn.maxBufferSize(), data));
+}
+
+TEST_F(HothCommandWriteTest, WritingFullBufferAtOffsetZeroSucceeds)
+{
+    // Test the case where you write the full buffer length at once to the 0th
+    // offset.
+
+    int bytes = hvn.maxBufferSize();
+    std::vector<uint8_t> data = {0x01};
+    data.resize(bytes);
+    ASSERT_EQ(bytes, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothCommandWriteTest, WritingOneByteToTheLastOffsetSucceeds)
+{
+    // Test the case where you write the last byte.
+
+    std::vector<uint8_t> data = {0x01};
+    ASSERT_EQ(1, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.write(session_, (hvn.maxBufferSize() - 1), data));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/meson.build b/command/test/meson.build
new file mode 100644
index 0000000..2adea16
--- /dev/null
+++ b/command/test/meson.build
@@ -0,0 +1,20 @@
+tests = [
+  'hoth_command_unittest',
+  'hoth_command_close_unittest',
+  'hoth_command_commit_unittest',
+  'hoth_command_delete_unittest',
+  'hoth_command_open_unittest',
+  'hoth_command_read_unittest',
+  'hoth_command_sessionstat_unittest',
+  'hoth_command_write_unittest',
+]
+
+foreach t : tests
+  test(
+    t,
+    executable(
+      t.underscorify(),
+      t + '.cpp',
+      implicit_include_directories: false,
+      dependencies: [hothcommand_dep, test_dep]))
+endforeach
diff --git a/dbus.cpp b/dbus.cpp
new file mode 100644
index 0000000..3a38ac8
--- /dev/null
+++ b/dbus.cpp
@@ -0,0 +1,98 @@
+// Copyright 2024 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 "dbus.hpp"
+
+#include "dbus_impl.hpp"
+
+#include <ipmid/api.h>
+
+#include <sdbusplus/exception.hpp>
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+DbusImpl::DbusImpl() : bus(ipmid_get_sd_bus_connection()) {}
+
+Dbus::SubTreeMapping DbusImpl::getHothdMapping()
+{
+    auto req =
+        bus.new_method_call("xyz.openbmc_project.ObjectMapper",
+                            "/xyz/openbmc_project/object_mapper",
+                            "xyz.openbmc_project.ObjectMapper", "GetSubTree");
+    req.append("/");
+    req.append(0);
+    req.append(std::vector<std::string>({"xyz.openbmc_project.Control.Hoth"}));
+    auto rsp = bus.call(req, kTimeout);
+    SubTreeMapping mapping;
+    rsp.read(mapping);
+    return mapping;
+}
+
+std::string hothIdToSvc(std::string_view hothId)
+{
+    std::string svc = "xyz.openbmc_project.Control.Hoth";
+    if (!hothId.empty())
+    {
+        svc += ".";
+        svc += hothId;
+    }
+    return svc;
+}
+
+bool DbusImpl::pingHothd(std::string_view hothId)
+{
+    try
+    {
+        auto req =
+            bus.new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus",
+                                "org.freedesktop.DBus", "GetNameOwner");
+        req.append(hothIdToSvc(hothId));
+        bus.call(req, kTimeout);
+        return true;
+    }
+    catch (const sdbusplus::exception::SdBusError& e)
+    {
+        return false;
+    }
+}
+
+sdbusplus::message::message DbusImpl::newHothdCall(
+    std::string_view hothId, const char* intf, const char* method)
+{
+    std::string svc = hothIdToSvc(hothId);
+    std::string obj = "/xyz/openbmc_project/Control/Hoth";
+    if (!hothId.empty())
+    {
+        obj += "/";
+        obj += hothId;
+    }
+    return bus.new_method_call(svc.c_str(), obj.c_str(), intf, method);
+}
+
+sdbusplus::message::message
+    DbusImpl::newHothdCall(std::string_view hothId, const char* method)
+{
+    return newHothdCall(hothId, "xyz.openbmc_project.Control.Hoth", method);
+}
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/dbus.hpp b/dbus.hpp
new file mode 100644
index 0000000..b01ad1d
--- /dev/null
+++ b/dbus.hpp
@@ -0,0 +1,74 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include <sdbusplus/sdbus.hpp>
+#include <sdbusplus/slot.hpp>
+#include <stdplus/cancel.hpp>
+
+#include <chrono>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+struct BusCall : public stdplus::Cancelable
+{
+    sdbusplus::slot::slot slot;
+    explicit BusCall(sdbusplus::slot::slot&& slot) : slot(std::move(slot)) {}
+    void cancel() noexcept override
+    {
+        delete this;
+    }
+};
+
+/** @class Dbus
+ *  @brief Overridable D-Bus interface for generic handler
+ */
+class Dbus
+{
+  public:
+    /** An arbitrary timeout to ensure that clients don't linger forever */
+    static constexpr auto asyncCallTimeout = std::chrono::minutes(3);
+
+    using SubTreeMapping = std::unordered_map<
+        std::string, std::unordered_map<std::string, std::vector<std::string>>>;
+
+    virtual ~Dbus() = default;
+
+    /** @brief Gets the D-Bus mapper information for all hoth instances
+     *
+     *  @throw exception::SdBusError - All dbus exceptions will be thrown
+     *                                 with this exception
+     *  @return The mapping of object paths and services to hoth interfaces.
+     */
+    virtual SubTreeMapping getHothdMapping() = 0;
+
+    /** @brief Determines if a hothd instance is running on the system
+     *
+     *  @return True if the hoth is running, false for other errors.
+     */
+    virtual bool pingHothd(std::string_view hothId) = 0;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/dbus_impl.hpp b/dbus_impl.hpp
new file mode 100644
index 0000000..022c0b3
--- /dev/null
+++ b/dbus_impl.hpp
@@ -0,0 +1,73 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus.hpp"
+
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <chrono>
+#include <string_view>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class DbusImpl
+ *  @brief D-Bus concrete implementation
+ *  @details Pass through all calls to the default D-Bus instance
+ */
+class DbusImpl : public virtual Dbus
+{
+  public:
+    /** @brief Timeout suitable for responding to IPMI queries before
+     *         the sending mechanism like kcsbridge issues a retry.
+     */
+    static constexpr auto kTimeout = std::chrono::seconds(4);
+
+    DbusImpl();
+
+    SubTreeMapping getHothdMapping() override;
+    bool pingHothd(std::string_view hothId) override;
+
+  protected:
+    /** @brief Helper for building a message targeting a hoth daemon
+     *
+     *  @param[in] hothId - The identifier of the hoth daemon
+     *  @param[in] intf    - The name of the D-Bus interface being called
+     *  @param[in] method  - The D-Bus method being called on the interface
+     *  @return The empty message which can be sent.
+     */
+    sdbusplus::message::message newHothdCall(
+        std::string_view hothId, const char* intf, const char* method);
+
+    /** @brief Helper for building a message targeting a hoth daemon
+     *         assuming the call is for the hoth interface.
+     *
+     *  @param[in] hothId - The identifier of the hoth daemon
+     *  @param[in] method  - The D-Bus method being called on the interface
+     *  @return The empty message which can be sent.
+     */
+    sdbusplus::message::message newHothdCall(std::string_view hothId,
+                                             const char* method);
+
+    sdbusplus::bus::bus bus;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/hoth.cpp b/hoth.cpp
new file mode 100644
index 0000000..d3918e7
--- /dev/null
+++ b/hoth.cpp
@@ -0,0 +1,267 @@
+// Copyright 2024 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 "hoth.hpp"
+
+#include <ipmid/api-types.hpp>
+#include <ipmid/handler.hpp>
+#include <stdplus/util/string.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+
+std::string_view HothBlobHandler::pathToHothId(std::string_view path)
+{
+    if (path.starts_with(hothPathPrefix) && path.ends_with(pathSuffix()))
+    {
+        auto id = path.substr(
+            hothPathPrefix.size(),
+            path.size() - pathSuffix().size() - hothPathPrefix.size());
+        auto count = std::count(id.begin(), id.end(), '/');
+
+        // There can only be prefix + pathSuffix or
+        // prefix + id/pathSuffix.
+        // prefix already has one `/`
+        if (count == 1)
+        {
+            return id.substr(0, id.size() - 1);
+        }
+        else if (count > 1)
+        {
+            return "";
+        }
+
+        return id;
+    }
+    return "";
+}
+
+std::string HothBlobHandler::hothIdToPath(std::string_view hothId)
+{
+    if (hothId.empty())
+    {
+        return stdplus::util::strCat(hothPathPrefix, pathSuffix());
+    }
+    return stdplus::util::strCat(hothPathPrefix, hothId, "/", pathSuffix());
+}
+
+HothBlob* HothBlobHandler::getSession(uint16_t id)
+{
+    auto search = sessions.find(id);
+    if (search == sessions.end())
+    {
+        return nullptr;
+    }
+
+    /* Not thread-safe, however, the blob handler deliberately assumes serial
+     * execution. */
+    return search->second.get();
+}
+
+std::optional<uint16_t> HothBlobHandler::getOnlySession(std::string_view hothId)
+{
+    /* This method is only valid if we only allow 1 session */
+    if (maxSessions() != 1)
+    {
+        return std::nullopt;
+    }
+
+    for (auto& session : pathSessions[std::string(hothId)])
+    {
+        return session;
+    }
+
+    return std::nullopt;
+}
+
+bool HothBlobHandler::canHandleBlob(const std::string& path)
+{
+    if (path.starts_with(hothPathPrefix) && path.ends_with(pathSuffix()))
+    {
+        // There can only be hothPathPrefix + pathSuffix or
+        // hothPathPrefix + id/pathSuffix
+        // hothPathPrefix already has one `/`
+        return std::count(path.begin() + hothPathPrefix.size(),
+                          path.end() - pathSuffix().size(), '/') <= 1;
+    }
+    return false;
+}
+
+std::vector<std::string> HothBlobHandler::getBlobIds()
+{
+    std::vector<std::string> ret;
+    for (const auto& obj : dbus().getHothdMapping())
+    {
+        std::string_view objView = obj.first;
+        std::string_view objPrefix = "/xyz/openbmc_project/Control/Hoth";
+        if (objView.substr(0, objPrefix.size()) != objPrefix)
+        {
+            continue;
+        }
+        objView.remove_prefix(objPrefix.size());
+        if (objView.empty())
+        {
+            ret.push_back(hothIdToPath(""));
+            continue;
+        }
+        if (objView[0] != '/')
+        {
+            continue;
+        }
+        objView.remove_prefix(1);
+        auto sep = objView.find('/');
+        if (sep != std::string_view::npos)
+        {
+            continue;
+        }
+        ret.push_back(hothIdToPath(objView));
+    }
+    return ret;
+}
+
+bool HothBlobHandler::deleteBlob(const std::string&)
+{
+    /* Hoth blob handler does not support a blob delete. */
+    return false;
+}
+
+bool HothBlobHandler::open(uint16_t session, uint16_t flags,
+                           const std::string& path)
+{
+    /* We require both flags set. */
+    if ((flags & requiredFlags()) != requiredFlags())
+    {
+        /* Both flags not set. */
+        return false;
+    }
+
+    auto findSess = sessions.find(session);
+    if (findSess != sessions.end())
+    {
+        /* This session is already active. */
+        return false;
+    }
+
+    auto hothId = std::string(pathToHothId(path));
+    auto pathSession = pathSessions.find(hothId);
+    if (pathSession != pathSessions.end() &&
+        pathSession->second.size() >= maxSessions())
+    {
+        return false;
+    }
+
+    // Prevent host from adding lots of bad entries to the table by verifying
+    // the hoth exists.
+    if (pathSession == pathSessions.end() && !dbus().pingHothd(hothId))
+    {
+        return false;
+    }
+
+    pathSessions[hothId].emplace(session);
+    sessions.emplace(session,
+                     std::make_unique<HothBlob>(session, std::move(hothId),
+                                                flags, maxBufferSize()));
+    return true;
+}
+
+std::vector<uint8_t> HothBlobHandler::read(uint16_t session, uint32_t offset,
+                                           uint32_t requestedSize)
+{
+    HothBlob* sess = getSession(session);
+    if (!sess || !(sess->state & blobs::StateFlags::open_read) ||
+        offset > sess->buffer.size())
+    {
+        throw ipmi::HandlerCompletion(ipmi::ccUnspecifiedError);
+    }
+
+    if (sess->buffer.size() > offset)
+    {
+        std::vector<uint8_t> ret(
+            std::min<size_t>(requestedSize, sess->buffer.size() - offset));
+        std::memcpy(ret.data(), sess->buffer.data() + offset, ret.size());
+        return ret;
+    }
+    return {};
+}
+
+bool HothBlobHandler::write(uint16_t session, uint32_t offset,
+                            const std::vector<uint8_t>& data)
+{
+    uint32_t newBufferSize = data.size() + offset;
+    HothBlob* sess = getSession(session);
+    if (!sess || !(sess->state & blobs::StateFlags::open_write) ||
+        newBufferSize > maxBufferSize())
+    {
+        return false;
+    }
+
+    /* Resize the buffer if what we're writing will go over the size */
+    if (newBufferSize > sess->buffer.size())
+    {
+        sess->buffer.resize(newBufferSize);
+        sess->state &= ~blobs::StateFlags::committed;
+    }
+
+    /* Clear the comitted bit if our data isn't identical to existing data */
+    if (std::memcmp(sess->buffer.data() + offset, data.data(), data.size()))
+    {
+        sess->state &= ~blobs::StateFlags::committed;
+    }
+    if (!data.empty())
+    {
+        std::memcpy(sess->buffer.data() + offset, data.data(), data.size());
+    }
+    return true;
+}
+
+bool HothBlobHandler::writeMeta(uint16_t, uint32_t, const std::vector<uint8_t>&)
+{
+    /* Hoth blob handler does not support meta write. */
+    return false;
+}
+
+bool HothBlobHandler::close(uint16_t session)
+{
+    auto session_it = sessions.find(session);
+    if (session_it == sessions.end())
+    {
+        return false;
+    }
+
+    auto path_it = pathSessions.find(session_it->second->hothId);
+    path_it->second.erase(session);
+    if (path_it->second.empty())
+    {
+        pathSessions.erase(path_it);
+    }
+
+    sessions.erase(session_it);
+    return true;
+}
+
+bool HothBlobHandler::expire(uint16_t session)
+{
+    return close(session);
+}
+
+} // namespace ipmi_hoth
diff --git a/hoth.hpp b/hoth.hpp
new file mode 100644
index 0000000..37dac70
--- /dev/null
+++ b/hoth.hpp
@@ -0,0 +1,128 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+#include <stdplus/cancel.hpp>
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace ipmi_hoth
+{
+
+struct HothBlob
+{
+    HothBlob(uint16_t id, std::string&& hothId, uint16_t flags,
+             uint32_t maxBufferSize) :
+        sessionId(id), hothId(std::move(hothId)), state(0)
+    {
+        if (flags & blobs::OpenFlags::read)
+        {
+            state |= blobs::StateFlags::open_read;
+        }
+        if (flags & blobs::OpenFlags::write)
+        {
+            state |= blobs::StateFlags::open_write;
+        }
+
+        /* Pre-allocate the buffer.capacity() with maxBufferSize */
+        buffer.reserve(maxBufferSize);
+    }
+    ~HothBlob()
+    {
+        /* We want to deliberately wipe the buffer to avoid leaking any
+         * sensitive ProdID secrets.
+         */
+        buffer.assign(buffer.capacity(), 0);
+    }
+
+    /* The blob handler session id. */
+    uint16_t sessionId;
+
+    /* The identifier for the hoth */
+    std::string hothId;
+
+    /* The current state. */
+    uint16_t state;
+
+    /* The staging buffer. */
+    std::vector<uint8_t> buffer;
+
+    /* Outstanding async operation */
+    stdplus::Cancel outstanding;
+};
+
+class HothBlobHandler : public blobs::GenericBlobInterface
+{
+  public:
+    static constexpr std::string_view hothPathPrefix = "/dev/hoth/";
+
+    bool canHandleBlob(const std::string& path) override;
+    std::vector<std::string> getBlobIds() override;
+    bool deleteBlob(const std::string& path) override;
+    virtual bool stat(const std::string& path, blobs::BlobMeta* meta) = 0;
+    bool open(uint16_t session, uint16_t flags,
+              const std::string& path) override;
+    std::vector<uint8_t> read(uint16_t session, uint32_t offset,
+                              uint32_t requestedSize) override;
+    bool write(uint16_t session, uint32_t offset,
+               const std::vector<uint8_t>& data) override;
+    bool writeMeta(uint16_t session, uint32_t offset,
+                   const std::vector<uint8_t>& data) override;
+    virtual bool commit(uint16_t session, const std::vector<uint8_t>& data) = 0;
+    bool close(uint16_t session) override;
+    virtual bool stat(uint16_t session, blobs::BlobMeta* meta) = 0;
+    bool expire(uint16_t session) override;
+
+    virtual internal::Dbus& dbus() = 0;
+    virtual std::string_view pathSuffix() const = 0;
+    virtual uint16_t requiredFlags() const = 0;
+    virtual uint16_t maxSessions() const = 0;
+    virtual uint32_t maxBufferSize() const = 0;
+
+    /** @brief Takes a valid hoth blob path and turns it into a hoth id
+     *
+     *  @param[in] path - Hoth blob path to parse
+     *  @return The hoth id represented by the path
+     */
+    std::string_view pathToHothId(std::string_view path);
+
+    /** @brief Takes a hoth id and turns it into a fully qualified path for
+     *         the current hoth handler.
+     *
+     *  @param[in] hothId - The hoth id
+     *  @return The fully qualified blob path
+     */
+    std::string hothIdToPath(std::string_view hothId);
+
+  protected:
+    HothBlob* getSession(uint16_t id);
+    std::optional<uint16_t> getOnlySession(std::string_view hothId);
+
+  private:
+    std::unordered_map<std::string, std::unordered_set<uint16_t>> pathSessions;
+    std::unordered_map<uint16_t, std::unique_ptr<HothBlob>> sessions;
+};
+
+} // namespace ipmi_hoth
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..cdab3c1
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,54 @@
+project(
+  'hoth-ipmi-blobs',
+  'cpp',
+  version: '0.1',
+  meson_version: '>=1.1.1',
+  default_options: [
+    'b_lundef=false',
+    'cpp_std=c++23',
+    'warning_level=3',
+    'werror=true',
+  ])
+
+sdbusplus_dep = dependency('sdbusplus', fallback: ['sdbusplus','sdbusplus_dep'])
+stdplus_dep = dependency('stdplus', fallback: ['stdplus','stdplus_dep'])
+ipmi_blob_dep = dependency('phosphor-ipmi-blobs')
+phosphor_logging_dep = dependency('phosphor-logging')
+
+meson.get_compiler('cpp').has_header_symbol(
+  'ipmid/api.h',
+  'ipmid_get_sd_bus_connection')
+
+hothblob_pre = declare_dependency(
+  include_directories: include_directories('.'),
+  dependencies: [
+    ipmi_blob_dep,
+    sdbusplus_dep,
+    stdplus_dep,
+  ])
+
+hothblob_lib = library(
+  'hothblob',
+  'dbus.cpp',
+  'hoth.cpp',
+  implicit_include_directories: false,
+  dependencies: hothblob_pre,
+  version: meson.project_version(),
+  install: true)
+
+hothblob_dep = declare_dependency(
+  link_with: hothblob_lib,
+  dependencies: hothblob_pre)
+
+if not get_option('tests').disabled()
+  subdir('test')
+endif
+
+subdir('command')
+if get_option('skm')
+  subdir('skmhss')
+endif
+
+if get_option('hoth-inband-update').allowed()
+  subdir('update')
+endif
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..86a8705
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,4 @@
+option('tests', type: 'feature', description: 'Build tests')
+option('skm', type: 'boolean', description: 'Support SKM')
+option('skmhss-hothid-config', type: 'string', value: '/usr/share/hoth-ipmi-blobs/hothid_basepaths.json', description: 'Path to basepath to hothId mapping Json config')
+option('hoth-inband-update', type: 'feature', description: 'Support hoth firmware update via IPMI')
diff --git a/skmhss/google3/g_ec_commands.h b/skmhss/google3/g_ec_commands.h
new file mode 100644
index 0000000..9a93e3c
--- /dev/null
+++ b/skmhss/google3/g_ec_commands.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024 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 SECURITY_CRYPTA_COMMANDS_CHROMIUMOS_G_EC_COMMANDS_H_
+#define SECURITY_CRYPTA_COMMANDS_CHROMIUMOS_G_EC_COMMANDS_H_
+
+#include <stdint.h>
+
+#define G_EC_HOST_REQUEST_VERSION 3
+#define G_EC_HOST_RESPONSE_VERSION 3
+
+#define G_EC_CMD_BOARD_SPECIFIC_BASE 0x3E00
+
+/* Given the private host command offset, calculate the true private host
+ * command value.
+ */
+#define G_EC_PRIVATE_HOST_COMMAND_VALUE(command)                               \
+    (G_EC_CMD_BOARD_SPECIFIC_BASE + (command))
+
+/* Version 3 request from host */
+typedef struct
+{
+    /* Structure version (=3)
+     *
+     * EC will return EC_RES_INVALID_HEADER if it receives a header with a
+     * version it doesn't know how to parse.
+     */
+    uint8_t struct_version;
+
+    /* Checksum of request and data; sum of all bytes including checksum should
+     * total to 0.
+     */
+    uint8_t checksum;
+
+    /* Command code */
+    uint16_t command;
+
+    /* Command version */
+    uint8_t command_version;
+
+    /* Unused byte in current protocol version; set to 0 */
+    uint8_t reserved;
+
+    /* Length of data which follows this header */
+    uint16_t data_len;
+} __attribute__((packed)) g_ec_host_request;
+
+/* Version 3 response from EC */
+typedef struct
+{
+    /* Structure version (=3) */
+    uint8_t struct_version;
+
+    /* Checksum of response and data; sum of all bytes including checksum should
+     * total to 0.
+     */
+    uint8_t checksum;
+
+    /* Result code (EC_RES_*) */
+    uint16_t result;
+
+    /* Length of data which follows this header */
+    uint16_t data_len;
+
+    /* Unused bytes in current protocol version; set to 0 */
+    uint16_t reserved;
+} __attribute__((packed)) g_ec_host_response;
+
+#endif // SECURITY_CRYPTA_COMMANDS_CHROMIUMOS_G_EC_COMMANDS_H_
diff --git a/skmhss/google3/host_commands.h b/skmhss/google3/host_commands.h
new file mode 100644
index 0000000..087f71c
--- /dev/null
+++ b/skmhss/google3/host_commands.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 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 __PRIVATE_CR51_INCLUDE_CR51_HOST_COMMANDS_H
+#define __PRIVATE_CR51_INCLUDE_CR51_HOST_COMMANDS_H
+
+#include <stdint.h>
+
+/* Read/Write/Erase Hoth's copy of SKM HSS persisted to gNVRAM. */
+#define EC_PRV_CMD_HOTH_SKM_HSS 0x0015
+
+#define SKM_HSS_STRUCT_SIZE 64
+enum skm_hss_op
+{
+    SKM_HSS_READ = 0,
+    SKM_HSS_WRITE = 1,
+    SKM_HSS_DELETE = 2,
+};
+
+/* Serves as the response to SKM_HSS_READ as well as payload to SKM_HSS_WRITE.
+ */
+struct ec_payload_skm_hss
+{
+    uint8_t data[SKM_HSS_STRUCT_SIZE];
+} __attribute__((packed));
+
+struct ec_request_skm_hss
+{
+    /* The operation is one of skm_hss_op. */
+    uint32_t operation;
+    /* Choose which (0-3) HSS to operate on. */
+    uint32_t slot;
+    struct ec_payload_skm_hss payload;
+} __attribute__((packed));
+
+#endif /* __PRIVATE_CR51_INCLUDE_CR51_HOST_COMMANDS_H */
diff --git a/skmhss/hoth_skmhss.cpp b/skmhss/hoth_skmhss.cpp
new file mode 100644
index 0000000..fc33250
--- /dev/null
+++ b/skmhss/hoth_skmhss.cpp
@@ -0,0 +1,464 @@
+// Copyright 2024 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 "hoth_skmhss.hpp"
+
+#include "config.hpp"
+
+#include <fmt/format.h>
+#include <google3/host_commands.h>
+
+#include <blobs-ipmid/blobs.hpp>
+#include <nlohmann/json.hpp>
+#include <phosphor-logging/log.hpp>
+#include <stdplus/cancel.hpp>
+#include <xyz/openbmc_project/Control/Hoth/error.hpp>
+
+#include <algorithm>
+#include <charconv>
+#include <cstring>
+#include <fstream>
+#include <memory>
+#include <optional>
+#include <span>
+#include <string>
+#include <vector>
+
+using phosphor::logging::entry;
+using phosphor::logging::log;
+
+using level = phosphor::logging::level;
+using SdBusError = sdbusplus::exception::SdBusError;
+using CommandFailure =
+    sdbusplus::xyz::openbmc_project::Control::Hoth::Error::CommandFailure;
+using ResponseFailure =
+    sdbusplus::xyz::openbmc_project::Control::Hoth::Error::ResponseFailure;
+using json = nlohmann::json;
+
+namespace ipmi_hoth
+{
+
+namespace internal
+{
+
+/**
+ * @brief: Get baseId from a blob id string
+ * @param blobId: Input blob id which is expected to only contain alphanumerical
+ *                characters and '/'.
+ * @returns: The baseId containing the blobId, stripping all contents from the
+ *           last '/'. If no '/' is present, an empty string is returned.
+ */
+static std::string_view getBaseFromID(std::string_view blobId)
+{
+    return blobId.substr(0, blobId.find_last_of('/') + 1);
+}
+
+/**
+ * @brief: Get the blob identifer under the baseId
+ * @param blobId: Input blob id which is expected to only contain alphanumerical
+ *                characters and '/'.
+ * @returns: The truncated blobId, by deleting the baseId prefix
+ *           in the input blobId.
+ */
+static std::string_view stripBaseFromID(std::string_view blobId)
+{
+    return blobId.substr(blobId.find_last_of('/') + 1);
+}
+
+/**
+ * @brief: Get the slot number from a blob id string
+ * @param blobId: Input blob id which is expected to only contain alphanumerical
+ *                characters and '/'.
+ * @returns: The slot number if the blob id has a tailing integer; otherwise
+ * returns a null pointer;
+ */
+static std::optional<uint32_t> extractSlotFromID(std::string_view blobId)
+{
+    std::string_view pathId = internal::stripBaseFromID(blobId);
+    uint32_t slot;
+    const auto res =
+        std::from_chars(pathId.data(), pathId.data() + pathId.size(), slot);
+
+    if (res.ec != std::errc() || res.ptr != pathId.data() + pathId.size())
+    {
+        return std::nullopt;
+    }
+
+    return slot;
+}
+
+} // namespace internal
+
+void HothSKMHSSBlobHandler::loadHSS()
+{
+    readBasePathConfig();
+    for (auto& [basePath, hothId] : hothIdBasePaths)
+    {
+        slotPopulated[basePath] = {};
+        inits[basePath] = {};
+        dels[basePath] = {};
+        /* Iterate all slots and find valid HSS copies */
+        for (uint32_t slot = 0; slot < maxNumHSSSlot; slot++)
+        {
+            auto& init = inits[basePath][slot];
+            init = readSKMHSSSlot(
+                basePath, slot, hothId,
+                [&](std::span<const uint8_t>) noexcept { init.reset(); });
+        }
+    }
+}
+
+/* read the hothid_basepaths.config and store it in hothIdBasePaths*/
+/**
+ * @brief: Read the hothid_basepaths.config and store it in a bimap.
+ *         It populates basePath <--> hothId mapping
+ * @param :
+ * @returns:
+ */
+void HothSKMHSSBlobHandler::readBasePathConfig()
+{
+    std::ifstream configFile(HOTHID_CONFIG);
+
+    // Check if the config file exists
+    if (configFile.fail())
+    {
+        log<level::ERR>(
+            "config file does not exist, populate map with defaults");
+
+        // There is no config file,
+        // So, populate with default values
+        hothIdBasePaths.insert({"/skm/hss-backup/", ""});
+        return;
+    }
+
+    try
+    {
+        json data = json::parse(configFile);
+        for (const auto& [basePath, hothId] : data.items())
+        {
+            hothIdBasePaths.insert({basePath, hothId});
+        }
+    }
+    catch (json::parse_error& ex)
+    {
+        log<level::ERR>("config parse error at ", entry("ERR_MSG=%d", ex.byte));
+    }
+}
+
+/**
+ * @brief: Get the hothId from a blob id string
+ * @param blobId: Input blob id which is expected to only contain alphanumerical
+ *                characters and '/'.
+ * @returns: The hothId from the map hothIdBasePaths if it exists; otherwise
+ * empty string
+ */
+std::string_view
+    HothSKMHSSBlobHandler::getHothIdFromPath(std::string_view blobId)
+{
+    auto base = internal::getBaseFromID(blobId);
+    auto it = hothIdBasePaths.left.find(static_cast<std::string>(base));
+
+    if (it != hothIdBasePaths.left.end())
+    {
+        return it->second;
+    }
+    return "";
+}
+
+/**
+ * @brief: Get the base path from a hothId string
+ * @param hothId: Input hothId string
+ * @returns: The hothId from the bimap hothIdBasePaths if it exists; otherwise
+ * empty string
+ */
+std::string_view
+    HothSKMHSSBlobHandler::getBaseFromHothId(std::string_view hothId)
+{
+    auto it = hothIdBasePaths.right.find(static_cast<std::string>(hothId));
+    if (it != hothIdBasePaths.right.end())
+    {
+        return it->second;
+    }
+    return "";
+}
+
+stdplus::Cancel HothSKMHSSBlobHandler::readSKMHSSSlot(
+    std::string_view base, uint32_t slot, std::string_view hothId,
+    fu2::unique_function<void(std::span<const uint8_t>)>&& cb)
+{
+    auto req = hothUtil_->readSKMHSS(slot);
+    return dbus_->SendHostCommand(
+        hothId, req,
+        [this, base, slot, cb = std::move(cb)](
+            std::optional<std::vector<uint8_t>> rsp) mutable noexcept {
+            try
+            {
+                auto data = hothUtil_->payloadECResponse(rsp.value());
+                this->slotPopulated[base][slot] =
+                    data.size() == SKM_HSS_STRUCT_SIZE;
+                cb(data);
+                return;
+            }
+            catch (const ResponseFailure& e)
+            {
+                log<level::ERR>("Invalid Hoth response",
+                                entry("ERR_MSG=%s", e.what()));
+            }
+            catch (...)
+            {}
+            cb({});
+        });
+}
+
+bool HothSKMHSSBlobHandler::canHandleBlob(const std::string& path)
+{
+    auto base = internal::getBaseFromID(path);
+    if (hothIdBasePaths.left.find(static_cast<std::string>(base)) ==
+        hothIdBasePaths.left.end())
+    {
+        return false;
+    }
+
+    auto hothId = getHothIdFromPath(path);
+    if (!dbus_->pingHothd(hothId))
+    {
+        return false;
+    }
+
+    /* The HSS path is expected to be "/skm/hss-backup/{0,1,2,...}" */
+    std::optional<uint32_t> slotPtr = internal::extractSlotFromID(path);
+    if (!slotPtr)
+    {
+        return false;
+    }
+    return *slotPtr < maxNumHSSSlot;
+}
+
+std::vector<std::string> HothSKMHSSBlobHandler::getBlobIds()
+{
+    std::vector<std::string> result;
+    for (auto& [basePath, hothId] : hothIdBasePaths)
+    {
+        if (!dbus_->pingHothd(hothId))
+        {
+            continue;
+        }
+
+        result.emplace_back(basePath);
+        for (size_t i = 0; i < slotPopulated[basePath].size(); ++i)
+        {
+            if (slotPopulated[basePath][i])
+            {
+                result.emplace_back(fmt::format("{}{}", basePath, i));
+            }
+        }
+    }
+    return result;
+}
+
+bool HothSKMHSSBlobHandler::stat(const std::string& path, blobs::BlobMeta* meta)
+{
+    std::optional<uint32_t> slotPtr = internal::extractSlotFromID(path);
+    auto base = internal::getBaseFromID(path);
+    if (!slotPtr)
+    {
+        return false;
+    }
+    *meta = {};
+    return slotPopulated[base][*slotPtr];
+}
+
+bool HothSKMHSSBlobHandler::open(uint16_t session, uint16_t flags,
+                                 const std::string& path)
+{
+    std::optional<uint32_t> slotPtr = internal::extractSlotFromID(path);
+    if (!slotPtr)
+    {
+        return false;
+    }
+    std::string_view hothId = getHothIdFromPath(path);
+    if (!HothBlobHandler::open(session, flags, hothIdToPath(hothId)))
+    {
+        return false;
+    }
+
+    std::string_view base = internal::getBaseFromID(path);
+    sessionSlot[base].emplace(session, *slotPtr);
+    HothBlob* blobIt = getSession(session);
+
+    if (!(blobIt->state & blobs::StateFlags::open_read))
+    {
+        return true;
+    }
+    auto finalState = blobIt->state;
+    blobIt->state = blobs::StateFlags::committing;
+    blobIt->outstanding = readSKMHSSSlot(
+        base, *slotPtr, hothId,
+        [blobIt, finalState](std::span<const uint8_t> data) noexcept {
+            auto outstanding = std::move(blobIt->outstanding);
+            if (data.size() == SKM_HSS_STRUCT_SIZE)
+            {
+                /* Set committed to denote that our buffer is consistent with
+                 * the remote copy, making commits a no-op until a write changes
+                 * it.
+                 */
+                blobIt->state = finalState | blobs::StateFlags::committed;
+                blobIt->buffer = std::vector<uint8_t>(data.begin(), data.end());
+                return;
+            }
+            blobIt->state = blobs::StateFlags::commit_error;
+            if (data.data() != nullptr &&
+                finalState & blobs::StateFlags::open_write)
+            {
+                /* If the call succeeded, but we were uninitialized we want to
+                 * allow a write to continue accessing the blob. This requires
+                 * us to populate the open_* flags that were originally set on
+                 * the blob. We don't do this for RO blobs since they can't
+                 * fill in valid data to read back.
+                 */
+                blobIt->state |= finalState;
+            }
+        });
+    return true;
+}
+
+bool HothSKMHSSBlobHandler::commit(uint16_t session,
+                                   const std::vector<uint8_t>& data)
+{
+    if (!data.empty())
+    {
+        log<level::ERR>("Unexpected data provided to commit call");
+        return false;
+    }
+
+    HothBlob* blobIt = getSession(session);
+    if (!blobIt)
+    {
+        return false;
+    }
+    if (blobIt->state &
+        (blobs::StateFlags::committed | blobs::StateFlags::committing))
+    {
+        return true;
+    }
+
+    std::string_view base = getBaseFromHothId(blobIt->hothId);
+    /* Clear remaining commit bits */
+    blobIt->state &= ~blobs::StateFlags::commit_error;
+    uint32_t slot = sessionSlot[base][session];
+    std::vector<uint8_t> req;
+    try
+    {
+        req = hothUtil_->writeSKMHSS(slot, blobIt->buffer);
+    }
+    catch (const CommandFailure& e)
+    {
+        blobIt->state |= blobs::StateFlags::commit_error;
+        log<level::ERR>("Hoth command failed", entry("ERR_MSG=%s", e.what()));
+        return false;
+    }
+
+    blobIt->state |= blobs::StateFlags::committing;
+    blobIt->outstanding = dbus_->SendHostCommand(
+        blobIt->hothId, req,
+        [this, base, slot,
+         blobIt](std::optional<std::vector<uint8_t>> rsp) noexcept {
+            auto outstanding = std::move(blobIt->outstanding);
+            blobIt->state &= ~blobs::StateFlags::committing;
+            try
+            {
+                hothUtil_->payloadECResponse(rsp.value());
+                blobIt->state |= blobs::StateFlags::committed;
+                slotPopulated[base][slot] = true;
+                return;
+            }
+            catch (const ResponseFailure& e)
+            {
+                log<level::ERR>("Invalid Hoth response",
+                                entry("ERR_MSG=%s", e.what()));
+            }
+            catch (...)
+            {}
+            blobIt->state |= blobs::StateFlags::commit_error;
+        });
+    return true;
+}
+
+bool HothSKMHSSBlobHandler::stat(uint16_t session, blobs::BlobMeta* meta)
+{
+    HothBlob* blobIt = getSession(session);
+    if (!blobIt)
+    {
+        return false;
+    }
+
+    meta->size = blobIt->buffer.size();
+    meta->blobState = blobIt->state;
+    return true;
+}
+
+bool HothSKMHSSBlobHandler::deleteBlob(const std::string& path)
+{
+    std::optional<uint32_t> slotPtr = internal::extractSlotFromID(path);
+    std::string_view hothId = getHothIdFromPath(path);
+    auto base = internal::getBaseFromID(path);
+    if (!slotPtr)
+    {
+        return false;
+    }
+    auto& del = dels[base][*slotPtr];
+    if (del)
+    {
+        return true;
+    }
+    auto& pop = slotPopulated[base][*slotPtr];
+    del = dbus_->SendHostCommand(
+        hothId, hothUtil_->deleteSKMHSS(*slotPtr),
+        [&pop, &del, this](std::optional<std::vector<uint8_t>> rsp) noexcept {
+            auto adel = std::move(del);
+            try
+            {
+                hothUtil_->payloadECResponse(rsp.value());
+                pop = false;
+                return;
+            }
+            catch (const ResponseFailure& e)
+            {
+                log<level::ERR>("Invalid Hoth response",
+                                entry("ERR_MSG=%s", e.what()));
+            }
+            catch (...)
+            {}
+        });
+    return true;
+}
+
+bool HothSKMHSSBlobHandler::close(uint16_t session)
+{
+    HothBlob* blobIt = getSession(session);
+    if (!blobIt)
+    {
+        return false;
+    }
+
+    std::string_view base = getBaseFromHothId(blobIt->hothId);
+    if (HothBlobHandler::close(session))
+    {
+        sessionSlot[base].erase(session);
+        return true;
+    }
+    return false;
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/hoth_skmhss.hpp b/skmhss/hoth_skmhss.hpp
new file mode 100644
index 0000000..6fa84a0
--- /dev/null
+++ b/skmhss/hoth_skmhss.hpp
@@ -0,0 +1,127 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "command/dbus_command.hpp"
+#include "hoth.hpp"
+#include "hoth_util.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+#include <boost/bimap.hpp>
+#include <function2/function2.hpp>
+
+#include <array>
+#include <span>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+namespace ipmi_hoth
+{
+
+class HothSKMHSSBlobHandler : public HothBlobHandler
+{
+  public:
+    explicit HothSKMHSSBlobHandler(internal::DbusCommand* dbus,
+                                   internal::HothUtil* hvnutil) :
+        dbus_(dbus), hothUtil_(hvnutil)
+    {
+        loadHSS();
+    }
+
+    /* Our callbacks require pinned memory */
+    HothSKMHSSBlobHandler(HothSKMHSSBlobHandler&&) = delete;
+    HothSKMHSSBlobHandler& operator=(HothSKMHSSBlobHandler&&) = delete;
+
+    bool canHandleBlob(const std::string& path) override;
+    std::vector<std::string> getBlobIds() override;
+    bool deleteBlob(const std::string& path) override;
+    bool stat(const std::string& path, blobs::BlobMeta* meta) override;
+    bool open(uint16_t session, uint16_t flags, const std::string& path);
+    bool commit(uint16_t session, const std::vector<uint8_t>& data) override;
+    bool stat(uint16_t session, blobs::BlobMeta* meta) override;
+    bool close(uint16_t session) override;
+
+    internal::Dbus& dbus()
+    {
+        return *dbus_;
+    }
+
+    std::string_view pathSuffix() const override
+    {
+        return "skm/hss";
+    }
+    uint16_t requiredFlags() const override
+    {
+        return 0;
+    }
+    uint16_t maxSessions() const override
+    {
+        /* We should allow a limited number of sessions per-blob */
+        return maxNumHSSSlot << 2;
+    }
+
+    /* the max buffer size is the constant size of HSS. */
+    uint32_t maxBufferSize() const override
+    {
+        return 64;
+    }
+
+  private:
+    /** @brief Reads the HSS slot and store the HSS data
+     *  The return data can be null, meaning the request failed. It can also
+     *  be an empty buffer, so the caller needs to check that it has enough
+     *  bytes to be a valid HSS.
+     *
+     * @param[in] slot: the slot number in Hoth
+     * @param[in] cb:   Run when the request completes with the HSS bytes
+     * @returns An object to cancel the request early.
+     */
+    stdplus::Cancel readSKMHSSSlot(
+        std::string_view base, uint32_t slot, std::string_view hothId,
+        fu2::unique_function<void(std::span<const uint8_t>)>&& cb);
+
+    /* Load HSS on Hoth storage through reading all slots. */
+    void loadHSS();
+
+    /* read the hothid_basepaths.config and store it in basePathMap*/
+    void readBasePathConfig();
+
+    /* get the hoth Id from the path from hothIdBasePaths map */
+    std::string_view getHothIdFromPath(std::string_view path);
+
+    /* get the base path from the hoth Id from hothIdBasePaths map */
+    std::string_view getBaseFromHothId(std::string_view hothId);
+
+    /* Number of HSS seeds stored in Hoth */
+    static constexpr uint32_t maxNumHSSSlot = 4;
+
+    std::unordered_map<std::string_view, std::unordered_map<uint16_t, uint32_t>>
+        sessionSlot;
+    std::unordered_map<std::string_view, std::array<bool, maxNumHSSSlot>>
+        slotPopulated;
+    std::unordered_map<std::string_view,
+                       std::array<stdplus::Cancel, maxNumHSSSlot>>
+        inits, dels;
+
+    /* basePath <---> hothId mapping */
+    boost::bimap<std::string, std::string> hothIdBasePaths;
+
+    internal::DbusCommand* dbus_;
+    internal::HothUtil* hothUtil_;
+};
+
+} // namespace ipmi_hoth
diff --git a/skmhss/hoth_util.hpp b/skmhss/hoth_util.hpp
new file mode 100644
index 0000000..c8e9da7
--- /dev/null
+++ b/skmhss/hoth_util.hpp
@@ -0,0 +1,91 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include <stdplus/raw.hpp>
+
+#include <cstdint>
+#include <numeric>
+#include <span>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class HothUtil
+ *  @brief An interface to Hoth SKM HSS operation
+ *  @details Translate Hoth requests to bytes array commands and analyze
+ *  Hoth responses.
+ */
+class HothUtil
+{
+  public:
+    virtual ~HothUtil() {};
+
+    /** @brief Calculates the checksum for an EC request with the given
+     * |contents|
+     * @param one or more spans (or vectors) of bytes
+     * @returns the checksum for a message with the given |contents|
+     */
+
+    template <typename... Ts>
+    static uint8_t calculateChecksum(Ts&&... ts)
+    {
+        return (std::accumulate(std::begin(ts), std::end(ts), 0) + ...);
+    }
+
+    /** @brief Wrap the EC SKM HSS request by serializing the concatenation of
+     * an EC request header and payload
+     * @param request: the EC SKM HSS request
+     * @returns the serialized EC request
+     */
+    virtual std::vector<uint8_t>
+        wrapECSKMHSSRequest(std::span<const uint8_t> request) = 0;
+
+    /** @brief Wrap the EC SKM HSS read request
+     * @param slot: the slot number in Hoth
+     * @returns the serialized EC request
+     */
+    virtual std::vector<uint8_t> readSKMHSS(uint32_t slot) = 0;
+
+    /** @brief Wrap the EC SKM HSS write request
+     * @param slot: the slot number in Hoth
+     * @data data: SKM HSS bytes
+     * @returns the serialized EC request
+     */
+    virtual std::vector<uint8_t> writeSKMHSS(uint32_t slot,
+                                             std::span<const uint8_t> data) = 0;
+
+    /** @brief Wrap the EC SKM HSS delete request
+     * @param slot: the slot number in Hoth
+     * @returns the serialized EC request
+     */
+    virtual std::vector<uint8_t> deleteSKMHSS(uint32_t slot) = 0;
+
+    /** @brief Validate the EC response and extract the payload by stripping off
+     * the header
+     * @param rsp: bytes array representing response received from EC
+     * @returns return the payload in an EC response iff the EC response is
+     * valid.
+     */
+    virtual std::span<const uint8_t>
+        payloadECResponse(std::span<const uint8_t> rsp) = 0;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/skmhss/hoth_util_impl.cpp b/skmhss/hoth_util_impl.cpp
new file mode 100644
index 0000000..bdb3c2b
--- /dev/null
+++ b/skmhss/hoth_util_impl.cpp
@@ -0,0 +1,138 @@
+// Copyright 2024 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 "hoth_util_impl.hpp"
+
+#include <google3/g_ec_commands.h>
+#include <google3/host_commands.h>
+
+#include <boost/endian/conversion.hpp>
+#include <phosphor-logging/log.hpp>
+#include <stdplus/raw.hpp>
+#include <xyz/openbmc_project/Control/Hoth/error.hpp>
+
+#include <cstdint>
+#include <limits>
+#include <memory>
+#include <span>
+#include <vector>
+
+using CommandFailure =
+    sdbusplus::xyz::openbmc_project::Control::Hoth::Error::CommandFailure;
+using ResponseFailure =
+    sdbusplus::xyz::openbmc_project::Control::Hoth::Error::ResponseFailure;
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+using namespace phosphor::logging;
+
+std::vector<uint8_t>
+    HothUtilImpl::wrapECSKMHSSRequest(std::span<const uint8_t> request)
+{
+    // Populate EC request header based on the request
+    g_ec_host_request skmHssHeader = {
+        /*struct_version*/ G_EC_HOST_REQUEST_VERSION,
+        /*checksum*/ 0,
+        /*command*/ G_EC_PRIVATE_HOST_COMMAND_VALUE(EC_PRV_CMD_HOTH_SKM_HSS),
+        /*command_version*/ 0,
+        /*reserved*/ 0,
+        /*data_len*/ static_cast<uint16_t>(request.size())};
+
+    boost::endian::native_to_little_inplace(skmHssHeader);
+    skmHssHeader.checksum -=
+        calculateChecksum(stdplus::raw::asSpan<uint8_t>(skmHssHeader), request);
+
+    // Concatenate the request header and the request into one uint8_t vector to
+    // be sent to EC
+    const auto requestHeaderPtr = reinterpret_cast<uint8_t*>(&skmHssHeader);
+    std::vector<uint8_t> hothCommand(requestHeaderPtr,
+                                     requestHeaderPtr + sizeof(skmHssHeader));
+    hothCommand.insert(hothCommand.end(), request.begin(), request.end());
+    return hothCommand;
+}
+
+std::vector<uint8_t> HothUtilImpl::readSKMHSS(uint32_t slot)
+{
+    struct ec_request_skm_hss request = {};
+    request.operation = SKM_HSS_READ;
+    request.slot = slot;
+    return wrapECSKMHSSRequest(stdplus::raw::asSpan<const uint8_t>(request));
+}
+
+std::vector<uint8_t>
+    HothUtilImpl::writeSKMHSS(uint32_t slot, std::span<const uint8_t> data)
+{
+    if (data.empty() || data.size() > SKM_HSS_STRUCT_SIZE)
+    {
+        log<level::ERR>("Bad HSS payload size",
+                        entry("REQ_SIZE=%lu SKM_HSS_SIZE=%lu",
+                              static_cast<unsigned long>(data.size()),
+                              static_cast<unsigned long>(SKM_HSS_STRUCT_SIZE)));
+        throw CommandFailure();
+    }
+    struct ec_request_skm_hss request = {};
+    request.operation = SKM_HSS_WRITE;
+    request.slot = slot;
+    std::memcpy(request.payload.data, data.data(), data.size());
+    return wrapECSKMHSSRequest(stdplus::raw::asSpan<const uint8_t>(request));
+}
+
+std::vector<uint8_t> HothUtilImpl::deleteSKMHSS(uint32_t slot)
+{
+    struct ec_request_skm_hss request = {};
+    request.operation = SKM_HSS_DELETE;
+    request.slot = slot;
+    return wrapECSKMHSSRequest(stdplus::raw::asSpan<const uint8_t>(request));
+}
+
+std::span<const uint8_t>
+    HothUtilImpl::payloadECResponse(std::span<const uint8_t> rsp)
+{
+    g_ec_host_response header{};
+
+    if (rsp.size() < sizeof(header))
+    {
+        log<level::ERR>("Response is too short",
+                        entry("LENGTH=%d", rsp.size()));
+        throw ResponseFailure();
+    }
+
+    std::memcpy(&header, rsp.data(), sizeof(header));
+    boost::endian::native_to_little_inplace(header);
+
+    if (header.struct_version != G_EC_HOST_RESPONSE_VERSION)
+    {
+        log<level::ERR>(
+            "Unsupported stuct_version in response",
+            entry("VERSION=%d", static_cast<uint8_t>(header.struct_version)));
+        throw ResponseFailure();
+    }
+
+    if (header.result != EC_RES_SUCCESS && header.result != EC_RES_UNAVAILABLE)
+    {
+        log<level::ERR>(
+            "Unexpected return code",
+            entry("RESULT=%d", static_cast<uint16_t>(header.result)));
+        throw ResponseFailure();
+    }
+
+    return rsp.last(rsp.size() - sizeof(header));
+}
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/skmhss/hoth_util_impl.hpp b/skmhss/hoth_util_impl.hpp
new file mode 100644
index 0000000..96a1876
--- /dev/null
+++ b/skmhss/hoth_util_impl.hpp
@@ -0,0 +1,76 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "hoth_util.hpp"
+
+#include <stdplus/raw.hpp>
+
+#include <cstdint>
+#include <span>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/* Host command response codes (16-bit).  Note that response codes should be
+ * stored in a uint16_t rather than directly in a value of this type.
+ */
+enum ec_status
+{
+    EC_RES_SUCCESS = 0,
+    EC_RES_INVALID_COMMAND = 1,
+    EC_RES_ERROR = 2,
+    EC_RES_INVALID_PARAM = 3,
+    EC_RES_ACCESS_DENIED = 4,
+    EC_RES_INVALID_RESPONSE = 5,
+    EC_RES_INVALID_VERSION = 6,
+    EC_RES_INVALID_CHECKSUM = 7,
+    EC_RES_IN_PROGRESS = 8,             /* Accepted, command in progress */
+    EC_RES_UNAVAILABLE = 9,             /* No response available */
+    EC_RES_TIMEOUT = 10,                /* We got a timeout */
+    EC_RES_OVERFLOW = 11,               /* Table / data overflow */
+    EC_RES_INVALID_HEADER = 12,         /* Header contains invalid data */
+    EC_RES_REQUEST_TRUNCATED = 13,      /* Didn't get the entire request */
+    EC_RES_RESPONSE_TOO_BIG = 14,       /* Response was too big to handle */
+    EC_RES_BUS_ERROR = 15,              /* Communications bus error */
+    EC_RES_BUSY = 16,                   /* Up but too busy.  Should retry */
+    EC_RES_INVALID_HEADER_VERSION = 17, /* Header version invalid */
+    EC_RES_INVALID_HEADER_CRC = 18,     /* Header CRC invalid */
+    EC_RES_INVALID_DATA_CRC = 19,       /* Data CRC invalid */
+    EC_RES_DUP_UNAVAILABLE = 20,        /* Can't resend response */
+};
+
+/** @class HothUtilImpl
+ * @brief Implementation of HothUtil interface
+ */
+class HothUtilImpl : public HothUtil
+{
+  public:
+    std::vector<uint8_t>
+        wrapECSKMHSSRequest(std::span<const uint8_t> request) override;
+    std::vector<uint8_t> readSKMHSS(uint32_t slot) override;
+    std::vector<uint8_t> writeSKMHSS(uint32_t slot,
+                                     std::span<const uint8_t> data) override;
+    std::vector<uint8_t> deleteSKMHSS(uint32_t slot) override;
+    std::span<const uint8_t>
+        payloadECResponse(std::span<const uint8_t> rsp) override;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/skmhss/main_skmhss.cpp b/skmhss/main_skmhss.cpp
new file mode 100644
index 0000000..48bd97b
--- /dev/null
+++ b/skmhss/main_skmhss.cpp
@@ -0,0 +1,31 @@
+// Copyright 2024 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 "command/dbus_command_impl.hpp"
+#include "hoth_skmhss.hpp"
+#include "hoth_util_impl.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <memory>
+
+extern "C" std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+    /** @brief Default instantiation of Dbus and HothUtil */
+    static ipmi_hoth::internal::DbusCommandImpl dbusCommand_impl;
+    static ipmi_hoth::internal::HothUtilImpl hothUtil_impl;
+
+    return std::make_unique<ipmi_hoth::HothSKMHSSBlobHandler>(
+        &dbusCommand_impl, &hothUtil_impl);
+}
diff --git a/skmhss/meson.build b/skmhss/meson.build
new file mode 100644
index 0000000..7a713a5
--- /dev/null
+++ b/skmhss/meson.build
@@ -0,0 +1,45 @@
+meson.get_compiler('cpp').has_header_symbol(
+  'boost/endian/conversion.hpp',
+  'boost::endian::native_to_little_inplace')
+
+conf_data = configuration_data()
+conf_data.set_quoted('HOTHID_CONFIG', get_option('skmhss-hothid-config'))
+conf_hpp = configure_file(
+  output: 'config.hpp',
+  configuration: conf_data)
+
+hothskmhss_pre = declare_dependency(
+  include_directories: include_directories('.'),
+  dependencies: [
+    hothblob_dep,
+    hothcommand_dep,
+    dependency('hothd-dbus'),
+    ipmi_blob_dep,
+    phosphor_logging_dep,
+    sdbusplus_dep,
+    stdplus_dep,
+  ])
+
+hothskmhss_lib = static_library(
+  'hothskmhss',
+  'hoth_skmhss.cpp',
+  'hoth_util_impl.cpp',
+  conf_hpp,
+  implicit_include_directories: false,
+  dependencies: hothskmhss_pre)
+
+hothskmhss_dep = declare_dependency(
+  link_with: hothskmhss_lib,
+  dependencies: hothskmhss_pre)
+
+shared_module(
+  'hothskmhss',
+  'main_skmhss.cpp',
+  implicit_include_directories: false,
+  dependencies: hothskmhss_dep,
+  install: true,
+  install_dir: get_option('libdir') / 'blob-ipmid')
+
+if not get_option('tests').disabled()
+  subdir('test')
+endif
diff --git a/skmhss/test/hoth_skmhss_close_unittest.cpp b/skmhss/test/hoth_skmhss_close_unittest.cpp
new file mode 100644
index 0000000..924a51c
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_close_unittest.cpp
@@ -0,0 +1,50 @@
+// Copyright 2024 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::_;
+using ::testing::Return;
+
+class HothSKMHSSCloseTest : public HothSKMHSSBasicTest
+{};
+
+TEST_F(HothSKMHSSCloseTest, CloseWithInvalidSessionFails)
+{
+    // Verify you cannot close an invalid session.
+
+    EXPECT_FALSE(hvn->close(session));
+}
+
+TEST_F(HothSKMHSSCloseTest, CloseWithValidSessionSuccess)
+{
+    // Verify you can close a valid session.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn->open(session, blobs::OpenFlags::write, legacyPath[2]));
+    EXPECT_TRUE(hvn->close(session));
+    EXPECT_FALSE(hvn->close(session));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_commit_unittest.cpp b/skmhss/test/hoth_skmhss_commit_unittest.cpp
new file mode 100644
index 0000000..a2aa971
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_commit_unittest.cpp
@@ -0,0 +1,120 @@
+// Copyright 2024 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+namespace ipmi_hoth
+{
+
+class HothSKMHSSCommitTest : public HothSKMHSSBasicTest
+{
+  protected:
+    void openAndWriteRandomHss(int idx)
+    {
+        std::vector<uint8_t> data(64);
+        std::srand(1);
+        std::generate(data.begin(), data.end(), std::rand);
+        EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+            .WillOnce(Return(true));
+        fu2::unique_function<void()> cb;
+        testing::StrictMock<MockCancel> cancel;
+        expectRead(idx, cb, cancel, data);
+        EXPECT_TRUE(
+            hvn->open(session, blobs::OpenFlags::read | blobs::OpenFlags::write,
+                      legacyPath[idx]));
+        cb();
+    }
+};
+
+TEST_F(HothSKMHSSCommitTest, InvalidSessionCommitIsRejected)
+{
+    // Verify the hoth command handler checks for a valid session.
+
+    EXPECT_FALSE(hvn->commit(session, std::vector<uint8_t>()));
+}
+
+TEST_F(HothSKMHSSCommitTest, CommitWithUnexpectedDataParamReturnsFalse)
+{
+    openAndWriteRandomHss(0);
+    EXPECT_FALSE(hvn->commit(session, std::vector<uint8_t>({1, 2, 3})));
+}
+
+TEST_F(HothSKMHSSCommitTest, MultiCommit)
+{
+    openAndWriteRandomHss(1);
+
+    // No change does nothing
+    EXPECT_TRUE(hvn->commit(session, {}));
+
+    EXPECT_TRUE(hvn->write(session, 0, {0}));
+
+    // Test an error case
+    EXPECT_CALL(hvnutil, writeSKMHSS(1, _))
+        .WillOnce(Return(std::vector<uint8_t>{11}));
+    fu2::unique_function<void()> cb;
+    testing::StrictMock<MockCancel> cancel;
+    expectCmd(/*slot=*/1, cb, cancel, /*data=*/{});
+    EXPECT_TRUE(hvn->commit(session, {}));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_write |
+                  blobs::StateFlags::open_read,
+              meta.blobState);
+
+    cb();
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(blobs::StateFlags::commit_error | blobs::StateFlags::open_write |
+                  blobs::StateFlags::open_read,
+              meta.blobState);
+
+    // Test repeat commit translates into success
+    EXPECT_CALL(hvnutil, writeSKMHSS(1, _))
+        .WillOnce(Return(std::vector<uint8_t>{11}));
+    std::array<uint8_t, 1> arr;
+    expectCmd(/*slot=*/1, cb, cancel,
+              /*data=*/std::span<uint8_t>(arr).subspan(0, 0));
+    EXPECT_TRUE(hvn->commit(session, {}));
+
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_write |
+                  blobs::StateFlags::open_read,
+              meta.blobState);
+
+    cb();
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_write |
+                  blobs::StateFlags::open_read,
+              meta.blobState);
+
+    // No change does nothing
+    EXPECT_TRUE(hvn->commit(session, {}));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_delete_unittest.cpp b/skmhss/test/hoth_skmhss_delete_unittest.cpp
new file mode 100644
index 0000000..a3518bf
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_delete_unittest.cpp
@@ -0,0 +1,107 @@
+// Copyright 2024 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+namespace ipmi_hoth
+{
+
+class HothSKMHSSDeleteTest : public HothSKMHSSBasicTest
+{};
+
+TEST_F(HothSKMHSSDeleteTest, DeleteWithNoHothd)
+{
+    DbusCommand::Cb cb;
+    testing::StrictMock<MockCancel> cancel;
+    EXPECT_CALL(hvnutil, deleteSKMHSS).WillOnce(Return(std::vector<uint8_t>()));
+    EXPECT_CALL(dbus, SendHostCommand("", testing::ElementsAre(), _))
+        .WillOnce([&](std::string_view, const std::vector<uint8_t>&,
+                      DbusCommand::Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(&cancel);
+        });
+    EXPECT_TRUE(hvn->deleteBlob("/skm/hss-backup/0"));
+    // Delete while delete in progress should not trigger a new delete
+    EXPECT_TRUE(hvn->deleteBlob("/skm/hss-backup/0"));
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn->stat("/skm/hss-backup/0", &meta));
+    EXPECT_CALL(cancel, cancel());
+    cb(std::nullopt);
+    EXPECT_TRUE(hvn->stat("/skm/hss-backup/0", &meta));
+}
+
+TEST_F(HothSKMHSSDeleteTest, DeleteSlotSucceeds)
+{
+    // Also check the cached blob list is updated.
+    std::vector<std::string> expected = {
+        "/skm/hss-backup/",  "/skm/hss-backup/0", "/skm/hss-backup/1",
+        "/skm/hss-backup/2", "/skm/hss-backup/3",
+    };
+    EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+        .WillRepeatedly(Return(true));
+    EXPECT_THAT(hvn->getBlobIds(), UnorderedElementsAreArray(expected));
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn->stat("/skm/hss-backup/0", &meta));
+
+    DbusCommand::Cb cb;
+    testing::StrictMock<MockCancel> cancel;
+    EXPECT_CALL(hvnutil, deleteSKMHSS).WillOnce(Return(std::vector<uint8_t>()));
+    EXPECT_CALL(dbus, SendHostCommand("", testing::ElementsAre(), _))
+        .WillOnce([&](std::string_view, const std::vector<uint8_t>&,
+                      DbusCommand::Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(&cancel);
+        });
+    EXPECT_CALL(hvnutil, payloadECResponse(_))
+        .WillOnce(Return(std::vector<uint8_t>()));
+
+    EXPECT_TRUE(hvn->deleteBlob(legacyPath[3]));
+    EXPECT_TRUE(hvn->stat("/skm/hss-backup/3", &meta));
+    EXPECT_THAT(hvn->getBlobIds(), UnorderedElementsAreArray(expected));
+    EXPECT_CALL(cancel, cancel());
+    cb(std::vector<uint8_t>());
+    expected.pop_back();
+    EXPECT_THAT(hvn->getBlobIds(), UnorderedElementsAreArray(expected));
+    EXPECT_FALSE(hvn->stat("/skm/hss-backup/3", &meta));
+
+    // Deleting a nonexistent slot will fall through to hoth
+    EXPECT_CALL(hvnutil, deleteSKMHSS).WillOnce(Return(std::vector<uint8_t>()));
+    EXPECT_CALL(dbus, SendHostCommand("", testing::ElementsAre(), _))
+        .WillOnce([&](std::string_view, const std::vector<uint8_t>&,
+                      DbusCommand::Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(&cancel);
+        });
+    EXPECT_TRUE(hvn->deleteBlob(legacyPath[3]));
+    EXPECT_CALL(cancel, cancel());
+    cb(std::nullopt);
+
+    // Opening deleted slot succeeds
+    EXPECT_CALL(dbus, pingHothd("")).WillOnce(Return(true));
+    EXPECT_TRUE(hvn->open(session, blobs::OpenFlags::write, legacyPath[3]));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_open_unittest.cpp b/skmhss/test/hoth_skmhss_open_unittest.cpp
new file mode 100644
index 0000000..033492f
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_open_unittest.cpp
@@ -0,0 +1,121 @@
+// Copyright 2024 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <span>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::_;
+using ::testing::Return;
+
+class HothSKMHSSOpenTest : public HothSKMHSSBasicTest
+{};
+
+TEST_F(HothSKMHSSOpenTest, OpenWithNoHothd)
+{
+    /* Hoth SKM HSS handler opens without a backing hoth daemon present. */
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(false));
+    EXPECT_FALSE(hvn->open(session, hvn->requiredFlags(), legacyPath[1]));
+}
+
+TEST_F(HothSKMHSSOpenTest, OpenRWUninitializedBlobSucceeds)
+{
+    /* Hoth SKM HSS handler opens an uninitialized path. */
+    hvn = createHandlerWithEmptyCache();
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    fu2::unique_function<void()> cb;
+    testing::StrictMock<MockCancel> cancel;
+    std::array<uint8_t, 1> rspdata;
+    expectRead(0, cb, cancel, std::span<uint8_t>(rspdata).subspan(0, 0));
+    EXPECT_TRUE(
+        hvn->open(session, blobs::OpenFlags::read | blobs::OpenFlags::write,
+                  legacyPath[0]));
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(meta.blobState, blobs::StateFlags::committing);
+    cb();
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(meta.blobState,
+              blobs::StateFlags::open_read | blobs::StateFlags::open_write |
+                  blobs::StateFlags::commit_error);
+}
+
+TEST_F(HothSKMHSSOpenTest, OpenROUninitializedBlobFails)
+{
+    /* Hoth SKM HSS handler opens an uninitialized path. */
+    hvn = createHandlerWithEmptyCache();
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    fu2::unique_function<void()> cb;
+    testing::StrictMock<MockCancel> cancel;
+    std::array<uint8_t, 1> rspdata;
+    expectRead(0, cb, cancel, std::span<uint8_t>(rspdata).subspan(0, 0));
+    EXPECT_TRUE(hvn->open(session, blobs::OpenFlags::read, legacyPath[0]));
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(meta.blobState, blobs::StateFlags::committing);
+    cb();
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(meta.blobState, blobs::StateFlags::commit_error);
+}
+
+TEST_F(HothSKMHSSOpenTest, OpenBlobWithSdBusErrorFails)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    fu2::unique_function<void()> cb;
+    testing::StrictMock<MockCancel> cancel;
+    expectRead(0, cb, cancel, {});
+    EXPECT_TRUE(
+        hvn->open(session, blobs::OpenFlags::read | blobs::OpenFlags::write,
+                  legacyPath[0]));
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(meta.blobState, blobs::StateFlags::committing);
+    cb();
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(meta.blobState, blobs::StateFlags::commit_error);
+}
+
+TEST_F(HothSKMHSSOpenTest, OpenOverloadedSessionFails)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+        .WillRepeatedly(Return(true));
+    uint16_t sessId = 0;
+    int low = std::min((uint16_t)legacyPath.size(), hvn->maxSessions());
+
+    for (int i = 0; i < low; i++)
+    {
+        EXPECT_TRUE(
+            hvn->open(sessId++, blobs::OpenFlags::write, legacyPath[i]));
+    }
+
+    for (int i = low; i < hvn->maxSessions(); i++)
+    {
+        EXPECT_TRUE(
+            hvn->open(sessId++, blobs::OpenFlags::write, legacyPath[0]));
+    }
+
+    EXPECT_FALSE(hvn->open(sessId++, blobs::OpenFlags::write, legacyPath[0]));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_read_unittest.cpp b/skmhss/test/hoth_skmhss_read_unittest.cpp
new file mode 100644
index 0000000..c47eef9
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_read_unittest.cpp
@@ -0,0 +1,154 @@
+// Copyright 2024 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+#include <ipmid/handler.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+using ::testing::Return;
+
+class HothSKMHSSReadTest : public HothSKMHSSBasicTest
+{
+  protected:
+    const uint32_t testOffset = 0;
+    const std::vector<uint8_t> testData = [] {
+        std::vector<uint8_t> ret;
+        for (uint8_t i = 0; i < 64; ++i)
+            ret.push_back(i);
+        return ret;
+    }();
+
+    void openUninitializedBlob(int idx)
+    {
+        // Any valid blobs have uninitialized buffer.
+        hvn = createHandlerWithEmptyCache();
+
+        EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+            .WillOnce(Return(true));
+        fu2::unique_function<void()> cb;
+        testing::StrictMock<MockCancel> cancel;
+        expectRead(idx, cb, cancel, testData);
+        EXPECT_TRUE(
+            hvn->open(session, blobs::OpenFlags::read, legacyPath[idx]));
+        cb();
+    }
+};
+
+TEST_F(HothSKMHSSReadTest, InvalidSessionReadIsRejected)
+{
+    // Verify that read checks for a valid session, returns empty buffer for
+    // failed check.
+
+    openUninitializedBlob(0);
+    uint16_t wrongSession = session + 1;
+    EXPECT_THROW(hvn->read(wrongSession, testOffset, testData.size()),
+                 ipmi::HandlerCompletion);
+}
+
+TEST_F(HothSKMHSSReadTest, ReadOpenBlobWithoutReadFlagReturnsError)
+{
+    // Verify that read checks for a valid state with open_read flag set.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn->open(session, 0, legacyPath[1]));
+    EXPECT_THROW(hvn->read(session, testOffset, testData.size()),
+                 ipmi::HandlerCompletion);
+}
+
+TEST_F(HothSKMHSSReadTest, ReadOffsetBeyondBufferSizeReturnsEmpty)
+{
+    // Verify that read with offset beyond buffer size returns empty buffer.
+
+    openUninitializedBlob(2);
+    uint32_t offsetBeyondBuffer = testData.size();
+    EXPECT_THAT(hvn->read(session, offsetBeyondBuffer, testData.size()),
+                IsEmpty());
+}
+
+TEST_F(HothSKMHSSReadTest, ReadFullWrittenData)
+{
+    // Verify that the read successfully reads back the written data.
+
+    openUninitializedBlob(3);
+    EXPECT_THAT(hvn->read(session, testOffset, testData.size()),
+                ContainerEq(testData));
+}
+
+TEST_F(HothSKMHSSReadTest, ReadWrittenDataAtOffset)
+{
+    // Verify that the read with offset returns the expected data.
+
+    openUninitializedBlob(0);
+    EXPECT_THAT(hvn->read(session, testOffset, testData.size()),
+                ContainerEq(testData));
+
+    // Try reading the written data byte by byte at each offset
+    for (size_t i = 0; i < testData.size(); ++i)
+    {
+        EXPECT_THAT(hvn->read(session, i, 1), ElementsAre(testData[i]));
+    }
+}
+
+TEST_F(HothSKMHSSReadTest, ReadFullWrittenDataWithBiggerRequestedSize)
+{
+    // Verify that read with requested size bigger than the written data will
+    // return a response buffer up to the end of the written buffer.
+
+    openUninitializedBlob(0);
+    uint32_t requestedSizeBeyondBuffer = testData.size() + 1;
+    EXPECT_THAT(hvn->read(session, testOffset, requestedSizeBeyondBuffer),
+                ContainerEq(testData));
+}
+
+TEST_F(HothSKMHSSReadTest, ReadWrittenDataAtOffsetWithBiggerRequestedSize)
+{
+    // Verify that read with requested size bigger than the written data at an
+    // offset will return a response buffer up to the end of the written buffer.
+
+    openUninitializedBlob(1);
+    uint32_t newOffset = testData.size() / 2;
+    uint32_t requestedSizeBeyondBuffer = testData.size() + 1;
+    std::vector<uint8_t> expectedData =
+        std::vector<uint8_t>(testData.begin() + newOffset, testData.end());
+
+    EXPECT_THAT(hvn->read(session, newOffset, requestedSizeBeyondBuffer),
+                ContainerEq(expectedData));
+}
+
+TEST_F(HothSKMHSSReadTest, ReadFullWrittenDataBeyondMaxBufferSize)
+{
+    // Verify that read with requested size bigger than the maximum buffer size
+    // will return a response buffer up to the end of the written buffer.
+
+    openUninitializedBlob(2);
+    uint32_t requestedSize = hvn->maxBufferSize() + 1;
+    EXPECT_THAT(hvn->read(session, testOffset, requestedSize),
+                ContainerEq(testData));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_unittest.cpp b/skmhss/test/hoth_skmhss_unittest.cpp
new file mode 100644
index 0000000..4784603
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_unittest.cpp
@@ -0,0 +1,76 @@
+// Copyright 2024 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 "hoth_skmhss_unittest.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+namespace ipmi_hoth
+{
+
+TEST_F(HothSKMHSSBasicTest, HothIdMainOnly)
+{
+    EXPECT_EQ("", hvn->pathToHothId(hvn->hothIdToPath("")));
+}
+
+TEST_F(HothSKMHSSBasicTest, CanHandleBlobChecksNameInvalid)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+        .WillRepeatedly(Return(true));
+    EXPECT_FALSE(hvn->canHandleBlob("asdf"));
+    EXPECT_FALSE(hvn->canHandleBlob("/skm/hss-backup"));
+    EXPECT_FALSE(hvn->canHandleBlob("/skm/hss-backup/4"));
+    EXPECT_FALSE(hvn->canHandleBlob("/skm/hss-backup/15"));
+    EXPECT_FALSE(hvn->canHandleBlob("/dev/hoth/command_passthru"));
+}
+
+TEST_F(HothSKMHSSBasicTest, CanHandleBlobChecksNameValid)
+{
+    EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+        .WillRepeatedly(Return(true));
+    EXPECT_TRUE(hvn->canHandleBlob("/skm/hss-backup/0"));
+    EXPECT_TRUE(hvn->canHandleBlob("/skm/hss-backup/1"));
+    EXPECT_TRUE(hvn->canHandleBlob("/skm/hss-backup/2"));
+    EXPECT_TRUE(hvn->canHandleBlob("/skm/hss-backup/3"));
+}
+
+TEST_F(HothSKMHSSBasicTest, GetBlobIdsAsHssUnintialized)
+{
+    hvn = createHandlerWithEmptyCache();
+    std::vector<std::string> expected = {"/skm/hss-backup/"};
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_THAT(hvn->getBlobIds(), ContainerEq(expected));
+}
+
+TEST_F(HothSKMHSSBasicTest, GetBlobIdsAsHssCommited)
+{
+    std::vector<std::string> expected = {
+        "/skm/hss-backup/",  "/skm/hss-backup/0", "/skm/hss-backup/1",
+        "/skm/hss-backup/2", "/skm/hss-backup/3",
+    };
+    EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+        .WillRepeatedly(Return(true));
+    EXPECT_THAT(hvn->getBlobIds(), UnorderedElementsAreArray(expected));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_unittest.hpp b/skmhss/test/hoth_skmhss_unittest.hpp
new file mode 100644
index 0000000..0973205
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_unittest.hpp
@@ -0,0 +1,154 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "command/test/dbus_command_mock.hpp"
+#include "hoth_skmhss.hpp"
+#include "hoth_util_mock.hpp"
+
+#include <sdbusplus/exception.hpp>
+#include <sdbusplus/test/sdbus_mock.hpp>
+#include <xyz/openbmc_project/Control/Hoth/error.hpp>
+
+#include <array>
+#include <cstdint>
+#include <memory>
+#include <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using ipmi_hoth::internal::DbusCommand;
+using ::testing::_;
+using ::testing::NotNull;
+using ::testing::Return;
+using ResponseFailure =
+    sdbusplus::xyz::openbmc_project::Control::Hoth::Error::ResponseFailure;
+
+namespace ipmi_hoth
+{
+
+struct MockCancel : public stdplus::Cancelable
+{
+    MOCK_METHOD(void, cancel, (), (noexcept, override));
+};
+
+class HothSKMHSSTest : public ::testing::Test
+{
+  public:
+    void expectCmd(uint8_t i, fu2::unique_function<void()>& cb,
+                   MockCancel& cancel, std::span<const uint8_t> data)
+    {
+        bool has_data = data.data() != nullptr;
+        EXPECT_CALL(dbus,
+                    SendHostCommand(
+                        "", testing::ElementsAre(static_cast<uint8_t>(10 + i)),
+                        _))
+            .WillOnce([&cb, &cancel, i,
+                       has_data](std::string_view, const std::vector<uint8_t>&,
+                                 DbusCommand::Cb&& icb) {
+                if (has_data)
+                {
+                    cb = [icb = std::move(icb), i]() mutable {
+                        icb(std::vector<uint8_t>{static_cast<uint8_t>(20 + i)});
+                    };
+                }
+                else
+                {
+                    cb = [icb = std::move(icb)]() mutable {
+                        icb(std::nullopt);
+                    };
+                }
+                return stdplus::Cancel(&cancel);
+            });
+        if (has_data)
+        {
+            EXPECT_CALL(hvnutil, payloadECResponse(testing::ElementsAre(
+                                     static_cast<uint8_t>(20 + i))))
+                .WillOnce(Return(data));
+        }
+        EXPECT_CALL(cancel, cancel()).WillOnce(Return());
+    }
+
+    void expectRead(uint8_t i, fu2::unique_function<void()>& cb,
+                    MockCancel& cancel, std::span<const uint8_t> data)
+    {
+        EXPECT_CALL(hvnutil, readSKMHSS(i))
+            .WillOnce(
+                Return(std::vector<uint8_t>{static_cast<uint8_t>(10 + i)}));
+        expectCmd(i, cb, cancel, data);
+    }
+
+    std::unique_ptr<HothSKMHSSBlobHandler>
+        createHandler(const std::vector<uint8_t>& data)
+    {
+        std::array<fu2::unique_function<void()>, 4> cbs;
+        std::array<testing::StrictMock<MockCancel>, 4> cancels;
+        for (uint8_t i = 0; i < 4; ++i)
+        {
+            expectRead(i, cbs[i], cancels[i], data);
+        }
+        auto ret = std::make_unique<HothSKMHSSBlobHandler>(&dbus, &hvnutil);
+        for (uint8_t i = 0; i < 4; ++i)
+        {
+            cbs[i]();
+        }
+        testing::Mock::VerifyAndClearExpectations(&hvnutil);
+        testing::Mock::VerifyAndClearExpectations(&dbus);
+        return ret;
+    }
+
+    // Create a handler with empty HSS cache.
+    std::unique_ptr<HothSKMHSSBlobHandler> createHandlerWithEmptyCache()
+    {
+        return createHandler({});
+    }
+
+    // Create a handler with test HSS cache.
+    std::unique_ptr<HothSKMHSSBlobHandler> createHandlerWithTestCache()
+    {
+        return createHandler(testHSSBytes);
+    }
+
+  protected:
+    // dbus mock object
+    testing::StrictMock<internal::DbusCommandMock> dbus;
+
+    // Hoth utility mock object
+    testing::StrictMock<internal::HothUtilMock> hvnutil;
+
+    const uint16_t session = 0;
+    const std::vector<std::string> legacyPath = {
+        "/skm/hss-backup/0",
+        "/skm/hss-backup/1",
+        "/skm/hss-backup/2",
+        "/skm/hss-backup/3",
+    };
+    const std::vector<uint8_t> testHSSBytes = std::vector<uint8_t>(64, 0x0b);
+};
+
+class HothSKMHSSBasicTest : public HothSKMHSSTest
+{
+  public:
+    HothSKMHSSBasicTest() :
+        hvn(HothSKMHSSBasicTest::createHandlerWithTestCache())
+    {}
+    std::unique_ptr<HothSKMHSSBlobHandler> hvn;
+};
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_write_unittest.cpp b/skmhss/test/hoth_skmhss_write_unittest.cpp
new file mode 100644
index 0000000..7098550
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_write_unittest.cpp
@@ -0,0 +1,119 @@
+// Copyright 2024 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::_;
+using ::testing::Return;
+
+class HothSKMHSSWriteTest : public HothSKMHSSBasicTest
+{
+  protected:
+    void openWriteCachedBlob(int)
+    {
+        EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+            .WillOnce(Return(true));
+        EXPECT_TRUE(hvn->open(session, blobs::OpenFlags::write, legacyPath[1]));
+    }
+};
+
+TEST_F(HothSKMHSSWriteTest, InvalidSessionWriteIsRejected)
+{
+    // Verify the hoth command handler checks for a valid session.
+
+    std::vector<uint8_t> data = {0x1, 0x2};
+
+    EXPECT_FALSE(hvn->write(session, 0, data));
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(hvn->stat(session, &meta));
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingTooMuchByOneByteFails)
+{
+    // Test the edge case of writing 1 byte too much with an offset of 0.
+    // writing 65 at offset 0 is invalid.
+
+    openWriteCachedBlob(0);
+    std::vector<uint8_t> data(hvn->maxBufferSize() + 1, 0x11);
+    EXPECT_FALSE(hvn->write(session, 0, data));
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(meta.size, 0);
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingTooMuchByOffsetOfOneFails)
+{
+    // Test the edge case of writing 64 bytes (which is fine) but at the
+    // offset 1, which makes it go over by 1 byte. Writing 64 at offset 1 is
+    // invalid.
+
+    openWriteCachedBlob(1);
+    std::vector<uint8_t> data(hvn->maxBufferSize(), 0x11);
+    EXPECT_FALSE(hvn->write(session, 1, data));
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingOneByteBeyondEndFromOffsetFails)
+{
+    // Test the edge case of writing to the last byte offset but trying to
+    // write two bytes. Writing 2 bytes at 63 is invalid.
+
+    openWriteCachedBlob(2);
+    std::vector<uint8_t> data = {0x01, 0x02};
+    EXPECT_FALSE(hvn->write(session, (hvn->maxBufferSize() - 1), data));
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingOneByteAtOffsetBeyondEndFails)
+{
+    // Test the edge case of writing one byte but exactly one byte beyond the
+    // buffer. Writing 1 byte at 64 is invalid.
+
+    openWriteCachedBlob(3);
+    std::vector<uint8_t> data = {0x01};
+    EXPECT_FALSE(hvn->write(session, hvn->maxBufferSize(), data));
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingFullBufferAtOffsetZeroSucceeds)
+{
+    // Test the case where you write the full buffer length at once to the 0th
+    // offset.
+
+    openWriteCachedBlob(0);
+    std::vector<uint8_t> data(hvn->maxBufferSize(), 0x11);
+    EXPECT_TRUE(hvn->write(session, 0, data));
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingOneByteToTheLastOffsetSucceeds)
+{
+    // Test the case where you write the last byte.
+
+    openWriteCachedBlob(0);
+    std::vector<uint8_t> data = {0x01};
+    EXPECT_TRUE(hvn->write(session, (hvn->maxBufferSize() - 1), data));
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn->stat(session, &meta));
+    EXPECT_EQ(meta.size, 64);
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_util_mock.hpp b/skmhss/test/hoth_util_mock.hpp
new file mode 100644
index 0000000..61334df
--- /dev/null
+++ b/skmhss/test/hoth_util_mock.hpp
@@ -0,0 +1,47 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "hoth_util.hpp"
+
+#include <stdplus/raw.hpp>
+
+#include <cstdint>
+#include <span>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+class HothUtilMock : public HothUtil
+{
+  public:
+    MOCK_METHOD1(wrapECSKMHSSRequest,
+                 std::vector<uint8_t>(std::span<const uint8_t> request));
+    MOCK_METHOD1(readSKMHSS, std::vector<uint8_t>(uint32_t slot));
+    MOCK_METHOD2(writeSKMHSS,
+                 std::vector<uint8_t>(uint32_t slot,
+                                      std::span<const uint8_t> data));
+    MOCK_METHOD1(deleteSKMHSS, std::vector<uint8_t>(uint32_t slot));
+    MOCK_METHOD1(payloadECResponse,
+                 std::span<const uint8_t>(std::span<const uint8_t> rsp));
+};
+
+} // namespace internal
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_util_unittest.cpp b/skmhss/test/hoth_util_unittest.cpp
new file mode 100644
index 0000000..0b71526
--- /dev/null
+++ b/skmhss/test/hoth_util_unittest.cpp
@@ -0,0 +1,173 @@
+// Copyright 2024 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 "dbus_mock.hpp"
+#include "hoth_util.hpp"
+#include "hoth_util_impl.hpp"
+
+#include <xyz/openbmc_project/Control/Hoth/error.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <numeric>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace ::testing;
+using CommandFailure =
+    sdbusplus::xyz::openbmc_project::Control::Hoth::Error::CommandFailure;
+using ResponseFailure =
+    sdbusplus::xyz::openbmc_project::Control::Hoth::Error::ResponseFailure;
+
+namespace ipmi_hoth
+{
+
+using namespace internal;
+
+class HothUtilTest : public ::testing::Test
+{
+  protected:
+    HothUtilTest() : hothUtil(std::make_unique<HothUtilImpl>(HothUtilImpl())) {}
+
+    std::unique_ptr<HothUtil> hothUtil;
+};
+
+TEST_F(HothUtilTest, Checksum)
+{
+    uint8_t header[] = {1, 3, 5, 7};
+    uint8_t body[] = {2, 4, 6, 8};
+
+    // (1 + 3 + 5 + 7) + (2 + 4 + 6 + 8) = 36
+    EXPECT_EQ(36, HothUtil::calculateChecksum(header, body));
+
+    // Now test what happens if the sum exceeds 256.
+    body[0] = 200;
+    body[1] = 202;
+
+    // (1 + 3 + 5 + 7) + (200 + 202 + 6 + 8) = 432
+    // 432 % 256 = 176
+    EXPECT_EQ(176, HothUtil::calculateChecksum(header, body));
+}
+
+TEST_F(HothUtilTest, WrapECSKMHSSRequest)
+{
+    std::vector<uint8_t> request(15);
+    std::generate(request.begin(), request.end(), std::rand);
+    std::vector<uint8_t> reqBytes;
+    reqBytes = std::move(hothUtil->wrapECSKMHSSRequest(request));
+    EXPECT_FALSE(reqBytes.empty());
+    EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+}
+
+TEST_F(HothUtilTest, ReadSKMHSS)
+{
+    std::vector<uint8_t> reqBytes;
+    reqBytes = std::move(hothUtil->readSKMHSS(0));
+    EXPECT_FALSE(reqBytes.empty());
+    EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+    reqBytes = std::move(hothUtil->readSKMHSS(1));
+    EXPECT_FALSE(reqBytes.empty());
+    EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+    reqBytes = std::move(hothUtil->readSKMHSS(2));
+    EXPECT_FALSE(reqBytes.empty());
+    EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+    reqBytes = std::move(hothUtil->readSKMHSS(3));
+    EXPECT_FALSE(reqBytes.empty());
+    EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+}
+
+TEST_F(HothUtilTest, WriteSKMHSS)
+{
+    std::vector<uint8_t> payload(128);
+    std::vector<uint8_t> reqBytes;
+    std::generate(payload.begin(), payload.end(), std::rand);
+    EXPECT_THROW(hothUtil->writeSKMHSS(0, payload), CommandFailure);
+    EXPECT_TRUE(reqBytes.empty());
+
+    payload.resize(64);
+    reqBytes = std::move(hothUtil->writeSKMHSS(1, payload));
+    EXPECT_FALSE(reqBytes.empty());
+    EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+    payload = {0, 0, 0, 0,   0,   0,   0,  0,  0, 0, 0, 0, 0,   0,  0, 0,
+               0, 0, 0, 0,   0,   0,   0,  0,  0, 0, 0, 0, 0,   0,  0, 0,
+               0, 0, 0, 0,   0,   0,   0,  0,  0, 0, 0, 0, 0,   0,  0, 0,
+               0, 0, 1, 110, 205, 147, 54, 43, 0, 0, 1, 1, 175, 58, 9, 239};
+    EXPECT_NO_THROW(hothUtil->writeSKMHSS(0, payload));
+    EXPECT_NO_THROW(hothUtil->writeSKMHSS(1, payload));
+    EXPECT_NO_THROW(hothUtil->writeSKMHSS(2, payload));
+    EXPECT_NO_THROW(hothUtil->writeSKMHSS(3, payload));
+}
+
+TEST_F(HothUtilTest, DeleteSKMHSS)
+{
+    std::vector<uint8_t> reqBytes;
+    reqBytes = std::move(hothUtil->deleteSKMHSS(0));
+    EXPECT_FALSE(reqBytes.empty());
+    EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+    reqBytes = std::move(hothUtil->deleteSKMHSS(1));
+    EXPECT_FALSE(reqBytes.empty());
+    EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+    reqBytes = std::move(hothUtil->deleteSKMHSS(2));
+    EXPECT_FALSE(reqBytes.empty());
+    EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+    reqBytes = std::move(hothUtil->deleteSKMHSS(3));
+    EXPECT_FALSE(reqBytes.empty());
+    EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+}
+
+TEST_F(HothUtilTest, PayloadECResponse)
+{
+    std::vector<uint8_t> validRsp = {0x03, 0xfd, 0x00, 0x00,
+                                     0x00, 0x00, 0x00, 0x00};
+    EXPECT_NO_THROW(hothUtil->payloadECResponse(validRsp));
+
+    std::vector<uint8_t> rspShortLength = {0x03, 0xfd};
+    EXPECT_THROW(hothUtil->payloadECResponse(rspShortLength), ResponseFailure);
+
+    std::vector<uint8_t> rspVersionErr = {0x05, 0xfb, 0x00, 0x00,
+                                          0x00, 0x00, 0x00, 0x00};
+    EXPECT_THROW(hothUtil->payloadECResponse(rspVersionErr), ResponseFailure);
+
+    std::vector<uint8_t> rspNonZeroRC = {0x03, 0xfc, 0x01, 0x00,
+                                         0x00, 0x00, 0x00, 0x00};
+    EXPECT_THROW(hothUtil->payloadECResponse(rspNonZeroRC), ResponseFailure);
+
+    std::vector<uint8_t> rspExpectedNonZeroRC = {0x03, 0xf4, 0x09, 0x00,
+                                                 0x00, 0x00, 0x00, 0x00};
+    EXPECT_NO_THROW(hothUtil->payloadECResponse(rspExpectedNonZeroRC));
+
+    std::vector<uint8_t> payload(64);
+    std::generate(payload.begin(), payload.end(), std::rand);
+    uint8_t payloadSum =
+        std::accumulate(std::begin(payload), std::end(payload), 0) % 256;
+    uint8_t checksum = 0x100 - (0x03 + 0x40 + payloadSum) % 256;
+    std::vector<uint8_t> validRspWithRandomHss = {0x03, 0x00, 0x00, 0x00,
+                                                  0x40, 0x00, 0x00, 0x00};
+    validRspWithRandomHss[1] = checksum;
+    validRspWithRandomHss.insert(validRspWithRandomHss.end(), payload.begin(),
+                                 payload.end());
+    auto data = hothUtil->payloadECResponse(validRspWithRandomHss);
+    EXPECT_EQ(std::vector<uint8_t>(data.begin(), data.end()), payload);
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/meson.build b/skmhss/test/meson.build
new file mode 100644
index 0000000..89bc57c
--- /dev/null
+++ b/skmhss/test/meson.build
@@ -0,0 +1,20 @@
+tests = [
+  'hoth_skmhss_unittest',
+  'hoth_skmhss_close_unittest',
+  'hoth_skmhss_commit_unittest',
+  'hoth_skmhss_delete_unittest',
+  'hoth_skmhss_open_unittest',
+  'hoth_skmhss_read_unittest',
+  'hoth_skmhss_write_unittest',
+  'hoth_util_unittest',
+]
+
+foreach t : tests
+  test(
+    t,
+    executable(
+      t.underscorify(),
+      t + '.cpp',
+      implicit_include_directories: false,
+      dependencies: [hothskmhss_dep, test_dep]))
+endforeach
diff --git a/test/dbus_mock.hpp b/test/dbus_mock.hpp
new file mode 100644
index 0000000..a2a9ed3
--- /dev/null
+++ b/test/dbus_mock.hpp
@@ -0,0 +1,45 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus.hpp"
+
+#include <ipmid/api.h>
+#include <systemd/sd-bus.h>
+
+#include <string_view>
+
+#include <gmock/gmock.h>
+
+sd_bus* ipmid_get_sd_bus_connection()
+{
+    return nullptr;
+}
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+class DbusMock : public virtual Dbus
+{
+  public:
+    MOCK_METHOD0(getHothdMapping, SubTreeMapping());
+    MOCK_METHOD1(pingHothd, bool(std::string_view));
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..4eed4c0
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,6 @@
+gtest = dependency('gtest', main: true, disabler: true, required: get_option('tests'))
+gmock = dependency('gmock', disabler: true, required: get_option('tests'))
+
+test_dep = declare_dependency(
+  include_directories: include_directories('.'),
+  dependencies: [gtest, gmock])
diff --git a/update/dbus_update.hpp b/update/dbus_update.hpp
new file mode 100644
index 0000000..7ec3b23
--- /dev/null
+++ b/update/dbus_update.hpp
@@ -0,0 +1,73 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus.hpp"
+
+#include <function2/function2.hpp>
+#include <stdplus/cancel.hpp>
+#include <xyz/openbmc_project/Control/Hoth/server.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class DbusUpdate
+ *  @brief Overridable D-Bus interface for command handler
+ */
+class DbusUpdate : public virtual Dbus
+{
+  public:
+    using FirmwareUpdateStatus = sdbusplus::xyz::openbmc_project::Control::
+        server::Hoth::FirmwareUpdateStatus;
+
+    using Cb = fu2::unique_function<void(FirmwareUpdateStatus)>;
+
+    /** @brief Implementation for UpdateFirmware
+     *  Executes hothd's updateFirmware with the given firmware binary. This
+     *  is an asynchronous call and should not return anything. Poll the status
+     *  of the update with GetFirmwareUpdateStatus.
+     *
+     *  @param[in] hothId - The identifier of the targeted hoth instance.
+     *  @param[in] firmwareData - Firmware binary to write to Hoth's
+     *                            firmware update SPI partition
+     *  @param[in] cb - The callback to execute when the request completes.
+     *  @return cancelable[stdplus::Cancel] - Drop to cancel the command early.
+     */
+    virtual stdplus::Cancel
+        UpdateFirmware(std::string_view hothId,
+                       const std::vector<uint8_t>& firmwareData, Cb&& cb) = 0;
+
+    /** @brief Implementation for GetFirmwareUpdateStatus
+     *  Executes hothd's getFirmwareUpdateStatus. This is an asynchronous call
+     *  that returns the status of the write to Hoth's firmware update SPI
+     *  partition.
+     *
+     *  @param[in] hothId - The identifier of the targeted hoth instance.
+     *  @param[in] cb - The callback to execute when the request completes.
+     *  @return cancelable[stdplus::Cancel] - Drop to cancel the command early.
+     */
+    virtual stdplus::Cancel
+        GetFirmwareUpdateStatus(std::string_view hothId, Cb&& cb) = 0;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/update/dbus_update_impl.cpp b/update/dbus_update_impl.cpp
new file mode 100644
index 0000000..c739ddf
--- /dev/null
+++ b/update/dbus_update_impl.cpp
@@ -0,0 +1,88 @@
+// Copyright 2024 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 "dbus_update_impl.hpp"
+
+#include <fmt/format.h>
+
+#include <optional>
+#include <string>
+#include <utility>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+stdplus::Cancel DbusUpdateImpl::UpdateFirmware(
+    std::string_view hothId, const std::vector<uint8_t>& firmwareData, Cb&& cb)
+{
+    auto req = newHothdCall(hothId, "UpdateFirmware");
+    req.append(firmwareData);
+    auto cHothId = std::string(hothId);
+    return stdplus::Cancel(new BusCall(req.call_async(
+        [cb = std::move(cb), hothId = std::move(cHothId)](
+            sdbusplus::message::message m) mutable noexcept {
+            if (m.is_method_error())
+            {
+                auto err = m.get_error();
+                fmt::print(stderr, "UpdateFirmware failed on `{}`: {}: {}\n",
+                           hothId, err->name, err->message);
+                cb(FirmwareUpdateStatus::Error);
+                return;
+            }
+            cb(FirmwareUpdateStatus::InProgress);
+        },
+        asyncCallTimeout)));
+}
+
+stdplus::Cancel
+    DbusUpdateImpl::GetFirmwareUpdateStatus(std::string_view hothId, Cb&& cb)
+{
+    auto req = newHothdCall(hothId, "GetFirmwareUpdateStatus");
+    auto cHothId = std::string(hothId);
+    return stdplus::Cancel(new BusCall(req.call_async(
+        [cb = std::move(cb), hothId = std::move(cHothId)](
+            sdbusplus::message::message m) mutable noexcept {
+            if (m.is_method_error())
+            {
+                auto err = m.get_error();
+                fmt::print(stderr,
+                           "GetFirmwareUpdateStatus failed on `{}`: {}: {}\n",
+                           hothId, err->name, err->message);
+                cb(FirmwareUpdateStatus::Error);
+                return;
+            }
+            try
+            {
+                std::string rsp;
+                m.read(rsp);
+                cb(sdbusplus::xyz::openbmc_project::Control::server::Hoth::
+                       convertFirmwareUpdateStatusFromString(rsp));
+            }
+            catch (const std::exception& e)
+            {
+                fmt::print(
+                    stderr,
+                    "GetFirmwareUpdateStatus failed unpacking on `{}`: {}\n",
+                    hothId, e.what());
+                cb(FirmwareUpdateStatus::Error);
+            }
+        },
+        asyncCallTimeout)));
+}
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/update/dbus_update_impl.hpp b/update/dbus_update_impl.hpp
new file mode 100644
index 0000000..60a2370
--- /dev/null
+++ b/update/dbus_update_impl.hpp
@@ -0,0 +1,45 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus_impl.hpp"
+#include "dbus_update.hpp"
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class DbusImpl
+ *  @brief D-Bus concrete implementation
+ *  @details Pass through all calls to the default D-Bus instance
+ */
+class DbusUpdateImpl : public DbusUpdate, public DbusImpl
+{
+  public:
+    stdplus::Cancel UpdateFirmware(std::string_view hothId,
+                                   const std::vector<uint8_t>& firmwareData,
+                                   Cb&& cb) override;
+    stdplus::Cancel GetFirmwareUpdateStatus(std::string_view hothId,
+                                            Cb&& cb) override;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/update/hoth_update.cpp b/update/hoth_update.cpp
new file mode 100644
index 0000000..59a6e33
--- /dev/null
+++ b/update/hoth_update.cpp
@@ -0,0 +1,130 @@
+// Copyright 2024 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 "hoth_update.hpp"
+
+#include <phosphor-logging/log.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+using phosphor::logging::entry;
+using phosphor::logging::log;
+
+using level = phosphor::logging::level;
+using SdBusError = sdbusplus::exception::SdBusError;
+
+namespace ipmi_hoth
+{
+
+void updateBlobState(uint16_t& state,
+                     internal::DbusUpdate::FirmwareUpdateStatus st)
+{
+    state &= ~(blobs::StateFlags::commit_error | blobs::StateFlags::committing |
+               blobs::StateFlags::committed);
+    switch (st)
+    {
+        case internal::DbusUpdate::FirmwareUpdateStatus::None:
+            break;
+        case internal::DbusUpdate::FirmwareUpdateStatus::InProgress:
+            state |= blobs::StateFlags::committing;
+            break;
+        case internal::DbusUpdate::FirmwareUpdateStatus::Error:
+            state |= blobs::StateFlags::commit_error;
+            break;
+        case internal::DbusUpdate::FirmwareUpdateStatus::Done:
+            state |= blobs::StateFlags::committed;
+            break;
+    }
+}
+
+bool HothUpdateBlobHandler::stat(const std::string& path, blobs::BlobMeta* meta)
+{
+    std::optional<uint16_t> sess = getOnlySession(pathToHothId(path));
+    if (!sess)
+    {
+        return false;
+    }
+
+    /* Call session stat with the session ID of the only session */
+    return stat(*sess, meta);
+}
+
+bool HothUpdateBlobHandler::commit(uint16_t session,
+                                   const std::vector<uint8_t>& data)
+{
+    if (!data.empty())
+    {
+        log<level::ERR>("Unexpected data provided to commit call");
+        return false;
+    }
+
+    HothBlob* sess = getSession(session);
+    if (!sess)
+    {
+        return false;
+    }
+
+    // If commit is called multiple times, return the same result as last time
+    if (sess->state &
+        (blobs::StateFlags::committing | blobs::StateFlags::committed))
+    {
+        return true;
+    }
+
+    updateBlobState(sess->state,
+                    internal::DbusUpdate::FirmwareUpdateStatus::InProgress);
+    sess->outstanding = dbus_->UpdateFirmware(
+        sess->hothId, sess->buffer,
+        [sess](internal::DbusUpdate::FirmwareUpdateStatus rsp) {
+            auto outstanding = std::move(sess->outstanding);
+            updateBlobState(sess->state, rsp);
+        });
+    return true;
+}
+
+bool HothUpdateBlobHandler::stat(uint16_t session, blobs::BlobMeta* meta)
+{
+    HothBlob* sess = getSession(session);
+    if (!sess)
+    {
+        // Return false since blobs::BlobMeta was not updated
+        return false;
+    }
+
+    // We only want to start a new status check if we don't have a pending job
+    // and we hoth't receivied a completion state (error / done).
+    if (!sess->outstanding && (sess->state & blobs::StateFlags::committing))
+    {
+        sess->outstanding = dbus_->GetFirmwareUpdateStatus(
+            sess->hothId,
+            [sess](internal::DbusUpdate::FirmwareUpdateStatus rsp) {
+                auto outstanding = std::move(sess->outstanding);
+                updateBlobState(sess->state, rsp);
+            });
+    }
+
+    meta->size = sess->buffer.size();
+    meta->blobState = sess->state;
+    // Return true since blobs::BlobMeta was updated
+    return true;
+}
+
+} // namespace ipmi_hoth
diff --git a/update/hoth_update.hpp b/update/hoth_update.hpp
new file mode 100644
index 0000000..1dab418
--- /dev/null
+++ b/update/hoth_update.hpp
@@ -0,0 +1,68 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus_update.hpp"
+#include "hoth.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+
+class HothUpdateBlobHandler : public HothBlobHandler
+{
+  public:
+    explicit HothUpdateBlobHandler(internal::DbusUpdate* dbus) : dbus_(dbus) {}
+
+    /* Our callbacks require pinned memory */
+    HothUpdateBlobHandler(HothUpdateBlobHandler&&) = delete;
+    HothUpdateBlobHandler& operator=(HothUpdateBlobHandler&&) = delete;
+
+    bool stat(const std::string& path, blobs::BlobMeta* meta) override;
+    bool commit(uint16_t session, const std::vector<uint8_t>& data) override;
+    bool stat(uint16_t session, blobs::BlobMeta* meta) override;
+
+    internal::Dbus& dbus() override
+    {
+        return *dbus_;
+    }
+    std::string_view pathSuffix() const override
+    {
+        return "firmware_update";
+    }
+    uint16_t requiredFlags() const override
+    {
+        return blobs::OpenFlags::write;
+    }
+    uint16_t maxSessions() const override
+    {
+        return 1;
+    }
+    uint32_t maxBufferSize() const override
+    {
+        return 1024 * 1024;
+    }
+
+  private:
+    internal::DbusUpdate* dbus_;
+};
+
+} // namespace ipmi_hoth
diff --git a/update/main_update.cpp b/update/main_update.cpp
new file mode 100644
index 0000000..fee3735
--- /dev/null
+++ b/update/main_update.cpp
@@ -0,0 +1,28 @@
+// Copyright 2024 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 "dbus_update_impl.hpp"
+#include "hoth_update.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <memory>
+
+extern "C" std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+    /** @brief Default instantiation of Dbus */
+    static ipmi_hoth::internal::DbusUpdateImpl dbusUpdate_impl;
+
+    return std::make_unique<ipmi_hoth::HothUpdateBlobHandler>(&dbusUpdate_impl);
+}
diff --git a/update/meson.build b/update/meson.build
new file mode 100644
index 0000000..1d2ae5d
--- /dev/null
+++ b/update/meson.build
@@ -0,0 +1,31 @@
+hothupdate_pre = declare_dependency(
+  include_directories: include_directories('.'),
+  dependencies: [
+    hothblob_dep,
+    dependency('hothd-dbus'),
+    phosphor_logging_dep,
+    sdbusplus_dep,
+  ])
+
+hothupdate_lib = static_library(
+  'hothupdate',
+  'dbus_update_impl.cpp',
+  'hoth_update.cpp',
+  implicit_include_directories: false,
+  dependencies: hothupdate_pre)
+
+hothupdate_dep = declare_dependency(
+  link_with: hothupdate_lib,
+  dependencies: hothupdate_pre)
+
+shared_module(
+  'hothupdate',
+  'main_update.cpp',
+  implicit_include_directories: false,
+  dependencies: hothupdate_dep,
+  install: true,
+  install_dir: get_option('libdir') / 'blob-ipmid')
+
+if not get_option('tests').disabled()
+  subdir('test')
+endif
diff --git a/update/test/dbus_update_mock.hpp b/update/test/dbus_update_mock.hpp
new file mode 100644
index 0000000..334d4be
--- /dev/null
+++ b/update/test/dbus_update_mock.hpp
@@ -0,0 +1,40 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus_mock.hpp"
+#include "dbus_update.hpp"
+
+#include <gmock/gmock.h>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+class DbusUpdateMock : public DbusUpdate, public DbusMock
+{
+  public:
+    MOCK_METHOD(stdplus::Cancel, UpdateFirmware,
+                (std::string_view hothId,
+                 const std::vector<uint8_t>& firmwareData, Cb&& cb),
+                (override));
+    MOCK_METHOD(stdplus::Cancel, GetFirmwareUpdateStatus,
+                (std::string_view hothId, Cb&& cb), (override));
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_close_unittest.cpp b/update/test/hoth_update_close_unittest.cpp
new file mode 100644
index 0000000..c069a9b
--- /dev/null
+++ b/update/test/hoth_update_close_unittest.cpp
@@ -0,0 +1,58 @@
+// Copyright 2024 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 "hoth_update_unittest.hpp"
+
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothUpdateCloseTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateCloseTest, CloseWithInvalidSessionFails)
+{
+    // Verify you cannot close an invalid session.
+
+    EXPECT_FALSE(hvn.close(session_));
+}
+
+TEST_F(HothUpdateCloseTest, CloseWithValidSessionSuccess)
+{
+    // Verify you can close a valid session.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.close(session_));
+}
+
+TEST_F(HothUpdateCloseTest, CloseValidSessionTwiceFails)
+{
+    // Verify that close actually closes the session, expect the second close
+    // of the same session to fail.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.close(session_));
+    EXPECT_FALSE(hvn.close(session_));
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_commit_unittest.cpp b/update/test/hoth_update_commit_unittest.cpp
new file mode 100644
index 0000000..1421c74
--- /dev/null
+++ b/update/test/hoth_update_commit_unittest.cpp
@@ -0,0 +1,216 @@
+// Copyright 2024 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 "hoth_update_unittest.hpp"
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::IsEmpty;
+using ::testing::NotNull;
+using ::testing::Return;
+
+using namespace std::literals;
+
+namespace ipmi_hoth
+{
+
+using Cb = internal::DbusUpdate::Cb;
+using FirmwareUpdateStatus = internal::DbusUpdate::FirmwareUpdateStatus;
+
+class HothUpdateCommitTest : public HothUpdateTest
+{
+  protected:
+    template <typename Req>
+    void expectUpdate(std::string_view hothId, Req&& req)
+    {
+        EXPECT_CALL(dbus, UpdateFirmware(hothId, req, _))
+            .WillOnce(
+                [&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+                    cb = std::move(icb);
+                    return stdplus::Cancel(std::nullopt);
+                });
+    }
+
+    Cb cb;
+};
+
+const auto static test_str = "Hello,\0 world!"s;
+const std::vector<uint8_t> static test_buf(test_str.begin(), test_str.end());
+
+TEST_F(HothUpdateCommitTest, InvalidSessionCommitIsRejected)
+{
+    // Verify the hoth update handler checks for a valid session.
+
+    EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>()));
+}
+
+TEST_F(HothUpdateCommitTest, NonEmptyDataParamFails)
+{
+    // Verify that we do not accept any data passed into the commit call.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>({1, 2, 3})));
+}
+
+TEST_F(HothUpdateCommitTest, DbusExceptionSetsCommitErrorStateAndFails)
+{
+    // Verify that when the dbus call hits an exception, commit method fails
+    // and that the state is set to blobs::StateFlags::commit_error
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    expectUpdate(/*hothId=*/"", ContainerEq(test_buf));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::Error);
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+
+    EXPECT_EQ(meta.blobState,
+              blobs::StateFlags::open_write | blobs::StateFlags::commit_error);
+}
+
+TEST_F(HothUpdateCommitTest, EmptyBufferCommitLegacyHothSuccessful)
+{
+    // Verify that we are able to succssfully commit an empty buffer
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    expectUpdate(/*hothId=*/"", IsEmpty());
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // No write call to populate the buffer here
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::Done);
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+
+    EXPECT_EQ(meta.size, 0);
+    EXPECT_EQ(meta.blobState,
+              blobs::StateFlags::open_write | blobs::StateFlags::committed);
+}
+
+TEST_F(HothUpdateCommitTest, EmptyBufferCommitNamedHothSuccessful)
+{
+    // Verify that we are able to succssfully commit an empty buffer
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(name))).WillOnce(Return(true));
+    expectUpdate(/*hothId=*/name, IsEmpty());
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), namedPath));
+    // No write call to populate the buffer here
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::Done);
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+
+    EXPECT_EQ(meta.size, 0);
+    EXPECT_EQ(meta.blobState,
+              blobs::StateFlags::open_write | blobs::StateFlags::committed);
+}
+
+TEST_F(HothUpdateCommitTest, NonEmptyBufferCommitSuccessful)
+{
+    // Verify that we are able to succssfully commit a buffer with valid data
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    expectUpdate(/*hothId=*/"", ContainerEq(test_buf));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::Done);
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+
+    EXPECT_EQ(meta.size, test_buf.size());
+    EXPECT_EQ(meta.blobState,
+              blobs::StateFlags::open_write | blobs::StateFlags::committed);
+}
+
+TEST_F(HothUpdateCommitTest, IdempotentSuccess)
+{
+    // Verify that repeated successful commits only result in one D-Bus call
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+    expectUpdate(/*hothId=*/"", IsEmpty());
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // No write call to populate the buffer here
+
+    // These calls will return due to the state being "committing". Calling
+    // stat is the only way to check if the state has changed to "committed"
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::Done);
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+
+    // These calls will return due to the state being "committed"
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    // Stat is also idempotent, no need to mock the D-Bus call here
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+
+    EXPECT_EQ(meta.size, 0);
+    EXPECT_EQ(meta.blobState,
+              blobs::StateFlags::open_write | blobs::StateFlags::committed);
+}
+
+TEST_F(HothUpdateCommitTest, ErrorRetry)
+{
+    // Verify that commit after error retries the update D-Bus call
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    // These calls will return due to the state being "commit_error"
+    expectUpdate(/*hothId=*/"", IsEmpty());
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::Error);
+    expectUpdate(/*hothId=*/"", IsEmpty());
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::Error);
+    expectUpdate(/*hothId=*/"", IsEmpty());
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::Error);
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+
+    EXPECT_EQ(meta.blobState,
+              blobs::StateFlags::open_write | blobs::StateFlags::commit_error);
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_delete_unittest.cpp b/update/test/hoth_update_delete_unittest.cpp
new file mode 100644
index 0000000..41aa16e
--- /dev/null
+++ b/update/test/hoth_update_delete_unittest.cpp
@@ -0,0 +1,32 @@
+// Copyright 2024 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 "hoth_update_unittest.hpp"
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+class HothUpdateDeleteTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateDeleteTest, VerifyHothDeleteFails)
+{
+    // The hoth update handler does not support delete.
+
+    EXPECT_FALSE(hvn.deleteBlob(legacyPath));
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_open_unittest.cpp b/update/test/hoth_update_open_unittest.cpp
new file mode 100644
index 0000000..f3aeaae
--- /dev/null
+++ b/update/test/hoth_update_open_unittest.cpp
@@ -0,0 +1,75 @@
+// Copyright 2024 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 "hoth_update_unittest.hpp"
+
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothUpdateOpenTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateOpenTest, OpenWithBadFlagsFails)
+{
+    // Hoth update handler open requires the write flag.
+
+    EXPECT_FALSE(hvn.open(session_, blobs::OpenFlags::read, legacyPath));
+}
+
+TEST_F(HothUpdateOpenTest, OpenEverythingSucceeds)
+{
+    // Hoth update handler open with everything correct.
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+}
+
+TEST_F(HothUpdateOpenTest, OpenSecondSessionFails)
+{
+    // Hoth update handler only allows 1 open session, verify second one fails
+    // regardless of whether the session ID is the same or not
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+        .WillRepeatedly(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.open(session_ + 1, hvn.requiredFlags(), legacyPath));
+}
+
+TEST_F(HothUpdateOpenTest, OpenCloseOpenSucceeds)
+{
+    // Hoth update handler should be able to open a session after closing an
+    // open one
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+        .WillRepeatedly(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.close(session_));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_read_unittest.cpp b/update/test/hoth_update_read_unittest.cpp
new file mode 100644
index 0000000..faa05b2
--- /dev/null
+++ b/update/test/hoth_update_read_unittest.cpp
@@ -0,0 +1,44 @@
+// Copyright 2024 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 "hoth_update_unittest.hpp"
+
+#include <ipmid/handler.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothUpdateReadTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateReadTest, VerifyHothReadFails)
+{
+    // The hoth update handler does not support read, expect empty vector
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_THROW(hvn.read(session_, 0, 1), ipmi::HandlerCompletion);
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_stat_unittest.cpp b/update/test/hoth_update_stat_unittest.cpp
new file mode 100644
index 0000000..54b66f1
--- /dev/null
+++ b/update/test/hoth_update_stat_unittest.cpp
@@ -0,0 +1,379 @@
+// Copyright 2024 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 "hoth_update_unittest.hpp"
+
+#include <string_view>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::NotNull;
+using ::testing::Return;
+
+using namespace std::literals;
+
+namespace ipmi_hoth
+{
+
+using Cb = internal::DbusUpdate::Cb;
+using FirmwareUpdateStatus = internal::DbusUpdate::FirmwareUpdateStatus;
+
+class HothUpdateStatTest : public HothUpdateTest
+{
+  protected:
+    blobs::BlobMeta meta_;
+    // Initialize expected_meta_ with empty members
+    blobs::BlobMeta expected_meta_;
+};
+class HothUpdateSessionStatTest : public HothUpdateStatTest
+{};
+
+struct MockCancel : stdplus::Cancelable
+{
+    MOCK_METHOD(void, cancel, (), (noexcept, override));
+};
+
+const auto static test_str = "Hello,\0 world!"s;
+const std::vector<uint8_t> static test_buf(test_str.begin(), test_str.end());
+
+TEST_F(HothUpdateStatTest, InvalidStatIsRejected)
+{
+    // Verify the hoth update handler checks for a valid session.
+
+    EXPECT_FALSE(hvn.stat(legacyPath, &meta_));
+}
+
+TEST_F(HothUpdateStatTest, StatBeforeCommitReturnsInitialState)
+{
+    // Verify stat returns initial state before commit
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.stat(legacyPath, &meta_));
+
+    expected_meta_.size = 0;
+    expected_meta_.blobState = blobs::StateFlags::open_write;
+    EXPECT_EQ(meta_, expected_meta_);
+}
+
+// Rest of the tests will be using session stat, since
+// stat with blob ID calls session ID after the initial checks
+
+TEST_F(HothUpdateSessionStatTest, InvalidSessionStatIsRejected)
+{
+    // Verify the hoth update handler checks for a valid session.
+
+    EXPECT_FALSE(hvn.stat(0, &meta_));
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatBeforeCommitReturnsInitialState)
+{
+    // Verify the session stat before commit returns initial state
+    // without any D-Bus call
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+    expected_meta_.size = 0;
+    expected_meta_.blobState = blobs::StateFlags::open_write;
+    EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatAfterWriteMetadataLengthMatches)
+{
+    // Verify that after writes, the length returned matches
+    // without any D-bus call
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+    // We wrote one byte to the last index, making the length the buffer size.
+    expected_meta_.size = test_buf.size();
+    expected_meta_.blobState = blobs::StateFlags::open_write;
+    EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatAfterErrorCommitReturnsStatus)
+{
+    // Verify that after commit errors out early, the session stat
+    // returns the initial status without any D-Bus call
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+    EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>({1, 2, 3})));
+
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+    expected_meta_.size = test_buf.size();
+    expected_meta_.blobState = blobs::StateFlags::open_write;
+    EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatErrorReturnsCommitError)
+{
+    // Verify that mocking GetFirmwareUpdateStatus ouptut to Error after
+    // a successful commit makes session stat return 'commit_error' status
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+    Cb cb;
+    EXPECT_CALL(dbus,
+                UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+        .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(std::nullopt);
+        });
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::InProgress);
+
+    EXPECT_CALL(dbus, GetFirmwareUpdateStatus(std::string_view(""), _))
+        .WillOnce([&](std::string_view, Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(std::nullopt);
+        });
+    struct blobs::BlobMeta meta;
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+
+    EXPECT_EQ(meta.size, test_buf.size());
+    EXPECT_EQ(meta.metadata.size(), 0);
+    EXPECT_EQ(meta.blobState,
+              blobs::StateFlags::open_write | blobs::StateFlags::committing);
+
+    cb(FirmwareUpdateStatus::Error);
+    EXPECT_TRUE(hvn.stat(session_, &meta));
+    EXPECT_EQ(meta.blobState,
+              blobs::StateFlags::open_write | blobs::StateFlags::commit_error);
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatInProgressReturnsCommitting)
+{
+    // Verify that mocking GetFirmwareUpdateStatus ouptut to InProgress after
+    // a successful commit makes session stat return 'committing' status
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+    Cb cb;
+    EXPECT_CALL(dbus,
+                UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+        .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(std::nullopt);
+        });
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::InProgress);
+
+    EXPECT_CALL(dbus, GetFirmwareUpdateStatus(std::string_view(""), _))
+        .Times(2)
+        .WillRepeatedly([&](std::string_view, Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(std::nullopt);
+        });
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    cb(FirmwareUpdateStatus::InProgress);
+
+    expected_meta_.size = test_buf.size();
+    expected_meta_.blobState =
+        blobs::StateFlags::open_write | blobs::StateFlags::committing;
+    EXPECT_EQ(meta_, expected_meta_);
+
+    // Check that repeated stats after callback trigger another call
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatDoneReturnsCommitted)
+{
+    // Verify that mocking GetFirmwareUpdateStatus ouptut to Done after
+    // a successful commit makes session stat return 'committed' status
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+    Cb cb;
+    EXPECT_CALL(dbus,
+                UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+        .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(std::nullopt);
+        });
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::InProgress);
+
+    EXPECT_CALL(dbus, GetFirmwareUpdateStatus(std::string_view(""), _))
+        .WillOnce([&](std::string_view, Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(std::nullopt);
+        });
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+    expected_meta_.size = test_buf.size();
+    expected_meta_.blobState =
+        blobs::StateFlags::open_write | blobs::StateFlags::committing;
+    EXPECT_EQ(meta_, expected_meta_);
+
+    cb(FirmwareUpdateStatus::Done);
+    expected_meta_.blobState =
+        blobs::StateFlags::open_write | blobs::StateFlags::committed;
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, MultipleInProgressReturnsCommitting)
+{
+    // Verify that repeated session stat calls while committing
+    // result in multiple D-Bus call
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+    testing::StrictMock<MockCancel> c;
+    Cb cb;
+    EXPECT_CALL(dbus,
+                UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+        .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(&c);
+        });
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+    // If commit is still outstanding we should not issue a new command
+    expected_meta_.size = test_buf.size();
+    expected_meta_.blobState =
+        blobs::StateFlags::open_write | blobs::StateFlags::committing;
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+
+    EXPECT_CALL(c, cancel());
+    cb(FirmwareUpdateStatus::InProgress);
+    testing::Mock::VerifyAndClearExpectations(&c);
+
+    EXPECT_CALL(dbus, GetFirmwareUpdateStatus(std::string_view(""), _))
+        .WillOnce([&](std::string_view, Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(&c);
+        });
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+    // Shouldn't trigger a new call
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+    // Shouldn't trigger a new call
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+
+    EXPECT_CALL(c, cancel());
+    cb(FirmwareUpdateStatus::InProgress);
+}
+
+TEST_F(HothUpdateSessionStatTest, IdempotentDoneReturnsCommitted)
+{
+    // Verify that repeated session stat calls while status is committed
+    // result in one D-Bus call
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+    Cb cb;
+    EXPECT_CALL(dbus,
+                UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+        .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(std::nullopt);
+        });
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::Done);
+
+    expected_meta_.size = test_buf.size();
+    expected_meta_.blobState =
+        blobs::StateFlags::open_write | blobs::StateFlags::committed;
+    // Shouldn't trigger a new call
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+    // Shouldn't trigger a new call
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+    // Shouldn't trigger a new call
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, IdempotentErrorReturnsCommitError)
+{
+    // Verify that repeated session stat calls while status is commit_error
+    // result in one D-Bus call
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+    // session, offset, data
+    EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+    Cb cb;
+    EXPECT_CALL(dbus,
+                UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+        .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+            cb = std::move(icb);
+            return stdplus::Cancel(std::nullopt);
+        });
+    EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+    cb(FirmwareUpdateStatus::Error);
+
+    expected_meta_.size = test_buf.size();
+    expected_meta_.blobState =
+        blobs::StateFlags::open_write | blobs::StateFlags::commit_error;
+    // Shouldn't trigger a new call
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+    // Shouldn't trigger a new call
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+    // Shouldn't trigger a new call
+    EXPECT_TRUE(hvn.stat(session_, &meta_));
+    EXPECT_EQ(meta_, expected_meta_);
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_unittest.cpp b/update/test/hoth_update_unittest.cpp
new file mode 100644
index 0000000..87ef59a
--- /dev/null
+++ b/update/test/hoth_update_unittest.cpp
@@ -0,0 +1,70 @@
+// Copyright 2024 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 "hoth_update_unittest.hpp"
+
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+namespace ipmi_hoth
+{
+
+class HothUpdateBasicTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateBasicTest, CanHandleBlobChecksNameInvalid)
+{
+    // Verify canHandleBlob checks and returns false on an invalid name.
+
+    EXPECT_FALSE(hvn.canHandleBlob("asdf"));
+    EXPECT_FALSE(hvn.canHandleBlob("dev/hoth/firmware_update"));
+    EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/firmware_update2"));
+    EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/hoth0/t/firmware_update"));
+    EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/command_passthru"));
+}
+
+TEST_F(HothUpdateBasicTest, CanHandleBlobChecksNameValid)
+{
+    // Verify canHandleBlob checks and returns true on the valid name.
+
+    EXPECT_TRUE(hvn.canHandleBlob("/dev/hoth/firmware_update"));
+    EXPECT_TRUE(hvn.canHandleBlob("/dev/hoth/hoth0/firmware_update"));
+}
+
+TEST_F(HothUpdateBasicTest, VerifyBehaviorOfBlobIds)
+{
+    // Verify the correct BlobIds is returned from the hoth update handler.
+    internal::Dbus::SubTreeMapping mapping = {
+        {"/xyz/openbmc_project/Control", {}},
+        {"/xyz/openbmc_project/Control/Hoth2nologue", {}},
+        {"/xyz/openbmc_project/Control/Hoth/nologue/2", {}},
+        {"/xyz/openbmc_project/Control/Hoth", {}},
+        {"/xyz/openbmc_project/Control/Hoth/hoth0", {}},
+        {"/xyz/openbmc_project/Control/Hoth/hoth1", {}},
+    };
+    EXPECT_CALL(dbus, getHothdMapping()).WillOnce(Return(mapping));
+    std::vector<std::string> expected = {
+        "/dev/hoth/firmware_update",
+        "/dev/hoth/hoth0/firmware_update",
+        "/dev/hoth/hoth1/firmware_update",
+    };
+    EXPECT_THAT(hvn.getBlobIds(), UnorderedElementsAreArray(expected));
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_unittest.hpp b/update/test/hoth_update_unittest.hpp
new file mode 100644
index 0000000..56bb72b
--- /dev/null
+++ b/update/test/hoth_update_unittest.hpp
@@ -0,0 +1,48 @@
+// Copyright 2024 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.
+
+#pragma once
+
+#include "dbus_update_mock.hpp"
+#include "hoth_update.hpp"
+
+#include <stdplus/util/string.hpp>
+
+#include <cstdint>
+#include <string>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+class HothUpdateTest : public ::testing::Test
+{
+  protected:
+    explicit HothUpdateTest() : hvn(&dbus) {}
+
+    // dbus mock object
+    testing::StrictMock<internal::DbusUpdateMock> dbus;
+
+    HothUpdateBlobHandler hvn;
+
+    const uint16_t session_ = 0;
+    const std::string legacyPath = stdplus::util::strCat(
+        HothUpdateBlobHandler::hothPathPrefix, hvn.pathSuffix());
+    const std::string name = "hoth0";
+    const std::string namedPath = stdplus::util::strCat(
+        HothUpdateBlobHandler::hothPathPrefix, name, "/", hvn.pathSuffix());
+};
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_write_unittest.cpp b/update/test/hoth_update_write_unittest.cpp
new file mode 100644
index 0000000..0b4a3e6
--- /dev/null
+++ b/update/test/hoth_update_write_unittest.cpp
@@ -0,0 +1,139 @@
+// Copyright 2024 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 "hoth_update_unittest.hpp"
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothUpdateWriteTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateWriteTest, InvalidSessionWriteIsRejected)
+{
+    // Verify the hoth update handler checks for a valid session.
+
+    std::vector<uint8_t> data = {0x1, 0x2};
+
+    EXPECT_FALSE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingTooMuchByOneByteFails)
+{
+    // Test the edge case of writing 1 byte too much with an offset of 0.
+    // writing hvn.maxBufferSize + 1 at offset 0 is invalid.
+
+    int bytes = hvn.maxBufferSize() + 1;
+    std::vector<uint8_t> data(0x11);
+    data.resize(bytes);
+    ASSERT_EQ(bytes, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingTooMuchByOffsetOfOne)
+{
+    // Test the edge case of writing hvn.maxBufferSize bytes (which is fine)
+    // but at the offset 1, which makes it go over by 1 byte.
+    // writing hvn.maxBufferSize + offset 1 is invalid.
+
+    int bytes = hvn.maxBufferSize();
+    std::vector<uint8_t> data(0x11);
+    data.resize(bytes);
+    ASSERT_EQ(bytes, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    // offset of 1.
+    EXPECT_FALSE(hvn.write(session_, 1, data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingOneByteBeyondEndFromOffsetFails)
+{
+    // Test the edge case of writing to the last byte offset but trying to
+    // write two bytes.
+    // writing 2 bytes at hvn.maxBufferSize - 1 is invalid.
+
+    std::vector<uint8_t> data = {0x01, 0x02};
+    ASSERT_EQ(2, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.write(session_, (hvn.maxBufferSize() - 1), data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingOneByteAtOffsetBeyondEndFails)
+{
+    // Test the edge case of writing one byte but exactly one byte beyond the
+    // buffer.
+    // writing 1 byte at hvn.maxBufferSize is invalid.
+
+    std::vector<uint8_t> data = {0x01};
+    ASSERT_EQ(1, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_FALSE(hvn.write(session_, hvn.maxBufferSize(), data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingFullBufferAtOffsetZeroSucceeds)
+{
+    // Test the case where you write the full buffer length at once to the 0th
+    // offset.
+
+    int bytes = hvn.maxBufferSize();
+    std::vector<uint8_t> data = {0x01};
+    data.resize(bytes);
+    ASSERT_EQ(bytes, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingOneByteToTheLastOffsetSucceeds)
+{
+    // Test the case where you write the last byte.
+
+    std::vector<uint8_t> data = {0x01};
+    ASSERT_EQ(1, data.size());
+
+    EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+    EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+    EXPECT_TRUE(hvn.write(session_, (hvn.maxBufferSize() - 1), data));
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/meson.build b/update/test/meson.build
new file mode 100644
index 0000000..9cfe4f5
--- /dev/null
+++ b/update/test/meson.build
@@ -0,0 +1,20 @@
+tests = [
+  'hoth_update_unittest',
+  'hoth_update_close_unittest',
+  'hoth_update_commit_unittest',
+  'hoth_update_delete_unittest',
+  'hoth_update_open_unittest',
+  'hoth_update_read_unittest',
+  'hoth_update_stat_unittest',
+  'hoth_update_write_unittest',
+]
+
+foreach t : tests
+  test(
+    t,
+    executable(
+      t.underscorify(),
+      t + '.cpp',
+      implicit_include_directories: false,
+      dependencies: [hothupdate_dep, test_dep]))
+endforeach