跳到主要内容
ecdsa.com
C · 命令行

NSS (Network Security Services)

NSS,Mozilla 为 Firefox 和 Thunderbird 提供支撑的密码与 TLS 库:两个签名层的 ECDSA 编码不一致(cryptohi 使用 DER,PK11 使用原始 r‖s),仅支持 P-256 到 P-521,另有 Ed25519 和 ML-KEM。
许可证
MPL-2.0
检查的版本
3.124 ·
源代码
hg.mozilla.org/projects/nss
文档
firefox-source-docs.mozilla.org/security/nss/index.html

兼容性概览

ECDSAEdDSA后量子

ECDSA 曲线
P-256P-384P-521
EdDSA
Ed25519

Network Security Services 是 Firefox、Thunderbird 和多个 Linux 系统组件(包括 Firefox 的 WebCrypto)底层的库。它通过 PKCS#11 工作:密钥存放在令牌中(内置的 softoken,或者智能卡、HSM),上层的一切都调用 PK11_* 函数。本页的事实基于 Alpine 3.24.2 的软件包 NSS 3.124 检查得出,初始化时没有使用密钥数据库(NSS_NoDB_Init)。

ECDSA 有两个层,生成的编码不同。cryptohi 中的 SEC_SignData 和 VFY_VerifyData 会对消息计算哈希,并使用 DER,即 X.509、CMS 和 OpenSSL 所要求的格式。PK11_Sign 和 PK11_Verify 接收摘要,使用原始 r‖s 格式,在 P-256 上为 64 字节。混用两者是 NSS 的经典错误;可以用 DER ⇄ 原始格式转换器转换。PK11_Sign 在 P-256 上还接受了 64 字节的摘要,并对其前 32 字节签名,因此要传入与曲线匹配的摘要。

nonce 是随机的——同一条消息的两个签名不同——s 也没有经过规范化;high-S 签名能验证通过。使用 SHA-256 的 ECDSA 在 P-384 密钥上签名和验证均成功,因此哈希算法并不与曲线绑定。失败原因可以区分:VFY_VerifyData 返回 SECFailure 后,对于原始格式或被截断的签名,PORT_GetError() 报告 SEC_ERROR_BAD_DER;对于针对其他数据的有效签名,则报告 SEC_ERROR_BAD_SIGNATURE。

softoken 只能在 P-256、P-384 和 P-521 上生成密钥;P-224 和 secp256k1 报错 SEC_ERROR_INVALID_KEY。Ed25519 可以通过 CKM_EC_EDWARDS_KEY_PAIR_GEN 和 CKM_EDDSA 使用;Ed448 则不行。ML-KEM-768 封装能完成往返测试(PK11_Encapsulate、PK11_Decapsulate),而头文件中声明的 ML-DSA 机制,这个构建中的 softoken 并不提供。可以在 ML-KEM 工具中对照这个 KEM。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/nss/run.sh 于2026年10月20日在容器镜像 alpine:3.24.2 中运行了 NSS (Network Security Services) 3.124;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    der-or-raw

    SEC_SignData 返回严格的 DER;PK11_Sign 在 P-256 上返回 64 字节的原始 r‖s。

    检查记录
    fact ecdsa-signature-encoding = der-or-raw
    evidence ecdsa-signature-encoding: SEC_SignData output parses as strict DER: yes; PK11_Sign: 64 bytes (P-256 raw r||s is 64)
  • ECDSA 的 nonce

    random

    同一条消息的两个 SEC_SignData 签名不同。

    检查记录
    fact ecdsa-nonce = random
    evidence ecdsa-nonce: two SEC_SignData signatures of one message are different
  • 签名时的 low-S

    either

    s 没有经过规范化:64 个签名中出现了 high-S 值。

    检查记录
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: signatures with s > n/2 among 64: yes
  • 验证时的 high-S

    accepts-high-s

    把 s 替换为 n − s 的 DER 签名通过了 VFY_VerifyData 验证。

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
  • 签名调用对哪些内容做哈希

    hashes-message-or-digest

    SEC_SignData 会先计算消息的哈希;PK11_Sign 对消息的 SHA-256 签名,得到的签名被 VFY_VerifyData 接受。

    检查记录
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: SEC_SignData hashes the message; PK11_Sign over SHA-256(message) verified with VFY_VerifyData: yes
  • 摘要长于曲线的阶

    truncated

    PK11_Sign 在 P-256 上接受了 64 字节的摘要;签名针对其前 32 字节验证通过。

    检查记录
    fact ecdsa-long-input = truncated
    evidence ecdsa-long-input: PK11_Sign accepted a 64-byte digest; the signature verifies over its first 32 bytes: yes
  • 哈希与曲线绑定

    none

    使用 SHA-256 的 ECDSA 在 P-384 密钥上签名和验证均成功。

    检查记录
    fact ecdsa-hash-curve-binding = none
    evidence ecdsa-hash-curve-binding: ECDSA with SHA-256 on a P-384 key: sign ok, verify ok
  • 格式错误的 ECDSA 签名

    returns-error

    对于原始 r‖s 和 3 字节的数据,VFY_VerifyData 报错 SEC_ERROR_BAD_DER;对于错误的消息,则报错 SEC_ERROR_BAD_SIGNATURE。

    检查记录
    fact ecdsa-malformed-signature = returns-error
    evidence ecdsa-malformed-signature: VFY_VerifyData returned SECFailure with: raw r||s SEC_ERROR_BAD_DER, 3 bytes SEC_ERROR_BAD_DER, wrong message SEC_ERROR_BAD_SIGNATURE
  • ECDSA 曲线

    P-256P-384P-521

    P-256、P-384 和 P-521;生成 P-224 和 secp256k1 密钥时返回 SEC_ERROR_INVALID_KEY。

    检查记录
    fact ecdsa-curves = P-256, P-384, P-521
    evidence ecdsa-curves: P-224: key generation SEC_ERROR_INVALID_KEY
    evidence ecdsa-curves: secp256k1: key generation SEC_ERROR_INVALID_KEY
  • EdDSA

    Ed25519

    Ed25519 签名与验证成功,两次生成的签名完全相同;生成 Ed448 密钥时返回 SEC_ERROR_INVALID_KEY。

    检查记录
    fact eddsa = Ed25519
    evidence eddsa: Ed25519: keygen ok, sign and verify ok, deterministic yes
    evidence eddsa: Ed448 (OID 1.3.101.113): SEC_ERROR_INVALID_KEY
  • 后量子算法

    ML-KEM

    ML-KEM-768 封装完成往返测试,密文为 1088 字节;生成 ML-DSA-65 密钥时返回 SEC_ERROR_INVALID_ALGORITHM,softoken 也不提供 SLH-DSA。

    检查记录
    fact pqc = ML-KEM
    evidence pqc: ML-KEM-768: ciphertext 1088 bytes, shared secrets match: yes
    evidence pqc: ML-DSA-65 key generation failed: SEC_ERROR_INVALID_ALGORITHM; CKM_ML_DSA_KEY_PAIR_GEN in the internal slot: no
    evidence pqc: CKM_SLH_DSA in the internal slot: no

