dalek-ed25519-verified/verification/extract.sh

101 lines
5 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# Regenerate the Lean model in gen/ from the Rust sources.
#
# SCOPE: field arithmetic + Edwards point arithmetic
# roots: crate::field, crate::backend::serial::u64::field,
# crate::backend::serial::curve_models, crate::edwards
# (same widening the reference solution used for its Tier-1 addition-law
# theorem; scalar-mul backends stay opaque — upstream Aeneas cannot
# translate them; they are modeled/axiomatized in
# gen/CurveField/FunsExternal.lean OUTSIDE every certificate's cone.
# decompress IS extracted since the phase-2 full lift — the source's
# step_2 uses the documented negate-then-conditional-assign rewrite).
#
# Rust --charon--> CurveField.llbc --aeneas--> gen/CurveField/*.lean
#
# The hand-written gen/CurveField/{TypesExternal,FunsExternal}.lean are NOT
# touched by regeneration (Aeneas only rewrites the *_Template variants).
# After regenerating, diff the templates against the hand-written files:
# diff gen/CurveField/FunsExternal_Template.lean gen/CurveField/FunsExternal.lean
#
P2-c: classify and pin the extraction boundary Aeneas emits a *_Template.lean naming everything the extracted code needs from outside itself — the extraction's own statement of its boundary. extract.sh has always said, in prose, "after regenerating, diff the template against the hand-written file". Prose is not a gate, and the diff cannot be one: the two files legitimately differ in almost every line, holes and Aeneas comments against real definitions and modeling policy. MEASURING FIRST CHANGED WHAT THIS ITEM SHOULD BE. The TODO offered two options — enforce the diff, or pin both files — and the answer turned out to be neither. Both files were ALREADY byte-pinned by Phase 0b. And two further things stand here: the generated Funs.lean imports the model and CALLS these externals, so the Lean compiler enforces their TYPES wherever the extracted code uses them; and the per-certificate exact cones catch any external that becomes, or stops being, an assumption anything depends on. What none of those three sees is the CLASSIFICATION: for each name the extraction asks for, whether this repository answers with an ASSUMPTION or with a PROOF. That is the tier-A/B claim the documents make in prose — the curve calls and the three curve types resolve to proven definitions rather than axioms, because gen/CurveField/Funs.lean opens `namespace curve25519_dalek` and so defines the very names Aeneas asks for. Nothing checked it. A regeneration that renamed one, or a model that quietly answered one with an axiom instead, would have left the documents claiming a proof where the repository had an assumption. Phase 0d recomputes the classification with model-correspondence.py (namespace-aware, so a definition inside a namespace counts under its full name) and requires equality with the committed MODEL-CORRESPONDENCE.txt. UNRESOLVED — the extraction asking for something nothing here provides — is a hard failure. dalek 43 MODEL 8 PROVEN 3 EXTRA anza 38 MODEL 0 PROVEN 4 EXTRA (no CurveSig crate) risc0 36 MODEL 8 PROVEN 4 EXTRA betrusted 35 MODEL 8 PROVEN 4 EXTRA selftest-correspondence.sh, five cases, negative-tested by disabling the comparison. The case that matters is 2: a PROVEN external answered by an axiom instead. No name changes anywhere, every byte pin still matches, and it compiles, because the signature is unchanged — before Phase 0d nothing in the button could tell. Trap recorded for whoever extends it: case 3 first deleted the PROVEN rows, which was VACUOUS on anza, since anza has none — it removed nothing, the table still matched, and the case passed while testing nothing. It now deletes the first row whatever its verdict AND asserts the file changed. extract.sh now points at the gate instead of asking a human to look. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
2026-07-31 15:53:31 +00:00
# That diff is a READING aid, not a gate — the two files legitimately differ in
# almost every line (the template holds holes and Aeneas's own comments; the
# model holds real definitions and the modeling policy). What IS enforced, by
# check.sh Phase 0d, is the classification: every name the template declares
# must be answered either by the hand-written model or by a real definition in
# the proven corpus, and which of the two must match MODEL-CORRESPONDENCE.txt.
# Regenerate that table with `python3 model-correspondence.py .` and commit the
# change deliberately — a proof silently becoming an assumption is exactly what
# the phase exists to stop.
#
# Usage: ./extract.sh
set -euo pipefail
source ~/aeneas-toolchain/env.sh
HERE="$(cd "$(dirname "$0")" && pwd)"
CRATE=~/GitClone/FormalVerification/sources/curve25519-dalek-source/curve25519-dalek
echo "[1/4] charon: Rust -> LLBC (field + curve_models + edwards + scalar [MERGED GEN])"
cd "$CRATE"
THE SIGNATURE APEX: the EdDSA verification equation, proven and audited `Proofs/SigApexSpec.lean`: - `verify_loop_full` — the extracted 32-byte comparison loop returns exactly the byte-equality of the two arrays (induction; axiom cone = exactly [propext, Classical.choice, Quot.sound]). - `verify_accepts_iff` — THE APEX: for a signature that parses, the extracted RustCrypto verifier accepts IFF the recomputed compressed point compress( [s]·B − [k]·A ) equals the signature's R byte-for-byte. The recomputation is grounded in the PROVEN curve model (every curve and scalar call is a certified definition); k is whatever scalar the SHA-512 oracle produces — the honest EdDSA acceptance criterion with the hash opaque. Boundary hygiene forced by the audit itself: - The public vartime_double_scalar_mul_basepoint dispatch pulled the AVX2 vector-backend axiom into the apex cone. Fixed at the build level: extract.sh pins RUSTFLAGS --cfg curve25519_dalek_backend="serial", so the SIMD arm compiles out; BackendKind has only Serial and get_selected_backend becomes a real definition (ok Serial). - subtle.Choice.unwrap_u8 upgraded from axiom to the documented model definition (Choice := U8; unwrap_u8 = self.0) — it sits on the verify path via compress → is_negative. - CurveSig modules added to GEN_MODULES (stale-olean incoherence otherwise). check.sh grows Phase 3b: the apex certificate's axiom cone must equal EXACTLY [propext, Classical.choice, Quot.sound, ed25519.Signature, sha2.Sha512, sha512_new, sha512_update, sha512_finalize_bytes, ed25519.Signature.to_bytes, signature.error.Error, Error.new] — the SHA-512 hash oracle plus the opaque wire-format types. NO curve axioms, NO scalar axioms, NO backend axioms, enforced on every button press. Full check.sh green: 16 standard certificates + the apex audit. Phase 2 (the point-level equation [s]B − [k]A = decompress R, needing to_bytes canonicity and decompress) remains deferred and documented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:45:55 +00:00
# Force the portable SERIAL backend (the one we verify): the SIMD dispatch
# arm is `#[cfg(curve25519_dalek_backend = "simd")]`, so pinning the cfg to
# "serial" removes it from the extraction — no vector-backend axiom leaks in.
export RUSTFLAGS='--cfg curve25519_dalek_backend="serial"'
charon cargo --preset=aeneas \
--start-from crate::field \
--start-from crate::backend::serial::u64::field \
--start-from crate::backend::serial::curve_models \
--start-from crate::edwards \
Merge scalar into CurveField; integrate the verify glue against the model Gen merge: extract.sh now co-extracts the Scalar52 backend and the public scalar::from_bytes_mod_order[_wide] conversions into the SAME CurveField model, so the whole library — field, curve_models, edwards, scalar — shares one type universe (Scalar is a single structure, not two). The scalar proof chain repoints by one import line (ScalarDenote: CurveScalar.Funs -> CurveField.Funs); check-scalar.sh's gen list follows. Both buttons — the scalar certificates and the field/group/dsm certificates — pass fresh over the merged gen, so the merge is proven-safe, not merely hoped-safe. Verify glue (gen/CurveSig): the extracted ed25519-dalek verify_sha512 path, integrated against the proven model: - TypesExternal.lean imports CurveField.Types, so CompressedEdwardsY / EdwardsPoint / Scalar in the glue ARE the proven model's types. Only the genuinely foreign types stay opaque: sha2.Sha512, ed25519.Signature, signature.error.Error. - FunsExternal.lean imports CurveField.Funs, so every curve/scalar call (compress, vartime_double_scalar_mul_basepoint, as_bytes, neg, from_bytes_mod_order[_wide]) resolves to a proven definition — no axioms. The `?`-operator plumbing (Try::branch, FromResidual::from_residual) and compressed_from_bytes get real definitions. Only the SHA-512 hasher (sha512_new/update/finalize_bytes) and two opaque wire accessors (Signature.to_bytes, Error.new) remain axiomatized — the deliberate, documented hash-oracle boundary. Audited: `verify_sha512`'s entire axiom cone is [propext, Classical.choice, Quot.sound, sha2.Sha512, sha512_new, sha512_update, sha512_finalize_bytes, ed25519.Signature.to_bytes, signature.error.Error.new] — zero curve axioms, zero scalar axioms. The verify path is definitionally grounded in the certified model; the only trust boundary is SHA-512. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:13:58 +00:00
--start-from 'crate::backend::serial::u64::scalar::_::add' \
--start-from 'crate::backend::serial::u64::scalar::_::sub' \
--start-from 'crate::backend::serial::u64::scalar::_::mul' \
--start-from 'crate::backend::serial::u64::scalar::_::square' \
--start-from 'crate::backend::serial::u64::scalar::_::montgomery_mul' \
--start-from 'crate::backend::serial::u64::scalar::_::montgomery_square' \
--start-from 'crate::backend::serial::u64::scalar::_::montgomery_reduce' \
--start-from 'crate::backend::serial::u64::scalar::_::montgomery_invert' \
--start-from 'crate::backend::serial::u64::scalar::_::as_montgomery' \
--start-from 'crate::backend::serial::u64::scalar::_::from_montgomery' \
--start-from 'crate::backend::serial::u64::scalar::_::from_bytes_wide' \
--start-from 'crate::scalar::_::from_bytes_mod_order' \
--start-from 'crate::scalar::_::from_bytes_mod_order_wide' \
--opaque 'crate::field::_::internal_invert_batch' \
--opaque 'crate::backend::serial::scalar_mul::variable_base' \
--opaque 'crate::backend::serial::scalar_mul::straus' \
--opaque 'crate::backend::serial::scalar_mul::precomputed_straus' \
--opaque 'crate::backend::serial::scalar_mul::pippenger' \
--opaque 'crate::backend::vector' \
--opaque 'crate::backend::get_selected_backend' \
--opaque 'crate::edwards::_::sum' \
--opaque 'crate::edwards::_::from_slice' \
--dest-file "$HERE/CurveField.llbc" \
-- --no-default-features
echo "[2/4] aeneas: LLBC -> Lean (split files, CurveField.* modules)"
cd "$HERE"
aeneas -backend lean -split-files -subdir CurveField -dest gen CurveField.llbc
echo "[3/4] charon: ed25519-dalek verify glue -> LLBC (SHA-512 opaque)"
SIGCRATE="$(dirname "$CRATE")/ed25519-dalek"
cd "$SIGCRATE"
charon cargo --preset=aeneas \
--start-from 'crate::verifying::verify_sha512' \
--start-from 'crate::verifying::recompute_r_sha512' \
--opaque 'crate::verifying::sha512_new' \
--opaque 'crate::verifying::sha512_update' \
--opaque 'crate::verifying::sha512_finalize_bytes' \
--opaque 'crate::signature::compressed_from_bytes' \
--opaque 'curve25519_dalek' \
--opaque 'sha2' --opaque 'digest' --opaque 'ed25519' \
--opaque 'signature' --opaque 'subtle' --opaque 'zeroize' \
--exclude 'hybrid_array' --exclude 'typenum' \
--hide-marker-traits \
--dest-file "$HERE/CurveSig.llbc"
echo "[4/4] aeneas: LLBC -> Lean (CurveSig.* modules; hand-maintained"
echo " TypesExternal.lean / FunsExternal.lean are NOT overwritten)"
cd "$HERE"
aeneas -backend lean -split-files -subdir CurveSig -dest gen CurveSig.llbc
echo "Done. Now run ./check.sh to type-check the regenerated model."