Skip to content

Commit 94002d0

Browse files
hyperpolymathclaude
andcommitted
feat(provers): close Phase 4 — 6 DL/probabilistic/B-method/comp-crypto backends
Add the six backends from `ECHIDNA-EXPANSION-TOMORROW`'s Phase 4 ("DL + probabilistic + comp-crypto"), completing the entire expansion roadmap. Each follows the dafny.rs / gnatprove.rs template with three-stage shape: format serializer (`to_<fmt>`), output parser (`parse_result`), 6+ unit tests. New backends, sub-tier, FFI u8, trust tier: - ELK (5h, 135, 3) — OWL 2 EL classifier - Konclude (5h, 136, 3) — full SROIQ DL reasoner - Storm (5i, 137, 2) — modern probabilistic model checker - ProB (5j, 138, 2) — B-method / Event-B model checker - EasyCrypt (5k, 139, 3) — game-based crypto proof assistant - CryptoVerif (5l, 140, 3) — automated computational crypto prover Composes into the four-corner crypto-verification matrix: symbolic-automatic → ProVerif symbolic-interactive → Tamarin computational-interactive → EasyCrypt computational-automatic → CryptoVerif Wired through: - ProverKind enum + factory dispatch - FromStr for CLI surface (multiple aliases each) - all() backend list - complexity / trust / implementation_time / default_executable tables - FFI kind_to_u8 / kind_from_u8 (135–140) - Phase 3 boundary test updated 134 → 140 40 new tests across 6 files; 972 lib tests passing (up from 932 at session start; 13 phase 3+4 tests above the 867-baseline pre-Phase-3). panic-attack assail clean (0 critical / 0 high / 0 tainted on all six). Phase 4 closes the expansion roadmap: with this and the prior Phase 3 commit (dec54bf), every 🟡 OPEN entry from `ECHIDNA-EXPANSION-TOMORROW-2026-04-28.md` is shipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dec54bf commit 94002d0

8 files changed

Lines changed: 1999 additions & 6 deletions

File tree

src/rust/ffi/mod.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,13 @@ pub fn kind_from_u8(kind: u8) -> Option<ProverKind> {
536536
132 => Some(ProverKind::IleanCoP),
537537
133 => Some(ProverKind::NanoCoP),
538538
134 => Some(ProverKind::MetTeL2),
539+
// Phase 4 (2026-04-27): DL + probabilistic + B-method + computational crypto.
540+
135 => Some(ProverKind::ELK),
541+
136 => Some(ProverKind::Konclude),
542+
137 => Some(ProverKind::Storm),
543+
138 => Some(ProverKind::ProB),
544+
139 => Some(ProverKind::EasyCrypt),
545+
140 => Some(ProverKind::CryptoVerif),
539546
_ => None,
540547
}
541548
}
@@ -1284,6 +1291,13 @@ pub fn kind_to_u8(kind: ProverKind) -> u8 {
12841291
ProverKind::IleanCoP => 132,
12851292
ProverKind::NanoCoP => 133,
12861293
ProverKind::MetTeL2 => 134,
1294+
// Phase 4 (2026-04-27): DL + probabilistic + B-method + computational crypto.
1295+
ProverKind::ELK => 135,
1296+
ProverKind::Konclude => 136,
1297+
ProverKind::Storm => 137,
1298+
ProverKind::ProB => 138,
1299+
ProverKind::EasyCrypt => 139,
1300+
ProverKind::CryptoVerif => 140,
12871301
}
12881302
}
12891303

