# API Doc - Optional Response Signing

AllScale Open API\
Version: v1\
Last updated: 2026-01-21

***

## Overview

**Applies only when `signed_response = true` for the store**

***

AllScale supports optional response signing to allow clients to verify:

* Response authenticity
* Payload integrity
* Request–response binding
* Protection against replayed or substituted responses

Response signing is **disabled by default**.

It becomes active only when the store is created or configured with:

```
signed_response = true
```

***

### When Response Signing Is Enabled

When enabled, every API response includes additional headers that allow the client to verify the response cryptographically.

***

### Response Headers

| Header               | Required    | Description                                  |
| -------------------- | ----------- | -------------------------------------------- |
| X-Response-Timestamp | Yes         | Unix timestamp (seconds) generated by server |
| X-Response-Nonce     | Yes         | Unique nonce generated per response          |
| X-Response-Signature | Yes         | HMAC signature of the response               |
| X-Request-Nonce      | Recommended | Echo of request nonce                        |
| X-Request-Id         | Yes         | Unique request identifier                    |

***

### Response Canonical String

When `signed_response = true`, the response signature is computed using:

```
STATUS_CODE
PATH
REQUEST_NONCE
REQUEST_BODY_SHA256
RESPONSE_TIMESTAMP
RESPONSE_NONCE
RESPONSE_BODY_SHA256
```

Each field is joined using newline characters (`\n`).

***

### Field Definitions

| Field                  | Description                         |
| ---------------------- | ----------------------------------- |
| STATUS\_CODE           | HTTP status code (e.g. 200)         |
| PATH                   | Request path (e.g. /v1/payments)    |
| REQUEST\_NONCE         | X-Nonce from the request            |
| REQUEST\_BODY\_SHA256  | SHA256 hash of raw request body     |
| RESPONSE\_TIMESTAMP    | Server timestamp                    |
| RESPONSE\_NONCE        | Random nonce generated per response |
| RESPONSE\_BODY\_SHA256 | SHA256 hash of raw response body    |

> ⚠️ Important: Hashes must be calculated using the exact raw request / response body bytes.

***

### Response Signature Algorithm

**Algorithm**

```
HMAC-SHA256
```

**Encoding**

```
Base64
```

**Formula**

```
signature = Base64(
    HMAC_SHA256(api_secret, canonical_response_string)
)
```

**Header format**

```
X-Response-Signature: v1=<signature>
```

***

### Response Example

#### Response Headers

```
HTTP/1.1 200 OK
X-Response-Timestamp: 1716501552
X-Response-Nonce: 8fae4c9d7e2b4b3aa1f2
X-Response-Signature: v1=dYlX5KQyZPzq2+7Gf+4...
X-Request-Nonce: b4d9a2a1-9c2b-4df4-8b8e-2a13a45fd321
X-Request-Id: req_84f12a8d
```

#### Response Body

```json
{
  "code": 0,
  "payload": {
    "pong": "ok"
  },
  "error": null,
  "request_id": "req_84f12a8d"
}
```

***

### Client Verification Steps

1. Extract headers:

* X-Response-Timestamp
* X-Response-Nonce
* X-Response-Signature
* X-Request-Nonce

2. Compute:

* REQUEST\_BODY\_SHA256
* RESPONSE\_BODY\_SHA256

3. Rebuild canonical string:

```
STATUS_CODE
PATH
REQUEST_NONCE
REQUEST_BODY_SHA256
RESPONSE_TIMESTAMP
RESPONSE_NONCE
RESPONSE_BODY_SHA256
```

4. Compute:

```ini
expected_signature = Base64(HMAC_SHA256(api_secret, canonical))
```

5. Compare:

```ini
expected_signature = == X-Response-Signature
```

6. Validate:

* Timestamp within ±5 minutes
* Response nonce not reused

***

### Postman Response Verification Script

Place this in **Postman → Tests tab**

