Repository navigation
feat(agreement)!: keys keep their public key and algorithm - #529
Conversation
Quest-Claim: 3a9d9a8e211756a5
PrivateKey computes its public key once at generate or load, and public_key() borrows it. PrivateKey and PublicKey report algorithm(), Algorithm is Eq, and PrivateKey's Debug shows the algorithm. rind-rustls and rind-aws-lc-rs drop the state they kept to work around the cost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fixed in d051e59: (Written by Claude Opus 5.5 with low effort) |
…ding `PublicKey` now holds a `&'static Algorithm`, whose function pointers Aeneas cannot translate, so `admin/prove` leaves `Algorithm` opaque and `Dispatch.lean` models it as which static it is, dispatching `generate` and `load` on it. P-256's random draw becomes a per-target name, modelled on aarch64 and left opaque on x86_64. The X25519 and P-256 `generate` and `from_scalar` theorems now also state the public key each key keeps, and `public_key` returns it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 9 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
There was a problem hiding this comment.
ℹ️ No critical issues — one wording/coverage nit in the proofs.
Reviewed changes
Reviewed the full 23-file diff end-to-end (Rust API, adapters, tests, and the Lean proof remodel), and verified locally: the admin/prove quote guard, cargo test -p rind --lib agreement, --test zeroing/--test debug with --features wipe, rind-aws-lc-rs (including --features wipe), rind-rustls --all-targets, clippy/fmt on the touched crates, and cargo check on the untouched consumers (rind-bench, rind-dart, rind-dimpl); the prove workflow is green on 3ffabe7.
agreement::PrivateKeykeeps its public key and algorithm, computed once at generate/load:public_key()returns&PublicKey,algorithm()is added to both key types,AlgorithmisEq, andto_pkcs8()plus the PKCS#8/SEC1 consistency checks reuse the stored public instead of a fresh base-point multiply.agreement::PublicKeyholds its algorithm, soto_spki()dispatches on it instead of inferring the curve from key length.- Adapters drop their caches: rind-rustls's
Activeborrows the key's own public key, and rind-aws-lc-rs drops itsalgorithmfields, mapping back from rind's algorithm viaAlgorithm::from_rind. - Wipe scope:
PrivateKey::dropwipes onlyself.private, andtests/zeroing.rsskips the cached public-key region — the maintainer-settled decision, matching AGENTS.md's "wipe nothing else". - Proofs:
agreement::Algorithmis modeled as a tag dispatchinggenerate/load, P-256's random draw moves behind per-targetP256Dispatch.lean,x25519Key/p256Keystate the public key each key holds, and the s2n-bignum base-point contracts move to*_generate/*_from_scalarwhere the computation now lives.
glm-5.3 | 𝕏
| point, as RFC 7748's 32 little-endian bytes. -/ | ||
| theorem x25519_public_key (h : Runs) (k : high.agreement.Keyed mid.x25519.PrivateKey) : | ||
| point, as RFC 7748's 32 little-endian bytes, which `x25519_generate` and | ||
| `x25519_from_scalar` show every key holds. -/ |
There was a problem hiding this comment.
The docstring's "every key" (and p256_public_key's in proofs/Rind/Aarch64/P256.lean:1152) overstates the coverage: only keys from generate and from_scalar are shown to hold their public key. On main, x25519_public_key took any Keyed and recomputed, so even a key returned by the opaque from_pkcs8 loader had its public_key() provably correct; on this branch nothing establishes x25519Key for such a key, since its stored public now comes from inside the opaque boundary. Consider rewording here (and qualifying the PR body's "no theorem is weaker than main's"), or keeping the public-key computation in translated code for the loader path.
Technical details
# Proof coverage of loader-derived keys' public key
## Affected sites
- `proofs/Rind/X86_64/X25519.lean:396` — docstring claims `x25519_generate`/`x25519_from_scalar` show "every key" holds its public key; only keys from those two entry points are covered.
- `proofs/Rind/Aarch64/P256.lean:1152-1154` — same wording on `p256_public_key`.
## Why coverage narrowed
On `main`, `x25519_public_key` took any `high.agreement.Keyed mid.x25519.PrivateKey` with no hypothesis, and `PrivateKey::public_key` recomputed via the translated `encode_public` — so the composed statement "whatever key the opaque `x25519_from_pkcs8` returns, `public_key()` is `rfcx25519(scalar, 9)`" was provable. On this branch `x25519_public_key` requires `x25519Key d key`, and nothing establishes that predicate for `from_pkcs8`-derived keys: the opaque `x25519_from_pkcs8` now also produces the stored `high.agreement.PublicKey` with no spec. (`p256_public_key` on `main` already required scalar-range hypotheses, so its loader-path coverage was hypothesis-gated before as well.)
## Required outcome
- Either the docstrings and PR body state the narrowed coverage accurately ("every key those loaders return"), or the loader path keeps the public-key computation in translated code so `x25519Key` stays derivable for it.
## Suggested approach (optional)
`x25519_from_pkcs8` could stay opaque but return only the key plus the v2 DER's carried public key (`Option<&[u8]>`), with `Agree::load` — which is translated on x86_64 — computing `key.encode_public(algorithm)` and the consistency check outside the opaque boundary, restoring the unconditional `public_key` guarantee for that path.
Summary
agreement::PrivateKeycomputes its public key once, when it is generated or loaded, and keeps it. The PKCS#8 and SEC1 loaders reuse that same computation for their consistency check, andto_pkcs8()reuses it instead of another base-point multiplication.PrivateKey::public_key()now returns&PublicKey(a borrow, no computation).PrivateKey::algorithm()andPublicKey::algorithm();AlgorithmisPartialEq/Eq.PrivateKey'sDebugprintsPrivateKey { algorithm: EcdhP256, .. }(the open follow-up from moq-dev/rind#383).PublicKeyholds its algorithm, soto_spki()no longer infers the curve from the length.Activedrops its cachedpub_keyand borrows the key's own. rind-aws-lc-rs drops thealgorithmfields fromPrivateKey,PublicKey, andParsedPublicKey, and maps back from rind's algorithm instead.PrivateKey's drop wipes only the private key.tests/zeroing.rsskips the cached public key's bytes for this type.admin/proveleavesagreement::Algorithmopaque, sincePublicKeynow reaches its function pointers, andproofs/Rind/X86_64/Dispatch.leanmodels it as which of the four statics it is, dispatchinggenerateandloadon it. P-256's random draw moves behind a per-targetP256Dispatch.lean: modelled on aarch64 as before, the translation's opaquenew_randomon x86_64. The X25519 and P-256generateandfrom_scalartheorems now also state the public key each key keeps (x25519Key,p256Key), andpublic_keyreturns it. No theorem is weaker thanmain's, and no axiom orsorryis added; the s2n-bignum base-point contracts move from*_public_keyto*_generateand*_from_scalarinAxioms.lean, since that is now where the public key is computed.Decisions
provecheck: ✅ fix the proofs in this PR, resized to M (rejected: land withprovefailing and plan a follow-up; storeAlgorithmIdinPublicKeyinstead). Reason:main's proofs pass and this PR is what breaks them.PrivateKey's drop: ✅ wipe onlyself.private, andtests/zeroing.rsskips the cached public-key region for this type (rejected: wipe the whole key asEd25519KeyPairdoes; wipe private only here and also changeEd25519KeyPair). This follows AGENTS.md's "wipe nothing else".Public API
Breaking:
agreement::PrivateKey::public_key()returns&PublicKeyinstead ofPublicKey. This is what lets rind-rustls hand rustls a borrowed&[u8]without its own copy. To migrate, add.clone()where an owned key outlives itsPrivateKey;key.public_key().as_ref()is unchanged. Additive:algorithm()on both key types,EqonAlgorithm. The adapters' public APIs are unchanged. No wire impact.Choices for review
EcdsaKeyPair::public_key()re-parsing its cached point) is dropped. The point is already cached; the re-parse is only the coordinate range and on-curve check, far below a microsecond next to a verification (~30 us). Skipping it would need an unchecked constructor on every curve'sVerifyingKey. That costs more than it saves.x25519_public_keyandp256_public_keynow take a key that satisfiesx25519Keyorp256Key, asgenerateandfrom_scalarprove every key they return does, instead of any curve key:public_keyonly reads the stored key, so its correctness is proven where the key is built.p256_public_keyno longer needs a scalar-range hypothesis.proofs/Rind/X86_64/P256Dispatch.leanexists only soDispatch.lean, whichadmin/provecopies to aarch64, can name P-256's random draw on both targets.Benchmarks (Apple M-series,
cargo bench -p rind-bench --bench x25519 --bench p256)A generated key still costs one base-point multiplication; each later
public_key()orto_pkcs8()is now free. The rustls handshake already cached, so its cost does not change.Follow-ups
Ed25519KeyPair(rind/src/high/signature.rs:1558) also wipes its public key, against the same rule. Recommendation: plan an XS quest to wipe only its secret parts and givetests/zeroing.rsthe same skip.Checks
just checkandnix develop .#prove --command admin/provepass.Models
Size: M
Implementer: Claude, claude-opus-5-5, unknown effort
Implementer: Claude Code, claude-opus-5-5, low effort (iterate, S)
Implementer: Claude Code, claude-opus-5-5, medium effort (iterate, M)
(Written by Claude Opus 5.5)
Review
Rewritten by
quest reviewafter each round.01a1c2df-aa52-7651-a3f1-3460ae164bef, round 1Code inspection found no actionable regressions and the change appears ready to land, though builds and tests were not run as instructed.