用法

在 softoken 中生成 P-256 密钥,用 `SEC_SignData` 签名,再用 `VFY_VerifyData` 验证这个 DER 签名。
#include <cryptohi.h>
#include <keyhi.h>
#include <nss.h>
#include <pk11pub.h>
#include <secoid.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    NSS_NoDB_Init(NULL);
    /* EC key generation takes the curve OID, DER-encoded, as its parameter. */
    SECOidData *curve = SECOID_FindOIDByTag(SEC_OID_ANSIX962_EC_PRIME256V1);
    unsigned char oid[18] = {0x06, (unsigned char)curve->oid.len};
    memcpy(oid + 2, curve->oid.data, curve->oid.len);
    SECItem params = {siBuffer, oid, curve->oid.len + 2};
    PK11SlotInfo *slot = PK11_GetInternalSlot();
    SECKEYPublicKey *pub = NULL;
    SECKEYPrivateKey *priv = PK11_GenerateKeyPair(slot, CKM_EC_KEY_PAIR_GEN, &params, &pub, PR_FALSE, PR_FALSE, NULL);

    /* SEC_SignData hashes the message and returns DER; PK11_Sign would return raw r||s over a digest. */
    unsigned char msg[] = "order #1042 approved";
    SECItem sig = {siBuffer, NULL, 0};
    SEC_SignData(&sig, msg, sizeof msg - 1, priv, SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE);
    SECStatus rv = VFY_VerifyData(msg, sizeof msg - 1, pub, &sig, SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE, NULL);
    printf("DER tag: 0x%02x, verified: %s\n", sig.data[0], rv == SECSuccess ? "yes" : "no");

    SECITEM_FreeItem(&sig, PR_FALSE);
    SECKEY_DestroyPrivateKey(priv);
    SECKEY_DestroyPublicKey(pub);
    PK11_FreeSlot(slot);
    return NSS_Shutdown() == SECSuccess ? 0 : 1;
}

代码片段测试工具会在容器镜像 alpine:3.24.2 中原样运行每个示例,只要输出不匹配,运行就会失败。

环境准备
apk add --no-cache -q build-base nss-dev=3.124-r0 >/dev/null
命令
cc -o usage usage.c $(pkg-config --cflags --libs nss) && ./usage
输出必须匹配(正则表达式)
DER tag: 0x30, verified: yes

库