Skip to content

uci: union P-256 scratch with cert_buf (+1,312 B), with lifetime proof (L2+L3) - #292

Merged
JC-000 merged 4 commits into
masterfrom
layout/l3-union-lifetime-test
Oct 12, 2026
Merged

JC-000 merged 4 commits into
masterfrom
layout/l3-union-lifetime-test

Conversation

@JC-000

@JC-000 JC-000 commented Oct 12, 2026

Copy link
Copy Markdown
Owner

Layout passes L2 + L3.

  • L2: on UCI, LIB_NISTCURVES_P256_BSS (1,312 B) is unioned with cert_buf (P256_SCRATCH_UNION), as ip65 has done since feat(cfg): ip65 refit — un-break BACKEND=ip65 via cert_buf/verify-scratch union #68. NET_BSS_TAIL goes from 63 B to 1,375 B free on every UCI profile; this is the onchip product's home for the PQ hybrid glue. comb + TRUST_BUNDLE=1 is now a $(error) + ld65 error, because trust_bundle runs ecdsa_verify out of cert_buf (accepted trade-off; not a shipped combination).
  • L3: VICE proof of the union's lifetime in tools/test_ecdsa_kat_oracle.py [3k]-[3n]: all 2,048 B of cert_buf junk-seeded, tp_mode=TP_M_FIRST, green on both shipped store builds (onchip-store 6dcec600…, comb-store 3726693a…).

Merge order: before the P-384 retirement PR, which conflicts in the three UCI cfgs and will be rebased onto this.

Commits (messages verbatim; each carries its red-green evidence)

961dcb2 uci: union LIB_NISTCURVES_P256_BSS with cert_buf (P256_SCRATCH_UNION)

ip65 has time-shared cert_buf with the library's 1,312 B of verify
scratch since #68 (SCRATCH_UNION). UCI kept the scratch in NET_BSS_TAIL,
leaving that region 63 B free on every profile. The lifetime rule is the
same on both backends (src/der_decode.s): cert_buf is dead once
x509_extract_pubkey returns -- the key copy, the SAN check and the SPKI
pin / trust hook all run inside it -- and ecdsa_verify runs only after.

Every UCI cfg (uci, uci-onchip = c64-https-uci.cfg; comb = -onchip.cfg and
its cold twin) now pins CERT_BUF_BSS at the head of CRYPTO_OVERLAY ($4200)
and routes LIB_NISTCURVES_P256_BSS to a P256_SCRATCH_UNION area on top of
it. The area is CERT_BUF_SIZE ($0800) long, so P-256 BSS growth past
cert_buf is an ld65 memory-area overflow, and src/der_decode.s asserts
(lderror) that the area starts at cert_buf and is no larger than it.

The one consumer that breaks the rule: on comb the cold bank runs
trust_bundle FROM cert_buf, and trust_bundle calls ecdsa_verify, whose
scratch would overwrite the running code. comb + TRUST_BUNDLE=1 is now a
Makefile $(error), with an lderror backstop in trust_bundle.s that fails
the link if the bundle's code ever runs inside the union. TRUST_BUNDLE
stays available on uci / uci-onchip, where it is resident and reads the
TCP ring. test_trust_policy_6502.py drops its comb-bundle image (its
verify is a Python stub, so it could not see the overwrite).
test_cold_bank.py's cfg delta loses the cert_buf pin, which both comb
cfgs now carry.

NET_BSS_TAIL tail free, every UCI profile: 63 -> 1,375 B (+1,312);
demo 43 -> 1,355. No other region moves (tools/measure_margins.py --build).
ip65 PRGs byte-identical.

Red-green: P256_SCRATCH_UNION start $4300 -> lderror "must start at
cert_buf"; size $0900 -> lderror "larger than cert_buf"; size $0500 ->
"overflows memory area 'P256_SCRATCH_UNION' by 32 bytes"; the Makefile
guard removed, comb + TRUST_BUNDLE=1 -> lderror "trust_bundle runs inside
P256_SCRATCH_UNION". All restored: green.

e0aab4c test_ecdsa_kat_oracle: prove the cert_buf union's lifetime in VICE ([3k]-[3n])

P256_SCRATCH_UNION (previous commit) puts the library's verify scratch on
cert_buf. It is safe only while nothing reads cert_buf once the scratch
is live. This group drives the image's own path, not a staged struct:
leaf into cert_buf -> tls_cert_stream_finish (key copy + SAN check) ->
tls_handle_cert_verify (CertificateVerify hash + ecdsa_verify), on any
UCI build that streams the Certificate:

[3k] the P-256 scratch symbols lie inside cert_buf (union linked)
[3l] the leaf is accepted, key extracted
[3m] the server signature verifies AND the verify overwrote the leaf
(else the overlap was not exercised)
[3n] a signature over another transcript is refused

ip65 runs no extra checks (its union is asserted in src/net/ip65/exports.s).

