TPM code updater

This commit introduces a TPM code updater that currently supports
reading the firmware version for both Infineon and Nuvoton TPM 2.0.
Support for firmware updates will be introduced in a future patch.

The updater's configuration are managed by the EM [1].
[1] https://gerrit.openbmc.org/c/openbmc/entity-manager/+/82416

Tested on Yosemite5 with the following steps:

1. Display the fw inventory:
```
curl --silent $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
  "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
  "Members": [
    {...},
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Yosemite5_TPM_4945"
    },
    {...}
  ],
  "Members@odata.count": 4,
  "Name": "Software Inventory Collection"
}
```

2. Query TPM version:
```
curl --silent $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/Yosemite5_TPM_4945
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Yosemite5_TPM_4945",
  "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
  "Description": "Unknown image",
  "Id": "Yosemite5_TPM_4945",
  "Name": "Software Inventory",
  "Status": {
    "Health": "Warning",
    "HealthRollup": "OK",
    "State": "Disabled"
  },
  "Updateable": false,
  "Version": "15.23"
}
```

Change-Id: I42568242356d55fe005ba1f41ddf8aaf9f682fc8
Signed-off-by: Kevin Tung <kevin.tung.openbmc@gmail.com>
diff --git a/common/include/utils.hpp b/common/include/utils.hpp
index 928b136..65d7022 100644
--- a/common/include/utils.hpp
+++ b/common/include/utils.hpp
@@ -2,11 +2,15 @@
 
 #include <sdbusplus/async.hpp>
 
+#include <functional>
+#include <optional>
+
 /**
  * @brief Asynchronously executes a shell command.
  * @param ctx Async context for monitoring the pipe.
  * @param cmd Shell command to execute.
  * @return Task resolving to true on success (exit code 0), false otherwise.
  */
-sdbusplus::async::task<bool> asyncSystem(sdbusplus::async::context& ctx,
-                                         const std::string& cmd);
+sdbusplus::async::task<bool> asyncSystem(
+    sdbusplus::async::context& ctx, const std::string& cmd,
+    std::optional<std::reference_wrapper<std::string>> result = std::nullopt);
diff --git a/common/src/utils.cpp b/common/src/utils.cpp
index 683ca84..2e6d147 100644
--- a/common/src/utils.cpp
+++ b/common/src/utils.cpp
@@ -4,11 +4,14 @@
 
 PHOSPHOR_LOG2_USING;
 
