-
-
Notifications
You must be signed in to change notification settings - Fork 1
ecdsa p256_audit
Audit of the leviathan-crypto ECDSA-P256 implementation
(AssemblyScript) against FIPS 186-5 (Digital Signature Standard)
§6, ECDSA, the NIST P-256 curve parameters from SP 800-186
§3.2.1.3, P-256, RFC 6979 (Deterministic Usage of the Digital
Signature Algorithm) §3.2, and draft-irtf-cfrg-det-sigs-with-noise-05
§4, Hedged-Deterministic Nonce Generation. The strict-S
verification posture follows RFC 6979 §3.5 on both signer and
verifier and is verified against the NIST ACVP ECDSA-FIPS186-5
corpora (keyGen, sigGen, sigVer) plus the C2SP Wycheproof
ecdsa_secp256r1_sha256_p1363 corpus.
| Meta | Description |
|---|---|
| Target: |
leviathan-crypto WebAssembly implementation (AssemblyScript) |
| Spec: | FIPS 186-5 §6, ECDSA, plus SP 800-186 §3.2.1.3, P-256, RFC 6979 §3.2, and draft-irtf-cfrg-det-sigs-with-noise-05 §4 |
| Parameter sets: | P-256 + SHA-256 only (single combination) |
| Test vectors: | RFC 6979 §A.2.5 (P-256 + SHA-256), NIST ACVP ECDSA-FIPS186-5 keyGen / sigGen / sigVer (filtered to P-256 + SHA-256), C2SP Wycheproof ecdsa_secp256r1_sha256_p1363
|
| Source files: |
src/asm/p256/field.ts, src/asm/p256/scalar.ts, src/asm/p256/point.ts, src/asm/p256/scalar_mult.ts, src/asm/p256/rfc6979.ts, src/asm/p256/ecdsa.ts, src/asm/p256/sha256.ts, src/asm/p256/hmac_sha256.ts, src/ts/ecdsa/index.ts, src/ts/ecdsa/der.ts, src/ts/ecdsa/validate.ts, src/ts/sign/suites/ecdsa-p256.ts
|
Per FIPS 186-5 §6.4.4, ECDSA Signature Verification. The
verifier enforces the strict equation r ≡ x(u1*G + u2*Q) mod n
with the leviathan-crypto low-S strict-gate layered on top.
ACVP testPassed=false sigVer records and the Wycheproof
malleability corpus exercise these checks.
- Public-key canonicality.
pointDecompressrejects pk encodings whose prefix is not0x02or0x03, whose x bytes encode a non-canonical field element (x >= p), or whosey^2 = x^3 - 3x + bsquare root does not exist.ecdsaVerifyreturns 0 on decompression failure. Thex < pgate is the explicitfeIsCanonical(x)check inserted immediately afterfeFromBytesinpointDecompress(src/asm/p256/point.ts);feFromBytesis a literal byte loader and does not reduce, so without this gate an adversarial x in[p, 2^256)would silently reduce inside the curve-equationfeMul/feSqrcalls and (for the reducing-to-on-curve shape) produce a malleable second encoding for the same logical pk. The curve-equation residue check below the gate remains as defence-in-depth. Surfacing test:test/unit/p256/ecdsa_pk_canonicality.test.tsconstructs the four adversarial shapes (trivial overflow, x = p, x = x_valid + p, x = x_off_curve + p) and cross-checks that the rejection in case 3 fires atfeIsCanonical, not atpointOnCurve. - Identity-element pk rejection. A decompressed pk equal
to the curve identity
(0:1:0)is rejected before the signature equation evaluates. The identity check usespointEqualagainstpointZero. -
rin[1, n-1].scalarIsCanonicalplusscalarIsZerorejectr >= nandr == 0before the verification equation runs. -
sin[1, n-1]. Same check on the s component. - Low-S strict-gate,
s <= n/2.scalarIsHighSrejects any signature withs > n/2before the equation evaluates. See Low-S Enforcement for the spec provenance and the FIPS 186-5 §6.4.4 reconciliation. - Signature equation.
r ≡ x(u1*G + u2*Q) mod nevaluated withpointMulBaseforu1*G,pointMulforu2*Q,pointAddfor the combination, and an affine projection of the result. The x-coordinate is reduced mod n before the comparison. - Verify gate ordering. Decompress pk, reject identity
pk, check
r ∈ [1, n-1], checks ∈ [1, n-1], checks <= n/2, then evaluate the equation. Every early return wipes the mutable region before exiting and returns 0.
ECDSA has a signature-malleability surface that Ed25519 does
not. Any valid signature (r, s) has a counterpart (r, n - s)
that also verifies under the same (pk, msgHash). Protocols
that hash or compare signature bytes break under this
transformation.
- Signer normalises to low-S.
ecdsaSignandecdsaSignInternalPknormalises ← min(s, n - s)after the per-call signing equation per RFC 6979 §3.5. ThescalarIsHighSmask-driven branch selects the canonical half without leaking the original bit on a timing observer. - Verifier rejects high-S.
ecdsaVerifyrejectss > n/2before evaluating the signature equation, per the leviathan-crypto strict-gate posture. The gate ordering inecdsa.tsputs the high-S rejection between thes != 0 && s < ncanonical check and thepointMul / pointMulBasecalls. - Wycheproof corpus exercises the strict-gate. The
ecdsa_secp256r1_sha256_p1363corpus (test/vectors/ecdsa_p256_wycheproof.ts, 262 records) contains every malleability variant the upstream maintains plus the strict-gate'valid'/'invalid'discrimination. The corpus drivesEcdsaP256.verifydirectly throughtest/unit/sign/sign-ecdsa-p256-vectors.test.ts. - ACVP reconciliation documented. ACVP sigVer records
use FIPS 186-5 §6.4.4 verbatim (high-S accepted). The
strict-gate diverges; the test reconciles per-record
testPassedagainst the high-S check via BigInt comparison so the ACVP corpus passes under the leviathan-crypto-strict semantics. Seetest/unit/p256/ecdsa_verify.test.tsand vector_audit.md §ECDSA-P256.
Per RFC 6979 §3.2, Generation of k. The deterministic K
derivation runs (d, H(m)) through an HMAC-DRBG (SP 800-90A) to
produce k. ACVP supplies an explicit per-record k for sigGen,
so it cannot exercise this derivation; the RFC 6979 §A.2.5
test vectors (P-256 + SHA-256 over "sample" and "test") are
the only public KAT for the K derivation itself.
- RFC 6979 §A.2.5 reproduces expected k byte-for-byte.
deriveKDeterministic(d, H(m), kOut)for the two RFC(d, m)pairs writes the RFC-supplied k value tokOutexactly.test/unit/p256/rfc6979.test.tsis the gate. - HMAC-SHA-256 backbone matches CAVP. The embedded
HMAC-SHA-256 driving the K-DRBG is a verbatim port of
src/asm/sha2/hmac.ts; correctness against NIST CAVP HMAC vectors is inherited from the sha2 audit and re-verified end-to-end via the RFC 6979 §A.2.5 reproduction. - K-derivation rejection-sampling terminates. The K-DRBG
loop re-samples until
k ∈ [1, n-1]per RFC 6979 §3.2. The probability of rejection per iteration is below(n - 1) / 2^256, vanishingly small for P-256; expected iterations is essentially 1. The substrate caps the loop at a fixed iteration count and traps on the unreachable branch. - bits2int truncation. The hash-to-scalar reduction
follows RFC 6979 §2.3.2; for P-256 + SHA-256 the
qlen == hlen == 256equality makes truncation a no-op.scalarReducehandles the mod-n step. - bits2octets re-conversion. RFC 6979 §2.3.4's
bits2octetsround-trip is implicit in the embedded HMAC inputs; the test corpus reproduces the RFC §A.2.5 values which exercises the path end-to-end.
Per draft-irtf-cfrg-det-sigs-with-noise-05 §4,
Hedged-Deterministic Nonce Generation. The hedged path mixes
per-call entropy into the K-DRBG seed so a successful fault on
one signature does not transfer to the next.
- Hedged-by-default at the suite layer.
EcdsaP256Suite.signandEcdsaP256Suite.signPrehashedboth generaternd = randomBytes(32)per call and thread it through_signInternalPk. The suite never exposes a deterministic-K knob. - Per-call entropy is 32 bytes.
validateEntropyenforcesrnd.length === 32at the TS surface; the WASM substrate stages exactly 32 bytes fromRND_STAGE. The 32-byte size matches the draft's recommended Z size for P-256 (32 bytes for the 256-bit curve). - Domain separation from RFC 6979 §3.2. The hedged
branch with all-zero
rndis NOT byte-equivalent to the pure-deterministic RFC 6979 §3.2 path. The two K values differ by design (intentional domain separation per the draft §4);test/unit/p256/hmac_sha256.test.tsconfirms this with the all-zero-rnd vs deterministic mismatch. - Verifier reproduces hedged signatures.
test/unit/p256/ecdsa_signhedged.test.tssigns the same(d, H(m))twice with two distinct rnd values, confirms the resulting signatures differ byte-for-byte, and confirms both verify under the matching pk. The hedged path is the recommended default per the draft §1 motivation. - rnd wiped on every path. Suite-level rnd allocated
via
randomBytes(32)is wiped infinallyafter the signature operation; class-level rnd staged into the WASMRND_STAGEslot is wiped byioWipe(mx)plus the WASMwipeBuffersinfinally.
Per the RFC 6979 §3.2 attack surface analysis: a fault that biases the K-DRBG inputs (d or H(m)) leaks the long-term scalar through standard ECDSA-with-known-k recovery. The library defends by requiring the caller to ALSO know the encoded pk.
- Caller pk vs derived pk compare.
ecdsaSignre-derivespk_check = compress([d]G)after the signature equation and compares it against the caller-suppliedpkOffvia a constant-time byte comparison helper. Mismatch wipes the mutable region and traps viaunreachable. - TypeScript rethrow.
src/ts/ecdsa/index.ts'srethrowTraphelper catchesWebAssembly.RuntimeErrorand rethrows asSigningError('sig-malformed-input', ...)so callers can branch on the failure. - Suite-layer collapse documented.
EcdsaP256Suite.signroutes through_signInternalPk, which derives pk inside the same WASM call and skips the cross-check. At the suite call site the comparison would be between two outputs of the same potentially-faulted module on the same call, so the defence collapses to no defence. The skip saves one fixed-base scalar mult per sign. See ecdsa-p256.md §Fault-Injection Defense and architecture.md §Threat model. - No side effects on caller buffers. sk, pk, msgHash, rnd, and sig buffers passed in by the caller are read but NEVER mutated by either the TS layer or the WASM. The wrapper copies each into the I/O staging region.
The ECDSA-P256 K-derivation runs through
src/asm/p256/sha256.ts, a verbatim port of
src/asm/sha2/sha256.ts. Only three permitted deviations apply.
- Verbatim port from sha2. Source pin commit recorded
in the file header. Re-verify via
diff src/asm/sha2/sha256.ts src/asm/p256/sha256.ts, ignoring the buffer-offset import lines and the SHA-224 strip. - Permitted deviation 1: buffer-offset imports. The
offset imports point at
./buffers(p256 local memory layout). The offset constant NAMES (SHA256_H,SHA256_BLOCK,SHA256_W,SHA256_OUT,SHA256_INPUT,SHA256_PARTIAL,SHA256_TOTAL) are preserved so the algorithm code compiles unchanged. - Permitted deviation 2: variant strip. SHA-224 IVs and entry points are stripped. ECDSA-P256 + SHA-256 is the only consumer; SHA-224 is dead code for this module.
- Permitted deviation 3:
sha256UpdateByteshelper. A module-internal helper appended at the bottom of the file loopsmemory.copyplussha256Updatein 64-byte chunks for the RFC 6979 K-derivation hot path where input pieces live at arbitrary memory offsets. - ABI invisibility.
sha256Init,sha256Update,sha256Final, andsha256UpdateBytesare NOT re-exported fromsrc/asm/p256/index.ts. The p256 ABI surfaces nosha256*function; the embedded copy is module-internal.
The RFC 6979 §3.2 HMAC-DRBG runs through
src/asm/p256/hmac_sha256.ts, a verbatim port of
src/asm/sha2/hmac.ts. Two permitted deviations apply.
- Verbatim port from sha2. Source pin commit recorded
in the file header. Re-verify via
diff src/asm/sha2/hmac.ts src/asm/p256/hmac_sha256.ts, ignoring the buffer-offset import lines. - Permitted deviation 1: buffer-offset imports. The
offset imports point at
./buffers. Offset constant NAMES (HMAC256_IPAD,HMAC256_OPAD,HMAC256_INNER) are preserved. - Permitted deviation 2: variant strip. HMAC-SHA-384 and HMAC-SHA-512 are stripped. ECDSA-P256 + HMAC-SHA-256 is the only consumer.
- ABI invisibility.
hmac256Init,hmac256Update,hmac256Finalare NOT re-exported fromsrc/asm/p256/index.ts. The p256 ABI surfaces nohmac256*function. - HMAC-DRBG K / V state buffers.
HMAC_DRBG_K(32 B) andHMAC_DRBG_V(32 B) live in the p256 mutable region; both are zeroed bywipeBufferson every entry-point exit.
Per RFC 6979 §7, Security Considerations, plus the standard ECDSA implementation discipline. Every operation that consumes secret-bearing data (the scalar d, the per-call nonce k, the HMAC-DRBG K / V state, intermediate scalar-mult points) runs a fixed-length loop with mask-driven conditional selects.
- Scalar reduce binary division.
scalarReduceandscalarReduce64run a bit-by-bit binary division with a fixed-iteration loop. The mask-driven conditional subtract and constant-time compare helpers carry no branches on byte values. - Field arithmetic branch-free on secret data.
feAdd,feSub,feNeg,feMul,feSqr,feInv,feSqrt,feFromBytes,feToBytes,feIsZero,feIsEqual,feIsOdd,feCondSwap, andfeCondNeguse straight-line arithmetic plus mask-driven selects. No conditional jump reads a secret bit. - Point arithmetic branch-free.
pointAdd,pointDouble,pointSub,pointNegate, andpointEqualrun the Renes-Costello-Batina 2016 complete-addition formulas (Algorithm 4 add, Algorithm 6 double, specialised fora = -3). The formulas are complete: they correctly handle identity,P = Q, andP = -Qwithout branches. - Scalar-mult ladder fixed loop.
pointMulandpointMulBaserun a constant-time double-and-add-always ladder consuming the scalar MSB-first. Each bit drives onepointDoubleand one maskedpointAdd; the operation set per bit is independent of the scalar bit value. - No precomputed tables.
pointMulBaseruns the samepointMulvariable-base ladder used for arbitrary points, not a fixed-base comb table. Comb tables index a precomputed array by secret scalar bits, which is a lookup table even under constant-time masked-select discipline. The project's architectural commitment (SECURITY.md §Side-channel resistance, architecture.md §Cipher Triptych) is register-only logic with no data-dependent memory access across every primitive; P-256 holds the same line as Serpent, AES, ChaCha20, and the hash family. - Low-S branch is mask-driven. The signer's
scalarIsHighSplus conditionalscalarNegateselects the canonical s through a mask rather than a conditional jump. - Public-data branches documented.
ecdsaSignbranches onisAllZero32(rnd)to dispatch between deterministic and hedged K derivation;rndis caller-supplied with a public mode-selection role and the branch leaks the dispatcher choice, not any secret bits.ecdsaVerifyis wholly public; the substrate maintains constant-time for implementation simplicity.pointDecompressbranches on the prefix byte and curve-equation residue, both public.
Per AGENTS.md "Wipe discipline". Every ECDSA-P256 path zeroes secret-derived state on the way out.
- WASM wipe at end of every export.
ecdsaKeygen,ecdsaSign,ecdsaSignInternalPk, andecdsaVerifyeach end with awipeBuffers()call equivalent that zeroes the mutable region fromMUTABLE_START(4096) toBUFFER_END(7054), covering all scratch field elements, scratch points, scratch scalars, HMAC-DRBG K / V state, embedded SHA-256 streaming state, and the ECDSA fault-check buffers. - Early-failure wipes. Each
unreachable/return 0path inside the high-level exports calls the wipe first. Decompose failure, scalar non-canonical, high-S rejection, identity-pk rejection, and the pk-fault check all clear before exiting. - TypeScript I/O staging wipe. The TS wrapper's
ioWipe(mx)helper zeroes the staging region aboveBUFFER_END(SEED_STAGE,PK_STAGE,SIG_STAGE,MSG_HASH_STAGE,RND_STAGE) to the end of linear memory. Every public method'sfinallyrunsioWipe(mx)followed bymx.wipeBuffers(). - rnd wipe at the suite layer.
EcdsaP256Suite.sign/signPrehashedwipe the locally-allocated rnd buffer infinallyafter every signature operation, before theinst.dispose()call. The wipe runs on both the success path and on every throw path. -
dispose()idempotent.EcdsaP256.disposeruns both WASM and TS wipes inside atry / catch {}so multiple calls are safe even after the module instance has been torn down.
Per src/ts/sign/suites/ecdsa-p256.ts and the
signaturesuite.md ECDSA-P256 suite
section.
-
EcdsaP256Suite.formatEnumis0x02. Matches the catalog row in signaturesuite.md §Format byte allocation. -
formatNameis'ecdsa-p256'. -
ctxDomainis'ecdsa-p256-envelope-v3'. Built into the suite for display purposes only; never bound into the signature because the suite rejects non-empty user_ctx. - Suite rejects non-empty user_ctx. Every entry point
(
sign,verify,signPrehashed,verifyPrehashed) checksctx.length > 0and throwsSigningError('sig-ctx-unsupported')with an error message routing callers to the classical+PQ hybrid suites at0x22/0x23(reserved). - Suite routes through
_signInternalPk.EcdsaP256Suite.signandsignPrehashedcallinst._signInternalPk(sk, digest, rnd)rather thaninst.sign(sk, pk, digest, rnd). The fault-injection defence is documented as collapsing at the suite call site; see Fault-Injection Defense. - Prehash algorithm pinned to
'sha-256'.prehashSize: 32andprehashAlgorithm: 'sha-256'are constants. Message-takingsign(msg)andverify(msg)paths route throughsha256OneShot(msg)fromsrc/ts/sign/hasher.ts. TheSignStream/VerifyStreampaths usecreateRunningHash('sha-256'), which returns a buffered shim (sha256Buffered) over the one-shotSHA256class; chunks are copied and concatenated atfinalize()so the output is byte-identical to a one-shot SHA-256 over the full message. -
wasmModulesis['p256', 'sha2']. The frozen array tells consumers to initialise both modules. Tests verify the array contents and that both modules are required at init. - Per-call WASM lifecycle. Each suite method
instantiates a fresh
EcdsaP256inside atry / finally { dispose() }block. No suite-level long-lived instance is held. - Conforms to
StreamableSignatureSuite. ECDSA-P256 ALWAYS prehashes the message via SHA-256 (FIPS 186-5 §6.4 requirement), so streaming is well-defined. The type test intest/unit/sign/sign-ecdsa-p256-suites.test.tsconfirms the suite satisfiesStreamableSignatureSuiteat the TS type level. - Digest-length contract.
signPrehashedandverifyPrehashedboth throwSigningError('sig-malformed-input')on a wrong-length digest, symmetric with the AGENTS.md "verifyPrehashed digest-length contract" rule.
| Document | Description |
|---|---|
| ecdsa-p256 | ECDSA-P256 public API reference |
| asm_p256 | p256 WASM module reference |
| signaturesuite |
EcdsaP256Suite const, envelope wire format |
| vector_audit | Test-vector tier classification and Rust verifier coverage |
| audits | Project audit index |
| architecture | Repository structure, build and CI, WASM modules, public API, test suite, and security posture |
- Sign Tools
-
SignatureSuite
- format-byte catalog, hybrid composite encodings, custom suite contract
- Serpent-256 TypeScript | WASM
-
Serpent,SerpentCtr,SerpentCbc,SerpentGenerator
-
- ChaCha20 TypeScript | WASM
-
ChaCha20,Poly1305,ChaCha20Poly1305,XChaCha20Poly1305,ChaCha20Generator
-
- AES TypeScript | WASM
-
AES,AESCbc,AESCtr,AESGCM,AESGCMSIV,AESGenerator
-
- ML-DSA TypeScript | WASM
- pure (FIPS 204):
MlDsa44,MlDsa65,MlDsa87 - pure-mode suites:
MlDsa44Suite,MlDsa65Suite,MlDsa87Suite - prehash suites:
MlDsa44PreHashSuite,MlDsa65PreHashSuite,MlDsa87PreHashSuite
- pure (FIPS 204):
- SLH-DSA TypeScript | WASM
- pure (FIPS 205):
SlhDsa128f,SlhDsa192f,SlhDsa256f - pure-mode suites:
SlhDsa128fSuite,SlhDsa192fSuite,SlhDsa256fSuite - prehash suites:
SlhDsa128fPreHashSuite,SlhDsa192fPreHashSuite,SlhDsa256fPreHashSuite
- pure (FIPS 205):
- Ed25519 TypeScript | WASM
-
Ed25519(pure + Ed25519ph),Ed25519Suite,Ed25519PreHashSuite
-
- ECDSA-P256 TypeScript | WASM
-
EcdsaP256(hedged + RFC 6979),EcdsaP256Suite - DER codec:
ecdsaSignatureToDer,ecdsaSignatureFromDer,encodeEcPrivateKey,decodeEcPrivateKey,pointDecompress
-
- Hybrid composites PQ-only | Classical+PQ
- PQ-only:
MlDsa44SlhDsa128fSuite,MlDsa65SlhDsa192fSuite,MlDsa87SlhDsa256fSuite - Classical+PQ:
MlDsa44Ed25519Suite,MlDsa65Ed25519Suite,MlDsa44EcdsaP256Suite,MlDsa65EcdsaP256Suite
- PQ-only:
- X25519 TypeScript | WASM
-
X25519,KeyAgreementError(RFC 7748)
-
- ML-KEM TypeScript | WASM
-
MlKem512,MlKem768,MlKem1024
-
-
Ratchet (SPQR)
-
KDFChain,ratchetInit,kemRatchetEncap,kemRatchetDecap,RatchetKeypair,SkippedKeyStore
-
- Hashing overview
- SHA-2 TypeScript | WASM
-
SHA256,SHA384,SHA512,SHA224,SHA512_224,SHA512_256 -
HMAC_SHA256,HMAC_SHA384,HMAC_SHA512,HKDF_SHA256,HKDF_SHA512
-
- SHA-3 TypeScript | WASM
-
SHA3_224,SHA3_256,SHA3_384,SHA3_512,SHAKE128,SHAKE256
-
- BLAKE3 TypeScript | WASM
-
BLAKE3,BLAKE3Stream,BLAKE3KeyedHash,BLAKE3KeyedHashStream -
BLAKE3DeriveKey,BLAKE3DeriveKeyStream,BLAKE3OutputReader,BLAKE3Hash
-
-
KMAC
-
CSHAKE128,CSHAKE256,KMAC128,KMAC256,KMACXOF128,KMACXOF256
-
-
Merkle
-
MerkleVerifier,MerkleLog -
SignedLog,Sha256Tree,Blake3Tree,MemoryStorage
-
-
Fortuna CSPRNG
-
Fortuna,SerpentGenerator,ChaCha20Generator,AESGenerator,SHA256Hash,SHA3_256Hash,BLAKE3Hash
-
- Utils TypeScript | WASM
-
constantTimeEqual,randomBytes,wipe, encoding helpers
-
-
TypeScript interfaces
-
Hash,KeyedHash,Blockcipher,Streamcipher,AEAD,Generator,HashFn
-