Mutation (onchip, C64_VICE_NO_REU=1), mutant M1 = defer the SAN check
from Certificate time to after the signature (a cert_buf read with the
scratch live):
union layout, unmutated 4298aecb... [3m] PASS C=0, 157/160 overwritten
union layout, M1 683faac8... [3m] FAIL C=1
master layout, M1 6a1fc605... [3m] C=0, 0/160 overwritten
So the red is the union's, not the mutant's. .o files and the PRG were
unlinked between builds, and the three hashes are distinct.

cea8459 test_ecdsa_kat_oracle: union group runs on the shipped TRUST_STORE products

Review finding (real defect): [3l]-[3n] failed on BOTH shipped products.
Under TRUST_STORE, x509_extract_pubkey tail-calls the trust hook
(cert_pin_check, src/tls_cert.s), which refuses while tp_mode is still
TP_M_NONE ("trust_pre never ran"). The earlier green run was on 4298aecb,
a plain uci-onchip image that ships nowhere.

  • set tp_mode = TP_M_FIRST (read from src/trust_policy.inc) when the
    label exists, as a first-use trust_pre would, before the extraction;
  • seed ALL 2,048 B of cert_buf with junk before staging the leaf, and
    re-seed it after the leaf is consumed and before each verify: every
    verify now runs over arbitrary bytes in its whole scratch span, which
    is the lifetime claim, not just over a 160 B leaf;
  • say in the docstring what this does NOT cover: the deframer, a real
    flight's interleaving, trust_pre/trust_post and the cold bank's calls,
    and everything after CertificateVerify (Finished, keys, the GET).

Green (union group, VICE):
uci-onchip-store 6dcec600... (shipped flags, C64_VICE_NO_REU=1)
4/4, [3m] C=0, 1,240 of 2,048 cert_buf bytes overwritten
uci-comb-store 3726693a... (shipped flags, REU)
4/4, [3m] C=0, 1,242 of 2,048 overwritten
Red:
tp_mode write removed, comb-store 3726693a...: [3l] FAIL C=1 -> (1, 3),
the reviewer's defect reproduced
mutant M1 (SAN check after the signature), uci-onchip 683faac8...:
[3m] FAIL C=1 -> (3, 1); still red under the junk seeding

8d9b7f9 test_ecdsa_kat_oracle: correct what the M1 mutant proves

CORRECTION to the docstring and to cea8459's commit message, which said
mutant M1 (the SAN check moved to after the signature) "turns [3m] red on
the union layout and leaves it green without it". That held for e0aab4c's
test; it is false since cea8459 re-seeds all of cert_buf with junk before
each verify. The deferred name check then reads junk on ANY layout, so M1
is red on master's layout too. Measured (uci-onchip, C64_VICE_NO_REU=1):

master layout + M1, this test 6a1fc605... [3k] FAIL, [3m] FAIL C=1
with 0 of 2048 overwritten

So M1's red shows only that a cert_buf read after the verify is caught.
What shows the union is exercised is [3k] (the scratch symbols lie inside
cert_buf) and [3m]'s changed > 0 (the verify really wrote over cert_buf):
on master's layout [3k] fails and [3m] reports 0 bytes overwritten. The
docstring now says so. Text only.

🤖 Generated with Claude Code

JC-000 and others added 4 commits October 7, 2026 18:37
ip65 has time-shared cert_buf with the library's 1,312 B of verify
scratch since #68 (SCRATCH_UNION). UCI kept the scratch in NET_BSS_TAIL,
leaving that region 63 B free on every profile. The lifetime rule is the
same on both backends (src/der_decode.s): cert_buf is dead once
x509_extract_pubkey returns -- the key copy, the SAN check and the SPKI
pin / trust hook all run inside it -- and ecdsa_verify runs only after.

Every UCI cfg (uci, uci-onchip = c64-https-uci.cfg; comb = -onchip.cfg and
its cold twin) now pins CERT_BUF_BSS at the head of CRYPTO_OVERLAY ($4200)
and routes LIB_NISTCURVES_P256_BSS to a P256_SCRATCH_UNION area on top of
it. The area is CERT_BUF_SIZE ($0800) long, so P-256 BSS growth past
cert_buf is an ld65 memory-area overflow, and src/der_decode.s asserts
(lderror) that the area starts at cert_buf and is no larger than it.

The one consumer that breaks the rule: on comb the cold bank runs
trust_bundle FROM cert_buf, and trust_bundle calls ecdsa_verify, whose
scratch would overwrite the running code. comb + TRUST_BUNDLE=1 is now a
Makefile $(error), with an lderror backstop in trust_bundle.s that fails
the link if the bundle's code ever runs inside the union. TRUST_BUNDLE
stays available on uci / uci-onchip, where it is resident and reads the
TCP ring. test_trust_policy_6502.py drops its comb-bundle image (its
verify is a Python stub, so it could not see the overwrite).
test_cold_bank.py's cfg delta loses the cert_buf pin, which both comb
cfgs now carry.

NET_BSS_TAIL tail free, every UCI profile: 63 -> 1,375 B (+1,312);
demo 43 -> 1,355. No other region moves (tools/measure_margins.py --build).
ip65 PRGs byte-identical.