-sdbusplus::async::task<bool> asyncSystem(sdbusplus::async::context& ctx,
-                                         const std::string& cmd)
+sdbusplus::async::task<bool> asyncSystem(
+    sdbusplus::async::context& ctx, const std::string& cmd,
+    std::optional<std::reference_wrapper<std::string>> result)
 {
-    int pipefd[2];
-    if (pipe(pipefd) == -1)
+    int exitPipefd[2];
+    int resultPipefd[2];
+
+    if (pipe(exitPipefd) == -1 || (result && pipe(resultPipefd) == -1))
     {
         error("Failed to create pipe for command: {CMD}", "CMD", cmd);
         co_return false;
@@ -17,32 +20,59 @@
     pid_t pid = fork();
     if (pid == 0)
     {
-        close(pipefd[0]);
+        close(exitPipefd[0]);
+
+        if (result)
+        {
+            close(resultPipefd[0]);
+            dup2(resultPipefd[1], STDOUT_FILENO);
+            dup2(resultPipefd[1], STDERR_FILENO);
+            close(resultPipefd[1]);
+        }
 
         int exitCode = std::system(cmd.c_str());
-        ssize_t status = write(pipefd[1], &exitCode, sizeof(exitCode));
+        ssize_t status = write(exitPipefd[1], &exitCode, sizeof(exitCode));
 
-        close(pipefd[1]);
+        close(exitPipefd[1]);
         _exit((status == sizeof(exitCode)) ? 0 : 1);
     }
     else if (pid > 0)
     {
-        close(pipefd[1]);
+        close(exitPipefd[1]);
 
-        auto fdio = std::make_unique<sdbusplus::async::fdio>(ctx, pipefd[0]);
+        if (result)
+        {
+            close(resultPipefd[1]);
+        }
+
+        auto fdio =
+            std::make_unique<sdbusplus::async::fdio>(ctx, exitPipefd[0]);
 
         if (!fdio)
         {
             error("Failed to create fdio for command: {CMD}", "CMD", cmd);
-            close(pipefd[0]);
+            close(exitPipefd[0]);
             co_return false;
         }
 
         co_await fdio->next();
 
+        if (result)
+        {
+            auto& resStr = result->get();
+            resStr.clear();
+            char buffer[1024];
+            ssize_t n;
+            while ((n = read(resultPipefd[0], buffer, sizeof(buffer))) > 0)
+            {
+                resStr.append(buffer, n);
+            }
+            close(resultPipefd[0]);
+        }
+
         int exitCode = -1;
-        ssize_t bytesRead = read(pipefd[0], &exitCode, sizeof(exitCode));
-        close(pipefd[0]);
+        ssize_t bytesRead = read(exitPipefd[0], &exitCode, sizeof(exitCode));
+        close(exitPipefd[0]);
 
         if (bytesRead != sizeof(exitCode))
         {
@@ -72,8 +102,13 @@
     else
     {
         error("Fork failed for command: {CMD}", "CMD", cmd);
-        close(pipefd[0]);
-        close(pipefd[1]);
+        close(exitPipefd[0]);
+        close(exitPipefd[1]);
+        if (result)
+        {
+            close(resultPipefd[0]);
+            close(resultPipefd[1]);
+        }
         co_return false;
     }
 }
diff --git a/meson.build b/meson.build
index e8a2f4f..4c25e15 100644
--- a/meson.build
+++ b/meson.build
@@ -80,6 +80,7 @@
         'deps': ['libgpiod'],
     },
     'i2cvr-software-update': {'dirs' : ['common/i2c', 'i2c-vr']},
+    'tpm-software-update': {'dirs': ['tpm']},
 }
 
 optioned_subdirs = []
diff --git a/meson.options b/meson.options
index 6c09029..32c88c2 100644
--- a/meson.options
+++ b/meson.options
@@ -84,6 +84,13 @@
     description: 'Enable EEPROM device update support.',
 )
 
