mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-09-05 20:30:43 +00:00
The differentiator. A single self-contained browser page (no libraries, offline)
renders each real cryptographic operation as typeset mathematics the instant it
runs, with the actual runtime values:
- E_1 = (γ, φ) = (g^r, pk^r·m), r ← Z_q (ElGamal ballot encryption)
- e = H((p,q,g), y, c, h_aux) mod q (Fiat-Shamir challenge)
- C' = { ReEnc_pk(C_π(i); ρ_i) } (Bayer-Groth verifiable shuffle)
- σ ← Ed25519.Sign_sk(SHA256(envelope)) (transport signature)
- s = a·B = b·A ∈ X25519, k = SHA256(…) (X25519 key agreement)
Math is rendered via a focused LaTeX→native-MathML converter written for exactly
the notation the instrumentation emits — so it works in any modern browser with
zero dependencies and nothing to ship. Unknown tokens fall back to literal text,
never crashing the view.
`evote cockpit` starts an HTTP server; on page connect it runs one full multi-
party ceremony, streaming every crypto event over SSE with configurable pacing
(--delay) so a human can follow along. A stakeholder sidebar highlights the
acting party; a phase timeline tracks setup→cards→voting→tally→verify; each op
shows its live values as expandable, copyable chips.
Verified in a real browser: all five operation kinds render correctly (96 sign,
36 challenge, 6 keyex, 2 encrypt, 5 shuffle in a 2-voter run), no console errors,
ceremony completes and verifies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
187 lines
5.5 KiB
Go
187 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"math/big"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"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
|
|
cockpitRunning atomic.Bool
|
|
)
|
|
|
|
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]")
|
|
}
|
|
return runCockpit()
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
cockpitCmd.Flags().IntVar(&cockpitPort, "port", 8090, "HTTP port")
|
|
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)")
|
|
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{
|
|
Addr: fmt.Sprintf(":%d", cockpitPort),
|
|
Handler: mux,
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
}
|
|
url := fmt.Sprintf("http://localhost:%d/", cockpitPort)
|
|
fmt.Println("========================================")
|
|
fmt.Println(" Swiss Post E-Voting — Live Crypto Cockpit")
|
|
fmt.Println("========================================")
|
|
fmt.Printf(" Open %s in your browser.\n", url)
|
|
fmt.Printf(" Voters: %d, Options: %d, pacing: %dms/event\n", cockpitVoters, cockpitOptions, cockpitDelayMs)
|
|
fmt.Println(" The election runs when the page connects; every crypto operation")
|
|
fmt.Println(" is rendered as typeset math with its real live values.")
|
|
return srv.ListenAndServe()
|
|
}
|
|
|
|
// 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 may run at a time (this is a single-viewer desktop tool).
|
|
if !cockpitRunning.CompareAndSwap(false, true) {
|
|
http.Error(w, "a ceremony is already streaming; reload after it finishes", http.StatusConflict)
|
|
return
|
|
}
|
|
defer cockpitRunning.Store(false)
|
|
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()
|
|
}
|