mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +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/target/
|
||||||
dogfood/pacta-verified-verify/Cargo.toml
|
dogfood/pacta-verified-verify/Cargo.toml
|
||||||
dogfood/pacta-verified-verify/Cargo.lock
|
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