@@ -1468,12 +1482,13 @@ mod tests {
14681482

14691483
#[test]
14701484
fn test_kind_from_u8_out_of_range() {
1471-
// 0–134 are valid; 135+ are out of range.
1472-
// (Boundary moved to 134 on 2026-04-27 adding Phase 3
1473-
// backends — KeYmaeraX (128), Qepcad (129), Redlog (130),
1474-
// MleanCoP (131), IleanCoP (132), NanoCoP (133), MetTeL2 (134).
1475-
// Previous boundary was 127 — Phase 1a/1b ATPs + Phase 5.)
1476-
assert!(kind_from_u8(135).is_none());
1485+
// 0–140 are valid; 141+ are out of range.
1486+
// (Boundary moved to 140 on 2026-04-27 adding Phase 4
1487+
// backends — ELK (135), Konclude (136), Storm (137),
1488+
// ProB (138), EasyCrypt (139), CryptoVerif (140).
1489+
// Previous boundary was 134 — Phase 3 connection-method /
1490+
// CAD / KeYmaeraX / MetTeL2.)
1491+
assert!(kind_from_u8(141).is_none());
14771492
assert!(kind_from_u8(200).is_none());
14781493
assert!(kind_from_u8(255).is_none());
14791494
}

src/rust/provers/cryptoverif.rs

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// SPDX-FileCopyrightText: 2026 ECHIDNA Project Team
2+
// SPDX-License-Identifier: PMPL-1.0-or-later
3+
4+
//! CryptoVerif backend — automatic prover for cryptographic protocols
5+
//! in the computational model.
6+
//!
7+
//! CryptoVerif is the **automatic** counterpart to EasyCrypt's
8+
//! interactive proof assistant: it produces concrete-security proofs
9+
//! by automated game-hopping using a built-in indistinguishability
10+
//! library. It was used to prove the formal security of TLS 1.3,
11+
//! Signal, WireGuard, and Kerberos.
12+
//!
13+
//! ## Why this backend exists
14+
//!
15+
//! EasyCrypt produces interactive proofs with concrete-security
16+
//! bounds. CryptoVerif produces *automated* proofs in the same model.
17+
//! Together they cover the computational-crypto verification space.
18+
//! ECHIDNA already has Tamarin and ProVerif (symbolic) — adding both
19+
//! computational tools gives us the four-corner crypto-verification
20+
//! matrix: symbolic-automatic (ProVerif), symbolic-interactive
21+
//! (Tamarin), computational-interactive (EasyCrypt),
22+
//! computational-automatic (CryptoVerif).
23+
//!
24+
//! ## Input format
25+
//!
26+
//! `.ocv` — CryptoVerif source (recommended modern syntax).
27+
//! `.cv` — legacy format (still supported by upstream).
28+
//!
29+
//! ## CLI invocation
30+
//!
31+
//! `cryptoverif -in <ocv-file> -out <result-dir>` for full automation.
32+
//! `cryptoverif -interactive` for hand-driven game-hopping.
33+
//!
34+
//! ## Output parsing
35+
//!
36+
//! CryptoVerif emits per-game status with a final verdict line:
37+
//!
38+
//! - `RESULT Proved <event-or-property>`
39+
//! - `RESULT Could not prove ...`
40+
//! - `Cannot prove ...` (failure mid-script)
41+
//!
42+
//! ## Integration tier
43+
//!
44+
//! Tier-5l / Phase-4. Trust tier 3 (small game-hopping engine,
45+
//! peer-reviewed, used for TLS 1.3 / Signal proofs). Complexity 4
46+
//! (probabilistic process calculi + indistinguishability library +
47+
//! cryptographic-game search).
48+
49+
#![allow(dead_code)]
50+
51+
use anyhow::{Context, Result};
52+
use async_trait::async_trait;
53+
use std::path::PathBuf;
54+
use std::process::Stdio;
55+
use tokio::io::AsyncWriteExt;
56+
use tokio::process::Command;
57+
58+
use super::{ProverBackend, ProverConfig, ProverKind};
59+
use crate::core::{Goal, ProofState, Tactic, TacticResult, Term};
60+
61+
/// CryptoVerif backend for automated computational crypto proofs.
62+
pub struct CryptoVerifBackend {
63+
config: ProverConfig,
64+
}
65+
66+
impl CryptoVerifBackend {
67+
pub fn new(config: ProverConfig) -> Self {
68+
CryptoVerifBackend { config }
69+
}
70+
71+
/// Render a `ProofState` into a minimal CryptoVerif `.ocv` file.
72+
///
73+
/// Each axiom is emitted as a `def` declaration; the goal is
74+
/// emitted as the `query` line that drives the prover.
75+
fn to_ocv(state: &ProofState) -> String {
76+
let mut ocv = String::new();
77+
ocv.push_str("(* SPDX-License-Identifier: PMPL-1.0-or-later *)\n");
78+
ocv.push_str("(* CryptoVerif input synthesised by ECHIDNA *)\n\n");
79+
for axiom in &state.context.axioms {
80+
ocv.push_str(&format!("def {}.\n", axiom));
81+
}
82+
if let Some(goal) = state.goals.first() {
83+
ocv.push_str(&format!("\nquery {}.\n", goal.target));
84+
}
85+
ocv.push_str("\nprocess 0\n");
86+
ocv
87+
}
88+
89+
/// Parse CryptoVerif's stdout to a boolean verdict.
90+
fn parse_result(output: &str) -> Result<bool> {
91+
let lower = output.to_ascii_lowercase();
92+
let positive = [
93+
"result proved",
94+
"all queries proved",
95+
"successfully proved",
96+
"[ok] proof complete",
97+
];
98+
let negative = [
99+
"could not prove",
100+
"cannot prove",
101+
"result not proved",
102+
"result rand_failure",
103+
"[error]",
104+
];
105+
if positive.iter().any(|m| lower.contains(m)) {
106+
return Ok(true);
107+
}
108+
if negative.iter().any(|m| lower.contains(m)) {
109+
return Ok(false);
110+
}
111+
Err(anyhow::anyhow!(
112+
"CryptoVerif output inconclusive: {}",
113+
output.lines().take(10).collect::<Vec<_>>().join("\n")
114+
))
115+
}
116+
}
117+
118+
#[async_trait]
119+
impl ProverBackend for CryptoVerifBackend {
120+
fn kind(&self) -> ProverKind {
121+
ProverKind::CryptoVerif
122+
}
123+
124+
async fn version(&self) -> Result<String> {
125+
let output = Command::new(&self.config.executable)
126+
.arg("-version")
127+
.output()
128+
.await
129+
.context("Failed to run cryptoverif -version")?;
130+
let stdout = String::from_utf8_lossy(&output.stdout);
131+
let stderr = String::from_utf8_lossy(&output.stderr);
132+
let version = if !stdout.is_empty() {
133+
stdout.lines().next().unwrap_or("cryptoverif").to_string()
134+
} else {
135+
stderr.lines().next().unwrap_or("cryptoverif").to_string()
136+
};
137+
Ok(version.trim().to_string())
138+
}
139+
140+
async fn parse_file(&self, path: PathBuf) -> Result<ProofState> {
141+
let content = super::bounded_read_proof_file(&path)
142+
.await
143+
.context("Failed to read .ocv file")?;
144+
let mut state = self.parse_string(&content).await?;
145+
state.metadata.insert(
146+
"source_path".to_string(),
147+
serde_json::Value::String(path.to_string_lossy().into_owned()),
148+
);
149+
Ok(state)
150+
}
151+
152+
async fn parse_string(&self, content: &str) -> Result<ProofState> {
153+
let mut state = ProofState::default();
154+
state.metadata.insert(
155+
"ocv_source".to_string(),
156+
serde_json::Value::String(content.to_string()),
157+
);
158+
for line in content.lines() {
159+
let trimmed = line.trim();
160+
if trimmed.is_empty() || trimmed.starts_with("(*") {
161+
continue;
162+
}
163+
if let Some(rest) = trimmed.strip_prefix("def ") {
164+
let body = rest.trim_end_matches('.').trim();
165+
if !body.is_empty() {
166+
state.context.axioms.push(body.to_string());
167+
}
168+
} else if let Some(rest) = trimmed.strip_prefix("query ") {
169+
let body = rest.trim_end_matches('.').trim();
170+
state.goals.push(Goal {
171+
id: format!("goal_{}", state.goals.len()),
172+
target: Term::Const(body.to_string()),
173+
hypotheses: vec![],
174+
});
175+
}
176+
}
177+
Ok(state)
178+
}
179+
180+
async fn apply_tactic(&self, _state: &ProofState, _tactic: &Tactic) -> Result<TacticResult> {
181+
Err(anyhow::anyhow!(
182+
"CryptoVerif is automation-first; interactive game-hopping is out of band"
183+
))
184+
}
185+
186+
async fn verify_proof(&self, state: &ProofState) -> Result<bool> {
187+
let ocv = Self::to_ocv(state);
188+
let mut child = Command::new(&self.config.executable)
189+
.arg("-in")
190+
.arg("/dev/stdin")
191+
.stdin(Stdio::piped())
192+
.stdout(Stdio::piped())
193+
.stderr(Stdio::piped())
194+
.spawn()
195+
.context("Failed to spawn cryptoverif")?;
196+
{
197+
let stdin = child
198+
.stdin
199+
.as_mut()
200+
.ok_or_else(|| anyhow::anyhow!("Failed to open cryptoverif stdin"))?;
201+
stdin
202+
.write_all(ocv.as_bytes())
203+
.await
204+
.context("Failed to write to cryptoverif stdin")?;
205+
}
206+
let output = child
207+
.wait_with_output()
208+
.await
209+
.context("Failed to wait for cryptoverif")?;
210+
let combined = format!(
211+
"{}\n{}",
212+
String::from_utf8_lossy(&output.stdout),
213+
String::from_utf8_lossy(&output.stderr)
214+
);
215+
Self::parse_result(&combined)
216+
}
217+
218+
async fn export(&self, state: &ProofState) -> Result<String> {
219+
Ok(Self::to_ocv(state))
220+
}
221+
222+
async fn suggest_tactics(
223+
&self,
224+
_state: &ProofState,
225+
_limit: usize,
226+
) -> Result<Vec<Tactic>> {
227+
Ok(vec![])
228+
}
229+
230+
async fn search_theorems(&self, _pattern: &str) -> Result<Vec<String>> {
231+
Ok(vec![])
232+
}
233+
234+
fn config(&self) -> &ProverConfig {
235+
&self.config
236+
}
237+
238+
fn set_config(&mut self, config: ProverConfig) {
239+
self.config = config;
240+
}
241+
}
242+
243+
#[cfg(test)]
244+
mod tests {
245+
use super::*;
246+
247+
#[test]
248+
fn test_cryptoverif_kind() {
249+
let config = ProverConfig::default();
250+
let backend = CryptoVerifBackend::new(config);
251+
assert_eq!(backend.kind(), ProverKind::CryptoVerif);
252+
}
253+
254+
#[test]
255+
fn test_cryptoverif_to_ocv_emits_query() {
256+
let mut state = ProofState::default();
257+
state.context.axioms.push("MAC_secure".to_string());
258+
state.goals.push(Goal {
259+
id: "goal_0".to_string(),
260+
target: Term::Const("event(authenticated(m)) ==> exists s. signed(s, m)".to_string()),
261+
hypotheses: vec![],
262+
});
263+
let ocv = CryptoVerifBackend::to_ocv(&state);
264+
assert!(ocv.contains("def MAC_secure"));
265+
assert!(ocv.contains("query event(authenticated(m))"));
266+
assert!(ocv.contains("process 0"));
267+
}
268+
269+
#[test]
270+
fn test_cryptoverif_parse_result_proved() {
271+
assert!(CryptoVerifBackend::parse_result("RESULT Proved authentication").expect("parse"));
272+
}
273+
274+
#[test]
275+
fn test_cryptoverif_parse_result_could_not_prove() {
276+
assert!(!CryptoVerifBackend::parse_result("RESULT Could not prove confidentiality")
277+
.expect("parse"));
278+
}
279+
280+
#[test]
281+
fn test_cryptoverif_parse_result_silence_errors() {
282+
assert!(CryptoVerifBackend::parse_result("warning: nothing").is_err());
283+
}
284+
285+
#[tokio::test]
286+
async fn test_cryptoverif_parse_string_extracts_def_and_query() {
287+
let config = ProverConfig::default();
288+
let backend = CryptoVerifBackend::new(config);
289+
let ocv = "def MAC_secure.\nquery event(auth(m)) ==> sender(m).\n";
290+
let state = backend.parse_string(ocv).await.expect("parse_string");
291+
assert_eq!(state.context.axioms.len(), 1);
292+
assert_eq!(state.goals.len(), 1);
293+
}
294+
}

0 commit comments

Comments
 (0)