hiero-ledger / hiero-ledger/hiero-sdk-cpp

[Intermediate]: Add HSM/offline signing support for multi-node transactions

Open
#976 6 comments 0 reactions 0 assignees View on GitHub
priority: low scope: api skill: intermediate status: ready for dev
Dominant language
C++
Stars
42
Forks
108
Avg merge
11h 45m
Merged PRs (30d)
2

Description

## 🧩 Intermediate Friendly

This issue is a good fit for contributors who are already familiar with the Hiero C++ SDK and feel comfortable navigating the codebase.

Intermediate Issues often involve:
- Exploring existing implementations
- Understanding how different components work together
- Making thoughtful changes that follow established patterns

The goal is to support deeper problem-solving while keeping the task clear, focused, and enjoyable to work on.

---

## 🐞 Problem Description

There is currently no native support in the Hiero C++ SDK for signing transactions using only the raw `bodyBytes`. This presents a challenge when integrating with **Hardware Security Modules (HSMs)**, which securely hold private keys and only allow signing of raw byte messages.

C++ is often used in lower-level systems, and HSM signing is particularly useful for embedded devices or other hardware-layer agents that require secure key management.

The required workflow for HSM signing is:
1. Create and freeze a transaction
2. Extract the canonical `bodyBytes` that require signing (per node)
3. Send the serialized bytes to an HSM for signing
4. Inject the signature back into the transaction
5. Submit the signed transaction to the Hiero network

Currently, signing the entire transaction (`transaction.toBytes()`) does not produce a network-valid signature because only the `bodyBytes` should be signed.

### Reference Issue

This request originates from: https://github.com/hiero-ledger/hiero-sdk-js/issues/3037

---

## 💡 Expected Outcome

Implement HSM-based transaction signing support **following the design document**:

**Design Document**: https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/proposals/manual-signature-hsm-design-proposal.md

> [!IMPORTANT]
> The implementation MUST follow the API design and test plan defined in the design document above. This ensures consistency across all Hiero SDK implementations.

### New API

#### 1. `SignableNodeTransactionBodyBytes` Class

A new class that represents a transaction body ready for external signing, associated with a specific node account ID and transaction ID:

```cpp
class SignableNodeTransactionBodyBytes {
public:
AccountId nodeAccountId;
TransactionId transactionId;
std::vector signableTransactionBodyBytes;
};
```

#### 2. `Transaction::getSignableNodeBodyBytesList()` Method

A new getter that returns a list of `SignableNodeTransactionBodyBytes` containing the canonical `bodyBytes` paired with their respective `nodeAccountId` and `transactionId` for signing:

```cpp
std::vector getSignableNodeBodyBytesList() const;
```

### Test Plan (from Design Document)

1. **Given** a transaction with one node, **when** its `bodyBytes` is signed externally and injected, **then** the transaction executes successfully.

2. **Given** a transaction with multiple nodes, **when** all node-specific signatures are correctly applied, **then** the transaction executes successfully with retries allowed.

3. **Given** a chunked transaction with one node, **when** all its chunk-specific `bodyBytes` are signed externally and injected, **then** the transaction executes successfully.

4. **Given** a chunked transaction with multiple nodes, **when** all chunk-specific and node-specific signatures are correctly applied, **then** the transaction executes successfully with retries allowed.

5. **Given** a transaction with an invalid or mismatched signature applied, **then** the transaction fails with `INVALID_SIGNATURE`.

6. **Given** a call to `getSignableNodeBodyBytesList()` on a frozen transaction, **then** the returned list size equals the number of node IDs.

7. **Given** a call to `getSignableNodeBodyBytesList()` before freezing, **then** an error is thrown.

---

## 🧠 Implementation Notes

### Reference Implementation

The JavaScript SDK has already implemented this feature. Use it as a reference:

| File | Purpose |
|------|---------|
| `hiero-sdk-js/src/transaction/SignableNodeTransactionBodyBytes.js` | The new class |
| `hiero-sdk-js/src/transaction/Transaction.js` (line ~1373) | The `signableNodeBodyBytesList` getter |
| `hiero-sdk-js/test/integration/TransactionIntegrationTest.js` (line ~1080) | HSM signing integration tests |
| `hiero-sdk-js/test/unit/Transaction.js` (line ~898) | Unit tests for getter |