```javascript
// ================================
// AllScale Response Signature Verify
// ================================

const API_SECRET = (pm.environment.get("API_SECRET") || "").trim();

function sha256Hex(str) {
    return CryptoJS.SHA256(str || "").toString(CryptoJS.enc.Hex);
}

function hmacSha256Base64(key, msg) {
    const mac = CryptoJS.HmacSHA256(msg, key);
    return CryptoJS.enc.Base64.stringify(mac);
}

function getHeader(name) {
    return pm.response.headers.get(name) || "";
}

pm.test("API_SECRET is set", function () {
    pm.expect(API_SECRET).to.not.eql("");
});

// ---- request-side values ----
const path = pm.request.url.getPath();
const requestNonce = pm.request.headers.get("X-Nonce") || "";
const requestBodyRaw = pm.request.body?.raw || "";
const requestBodySha = sha256Hex(requestBodyRaw);

// ---- response-side values ----
const statusCode = pm.response.code;
const responseTimestamp = getHeader("X-Response-Timestamp");
const responseNonce = getHeader("X-Response-Nonce");
const responseSigHeader = getHeader("X-Response-Signature");
const echoedRequestNonce = getHeader("X-Request-Nonce");

// ---- sanity checks ----
pm.test("Response signing headers present", function () {
    pm.expect(responseTimestamp).to.not.eql("");
    pm.expect(responseNonce).to.not.eql("");
    pm.expect(responseSigHeader).to.match(/^v1=.+/);
});

// ---- timestamp check ----
pm.test("Response timestamp within ±5 minutes", function () {
    const now = Math.floor(Date.now() / 1000);
    const ts = parseInt(responseTimestamp, 10);
    pm.expect(Math.abs(now - ts)).to.be.below(300);
});

// ---- response body hash ----
const responseBody = pm.response.text() || "";
const responseBodySha = sha256Hex(responseBody);

// ---- canonical string ----
const canonical = [
    String(statusCode),
    path,
    echoedRequestNonce || requestNonce,
    requestBodySha,
    String(responseTimestamp),
    responseNonce,
    responseBodySha,
].join("\n");

// ---- signature verification ----
const expectedSig = "v1=" + hmacSha256Base64(API_SECRET, canonical);

pm.test("Response signature matches", function () {
    pm.expect(responseSigHeader).to.eql(expectedSig);
});

// ---- optional replay protection ----
pm.test("Response nonce not replayed", function () {
    const key = "RESP_NONCE_CACHE";
    const ttl = 600;
    const now = Math.floor(Date.now() / 1000);

    let cache = [];
    try {
        cache = JSON.parse(pm.environment.get(key) || "[]");
    } catch {
    }

    cache = cache.filter(x => now - x.ts < ttl);
    pm.expect(cache.some(x => x.nonce === responseNonce)).to.eql(false);

    cache.push({nonce: responseNonce, ts: now});
    pm.environment.set(key, JSON.stringify(cache));
});
```

***

### Replay Protection

Responses are protected using:

* Timestamp validation (±5 minutes)
* Unique response nonce
* Request–response binding via request nonce
* Optional nonce replay cache on client side

***

### Error Handling

Even error responses are signed.

Example:

```json
{
  "code": 20002,
  "payload": null,
  "error": {
    "message": "Bad signature",
    "details": {
      "reason": "signature_mismatch"
    }
  },
  "request_id": "req_xxx"
}
```

Clients must still verify the response signature before trusting the error.

***

### Important Notes

#### ✔ When Response Signing Is Enabled

* Client must verify signature
* Client must validate timestamp
* Client should cache nonces
* Client must reject mismatched responses

#### ✔ When Response Signing Is Disabled

* No `X-Response-*` headers are returned
* Client should skip verification
* Suitable for:
  * Internal testing
  * Low-risk integrations
  * Legacy compatibility

***

### Best Practices

* Always verify response signature before processing data
* Cache response nonces for at least 5 minutes
* Reject responses with invalid timestamps
* Use raw response body for hashing
* Log request\_id for debugging
* Never disable verification in production

***

End of document.
