认证

访问 Fystack API 的任何 endpoint 都需要对请求进行认证。本指南将介绍认证的工作原理。Fystack 使用基于 HMAC 的认证来安全地验证 API 请求的真实性。

HMAC 认证

Fystack API 使用安全的、基于 HMAC 的认证体系,要求你使用 API secret 对请求进行签名。每个请求都需要三个认证请求头:

Required authentication headers

ACCESS-API-KEY: your_api_key
ACCESS-TIMESTAMP: current_unix_timestamp
ACCESS-SIGN: base64_encoded_hmac_signature

HMAC 签名的生成方式是:将 HTTP 方法、请求路径、时间戳以及请求体(如有)组合起来,然后使用你的 API secret 进行签名。

带认证请求头的请求示例

以下示例展示了如何向 Fystack API 发送经过认证的请求:

Example authenticated request

curl https://api.fystack.io/api/v1/wallets \
-H "ACCESS-API-KEY: your_api_key" \
-H "ACCESS-TIMESTAMP: 1667836889" \
-H "ACCESS-SIGN: YourBase64EncodedSignature=="

计算 ACCESS-SIGN

计算 HMAC 签名需要以下步骤:

  1. 按固定顺序使用请求参数构建规范字符串:method、path、timestamp、body
  2. 使用你的 API secret 以 HMAC-SHA256 对该字符串签名
  3. 将 HMAC 摘要进行十六进制编码,再对该十六进制字符串进行 base64 编码

请始终妥善保管你的 API secret,如怀疑已泄露请立即重置。以下是各语言计算签名的方式:

import CryptoJS from 'crypto-js'

function computeSignature(
  apiSecret: string,
  method: string,
  path: string,
  timestamp: string,
  body: string = ''
): string {
  // Build canonical string in exact order
  const canonical = `method=${method}&path=${path}&timestamp=${timestamp}&body=${body}`

  // HMAC-SHA256 -> hex -> base64
  const hexDigest = CryptoJS.HmacSHA256(canonical, apiSecret)
    .toString(CryptoJS.enc.Hex)

  return btoa(hexDigest)
}

// Example usage
const timestamp = Math.floor(Date.now() / 1000).toString()
const signature = computeSignature(
  'your_api_secret', 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  timestamp
)

const headers = {
  'ACCESS-API-KEY': 'your_api_key',
  'ACCESS-TIMESTAMP': timestamp,
  'ACCESS-SIGN': signature
}

请求签名流程

  1. 按固定顺序构建规范字符串:method={METHOD}&path={PATH}&timestamp={TIMESTAMP}&body={BODY}

    • method:大写的 HTTP 方法(GET、POST 等)
    • path:请求路径(例如 /api/v1/workspaces/{id}/wallets)
    • timestamp:以秒为单位的 UNIX 时间戳
    • body:JSON 序列化后的请求体,GET 请求则为空字符串
  2. 生成 HMAC-SHA256 签名:

    • 使用你的 API secret 对规范字符串签名
    • 对摘要进行十六进制编码
    • 对十六进制字符串进行 base64 编码
  3. 添加必需的请求头:

    • ACCESS-API-KEY:你的 API key
    • ACCESS-TIMESTAMP:签名中使用的时间戳
    • ACCESS-SIGN:base64 编码后的签名

请妥善保管你的 API secret,切勿与他人共享。API key 可以共享,但 secret 必须保密。

构建 Body 字符串

无论是 HMAC 还是 Ed25519,规范字符串中的 body 都必须与你实际发送的字节完全一致。对于 GET 请求或任何没有请求体的请求,它应为空字符串,绝不能是 "{}" 或 null。

需要避免的错误:对 payload 序列化两次,一次用于签名,一次在 HTTP 客户端发送时。如果你签名的内容与服务器收到的内容在字节上不完全一致,即使 payload 在逻辑上是相同的 JSON,ValidateAccessSign 也会失败。请只序列化一次,然后将这个完全相同的字符串既用于构建规范字符串,也作为实际的请求体;不要把原始的 object/dict/map 交给 HTTP 客户端,并指望第二次独立的序列化调用能精确复现它。根据语言的不同,这要么会明显报错(某些 HTTP 客户端会直接拒绝或错误处理非字符串的请求体),要么会悄无声息地表现为难以排查的签名不匹配(当两次序列化调用因库升级、默认选项不同或 NaN 等特殊值而产生差异时),因此不要依赖任何一种结果,只需避免重复序列化。

