swisspost-evoting-go-poc/pkg/party/ceremony.go
saymrwulf 313db92321 Add party layer scaffolding: PKI bootstrap + signed handshake
pkg/party models each endpoint of the system as a separate object holding only
its own private state, wired together through the transport bus.

- ceremony.go: NewCeremony bootstraps the Ed25519 root CA, enrolls all parties
  (setup, 4 CCs, electoral board, voting server, verifier, N voters) with
  CA-signed identity certs, registers each in the directory, and wires its
  handler into the bus.
- parties.go: the six party types and a shared hello/ack handshake; Handshake()
  proves the full sign -> route -> verify -> reply -> verify path for every
  party before any election logic runs.
- state.go: per-party private state structs (nothing shared across parties).
- transcript.go: PublicTranscript, the append-only bulletin board a remote
  verifier will consume (no secrets).
- phases.go: phase handlers reject unknown message types cleanly (the transport
  boundary never panics on unexpected input) — filled in over the next commits.

Transport CA API simplified to own its serial counter (NewCA/Issue).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:07:31 +02:00

158 lines
4.4 KiB
Go

// Package party implements the multi-party e-voting ceremony. Each party
// (setup component, four control components, electoral board, voting server,
// voter clients, verifier) is a distinct endpoint holding only its own private
// state; all data that crosses between parties travels as Ed25519-signed
// envelopes over the transport bus (pkg/transport), with the signature and key
// agreement performed in Rust (pkg/transportsec).
//
// This mirrors the Swiss Post trust structure without emulating its scale: the
// point is the trust boundaries and their transport security, not throughput.
package party
import (
"fmt"
"github.com/user/evote/pkg/protocol"
"github.com/user/evote/pkg/transport"
)
// Party name constants used as transport addresses and certificate CNs.
const (
NameSetup = "setup-component"
NameEB = "electoral-board"
NameServer = "voting-server"
NameVerifier = "verifier"
)
// CCName returns the transport name of control component j.
func CCName(j int) string { return fmt.Sprintf("control-component-%d", j) }
// VoterName returns the transport name of voter v.
func VoterName(v int) string { return fmt.Sprintf("voter-%04d", v) }
// Ceremony wires the parties, the PKI, and the bus together.
type Ceremony struct {
Config *protocol.Config
CA *transport.CA
Dir *transport.Directory
Bus *transport.Bus
Setup *SetupComponent
CCs []*ControlComponent
EB *ElectoralBoard
Server *VotingServer
Voters []*VoterClient
Verifier *VerifierParty
Transcript *PublicTranscript
logf func(string, ...any)
}
// NewCeremony bootstraps the PKI and every party identity, registering each in
// the CA-anchored directory and wiring its handler into the bus. It does not run
// the election — call RunSetup/RunVoting/RunTally/RunVerify (added incrementally).
func NewCeremony(cfg *protocol.Config, logf func(string, ...any)) (*Ceremony, error) {
if logf == nil {
logf = func(string, ...any) {}
}
c := &Ceremony{Config: cfg, logf: logf}
// 1. Root CA (Ed25519, Rust-signed).
ca, err := transport.NewCA("evote-root-ca")
if err != nil {
return nil, fmt.Errorf("CA: %w", err)
}
c.CA = ca
c.Dir = transport.NewDirectory(ca)
c.Bus = transport.NewBus(c.Dir)
c.Bus.Log = func(format string, args ...any) { c.logf(format, args...) }
// 2. Enroll every party: issue an Ed25519 identity cert and register it.
enroll := func(name string) (*transport.Identity, error) {
id, err := ca.Issue(name)
if err != nil {
return nil, err
}
if err := c.Dir.Register(id); err != nil {
return nil, err
}
return id, nil
}
setupID, err := enroll(NameSetup)
if err != nil {
return nil, err
}
c.Setup = &SetupComponent{id: setupID, cer: c}
c.CCs = make([]*ControlComponent, cfg.NumCCs)
for j := 0; j < cfg.NumCCs; j++ {
id, err := enroll(CCName(j))
if err != nil {
return nil, err
}
c.CCs[j] = &ControlComponent{index: j, id: id, cer: c}
}
ebID, err := enroll(NameEB)
if err != nil {
return nil, err
}
c.EB = &ElectoralBoard{id: ebID, cer: c}
serverID, err := enroll(NameServer)
if err != nil {
return nil, err
}
c.Server = &VotingServer{id: serverID, cer: c}
verID, err := enroll(NameVerifier)
if err != nil {
return nil, err
}
c.Verifier = &VerifierParty{id: verID, cer: c}
c.Voters = make([]*VoterClient, cfg.NumVoters)
for v := 0; v < cfg.NumVoters; v++ {
id, err := enroll(VoterName(v))
if err != nil {
return nil, err
}
c.Voters[v] = &VoterClient{index: v, id: id, cer: c}
}
// 3. Register message handlers on the bus.
c.Bus.Handle(NameSetup, c.Setup.handle)
for _, cc := range c.CCs {
c.Bus.Handle(cc.id.Name, cc.handle)
}
c.Bus.Handle(NameEB, c.EB.handle)
c.Bus.Handle(NameServer, c.Server.handle)
c.Bus.Handle(NameVerifier, c.Verifier.handle)
for _, v := range c.Voters {
c.Bus.Handle(v.id.Name, v.handle)
}
c.Transcript = &PublicTranscript{}
return c, nil
}
// send is a helper the parties use to sign and dispatch a typed message,
// returning the (verified) reply payload.
func (c *Ceremony) send(from *transport.Identity, to, msgType string, payload any) (*transport.Envelope, error) {
data, err := transport.MarshalPayload(payload)
if err != nil {
return nil, err
}
env := &transport.Envelope{From: from.Name, To: to, Type: msgType, Nonce: c.nextNonce(), Payload: data}
if err := env.Seal(from); err != nil {
return nil, err
}
return c.Bus.Send(env)
}
var globalNonce uint64
func (c *Ceremony) nextNonce() uint64 { globalNonce++; return globalNonce }