Red-green: P256_SCRATCH_UNION start $4300 -> lderror "must start at
cert_buf"; size $0900 -> lderror "larger than cert_buf"; size $0500 ->
"overflows memory area 'P256_SCRATCH_UNION' by 32 bytes"; the Makefile
guard removed, comb + TRUST_BUNDLE=1 -> lderror "trust_bundle runs inside
P256_SCRATCH_UNION". All restored: green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…3k]-[3n])

P256_SCRATCH_UNION (previous commit) puts the library's verify scratch on
cert_buf. It is safe only while nothing reads cert_buf once the scratch
is live. This group drives the image's own path, not a staged struct:
leaf into cert_buf -> tls_cert_stream_finish (key copy + SAN check) ->
tls_handle_cert_verify (CertificateVerify hash + ecdsa_verify), on any
UCI build that streams the Certificate:

  [3k] the P-256 scratch symbols lie inside cert_buf (union linked)
  [3l] the leaf is accepted, key extracted
  [3m] the server signature verifies AND the verify overwrote the leaf
       (else the overlap was not exercised)
  [3n] a signature over another transcript is refused

ip65 runs no extra checks (its union is asserted in src/net/ip65/exports.s).

Mutation (onchip, C64_VICE_NO_REU=1), mutant M1 = defer the SAN check
from Certificate time to after the signature (a cert_buf read with the
scratch live):
  union layout, unmutated  4298aecb...  [3m] PASS C=0, 157/160 overwritten
  union layout, M1         683faac8...  [3m] FAIL C=1
  master layout, M1        6a1fc605...  [3m] C=0, 0/160 overwritten
So the red is the union's, not the mutant's. .o files and the PRG were
unlinked between builds, and the three hashes are distinct.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oducts

Review finding (real defect): [3l]-[3n] failed on BOTH shipped products.
Under TRUST_STORE, x509_extract_pubkey tail-calls the trust hook
(cert_pin_check, src/tls_cert.s), which refuses while tp_mode is still
TP_M_NONE ("trust_pre never ran"). The earlier green run was on 4298aecb,
a plain uci-onchip image that ships nowhere.

  - set tp_mode = TP_M_FIRST (read from src/trust_policy.inc) when the
    label exists, as a first-use trust_pre would, before the extraction;
  - seed ALL 2,048 B of cert_buf with junk before staging the leaf, and
    re-seed it after the leaf is consumed and before each verify: every
    verify now runs over arbitrary bytes in its whole scratch span, which
    is the lifetime claim, not just over a 160 B leaf;
  - say in the docstring what this does NOT cover: the deframer, a real
    flight's interleaving, trust_pre/trust_post and the cold bank's calls,
    and everything after CertificateVerify (Finished, keys, the GET).

Green (union group, VICE):
  uci-onchip-store 6dcec600... (shipped flags, C64_VICE_NO_REU=1)
    4/4, [3m] C=0, 1,240 of 2,048 cert_buf bytes overwritten
  uci-comb-store   3726693a... (shipped flags, REU)
    4/4, [3m] C=0, 1,242 of 2,048 overwritten
Red:
  tp_mode write removed, comb-store 3726693a...: [3l] FAIL C=1 -> (1, 3),
    the reviewer's defect reproduced
  mutant M1 (SAN check after the signature), uci-onchip 683faac8...:
    [3m] FAIL C=1 -> (3, 1); still red under the junk seeding

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CORRECTION to the docstring and to cea8459's commit message, which said
mutant M1 (the SAN check moved to after the signature) "turns [3m] red on
the union layout and leaves it green without it". That held for e0aab4c's
test; it is false since cea8459 re-seeds all of cert_buf with junk before
each verify. The deferred name check then reads junk on ANY layout, so M1
is red on master's layout too. Measured (uci-onchip, C64_VICE_NO_REU=1):

  master layout + M1, this test   6a1fc605...   [3k] FAIL, [3m] FAIL C=1
                                                with 0 of 2048 overwritten

So M1's red shows only that a cert_buf read after the verify is caught.
What shows the union is exercised is [3k] (the scratch symbols lie inside
cert_buf) and [3m]'s `changed > 0` (the verify really wrote over cert_buf):
on master's layout [3k] fails and [3m] reports 0 bytes overwritten. The
docstring now says so. Text only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 merged commit c566ea9 into master Oct 12, 2026
JC-000 added a commit that referenced this pull request Oct 12, 2026
The capture predated #292's P256_SCRATCH_UNION and #290's sha256 roll, so
its five PRG hashes and labels were the pre-rebase links. Regenerated with
test_rig_scratch.py --build --write-fixtures; --build re-run: matrix PASS,
17/17, hashes uci ed6871b2, uci-onchip eaa43a6a, uci-comb 8f8a0d05,
uci-comb-trust 936884e9, uci-comb-demo-trust b66eaf0c.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 deleted the layout/l3-union-lifetime-test branch October 14, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant