mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-04 20:03:40 +00:00
warden quorum boundary: 4 provably-equivalent verifier members, live
- dogfood/quorum/verify-{dalek,anza,risc0,betrusted}: verify-only crates
built from the pinned proven source workspaces (serial backends pinned
per fork; anza entry is the certificate-covered verify_sha512, not the
default Zebra-lineage verify())
- src/pacta/quorum.py: unanimity-required acceptance, divergence
taxonomy (semantic-edge vs unexplained/tamper), small-order/canonicity
edge flags, per-member provenance sidecars with binary hashes
- live smoke: 4/4 members agree on accept and reject
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6cd08b771d
commit
bbc99a9127
10 changed files with 696 additions and 0 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -18,3 +18,7 @@ dogfood/state/
|
|||
dogfood/pacta-verified-verify/target/
|
||||
dogfood/pacta-verified-verify/Cargo.toml
|
||||
dogfood/pacta-verified-verify/Cargo.lock
|
||||
dogfood/quorum/*/Cargo.toml
|
||||
dogfood/quorum/*/Cargo.lock
|
||||
dogfood/quorum/*/target/
|
||||
dogfood/state/quorum/
|
||||
|
|
|
|||
13
dogfood/quorum/verify-anza/Cargo.toml.template
Normal file
13
dogfood/quorum/verify-anza/Cargo.toml.template
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Rendered by `pacta wallet build-quorum` - {{SOURCE}} is replaced with the
|
||||
# local checkout of the PINNED proven anza cryptography workspace.
|
||||
# Committed as a template so the repo never hardcodes a machine path.
|
||||
[package]
|
||||
name = "pacta-verify-anza"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
solana-ed25519 = { path = "{{SOURCE}}/curve25519/solana-ed25519" }
|
||||
|
||||
[workspace]
|
||||
78
dogfood/quorum/verify-anza/src/main.rs
Normal file
78
dogfood/quorum/verify-anza/src/main.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
//! warden quorum member: anza (Solana) solana-ed25519 verify path.
|
||||
//!
|
||||
//! Built against the PINNED proven source workspace, serial backend pinned
|
||||
//! (`--cfg curve25519_serial_only`). The entry point is `verify_sha512`
|
||||
//! (== `verify_dalek`) - the certificate-covered path - and deliberately
|
||||
//! NOT the crate's default `verify()`, whose Zebra-lineage semantics are
|
||||
//! outside this fork's proof boundary. This fork's accept() is strictly
|
||||
//! stricter than upstream's: it rejects A = 0 and a legacy list of
|
||||
//! excluded small-order R values, so a divergence against the dalek-family
|
||||
//! members on such inputs is a documented semantic edge, not tampering.
|
||||
//!
|
||||
//! Usage: <pubkey-hex-32B> <sig-hex-64B> <payload-file>
|
||||
//! stdout OK / INVALID; exit 0 = accept, 1 = reject, 2 = input error.
|
||||
|
||||
use curve25519::ed_sigs::{Signature, VerificationKey};
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
|
||||
if s.len() % 2 != 0 {
|
||||
return Err("odd-length hex".into());
|
||||
}
|
||||
(0..s.len() / 2)
|
||||
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(|e| e.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() != 4 {
|
||||
eprintln!("usage: {} <pubkey-hex> <sig-hex> <payload-file>", args[0]);
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
let pk_bytes = match hex_decode(&args[1]) {
|
||||
Ok(b) if b.len() == 32 => b,
|
||||
_ => {
|
||||
eprintln!("error: public key must be 32 bytes of hex");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let sig_bytes = match hex_decode(&args[2]) {
|
||||
Ok(b) if b.len() == 64 => b,
|
||||
_ => {
|
||||
eprintln!("error: signature must be 64 bytes of hex");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let payload = match std::fs::read(&args[3]) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("error: cannot read payload: {e}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let mut pk_array = [0u8; 32];
|
||||
pk_array.copy_from_slice(&pk_bytes);
|
||||
let verification_key = match VerificationKey::try_from(pk_array) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
// Undecodable key = REJECT verdict (same contract as the other
|
||||
// quorum members): all members must judge the same bytes.
|
||||
println!("INVALID");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
let mut sig_array = [0u8; 64];
|
||||
sig_array.copy_from_slice(&sig_bytes);
|
||||
let signature = Signature::from_bytes(&sig_array);
|
||||
match verification_key.verify_sha512(&signature, &payload) {
|
||||
Ok(()) => {
|
||||
println!("OK");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(_) => {
|
||||
println!("INVALID");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
dogfood/quorum/verify-betrusted/Cargo.toml.template
Normal file
13
dogfood/quorum/verify-betrusted/Cargo.toml.template
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Rendered by `pacta wallet build-quorum` - {{SOURCE}} is replaced with the
|
||||
# local checkout of the PINNED proven source workspace for this fork.
|
||||
# Committed as a template so the repo never hardcodes a machine path.
|
||||
[package]
|
||||
name = "pacta-verify-betrusted"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
ed25519-dalek = { path = "{{SOURCE}}/ed25519-dalek", default-features = false, features = ["fast", "zeroize"] }
|
||||
|
||||
[workspace]
|
||||
75
dogfood/quorum/verify-betrusted/src/main.rs
Normal file
75
dogfood/quorum/verify-betrusted/src/main.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
//! warden quorum member: betrusted curve25519-dalek fork verify path.
|
||||
//!
|
||||
//! Built against the PINNED proven source workspace (the commit named in
|
||||
//! the build provenance sidecar), serial backend pinned - the exact code
|
||||
//! path whose correctness certificates the transparency log carries for
|
||||
//! this fork. Verify-only on purpose: quorum members judge, they never
|
||||
//! sign.
|
||||
//!
|
||||
//! Usage: <pubkey-hex-32B> <sig-hex-64B> <payload-file>
|
||||
//! stdout OK / INVALID; exit 0 = accept, 1 = reject, 2 = input error.
|
||||
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
|
||||
if s.len() % 2 != 0 {
|
||||
return Err("odd-length hex".into());
|
||||
}
|
||||
(0..s.len() / 2)
|
||||
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(|e| e.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() != 4 {
|
||||
eprintln!("usage: {} <pubkey-hex> <sig-hex> <payload-file>", args[0]);
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
let pk_bytes = match hex_decode(&args[1]) {
|
||||
Ok(b) if b.len() == 32 => b,
|
||||
_ => {
|
||||
eprintln!("error: public key must be 32 bytes of hex");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let sig_bytes = match hex_decode(&args[2]) {
|
||||
Ok(b) if b.len() == 64 => b,
|
||||
_ => {
|
||||
eprintln!("error: signature must be 64 bytes of hex");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let payload = match std::fs::read(&args[3]) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("error: cannot read payload: {e}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let mut pk_array = [0u8; 32];
|
||||
pk_array.copy_from_slice(&pk_bytes);
|
||||
let verifying_key = match VerifyingKey::from_bytes(&pk_array) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
// A key that fails point decompression is a REJECT verdict, not
|
||||
// an input error: quorum members must all judge the same bytes.
|
||||
println!("INVALID");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
let mut sig_array = [0u8; 64];
|
||||
sig_array.copy_from_slice(&sig_bytes);
|
||||
let signature = Signature::from_bytes(&sig_array);
|
||||
match verifying_key.verify(&payload, &signature) {
|
||||
Ok(()) => {
|
||||
println!("OK");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(_) => {
|
||||
println!("INVALID");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
dogfood/quorum/verify-dalek/Cargo.toml.template
Normal file
13
dogfood/quorum/verify-dalek/Cargo.toml.template
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Rendered by `pacta wallet build-quorum` - {{SOURCE}} is replaced with the
|
||||
# local checkout of the PINNED proven source workspace for this fork.
|
||||
# Committed as a template so the repo never hardcodes a machine path.
|
||||
[package]
|
||||
name = "pacta-verify-dalek"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
ed25519-dalek = { path = "{{SOURCE}}/ed25519-dalek", default-features = false, features = ["fast", "zeroize"] }
|
||||
|
||||
[workspace]
|
||||
75
dogfood/quorum/verify-dalek/src/main.rs
Normal file
75
dogfood/quorum/verify-dalek/src/main.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
//! warden quorum member: upstream curve25519-dalek verify path.
|
||||
//!
|
||||
//! Built against the PINNED proven source workspace (the commit named in
|
||||
//! the build provenance sidecar), serial backend pinned - the exact code
|
||||
//! path whose correctness certificates the transparency log carries for
|
||||
//! this fork. Verify-only on purpose: quorum members judge, they never
|
||||
//! sign.
|
||||
//!
|
||||
//! Usage: <pubkey-hex-32B> <sig-hex-64B> <payload-file>
|
||||
//! stdout OK / INVALID; exit 0 = accept, 1 = reject, 2 = input error.
|
||||
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
|
||||
if s.len() % 2 != 0 {
|
||||
return Err("odd-length hex".into());
|
||||
}
|
||||
(0..s.len() / 2)
|
||||
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(|e| e.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() != 4 {
|
||||
eprintln!("usage: {} <pubkey-hex> <sig-hex> <payload-file>", args[0]);
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
let pk_bytes = match hex_decode(&args[1]) {
|
||||
Ok(b) if b.len() == 32 => b,
|
||||
_ => {
|
||||
eprintln!("error: public key must be 32 bytes of hex");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let sig_bytes = match hex_decode(&args[2]) {
|
||||
Ok(b) if b.len() == 64 => b,
|
||||
_ => {
|
||||
eprintln!("error: signature must be 64 bytes of hex");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let payload = match std::fs::read(&args[3]) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("error: cannot read payload: {e}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let mut pk_array = [0u8; 32];
|
||||
pk_array.copy_from_slice(&pk_bytes);
|
||||
let verifying_key = match VerifyingKey::from_bytes(&pk_array) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
// A key that fails point decompression is a REJECT verdict, not
|
||||
// an input error: quorum members must all judge the same bytes.
|
||||
println!("INVALID");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
let mut sig_array = [0u8; 64];
|
||||
sig_array.copy_from_slice(&sig_bytes);
|
||||
let signature = Signature::from_bytes(&sig_array);
|
||||
match verifying_key.verify(&payload, &signature) {
|
||||
Ok(()) => {
|
||||
println!("OK");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(_) => {
|
||||
println!("INVALID");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
dogfood/quorum/verify-risc0/Cargo.toml.template
Normal file
13
dogfood/quorum/verify-risc0/Cargo.toml.template
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Rendered by `pacta wallet build-quorum` - {{SOURCE}} is replaced with the
|
||||
# local checkout of the PINNED proven source workspace for this fork.
|
||||
# Committed as a template so the repo never hardcodes a machine path.
|
||||
[package]
|
||||
name = "pacta-verify-risc0"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
ed25519-dalek = { path = "{{SOURCE}}/ed25519-dalek", default-features = false, features = ["fast", "zeroize"] }
|
||||
|
||||
[workspace]
|
||||
75
dogfood/quorum/verify-risc0/src/main.rs
Normal file
75
dogfood/quorum/verify-risc0/src/main.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
//! warden quorum member: risc0 curve25519-dalek fork verify path.
|
||||
//!
|
||||
//! Built against the PINNED proven source workspace (the commit named in
|
||||
//! the build provenance sidecar), serial backend pinned - the exact code
|
||||
//! path whose correctness certificates the transparency log carries for
|
||||
//! this fork. Verify-only on purpose: quorum members judge, they never
|
||||
//! sign.
|
||||
//!
|
||||
//! Usage: <pubkey-hex-32B> <sig-hex-64B> <payload-file>
|
||||
//! stdout OK / INVALID; exit 0 = accept, 1 = reject, 2 = input error.
|
||||
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
|
||||
if s.len() % 2 != 0 {
|
||||
return Err("odd-length hex".into());
|
||||
}
|
||||
(0..s.len() / 2)
|
||||
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(|e| e.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() != 4 {
|
||||
eprintln!("usage: {} <pubkey-hex> <sig-hex> <payload-file>", args[0]);
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
let pk_bytes = match hex_decode(&args[1]) {
|
||||
Ok(b) if b.len() == 32 => b,
|
||||
_ => {
|
||||
eprintln!("error: public key must be 32 bytes of hex");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let sig_bytes = match hex_decode(&args[2]) {
|
||||
Ok(b) if b.len() == 64 => b,
|
||||
_ => {
|
||||
eprintln!("error: signature must be 64 bytes of hex");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let payload = match std::fs::read(&args[3]) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("error: cannot read payload: {e}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let mut pk_array = [0u8; 32];
|
||||
pk_array.copy_from_slice(&pk_bytes);
|
||||
let verifying_key = match VerifyingKey::from_bytes(&pk_array) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
// A key that fails point decompression is a REJECT verdict, not
|
||||
// an input error: quorum members must all judge the same bytes.
|
||||
println!("INVALID");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
let mut sig_array = [0u8; 64];
|
||||
sig_array.copy_from_slice(&sig_bytes);
|
||||
let signature = Signature::from_bytes(&sig_array);
|
||||
match verifying_key.verify(&payload, &signature) {
|
||||
Ok(()) => {
|
||||
println!("OK");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(_) => {
|
||||
println!("INVALID");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
337
src/pacta/quorum.py
Normal file
337
src/pacta/quorum.py
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
"""warden's quorum acceptance boundary.
|
||||
|
||||
N-version verification where the versions are provably equivalent on the
|
||||
proven domain: each quorum member is an Ed25519 verifier compiled from a
|
||||
source workspace whose correctness certificates are machine-checked in
|
||||
Lean 4 and replay-attested in the transparency log. Classic N-version
|
||||
programming fails because independent implementations share design bugs;
|
||||
here each member's accept() is characterized by a theorem, so runtime
|
||||
disagreement cannot be a semantics bug on the proven domain - it is
|
||||
either a documented semantic edge between the forks' accept() predicates
|
||||
(anza rejects A = 0 and a legacy excluded-R list) or evidence of build
|
||||
corruption / fault / tampering.
|
||||
|
||||
Fail-closed is unconditional: acceptance requires unanimity. The
|
||||
divergence taxonomy only grades the alarm:
|
||||
|
||||
- ``unanimous-accept`` / ``unanimous-reject``: the boring, common cases.
|
||||
- ``semantic-edge``: members disagree AND the input lies in a documented
|
||||
degenerate class (small-order R on the legacy exclusion list, zero A,
|
||||
non-canonical scalar). Verdict: reject; incident severity ``note``.
|
||||
- ``unexplained``: members disagree and no documented edge explains it.
|
||||
Verdict: reject; incident severity ``tamper`` - the wallet latches.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .dogfood import _git_commit, _tool_version # shared provenance helpers
|
||||
|
||||
QUORUM_STATE_DIR = Path("dogfood") / "state" / "quorum"
|
||||
|
||||
# The four proven forks. `source_subdir` is the conventional checkout name
|
||||
# under --sources-root; `component` names the transparency-log leaf whose
|
||||
# certificates cover this member's verify path.
|
||||
QUORUM_BACKENDS: dict[str, dict[str, Any]] = {
|
||||
"dalek": {
|
||||
"component": "dalek-ed25519-verified",
|
||||
"source_subdir": "curve25519-dalek-source",
|
||||
"crate": Path("dogfood") / "quorum" / "verify-dalek",
|
||||
"backend_cfg": 'curve25519_dalek_backend="serial"',
|
||||
"entry_point": "ed25519_dalek::VerifyingKey::verify (pinned workspace)",
|
||||
"semantics": "upstream-canonical",
|
||||
"workspace_marker": Path("ed25519-dalek") / "Cargo.toml",
|
||||
},
|
||||
"anza": {
|
||||
"component": "anza-ed25519-verified",
|
||||
"source_subdir": "anza-cryptography-source",
|
||||
"crate": Path("dogfood") / "quorum" / "verify-anza",
|
||||
"backend_cfg": "curve25519_serial_only",
|
||||
"entry_point": "curve25519::ed_sigs::VerificationKey::verify_sha512 (pinned workspace; NOT the default Zebra-lineage verify())",
|
||||
"semantics": "anza-strict (rejects A=0 and the legacy excluded-R list)",
|
||||
"workspace_marker": Path("curve25519") / "solana-ed25519" / "Cargo.toml",
|
||||
},
|
||||
"risc0": {
|
||||
"component": "risc0-ed25519-verified",
|
||||
"source_subdir": "risc0-curve25519-dalek-source",
|
||||
"crate": Path("dogfood") / "quorum" / "verify-risc0",
|
||||
"backend_cfg": 'curve25519_dalek_backend="serial"',
|
||||
"entry_point": "ed25519_dalek::VerifyingKey::verify (pinned workspace)",
|
||||
"semantics": "upstream-canonical",
|
||||
"workspace_marker": Path("ed25519-dalek") / "Cargo.toml",
|
||||
},
|
||||
"betrusted": {
|
||||
"component": "betrusted-ed25519-verified",
|
||||
"source_subdir": "betrusted-curve25519-dalek-source",
|
||||
"crate": Path("dogfood") / "quorum" / "verify-betrusted",
|
||||
"backend_cfg": 'curve25519_dalek_backend="serial"',
|
||||
"entry_point": "ed25519_dalek::VerifyingKey::verify (pinned workspace)",
|
||||
"semantics": "upstream-canonical",
|
||||
"workspace_marker": Path("ed25519-dalek") / "Cargo.toml",
|
||||
},
|
||||
}
|
||||
|
||||
# The eight small-order points' canonical encodings plus their sign-flipped
|
||||
# variants - the classic excluded-R list (as used by Solana's legacy
|
||||
# exclusion and libsodium's checks). Presence of R (or A) on this list is
|
||||
# what makes an inter-fork divergence a *documented semantic edge*.
|
||||
SMALL_ORDER_ENCODINGS: frozenset[bytes] = frozenset(
|
||||
bytes.fromhex(h)
|
||||
for h in (
|
||||
"0100000000000000000000000000000000000000000000000000000000000000", # identity
|
||||
"0000000000000000000000000000000000000000000000000000000000000000", # (0, 0)-ish y=0 encoding
|
||||
"0000000000000000000000000000000000000000000000000000000000000080", # y=0, sign flipped
|
||||
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", # -1 = p-1 (order 2)
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a", # order-8 point
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa", # order-8, sign flipped
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05", # order-8 point
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85", # order-8, sign flipped
|
||||
"0100000000000000000000000000000000000000000000000000000000000080", # identity, sign flipped
|
||||
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", # p-1 with high bit
|
||||
)
|
||||
)
|
||||
|
||||
# Group order l (little-endian comparison target for canonical s).
|
||||
_L = 2**252 + 27742317777372353535851937790883648493
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MemberVerdict:
|
||||
backend: str
|
||||
verdict: str # "accept" | "reject" | "error"
|
||||
detail: str | None = None
|
||||
binary_sha256: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class QuorumResult:
|
||||
accepted: bool
|
||||
classification: str # unanimous-accept | unanimous-reject | semantic-edge | unexplained
|
||||
verdicts: list[MemberVerdict] = field(default_factory=list)
|
||||
edge_flags: list[str] = field(default_factory=list)
|
||||
incident: dict[str, Any] | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"accepted": self.accepted,
|
||||
"classification": self.classification,
|
||||
"verdicts": [
|
||||
{
|
||||
"backend": v.backend,
|
||||
"verdict": v.verdict,
|
||||
"detail": v.detail,
|
||||
"binary_sha256": v.binary_sha256,
|
||||
}
|
||||
for v in self.verdicts
|
||||
],
|
||||
"edge_flags": self.edge_flags,
|
||||
"incident": self.incident,
|
||||
}
|
||||
|
||||
|
||||
def semantic_edge_flags(public_key: bytes, signature: bytes) -> list[str]:
|
||||
"""Name the documented degenerate classes this input falls into.
|
||||
|
||||
These are exactly the classes where the four proven accept() predicates
|
||||
are allowed to differ; anything outside them that still diverges is
|
||||
treated as tampering.
|
||||
"""
|
||||
flags: list[str] = []
|
||||
r_bytes, s_bytes = signature[:32], signature[32:]
|
||||
if public_key == b"\x00" * 32:
|
||||
flags.append("zero-public-key")
|
||||
if public_key in SMALL_ORDER_ENCODINGS:
|
||||
flags.append("small-order-public-key")
|
||||
if r_bytes in SMALL_ORDER_ENCODINGS:
|
||||
flags.append("small-order-R (legacy exclusion list)")
|
||||
if int.from_bytes(s_bytes, "little") >= _L:
|
||||
flags.append("non-canonical-s (s >= group order)")
|
||||
return flags
|
||||
|
||||
|
||||
def binary_path(backend: str, state_dir: str | Path | None = None) -> Path:
|
||||
return Path(state_dir or QUORUM_STATE_DIR) / f"pacta-verify-{backend}"
|
||||
|
||||
|
||||
def _sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1 << 20), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def build_quorum_member(
|
||||
backend: str,
|
||||
sources_root: str | Path,
|
||||
state_dir: str | Path | None = None,
|
||||
timeout: int = 900,
|
||||
) -> dict[str, Any]:
|
||||
"""Build one quorum member from its pinned proven source workspace.
|
||||
|
||||
Returns a provenance dict on success; raises RuntimeError with the
|
||||
cargo tail on failure. Serial backend pinned per fork exactly as the
|
||||
verified extraction pins it.
|
||||
"""
|
||||
spec = QUORUM_BACKENDS[backend]
|
||||
source = (Path(sources_root).expanduser() / spec["source_subdir"]).resolve()
|
||||
crate = Path(spec["crate"]).resolve()
|
||||
if not (source / spec["workspace_marker"]).exists():
|
||||
raise RuntimeError(
|
||||
f"{source} does not look like the pinned {backend} source workspace "
|
||||
f"(missing {spec['workspace_marker']})"
|
||||
)
|
||||
cargo = shutil.which("cargo")
|
||||
if not cargo:
|
||||
raise RuntimeError("cargo is not available on PATH")
|
||||
template = (crate / "Cargo.toml.template").read_text(encoding="utf-8")
|
||||
(crate / "Cargo.toml").write_text(template.replace("{{SOURCE}}", str(source)), encoding="utf-8")
|
||||
env = dict(os.environ)
|
||||
env["RUSTFLAGS"] = (env.get("RUSTFLAGS", "") + f" --cfg {spec['backend_cfg']}").strip()
|
||||
completed = subprocess.run(
|
||||
[cargo, "build", "--release", "--quiet"],
|
||||
cwd=str(crate),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
tail = "\n".join((completed.stderr or completed.stdout or "").strip().splitlines()[-12:])
|
||||
raise RuntimeError(f"cargo build failed for quorum member {backend}:\n{tail}")
|
||||
built = crate / "target" / "release" / f"pacta-verify-{backend}"
|
||||
if not built.exists():
|
||||
raise RuntimeError(f"cargo reported success but {built} does not exist")
|
||||
out = binary_path(backend, state_dir)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(built, out)
|
||||
provenance = {
|
||||
"type": "pacta.quorum.member_provenance.v1",
|
||||
"backend": backend,
|
||||
"component": spec["component"],
|
||||
"semantics": spec["semantics"],
|
||||
"source_workspace": str(source),
|
||||
"source_commit": _git_commit(source),
|
||||
"backend_cfg": spec["backend_cfg"],
|
||||
"entry_point": spec["entry_point"],
|
||||
"rustc_version": _tool_version("rustc"),
|
||||
"cargo_version": _tool_version("cargo"),
|
||||
"binary_sha256": _sha256_file(out),
|
||||
}
|
||||
out.with_suffix(".provenance.json").write_text(
|
||||
json.dumps(provenance, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
return provenance
|
||||
|
||||
|
||||
def member_provenance(backend: str, state_dir: str | Path | None = None) -> dict[str, Any]:
|
||||
sidecar = binary_path(backend, state_dir).with_suffix(".provenance.json")
|
||||
if sidecar.exists():
|
||||
return json.loads(sidecar.read_text(encoding="utf-8"))
|
||||
return {}
|
||||
|
||||
|
||||
class QuorumVerifier:
|
||||
"""Run every member on the same bytes; demand unanimity; grade dissent."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
members: dict[str, Path],
|
||||
min_members: int = 2,
|
||||
timeout: int = 30,
|
||||
) -> None:
|
||||
if len(members) < min_members:
|
||||
raise ValueError(
|
||||
f"quorum needs at least {min_members} members, got {len(members)}: "
|
||||
f"{sorted(members)}"
|
||||
)
|
||||
missing = {name: path for name, path in members.items() if not Path(path).exists()}
|
||||
if missing:
|
||||
raise ValueError(f"quorum member binaries missing: {missing}")
|
||||
self.members = {name: Path(path) for name, path in members.items()}
|
||||
self.timeout = timeout
|
||||
|
||||
def verify(self, payload: bytes, signature: bytes, public_key: bytes) -> QuorumResult:
|
||||
if len(signature) != 64 or len(public_key) != 32:
|
||||
raise ValueError("signature must be 64 bytes and public key 32 bytes")
|
||||
verdicts: list[MemberVerdict] = []
|
||||
with tempfile.TemporaryDirectory(prefix="pacta-quorum-") as tmp:
|
||||
payload_path = Path(tmp) / "payload.bin"
|
||||
payload_path.write_bytes(payload)
|
||||
for name, binary in sorted(self.members.items()):
|
||||
verdicts.append(self._run_member(name, binary, public_key, signature, payload_path))
|
||||
return self._judge(verdicts, payload, signature, public_key)
|
||||
|
||||
def _run_member(
|
||||
self, name: str, binary: Path, public_key: bytes, signature: bytes, payload_path: Path
|
||||
) -> MemberVerdict:
|
||||
sha = _sha256_file(binary)
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[str(binary), public_key.hex(), signature.hex(), str(payload_path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return MemberVerdict(name, "error", "timeout", sha)
|
||||
if completed.returncode == 0:
|
||||
return MemberVerdict(name, "accept", None, sha)
|
||||
if completed.returncode == 1:
|
||||
return MemberVerdict(name, "reject", None, sha)
|
||||
detail = (completed.stderr or completed.stdout or "member error").strip()
|
||||
return MemberVerdict(name, "error", detail, sha)
|
||||
|
||||
def _judge(
|
||||
self,
|
||||
verdicts: list[MemberVerdict],
|
||||
payload: bytes,
|
||||
signature: bytes,
|
||||
public_key: bytes,
|
||||
) -> QuorumResult:
|
||||
kinds = {v.verdict for v in verdicts}
|
||||
if kinds == {"accept"}:
|
||||
return QuorumResult(True, "unanimous-accept", verdicts)
|
||||
if kinds == {"reject"}:
|
||||
return QuorumResult(False, "unanimous-reject", verdicts)
|
||||
# Divergence (including any member error): reject, then grade.
|
||||
flags = semantic_edge_flags(public_key, signature)
|
||||
classification = "semantic-edge" if flags and "error" not in kinds else "unexplained"
|
||||
incident = {
|
||||
"type": "pacta.quorum.divergence.v1",
|
||||
"severity": "note" if classification == "semantic-edge" else "tamper",
|
||||
"classification": classification,
|
||||
"edge_flags": flags,
|
||||
"verdicts": [
|
||||
{"backend": v.backend, "verdict": v.verdict, "detail": v.detail, "binary_sha256": v.binary_sha256}
|
||||
for v in verdicts
|
||||
],
|
||||
"payload_sha256": hashlib.sha256(payload).hexdigest(),
|
||||
"signature_hex": signature.hex(),
|
||||
"public_key_hex": public_key.hex(),
|
||||
}
|
||||
return QuorumResult(False, classification, verdicts, flags, incident)
|
||||
|
||||
|
||||
def load_quorum(
|
||||
backends: list[str] | None = None,
|
||||
state_dir: str | Path | None = None,
|
||||
min_members: int = 2,
|
||||
) -> QuorumVerifier:
|
||||
"""Assemble the quorum from built member binaries in the state dir."""
|
||||
names = backends or list(QUORUM_BACKENDS)
|
||||
members = {
|
||||
name: binary_path(name, state_dir)
|
||||
for name in names
|
||||
if binary_path(name, state_dir).exists()
|
||||
}
|
||||
return QuorumVerifier(members, min_members=min_members)
|
||||
Loading…
Reference in a new issue