Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,7 @@ name = "equivalence_performance"
harness = false
required-features = ["host"]

[[bench]]
name = "ed25519_strict"
harness = false

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ cargo run -p mithril-dwarf-harness --bin audit

### Intentional divergences

Five places where dwarf's observable behaviour differs from upstream (e.g. ed25519 `verify` vs `verify_strict`, BLS identity rejected at pairing time rather than at deserialise, asymmetric epoch-chaining) are documented and pin-tested in [`tests/intentional_divergences.rs`](mithril-dwarf-harness/tests/intentional_divergences.rs). Each is verdict-equivalent on real chains; a corpus-wide gate catches any future change that breaks that equivalence.
Four places where dwarf's observable behaviour differs from upstream (BLS identity rejected at pairing time rather than at deserialise, asymmetric epoch-chaining, usize-vs-u64 BLS scalar index width, bytewise NextAvk chain compare) are documented and pin-tested in [`tests/intentional_divergences.rs`](mithril-dwarf-harness/tests/intentional_divergences.rs). Each is verdict-equivalent on real chains; a corpus-wide gate catches any future change that breaks that equivalence.

### Upstream drift CI

Expand Down
46 changes: 46 additions & 0 deletions benches/ed25519_strict.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Host-side instruction-count comparison of `verify` (non-strict,
// cofactored) vs `verify_strict` (un-cofactored + small-order checks)
// on a representative legitimate Ed25519 signature. The genesis cert
// path in dwarf invokes this exactly once per chain.
//
// iai-callgrind reports x86_64 instruction counts; the *ratio* between
// the two paths is what carries over to the RISC0 RV32 cycle delta. The
// absolute zkVM cycle count must be confirmed downstream in `oaks_cert`
// with `--features guest-bench`.

use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use iai_callgrind::{library_benchmark, library_benchmark_group, main};
use std::hint::black_box;

const SEED: [u8; 32] = [42u8; 32];
const MSG: &[u8] = b"ed25519 strict-vs-legacy verify cycle pin";

fn fixture() -> (VerifyingKey, Signature) {
let sk = SigningKey::from_bytes(&SEED);
let vk = sk.verifying_key();
let sig = sk.sign(MSG);
(vk, sig)
}

#[library_benchmark]
#[bench::measure(fixture())]
fn bench_verify_legacy(input: (VerifyingKey, Signature)) {
let (vk, sig) = input;
let r = black_box(&vk).verify(black_box(MSG), black_box(&sig));
black_box(r).unwrap();
}

#[library_benchmark]
#[bench::measure(fixture())]
fn bench_verify_strict(input: (VerifyingKey, Signature)) {
let (vk, sig) = input;
let r = black_box(&vk).verify_strict(black_box(MSG), black_box(&sig));
black_box(r).unwrap();
}

library_benchmark_group!(
name = ed25519_benches;
benchmarks = bench_verify_legacy, bench_verify_strict
);

main!(library_benchmark_groups = ed25519_benches);
10 changes: 2 additions & 8 deletions mithril-dwarf-harness/src/checks_genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,8 @@ pub fn mithril_epoch_matches_protocol_message(cert: &Certificate) -> CheckResult
/// `MithrilCertificateVerifier::verify_genesis_certificate` does:
/// `ProtocolGenesisVerificationKey::verify(signed_message_bytes,
/// genesis_signature)`, which internally is
/// `ed25519_dalek::VerifyingKey::verify_strict`.
///
/// NOTE: dwarf's `verify_ed25519_signature` deliberately uses the
/// non-strict `.verify()` to save cycles inside the zkVM. The malleability
/// gap is a documented, intentional cost/security tradeoff — not a bug.
/// On a legitimately-signed corpus (which is what the test data is) the
/// two paths agree; only a constructed malleability twin would surface
/// the divergence.
/// `ed25519_dalek::VerifyingKey::verify_strict`. Dwarf now matches
/// (see `verify_ed25519_signature`).
pub fn mithril_ed25519_verify(
cert: &Certificate,
genesis_vk: &ProtocolGenesisVerificationKey,
Expand Down
21 changes: 4 additions & 17 deletions mithril-dwarf-harness/src/mutation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,26 +203,13 @@ pub enum Mutation {
// -----------------------------------------------------------------
// Intentional-divergence mutations — RESERVED.
//
// Reserved for KNOWN tradeoffs where dwarf accepts what upstream
// Reserved for known tradeoffs where dwarf accepts what upstream
// Mithril rejects, for cycle savings approved at design time. The
// harness counts such cases in a distinct
// `mutations_intentional_divergence` bucket rather than flagging
// them as CRITICAL false positives.
//
// No variants are currently defined — an earlier draft included
// `Ed25519MalleabilityTwin` (replace `s` with `s + L`) on the
// theory that dwarf's non-strict `vk.verify` would accept where
// upstream's `verify_strict` would reject. Empirically that does
// not happen: `ed25519-dalek` 2.1.1's non-strict `vk.verify` also
// routes through `Scalar::from_canonical_bytes(s)` which rejects
// any `s >= L`. The real `verify` vs `verify_strict` difference in
// dalek 2.x is the post-canonicality subgroup check on `R` (and
// `A`/the vk); constructing a twin that exploits that requires
// either a maliciously crafted public key (not our threat model —
// genesis VK is fixed) or breaking ed25519. So dwarf's non-strict
// verify is operationally equivalent to `verify_strict` for the
// mainnet genesis signature path. The bucket is preserved so a
// future genuine divergence can be added cleanly.
// them as CRITICAL false positives. No variants are currently
// defined; the bucket is preserved so a future genuine divergence
// can be added cleanly.
// -----------------------------------------------------------------
}

Expand Down
12 changes: 6 additions & 6 deletions mithril-dwarf-harness/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ pub struct ReportSummary {
/// actually adversarial. Fails the test.
pub mutations_insufficient: usize,
/// Mithril rejected, dwarf accepted — but the mutation is tagged
/// as a **known intentional divergence** (currently only
/// `Ed25519MalleabilityTwin`, reflecting dwarf's cycle-saving
/// non-strict ed25519 verify). Does NOT fail the test — the
/// counter exists so the cost-of-safety tradeoff is visible in
/// every harness run.
/// as a **known intentional divergence** approved at design time.
/// Does NOT fail the test; the counter exists so the cost-of-safety
/// tradeoff is visible in every harness run. No variants are
/// currently classified here (the `Mutation::is_intentional_divergence`
/// bucket is reserved for future use).
pub mutations_intentional_divergence: usize,
}

Expand Down Expand Up @@ -108,7 +108,7 @@ pub fn render_report(audits: &[CertAudit], mutated: &[CertAudit]) -> (String, Re
(security contract satisfied, surfaced for visibility)\n\
- INTENTIONAL DIVERGENCE Mithril rejected, dwarf accepted, BY DESIGN\n\
(cycle-saving tradeoff explicitly approved at\n\
design time, e.g. non-strict ed25519 verify)\n\n",
design time; no variants currently classified here)\n\n",
);

for a in mutated {
Expand Down
9 changes: 5 additions & 4 deletions mithril-dwarf-harness/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,11 @@ pub struct CertAudit {
pub full_verify: CheckComparison,
/// `true` if this audit corresponds to a mutation that is **known**
/// to produce a `(mithril_rejects, dwarf_accepts)` divergence by
/// design (current sole case: `Ed25519MalleabilityTwin` — dwarf's
/// cycle-saving non-strict ed25519 verify). The report + test
/// contract treat such outcomes as an **expected, documented**
/// divergence rather than a CRITICAL false positive.
/// design. The report + test contract treat such outcomes as an
/// **expected, documented** divergence rather than a CRITICAL
/// false positive. No mutation variants are currently classified
/// here; the field is preserved so future divergences can be
/// added cleanly.
pub mutation_intentionally_diverges: bool,
}

Expand Down
53 changes: 5 additions & 48 deletions mithril-dwarf-harness/tests/intentional_divergences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
//! | # | Divergence | Layer | Verdict-equivalent? |
//! |---|---------------------------------------------------------|-----------|---------------------|
//! | 1 | BLS identity-point defence layer | crypto | Yes (pinned) |
//! | 2 | Ed25519 non-strict verify | crypto | Yes (empirical) |
//! | 3 | `verify_epoch_chaining` direction asymmetry | check | Conditionally |
//! | 4 | Check ordering in `verify_standard_certificate` | orchestr. | Yes (top-level) |
//! | 5 | usize-vs-u64 BLS scalar index width on RISC0 | platform | Yes (BLS math) |
//! | 6 | NextAvk chain compare: bytewise vs structural | check | On real chains |
//!
//! Closed divergences (kept here for audit trail):
//! - #2 — Ed25519 non-strict verify. Aligned with upstream by switching
//! to `verify_strict` at the genesis-cert call site; measured cost
//! ~74k host cycles per chain (one call per chain, genesis-only).

use mithril_dwarf::certificate_verification::VerifyError;
use mithril_dwarf_harness::{
Expand Down Expand Up @@ -68,53 +72,6 @@ fn divergence_1_bls_identity_defence_layer_pinned() {
);
}

// Divergence #2 — Ed25519 non-strict verify (vs upstream's `verify_strict`)
//
// Dwarf calls `ed25519_dalek::VerifyingKey::verify` (mod.rs); upstream
// calls `verify_strict` via `ProtocolGenesisVerificationKey::verify`.
// `verify_strict` adds subgroup checks on `R` / `A`; the non-strict path
// skips them for cycle savings.
//
// Under `ed25519-dalek` 2.1.1 both paths route through
// `Scalar::from_canonical_bytes(s)`, which rejects any `s >= L`, so the
// malleability twin (`s + L`) is rejected by both — verdicts match. The
// pin catches a future dalek bump that loosens the non-strict path.

/// Pin: dalek 2.x rejects `s >= L` in both `verify` and `verify_strict`.
#[test]
fn divergence_2_ed25519_non_strict_pinned() {
use ed25519_dalek::{Signature, Verifier, VerifyingKey};

// Construct an arbitrary signature where s >= L. The Ed25519 group
// order L is `2^252 + 27742317777372353535851937790883648493`. Any
// 32-byte value with bit 254 set and lower bits high enough exceeds
// L. For the pin we use s = 2^255 - 1 (all-ones), which is well
// above L and is the canonical "out-of-range" representative.
let mut sig_bytes = [0u8; 64];
sig_bytes[..32].fill(0u8); // R = any 32 bytes (we don't care about the point validity here)
sig_bytes[32..].fill(0xFFu8); // s = 0xFF...FF, which is >= L
let sig = Signature::from_bytes(&sig_bytes);

// Point validity of `R` / `A` is irrelevant; the assertion turns
// entirely on the scalar canonicality check.
let vk_bytes: [u8; 32] = [
0xed, 0x4d, 0xc2, 0x46, 0x3a, 0x65, 0xa8, 0x70, 0x07, 0x4a, 0xd6, 0x6e, 0xa9, 0x66, 0x2a,
0x76, 0xee, 0xed, 0x5c, 0x4f, 0xfb, 0x73, 0xdc, 0x4d, 0x49, 0xb7, 0x80, 0x12, 0xfd, 0x42,
0xe6, 0x86,
];
let vk = VerifyingKey::from_bytes(&vk_bytes).expect("VK construction");
let msg = b"divergence-2-pin";

assert!(
vk.verify(msg, &sig).is_err(),
"non-strict ed25519 verify accepts s >= L — switch dwarf to verify_strict"
);
assert!(
vk.verify_strict(msg, &sig).is_err(),
"verify_strict accepts s >= L; investigate"
);
}

// Divergence #3 — `verify_epoch_chaining` direction asymmetry
//
// Dwarf's `verify_epoch_chaining` rejects when
Expand Down
29 changes: 24 additions & 5 deletions src/certificate_verification/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,16 @@ pub fn verify_certificate_chain(
Ok(())
}

/// Non-strict `verify`; `verify_strict` is empirically equivalent under
/// `ed25519-dalek` 2.x against malleability twins (both route through
/// `Scalar::from_canonical_bytes`). Pinned by the divergence registry.
/// Aligned with upstream `ProtocolGenesisVerificationKey::verify` —
/// `verify_strict` adds small-order checks on R / A and uses the
/// un-cofactored equation. Genesis-only path; +~49,800 RISC0 cycles
/// per chain (measured in `oaks_cert` with `--features guest-bench`).
fn verify_ed25519_signature(
message: &[u8],
signature: &[u8],
public_key: &[u8; 32],
) -> Result<(), VerifyError> {
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use ed25519_dalek::{Signature, VerifyingKey};

if signature.len() != 64 {
return Err(VerifyError::InvalidGenesisSignature);
Expand All @@ -181,7 +182,7 @@ fn verify_ed25519_signature(
let vk =
VerifyingKey::from_bytes(public_key).map_err(|_| VerifyError::InvalidGenesisSignature)?;

vk.verify(message, &sig)
vk.verify_strict(message, &sig)
.map_err(|_| VerifyError::Ed25519VerificationFailed)?;

Ok(())
Expand All @@ -195,4 +196,22 @@ mod tests {
fn verify_error_fits_in_4_bytes() {
assert!(core::mem::size_of::<VerifyError>() <= 4);
}

/// Regression pin: ed25519 path must reject a small-order public
/// key (identity point). `verify_strict` enforces this via
/// `is_small_order()`; legacy `verify` would accept a crafted
/// `(R=identity, s=0)` signature. Fails if the call site reverts to
/// non-strict.
#[test]
fn ed25519_rejects_small_order_public_key() {
let mut identity_vk = [0u8; 32];
identity_vk[0] = 0x01;
let mut sig_bytes = [0u8; 64];
sig_bytes[0] = 0x01;
assert!(matches!(
verify_ed25519_signature(b"small-order pin", &sig_bytes, &identity_vk),
Err(VerifyError::Ed25519VerificationFailed)
| Err(VerifyError::InvalidGenesisSignature)
));
}
}
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ pub mod certificate_verification;
pub mod parser;

#[cfg(feature = "host")]
pub use mithril_client::{CardanoTransactionsProofs, Client, ClientBuilder};
pub use mithril_client::{
AggregatorDiscoveryType, CardanoTransactionsProofs, Client, ClientBuilder,
GenesisVerificationKey,
};
#[cfg(feature = "host")]
pub use mithril_common::{
certificate_chain::{
Expand Down
Loading