const payload = { name: 'my-integration' }
const body = JSON.stringify(payload) // serialize once

// sign() uses `body` in the canonical string, exactly as below
const signature = computeSignature(apiSecret, 'POST', path, timestamp, body)

await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', /* ...auth headers */ },
  // `fetch` requires body to already be a string (or Blob/ArrayBuffer/etc),
  // it will not JSON-serialize `payload` for you, pass `body`, not `payload`
  body
})

Ed25519 签名

如果你的 API key 使用的是客户端密钥对而不是 HMAC secret,请使用你的 Ed25519 私钥对同一个规范字符串签名,而不是计算 HMAC。与 HMAC 流程相比有两点不同:

  • 直接对规范字符串的字节签名,没有单独的哈希步骤,Ed25519 会在内部完成哈希。
  • 直接对原始的 64 字节签名进行 base64 编码,没有十六进制编码步骤。
ACCESS-SIGN = base64( Ed25519_Sign(privateKey, canonical_string) )

如果你使用的是 @fystack/sdk,只需在 credentials 中传入一个 signer(LocalPrivateKeySigner 或 AwsKmsSigner),SDK 就会为你完成签名,详见 API Keys。以下示例适用于不使用 SDK、以其他语言进行签名的场景。

const crypto = require('crypto');

function computeSignature(privateKeyPem, method, path, timestamp, body = '') {
  const canonical = `method=${method}&path=${path}&timestamp=${timestamp}&body=${body}`;

  const privateKey = crypto.createPrivateKey(privateKeyPem);
  // No digest algorithm, Ed25519 signs the message directly
  const signature = crypto.sign(null, Buffer.from(canonical), privateKey);

  return signature.toString('base64');
}

// Usage
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = computeSignature(
  process.env.PRIVATE_KEY_PEM, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  timestamp
);

切勿将你的 Ed25519 私钥发送给 Fystack,也不要将其嵌入客户端或浏览器代码中,注册时只需提供公钥。生产环境中,请将密钥保存在 AWS KMS 等硬件级存储中,而不是 PEM 文件中,详见客户端密钥对。

通过 AWS KMS 签名(不使用 SDK)

如果你在 Node.js 上使用 @fystack/sdk,AwsKmsSigner 已经为你完成了这一步,详见客户端密钥对。对于其他所有后端,或者你只是想了解其机制,同样的方法适用于任何语言的 AWS SDK:使用 MessageType: RAW 和 SigningAlgorithm: ED25519_SHA_512 对原始规范字符串调用 KMS Sign,然后对返回的签名字节进行 base64 编码。这正是 AwsKmsSigner 的内部实现,下面的 Node.js 示例就是直接写出的相同逻辑。

MessageType 必须为 RAW,而不是 DIGEST。KMS 的 ED25519_SHA_512 算法要求传入未经哈希的消息并在内部进行哈希,这与本页其他部分中 Ed25519 的工作方式一致。传入 DIGEST 会对一个不同的、无法互通的值进行签名。

import { KMSClient, SignCommand } from '@aws-sdk/client-kms'

async function computeSignature(kmsClient, keyId, method, path, timestamp, body = '') {
  const canonical = `method=${method}&path=${path}&timestamp=${timestamp}&body=${body}`

  const response = await kmsClient.send(new SignCommand({
    KeyId: keyId,
    Message: Buffer.from(canonical, 'utf8'),
    MessageType: 'RAW',
    SigningAlgorithm: 'ED25519_SHA_512'
  }))

  return Buffer.from(response.Signature).toString('base64')
}

上面的 kmsClient/client/$kms 就是你所用语言的 AWS SDK 提供的 KMS 客户端,按你平时的方式配置即可。本地与生产环境的通用配置模式请参阅客户端密钥对(本地将其指向 LocalStack/ministack 并使用静态测试凭证,生产环境去掉这两项,由 IAM 角色提供凭证)。无论使用哪种语言,思路都相同,只是通过各 SDK 自身的配置选项来实现。