### C++ SDK Files to Modify

| File | Changes |
|------|---------|
| `src/sdk/main/include/SignableNodeTransactionBodyBytes.h` | **New file** - Create the class |
| `src/sdk/main/include/Transaction.h` | Add `getSignableNodeBodyBytesList()` declaration |
| `src/sdk/main/src/Transaction.cc` | Implement `getSignableNodeBodyBytesList()` |
| `src/sdk/main/include/ChunkedTransaction.h` | Override for chunked transactions |
| `src/sdk/main/src/ChunkedTransaction.cc` | Implement override |

### Existing Code Patterns

The C++ SDK already has similar patterns to follow:

```cpp
// Existing addSignature (Transaction.h:154)
virtual SdkRequestType& addSignature(const std::shared_ptr& publicKey,
const std::vector& signature);

// Existing bodyBytes access (PrivateKey.cc:81)
const std::vector signature = sign(
internal::Utilities::stringToByteVector(transactionToSign.bodybytes()));
```

### Usage Example (C++ adaptation from design doc)

```cpp
// Create and freeze transaction
auto tx = TransferTransaction()
.addHbarTransfer(senderId, Hbar::fromTinybars(-100))
.addHbarTransfer(receiverId, Hbar::fromTinybars(100))
.setTransactionId(TransactionId::generate(senderId))
.freezeWith(&client);

// Get signable body bytes for each node
auto signableList = tx.getSignableNodeBodyBytesList();

// Sign externally with HSM (placeholder - not part of SDK)
for (const auto& signable : signableList) {
auto signature = hsmSign(signable.signableTransactionBodyBytes);

// Add signature back to transaction
tx.addSignature(senderPublicKey, signature);
}

// Execute
auto response = tx.execute(client);
```

---

## ✅ Acceptance Criteria

To help get this change merged smoothly:

- [ ] **Follow design document**: API matches the design specification
- [ ] **New class created**: `SignableNodeTransactionBodyBytes` with required fields
- [ ] **Getter implemented**: `getSignableNodeBodyBytesList()` on `Transaction`
- [ ] **Chunked support**: Works correctly for chunked transactions (e.g., `FileAppendTransaction`)
- [ ] **All 7 test cases pass**: Unit and integration tests per the test plan
- [ ] **Example added**: Demonstrates HSM signing workflow
- [ ] **Follows patterns**: Matches existing C++ SDK conventions
- [ ] **Pass all CI checks**
- [ ] **Review:** All code review feedback addressed

---

## 📋 Contribution Guide

To help your contribution go as smoothly as possible, we recommend following these steps:

- [ ] Comment `/assign` to request the issue
- [ ] Wait for assignment
- [ ] Fork the repository and create a branch
- [ ] Set up the project using the instructions in `README.md`
- [ ] Make the requested changes
- [ ] Sign each commit using `-s -S`
- [ ] Push your branch and open a pull request

Read [Workflow Guide](docs/training/workflow.md) for step-by-step workflow guidance.
Read [README.md](README.md) for setup instructions.

❗ Pull requests **cannot be merged** without `S` and `s` signed commits.
See the [Signing Guide](docs/training/signing.md).

---

## 📚 Additional Context or Resources

### References

| Document | Purpose |
|----------|---------|
| **[SDK Design Document](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/proposals/manual-signature-hsm-design-proposal.md)** | **Authoritative source** - defines API and test plan |
| [JS SDK Implementation PR](https://github.com/hiero-ledger/hiero-sdk-js/pull/3119) | Reference implementation |
| [Original Issue](https://github.com/hiero-ledger/hiero-sdk-js/issues/3037) | Background context |

### Why HSM Signing Matters

Hardware Security Modules provide:
- **Key isolation**: Private keys never leave secure hardware
- **Compliance**: Required for many enterprise security standards
- **Auditability**: Hardware-enforced signing policies

If you have questions while working on this issue, feel free to ask!

You can reach the community and maintainers here:
[Hiero-SDK-C++ Discord](https://discord.com/channels/905194001349627914/1337424839761465364)

Whether you need help finding the right file, understanding existing code, or confirming your approach — we're happy to help.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.