> ## Documentation Index
> Fetch the complete documentation index at: https://docs.leapfinancial.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Integrity

> Request signing with HMAC-SHA256 over the X-Signature header

## 1. Purpose & Scope

Platform issues server-to-server requests, over HTTPS, to endpoints operated by the Partner. Because these requests may create accounts, move funds, or change the state of financial operations, the receiving endpoint must be able to prove two properties for every request before acting on it:

* **Authenticity** — the request was generated by Platform and not by a third party.
* **Integrity** — the body is exactly what Platform sent, with no alteration by any intermediary along the way.

This section defines a keyed message-authentication scheme (**HMAC-SHA256**) that provides both properties. It applies to **every** request Platform sends to the interfaces described in this documentation: the signature travels in the `X-Signature` header, accompanied by an `X-Timestamp` header that forms part of the signed message (see 3.2).

<Info>
  **Scope.** This section covers message-level authentication only, and it does not replace the protection applied to the channel itself — see [Communication Security](/vna/client-interfaces/communication-security). The two complement each other as part of a defense-in-depth posture: TLS protects the data in transit, the signature proves who produced the payload and that it arrived unaltered.
</Info>

## 2. Security Rationale & Best Practices

The scheme is not proprietary; it is a direct application of well-established, standards-based cryptographic guidance. This section documents the reasoning so that the Partner's security teams can validate the approach against their own compliance frameworks.

### 2.1 Why a keyed MAC (HMAC) in addition to TLS

TLS protects data *in transit* between two network peers, but it does not, by itself, prove to the application *which party produced a given payload* — especially in environments with TLS-terminating proxies, load balancers, WAFs, or API gateways where the original peer identity is lost before the request reaches the application. A keyed MAC binds the request body to a secret shared only by Platform and the Partner, producing an application-level proof of authenticity and integrity that survives intermediaries.

<Note>
  **This is not an argument against transport-level controls.** TLS is mandatory on every connection, and a Partner may additionally request one of the network controls described in [Communication Security](/vna/client-interfaces/communication-security). Those controls operate below the application and remain necessary. The signature addresses what no transport-level control can: it travels with the payload, so the proof of authenticity and integrity is still verifiable at the application, after any intermediary has terminated the TLS connection.
</Note>

### 2.2 Why HMAC-SHA256 specifically

