错误码
DAT 官方支持的各服务库共用的错误码。
每个码都带有 影响、重试 两个值,部分还会额外带上 可疑 标签。
影响 — 服务受到多大冲击
这是设置告警的依据,只看一件事:“现在服务停了吗”。
| 影响 | 含义 | 例子 |
|---|---|---|
| 致命 | 服务或某项功能停止。无法签发、同步永久失败、初始化失败 | 签发服务器手上一张可用证书都没有 |
| 部分 | 部分请求或周期失败,但服务继续运行,通常会自行恢复 | CMS 的一个周期失败,现有证书继续工作 |
| 无影响 | 拒绝掉一个请求就结束了 | 收到被篡改的令牌,过滤掉即可 |
无影响 不是告警的对象。如果一条错误输入就要让所有值班人员去看,告警本身就失去意义了。
可疑 — 持续出现就要排查
带 可疑 标签的码,只出现一次时属于正常运行的一部分。客户端随时可能发来错误的值,把它过滤掉正是库的本职。
但如果这类错误持续发生,或者从某个来源集中涌来,那就是两种情况之一。
- 配置异常 — 部署有误、还残留着旧版客户端、或者证书对不上。
- 攻击尝试 — 有人在篡改令牌或密钥、试图通过验证,或者在探测哪些值有效。
所以这些码正确的做法是把计数做成指标,只在超过阈值时才通知。
重试
| 重试 | 含义 |
|---|---|
| 暂时 | 退避后重试即可解决 |
| 永久 | 禁止重试,必须修正配置或输入 |
| 状态 | 是信号,不是错误 |
令牌
收到的令牌字符串本身的问题。
DAT_TOKEN_MALFORMED 以点分隔的部分不是 5 个,或 expire 不是纯十进制,或 cid 不是纯十六进制,或 plain、secure 不是 base64url,或数值字段超出了整数表示范围。
arrow_forward拒绝请求
DAT_TOKEN_EXPIREDexpire <= now。整点也算过期 — expire == now 就视为已经过期。
arrow_forward引导重新签发令牌
DAT_TOKEN_UNKNOWN无法归入以上任何一类的令牌错误。
arrow_forward查看日志
过期和格式错误必须区分开
两者的处理正好相反 — 过期是正常的生命周期结束,让对方刷新令牌即可;格式错误则说明它根本不是一个有效签发的令牌,必须拒绝。
解析时先确定结构,再看取值。像 "1.2.3" 这种部分不足的字符串不是过期的令牌,而根本就不是令牌,因此是 DAT_TOKEN_MALFORMED。
expire 字段带符号(如 +100)同样属于格式错误而非过期。只接受纯 ASCII 数字。
证书
证书字符串的格式,以及该证书现在是否可用的问题。
DAT_CERT_MALFORMED 以点分隔的部分不是 8 个,或 cid、start、duration、ttl 解析失败,或密钥字段不是 base64url,或 start + duration + ttl 超出了 u64。
arrow_forward重新分发证书
DAT_CERT_EXPIREDstart + duration + ttl < now。完全过期,既不能签发也不能验证。
arrow_forward更新证书
DAT_CERT_NOT_YET_ISSUABLEnow < start。签发窗口尚未开启。
arrow_forward等待
DAT_CERT_ISSUANCE_ENDEDnow > start + duration,但 ttl 还有剩余。已无法签发,只能验证。
arrow_forward分发新证书
DAT_CERT_VERIFY_ONLY只含公钥、不含签名私钥的证书。可以验证但无法签发。
arrow_forward检查分发配置
DAT_CERT_NOT_FOUND 没有持有与令牌 cid 对应的证书。要么令牌是伪造的,要么分发出了问题。
arrow_forward拒绝请求
DAT_CERT_NOT_SYNCED 该 cid 还没从 CMS 收到。刚分发新证书之后会短暂出现。
arrow_forward同步后重试
DAT_CERT_DUPLICATE_CID 正在 import 的列表中同一个 cid 出现了两次以上。
arrow_forward检查服务器响应
DAT_CERT_UNKNOWN无法归入以上任何一类的证书错误。
arrow_forward查看日志
DAT_CERT_NOT_FOUND 和 DAT_CERT_NOT_SYNCED 表面症状相同,但处理方式不同。前者是从未签发过的 cid,等下去也不会出现;后者只要同步跟上就会消失。
DAT_CERT_NOT_FOUND 出现一次过滤掉即可,但如果突然增多,就说明分发对不上了,或者有伪造令牌在流通。
签名
DAT_SIG_MISMATCH签名验证以不匹配结束。HMAC 值不同,或 ECDSA verify 返回 false。
arrow_forward阻断会话,记入安全日志
DAT_SIG_MALFORMED 签名部分为空,或不是 base64url,或 ECDSA 的 r‖s 长度与曲线不符,或 DER 转换失败。
arrow_forward拒绝请求
DAT_SIG_KEY_MISSING用 verify-only 的密钥尝试签名。运行时不存在私钥。
arrow_forward检查签发服务器配置
DAT_SIG_BACKEND签名、验证的运算本身没能执行。密钥类型错误、句柄已释放,或密码库内部错误。
arrow_forward检查密钥类型与库
DAT_SIG_UNKNOWN无法归入以上任何一类的签名错误。
arrow_forward查看日志
不要把不匹配和后端故障混为一谈
这两个码所在的轴正好相反。
DAT_SIG_MISMATCH— 只是收到的签名对不上,对服务没有影响,但持续出现就属于 可疑。DAT_SIG_BACKEND— 验证运算本身跑不起来,是实现侧的问题,不属于可疑。
把密钥类型错误或库的 bug 报成“签名不匹配”,等于把实现侧出故障的情况混进了攻击指标。反过来,真正的伪造被归类成后端错误,就会整个从可疑指标里漏掉。
加密
secure 载荷的加解密问题。
DAT_CRYPTO_TAG_MISMATCHAES-GCM 认证标签不匹配。secure 被篡改,或者证书密钥不同。
arrow_forward阻断会话,记入安全日志
DAT_CRYPTO_DATA_INVALID 密文非空但不超过 IV(12 字节),或输入超出了实现上限(如 INT_MAX)。
arrow_forward拒绝请求
DAT_CRYPTO_BACKEND加解密运算没能执行。平台不支持 GCM,或上下文初始化失败。
arrow_forward确认平台支持情况
DAT_CRYPTO_UNKNOWN无法归入以上任何一类的加解密错误。
arrow_forward查看日志
空的 secure 载荷不是错误。 空输入得到空输出,不会产生任何错误码。
在跳过签名验证的路径上,GCM 标签是唯一的完整性检查。因此不把 DAT_CRYPTO_TAG_MISMATCH 和其他解密失败归为同一个码。
密钥
DAT_KEY_INVALID 密钥长度与声明的算法不符(HMAC 32/48/64,AES 16/32),或点不在曲线上,或 d ∉ [1,n-1],或不是非压缩(0x04)格式,或私钥与公钥不成对。
arrow_forward更换密钥
DAT_KEY_VERIFY_ONLY_UNSUPPORTED对 HMAC 系算法请求了 verify-only 导出。
arrow_forward更换算法
DAT_KEY_UNKNOWN无法归入以上任何一类的密钥错误。
arrow_forward查看日志
看起来相似但完全不同的三个:
| 错误码 | 含义 |
|---|---|
DAT_KEY_VERIFY_ONLY_UNSUPPORTED | 算法的结构性限制。 HMAC 是对称密钥,本就没有公钥的概念 |
DAT_SIG_KEY_MISSING | 运行时状态。 当前这把密钥里没有私钥 |
DAT_CERT_VERIFY_ONLY | 分发形态。 这张证书是按仅供验证分发的 |
管理器
持有证书并用于签发、验证的对象的状态。
DAT_MANAGER_NO_CERTIFICATE一张证书都没有持有。要么还没 import,要么 CMS 首次同步失败。
arrow_forward检查 CMS 连接
DAT_MANAGER_NO_ISSUABLE_CERTIFICATE有证书,但现在没有一张能用于签发。原因会一并传出。
arrow_forward看原因(cause)判断 — 见下表
DAT_MANAGER_DISPOSED使用了已经释放的管理器或证书。
arrow_forward修正调用代码
DAT_MANAGER_UNKNOWN无法归入以上任何一类的管理器错误。
arrow_forward查看日志
DAT_MANAGER_NO_ISSUABLE_CERTIFICATE 的原因(cause)是以下四者之一。每种原因该做的事完全不同。
| 原因 | 含义 | 重试 | 处理 |
|---|---|---|---|
DAT_CERT_NOT_YET_ISSUABLE | 签发窗口尚未开始 | 临时 | 等一等就好 |
DAT_CERT_ISSUANCE_ENDED | 签发窗口结束,只能验证 | 永久 | 需要分发新证书 |
DAT_CERT_EXPIRED | 持有的证书全部过期 | 永久 | 需要更新证书 |
DAT_CERT_VERIFY_ONLY | 持有的证书全是仅供验证 | 永久 | 分发配置出错了 |
如果签发服务器被配置成只接收仅供验证的证书,出来的就是 DAT_CERT_VERIFY_ONLY。等多久都不会好,所以不属于重试的范畴。
配置
调用方传入的值有问题。CONFIG 系全部是必须改代码的错误,运行中出现就说明部署有误。
DAT_CONFIG_ALG_UNSUPPORTED 未知的算法名称。必须与线上表示(ECDSA-P256、IV-AES256-GCM)完全一致。
arrow_forward核对算法名称
DAT_CONFIG_ARGUMENT_INVALID 必填参数为 null,或超出允许范围(负的时间值、interval <= 0),或类型不受支持(在动态类型语言中把数字或布尔值当作 payload 传入),或待签名的 body 为空。
arrow_forward修正调用代码
DAT_CONFIG_URI_INVALIDCMS 服务器 URI 不符合规范。无法解析、scheme 不是 http/https、或带有路径或查询串。
arrow_forward修正 URI
DAT_CONFIG_UNKNOWN无法归入以上任何一类的配置错误。
arrow_forward查看日志
内部
运行环境与运行时的问题。
DAT_INTERNAL_UNAVAILABLE 根本没有密码后端或运行时 API。缺少 crypto.subtle、平台不支持 AES-GCM、运行时版本过低。
arrow_forward检查部署与平台
DAT_INTERNAL_UNKNOWN内存分配失败、随机数生成失败、获取锁失败,或者走到了设计上不可达的分支。
arrow_forward查看日志
DAT_INTERNAL_UNAVAILABLE 修好部署环境就能解决,而 DAT_INTERNAL_UNKNOWN 多半是运行时故障或库的 bug。
CMS 同步
不使用 CMS 同步就不会出现这些码。
DAT_CMS_UNREACHABLEDNS 失败、连接被拒、TLS 失败、超时。超时不单列错误码而是并入这里 — 因为处理方式相同。
arrow_forward退避后重试
DAT_CMS_UNAUTHORIZED服务器返回 401。令牌缺失或有误。
arrow_forward核对令牌配置
DAT_CMS_FORBIDDEN服务器返回 403。令牌有效,但没有该端点的权限。
arrow_forward核对令牌等级
DAT_CMS_ENDPOINT_NOT_FOUND服务器返回 404。URL 有误。
arrow_forward核对 URL 配置
DAT_CMS_SERVER_ERROR服务器返回 5xx。
arrow_forward退避后重试
DAT_CMS_HTTP_STATUS上述之外的非 2xx 响应。
arrow_forward查看状态码
DAT_CMS_MALFORMED响应里没有 version 行,或 version 行不是纯十进制,或超出了范围。
arrow_forward核对服务器版本
DAT_CMS_IMPORT_FAILED 响应收到了,但证书没能应用。原因放在 cause 里。
arrow_forward查看 cause 中的 CERT_* / KEY_*
DAT_CMS_VERSION_RESET服务器返回了比客户端更旧的 version。这是全量重新同步的指示。
arrow_forward已自动处理
DAT_CMS_NOT_SYNCED一次都还没有同步成功。
arrow_forward等待首次同步
DAT_CMS_SYNC_IN_PROGRESS上一次同步还在跑,因此跳过了本周期。这不是错误。
DAT_CMS_NOT_SUPPORTED构建时没有包含 CMS 功能。feature 未启用,或未内置 CURL。
arrow_forward检查构建选项
DAT_CMS_UNKNOWN无法归入以上任何一类的 CMS 错误。
arrow_forward查看日志
被判定为同步永久失败的码(UNAUTHORIZED、FORBIDDEN、ENDPOINT_NOT_FOUND、MALFORMED、IMPORT_FAILED)全部是致命。重试也解决不了,而证书还在不断过期,放着不管服务必然会停。
反过来,UNREACHABLE、SERVER_ERROR 属于部分。现有证书继续工作,下个周期通常会自行恢复 — 但一直失败下去最终会升级成致命。 请以连续失败次数为基准设置告警。
同步失败不会以异常抛出
即使首次同步失败,管理器也会正常返回 — 晚一点同步上也比一开始就起不来要好。失败会改为保留为可查询的状态。
| 客户端 | 查询方式 |
|---|---|
| Rust | manager.last_error().await |
| Go | manager.LastError() |
| JavaScript | manager.lastError() |
| Python | manager.last_error() |
| Ruby | manager.last_error |
| Java/Kotlin | manager.lastError |
| C# | manager.LastError |
| C/C++ | dat_cms_manager_last_error(m) |
一次都没成功过时是 DAT_CMS_NOT_SYNCED,一切正常时为空。
服务器
CMS 服务器产生的码。客户端不会产生这些码,只会收到。
DAT_AUTH_UNAUTHORIZED 没有 Authorization 头,或令牌未注册在任何等级中。
DAT_AUTH_FORBIDDEN令牌已注册,但不是该端点所要求的等级。
DAT_AUTH_DISABLED一个令牌都没配置,认证被整体关闭。连证书签发 API 也在无认证状态下开放。它不会作为响应返回,只会打印到启动日志。
arrow_forward立即设置令牌
DAT_REQ_MALFORMED无法解析路径或查询参数,或参数超出允许范围(负的 delay、超过十年等)。
DAT_REQ_ALG_UNSUPPORTED不认识请求路径中的算法名称。
DAT_REQ_NOT_FOUND没有这条路由,或者方法不匹配。
DAT_REQ_TOO_LARGE请求体大小超出上限。
DAT_REQ_UNKNOWN无法归入以上任何一类的请求错误。
DAT_STORE_UNAVAILABLE数据库连接断开、连接池耗尽、锁竞争、超时。这是唯一使用 503 的码,也是客户端得以判断“这个等一等就好”的信号。
arrow_forward退避后重试
DAT_STORE_UNKNOWN读写失败、表不存在、schema 不匹配,或已存储的证书行损坏。
arrow_forward检查数据库状态
响应信封:
{
"code": "DAT_REQ_ALG_UNSUPPORTED",
"details": { "algorithm": "BOGUS-ALG" }
}在生成和处理证书过程中出现的错误,服务器同样直接使用上面的公共码(DAT_CERT_*、DAT_KEY_*、DAT_CONFIG_*)。
收到服务器的码之后
客户端会用自己的 CMS 码把服务器的码包起来,原始码保存在 cause 中。
| 收到的码 | HTTP | 客户端产生的码 |
|---|---|---|
DAT_AUTH_UNAUTHORIZED | 401 | DAT_CMS_UNAUTHORIZED |
DAT_AUTH_FORBIDDEN | 403 | DAT_CMS_FORBIDDEN |
DAT_REQ_NOT_FOUND | 404 | DAT_CMS_ENDPOINT_NOT_FOUND |
DAT_REQ_*(其他) | 400·405·413 | DAT_CMS_HTTP_STATUS |
DAT_STORE_UNAVAILABLE | 503 | DAT_CMS_SERVER_ERROR |
DAT_STORE_UNKNOWN | 500 | DAT_CMS_SERVER_ERROR |
| (version 回退) | 200 | DAT_CMS_VERSION_RESET |
按症状查找
| 症状 | 错误码 |
|---|---|
| 刚登录时正常,过一会儿就被拒绝 | DAT_TOKEN_EXPIRED — 令牌到期了,重新签发即可 |
| 只有某台服务器验证失败 | DAT_CERT_NOT_SYNCED — 那台服务器还没收到新的 CID |
| 所有服务器都拒绝同一个令牌 | DAT_CERT_NOT_FOUND — 这是从未签发过的 CID |
| 签发服务器造不出令牌 | DAT_MANAGER_NO_ISSUABLE_CERTIFICATE + DAT_CERT_VERIFY_ONLY — 按 verify-only 分发了 |
| 只在刚启动时签发失败 | DAT_MANAGER_NO_CERTIFICATE — 还没完成首次同步,稍后会好 |
| CMS 同步持续失败 | DAT_CMS_UNAUTHORIZED — 令牌错了,重试也解决不了 |
| 一张证书都没收到 | DAT_CMS_ENDPOINT_NOT_FOUND — URL 写错了 |
| 只在某个平台上失败 | DAT_INTERNAL_UNAVAILABLE — 缺少密码后端 |
| 验证失败突然增多 | DAT_SIG_MISMATCH — 一次无害,但成批出现就是伪造尝试 |
| secure 解密突然失败 | DAT_CRYPTO_TAG_MISMATCH — 证书对不上,或者是篡改 |
| CMS 启动日志出现警告 | DAT_AUTH_DISABLED — 认证关着。 签发 API 是敞开的 |
附录
错误码语法
DAT_<领域>_<原因>- 同一原因出现在不同领域时,原因名是相同的。
DAT_TOKEN_MALFORMED和DAT_CERT_MALFORMED只是对象不同,含义一致。 _UNKNOWN是各领域的兜底专用,不会用作“未知算法”之类的其他含义(那是_UNSUPPORTED)。- 错误码字符串是公开契约。消息可以随意改,但码不改。
| 分类 | 码前缀 |
|---|---|
| 令牌 | DAT_TOKEN_ |
| 证书 | DAT_CERT_ |
| 签名 | DAT_SIG_ |
| 加密 | DAT_CRYPTO_ |
| 密钥 | DAT_KEY_ |
| 管理器 | DAT_MANAGER_ |
| 配置 | DAT_CONFIG_ |
| 内部 | DAT_INTERNAL_ |
| CMS 同步 | DAT_CMS_ |
| 服务器 | DAT_AUTH_ · DAT_REQ_ · DAT_STORE_ |
各客户端的取用方式
| 客户端 | 错误类型 | 错误码 | 重试分类 | 安全事件 |
|---|---|---|---|---|
| Rust | DatError enum | err.code() | err.retry() | err.security_event() |
| Go | *dat.Error | err.Code | dat.Retry(err) | dat.SecurityEvent(err) |
| JavaScript | DatError extends Error | e.code | e.retry | e.securityEvent |
| Python | DatError(ValueError, RuntimeError) | e.code | e.retry | e.security_event |
| Ruby | Saro::Dat::Error | e.code | e.retry | e.security_event? |
| Java/Kotlin | DatException | e.code | e.retry | e.securityEvent |
| C# | DatException | e.Code | e.Retry | e.SecurityEvent |
| C/C++ | dat_error_t | dat_error_code(e) | dat_error_retry(e) | dat_error_is_security_event(e) |
| CMS 服务器 | JSON 信封 | code 字段 | — | — |
安全事件 只在伪造、篡改已经确定的两种情况(DAT_SIG_MISMATCH、DAT_CRYPTO_TAG_MISMATCH)下返回 true。本文档的 可疑 标签范围更宽(还包括被篡改的令牌、密钥和请求),目前只是文档层面的分类,并未通过客户端 API 暴露。
影响 等级同样是文档层面的分类,因为同一个码在不同地方发生时打击并不相同 — 例如 DAT_KEY_INVALID 在过滤收到的令牌时没有影响,但如果是在 CMS 同步过程中读证书时出现,整个同步就会失败。
下层原因不会被丢弃。 DAT_MANAGER_NO_ISSUABLE_CERTIFICATE 和 DAT_CMS_IMPORT_FAILED 通过各语言的异常链(cause / __cause__ / InnerException / Unwrap())传递原因。
C/C++ 同时保留整数值
dat_error_t 原有的整数值为了 ABI 兼容而保留,但字符串码才是正本。库不再返回旧的值,因此 err == DAT_ERROR_INVALID_DAT 这样的比较不会匹配,请改用 dat_error_code(e) 对照。
C 没有异常链,因此原因需通过 dat_manager_issuable_cause() 单独获取。