> For the complete documentation index, see [llms.txt](https://docs.goassay.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.goassay.ai/developers/zh/shang-jia-ni-de-api-jin-jie/write-your-own-verifier.md).

# 自行实现验证

用其他语言实现 Assay 的 Ed25519 请求验证，并用共享测试夹具核对。

如果所用语言没有 Assay 验证器，请参考此页。实现下方请求签名规范，再用共享测试向量核对。Node、Python 或 Go 请使用[现有库](/developers/zh/shang-jia-ni-de-api-ru-men/verify-in-your-code.md)。

## 工作原理

Assay 使用 Ed25519，对 RFC 9421 风格的 HTTP Message Signatures 签名基串签名。`Signature-Input` 描述覆盖的组件和参数。`Signature` 携带签名字节。两者都是结构化字典；请选择名为 `assay` 的成员。

从配置的可信 JWKS `https://api.goassay.ai/.well-known/assay-request-keys.json` 获取公钥。绝不能让请求选择密钥获取 URL。在框架解码路径或解析 JSON 前，读取原始请求目标和原始请求体。

## 按顺序实现检查

1. 将两个请求头解析为结构化字典。请求头或 `assay` 成员缺失时返回 `MISSING`；解析失败或签名成员不是字节序列时返回 `MALFORMED`。
2. 要求 `Signature-Input` 含组件内层列表。检查下方参数。标签缺失或不同为 `WRONG_TAG`；必需参数无效为 `MALFORMED`。
3. 要求组件列表和顺序与下方完全一致。非空请求体没有被签名覆盖的 `content-digest` 时，返回 `MALFORMED`。如果覆盖摘要，则要求存在 `Content-Digest`，解析其 `sha-256` 字节序列，并与原始请求体的 SHA-256 比较。摘要缺失为 `MISSING`，结构化语法无效为 `MALFORMED`，SHA-256 值缺失或不同为 `DIGEST_MISMATCH`。
4. 按 Unix 秒检查时间窗口。要求 `expires >= created`、`expires - created <= 300`，以及 `created - 60 <= now <= expires + 60`。否则返回 `EXPIRED`。这些边界均包含等号。
5. 严格按下方说明构建签名基串。去除首尾空白后的调用 ID 和 authority 必须非空，请求目标必须有效；否则返回 `MALFORMED`。
6. 在可信密钥集中查找 `keyid`，并对 UTF-8 基串字节验证 Ed25519。没有可用密钥时为 `UNKNOWN_KEY`；密码学检查失败时为 `BAD_SIGNATURE`。
7. 仅在密码学验证后，原子地检查并保存随机数。保留 `max(1, expires + 60 - now)` 秒。已有有效记录时返回 `REPLAYED`。存储出错必须拒绝请求，不能运行应用。

| 参数                  | 必需的值或类型         |
| ------------------- | --------------- |
| `created`、`expires` | 整数，单位为 Unix 秒   |
| `nonce`             | 非空字符串           |
| `keyid`             | 标识公钥的非空字符串      |
| `alg`               | `ed25519`       |
| `tag`               | `assay-request` |

Assay 用 16 个随机字节经 base64url 编码生成随机数。验证只要求非空字符串；不要另加不同的随机数长度要求。

## 构建规范形式

不覆盖摘要时，组件列表必须是：

```
("@method" "@authority" "@path" "@query" "x-assay-call-id")
```

覆盖摘要时，在 `"x-assay-call-id"` 前紧邻插入 `"content-digest"`。不接受其他组件顺序。空请求体也可以覆盖摘要；此时对空字节串计算哈希。

| 组件                | 基串中的值                                                                                      |
| ----------------- | ------------------------------------------------------------------------------------------ |
| `@method`         | 大写 HTTP 方法                                                                                 |
| `@authority`      | 如已配置，则使用公开 authority；否则使用去除首尾空白并转为小写的 `Host`。保留收到的端口。如果缺少 `Host` 且输入为绝对 URL，库会使用该 URL 的主机。 |
| `@path`           | 原始路径；为空时使用 `/`                                                                             |
| `@query`          | `?` 后接原始查询字符串；没有查询时仅用 `?`                                                                  |
| `content-digest`  | 覆盖摘要时，使用去除首尾空白的完整 `Content-Digest` 请求头值                                                    |
| `x-assay-call-id` | 去除首尾空白的 `X-Assay-Call-Id` 请求头值                                                             |

对于 `/v1/search?q=a%20b` 这类源站形式的目标，在第一个 `?` 处分割。保留查询顺序、百分号转义和路径段，不要解码、排序、解析路径或重新编码。拒绝以 `//` 开头或缺少前导 `/` 的目标。库也可通过 URL 解析器接受绝对 URL；优先使用服务器的原始源站形式目标，避免验证前被规范化。

每行写成 `"component-name": value`。追加 `"@signature-params":` ，后接 `assay` 成员的原始内层列表和参数。即其 `assay=` 之后、下一个字典成员之前的文本；保留参数顺序、空格和转义。不要包含 `assay=`，也不要从无序映射重建参数。

用单个 LF（`\n`）连接各行，末尾不加换行。编码为 UTF-8。直接对这些字节验证普通 Ed25519，不要另加哈希，也不要使用 Ed25519ph。

常见摘要请求头为 `sha-256=:BASE64_DIGEST:`。摘要和签名在结构化字节序列中使用标准 base64。JWK 的 `x` 公钥值使用 base64url。选择 `kid` 匹配、`kty="OKP"`、`crv="Ed25519"` 且公钥可用的密钥；跳过不可用密钥。按[生产缓存规则](/developers/zh/shang-jia-ni-de-api-jin-jie/run-in-production.md)处理轮换。

## 示例：夹具中的确切基串

第一个正向夹具的原始请求体为 `{"q":"a b"}`。其规范基串是以下七行，末尾没有换行：

```
"@method": POST
"@authority": api.example.test
"@path": /v1/search
"@query": ?q=a%20b
"content-digest": sha-256=:dm7Bv3nwO4zfpapceY8ZEGZiaYiTOc8C3j72DLhHCSw=:
"x-assay-call-id": c-1
"@signature-params": ("@method" "@authority" "@path" "@query" "content-digest" "x-assay-call-id");created=1760700000;expires=1760700300;nonce="AQEBAQEBAQEBAQEBAQEBAQ";keyid="test-2026-10";alg="ed25519";tag="assay-request"
```

要检查确切的预期字符串，并独立验证夹具签名，请在验证器仓库副本中运行以下代码。需要 Node 20 或更新版本，且仅使用内置模块：

```sh
node --input-type=module <<'JS'
import { readFileSync } from "node:fs";
import { createPublicKey, verify } from "node:crypto";
const vectors = JSON.parse(readFileSync("test-vectors/v1.json", "utf8"));
const c = vectors.cases[0];
const signature = c.expected.headers.Signature.match(/^assay=:([^:]+):$/)[1];
const ok = verify(
  null,
  Buffer.from(c.expected.signatureBase, "utf8"),
  createPublicKey({ key: vectors.publicKeyJwk, format: "jwk" }),
  Buffer.from(signature, "base64"),
);
if (!ok) throw new Error("fixture signature failed");
console.log(JSON.stringify(c.expected.signatureBase));
console.log("fixture signature verified");
JS
```

这会检查夹具本身。你的实现还必须从请求重建基串，并与 `expected.signatureBase` 逐字节比较。

## 确认是否成功

让验证器运行 `test-vectors/v1.json` 和 `test-vectors/negative.json` 中的每个用例。正向用例中，将 `request.headers` 与 `expected.headers` 合并，使用 `publicKeyJwk`，将请求体编码为 UTF-8，并将时钟固定到 `params.created`。必须接受请求，且基串与预期完全一致。

负向用例使用各自提供的 `now`，并要求确切匹配 `expectedReason`；`OK` 表示接受。每个用例使用全新重放存储，但在同一用例的 `sequence` 内保留。重放用例通过这种方式证明首次调用通过、下次失败。夹具种子和公钥只能用于测试。

集成测试应包含并发和存储故障检查。生产存储必须跨实例共享随机数状态，并原子地执行检查和插入。

## 错误

签名拒绝时返回 HTTP `401`、`content-type: application/json` 和 `{"error":"invalid_assay_signature","reason":"CODE"}`。保留检查顺序，让原因与夹具一致。基础设施故障也必须继续拒绝请求；它们不是签名错误的证据。

完整的调用方可见原因及解决方法见[拒绝原因](/developers/zh/shang-jia-ni-de-api-ru-men/reject-reasons.md)。

## 延伸阅读

* [在生产环境运行验证](/developers/zh/shang-jia-ni-de-api-jin-jie/run-in-production.md)。
* 上架你的 API（可选）：[自行实现验证](https://docs.goassay.ai/list-your-api/zh/list-a-catalogue-tool)。

## 下一步

* [链上质押与处罚](/developers/zh/shang-jia-ni-de-api-jin-jie/staking-and-penalties.md)。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.goassay.ai/developers/zh/shang-jia-ni-de-api-jin-jie/write-your-own-verifier.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
