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

# 鉴权指南

> 说明 API Key 签名鉴权、生产密钥的安全领取方式、RSA 密钥生成和三类 IP 白名单配置。

## 📄 正文

无论您是交易所、钱包还是平台方，只要拿到 DCS 交付的一对密钥，就可以用一套统一的签名规则调用所有 `/open-api/` 接口——本页帮您把鉴权一次配通。作为持牌、自有 BIN 的发卡机构，DCS 对每一次接口调用都要求可验证的来源与防重放保护。

本页覆盖三件事：调用接口的 **HMAC 签名**、生产环境**密钥的安全提取**、以及授权通知所用的 **RSA 公钥生成**。

***

## 1. 密钥与请求头

### 您会从 DCS 拿到什么

| 字段          | 描述                                    | 谁提供 |
| :---------- | :------------------------------------ | :-- |
| `apiKey`    | 调用 `/open-api/` 的全局唯一标识，用于身份识别与请求来源追踪 | DCS |
| `secretKey` | 用于计算请求签名的密钥，**请妥善保管，切勿泄露给任何第三方**      | DCS |

> 沙盒环境的 `apiKey`/`secretKey` 请联系 DCS 团队获取；生产环境必须走 [安全提取流程](#3-生产环境密钥的安全提取)。

### 每个请求必带的头

接入机构对 `/open-api/` 的每次调用，都需在 HTTP 头中携带（请求体为 JSON 时还需带 `Content-Type: application/json`，官网示例未列出，但缺失会导致 `415` 错误）：

| Header             | 必填       | 说明                                                                                       |
| :----------------- | :------- | :--------------------------------------------------------------------------------------- |
| `Content-Type`     | REQUIRED | 固定 `application/json`。JSON 请求体必须携带该头，否则后端返回 `415 Unsupported Media Type`——这是接入初期最常见的一类失败 |
| `X-DAPI-API-KEY`   | REQUIRED | DCS 交付的 `apiKey`                                                                         |
| `X-DAPI-TIMESTAMP` | REQUIRED | 请求时间戳（Unix 毫秒时间戳，与时区无关），用于防重放                                                            |
| `X-DAPI-NONCE`     | REQUIRED | 随机数，取值范围 `[10000, 99999]`；每次请求都应重新生成                                                     |
| `X-DAPI-SIGN`      | REQUIRED | 按下文规则计算的 HMAC-SHA256 签名（十六进制小写）                                                          |

<Warning>
  `secretKey` 仅用于本地计算签名，切勿在请求头中明文传输。
</Warning>

***

## 2. 接口签名（HMAC-SHA256）

### 签名规则

使用 **HmacSHA256** 算法，以 `secretKey` 为密钥，对拼接串签名：

```
X-DAPI-SIGN = HmacSHA256( apiKey + timestamp + nonce + payload , secretKey )
```

输出为十六进制小写字符串。

**拼接串各段说明：**

| 段           | 取值                                                         |
| :---------- | :--------------------------------------------------------- |
| `apiKey`    | 与 `X-DAPI-API-KEY` 一致                                      |
| `timestamp` | 与 `X-DAPI-TIMESTAMP` 一致（毫秒时间戳）                             |
| `nonce`     | 与 `X-DAPI-NONCE` 一致                                        |
| `payload`   | **GET** 请求：URL 编码后的查询参数（query string）；**其他方法**：原始请求体（body） |

```text theme={null}
if method is GET:
    payload = url.encodedQuery()     // 例如 cardOrderRef=14
else:
    payload = body                   // 原始 JSON 请求体字符串
```

> 签名算法固定为 HmacSHA256，请一律使用该算法。

### 防重放（谁做：DCS 校验 / 接入机构生成）

* **TIMESTAMP**：DCS 默认只接受 **5 秒内** 的请求；超时请求一律拒绝。接入机构请确保本地时钟与 UTC 同步。
* **NONCE**：每次请求生成一个 `[10000, 99999]` 的随机数，请勿复用。接入机构仍需为业务写操作设置幂等键，并避免在 5 秒窗口内重放完全相同的请求。

### 签名实现示例

**JavaScript（Postman Pre-request Script）**

```javascript theme={null}
const CryptoJS = require('crypto-js');

const apiSecret = '{your secret key}';
const apiKey    = '{your api key}';

// GET 用查询参数；其他方法用请求体，二选一拼接
const queryParams  = '{your query param}';   // 例如 cardOrderRef=14
const requestBody  = '{your request body}';  // POST/PUT 时的原始 body
const nonce        = 10010;
const timestamp    = Date.now().toString();

const dataToSign = apiKey + timestamp + nonce + queryParams + requestBody;
const signature  = CryptoJS.HmacSHA256(dataToSign, apiSecret).toString(CryptoJS.enc.Hex);
```

**Java**

```java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;

public final class HmacSignature {
    private static final String HMAC_SHA256 = "HmacSHA256";

    public static String getSignature(String apiSecret, String data) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(apiSecret.getBytes(), HMAC_SHA256);
        Mac mac = Mac.getInstance(HMAC_SHA256);
        mac.init(keySpec);
        byte[] hmac = mac.doFinal(data.getBytes());
        return Hex.encodeHexString(hmac);
    }
}
```

### 完整请求示例（GET）

```bash theme={null}
curl --location 'https://{domain}/open-api/card-order/v1/detail?cardOrderRef=14' \
  --header 'Content-Type: application/json' \
  --header 'X-DAPI-API-KEY: 697EA72DACF742F280943DAB211E6C2B' \
  --header 'X-DAPI-TIMESTAMP: 1743044911331' \
  --header 'X-DAPI-NONCE: 10100' \
  --header 'X-DAPI-SIGN: b979d2ea5c70187ddac7d7a5937c40c87a58f765594b740e596d25fca96f2dcb'
```

> 此例中 `payload = cardOrderRef=14`（GET 的查询参数）。验证您的实现时，先用 DCS 提供的同一组 `apiKey/timestamp/nonce/payload` 复算出相同的 `X-DAPI-SIGN`，再上线。

### 统一响应结构

所有 `/open-api/` 接口返回统一结构：

```json theme={null}
{
  "code": "SYS_SUCCESS",
  "message": null,
  "messageDetail": null,
  "data": { }
}
```

| 字段              | 说明                                      |
| :-------------- | :-------------------------------------- |
| `code`          | 业务状态码，成功为 `SYS_SUCCESS`；失败为具体错误码（见错误码页） |
| `message`       | 简要信息，成功时通常为 `null`                      |
| `messageDetail` | 详细信息对象，通常为 `null`                       |
| `data`          | 业务数据                                    |

> 判断成功请以 `code == "SYS_SUCCESS"` 为准；失败时 `code` 为具体错误码（见错误码页）。

***

## 3. 生产环境密钥的安全提取

为避免生产环境的 `apiKey`/`secretKey` 在交付途中泄露，**生产密钥不直接发给您**，而是走一次性安全提取流程。

> 沙盒环境无需此流程，直接联系 DCS 团队获取即可。

### 流程（谁做）

1. **接入机构提供**：一个安全邮箱地址 + 一个用于提取的请求 IP。
   * 邮箱用于接收提取指引；该 IP 会被加入「提取密钥」白名单。
2. **DCS 发送**：安全邮箱收到一封邮件，内含一个**仅一次有效**的临时安全链接。
3. **接入机构提取**：把 `extractUrl` 与 `extractSecretKey` 拼接后，**在指定 IP 的机器上执行**，即可领取 `apiKey`/`secretKey`。

### 邮件中的字段

| 字段                 | 类型     | 描述          |
| :----------------- | :----- | :---------- |
| `expireTime`       | string | 提取安全码的有效期   |
| `extractSecretKey` | string | 密钥提取安全码     |
| `extractUrl`       | string | 密钥提取 URL    |
| `howToUse`         | string | 使用方式说明      |
| `notes`            | string | 提示：链接仅可提取一次 |

**成功响应**

```json theme={null}
{
  "code": "SYS_SUCCESS",
  "message": null,
  "messageDetail": null,
  "data": {
    "expireTime": "2024-10-21T16:52+08:00[Asia/Shanghai]",
    "extractSecretKey": "4f72b1e9c49e4ac1bbc2cd12b5e44993",
    "extractUrl": "https://{domain}/internal/open-api/v1/secret-extract/",
    "howToUse": "Please concatenate the url with the secret-key and execute it on the specified machine.",
    "notes": "This link is only valid for one AKSK extraction, if the content is not properly accessed, the AKSK may have been compromised, please contact us promptly."
  }
}
```

**失败响应**

```json theme={null}
{
  "code": "ERROR-CODE",
  "message": "simple describe, see error-code list",
  "messageDetail": null,
  "data": null
}
```

<Warning>
  **若未能成功领取密钥**：说明链接可能已在有效期内被使用、密钥存在泄露风险。请立即联系 DCS 商务，重新走邮件流程换发。
</Warning>

***

## 4. RSA 公钥（授权通知专用）

授权转发通知（DCS → 接入机构 `auth_url`）对敏感数据（如交易金额、币种）采用 **RSA-2048 双向签名 + 加密**，与上文调用接口用的 HMAC 是两套独立机制。授权通知没有额外的 AES/IV 层：业务字段整体以 RSA 分段加密，签名算法为 `SHA256withRSA`。

* **HMAC**：接入机构调用 `/open-api/` 接口时使用（本页第 1–2 节）。
* **RSA**：DCS 向接入机构推送授权通知时使用，保护授权流程上的敏感字段。

接入机构需自行生成一对 RSA 密钥，把**公钥**（`external_public_key`）交给 DCS，私钥自己保管。

### 生成方式（谁做：接入机构）

```bash theme={null}
# 1. 生成 2048 位 RSA 私钥
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

# 2. 从私钥导出公钥
openssl rsa -pubout -in private_key.pem -out public_key.pem

# 3. 去掉头尾标记与换行，得到提交给 DCS 的单行公钥串
cat public_key.pem | sed '/BEGIN/d; /END/d' | tr -d '\n'
```

第 3 步输出的单行字符串即 `external_public_key`，连同 `auth_url` 一并提供给 DCS。

### DCS RSA 公钥（谁给：DCS）

接入机构验证 DCS 通知签名、加密返回数据时，使用 DCS 的 RSA 公钥。

* **沙盒环境**：

```text theme={null}
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7FDHTQgX8vDvBZMlOOQm
xqoEw/gvk2bRQFa8UooAq4fWkChBpWM+bi0vu3T00rxqenbFDZoz5QE+/Mhhk+4X
Nvo47rSnhP6Nhkjivo8S8wUuHZbk20EwaqVVVpjYVvdFWSeLHkumofdj5HdbgzUI
nqslVdtAGMYJCzH0KlfWnVUuW2BnIk0wro68rNk0bqqbCkcnLLXpyElKxZCAvvo4
LRYcS81jiG78a6EzgbzLjuNvYH5vOj9dh49QevTBCyBUC35JVt85VC3RUGny9CKL
dn3x9XOLKRdndYEWGU5ZJVZdioNgWhU40TBVjveqbORSs+ApAZOWTovrKvABthXp
hQIDAQAB
-----END PUBLIC KEY-----
```

* **生产环境**：请联系 DCS 团队获取。

### 加解密与签名 Java 示例

签名算法为 `SHA256withRSA`，密钥 2048 位，加解密按块分段（加密块 245 字节 / 解密块 256 字节）：

```java theme={null}
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class RSAUtil {
    private static final String ALGORITHM = "RSA";
    private static final int KEY_SIZE = 2048;
    private static final int MAX_ENCRYPT_BLOCK = KEY_SIZE / 8 - 11; // 245
    private static final int MAX_DECRYPT_BLOCK = KEY_SIZE / 8;      // 256

    // 公钥加密（分段）
    public static String encrypt(String data, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] encryptedBytes = doSegment(cipher, data.getBytes(), MAX_ENCRYPT_BLOCK);
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    // 私钥解密（分段）
    public static String decrypt(String encryptedData, PrivateKey privateKey) throws Exception {
        byte[] dataBytes = Base64.getDecoder().decode(encryptedData);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decryptedBytes = doSegment(cipher, dataBytes, MAX_DECRYPT_BLOCK);
        return new String(decryptedBytes);
    }

    // 从 Base64 字符串还原公钥
    public static PublicKey parsePublicKey(String base64PublicKey) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(base64PublicKey);
        return KeyFactory.getInstance(ALGORITHM).generatePublic(new X509EncodedKeySpec(keyBytes));
    }

    // 从 Base64 字符串还原私钥
    public static PrivateKey parsePrivateKey(String base64PrivateKey) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(base64PrivateKey);
        return KeyFactory.getInstance(ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
    }

    // 私钥签名（SHA256withRSA）
    public static String sign(String data, PrivateKey privateKey) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(data.getBytes());
        return Base64.getEncoder().encodeToString(signature.sign());
    }

    // 公钥验签
    public static boolean verify(String data, String sign, PublicKey publicKey) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(publicKey);
        signature.update(data.getBytes());
        return signature.verify(Base64.getDecoder().decode(sign));
    }

    private static byte[] doSegment(Cipher cipher, byte[] data, int maxBlock) throws Exception {
        int inputLen = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offset = 0;
        while (offset < inputLen) {
            int blockLen = Math.min(inputLen - offset, maxBlock);
            out.write(cipher.doFinal(data, offset, blockLen));
            offset += blockLen;
        }
        return out.toByteArray();
    }
}
```

> 授权通知的完整加解密与验签细节见 [授权转发与加密](../how-to-use/transactions/authorization)。

***

## 5. IP 白名单（三类，缺一不可）

DCS 对所有交互均设白名单。接入机构需向 DCS 提供**沙盒与生产各自**的静态出口 IP，分别用于以下三个方向：

| 白名单         | 方向                       | 用途                        | 谁提供  |
| :---------- | :----------------------- | :------------------------ | :--- |
| 接口调用白名单     | 接入机构 → DCS               | 调用 `/open-api/` 接口的网络出口地址 | 接入机构 |
| Webhook 白名单 | DCS → 接入机构 `webhook_url` | 接收卡订单/交易等事件通知的出口地址        | 接入机构 |
| 授权转发白名单     | DCS → 接入机构 `auth_url`    | 接收授权转发通知请求的出口地址           | 接入机构 |

> 沙盒与生产环境分别配置，请勿混用。生产密钥的「提取 IP」（第 3 节）与「接口调用 IP」可以不同，请分别向 DCS 说明用途。

***

## 下一步

鉴权配通后，前往 [快速开始](../getting-started/quickstart) 跑通第一张卡，或先在 [前置准备](../getting-started/first-steps) 确认开通、回调与白名单已就绪。授权通知的 RSA 加解密细节见 [授权转发与加密](../how-to-use/transactions/authorization)。