+option(
+    'tpm-software-update',
+    type: 'feature',
+    value: 'enabled',
+    description: 'Enable TPM software update support.',
+)
+
 # Variables
 option(
     'active-bmc-max-allowed',
diff --git a/tpm/README.md b/tpm/README.md
new file mode 100644
index 0000000..5e055ee
--- /dev/null
+++ b/tpm/README.md
@@ -0,0 +1,30 @@
+# TPM Update Daemon
+
+This daemon handles firmware version retrieval and firmware update processes for
+TPM devices. Currently, it supports reading the firmware version of both
+Infineon and Nuvoton TPM 2.0 chips. Firmware update support will be added in a
+future patch.
+
+## Entity Manager Configuration Example
+
+The snippet below demonstrates how to configure a TPM device in Entity Manager.
+
+```json
+{
+  "FirmwareInfo": {
+    "CompatibleHardware": "com.meta.Hardware.Yosemite5.TPM",
+    "VendorIANA": 40981
+  },
+  "Name": "Yosemite5_TPM",
+  "TPMIndex": 0,
+  "Type": "TPM2Firmware"
+}
+```
+
+## Entity Manager Interface
+
+The D-Bus interface name for TPM configuration will be as follows:
+
+```bash
+xyz.openbmc_project.Configuration.TPM2Firmware
+```
diff --git a/tpm/meson.build b/tpm/meson.build
new file mode 100644
index 0000000..04ab0f4
--- /dev/null
+++ b/tpm/meson.build
@@ -0,0 +1,18 @@
+tpm_device_include = include_directories('.')
+
+executable(
+    'phosphor-tpm-software-update',
+    'tpm_software_manager.cpp',
+    'tpm_device.cpp',
+    'tpm2/tpm2.cpp',
+    include_directories: [common_include, tpm_device_include],
+    dependencies: [libpldm_dep, phosphor_logging_dep, sdbusplus_dep],
+    link_with: [software_common_lib, libpldmutil],
+    install: true,
+    install_dir: get_option('libexecdir') / 'phosphor-code-mgmt',
+)
+
+install_data(
+    'xyz.openbmc_project.Software.TPM.service',
+    install_dir: systemd_system_unit_dir,
+)
diff --git a/tpm/tpm2/tpm2.cpp b/tpm/tpm2/tpm2.cpp
new file mode 100644
index 0000000..bd3ca86
--- /dev/null
+++ b/tpm/tpm2/tpm2.cpp
@@ -0,0 +1,142 @@
+#include "tpm2.hpp"
+
+#include "common/include/utils.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+#include <cstdio>
+#include <regex>
+#include <sstream>
+
+PHOSPHOR_LOG2_USING;
+
+static constexpr std::string_view getCapPropertiesCmd =
+    "/usr/bin/tpm2_getcap properties-fixed";
+static constexpr std::string_view fwVer1Property = "TPM2_PT_FIRMWARE_VERSION_1";
+static constexpr std::string_view fwVer2Property = "TPM2_PT_FIRMWARE_VERSION_2";
+static constexpr std::string_view manufacturerProperty = "TPM2_PT_MANUFACTURER";
+
+static constexpr std::string_view hexPattern = R"(^\s*raw:\s+0x([0-9a-fA-F]+))";
+
+enum class Tpm2Vendor
+{
+    IFX,
+    Nuvoton
+};
+
+// Reference: https://trustedcomputinggroup.org/resource/vendor-id-registry/
+static const std::unordered_map<uint32_t, Tpm2Vendor> validManufactureIDs = {
+    {0x49465800, Tpm2Vendor::IFX}, {0x4E544300, Tpm2Vendor::Nuvoton}};
+
+static std::string getTPMResourceManagerPath(uint8_t tpmIndex)
+{
+    return "/dev/tpmrm" + std::to_string(tpmIndex);
+}
+
+sdbusplus::async::task<bool> TPM2Interface::getProperty(
+    std::string_view property, uint32_t& value)
+{
+    // Reference: https://tpm2-tools.readthedocs.io/en/latest/man/common/tcti/
+    // The TCTI or "Transmission Interface" is the communication mechanism
+    // with the TPM. TCTIs can be changed for communication with TPMs across
+    // different mediums.
+    auto tcti = "device:" + getTPMResourceManagerPath(tpmIndex);
+    auto cmd = std::string(getCapPropertiesCmd) + " --tcti " + tcti +
+               " | grep -A1 " + std::string(property);
+
+    std::string output;
+    if (!co_await asyncSystem(ctx, cmd, output))
+    {
+        error("Failed to run command: {CMD}", "CMD", cmd);
+        co_return false;
+    }
+
+    const std::regex regexPattern{std::string(hexPattern)};
+    std::smatch match;
+    std::istringstream stream(output);
+    std::string line;
+
+    while (std::getline(stream, line))
+    {
+        if (std::regex_search(line, match, regexPattern) && match.size() >= 2)
+        {
+            try
+            {
+                value = std::stoul(match[1].str(), nullptr, 16);
+                co_return true;
+            }
+            catch (const std::exception& e)
+            {
+                error("Failed to parse hex value for property {PT}: {ERR}",
+                      "PT", property, "ERR", e.what());
+                co_return false;
+            }
+        }
+    }
+
+    error("No matching hex value found for property: {PT}", "PT", property);
+    co_return false;
+}
+
+sdbusplus::async::task<bool> TPM2Interface::updateFirmware(const uint8_t* image,
+                                                           size_t image_size)
+{
+    (void)image;
+    (void)image_size;
+
+    error("TPM2 firmware update is not supported");
+    co_return false;
+}
+
+sdbusplus::async::task<bool> TPM2Interface::getVersion(std::string& version)
+{
+    uint32_t manufacturerId = 0;
+    uint32_t fwVer = 0;
+    std::string tpmVer1;
+    std::string tpmVer2;
+
+    if (!co_await getProperty(manufacturerProperty, manufacturerId))
+    {
+        error("Failed to retrieve TPM manufacturer ID");
+        co_return false;
+    }
+
+    auto it = validManufactureIDs.find(manufacturerId);
+
+    if (it == validManufactureIDs.end())
+    {
+        error("Invalid TPM manufacturer ID: {ID}", "ID", lg2::hex,
+              manufacturerId);
+        co_return false;
+    }
+
+    auto vendor = it->second;
+
+    if (!co_await getProperty(fwVer1Property, fwVer))
+    {
+        error("Failed to retrieve TPM firmware version 1");
+        co_return false;
+    }
+
+    tpmVer1 = std::to_string(fwVer >> 16) + "." +
+              std::to_string(fwVer & 0xFFFF);
+
+    if (vendor == Tpm2Vendor::Nuvoton)
+    {
+        if (!co_await getProperty(fwVer2Property, fwVer))
+        {
+            error("Failed to retrieve TPM firmware version 2");
+            co_return false;
+        }
+
+        tpmVer2 = std::to_string(fwVer >> 16) + "." +
+                  std::to_string(fwVer & 0xFFFF);
+        version = tpmVer1 + "." + tpmVer2;
+    }
+    else
+    {
+        version = tpmVer1;
+    }
+
+    co_return true;
+}
diff --git a/tpm/tpm2/tpm2.hpp b/tpm/tpm2/tpm2.hpp
new file mode 100644
index 0000000..cf3c097
--- /dev/null
+++ b/tpm/tpm2/tpm2.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "tpm/tpm_device.hpp"
+
+#include <string_view>
+
+class TPM2Interface : public TPMInterface
+{
+  public:
+    TPM2Interface(sdbusplus::async::context& ctx, uint8_t tpmIndex) :
+        TPMInterface(ctx, tpmIndex)
+    {}
+
+    bool isUpdateSupported() const final
+    {
+        // Currently, we do not support TPM2 firmware updates
+        return false;
+    }
+
+    sdbusplus::async::task<bool> updateFirmware(const uint8_t* image,
+                                                size_t image_size) final;
+
+    sdbusplus::async::task<bool> getVersion(std::string& version) final;
+
+  private:
+    sdbusplus::async::task<bool> getProperty(std::string_view property,
+                                             uint32_t& value);
+};
diff --git a/tpm/tpm_device.cpp b/tpm/tpm_device.cpp
new file mode 100644
index 0000000..43f8853
--- /dev/null
+++ b/tpm/tpm_device.cpp
@@ -0,0 +1,66 @@
+#include "tpm_device.hpp"
+
+#include "tpm2/tpm2.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+
+PHOSPHOR_LOG2_USING;
+
+TPMDevice::TPMDevice(sdbusplus::async::context& ctx, enum TPMType tpmType,
+                     uint8_t tpmIndex, SoftwareConfig& config,
+                     ManagerInf::SoftwareManager* parent) :
+    Device(ctx, config, parent, {RequestedApplyTimes::OnReset})
+{
+    switch (tpmType)
+    {
+        case TPMType::TPM2:
+            tpmInterface = std::make_unique<TPM2Interface>(ctx, tpmIndex);
+            break;
+        default:
+            tpmInterface = nullptr;
+            error("Unsupported TPM type: {TYPE}", "TYPE",
+                  static_cast<int>(tpmType));
+            break;
+    }
+}
+
+sdbusplus::async::task<bool> TPMDevice::updateDevice(const uint8_t* image,
+                                                     size_t imageSize)
+{
+    if (tpmInterface == nullptr)
+    {
+        error("TPM interface is not initialized");
+        co_return false;
+    }
+
+    setUpdateProgress(10);
+
+    if (!co_await tpmInterface->updateFirmware(image, imageSize))
+    {
+        error("Failed to update TPM firmware");
+        co_return false;
+    }
+
+    setUpdateProgress(100);
+    debug("Successfully updated TPM");
+    co_return true;
+}
+
+sdbusplus::async::task<std::string> TPMDevice::getVersion()
+{
+    std::string version = "Unknown";
+
+    if (tpmInterface == nullptr)
+    {
+        error("TPM interface is not initialized");
+        co_return version;
+    }
+
+    if (!co_await tpmInterface->getVersion(version))
+    {
+        error("Failed to get TPM version");
+    }
+
+    co_return version;
+}
diff --git a/tpm/tpm_device.hpp b/tpm/tpm_device.hpp
new file mode 100644
index 0000000..a6e220e
--- /dev/null
+++ b/tpm/tpm_device.hpp
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "common/include/device.hpp"
+#include "common/include/software_manager.hpp"
+
+#include <sdbusplus/async.hpp>
+
+#include <unordered_map>
+
+namespace SoftwareInf = phosphor::software;
+namespace ManagerInf = SoftwareInf::manager;
+
+enum class TPMType
+{
+    TPM2,
+};
+
+class TPMInterface
+{
+  public:
+    TPMInterface(sdbusplus::async::context& ctx, uint8_t tpmIndex) :
+        ctx(ctx), tpmIndex(tpmIndex)
+    {}
+    virtual ~TPMInterface() = default;
+    TPMInterface(const TPMInterface&) = delete;
+    TPMInterface& operator=(const TPMInterface&) = delete;
+    TPMInterface(TPMInterface&&) = delete;
+    TPMInterface& operator=(TPMInterface&&) = delete;
+
+    virtual bool isUpdateSupported() const = 0;
+    virtual sdbusplus::async::task<bool> getVersion(std::string& version) = 0;
+    virtual sdbusplus::async::task<bool> updateFirmware(const uint8_t* image,
+                                                        size_t imageSize) = 0;
+
+  protected:
+    sdbusplus::async::context& ctx;
+    uint8_t tpmIndex;
+};
+
+class TPMDevice : public Device
+{
+  public:
+    using Device::softwareCurrent;
+
+    TPMDevice(sdbusplus::async::context& ctx, enum TPMType tpmType,
+              uint8_t tpmIndex, SoftwareConfig& config,
+              ManagerInf::SoftwareManager* parent);
+
+    sdbusplus::async::task<bool> updateDevice(const uint8_t* image,
+                                              size_t image_size) final;
+
+    sdbusplus::async::task<std::string> getVersion();
+
+    bool isUpdateSupported() const
+    {
+        return tpmInterface ? tpmInterface->isUpdateSupported() : false;
+    }
+
+  private:
+    std::unique_ptr<TPMInterface> tpmInterface;
+};
+
+inline bool stringToTPMType(const std::string& type, TPMType& tpmType)
+{
+    static const std::unordered_map<std::string, TPMType> tpmTypeMap = {
+        {"TPM2Firmware", TPMType::TPM2},
+    };
+
+    auto it = tpmTypeMap.find(type);
+    if (it != tpmTypeMap.end())
+    {
+        tpmType = it->second;
+        return true;
+    }
+    return false;
+}
diff --git a/tpm/tpm_software_manager.cpp b/tpm/tpm_software_manager.cpp
new file mode 100644
index 0000000..227cd84
--- /dev/null
+++ b/tpm/tpm_software_manager.cpp
@@ -0,0 +1,87 @@
+#include "tpm_software_manager.hpp"
+
+#include "common/include/dbus_helper.hpp"
+#include "tpm_device.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+PHOSPHOR_LOG2_USING;
+
+namespace SoftwareInf = phosphor::software;
+
+void TPMSoftwareManager::start()
+{
+    std::vector<std::string> configIntfs = {
+        "xyz.openbmc_project.Configuration.TPM2Firmware",
+    };
+
+    ctx.spawn(initDevices(configIntfs));
+    ctx.run();
+}
+
+sdbusplus::async::task<bool> TPMSoftwareManager::initDevice(
+    const std::string& service, const std::string& path, SoftwareConfig& config)
+{
+    const std::string configIface =
+        "xyz.openbmc_project.Configuration." + config.configType;
+
+    std::optional<uint8_t> tpmIndex = co_await dbusGetRequiredProperty<uint8_t>(
+        ctx, service, path, configIface, "TPMIndex");
+
+    if (!tpmIndex.has_value())
+    {
+        error("Missing property: TPMIndex");
+        co_return false;
+    }
+
+    std::optional<std::string> type =
+        co_await dbusGetRequiredProperty<std::string>(ctx, service, path,
+                                                      configIface, "Type");
+    if (!type.has_value())
+    {
+        error("Missing property: Type");
+        co_return false;
+    }
+
+    TPMType tpmType;
+    if (!stringToTPMType(type.value(), tpmType))
+    {
+        error("Invalid TPM type: {TYPE}", "TYPE", type.value());
+        co_return false;
+    }
+
+    debug("TPM device: TPM Index={INDEX}, Type={TYPE}", "INDEX",
+          tpmIndex.value(), "TYPE", type.value());
+
+    auto tpmDevice = std::make_unique<TPMDevice>(ctx, tpmType, tpmIndex.value(),
+                                                 config, this);
+
+    std::unique_ptr<SoftwareInf::Software> software =
+        std::make_unique<SoftwareInf::Software>(ctx, *tpmDevice);
+
+    software->setVersion(co_await tpmDevice->getVersion());
+
+    if (tpmDevice->isUpdateSupported())
+    {
+        std::set<RequestedApplyTimes> allowedApplyTimes = {
+            RequestedApplyTimes::OnReset};
+
+        software->enableUpdate(allowedApplyTimes);
+    }
+
+    tpmDevice->softwareCurrent = std::move(software);
+
+    devices.insert({config.objectPath, std::move(tpmDevice)});
+
+    co_return true;
+}
+
+int main()
+{
+    sdbusplus::async::context ctx;
+
+    TPMSoftwareManager tpmSoftwareManager(ctx);
+
+    tpmSoftwareManager.start();
+    return 0;
+}
diff --git a/tpm/tpm_software_manager.hpp b/tpm/tpm_software_manager.hpp
new file mode 100644
index 0000000..8154e35
--- /dev/null
+++ b/tpm/tpm_software_manager.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "common/include/software_manager.hpp"
+
+namespace ManagerInf = phosphor::software::manager;
+
+const std::string configTypeTPM = "TPM";
+
+class TPMSoftwareManager : public ManagerInf::SoftwareManager
+{
+  public:
+    TPMSoftwareManager(sdbusplus::async::context& ctx) :
+        SoftwareManager(ctx, configTypeTPM)
+    {}
+
+    void start();
+
+    sdbusplus::async::task<bool> initDevice(const std::string& service,
+                                            const std::string& path,
+                                            SoftwareConfig& config) final;
+};
diff --git a/tpm/xyz.openbmc_project.Software.TPM.service b/tpm/xyz.openbmc_project.Software.TPM.service
new file mode 100644
index 0000000..9598b91
--- /dev/null
+++ b/tpm/xyz.openbmc_project.Software.TPM.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=TPM Device Code Update Daemon
+Requires=xyz.openbmc_project.EntityManager.service
+After=xyz.openbmc_project.EntityManager.service
+
+[Service]
+ExecStart=/usr/libexec/phosphor-code-mgmt/phosphor-tpm-software-update
+Restart=always
+Type=dbus
+BusName=xyz.openbmc_project.Software.TPM
+
+[Install]
+WantedBy=multi-user.target