mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-09-04 20:23:55 +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>
103 lines
3.2 KiB
Go
103 lines
3.2 KiB
Go
package transport
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/binary"
|
||
"encoding/json"
|
||
"fmt"
|
||
|
||
"github.com/user/evote/pkg/trace"
|
||
"github.com/user/evote/pkg/transportsec"
|
||
)
|
||
|
||
// Envelope is a single authenticated message from one party to another. The
|
||
// signature (Ed25519, produced in Rust) covers the canonical byte encoding of
|
||
// all other fields, so From/To/Type/Nonce/Payload are all integrity-protected.
|
||
// Encrypted marks whether Payload is AES-GCM ciphertext (see SecureChannel).
|
||
type Envelope struct {
|
||
From string `json:"from"`
|
||
To string `json:"to"`
|
||
Type string `json:"type"`
|
||
Nonce uint64 `json:"nonce"`
|
||
Encrypted bool `json:"encrypted"`
|
||
Payload []byte `json:"payload"`
|
||
Signature []byte `json:"signature"`
|
||
}
|
||
|
||
// signingBytes returns the canonical bytes covered by the signature: a
|
||
// length-prefixed concatenation of every field except the signature itself.
|
||
// Length-prefixing makes the encoding injective (no field-boundary ambiguity).
|
||
func (e *Envelope) signingBytes() []byte {
|
||
var b []byte
|
||
appendField := func(data []byte) {
|
||
var l [8]byte
|
||
binary.BigEndian.PutUint64(l[:], uint64(len(data)))
|
||
b = append(b, l[:]...)
|
||
b = append(b, data...)
|
||
}
|
||
appendField([]byte(e.From))
|
||
appendField([]byte(e.To))
|
||
appendField([]byte(e.Type))
|
||
var nonce [8]byte
|
||
binary.BigEndian.PutUint64(nonce[:], e.Nonce)
|
||
appendField(nonce[:])
|
||
if e.Encrypted {
|
||
appendField([]byte{1})
|
||
} else {
|
||
appendField([]byte{0})
|
||
}
|
||
appendField(e.Payload)
|
||
// Hash the concatenation to a fixed 32-byte digest that is what actually
|
||
// gets signed (keeps signed inputs short and uniform).
|
||
h := sha256.Sum256(b)
|
||
return h[:]
|
||
}
|
||
|
||
// Seal signs the envelope with the sender identity's Ed25519 key (via Rust).
|
||
func (e *Envelope) Seal(sender *Identity) error {
|
||
sig, err := transportsec.Ed25519Sign(sender.SigningSeed(), e.signingBytes())
|
||
if err != nil {
|
||
return fmt.Errorf("seal %s->%s: %w", e.From, e.To, err)
|
||
}
|
||
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
|
||
}
|
||
|
||
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).
|
||
func (e *Envelope) Verify(senderEdPub []byte) error {
|
||
if err := transportsec.Ed25519Verify(senderEdPub, e.signingBytes(), e.Signature); err != nil {
|
||
return fmt.Errorf("envelope %s->%s type=%s: %w", e.From, e.To, e.Type, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// MarshalPayload JSON-encodes v into the payload.
|
||
func MarshalPayload(v any) ([]byte, error) { return json.Marshal(v) }
|
||
|
||
// UnmarshalPayload JSON-decodes the payload into v.
|
||
func UnmarshalPayload(data []byte, v any) error { return json.Unmarshal(data, v) }
|