Skip to content
ecdsa.com
C · Command line

NSS (Network Security Services)

NSS, Mozilla's crypto and TLS library behind Firefox and Thunderbird: two signing layers that disagree on the ECDSA encoding (DER through cryptohi, raw r‖s through PK11), P-256 to P-521 only, Ed25519 and ML-KEM.
License
MPL-2.0
Checked version
3.124 ·
Source
hg.mozilla.org/projects/nss
Documentation
firefox-source-docs.mozilla.org/security/nss/index.html

Compatibility at a glance

ECDSAEdDSAPost-quantum

ECDSA curves
P-256P-384P-521
EdDSA
Ed25519

Network Security Services is the library under Firefox, Thunderbird and several Linux system components, including Firefox's WebCrypto. It works through PKCS#11: keys live in a token (the built-in softoken, or a smart card or HSM), and everything above it calls the PK11_* functions. The facts here were checked on the Alpine 3.24.2 package, NSS 3.124, initialised without a key database (NSS_NoDB_Init).

ECDSA has two layers that produce different encodings. SEC_SignData and VFY_VerifyData in cryptohi hash the message and use DER, the form X.509, CMS and OpenSSL expect. PK11_Sign and PK11_Verify take a digest and use the raw r‖s form, 64 bytes on P-256. Mixing them is the classic NSS mistake; convert with the DER ⇄ raw converter. PK11_Sign also accepted a 64-byte digest on P-256 and signed its first 32 bytes, so pass a digest that matches the curve.

Nonces are random — two signatures of the same message differed — and s is not normalized; a high-S signature verified. ECDSA with SHA-256 signed and verified on a P-384 key, so the hash is not tied to the curve. Failures are distinguishable: after VFY_VerifyData returns SECFailure, PORT_GetError() reports SEC_ERROR_BAD_DER for a raw or truncated signature and SEC_ERROR_BAD_SIGNATURE for a valid one over other data.

The softoken generated keys only on P-256, P-384 and P-521; P-224 and secp256k1 failed with SEC_ERROR_INVALID_KEY. Ed25519 works through CKM_EC_EDWARDS_KEY_PAIR_GEN and CKM_EDDSA; Ed448 does not. ML-KEM-768 encapsulation round-trips (PK11_Encapsulate, PK11_Decapsulate), while the headers declare ML-DSA mechanisms that the softoken in this build does not offer. Compare the KEM with the ML-KEM tool.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/nss/run.sh ran NSS (Network Security Services) 3.124 in the container image alpine:3.24.2 on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    der-or-raw

    SEC_SignData returned strict DER; PK11_Sign returned 64 bytes of raw r‖s on P-256.

    Recorded by the check
    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

    Two SEC_SignData signatures of one message were different.

    Recorded by the check
    fact ecdsa-nonce = random
    evidence ecdsa-nonce: two SEC_SignData signatures of one message are different
  • Low-S when signing

    either

    s is not normalized: high-S values appeared among 64 signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: signatures with s > n/2 among 64: yes
  • High-S when verifying

    accepts-high-s

    A DER signature with s replaced by n − s verified with VFY_VerifyData.

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
  • What the signing call hashes

    hashes-message-or-digest

    SEC_SignData hashes the message; PK11_Sign over SHA-256 of the message produced a signature that VFY_VerifyData accepted.

    Recorded by the check
    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
  • Digest longer than the curve order

    truncated

    PK11_Sign accepted a 64-byte digest on P-256; the signature verified over its first 32 bytes.

    Recorded by the check
    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
  • Hash bound to the curve

    none

    ECDSA with SHA-256 signed and verified on a P-384 key.

    Recorded by the check
    fact ecdsa-hash-curve-binding = none
    evidence ecdsa-hash-curve-binding: ECDSA with SHA-256 on a P-384 key: sign ok, verify ok
  • Malformed ECDSA signature

    returns-error

    VFY_VerifyData failed with SEC_ERROR_BAD_DER for raw r‖s and for 3 bytes, and with SEC_ERROR_BAD_SIGNATURE for a wrong message.

    Recorded by the check
    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 curves

    P-256P-384P-521

    P-256, P-384 and P-521; P-224 and secp256k1 key generation returned SEC_ERROR_INVALID_KEY.

    Recorded by the check
    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 signed, verified and produced identical signatures twice; Ed448 key generation returned SEC_ERROR_INVALID_KEY.

    Recorded by the check
    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
  • Post-quantum algorithms

    ML-KEM

    ML-KEM-768 encapsulation round-tripped with a 1088-byte ciphertext; ML-DSA-65 key generation returned SEC_ERROR_INVALID_ALGORITHM, and SLH-DSA is not offered by the softoken.

    Recorded by the check
    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

Usage

Generate a P-256 key in the softoken, sign with `SEC_SignData` and verify the DER signature with `VFY_VerifyData`.
#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;
}

The snippet harness runs every sample verbatim in the container image alpine:3.24.2, and the run fails unless the output matches.

Setup
apk add --no-cache -q build-base nss-dev=3.124-r0 >/dev/null
Command
cc -o usage usage.c $(pkg-config --cflags --libs nss) && ./usage
Output must match (regular expression)
DER tag: 0x30, verified: yes

Libraries