mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-09-13 21:50:28 +00:00
Compare commits
No commits in common. "a1e4ec6ce734fa845c80856ae541746e033b7200" and "cad6e112e3ba9a75c4723f800c1f42abde7dd5f4" have entirely different histories.
a1e4ec6ce7
...
cad6e112e3
29 changed files with 2 additions and 1384 deletions
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"version": "0.0.1",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "cockpit",
|
|
||||||
"runtimeExecutable": "./evote",
|
|
||||||
"runtimeArgs": ["cockpit", "--port", "8092", "--voters", "2", "--options", "3", "--delay", "40"],
|
|
||||||
"port": 8092
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,8 +8,5 @@ testdata/*.json
|
||||||
# Rust build artifacts (the static lib is rebuilt via `make rust` / `cargo build`)
|
# Rust build artifacts (the static lib is rebuilt via `make rust` / `cargo build`)
|
||||||
rust/**/target/
|
rust/**/target/
|
||||||
|
|
||||||
# Local Claude Code settings (keep launch.json, ignore per-user settings)
|
|
||||||
.claude/settings.local.json
|
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
||||||
28
README.md
28
README.md
|
|
@ -68,10 +68,6 @@ make test # runs cargo test + go test ./...
|
||||||
./evote netdemo --voters 10 --options 3
|
./evote netdemo --voters 10 --options 3
|
||||||
./evote netdemo --voters 3 --options 2 --verbose # log every signed message
|
./evote netdemo --voters 3 --options 2 --verbose # log every signed message
|
||||||
|
|
||||||
# Watch the cryptography execute as live math — two surfaces, one event stream:
|
|
||||||
./evote cockpit --voters 3 --options 3 # browser: typeset MathML at http://localhost:8090
|
|
||||||
./evote cockpit --tmux --voters 3 # terminal: one tmux pane per stakeholder
|
|
||||||
|
|
||||||
# Serve presentations on local network (for iPad viewing)
|
# Serve presentations on local network (for iPad viewing)
|
||||||
./evote serve --port 8080
|
./evote serve --port 8080
|
||||||
|
|
||||||
|
|
@ -79,30 +75,6 @@ make test # runs cargo test + go test ./...
|
||||||
./evote present
|
./evote present
|
||||||
```
|
```
|
||||||
|
|
||||||
The `cockpit` command is a teaching instrument: it runs the full multi-party
|
|
||||||
ceremony and streams **every cryptographic operation** as **the mathematics that
|
|
||||||
is executing, with the real runtime values**, the instant it runs. It goes down
|
|
||||||
to the level of the Swiss Post `crypto-primitives` class structure — you can watch
|
|
||||||
the Bayer-Groth shuffle proof being *constructed*, not just referenced:
|
|
||||||
|
|
||||||
- ElGamal encryption, Fiat-Shamir challenges, Ed25519 signatures, X25519 key agreement
|
|
||||||
- **Pedersen matrix commitments** (`CommitmentService`)
|
|
||||||
- **all five Bayer-Groth sub-arguments** (`ShuffleArgument`, `ProductArgument`,
|
|
||||||
`HadamardArgument`, `ZeroArgument`, `SingleValueProductArgument`,
|
|
||||||
`MultiExponentiationArgument`)
|
|
||||||
- **partial decryption + decryption proofs** (`DecryptionProofService`)
|
|
||||||
|
|
||||||
One `pkg/trace` event stream feeds two surfaces:
|
|
||||||
|
|
||||||
- **Browser** (default): a self-contained page renders each operation as typeset
|
|
||||||
MathML (no libraries, offline). A stakeholder sidebar highlights the acting
|
|
||||||
party; a phase timeline tracks setup→cards→voting→tally→verify.
|
|
||||||
- **Terminal** (`--tmux`): one tmux pane per stakeholder, each rendering its
|
|
||||||
party's operations as colored ASCII/Unicode math — the "autonomous parties"
|
|
||||||
view.
|
|
||||||
|
|
||||||
`--delay` paces the events so a human can follow along.
|
|
||||||
|
|
||||||
The `demo` command runs the whole protocol in one process. `netdemo` runs the
|
The `demo` command runs the whole protocol in one process. `netdemo` runs the
|
||||||
**multi-party** architecture: every party is a separate endpoint holding only its
|
**multi-party** architecture: every party is a separate endpoint holding only its
|
||||||
own private state, and every message between them is Ed25519-signed (and, for
|
own private state, and every message between them is Ed25519-signed (and, for
|
||||||
|
|
|
||||||
|
|
@ -1,220 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"math/big"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/user/evote/pkg/party"
|
|
||||||
"github.com/user/evote/pkg/protocol"
|
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cockpitPort int
|
|
||||||
cockpitVoters int
|
|
||||||
cockpitOptions int
|
|
||||||
cockpitDelayMs int
|
|
||||||
cockpitTmux bool
|
|
||||||
cockpitNoOpen bool
|
|
||||||
cockpitMu sync.Mutex // serializes ceremonies (global trace context)
|
|
||||||
)
|
|
||||||
|
|
||||||
var cockpitCmd = &cobra.Command{
|
|
||||||
Use: "cockpit",
|
|
||||||
Short: "Watch the cryptography execute live in the browser (typeset math)",
|
|
||||||
Long: "Runs the multi-party election and streams every cryptographic operation " +
|
|
||||||
"— sampling, ElGamal encryption, Fiat-Shamir challenges, the Bayer-Groth " +
|
|
||||||
"shuffle, Ed25519 signatures, X25519 key agreement — to a browser page that " +
|
|
||||||
"renders each as typeset mathematics with the real runtime values, the instant " +
|
|
||||||
"it runs. Open the printed URL.",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if cockpitVoters < 1 || cockpitVoters > 200 {
|
|
||||||
return fmt.Errorf("--voters must be in [1, 200]")
|
|
||||||
}
|
|
||||||
if cockpitOptions < 1 || cockpitOptions > 200 {
|
|
||||||
return fmt.Errorf("--options must be in [1, 200]")
|
|
||||||
}
|
|
||||||
if cockpitTmux {
|
|
||||||
return runCockpitTmux()
|
|
||||||
}
|
|
||||||
return runCockpit()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
cockpitCmd.Flags().IntVar(&cockpitPort, "port", 8090, "HTTP port (browser mode)")
|
|
||||||
cockpitCmd.Flags().IntVar(&cockpitVoters, "voters", 3, "Number of voters")
|
|
||||||
cockpitCmd.Flags().IntVar(&cockpitOptions, "options", 3, "Number of voting options")
|
|
||||||
cockpitCmd.Flags().IntVar(&cockpitDelayMs, "delay", 350, "Milliseconds between events (pacing so it's watchable)")
|
|
||||||
cockpitCmd.Flags().BoolVar(&cockpitTmux, "tmux", false, "Terminal mode: one tmux pane per stakeholder instead of the browser")
|
|
||||||
cockpitCmd.Flags().BoolVar(&cockpitNoOpen, "no-open", false, "Do not open the browser automatically")
|
|
||||||
rootCmd.AddCommand(cockpitCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runCockpit() error {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
// The cockpit page + assets are embedded under web/ (see serve.go's webContent).
|
|
||||||
webFS, err := fs.Sub(webContent, "web")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileServer := http.FileServer(http.FS(webFS))
|
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path == "/" {
|
|
||||||
data, err := fs.ReadFile(webFS, "cockpit.html")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "cockpit.html not found", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
w.Write(data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileServer.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
// SSE stream: on connect, run one ceremony and stream its crypto events.
|
|
||||||
mux.HandleFunc("/events", cockpitEventsHandler)
|
|
||||||
|
|
||||||
srv := &http.Server{
|
|
||||||
Handler: mux,
|
|
||||||
ReadHeaderTimeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind the listener first so we only open the browser once we're accepting
|
|
||||||
// connections — and so a busy port fails with a clear message, not a race.
|
|
||||||
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", cockpitPort))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot listen on port %d (is another cockpit already running?): %w", cockpitPort, err)
|
|
||||||
}
|
|
||||||
url := fmt.Sprintf("http://localhost:%d/", cockpitPort)
|
|
||||||
fmt.Println("========================================")
|
|
||||||
fmt.Println(" Swiss Post E-Voting — Live Crypto Cockpit")
|
|
||||||
fmt.Println("========================================")
|
|
||||||
fmt.Printf(" Watching at %s\n", url)
|
|
||||||
fmt.Printf(" Voters: %d, Options: %d, pacing: %dms/event\n", cockpitVoters, cockpitOptions, cockpitDelayMs)
|
|
||||||
fmt.Println(" The election plays automatically when the page opens.")
|
|
||||||
fmt.Println(" Press Ctrl+C here to stop.")
|
|
||||||
if !cockpitNoOpen {
|
|
||||||
go openBrowser(url)
|
|
||||||
}
|
|
||||||
return srv.Serve(ln)
|
|
||||||
}
|
|
||||||
|
|
||||||
// openBrowser opens url in the default browser, cross-platform. Best-effort.
|
|
||||||
func openBrowser(url string) {
|
|
||||||
time.Sleep(300 * time.Millisecond) // let Serve settle
|
|
||||||
var cmd string
|
|
||||||
var args []string
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "darwin":
|
|
||||||
cmd = "open"
|
|
||||||
case "windows":
|
|
||||||
cmd, args = "rundll32", []string{"url.dll,FileProtocolHandler"}
|
|
||||||
default:
|
|
||||||
cmd = "xdg-open"
|
|
||||||
}
|
|
||||||
_ = exec.Command(cmd, append(args, url)...).Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// cockpitEventsHandler streams one ceremony's crypto events as Server-Sent
|
|
||||||
// Events. It subscribes a buffered sink, runs the ceremony in a goroutine, and
|
|
||||||
// drains the buffer to the client with pacing so a human can follow along.
|
|
||||||
func cockpitEventsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
flusher, ok := w.(http.Flusher)
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// The trace stream and its party/phase context are global, so only one
|
|
||||||
// ceremony runs at a time. A second viewer waits gracefully for the first to
|
|
||||||
// finish rather than getting an error (single-viewer desktop tool).
|
|
||||||
cockpitMu.Lock()
|
|
||||||
defer cockpitMu.Unlock()
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
|
||||||
w.Header().Set("Connection", "keep-alive")
|
|
||||||
|
|
||||||
sink := trace.NewChanSink(8192)
|
|
||||||
unsub := trace.Subscribe(sink)
|
|
||||||
defer unsub()
|
|
||||||
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() { done <- runCockpitCeremony() }()
|
|
||||||
|
|
||||||
delay := time.Duration(cockpitDelayMs) * time.Millisecond
|
|
||||||
send := func(eventType string, payload any) {
|
|
||||||
b, _ := json.Marshal(payload)
|
|
||||||
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", eventType, b)
|
|
||||||
flusher.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case e := <-sink.C:
|
|
||||||
send("op", e)
|
|
||||||
if delay > 0 {
|
|
||||||
time.Sleep(delay)
|
|
||||||
}
|
|
||||||
case err := <-done:
|
|
||||||
// Drain any remaining buffered events before closing.
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case e := <-sink.C:
|
|
||||||
send("op", e)
|
|
||||||
default:
|
|
||||||
msg := "complete"
|
|
||||||
if err != nil {
|
|
||||||
msg = "error: " + err.Error()
|
|
||||||
}
|
|
||||||
send("done", map[string]string{"status": msg})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-r.Context().Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// runCockpitCeremony runs a full multi-party election (the same one netdemo
|
|
||||||
// runs), emitting trace events as it goes.
|
|
||||||
func runCockpitCeremony() error {
|
|
||||||
cfg := protocol.DefaultConfig(cockpitVoters, cockpitOptions)
|
|
||||||
c, err := party.NewCeremony(cfg, func(string, ...any) {})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.RunSetup(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.RunCards(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
selections := make([][]int, cockpitVoters)
|
|
||||||
for v := 0; v < cockpitVoters; v++ {
|
|
||||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(cockpitOptions)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
selections[v] = []int{int(n.Int64())}
|
|
||||||
}
|
|
||||||
if err := c.RunVoting(selections); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.RunTally(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.RunVerify()
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/user/evote/pkg/party"
|
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
// runCockpitTmux runs the ceremony and shows one tmux pane per stakeholder, each
|
|
||||||
// rendering that party's live crypto activity as ASCII/Unicode math. The panes
|
|
||||||
// follow a shared NDJSON event file that the ceremony writes to, paced.
|
|
||||||
func runCockpitTmux() error {
|
|
||||||
if _, err := exec.LookPath("tmux"); err != nil {
|
|
||||||
return fmt.Errorf("tmux not found on PATH: %w", err)
|
|
||||||
}
|
|
||||||
if os.Getenv("TMUX") != "" {
|
|
||||||
return fmt.Errorf("already inside a tmux session — run `evote cockpit --tmux` from a plain terminal (tmux cannot nest an attach)")
|
|
||||||
}
|
|
||||||
exe, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared event file the panes tail and the ceremony appends to.
|
|
||||||
f, err := os.CreateTemp("", "evote-cockpit-*.ndjson")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
eventPath := f.Name()
|
|
||||||
defer os.Remove(eventPath)
|
|
||||||
|
|
||||||
// One pane per infrastructure party, plus one aggregate pane for all voters.
|
|
||||||
panes := []string{
|
|
||||||
party.NameSetup,
|
|
||||||
party.CCName(0), party.CCName(1), party.CCName(2), party.CCName(3),
|
|
||||||
party.NameEB, party.NameServer, party.NameVerifier,
|
|
||||||
"voters",
|
|
||||||
}
|
|
||||||
|
|
||||||
session := "evote-cockpit"
|
|
||||||
_ = exec.Command("tmux", "kill-session", "-t", session).Run() // best-effort clean slate
|
|
||||||
|
|
||||||
paneCmd := func(role string) string {
|
|
||||||
return fmt.Sprintf("%s panelview --role=%s --file=%s", exe, role, eventPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// First pane creates the (detached) session.
|
|
||||||
if out, err := exec.Command("tmux", "new-session", "-d", "-s", session, paneCmd(panes[0])).CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("tmux new-session: %v: %s", err, out)
|
|
||||||
}
|
|
||||||
for _, role := range panes[1:] {
|
|
||||||
if out, err := exec.Command("tmux", "split-window", "-t", session, paneCmd(role)).CombinedOutput(); err != nil {
|
|
||||||
exec.Command("tmux", "kill-session", "-t", session).Run()
|
|
||||||
return fmt.Errorf("tmux split-window: %v: %s", err, out)
|
|
||||||
}
|
|
||||||
// Re-tile after each split so panes stay balanced.
|
|
||||||
exec.Command("tmux", "select-layout", "-t", session, "tiled").Run()
|
|
||||||
}
|
|
||||||
exec.Command("tmux", "select-layout", "-t", session, "tiled").Run()
|
|
||||||
exec.Command("tmux", "set-option", "-t", session, "mouse", "on").Run()
|
|
||||||
|
|
||||||
// Run the ceremony in the background, writing paced NDJSON to the file.
|
|
||||||
go writeCeremonyEvents(f)
|
|
||||||
|
|
||||||
fmt.Printf("Launching tmux cockpit '%s' — %d panes. Detach with Ctrl-b d; it closes when you exit.\n", session, len(panes))
|
|
||||||
time.Sleep(400 * time.Millisecond) // let panes open the file first
|
|
||||||
|
|
||||||
// Attach in the foreground; blocks until the user detaches or exits.
|
|
||||||
attach := exec.Command("tmux", "attach", "-t", session)
|
|
||||||
attach.Stdin, attach.Stdout, attach.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
||||||
err = attach.Run()
|
|
||||||
|
|
||||||
exec.Command("tmux", "kill-session", "-t", session).Run()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("tmux attach failed (run this from an interactive terminal): %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeCeremonyEvents subscribes a paced file sink, runs the full ceremony, and
|
|
||||||
// writes a final done marker.
|
|
||||||
func writeCeremonyEvents(f *os.File) {
|
|
||||||
delay := time.Duration(cockpitDelayMs) * time.Millisecond
|
|
||||||
sink := &fileSink{f: f, delay: delay, enc: json.NewEncoder(f)}
|
|
||||||
unsub := trace.Subscribe(sink)
|
|
||||||
defer unsub()
|
|
||||||
|
|
||||||
_ = runCockpitCeremony()
|
|
||||||
|
|
||||||
f.WriteString(`{"done":true}` + "\n")
|
|
||||||
f.Sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
// fileSink appends each event as one JSON line, pacing between events so the
|
|
||||||
// panes render at a watchable rate.
|
|
||||||
type fileSink struct {
|
|
||||||
f *os.File
|
|
||||||
delay time.Duration
|
|
||||||
enc *json.Encoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fileSink) Handle(e trace.Event) {
|
|
||||||
_ = s.enc.Encode(e) // Encode writes the trailing newline
|
|
||||||
s.f.Sync()
|
|
||||||
if s.delay > 0 {
|
|
||||||
time.Sleep(s.delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
panelRole string
|
|
||||||
panelFile string
|
|
||||||
panelWidth int
|
|
||||||
)
|
|
||||||
|
|
||||||
// panelViewCmd is run inside each tmux pane. It follows a shared NDJSON event
|
|
||||||
// file and renders the operations belonging to one stakeholder as ASCII/Unicode
|
|
||||||
// math. It is an internal helper for `evote cockpit --tmux`, but works standalone
|
|
||||||
// against any NDJSON trace file.
|
|
||||||
var panelViewCmd = &cobra.Command{
|
|
||||||
Use: "panelview",
|
|
||||||
Short: "Render one stakeholder's live crypto activity (used by cockpit --tmux)",
|
|
||||||
Hidden: true,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if panelRole == "" || panelFile == "" {
|
|
||||||
return fmt.Errorf("--role and --file are required")
|
|
||||||
}
|
|
||||||
return runPanelView()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
panelViewCmd.Flags().StringVar(&panelRole, "role", "", "Stakeholder role to display (or 'voters', 'all')")
|
|
||||||
panelViewCmd.Flags().StringVar(&panelFile, "file", "", "Shared NDJSON event file to follow")
|
|
||||||
panelViewCmd.Flags().IntVar(&panelWidth, "width", 0, "Wrap width (0 = terminal default)")
|
|
||||||
rootCmd.AddCommand(panelViewCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ANSI colors keyed by operation kind.
|
|
||||||
var kindColor = map[trace.Kind]string{
|
|
||||||
trace.KindSign: "\x1b[35m", // magenta
|
|
||||||
trace.KindShuffle: "\x1b[95m", // bright magenta
|
|
||||||
trace.KindEncrypt: "\x1b[32m", // green
|
|
||||||
trace.KindDecrypt: "\x1b[36m", // cyan
|
|
||||||
trace.KindChallenge: "\x1b[33m", // yellow
|
|
||||||
trace.KindKeyEx: "\x1b[34m", // blue
|
|
||||||
trace.KindProof: "\x1b[36m",
|
|
||||||
trace.KindVerify: "\x1b[92m",
|
|
||||||
trace.KindSample: "\x1b[36m",
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ansiReset = "\x1b[0m"
|
|
||||||
ansiBold = "\x1b[1m"
|
|
||||||
ansiDim = "\x1b[2m"
|
|
||||||
)
|
|
||||||
|
|
||||||
func panelMatches(role, party string) bool {
|
|
||||||
if role == "all" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if role == "voters" {
|
|
||||||
return strings.HasPrefix(party, "voter")
|
|
||||||
}
|
|
||||||
return party == role
|
|
||||||
}
|
|
||||||
|
|
||||||
func runPanelView() error {
|
|
||||||
// Header.
|
|
||||||
title := panelRole
|
|
||||||
switch {
|
|
||||||
case panelRole == "voters":
|
|
||||||
title = "Voters"
|
|
||||||
case strings.HasPrefix(panelRole, "control-component-"):
|
|
||||||
title = "CC" + strings.TrimPrefix(panelRole, "control-component-")
|
|
||||||
default:
|
|
||||||
title = strings.Title(strings.ReplaceAll(panelRole, "-", " "))
|
|
||||||
}
|
|
||||||
fmt.Printf("%s%s▐ %s ▌%s\n", ansiBold, "\x1b[7m", title, ansiReset)
|
|
||||||
fmt.Printf("%swaiting for activity…%s\n", ansiDim, ansiReset)
|
|
||||||
|
|
||||||
f, err := openWithRetry(panelFile, 5*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
reader := bufio.NewReader(f)
|
|
||||||
|
|
||||||
shown := 0
|
|
||||||
for {
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if len(line) > 0 {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if line == `{"done":true}` {
|
|
||||||
fmt.Printf("\n%s%s✓ ceremony complete%s\n", ansiBold, "\x1b[32m", ansiReset)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var e trace.Event
|
|
||||||
if json.Unmarshal([]byte(line), &e) == nil && panelMatches(panelRole, e.Party) {
|
|
||||||
if shown == 0 {
|
|
||||||
fmt.Print("\x1b[1A\x1b[2K") // erase the "waiting…" line
|
|
||||||
}
|
|
||||||
printPanelEvent(e)
|
|
||||||
shown++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil { // EOF: wait for more lines
|
|
||||||
time.Sleep(80 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printPanelEvent(e trace.Event) {
|
|
||||||
col := kindColor[e.Kind]
|
|
||||||
if col == "" {
|
|
||||||
col = "\x1b[37m"
|
|
||||||
}
|
|
||||||
fmt.Printf("%s%s%-9s%s %s#%d%s %s\n", ansiBold, col, strings.ToUpper(string(e.Kind)), ansiReset,
|
|
||||||
ansiDim, e.Seq, ansiReset, e.Caption)
|
|
||||||
if e.ASCII != "" {
|
|
||||||
fmt.Printf(" %s%s%s\n", col, e.ASCII, ansiReset)
|
|
||||||
} else if e.LaTeX != "" {
|
|
||||||
fmt.Printf(" %s%s%s\n", ansiDim, e.LaTeX, ansiReset)
|
|
||||||
}
|
|
||||||
if len(e.Values) > 0 {
|
|
||||||
parts := make([]string, 0, len(e.Values))
|
|
||||||
for k, v := range e.Values {
|
|
||||||
parts = append(parts, fmt.Sprintf("%s=%s", k, trace.Short(v)))
|
|
||||||
}
|
|
||||||
fmt.Printf(" %s%s%s\n", ansiDim, strings.Join(parts, " "), ansiReset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// openWithRetry waits up to timeout for the file to appear (the writer may start
|
|
||||||
// a moment after the panes).
|
|
||||||
func openWithRetry(path string, timeout time.Duration) (*os.File, error) {
|
|
||||||
deadline := time.Now().Add(timeout)
|
|
||||||
for {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err == nil {
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
if time.Now().After(deadline) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,279 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>E-Voting — Live Crypto Cockpit</title>
|
|
||||||
<style>
|
|
||||||
:root{
|
|
||||||
--bg:#ffffff; --fg:#1f2328; --dim:#57606a; --line:#d0d7de; --soft:#f6f8fa;
|
|
||||||
--accent:#0969da; --ok:#1a7f37; --warn:#9a6700; --sign:#8250df; --shuffle:#bf3989;
|
|
||||||
--sample:#0a7ea3; --encrypt:#1a7f37; --challenge:#9a6700; --keyex:#6639ba;
|
|
||||||
}
|
|
||||||
*{box-sizing:border-box}
|
|
||||||
html,body{margin:0;height:100%}
|
|
||||||
body{background:var(--bg);color:var(--fg);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
|
|
||||||
header{padding:14px 20px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:16px;flex-wrap:wrap;}
|
|
||||||
header h1{font-size:17px;margin:0;font-weight:700;}
|
|
||||||
header .sub{color:var(--dim);font-size:13px;}
|
|
||||||
.timeline{display:flex;gap:6px;margin-left:auto;flex-wrap:wrap;}
|
|
||||||
.phase{font-size:11px;text-transform:uppercase;letter-spacing:.5px;padding:4px 10px;border:1px solid var(--line);border-radius:999px;color:var(--dim);}
|
|
||||||
.phase.active{background:var(--accent);color:#fff;border-color:var(--accent);}
|
|
||||||
.phase.done{background:var(--soft);color:var(--ok);border-color:var(--ok);}
|
|
||||||
main{display:grid;grid-template-columns:230px 1fr;height:calc(100% - 56px);}
|
|
||||||
@media(max-width:760px){main{grid-template-columns:1fr;}#parties{display:none}}
|
|
||||||
#parties{border-right:1px solid var(--line);padding:14px;overflow:auto;background:var(--soft);}
|
|
||||||
#parties h2{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--dim);margin:0 0 10px;}
|
|
||||||
.party{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:7px;font-size:13px;color:var(--dim);transition:background .2s,color .2s;}
|
|
||||||
.party .dot{width:8px;height:8px;border-radius:50%;background:var(--line);flex:none;}
|
|
||||||
.party.live{background:#fff;color:var(--fg);font-weight:600;box-shadow:0 0 0 1px var(--line);}
|
|
||||||
.party.live .dot{background:var(--accent);box-shadow:0 0 0 3px rgba(9,105,218,.15);}
|
|
||||||
#feed{overflow:auto;padding:18px 22px;}
|
|
||||||
.op{border:1px solid var(--line);border-left-width:4px;border-radius:10px;padding:12px 16px;margin:0 0 12px;animation:pop .25s ease;}
|
|
||||||
@keyframes pop{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
||||||
.op .row{display:flex;align-items:baseline;gap:10px;margin-bottom:8px;flex-wrap:wrap;}
|
|
||||||
.badge{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 8px;border-radius:999px;color:#fff;}
|
|
||||||
.op .who{font-weight:600;font-size:13px;}
|
|
||||||
.op .cap{color:var(--dim);font-size:13px;}
|
|
||||||
.op .seq{margin-left:auto;color:var(--line);font-size:12px;font-variant-numeric:tabular-nums;}
|
|
||||||
math{font-size:19px;}
|
|
||||||
.vals{margin-top:10px;display:flex;flex-wrap:wrap;gap:6px;}
|
|
||||||
.val{font:12px ui-monospace,SFMono-Regular,Menlo,monospace;background:var(--soft);border:1px solid var(--line);border-radius:6px;padding:2px 8px;cursor:pointer;color:var(--fg);}
|
|
||||||
.val .k{color:var(--accent);}
|
|
||||||
.val.open{white-space:normal;word-break:break-all;max-width:100%;}
|
|
||||||
.k-sign{border-left-color:var(--sign)} .k-sign .badge{background:var(--sign)}
|
|
||||||
.k-shuffle{border-left-color:var(--shuffle)} .k-shuffle .badge{background:var(--shuffle)}
|
|
||||||
.k-sample{border-left-color:var(--sample)} .k-sample .badge{background:var(--sample)}
|
|
||||||
.k-encrypt{border-left-color:var(--encrypt)} .k-encrypt .badge{background:var(--encrypt)}
|
|
||||||
.k-decrypt{border-left-color:var(--sample)} .k-decrypt .badge{background:var(--sample)}
|
|
||||||
.k-challenge{border-left-color:var(--challenge)} .k-challenge .badge{background:var(--challenge)}
|
|
||||||
.k-keyex{border-left-color:var(--keyex)} .k-keyex .badge{background:var(--keyex)}
|
|
||||||
.k-proof{border-left-color:var(--accent)} .k-proof .badge{background:var(--accent)}
|
|
||||||
.k-verify{border-left-color:var(--ok)} .k-verify .badge{background:var(--ok)}
|
|
||||||
.k-note .badge{background:var(--dim)}
|
|
||||||
#status{padding:10px 22px;border-top:1px solid var(--line);color:var(--dim);font-size:13px;background:var(--soft);display:flex;align-items:center;gap:14px;}
|
|
||||||
.done-banner{color:var(--ok);font-weight:700;}
|
|
||||||
#replay{margin-left:auto;display:none;border:1px solid var(--accent);background:var(--accent);color:#fff;font:600 13px inherit;padding:6px 16px;border-radius:999px;cursor:pointer;}
|
|
||||||
#replay:hover{background:#0a5bc4;}
|
|
||||||
#replay.show{display:inline-block;}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>🔐 Live Crypto Cockpit</h1>
|
|
||||||
<span class="sub">the cryptography, rendered as it runs</span>
|
|
||||||
<div class="timeline" id="timeline"></div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<aside id="parties"><h2>Stakeholders</h2><div id="partyList"></div></aside>
|
|
||||||
<section id="feed"></section>
|
|
||||||
</main>
|
|
||||||
<div id="status"><span id="statusText">Starting the election…</span><button id="replay" onclick="location.reload()">▶ Run the election again</button></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// compact value elision (mirrors trace.Short in Go)
|
|
||||||
function short(s){ if(s.length<=15) return s; return s.slice(0,8)+"…"+s.slice(-6); }
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Focused LaTeX -> MathML converter.
|
|
||||||
// Handles exactly the subset emitted by pkg/trace templates. Native MathML
|
|
||||||
// renders offline in every modern browser — no library, no fonts to ship.
|
|
||||||
// Unknown tokens fall back to literal text so nothing ever crashes the view.
|
|
||||||
// ============================================================
|
|
||||||
const GREEK={sigma:"σ",gamma:"γ",rho:"ρ",pi:"π",phi:"φ",tau:"τ",lambda:"λ",mu:"μ",delta:"δ"};
|
|
||||||
const OPS={cdot:"⋅",in:"∈",gets:"←",Vert:"∥",times:"×",oplus:"⊕",bmod:"mod",
|
|
||||||
circ:"∘",ast:"∗",sim:"∼",log:"log",prod:"∏",sum:"∑",
|
|
||||||
cdots:"⋯",ldots:"…",dots:"…",vdots:"⋮",ddots:"⋱",
|
|
||||||
qquad:" ",quad:" "};
|
|
||||||
// big operators get msubsup limits from a following _{}^{}
|
|
||||||
const BIGOP={prod:"∏",sum:"∑"};
|
|
||||||
const BB={Z:"ℤ",X:"𝕏",N:"ℕ",G:"𝔾",F:"𝔽",Q:"ℚ"};
|
|
||||||
const CAL={H:"ℋ",R:"ℛ",C:"𝒞",E:"ℰ"};
|
|
||||||
const MML="http://www.w3.org/1998/Math/MathML";
|
|
||||||
|
|
||||||
function el(tag,...kids){ const m=document.createElementNS(MML,tag);
|
|
||||||
for(const k of kids){ if(k==null) continue; m.appendChild(typeof k==="string"?document.createTextNode(k):k);} return m; }
|
|
||||||
function mi(t){return el("mi",t);} function mo(t){return el("mo",t);} function mn(t){return el("mn",t);}
|
|
||||||
function mtext(t){return el("mtext",t);}
|
|
||||||
function mspace(){const s=el("mspace"); s.setAttribute("width","0.5em"); return s;}
|
|
||||||
|
|
||||||
function tokenize(src){
|
|
||||||
const out=[]; let i=0;
|
|
||||||
while(i<src.length){
|
|
||||||
const c=src[i];
|
|
||||||
if(c==="\\"){
|
|
||||||
let j=i+1,name="";
|
|
||||||
if(/[a-zA-Z]/.test(src[j])){ while(j<src.length&&/[a-zA-Z]/.test(src[j])){name+=src[j++];} }
|
|
||||||
else { name=src[j]; j++; }
|
|
||||||
out.push({t:"cmd",v:name}); i=j; continue;
|
|
||||||
}
|
|
||||||
if(c==="{"){ let depth=1,j=i+1; while(j<src.length&&depth>0){ if(src[j]==="{")depth++; else if(src[j]==="}")depth--; if(depth>0)j++; }
|
|
||||||
out.push({t:"grp",v:src.slice(i+1,j)}); i=j+1; continue; }
|
|
||||||
if(c==="^"||c==="_"){ out.push({t:c}); i++; continue; }
|
|
||||||
if(c===" "){ i++; continue; }
|
|
||||||
out.push({t:"chr",v:c}); i++;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(src,values){
|
|
||||||
const toks=tokenize(src); const nodes=[];
|
|
||||||
for(let i=0;i<toks.length;i++){
|
|
||||||
const tk=toks[i];
|
|
||||||
if(tk.t==="^"||tk.t==="_"){
|
|
||||||
const base=nodes.pop()||mi("");
|
|
||||||
const readScript=()=>{ const nxt=toks[++i]; return nxt&&nxt.t==="grp"?groupOf(render(nxt.v,values)):(nxt?renderOne(nxt,values):null)||mi(""); };
|
|
||||||
let sup=null,sub=null;
|
|
||||||
if(tk.t==="^") sup=readScript(); else sub=readScript();
|
|
||||||
// pair a complementary script if the next token supplies it (x_{..}^{..})
|
|
||||||
const nx=toks[i+1];
|
|
||||||
if(nx&&(nx.t==="^"||nx.t==="_")&&((nx.t==="^")!==(sup!=null))){
|
|
||||||
i++; if(nx.t==="^") sup=readScript(); else sub=readScript();
|
|
||||||
}
|
|
||||||
if(sub&&sup) nodes.push(el("msubsup",base,sub,sup));
|
|
||||||
else if(sub) nodes.push(el("msub",base,sub));
|
|
||||||
else nodes.push(el("msup",base,sup));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const consume=()=>toks[++i];
|
|
||||||
const n=renderOne(tk,values,consume);
|
|
||||||
if(Array.isArray(n)) nodes.push(...n); else if(n) nodes.push(n);
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
function groupOf(arr){ if(Array.isArray(arr)) return arr.length===1?arr[0]:el("mrow",...arr); return arr; }
|
|
||||||
|
|
||||||
function renderOne(tk,values,consume){
|
|
||||||
if(tk.t==="grp") return groupOf(render(tk.v,values));
|
|
||||||
if(tk.t==="chr"){
|
|
||||||
const c=tk.v;
|
|
||||||
if(/[0-9]/.test(c)) return mn(c);
|
|
||||||
if(/[a-zA-Z]/.test(c)) return mi(c);
|
|
||||||
if(c==="'") return mo("′");
|
|
||||||
return mo(c);
|
|
||||||
}
|
|
||||||
if(tk.t==="cmd"){
|
|
||||||
const v=tk.v;
|
|
||||||
if(v==="VAL"){ const g=consume&&consume(); const name=g?g.v:""; const full=(values&&values[name])||name;
|
|
||||||
return mtext(" "+short(full)+" "); }
|
|
||||||
if(v in GREEK) return mi(GREEK[v]);
|
|
||||||
if(v==="mathbb"||v==="mathcal"||v==="mathbf"||v==="boldsymbol"||v==="mathrm"||v==="text"||v==="operatorname"){
|
|
||||||
const g=consume&&consume(); const inner=g?g.v:"";
|
|
||||||
if(v==="mathbb") return mi(BB[inner]||inner);
|
|
||||||
if(v==="mathcal") return mi(CAL[inner]||inner);
|
|
||||||
if(v==="mathrm"||v==="operatorname"){ const t=el("mi",inner); t.setAttribute("mathvariant","normal"); return t; }
|
|
||||||
if(v==="text") return mtext(inner);
|
|
||||||
const b=groupOf(render(inner,values)); if(b&&b.setAttribute) b.setAttribute("mathvariant","bold"); return b;
|
|
||||||
}
|
|
||||||
if(v==="frac"){ const a=consume&&consume(),b=consume&&consume();
|
|
||||||
return el("mfrac",groupOf(render(a?a.v:"",values)),groupOf(render(b?b.v:"",values))); }
|
|
||||||
if(v==="sqrt"){ const a=consume&&consume(); return el("msqrt",groupOf(render(a?a.v:"",values))); }
|
|
||||||
if(v==="xleftarrow"){ const a=consume&&consume();
|
|
||||||
return el("mover",mo("←"),el("mtext",a?a.v.replace(/\\\$/,"$"):"")); }
|
|
||||||
if(v==="big"||v==="Big"||v==="bigg"||v==="Bigg"||v==="left"||v==="right"||v==="displaystyle"||v==="textstyle") return null;
|
|
||||||
// spacing control symbols: \, \: \; \ (backslash-space) -> spaces; \! -> nothing
|
|
||||||
if(v==="!") return null;
|
|
||||||
if(v==="," ){ const s=el("mspace"); s.setAttribute("width","0.17em"); return s; }
|
|
||||||
if(v===":" ){ const s=el("mspace"); s.setAttribute("width","0.22em"); return s; }
|
|
||||||
if(v===";" ){ const s=el("mspace"); s.setAttribute("width","0.28em"); return s; }
|
|
||||||
if(v===" " ){ const s=el("mspace"); s.setAttribute("width","0.33em"); return s; }
|
|
||||||
if(v in OPS){ const s=OPS[v]; if(s===" ") return mspace();
|
|
||||||
if(/^[a-z]+$/i.test(s)){ const o=el("mo",s); o.setAttribute("lspace","0.25em"); o.setAttribute("rspace","0.25em"); return o; }
|
|
||||||
return mo(s); }
|
|
||||||
if(v==="{"||v==="}"||v==="|") return mo(v==="|"?"∥":v);
|
|
||||||
if(v==="\\") return null;
|
|
||||||
return mi(v); // unknown: literal, never crash
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mathFor(latex,values){
|
|
||||||
const m=el("math"); m.setAttribute("display","block");
|
|
||||||
try{ m.appendChild(el("mrow",...render(latex,values))); }
|
|
||||||
catch(e){ m.appendChild(mtext(latex)); }
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Live UI
|
|
||||||
// ============================================================
|
|
||||||
const PARTY_ORDER=["setup-component","control-component-0","control-component-1",
|
|
||||||
"control-component-2","control-component-3","electoral-board","voting-server","verifier"];
|
|
||||||
const PHASES=["setup","cards","voting","tally","verify"];
|
|
||||||
const feed=document.getElementById("feed");
|
|
||||||
const statusEl=document.getElementById("statusText");
|
|
||||||
const replayBtn=document.getElementById("replay");
|
|
||||||
const partyList=document.getElementById("partyList");
|
|
||||||
const timeline=document.getElementById("timeline");
|
|
||||||
const partyEls={};
|
|
||||||
let curPhase="";
|
|
||||||
|
|
||||||
function label(p){ return p.replace("control-component-","CC").replace("electoral-board","Electoral Board")
|
|
||||||
.replace("voting-server","Voting Server").replace("setup-component","Setup").replace("verifier","Verifier"); }
|
|
||||||
|
|
||||||
function initUI(){
|
|
||||||
for(const p of PARTY_ORDER){
|
|
||||||
const d=document.createElement("div"); d.className="party";
|
|
||||||
d.innerHTML='<span class="dot"></span><span>'+label(p)+'</span>';
|
|
||||||
partyList.appendChild(d); partyEls[p]=d;
|
|
||||||
}
|
|
||||||
for(const ph of PHASES){ const s=document.createElement("span"); s.className="phase"; s.textContent=ph; s.dataset.ph=ph; timeline.appendChild(s); }
|
|
||||||
}
|
|
||||||
function setPhase(ph){
|
|
||||||
if(ph===curPhase) return; curPhase=ph;
|
|
||||||
for(const s of timeline.children){
|
|
||||||
const i=PHASES.indexOf(s.dataset.ph), c=PHASES.indexOf(ph);
|
|
||||||
s.className="phase"+(i<c?" done":i===c?" active":"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function markLive(party){
|
|
||||||
for(const p in partyEls) partyEls[p].classList.remove("live");
|
|
||||||
if(party && partyEls[party]) partyEls[party].classList.add("live");
|
|
||||||
}
|
|
||||||
function escapeHtml(s){ return (s||"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c])); }
|
|
||||||
|
|
||||||
function addOp(e){
|
|
||||||
if(e.phase) setPhase(e.phase);
|
|
||||||
markLive(e.party);
|
|
||||||
const card=document.createElement("div");
|
|
||||||
card.className="op k-"+e.kind;
|
|
||||||
const row=document.createElement("div"); row.className="row";
|
|
||||||
row.innerHTML='<span class="badge">'+escapeHtml(e.kind)+'</span>'+
|
|
||||||
'<span class="who">'+escapeHtml(e.party||"")+'</span>'+
|
|
||||||
'<span class="cap">'+escapeHtml(e.caption||"")+'</span>'+
|
|
||||||
'<span class="seq">#'+e.seq+'</span>';
|
|
||||||
card.appendChild(row);
|
|
||||||
if(e.latex) card.appendChild(mathFor(e.latex,e.values||{}));
|
|
||||||
if(e.values && Object.keys(e.values).length){
|
|
||||||
const vd=document.createElement("div"); vd.className="vals";
|
|
||||||
for(const k in e.values){
|
|
||||||
const full=e.values[k];
|
|
||||||
const chip=document.createElement("span"); chip.className="val"; chip.title="click to expand / copy";
|
|
||||||
const paint=()=>{ chip.innerHTML='<span class="k">'+escapeHtml(k)+'</span> = '+escapeHtml(chip.classList.contains("open")?full:short(full)); };
|
|
||||||
paint();
|
|
||||||
chip.onclick=()=>{ chip.classList.toggle("open"); paint();
|
|
||||||
if(chip.classList.contains("open")&&navigator.clipboard) navigator.clipboard.writeText(full).catch(()=>{}); };
|
|
||||||
vd.appendChild(chip);
|
|
||||||
}
|
|
||||||
card.appendChild(vd);
|
|
||||||
}
|
|
||||||
feed.appendChild(card);
|
|
||||||
feed.scrollTop=feed.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
initUI();
|
|
||||||
const es=new EventSource("/events");
|
|
||||||
let count=0;
|
|
||||||
es.addEventListener("op",ev=>{ const e=JSON.parse(ev.data); count++; addOp(e);
|
|
||||||
statusEl.textContent="Streaming live crypto operations… "+count+" so far"; });
|
|
||||||
es.addEventListener("done",ev=>{ markLive(null);
|
|
||||||
for(const s of timeline.children) s.className="phase done";
|
|
||||||
statusEl.innerHTML='<span class="done-banner">✓ Election complete and verified.</span> '+count+' cryptographic operations rendered live.';
|
|
||||||
replayBtn.classList.add("show");
|
|
||||||
es.close(); });
|
|
||||||
es.onerror=()=>{ if(count===0) statusEl.textContent="Waiting for the election to start…"; };
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1016,7 +1016,6 @@ r = <span class="red">833704513946265251405968611993787771762...</span>
|
||||||
<span class="cmt">"The output ciphertexts are the correct multi-exponentiations</span>
|
<span class="cmt">"The output ciphertexts are the correct multi-exponentiations</span>
|
||||||
<span class="cmt"> of the inputs with the permutation matrix as exponents."</span></div>
|
<span class="cmt"> of the inputs with the permutation matrix as exponents."</span></div>
|
||||||
<div class="analogy-box"><strong>Simplified intuition:</strong> Imagine you have 6 sealed boxes numbered 1-6. You rearrange them and put them in new boxes. The ProductArgument proves "I used each number exactly once" (it's a valid permutation). The MultiExponentiationArgument proves "the new boxes contain the same items as the old boxes" (the ciphertexts were correctly re-encrypted, not swapped for fakes).</div>
|
<div class="analogy-box"><strong>Simplified intuition:</strong> Imagine you have 6 sealed boxes numbered 1-6. You rearrange them and put them in new boxes. The ProductArgument proves "I used each number exactly once" (it's a valid permutation). The MultiExponentiationArgument proves "the new boxes contain the same items as the old boxes" (the ciphertexts were correctly re-encrypted, not swapped for fakes).</div>
|
||||||
<div class="insight-box"><strong>Watch it happen.</strong> Run <code>evote cockpit</code> (browser) or <code>evote cockpit --tmux</code> (terminal): as the mix-net executes, each of these sub-arguments is rendered as live typeset mathematics with the real runtime values, in the order it is constructed — the Pedersen commitment to the permutation matrix, then the Product / Hadamard / Zero / SingleValueProduct / MultiExponentiation arguments, then the partial decryptions. The tree above is not a diagram of the code; it <em>is</em> the code, narrating itself.</div>
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
package mixnet
|
package mixnet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/user/evote/pkg/hash"
|
"github.com/user/evote/pkg/hash"
|
||||||
emath "github.com/user/evote/pkg/math"
|
emath "github.com/user/evote/pkg/math"
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var oneBI = big.NewInt(1)
|
var oneBI = big.NewInt(1)
|
||||||
|
|
@ -103,19 +101,5 @@ func (ck CommitmentKey) CommitMatrix(A *emath.ZqMatrix, r *emath.ZqVector) *emat
|
||||||
for j := 0; j < A.NumCols(); j++ {
|
for j := 0; j < A.NumCols(); j++ {
|
||||||
commitments[j] = ck.Commit(A.GetColumn(j), r.Get(j))
|
commitments[j] = ck.Commit(A.GetColumn(j), r.Get(j))
|
||||||
}
|
}
|
||||||
result := emath.GqVectorOf(commitments...)
|
return emath.GqVectorOf(commitments...)
|
||||||
trace.EmitFunc(func() trace.Event {
|
|
||||||
return trace.Event{
|
|
||||||
Kind: trace.KindCommit,
|
|
||||||
Caption: fmt.Sprintf("Pedersen commitment to a %d×%d matrix (one per column)", A.NumRows(), A.NumCols()),
|
|
||||||
LaTeX: `\mathbf{c}_A = \mathrm{Comm}_{ck}(A;\, \mathbf{r}), \quad c_{A,j} = h^{r_j} \textstyle\prod_{i=1}^{n} g_i^{A_{ij}}`,
|
|
||||||
ASCII: "c_A,j = h^r_j · Π_i g_i^{A_ij} (Pedersen, per column)",
|
|
||||||
Values: map[string]string{
|
|
||||||
"rows": fmt.Sprintf("%d", A.NumRows()),
|
|
||||||
"cols": fmt.Sprintf("%d", A.NumCols()),
|
|
||||||
"c_A0": result.Get(0).Value().String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,6 @@ func GenHadamardArgument(
|
||||||
n := A.NumRows()
|
n := A.NumRows()
|
||||||
m := A.NumCols()
|
m := A.NumCols()
|
||||||
|
|
||||||
emitArgument("hadamard",
|
|
||||||
"Hadamard argument: prove the entrywise product of A's columns equals b",
|
|
||||||
`\text{HadamardArgument}:\ \mathbf{b} = \mathbf{a}_1 \circ \mathbf{a}_2 \circ \cdots \circ \mathbf{a}_m \quad(\circ = \text{entrywise product})`,
|
|
||||||
"HadamardArgument: b = a_1 ∘ a_2 ∘ … ∘ a_m (∘ = entrywise product)",
|
|
||||||
dims(m, n))
|
|
||||||
|
|
||||||
// 1. Compute intermediate products b_j = ∏_{i=0}^j A[:,i] (entrywise)
|
// 1. Compute intermediate products b_j = ∏_{i=0}^j A[:,i] (entrywise)
|
||||||
bIntermediate := make([]*emath.ZqVector, m)
|
bIntermediate := make([]*emath.ZqVector, m)
|
||||||
bIntermediate[0] = A.GetColumn(0)
|
bIntermediate[0] = A.GetColumn(0)
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,6 @@ func GenMultiExponentiationArgument(
|
||||||
m := A.NumCols()
|
m := A.NumCols()
|
||||||
zero, _ := emath.NewZqElement(big.NewInt(0), zqGroup)
|
zero, _ := emath.NewZqElement(big.NewInt(0), zqGroup)
|
||||||
|
|
||||||
emitArgument("multiexp",
|
|
||||||
"Multi-exponentiation argument: prove the target ciphertext is the committed re-encryption product",
|
|
||||||
`\text{MultiExpArgument}:\ C = \Big(\prod_{i=1}^{m}\prod_{j=1}^{n} C_{ij}^{A_{ij}}\Big)\cdot \mathrm{ReEnc}_{pk}(1;\rho)`,
|
|
||||||
"MultiExpArgument: C = Π_ij C_ij^{A_ij} · ReEnc_pk(1; ρ)",
|
|
||||||
dims(m, n))
|
|
||||||
|
|
||||||
// 1. Random values
|
// 1. Random values
|
||||||
a0 := emath.RandomZqVector(n, zqGroup)
|
a0 := emath.RandomZqVector(n, zqGroup)
|
||||||
r0 := emath.RandomZqElement(zqGroup)
|
r0 := emath.RandomZqElement(zqGroup)
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,6 @@ func GenProductArgument(
|
||||||
n := A.NumRows()
|
n := A.NumRows()
|
||||||
m := A.NumCols()
|
m := A.NumCols()
|
||||||
|
|
||||||
emitArgument("product",
|
|
||||||
"Product argument: prove the product of all matrix entries equals b",
|
|
||||||
`\text{ProductArgument}:\ \prod_{i=1}^{n}\prod_{j=1}^{m} A_{ij} = b \quad(\text{via Hadamard} \circ \text{SVP})`,
|
|
||||||
"ProductArgument: Π_ij A_ij = b (Hadamard ∘ single-value-product)",
|
|
||||||
dims(m, n))
|
|
||||||
|
|
||||||
if m == 1 {
|
if m == 1 {
|
||||||
// Single column: just use SVP directly
|
// Single column: just use SVP directly
|
||||||
svp := GenSingleValueProductArgument(cA.Get(0), b, A.GetColumn(0), r.Get(0), pk, ck, group)
|
svp := GenSingleValueProductArgument(cA.Get(0), b, A.GetColumn(0), r.Get(0), pk, ck, group)
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,6 @@ func GenShuffleArgument(
|
||||||
N := C.Size()
|
N := C.Size()
|
||||||
m, n := GetMatrixDimensions(N)
|
m, n := GetMatrixDimensions(N)
|
||||||
|
|
||||||
emitArgument("shuffle",
|
|
||||||
"Shuffle argument: prove C' is a permutation + re-encryption of C, without revealing π",
|
|
||||||
`\text{ShuffleArgument}:\ \prod_i C_i^{\,x^{i}} \;\sim\; \prod_i C'^{\,x^{\pi(i)}}_i`,
|
|
||||||
"ShuffleArgument: Π_i C_i^{x^i} ~ Π_i C'_i^{x^π(i)} (bound via c_A, c_B; challenges x,y,z)",
|
|
||||||
dims(m, n))
|
|
||||||
|
|
||||||
// 1. Convert permutation to m×n matrix A
|
// 1. Convert permutation to m×n matrix A
|
||||||
aCols := make([]*emath.ZqVector, m)
|
aCols := make([]*emath.ZqVector, m)
|
||||||
rA := emath.RandomZqVector(m, zqGroup)
|
rA := emath.RandomZqVector(m, zqGroup)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package mixnet
|
package mixnet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/user/evote/pkg/elgamal"
|
"github.com/user/evote/pkg/elgamal"
|
||||||
|
|
@ -34,12 +33,6 @@ func GenSingleValueProductArgument(
|
||||||
n := a.Size()
|
n := a.Size()
|
||||||
zero, _ := emath.NewZqElement(big.NewInt(0), zqGroup)
|
zero, _ := emath.NewZqElement(big.NewInt(0), zqGroup)
|
||||||
|
|
||||||
emitArgument("svp",
|
|
||||||
"Single-value product argument: prove the product of a committed vector equals b",
|
|
||||||
`\text{SingleValueProductArgument}:\ \prod_{i=1}^{n} a_i = b, \quad c_a = \mathrm{Comm}(a; r)`,
|
|
||||||
"SingleValueProductArgument: Π_i a_i = b for committed vector a",
|
|
||||||
map[string]string{"n": fmt.Sprintf("%d", n), "b": b.Value().String()})
|
|
||||||
|
|
||||||
// 1. Compute partial products b_k = Π_{i=0}^k a_i
|
// 1. Compute partial products b_k = Π_{i=0}^k a_i
|
||||||
bPartial := make([]emath.ZqElement, n)
|
bPartial := make([]emath.ZqElement, n)
|
||||||
bPartial[0] = a.Get(0)
|
bPartial[0] = a.Get(0)
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package mixnet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
// emitArgument publishes a KindArgument trace event for one Bayer-Groth
|
|
||||||
// sub-argument, mirroring the *ArgumentService classes of the Swiss Post
|
|
||||||
// crypto-primitives library. Cheap when tracing is off.
|
|
||||||
func emitArgument(name, caption, latex, ascii string, values map[string]string) {
|
|
||||||
trace.EmitFunc(func() trace.Event {
|
|
||||||
return trace.Event{
|
|
||||||
Kind: trace.KindArgument,
|
|
||||||
Caption: caption,
|
|
||||||
LaTeX: latex,
|
|
||||||
ASCII: ascii,
|
|
||||||
Values: values,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func dims(m, n int) map[string]string {
|
|
||||||
return map[string]string{"m": fmt.Sprintf("%d", m), "n": fmt.Sprintf("%d", n)}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
package mixnet
|
package mixnet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/user/evote/pkg/elgamal"
|
"github.com/user/evote/pkg/elgamal"
|
||||||
emath "github.com/user/evote/pkg/math"
|
emath "github.com/user/evote/pkg/math"
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// VerifiableShuffle holds the result of a verifiable shuffle.
|
// VerifiableShuffle holds the result of a verifiable shuffle.
|
||||||
|
|
@ -33,20 +30,6 @@ func GenVerifiableShuffle(
|
||||||
// Generate shuffle argument
|
// Generate shuffle argument
|
||||||
arg := GenShuffleArgument(C, shuffle.Shuffled, shuffle.Perm, shuffle.Rho, pk, ck, group)
|
arg := GenShuffleArgument(C, shuffle.Shuffled, shuffle.Perm, shuffle.Rho, pk, ck, group)
|
||||||
|
|
||||||
trace.EmitFunc(func() trace.Event {
|
|
||||||
return trace.Event{
|
|
||||||
Kind: trace.KindShuffle,
|
|
||||||
Caption: fmt.Sprintf("Bayer-Groth verifiable shuffle of %d ciphertexts", N),
|
|
||||||
LaTeX: `\mathbf{C}' = \big\{\, \mathrm{ReEnc}_{pk}\!\big(C_{\pi(i)};\, \rho_i\big) \,\big\}_{i=1}^{\VAL{N}}, \qquad \pi \xleftarrow{\$} S_{\VAL{N}}`,
|
|
||||||
ASCII: "C' = { ReEnc_pk(C_π(i); ρ_i) } for a secret permutation π",
|
|
||||||
Values: map[string]string{
|
|
||||||
"N": fmt.Sprintf("%d", N),
|
|
||||||
"m": fmt.Sprintf("%d", N/n),
|
|
||||||
"n": fmt.Sprintf("%d", n),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return VerifiableShuffle{
|
return VerifiableShuffle{
|
||||||
ShuffledCiphertexts: shuffle.Shuffled,
|
ShuffledCiphertexts: shuffle.Shuffled,
|
||||||
Argument: arg,
|
Argument: arg,
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,6 @@ func GenZeroArgument(
|
||||||
n := A.NumRows()
|
n := A.NumRows()
|
||||||
m := A.NumCols()
|
m := A.NumCols()
|
||||||
|
|
||||||
emitArgument("zero",
|
|
||||||
"Zero argument: prove a bilinear map over A and B vanishes",
|
|
||||||
`\text{ZeroArgument}:\ \sum_{i=1}^{m} \mathbf{a}_i \ast_y \mathbf{b}_i = 0, \quad \mathbf{a}\ast_y\mathbf{b}=\textstyle\sum_j a_j b_j y^{j}`,
|
|
||||||
"ZeroArgument: Σ_i (a_i ∗_y b_i) = 0 with bilinear star-map ∗_y",
|
|
||||||
dims(m, n))
|
|
||||||
|
|
||||||
// 1. Prepend random a_0 to A, append random b_m to B
|
// 1. Prepend random a_0 to A, append random b_m to B
|
||||||
a0 := emath.RandomZqVector(n, zqGroup)
|
a0 := emath.RandomZqVector(n, zqGroup)
|
||||||
r0 := emath.RandomZqElement(zqGroup)
|
r0 := emath.RandomZqElement(zqGroup)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/user/evote/pkg/elgamal"
|
"github.com/user/evote/pkg/elgamal"
|
||||||
"github.com/user/evote/pkg/hash"
|
"github.com/user/evote/pkg/hash"
|
||||||
emath "github.com/user/evote/pkg/math"
|
emath "github.com/user/evote/pkg/math"
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
"github.com/user/evote/pkg/transport"
|
"github.com/user/evote/pkg/transport"
|
||||||
"github.com/user/evote/pkg/zkp"
|
"github.com/user/evote/pkg/zkp"
|
||||||
)
|
)
|
||||||
|
|
@ -52,7 +51,6 @@ func (c *Ceremony) RunSetup() error {
|
||||||
group := cfg.Group
|
group := cfg.Group
|
||||||
zq := emath.ZqGroupFromGqGroup(group)
|
zq := emath.ZqGroupFromGqGroup(group)
|
||||||
|
|
||||||
trace.Phase("setup")
|
|
||||||
c.logf("\n--- SETUP PHASE (multi-party) ---")
|
c.logf("\n--- SETUP PHASE (multi-party) ---")
|
||||||
|
|
||||||
// 1. Encoding primes (public, computed by the setup component).
|
// 1. Encoding primes (public, computed by the setup component).
|
||||||
|
|
@ -170,7 +168,6 @@ func (p *ControlComponent) handleGenCCKeys(env *transport.Envelope) (*transport.
|
||||||
group := cfg.Group
|
group := cfg.Group
|
||||||
zq := emath.ZqGroupFromGqGroup(group)
|
zq := emath.ZqGroupFromGqGroup(group)
|
||||||
|
|
||||||
trace.SetContext(p.id.Name, "setup")
|
|
||||||
kp := elgamal.GenKeyPair(group, req.NumOptions)
|
kp := elgamal.GenKeyPair(group, req.NumOptions)
|
||||||
proofs := make([]zkp.SchnorrProof, req.NumOptions)
|
proofs := make([]zkp.SchnorrProof, req.NumOptions)
|
||||||
wireProofs := make([]wireSchnorr, req.NumOptions)
|
wireProofs := make([]wireSchnorr, req.NumOptions)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
emath "github.com/user/evote/pkg/math"
|
emath "github.com/user/evote/pkg/math"
|
||||||
"github.com/user/evote/pkg/mixnet"
|
"github.com/user/evote/pkg/mixnet"
|
||||||
"github.com/user/evote/pkg/returncodes"
|
"github.com/user/evote/pkg/returncodes"
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
"github.com/user/evote/pkg/transport"
|
"github.com/user/evote/pkg/transport"
|
||||||
"github.com/user/evote/pkg/zkp"
|
"github.com/user/evote/pkg/zkp"
|
||||||
)
|
)
|
||||||
|
|
@ -47,7 +46,6 @@ type finalResp struct {
|
||||||
// posts its shuffle and decryption proofs to the public transcript (the
|
// posts its shuffle and decryption proofs to the public transcript (the
|
||||||
// bulletin board), which the verifier re-checks in RunVerify.
|
// bulletin board), which the verifier re-checks in RunVerify.
|
||||||
func (c *Ceremony) RunTally() error {
|
func (c *Ceremony) RunTally() error {
|
||||||
trace.Phase("tally")
|
|
||||||
c.logf("\n--- TALLY PHASE (multi-party) ---")
|
c.logf("\n--- TALLY PHASE (multi-party) ---")
|
||||||
group := c.Config.Group
|
group := c.Config.Group
|
||||||
|
|
||||||
|
|
@ -138,7 +136,6 @@ func (p *ControlComponent) handleShuffle(env *transport.Envelope) (*transport.En
|
||||||
// Remaining public key = CCs[stage..] + EB, read from the public transcript.
|
// Remaining public key = CCs[stage..] + EB, read from the public transcript.
|
||||||
remaining := remainingPK(p.cer.Transcript, req.Stage, cfg.NumCCs)
|
remaining := remainingPK(p.cer.Transcript, req.Stage, cfg.NumCCs)
|
||||||
|
|
||||||
trace.SetContext(p.id.Name, "tally")
|
|
||||||
vs := mixnet.GenVerifiableShuffle(in, remaining, group)
|
vs := mixnet.GenVerifiableShuffle(in, remaining, group)
|
||||||
|
|
||||||
// Partial decrypt with this CC's private key; produce decryption proofs.
|
// Partial decrypt with this CC's private key; produce decryption proofs.
|
||||||
|
|
@ -151,19 +148,6 @@ func (p *ControlComponent) handleShuffle(env *transport.Envelope) (*transport.En
|
||||||
decProofs[i] = zkp.GenDecryptionProof(ct, p.st.keyPair.SK, p.st.keyPair.PK, msg, group)
|
decProofs[i] = zkp.GenDecryptionProof(ct, p.st.keyPair.SK, p.st.keyPair.PK, msg, group)
|
||||||
}
|
}
|
||||||
out := elgamal.NewCiphertextVector(decrypted)
|
out := elgamal.NewCiphertextVector(decrypted)
|
||||||
trace.EmitFunc(func() trace.Event {
|
|
||||||
return trace.Event{
|
|
||||||
Party: p.id.Name,
|
|
||||||
Kind: trace.KindDecrypt,
|
|
||||||
Caption: fmt.Sprintf("%s partially decrypts %d ciphertexts and proves correctness", p.id.Name, out.Size()),
|
|
||||||
LaTeX: `\phi_i' = \phi_i \cdot \gamma_i^{-sk}, \quad \text{proof: } \log_g pk = \log_{\gamma_i} (\gamma_i^{sk})`,
|
|
||||||
ASCII: "φ'_i = φ_i · γ_i^{-sk} + ZK proof log_g(pk) = log_γ(γ^sk)",
|
|
||||||
Values: map[string]string{
|
|
||||||
"count": fmt.Sprintf("%d", out.Size()),
|
|
||||||
"proof_e": decProofs[0].E.Value().String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Post proofs to the transcript (bulletin board).
|
// Post proofs to the transcript (bulletin board).
|
||||||
p.cer.Transcript.Shuffles = append(p.cer.Transcript.Shuffles, vs)
|
p.cer.Transcript.Shuffles = append(p.cer.Transcript.Shuffles, vs)
|
||||||
|
|
@ -187,7 +171,6 @@ func (p *ElectoralBoard) handleFinalMix(env *transport.Envelope) (*transport.Env
|
||||||
return nil, fmt.Errorf("eb final input: %w", err)
|
return nil, fmt.Errorf("eb final input: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
trace.SetContext(p.id.Name, "tally")
|
|
||||||
vs := mixnet.GenVerifiableShuffle(in, p.st.keyPair.PK, group)
|
vs := mixnet.GenVerifiableShuffle(in, p.st.keyPair.PK, group)
|
||||||
p.cer.Transcript.Shuffles = append(p.cer.Transcript.Shuffles, vs)
|
p.cer.Transcript.Shuffles = append(p.cer.Transcript.Shuffles, vs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
package party
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestCeremonyEmitsLiveCryptoEvents runs the full ceremony with a trace sink
|
|
||||||
// attached and confirms that the real cryptographic operations emit events
|
|
||||||
// carrying live runtime values — the foundation of the "watch the math execute"
|
|
||||||
// cockpit. It checks that each headline operation kind appears with non-empty
|
|
||||||
// values and correct LaTeX.
|
|
||||||
func TestCeremonyEmitsLiveCryptoEvents(t *testing.T) {
|
|
||||||
sink := &trace.SliceSink{}
|
|
||||||
unsub := trace.Subscribe(sink)
|
|
||||||
defer unsub()
|
|
||||||
|
|
||||||
// 6 voters → N=6 ballots → the shuffle matrix is 2×3 (m>1), so the full
|
|
||||||
// Bayer-Groth argument tree runs (Hadamard/Zero only fire when m>1).
|
|
||||||
cfg := testConfig(t, 6, 3)
|
|
||||||
c, err := NewCeremony(cfg, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewCeremony: %v", err)
|
|
||||||
}
|
|
||||||
if err := c.RunSetup(); err != nil {
|
|
||||||
t.Fatalf("RunSetup: %v", err)
|
|
||||||
}
|
|
||||||
if err := c.RunCards(); err != nil {
|
|
||||||
t.Fatalf("RunCards: %v", err)
|
|
||||||
}
|
|
||||||
if err := c.RunVoting([][]int{{0}, {1}, {2}, {0}, {1}, {2}}); err != nil {
|
|
||||||
t.Fatalf("RunVoting: %v", err)
|
|
||||||
}
|
|
||||||
if err := c.RunTally(); err != nil {
|
|
||||||
t.Fatalf("RunTally: %v", err)
|
|
||||||
}
|
|
||||||
if err := c.RunVerify(); err != nil {
|
|
||||||
t.Fatalf("RunVerify: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
events := sink.Snapshot()
|
|
||||||
if len(events) == 0 {
|
|
||||||
t.Fatal("no trace events emitted during a full ceremony")
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := map[trace.Kind]trace.Event{}
|
|
||||||
for _, e := range events {
|
|
||||||
seen[e.Kind] = e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every headline operation must have fired at least once — including the
|
|
||||||
// deep Bayer-Groth layers (commitment, sub-arguments, partial decryption)
|
|
||||||
// that mirror the Swiss Post crypto-primitives class structure.
|
|
||||||
for _, k := range []trace.Kind{
|
|
||||||
trace.KindSign, // Ed25519 transport signatures
|
|
||||||
trace.KindKeyEx, // X25519 ECDH (card delivery)
|
|
||||||
trace.KindEncrypt, // ballot encryption
|
|
||||||
trace.KindChallenge, // Fiat-Shamir challenge
|
|
||||||
trace.KindShuffle, // Bayer-Groth mix-net (top level)
|
|
||||||
trace.KindCommit, // Pedersen matrix commitment
|
|
||||||
trace.KindArgument, // a Bayer-Groth sub-argument
|
|
||||||
trace.KindDecrypt, // CC partial decryption + proof
|
|
||||||
} {
|
|
||||||
e, ok := seen[k]
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("no %q event emitted", k)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if e.LaTeX == "" {
|
|
||||||
t.Errorf("%q event has empty LaTeX", k)
|
|
||||||
}
|
|
||||||
if len(e.Values) == 0 {
|
|
||||||
t.Errorf("%q event carries no live values", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All five Bayer-Groth sub-arguments must appear (matching the Swiss Post
|
|
||||||
// *ArgumentService classes). We identify them by a keyword in the caption.
|
|
||||||
argCaptions := map[string]bool{}
|
|
||||||
for _, e := range events {
|
|
||||||
if e.Kind == trace.KindArgument {
|
|
||||||
argCaptions[e.Caption] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, want := range []string{"Shuffle argument", "Product argument", "Hadamard argument",
|
|
||||||
"Zero argument", "Single-value product argument", "Multi-exponentiation argument"} {
|
|
||||||
found := false
|
|
||||||
for cap := range argCaptions {
|
|
||||||
if len(cap) >= len(want) && cap[:len(want)] == want {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("sub-argument %q never appeared in the trace", want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spot-check that the shuffle event reports the padded ballot count (N>=3).
|
|
||||||
if sh, ok := seen[trace.KindShuffle]; ok {
|
|
||||||
if sh.Values["N"] == "" {
|
|
||||||
t.Error("shuffle event missing N")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events must be tagged with a phase, and signatures with the acting party.
|
|
||||||
if seen[trace.KindSign].Party == "" {
|
|
||||||
t.Error("signature event not attributed to a party")
|
|
||||||
}
|
|
||||||
if seen[trace.KindShuffle].Phase != "tally" {
|
|
||||||
t.Errorf("shuffle phase = %q, want tally", seen[trace.KindShuffle].Phase)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("captured %d live crypto events across %d kinds", len(events), len(seen))
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
|
|
||||||
"github.com/user/evote/pkg/elgamal"
|
"github.com/user/evote/pkg/elgamal"
|
||||||
"github.com/user/evote/pkg/mixnet"
|
"github.com/user/evote/pkg/mixnet"
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunVerify has the verifier party independently re-check the public transcript:
|
// RunVerify has the verifier party independently re-check the public transcript:
|
||||||
|
|
@ -14,7 +13,6 @@ import (
|
||||||
// decryptions). It returns nil only if every check passes. The verifier holds no
|
// decryptions). It returns nil only if every check passes. The verifier holds no
|
||||||
// secret — it works purely from the bulletin-board transcript.
|
// secret — it works purely from the bulletin-board transcript.
|
||||||
func (c *Ceremony) RunVerify() error {
|
func (c *Ceremony) RunVerify() error {
|
||||||
trace.SetContext(NameVerifier, "verify")
|
|
||||||
c.logf("\n--- VERIFICATION PHASE (multi-party) ---")
|
c.logf("\n--- VERIFICATION PHASE (multi-party) ---")
|
||||||
tr := c.Transcript
|
tr := c.Transcript
|
||||||
group := c.Config.Group
|
group := c.Config.Group
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/user/evote/pkg/hash"
|
"github.com/user/evote/pkg/hash"
|
||||||
emath "github.com/user/evote/pkg/math"
|
emath "github.com/user/evote/pkg/math"
|
||||||
"github.com/user/evote/pkg/returncodes"
|
"github.com/user/evote/pkg/returncodes"
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
"github.com/user/evote/pkg/transport"
|
"github.com/user/evote/pkg/transport"
|
||||||
"github.com/user/evote/pkg/zkp"
|
"github.com/user/evote/pkg/zkp"
|
||||||
)
|
)
|
||||||
|
|
@ -42,7 +41,6 @@ type wireBallot struct {
|
||||||
// server routes each ballot to all CCs for proof verification and stores the
|
// server routes each ballot to all CCs for proof verification and stores the
|
||||||
// accepted ballots. Selections are provided per voter (indexed by voter).
|
// accepted ballots. Selections are provided per voter (indexed by voter).
|
||||||
func (c *Ceremony) RunVoting(selections [][]int) error {
|
func (c *Ceremony) RunVoting(selections [][]int) error {
|
||||||
trace.Phase("voting")
|
|
||||||
c.logf("\n--- VOTING PHASE (multi-party) ---")
|
c.logf("\n--- VOTING PHASE (multi-party) ---")
|
||||||
for v, voter := range c.Voters {
|
for v, voter := range c.Voters {
|
||||||
sel := []int{0}
|
sel := []int{0}
|
||||||
|
|
@ -83,20 +81,6 @@ func (p *VoterClient) castBallot(selected []int) (*transport.Envelope, error) {
|
||||||
}
|
}
|
||||||
msgRandomness := emath.RandomZqElement(zq)
|
msgRandomness := emath.RandomZqElement(zq)
|
||||||
ct := elgamal.Encrypt(elgamal.NewMessage(emath.GqVectorOf(msgElems...)), msgRandomness, p.st.electionPK)
|
ct := elgamal.Encrypt(elgamal.NewMessage(emath.GqVectorOf(msgElems...)), msgRandomness, p.st.electionPK)
|
||||||
trace.EmitFunc(func() trace.Event {
|
|
||||||
return trace.Event{
|
|
||||||
Party: p.id.Name,
|
|
||||||
Kind: trace.KindEncrypt,
|
|
||||||
Caption: fmt.Sprintf("%s encrypts ballot (ElGamal over G_q)", p.id.Name),
|
|
||||||
LaTeX: `E_1 = (\gamma,\, \boldsymbol{\phi}) = \big(g^{r},\; pk^{r}\cdot m\big), \qquad r \xleftarrow{\$} \mathbb{Z}_q`,
|
|
||||||
ASCII: "E1 = (g^r, pk^r · m), r ← Z_q",
|
|
||||||
Values: map[string]string{
|
|
||||||
"r": msgRandomness.Value().String(),
|
|
||||||
"gamma": ct.Gamma.Value().String(),
|
|
||||||
"m0": voteElem.Value().String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 2. Verification-card key pair.
|
// 2. Verification-card key pair.
|
||||||
p.st.vcSK = emath.RandomZqElement(zq)
|
p.st.vcSK = emath.RandomZqElement(zq)
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
package trace
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
// Short elides a long value for compact display: keeps the first and last few
|
|
||||||
// characters. Renderers that want the full value use the raw Values map.
|
|
||||||
func Short(s string) string {
|
|
||||||
const head, tail = 8, 6
|
|
||||||
if len(s) <= head+tail+1 {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return s[:head] + "…" + s[len(s)-tail:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChanSink forwards events to a buffered channel, dropping if the consumer is
|
|
||||||
// too slow (tracing must never stall the ceremony). Dropped counts are tracked.
|
|
||||||
type ChanSink struct {
|
|
||||||
C chan Event
|
|
||||||
dropped uint64
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewChanSink creates a ChanSink with the given buffer size.
|
|
||||||
func NewChanSink(buffer int) *ChanSink {
|
|
||||||
return &ChanSink{C: make(chan Event, buffer)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ChanSink) Handle(e Event) {
|
|
||||||
select {
|
|
||||||
case s.C <- e:
|
|
||||||
default:
|
|
||||||
s.mu.Lock()
|
|
||||||
s.dropped++
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dropped returns how many events were dropped due to a full buffer.
|
|
||||||
func (s *ChanSink) Dropped() uint64 {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return s.dropped
|
|
||||||
}
|
|
||||||
|
|
||||||
// SliceSink collects events in memory (used in tests).
|
|
||||||
type SliceSink struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
Events []Event
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SliceSink) Handle(e Event) {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.Events = append(s.Events, e)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot returns a copy of the collected events.
|
|
||||||
func (s *SliceSink) Snapshot() []Event {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
out := make([]Event, len(s.Events))
|
|
||||||
copy(out, s.Events)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
// Package trace is a live event stream for the cryptographic operations the
|
|
||||||
// system performs. Each meaningful operation (sampling, encryption, commitment,
|
|
||||||
// Fiat-Shamir challenge, shuffle, signature, key agreement) emits one Event
|
|
||||||
// carrying its notation as a LaTeX template plus the REAL runtime values, so a
|
|
||||||
// renderer can show the mathematics that is executing at the instant it runs.
|
|
||||||
//
|
|
||||||
// The stream is surface-agnostic: a terminal view renders ASCII/Unicode, a
|
|
||||||
// browser view renders typeset LaTeX (KaTeX) — both consume the same events.
|
|
||||||
//
|
|
||||||
// Tracing is off by default and has near-zero cost when no sink is attached:
|
|
||||||
// Emit returns immediately if there are no subscribers. Instrumentation sites
|
|
||||||
// wrap value formatting in a closure (via EmitFunc) so that formatting work is
|
|
||||||
// skipped entirely when tracing is off.
|
|
||||||
package trace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Kind categorizes an operation so a renderer can group or icon it.
|
|
||||||
type Kind string
|
|
||||||
|
|
||||||
const (
|
|
||||||
KindSample Kind = "sample" // random sampling from Z_q / permutations
|
|
||||||
KindEncrypt Kind = "encrypt" // ElGamal encryption
|
|
||||||
KindDecrypt Kind = "decrypt" // (partial) decryption
|
|
||||||
KindExp Kind = "exp" // group exponentiation of note
|
|
||||||
KindCommit Kind = "commit" // Pedersen commitment
|
|
||||||
KindChallenge Kind = "challenge" // Fiat-Shamir challenge derivation
|
|
||||||
KindProof Kind = "proof" // ZK proof generation / verification
|
|
||||||
KindArgument Kind = "argument" // a Bayer-Groth sub-argument (product/Hadamard/zero/SVP/multi-exp)
|
|
||||||
KindShuffle Kind = "shuffle" // mix-net permutation + re-encryption
|
|
||||||
KindSign Kind = "sign" // Ed25519 signature (transport)
|
|
||||||
KindVerify Kind = "verify" // signature / proof verification
|
|
||||||
KindKeyEx Kind = "keyex" // X25519 key agreement
|
|
||||||
KindNote Kind = "note" // phase/step narration, no math
|
|
||||||
)
|
|
||||||
|
|
||||||
// Event is one instrumented operation.
|
|
||||||
type Event struct {
|
|
||||||
Seq uint64 `json:"seq"` // monotonic sequence number
|
|
||||||
Party string `json:"party"` // which stakeholder performed it
|
|
||||||
Phase string `json:"phase"` // setup / voting / tally / verify
|
|
||||||
Kind Kind `json:"kind"` // operation category
|
|
||||||
Caption string `json:"caption"` // one-line human description
|
|
||||||
LaTeX string `json:"latex"` // notation, with \VAL{name} placeholders
|
|
||||||
ASCII string `json:"ascii"` // terminal-friendly fallback (optional)
|
|
||||||
Values map[string]string `json:"values"` // placeholder name -> real runtime value (decimal/hex)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sink receives events. Implementations must be safe for concurrent use and must
|
|
||||||
// not block the emitter for long (buffer or drop internally if needed).
|
|
||||||
type Sink interface {
|
|
||||||
Handle(Event)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
mu sync.RWMutex
|
|
||||||
sinks []Sink
|
|
||||||
active atomic.Bool // fast path: true when at least one sink is attached
|
|
||||||
seqCtr atomic.Uint64
|
|
||||||
curParty atomic.Value // string: default party if an emit omits one
|
|
||||||
curPhase atomic.Value // string: current phase
|
|
||||||
)
|
|
||||||
|
|
||||||
// Subscribe attaches a sink and returns an unsubscribe function.
|
|
||||||
func Subscribe(s Sink) func() {
|
|
||||||
mu.Lock()
|
|
||||||
sinks = append(sinks, s)
|
|
||||||
active.Store(true)
|
|
||||||
mu.Unlock()
|
|
||||||
return func() {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
for i, x := range sinks {
|
|
||||||
if x == s {
|
|
||||||
sinks = append(sinks[:i], sinks[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
active.Store(len(sinks) > 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enabled reports whether any sink is attached. Instrumentation can check this
|
|
||||||
// to skip expensive value formatting.
|
|
||||||
func Enabled() bool { return active.Load() }
|
|
||||||
|
|
||||||
// SetContext sets the default party/phase stamped onto events that omit them.
|
|
||||||
func SetContext(party, phase string) {
|
|
||||||
curParty.Store(party)
|
|
||||||
curPhase.Store(phase)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase sets just the current phase.
|
|
||||||
func Phase(phase string) { curPhase.Store(phase) }
|
|
||||||
|
|
||||||
func ctxParty() string {
|
|
||||||
if v, ok := curParty.Load().(string); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
func ctxPhase() string {
|
|
||||||
if v, ok := curPhase.Load().(string); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit publishes an event. It fills Seq, and Party/Phase from context if unset.
|
|
||||||
// Returns immediately when tracing is disabled.
|
|
||||||
func Emit(e Event) {
|
|
||||||
if !active.Load() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.Seq = seqCtr.Add(1)
|
|
||||||
if e.Party == "" {
|
|
||||||
e.Party = ctxParty()
|
|
||||||
}
|
|
||||||
if e.Phase == "" {
|
|
||||||
e.Phase = ctxPhase()
|
|
||||||
}
|
|
||||||
mu.RLock()
|
|
||||||
current := sinks
|
|
||||||
mu.RUnlock()
|
|
||||||
for _, s := range current {
|
|
||||||
s.Handle(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmitFunc builds and emits an event only when tracing is enabled, so callers
|
|
||||||
// can defer all value formatting into build. Use this at hot instrumentation
|
|
||||||
// sites where formatting big integers would otherwise cost even when tracing off.
|
|
||||||
func EmitFunc(build func() Event) {
|
|
||||||
if !active.Load() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Emit(build())
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
package trace
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestDisabledByDefaultIsCheap(t *testing.T) {
|
|
||||||
if Enabled() {
|
|
||||||
t.Fatal("tracing should be off with no sinks")
|
|
||||||
}
|
|
||||||
called := false
|
|
||||||
EmitFunc(func() Event { called = true; return Event{} })
|
|
||||||
if called {
|
|
||||||
t.Fatal("EmitFunc built an event while tracing was disabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubscribeReceivesEvents(t *testing.T) {
|
|
||||||
sink := &SliceSink{}
|
|
||||||
unsub := Subscribe(sink)
|
|
||||||
defer unsub()
|
|
||||||
|
|
||||||
SetContext("control-component-0", "tally")
|
|
||||||
Emit(Event{
|
|
||||||
Kind: KindChallenge,
|
|
||||||
Caption: "Fiat-Shamir challenge",
|
|
||||||
LaTeX: `e = \mathcal{H}(g, y, c) \bmod q`,
|
|
||||||
Values: map[string]string{"e": "12345678901234567890"},
|
|
||||||
})
|
|
||||||
Emit(Event{Party: "voter-0001", Kind: KindEncrypt, Caption: "encrypt ballot"})
|
|
||||||
|
|
||||||
got := sink.Snapshot()
|
|
||||||
if len(got) != 2 {
|
|
||||||
t.Fatalf("got %d events, want 2", len(got))
|
|
||||||
}
|
|
||||||
if got[0].Seq == 0 || got[1].Seq <= got[0].Seq {
|
|
||||||
t.Fatalf("sequence numbers not monotonic: %d, %d", got[0].Seq, got[1].Seq)
|
|
||||||
}
|
|
||||||
if got[0].Party != "control-component-0" || got[0].Phase != "tally" {
|
|
||||||
t.Fatalf("context not stamped: %+v", got[0])
|
|
||||||
}
|
|
||||||
if got[1].Party != "voter-0001" {
|
|
||||||
t.Fatalf("explicit party overridden: %+v", got[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
unsub()
|
|
||||||
if Enabled() {
|
|
||||||
t.Fatal("unsubscribe did not clear the last sink")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShortElision(t *testing.T) {
|
|
||||||
if Short("12345") != "12345" {
|
|
||||||
t.Fatal("short values must pass through")
|
|
||||||
}
|
|
||||||
long := "1234567890123456789012345"
|
|
||||||
e := Short(long)
|
|
||||||
if e == long || len(e) >= len(long) {
|
|
||||||
t.Fatalf("long value not elided: %q", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/user/evote/pkg/symmetric"
|
"github.com/user/evote/pkg/symmetric"
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
"github.com/user/evote/pkg/transportsec"
|
"github.com/user/evote/pkg/transportsec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -55,26 +54,11 @@ func (id *Identity) NewSecureChannel(peerName string, peerXPub []byte) (*SecureC
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ECDH %s<->%s: %w", id.Name, peerName, err)
|
return nil, fmt.Errorf("ECDH %s<->%s: %w", id.Name, peerName, err)
|
||||||
}
|
}
|
||||||
sessionKey := deriveSessionKey(shared, id.XPub, peerXPub)
|
|
||||||
trace.EmitFunc(func() trace.Event {
|
|
||||||
return trace.Event{
|
|
||||||
Party: id.Name,
|
|
||||||
Kind: trace.KindKeyEx,
|
|
||||||
Caption: fmt.Sprintf("%s ⇄ %s: X25519 key agreement", id.Name, peerName),
|
|
||||||
LaTeX: `s = a \cdot B = b \cdot A \in \mathbb{X}_{25519}, \qquad k = \mathrm{SHA256}(\text{ctx} \,\Vert\, A \,\Vert\, B \,\Vert\, s)`,
|
|
||||||
ASCII: "s = a·B = b·A ; k = SHA256(ctx ‖ A ‖ B ‖ s)",
|
|
||||||
Values: map[string]string{
|
|
||||||
"peer": peerName,
|
|
||||||
"shared": hexOf(shared),
|
|
||||||
"session": hexOf(sessionKey),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return &SecureChannel{
|
return &SecureChannel{
|
||||||
local: id,
|
local: id,
|
||||||
peerName: peerName,
|
peerName: peerName,
|
||||||
peerXPub: peerXPub,
|
peerXPub: peerXPub,
|
||||||
sessionKey: sessionKey,
|
sessionKey: deriveSessionKey(shared, id.XPub, peerXPub),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
"github.com/user/evote/pkg/transportsec"
|
"github.com/user/evote/pkg/transportsec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -60,34 +59,9 @@ func (e *Envelope) Seal(sender *Identity) error {
|
||||||
return fmt.Errorf("seal %s->%s: %w", e.From, e.To, err)
|
return fmt.Errorf("seal %s->%s: %w", e.From, e.To, err)
|
||||||
}
|
}
|
||||||
e.Signature = sig
|
e.Signature = sig
|
||||||
trace.EmitFunc(func() trace.Event {
|
|
||||||
return trace.Event{
|
|
||||||
Party: e.From,
|
|
||||||
Kind: trace.KindSign,
|
|
||||||
Caption: fmt.Sprintf("%s signs %q → %s", e.From, e.Type, e.To),
|
|
||||||
LaTeX: `\sigma \gets \mathrm{Ed25519.Sign}_{sk}\!\big(\mathrm{SHA256}(\text{envelope})\big),\quad |\sigma| = 64\text{ B}`,
|
|
||||||
ASCII: "σ ← Ed25519.Sign(sk, H(envelope))",
|
|
||||||
Values: map[string]string{
|
|
||||||
"party": e.From,
|
|
||||||
"to": e.To,
|
|
||||||
"type": e.Type,
|
|
||||||
"sigma": hexOf(sig),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hexOf(b []byte) string {
|
|
||||||
const hexdigits = "0123456789abcdef"
|
|
||||||
out := make([]byte, len(b)*2)
|
|
||||||
for i, c := range b {
|
|
||||||
out[i*2] = hexdigits[c>>4]
|
|
||||||
out[i*2+1] = hexdigits[c&0x0f]
|
|
||||||
}
|
|
||||||
return string(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify checks the envelope signature against senderEdPub (via Rust).
|
// Verify checks the envelope signature against senderEdPub (via Rust).
|
||||||
func (e *Envelope) Verify(senderEdPub []byte) error {
|
func (e *Envelope) Verify(senderEdPub []byte) error {
|
||||||
if err := transportsec.Ed25519Verify(senderEdPub, e.signingBytes(), e.Signature); err != nil {
|
if err := transportsec.Ed25519Verify(senderEdPub, e.signingBytes(), e.Signature); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package zkp
|
||||||
import (
|
import (
|
||||||
"github.com/user/evote/pkg/hash"
|
"github.com/user/evote/pkg/hash"
|
||||||
emath "github.com/user/evote/pkg/math"
|
emath "github.com/user/evote/pkg/math"
|
||||||
"github.com/user/evote/pkg/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenSchnorrProof generates a Schnorr proof of knowledge of discrete log.
|
// GenSchnorrProof generates a Schnorr proof of knowledge of discrete log.
|
||||||
|
|
@ -68,19 +67,6 @@ func schnorrChallenge(group *emath.GqGroup, y emath.GqElement, c emath.GqElement
|
||||||
hAux,
|
hAux,
|
||||||
)
|
)
|
||||||
e, _ := emath.NewZqElement(eVal, zqGroup)
|
e, _ := emath.NewZqElement(eVal, zqGroup)
|
||||||
trace.EmitFunc(func() trace.Event {
|
|
||||||
return trace.Event{
|
|
||||||
Kind: trace.KindChallenge,
|
|
||||||
Caption: "Fiat-Shamir challenge (Schnorr proof)",
|
|
||||||
LaTeX: `e = \mathcal{H}\big((p,q,g),\, y,\, c,\, h_{\mathrm{aux}}\big) \bmod q`,
|
|
||||||
ASCII: "e = H((p,q,g), y, c, h_aux) mod q",
|
|
||||||
Values: map[string]string{
|
|
||||||
"e": e.Value().String(),
|
|
||||||
"y": y.Value().String(),
|
|
||||||
"c": c.Value().String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1016,7 +1016,6 @@ r = <span class="red">833704513946265251405968611993787771762...</span>
|
||||||
<span class="cmt">"The output ciphertexts are the correct multi-exponentiations</span>
|
<span class="cmt">"The output ciphertexts are the correct multi-exponentiations</span>
|
||||||
<span class="cmt"> of the inputs with the permutation matrix as exponents."</span></div>
|
<span class="cmt"> of the inputs with the permutation matrix as exponents."</span></div>
|
||||||
<div class="analogy-box"><strong>Simplified intuition:</strong> Imagine you have 6 sealed boxes numbered 1-6. You rearrange them and put them in new boxes. The ProductArgument proves "I used each number exactly once" (it's a valid permutation). The MultiExponentiationArgument proves "the new boxes contain the same items as the old boxes" (the ciphertexts were correctly re-encrypted, not swapped for fakes).</div>
|
<div class="analogy-box"><strong>Simplified intuition:</strong> Imagine you have 6 sealed boxes numbered 1-6. You rearrange them and put them in new boxes. The ProductArgument proves "I used each number exactly once" (it's a valid permutation). The MultiExponentiationArgument proves "the new boxes contain the same items as the old boxes" (the ciphertexts were correctly re-encrypted, not swapped for fakes).</div>
|
||||||
<div class="insight-box"><strong>Watch it happen.</strong> Run <code>evote cockpit</code> (browser) or <code>evote cockpit --tmux</code> (terminal): as the mix-net executes, each of these sub-arguments is rendered as live typeset mathematics with the real runtime values, in the order it is constructed — the Pedersen commitment to the permutation matrix, then the Product / Hadamard / Zero / SingleValueProduct / MultiExponentiation arguments, then the partial decryptions. The tree above is not a diagram of the code; it <em>is</em> the code, narrating itself.</div>
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue