chore(libs): bump nistcurves v0.6.0 -> v0.9.1 — both backends link, cfgs untouched - #94
Merged
Conversation
…gs untouched Takes the P-256 ECDSA verify library to upstream latest. The headline reason is security: v0.7.0 added a FIPS 186-5 §3.3 public-key validation gate (Qx,Qy in [0,p-1] plus on-curve Qy^2 = Qx^3 - 3Qx + b mod p, C=1 before any scalar multiplication). c64-https hands ecdsa_verify_256 a Q parsed straight out of an attacker-supplied certificate (src/tls_cert.s -> ecdsa_pubkey_x/y) and performs no range or on-curve check of its own, so this closes a real gap on our side. No cfg change. The v0.7.0/v0.8.0 CRYPTO_HOT overflow is gone: segment v0.6.0 v0.9.1 delta LIB_..._P256_CODE $1FB4 (8116) $2084 (8324) +208 FIPS gate LIB_..._P256_RODATA $01E0 (480) $00C0 (192) -288 dead vectors LIB_..._P256_BSS $0520 (1312) $0520 (1312) 0 UCI CRYPTO_HOT last byte used: $9FFE (1 B free) -> $9FAE (81 B free) The +208 reconciles the recorded failure exactly (1 - 208 = -207, the "overflows CRYPTO_HOT by 207 bytes" in CLAUDE.md); v0.9.0's deletion of 288 B of dead RFC 6979 self-test vectors from curve256.o covers it. The shelved remedy of routing LIB_NISTCURVES_P256_RODATA into CRYPTO_OVERLAY is therefore NOT adopted: the slot stays free for the three flags that contend for it, and tools/uci/_memory_policy.py sees an unchanged $4200-$5FFF. Fixes a latent silent-corruption trap in our own wrapper. v0.9.0 gave each archive a per-variant zp_config member, so nistcurves-p256-verify.a now ships zp_config_p256verify.o. build_nistcurves_p256.sh rebuilt a hardcoded zp_config.o with c64-https's ZP overrides and re-archived strictly by `ar65 t upstream.a`, so the override object would have been dropped without a word and the upstream default archived instead -- restoring zp_ptr2 = $fd (collides with zp_temp/zp_count during cert parsing) and fp_mul_i/j = $2c/$2d (inside the fe25519 claim $2c-$37). No link error; runtime corruption. Both wrappers now discover the member name, apply the matching upstream -D variant gate, hard-error on an absent/ambiguous/unknown name, and post-check the emitted object with od65. Adds a link-time version floor (.assert LIB_NISTCURVES_VERSION_MINOR >= 9) in src/crypto/ecdsa_verify.s so a silent downgrade below the validation gate is a loud ld65 error rather than a quietly reopened gap -- our KAT vectors all carry well-formed public keys, so the tests would not notice. Negative-tested: flipping it to >= 10 fails the link with no PRG. Costs zero PRG bytes and, from v0.9.0, zero ld65 warnings (the version equates export :abs). ABI-break audit, measured rather than inferred. v0.9.0 bumps LIB_ABI_VERSION 0 -> 1 and removes exports. Diffing every export of the staged archive at both pins against every import of c64-https's own objects: 31 symbols left the archive, and the intersection with what we import is EMPTY. Our entire consumed surface is four symbols -- ec_base_x, ec_gx256, ec_scalar_mul_var, ecdsa_verify_256. The removed zero-page slots are doubly moot: c64-https contains no .importzp directives at all and defines every slot locally. Also keeps `make p384-overlay` at its pre-existing blocker. The bump initially moved that failure earlier (the same per-variant manifest rename); with the member-discovery fix the chain again stops exactly at LIB_NISTCURVES_SHA384_- TABLES overflowing OVERLAY_REGION by 1536 bytes, unchanged. Evidence: make clean && make ip65 47,105 B d522e68469b8fc09709f34e64fd5573a0a72465349374a26257194bd5ed96cca make clean && make BACKEND=uci UCI 62,977 B 66e37037deb9b295d3594a50b2bacfe95d09bb7e62f3dfc401c51b03c2461fed tools/test_ecdsa_kat_oracle.py 6/6 tools/test_x509.py 11/11 tools/test_x25519.py 73/73 (control, unmoved) Plus the three non-default profiles link: uci+onchip, ip65+onchip, uci+onchip-comb. Not measured: wall-clock. Every timing figure in CLAUDE.md was taken at the v0.6.0 pin and is now labelled as such. Expected drift is a small regression (the gate adds 2 fp_cmp + 3 mod-p muls + 4 add/subs per verify, noise against a multi-second scalar mul); nothing else in v0.7.0-v0.9.1 touches a hot path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#92's §1 gate asserts `LIB_ABI_VERSION = 0`. nistcurves v0.9.0 bumped it to 1 (the same release that removed 31 exports), so merging the two PRs fires the gate by design: ld65: Error: src/lib_contract_asserts.s(135): libs/nistcurves: exported-surface generation changed (LIB_ABI_VERSION != 0) ... That is the assert working — a named error with file, line and the remedy, rather than a silent surface change. Flipped the expected value to 1 as the message instructs; the assert stays. Safety of the new generation is established by the link itself: an import of any of the 31 removed symbols would be an unresolved external. Both backends link, so we reference none of them. Verified after the flip: ip65 47,105 B sha d522e684…, uci 62,977 B, ECDSA KAT 6/6 including the three negative CAVP vectors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Takes
libs/nistcurvesfrom its v0.6.0 pin to v0.9.1 (upstream latest). This library is always-resident and provides the P-256 verify every TLS handshake depends on.Why this bump is worth real effort
v0.7.0 added a FIPS 186-5 §3.3 public-key validation gate:
Qx, Qy ∈ [0, p−1]plus the on-curve checkQy² ≡ Qx³ − 3·Qx + b (mod p), returningC=1before any scalar multiplication.That is directly load-bearing for us. The
Qhanded toecdsa_verify_256is parsed straight out of an attacker-supplied certificate —src/tls_cert.s→ecdsa_pubkey_x/ecdsa_pubkey_y→src/crypto/ecdsa_verify.s— and c64-https performs no range or on-curve check of its own. Before this pin, nothing in the tree checked that the certificate's public key was even a point on P-256.The CRYPTO_HOT overflow is gone, and no cfg change was needed
The recorded blocker was real: at v0.7.0/v0.8.0,
LIB_NISTCURVES_P256_RODATAoverflowedCRYPTO_HOTby 207 B. Measured at v0.9.1 frombuild/c64-https.map:LIB_NISTCURVES_P256_CODE$1FB4(8,116)$2084(8,324)LIB_NISTCURVES_P256_RODATA$01E0(480)$00C0(192)LIB_NISTCURVES_P256_BSS$0520(1,312)$0520(1,312)UCI
CRYPTO_HOTlast byte used:$9FFE(1 B free) →$9FAE(81 B free).The
+208reconciles the old failure exactly — 1 − 208 = −207, the "overflows CRYPTO_HOT by 207 bytes" recorded inCLAUDE.md. What changed is the other term: v0.9.0 (upstream #91) deleted 288 B of dead RFC 6979 self-test vectors fromcurve256.o, an object our archive ships.So the shelved remedy — routing
LIB_NISTCURVES_P256_RODATAintoCRYPTO_OVERLAY— is not adopted. Bothcfg/*.cfgare byte-for-byte untouched.CRYPTO_OVERLAYstays entirely free, the three mutually exclusive flags that contend for it (USE_X25519_SIBLING,EMBED_P256_OVERLAY,USE_OVERLAY_P384_EMBED) keep exactly the contention they had, andtools/uci/_memory_policy.pysees an unchanged$4200-$5FFF— nothing for the packaging lane to absorb.One caveat for the next bump: 81 B is the whole margin, and 288 B of it was a one-off recovery of dead weight that cannot be recovered twice.
Upstream's notes say 384 B of vectors removed; we measure 288 B because only
curve256.ois in our archive — the other 96 B arecurve384.o, P-384 only. Not a discrepancy.The defect this bump actually surfaced
v0.9.0 (upstream #90) gave each of the nine archives its own per-variant ZP object, so
nistcurves-p256-verify.anow shipszp_config_p256verify.o;zp_config.ono longer exists in it.tools/integration/build_nistcurves_p256.shstep 3 rebuilt a file hardcoded aszp_config.owith c64-https's ZP-slot overrides, and step 5 re-archives strictly byar65 t upstream.a. The override object would therefore have been silently dropped and the upstream-default member archived in its place, restoring exactly the collisions the overrides exist to prevent:zp_ptr2$3d→$fd, colliding with c64-httpszp_temp/zp_countduring certificate parsingfp_mul_i/fp_mul_j$39/$3a→$2c/$2d, inside the fe25519 claim$2c-$37There is no link error for this. It is runtime memory corruption surfacing several layers from its cause — the class of bug this repo has spent the week eliminating.
Both wrappers now discover the member name, apply the matching upstream
-Dvariant gate (it selects which slots get.exportzp), hard-error on an absent / ambiguous / unrecognised name, and post-check the emitted object withod65so the override is proven rather than assumed. Verified in the linked image viabuild/labels.txt:fp_mul_i=$39,fp_mul_j=$3A,zp_ptr2=$3D.ABI-break audit — measured, not inferred
v0.9.0 bumps
LIB_ABI_VERSION0 → 1 and removes exported symbols. Rather than infer safety from green tests, I dumped every export of the staged archive at both pins and every import of c64-https's own objects:zp_config_p256verify.o/precalc_manifest_p256verify.o)ec_base_x,ec_gx256,ec_scalar_mul_var,ecdsa_verify_256The removed zero-page slots (
proc_port,fp_loop, the fourpoly_*) are doubly moot: c64-https contains zero.importzpdirectives and defines every slot locally insrc/constants.inc/src/crypto/shared/zp_canon.inc.Merge note for Lane C: whichever of our two PRs lands second must change
src/lib_contract_asserts.s's §1 gate from.assert LIB_ABI_VERSION = 0to= 1. That is the gate doing its job — do not delete it. This PR does not contain or touch that file.Contract §4 attribute audit (v0.8.0 clause)
Checked v0.9.1's declared obligations against our
SEGMENTS{}lines in both cfgs:align = $100preserved onLIB_NISTCURVES_TABLESandLIB_NISTCURVES_SHA384_TABLES✅type = rwpreserved onLIB_NISTCURVES_P384_DATA_BSSand onLIB_NISTCURVES_P256_LIMLEE_BSS(onchip cfg) ✅type = bssonLIB_NISTCURVES_P256_BSSis safe and verified:libs/nistcurves/src/data_p256.sis 32.resdirectives with no.byte/.word, so there are no initialised bytes for the silentrw→bssdrop to lose ✅One benign deviation recorded in
CLAUDE.md: we declareLIB_NISTCURVES_BSSasbsswhere upstream declaresrw, anddata_shared.scarries a single.byte 0there. Inert for us — the wrapper dropsdata_shared.o— and zero-valued regardless.LIB_NISTCURVES_SHARED_PRIMITIVESis still$0007at v0.9.1 (andSHARED_CONSUMESis$0007too), so the wrapper'smul_8x8.o/data_shared.odrops remain correct and necessary. Lane C's caveat stands: that equate describes the upstream archive, and our member surgery is invisible to it.Version floor assert, negative-tested
src/crypto/ecdsa_verify.s— the file that consumes the library — now carries.assert LIB_NISTCURVES_VERSION_MINOR >= 9, lderror. A silent downgrade below the validation gate would otherwise pass every test we own, because all our KAT vectors carry well-formed public keys.Proven to fire, not just present. Flipping it to
>= 10:…and no PRG is produced. It costs zero PRG bytes (hashes identical with and without it) and, from v0.9.0, zero
ld65warnings — the version equates are exported:abs(upstream #95/#96).P-384 failure point held steady
The bump initially moved
make p384-overlay's failure earlier: the same per-variant rename turnedlib_manifest.ointolib_manifest_sha384.o, which the P-384 wrapper's hardcoded archive list could not find. Applied the same discover-by-name fix, and verified the chain again stops at the pre-existing, documented blocker, unchanged:No P-384 regression from this bump. P-384 remains unbuildable for its own reasons.
Evidence bar — 4/4
Control, expected to be unaffected and was:
The three non-default profiles also link (packaging ships two of them):
Sizes are region-padded, so identical size with a different hash is expected and is why only the PRG hash is quoted.
Explicitly not done
No hardware run, and no wall-clock re-measurement. Every timing figure in
CLAUDE.mdwas measured at the v0.6.0 pin; this PR labels them as such so nobody quotes them as v0.9.1 numbers. The expected drift is small but its sign is a regression, not a speedup: v0.7.0's gate adds 2fp_cmp+ 3 mod-p muls + 4 mod-p add/subs per verify (noise against a multi-second scalar multiplication), and nothing else in v0.7.0–v0.9.1 touches a hot path — the rest is manifest equates, dead-data removal and export hygiene. Re-runtools/uci/bench_ecdsa_u64e.pyon hardware to replace them.🤖 Generated with Claude Code