* **Standardized construction.** HMAC is defined in RFC 2104 and standardized as FIPS PUB 198-1 (*The Keyed-Hash Message Authentication Code*). Its security is formally analyzed and does not depend on the secrecy of the algorithm — only on the secrecy of the key (Kerckhoffs's principle).
* **Approved hash.** SHA-256 is a NIST-approved hash function (FIPS 180-4) and is recommended for MAC use by NIST SP 800-107 Rev. 1. It offers a 256-bit security margin with no known practical attacks against HMAC-SHA256.
* **Symmetric & low-overhead.** Verification is a single hash computation — fast enough to run on every request without meaningful latency, unlike asymmetric signatures.
* **Ubiquitous.** Native, audited implementations exist in every mainstream platform (.NET `HMACSHA256`, Java `Mac`, Node `crypto`, Python `hmac`), removing the need for custom cryptographic code.

### 2.3 Alignment with industry guidance

| Reference | Relevance |
| - | - |
| OWASP API Security Top 10 (2023) | Addresses Broken Authentication for machine-to-machine traffic by requiring cryptographic verification of each request. |
| OWASP Cheat Sheets (Webhook / TLS) | Recommend signing request payloads with a shared secret and comparing signatures in constant time. |
| RFC 2104 / FIPS 198-1 | Define the HMAC construction used here. |
| NIST SP 800-107 Rev. 1 / FIPS 180-4 | Approve SHA-256 for keyed-hash message authentication. |
| NIST SP 800-57 | Guides secret length, storage, and rotation (see section 6). |

<Note>
  **Conclusion.** HMAC-SHA256 request signing is a recognized best-practice pattern for authenticating server-to-server requests. Implementing it as specified here brings the Partner integration in line with OWASP and NIST guidance.
</Note>

## 3. Signature Scheme Specification

### 3.1 Algorithm parameters

| Parameter | Value |
| - | - |
| MAC algorithm | HMAC-SHA256 |
| Secret key | Shared secret specific to each Partner (the `SecretKey`), generated and provisioned out-of-band by Platform. Interpreted as its raw ASCII byte representation when used as the HMAC key. |
| Signed message | The exact raw bytes of the `X-Timestamp` header value, followed by a single `.` byte, followed by the exact raw bytes of the HTTP request body as received on the wire (UTF-8 encoded JSON). See 3.2. |
| Timestamp header | `X-Timestamp` — UTC timestamp in ISO-8601 format (e.g. `2026-01-15T18:42:07.123Z`), generated at send time. Its exact string value is part of the signed message. |
| Output encoding | Base64 (standard alphabet, with padding). |
| Transport headers | `X-Signature`, `X-Timestamp` |
| Applies to | Every `POST` request Platform sends to the Partner interfaces, with `Content-Type: application/json`. |

### 3.2 Canonicalization rule (critical)

The signed message is built by concatenating three byte sequences, in order: the `X-Timestamp` header value, a literal `.` separator, and the request body — each as a **byte sequence**, not a logical object:

```text theme={null}
signedPayload = bytes(X-Timestamp) + bytes(".") + rawBodyBytes
```

The Partner must verify the signature against the **raw body bytes exactly as received**, before any parsing, deserialization, re-serialization, or pretty-printing, combined with the exact `X-Timestamp` header string as received.

<Warning>
  **Do not re-serialize before verifying.** Re-serializing the JSON (e.g. deserializing to an object and serializing it again) can change byte-level details — key ordering, whitespace, number formatting, Unicode escaping — and will cause a correct signature to fail verification. Always capture and hash the **raw request body buffer**, and use the `X-Timestamp` header value exactly as received (no reformatting).
</Warning>

### 3.3 Required idempotency field

Every request body includes a mandatory `TransactionId` field: an idempotency key that Platform derives deterministically from the underlying operation. Retrying a call always reproduces the same `TransactionId`, which is what allows the Partner to detect a delivery it has already executed and safely ignore it instead of executing the operation twice.

`GetCreditStatus` is the exception: each poll carries its own distinct `TransactionId`, since every poll is a new query rather than a redelivery of a previous one.

### 3.4 HTTP header

```http theme={null}
POST /GetCreateAccountUrl HTTP/1.1
Host: partner.example.com
Content-Type: application/json
X-Timestamp: 2026-01-15T18:42:07.123Z
X-Signature: Zk3mQpX9vLc7RtN2wJh8bD1sF6aG0uYeK4rTq5Xz...

{"transactionId":"5c9e6c3a-8b1a-5e2f-9a1b-0d4e6f2c7a10","subscriberId":"SUB-000123", ... }
```

## 4. Verification Procedure (Mandatory)

Every inbound request must be verified with the following steps **before any business logic executes**:

<Steps>
  <Step title="Read the raw body">
    Capture the request body as a byte buffer without deserializing it. Keep this exact buffer for both verification and later parsing.
  </Step>

  <Step title="Read the headers">
    Extract `X-Timestamp` and `X-Signature`. If either is absent or empty, reject the request with `401 Unauthorized`.
  </Step>

  <Step title="Build the signed payload">
    Concatenate the raw `X-Timestamp` header bytes, a literal `.` byte, and the raw body bytes (see 3.2).
  </Step>

  <Step title="Recompute">
    Compute the HMAC-SHA256 signature over the signed payload, encoded in Base64, using the shared secret assigned to this Partner.
  </Step>

  <Step title="Compare in constant time">
    Compare the recomputed value with the received `X-Signature` header using a fixed-time comparison (never `==` / ordinary string equality).
  </Step>

  <Step title="Decide">
    If they match, continue processing. If they do not, reject with `401 Unauthorized` and log the event (without logging the secret).
  </Step>
</Steps>

<Warning>
  **Fail closed.** If the secret is not configured, the body cannot be read, or any step throws, the request **must** be rejected. Never default to "accept" on error.
</Warning>

## 5. Reference Implementation (C# / .NET)

The following ASP.NET Core middleware verifies the signature over the raw body and rejects invalid requests. It buffers the body so that downstream model binding still works.

```csharp theme={null}
using System.Security.Cryptography;
using System.Text;

public sealed class SignatureVerificationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _secret; // provisioned per-Partner, from secure config

    public SignatureVerificationMiddleware(RequestDelegate next, string secret)
    {
        _next = next;
        _secret = secret;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 1. Read the RAW body without consuming it for downstream handlers.
        context.Request.EnableBuffering();

        byte[] rawBody;
        using (var ms = new MemoryStream())
        {
            await context.Request.Body.CopyToAsync(ms);
            rawBody = ms.ToArray();
            context.Request.Body.Position = 0; // rewind for model binding
        }

        // 2. Read the headers.
        var timestamp = context.Request.Headers["X-Timestamp"].ToString();
        var provided = context.Request.Headers["X-Signature"].ToString();
        if (string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(provided))
        {
            await Reject(context, "Missing X-Timestamp or X-Signature header");
            return;
        }

        // 3. Build the signed payload: timestamp + "." + raw body.
        byte[] signedPayload = Encoding.UTF8.GetBytes(timestamp)
            .Concat(Encoding.UTF8.GetBytes("."))
            .Concat(rawBody)
            .ToArray();

        // 4. Recompute HMAC-SHA256 over the signed payload.
        using var hmac = new HMACSHA256(Encoding.ASCII.GetBytes(_secret));
        byte[] computedHash = hmac.ComputeHash(signedPayload);
        string computed = Convert.ToBase64String(computedHash);

        // 5. Constant-time comparison (avoids timing side channels).
        bool valid = CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(computed),
            Encoding.UTF8.GetBytes(provided));

        // 6. Decide.
        if (!valid)
        {
            await Reject(context, "Invalid signature");
            return;
        }

        await _next(context);
    }

    private static async Task Reject(HttpContext ctx, string reason)
    {
        ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
        await ctx.Response.WriteAsync($"{{\"error\":\"{reason}\"}}");
    }
}

// Registration (Program.cs):
// app.UseMiddleware<SignatureVerificationMiddleware>(builder.Configuration["Platform:SharedSecret"]);
```

<Note>
  `CryptographicOperations.FixedTimeEquals` (available in .NET Core 2.1+) performs the required constant-time comparison. Do not substitute `string.Equals` or `==`.
</Note>

## 6. Operational & Security Requirements

The shared secret is generated by Platform and delivered to the Partner through a secure channel. Each Partner receives a distinct secret so that a compromise is isolated to a single integration. On the Platform side it is held in a secrets manager and is never stored in source code or in repositories; the Partner is expected to protect its copy to the same standard.

### 6.1 Response semantics

| Condition | HTTP status |
| - | - |
| Signature valid | `200 OK` (or the endpoint's normal success response) |
| Header missing / empty | `401 Unauthorized` |
| Signature mismatch | `401 Unauthorized` |
| Secret not configured / internal error | `401` or `500` — never process the request |

## 7. Appendix A — Verifiable Test Vector

Use the following vector to confirm your implementation before go-live. Your computed signature **must equal the expected value exactly**.

| Field | Value |
| - | - |
| Secret (ASCII) | `wh_secret_9f2a7c1e4b8d0a6f3e5c2b1d8a4f7e0c` |
| `X-Timestamp` | `2026-01-15T18:42:07.123Z` |
| Raw body (146 bytes) | `{"transactionId":"5c9e6c3a-8b1a-5e2f-9a1b-0d4e6f2c7a10","subscriberId":"SUB-000123","amount":150.75,"currency":"PEN","reference":"OP-2026-778812"}` |
| Signed payload (171 bytes) | `X-Timestamp` + `"."` + raw body, i.e. `2026-01-15T18:42:07.123Z.{"transactionId":...}` |
| Expected `X-Signature` | `nQdLCID8zS6tVNG6+Exu5nPiYA8IEAakeo8qVbocKOk=` |

Reproduce from a shell:

```bash theme={null}
TS='2026-01-15T18:42:07.123Z'
BODY='{"transactionId":"5c9e6c3a-8b1a-5e2f-9a1b-0d4e6f2c7a10","subscriberId":"SUB-000123","amount":150.75,"currency":"PEN","reference":"OP-2026-778812"}'
printf '%s.%s' "$TS" "$BODY" \
  | openssl dgst -sha256 -hmac 'wh_secret_9f2a7c1e4b8d0a6f3e5c2b1d8a4f7e0c' -binary \
  | base64
# => nQdLCID8zS6tVNG6+Exu5nPiYA8IEAakeo8qVbocKOk=
```

## 8. Appendix B — Frequently Asked Questions

<AccordionGroup>
  <Accordion title="My signature almost works but fails intermittently. Why?">
    Almost always a canonicalization issue (see 3.2). Confirm you are hashing the raw body buffer and not a re-serialized object. Watch for a framework that trims whitespace, reorders keys, or rewrites numbers/dates.
  </Accordion>

  <Accordion title="Should the secret be treated as text or bytes?">
    As the HMAC key, the secret is used as its **ASCII byte representation**. Provision secrets using ASCII-safe characters to avoid any ambiguity.
  </Accordion>

  <Accordion title="Is Base64 URL-safe or standard?">
    Standard Base64 (with `+`, `/` and `=` padding), as produced by `Convert.ToBase64String`.
  </Accordion>

  <Accordion title="Does signing replace authorization?">
    No. These are two separate checks. Signing resolves **authenticity and integrity**: it proves to the Partner that the request genuinely came from Platform and that the body was not altered in transit. Once that is confirmed, a separate question remains — whether that specific operation is permitted (amounts, limits, business rules, etc.). That business authorization is not covered by the signature and remains the Partner's responsibility to implement in its own logic.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.