summaryrefslogtreecommitdiffstats
path: root/katzenpost
diff options
context:
space:
mode:
Diffstat (limited to 'katzenpost')
-rw-r--r--katzenpost/README.md30
-rw-r--r--katzenpost/cmd/yamn-dispatcher/main.go115
-rw-r--r--katzenpost/cmd/yamn-submit/main.go73
-rw-r--r--katzenpost/dispatcher/codec.go29
-rw-r--r--katzenpost/dispatcher/envelope.go45
-rw-r--r--katzenpost/dispatcher/envelope_test.go21
-rw-r--r--katzenpost/dispatcher/frame.go84
-rw-r--r--katzenpost/dispatcher/frame_test.go46
-rw-r--r--katzenpost/dispatcher/reassembler.go91
-rw-r--r--katzenpost/go.mod35
-rw-r--r--katzenpost/go.sum63
11 files changed, 632 insertions, 0 deletions
diff --git a/katzenpost/README.md b/katzenpost/README.md
new file mode 100644
index 0000000..938c2b0
--- /dev/null
+++ b/katzenpost/README.md
@@ -0,0 +1,30 @@
+# Katzenpost YAMN dispatcher
+
+The dispatcher runs on `kvara`. It is a send-only Katzenpost service: it
+receives an already encrypted YAMN packet and forwards it to the selected YAMN
+entry remailer using SMTP. It never receives a browser request and must not
+implement fetch, view, AEC, replies, or message storage.
+
+`pietro` is responsible for building the YAMN packet and submitting the opaque
+envelope to the Katzenpost service through `kpclientd`.
+
+## Trust boundary
+
+The only data the dispatcher may see is:
+
+- the entry remailer SMTP address;
+- the complete, already encrypted YAMN envelope.
+
+It must reject all other recipient domains and must not write payloads,
+subjects, addresses, or SMTP transcripts to disk or logs.
+
+## Deployment shape
+
+```text
+pietro: yamnweb onion service -> local kpclientd -> Katzenpost
+ -> kvara dispatcher -> YAMN entry SMTP
+```
+
+The service capability name is `yamn-dispatch-v1`. It must be registered in
+the Katzenpost PKI before `pietro` can discover it. The code is intentionally
+not configured with live hosts, credentials, or service keys.
diff --git a/katzenpost/cmd/yamn-dispatcher/main.go b/katzenpost/cmd/yamn-dispatcher/main.go
new file mode 100644
index 0000000..8f10034
--- /dev/null
+++ b/katzenpost/cmd/yamn-dispatcher/main.go
@@ -0,0 +1,115 @@
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "git.virebent.art/virebent/yamnweb/katzenpost/dispatcher"
+ "github.com/katzenpost/katzenpost/core/log"
+ "github.com/katzenpost/katzenpost/server/cborplugin"
+)
+
+const capability = "yamn-dispatch-v1"
+
+type plugin struct {
+ write func(cborplugin.Command)
+ reassembler *dispatcher.Reassembler
+ allowedDomains map[string]struct{}
+}
+
+func (p *plugin) OnCommand(command cborplugin.Command) error {
+ request, ok := command.(*cborplugin.Request)
+ if !ok {
+ return errors.New("unexpected plugin command")
+ }
+ frame, err := dispatcher.DecodeFrame(request.Payload)
+ if err != nil {
+ return err
+ }
+ encoded, complete, err := p.reassembler.Add(frame)
+ if err != nil || !complete {
+ return err
+ }
+ envelope, err := dispatcher.DecodeEnvelope(encoded)
+ if err != nil {
+ return err
+ }
+ if err := envelope.Validate(p.allowedDomains); err != nil {
+ return err
+ }
+ // PoC sink: successful validation intentionally has no external side effect.
+ return nil
+}
+
+func (p *plugin) RegisterConsumer(server *cborplugin.Server) {
+ p.write = server.Write
+}
+
+func parseDomains(value string) (map[string]struct{}, error) {
+ domains := make(map[string]struct{})
+ for _, domain := range strings.Split(value, ",") {
+ domain = strings.ToLower(strings.TrimSpace(domain))
+ if domain == "" || strings.ContainsAny(domain, "@/\\: ") {
+ return nil, errors.New("invalid allowed domain")
+ }
+ domains[domain] = struct{}{}
+ }
+ if len(domains) == 0 {
+ return nil, errors.New("at least one allowed domain is required")
+ }
+ return domains, nil
+}
+
+func run() error {
+ var allowed string
+ var logDir string
+ var logLevel string
+ flag.StringVar(&allowed, "allowed-domains", "remailer.example", "comma-separated entry remailer domains")
+ flag.StringVar(&logDir, "log-dir", "/tmp", "operational log directory")
+ flag.StringVar(&logLevel, "log-level", "NOTICE", "Katzenpost log level")
+ flag.Parse()
+
+ domains, err := parseDomains(allowed)
+ if err != nil {
+ return err
+ }
+ info, err := os.Stat(logDir)
+ if err != nil || !info.IsDir() {
+ return errors.New("log directory is unavailable")
+ }
+ backend, err := log.New(filepath.Join(logDir, "yamn-dispatch.log"), logLevel, false)
+ if err != nil {
+ return fmt.Errorf("initialize logging: %w", err)
+ }
+ logger := backend.GetLogger("yamn_dispatch")
+
+ socketDir, err := os.MkdirTemp("", "yamn-dispatch-")
+ if err != nil {
+ return fmt.Errorf("create socket directory: %w", err)
+ }
+ defer os.RemoveAll(socketDir)
+ socketPath := filepath.Join(socketDir, "plugin.sock")
+ service := &plugin{
+ reassembler: dispatcher.NewReassembler(5 * time.Minute),
+ allowedDomains: domains,
+ }
+ server := cborplugin.NewServer(logger, socketPath, new(cborplugin.RequestFactory), service)
+ if _, err := fmt.Fprintln(os.Stdout, socketPath); err != nil {
+ return fmt.Errorf("publish socket path: %w", err)
+ }
+ server.Accept()
+ server.Wait()
+ return nil
+}
+
+func main() {
+ if err := run(); err != nil {
+ fmt.Fprintln(os.Stderr, "yamn-dispatcher failed")
+ os.Exit(1)
+ }
+}
diff --git a/katzenpost/cmd/yamn-submit/main.go b/katzenpost/cmd/yamn-submit/main.go
new file mode 100644
index 0000000..cde35f9
--- /dev/null
+++ b/katzenpost/cmd/yamn-submit/main.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ "git.virebent.art/virebent/yamnweb/katzenpost/dispatcher"
+ "github.com/katzenpost/hpqc/hash"
+ clientconfig "github.com/katzenpost/katzenpost/client/config"
+ "github.com/katzenpost/katzenpost/client/thin"
+)
+
+const capability = "yamn-dispatch-v1"
+
+func run() error {
+ var configPath string
+ var settle time.Duration
+ flag.StringVar(&configPath, "config", "thinclient.toml", "thin-client configuration")
+ flag.DurationVar(&settle, "settle", 2*time.Second, "time allowed for kpclientd to queue frames")
+ flag.Parse()
+
+ input, err := io.ReadAll(io.LimitReader(os.Stdin, dispatcher.MaxEnvelopeBytes*2))
+ if err != nil {
+ return fmt.Errorf("read envelope: %w", err)
+ }
+ var envelope dispatcher.Envelope
+ if err := json.Unmarshal(input, &envelope); err != nil {
+ return errors.New("invalid envelope input")
+ }
+ encoded, err := dispatcher.EncodeEnvelope(envelope)
+ if err != nil {
+ return err
+ }
+ frames, err := dispatcher.SplitEnvelope(encoded)
+ if err != nil {
+ return err
+ }
+
+ config, err := thin.LoadFile(configPath)
+ if err != nil {
+ return fmt.Errorf("load thin-client config: %w", err)
+ }
+ client := thin.NewThinClient(config, &clientconfig.Logging{Level: "ERROR", Disable: true})
+ defer client.Close()
+ if err := client.Dial(); err != nil {
+ return fmt.Errorf("connect to kpclientd: %w", err)
+ }
+ service, err := client.GetService(capability)
+ if err != nil {
+ return fmt.Errorf("find dispatcher service: %w", err)
+ }
+ destination := hash.Sum256(service.MixDescriptor.IdentityKey)
+ for _, frame := range frames {
+ if err := client.SendMessageWithoutReply(frame, &destination, service.RecipientQueueID); err != nil {
+ return fmt.Errorf("send frame: %w", err)
+ }
+ }
+ time.Sleep(settle)
+ fmt.Fprintln(os.Stdout, "accepted")
+ return nil
+}
+
+func main() {
+ if err := run(); err != nil {
+ fmt.Fprintf(os.Stderr, "submission failed: %v\n", err)
+ os.Exit(1)
+ }
+}
diff --git a/katzenpost/dispatcher/codec.go b/katzenpost/dispatcher/codec.go
new file mode 100644
index 0000000..3053dde
--- /dev/null
+++ b/katzenpost/dispatcher/codec.go
@@ -0,0 +1,29 @@
+package dispatcher
+
+import (
+ "fmt"
+
+ "github.com/fxamacker/cbor/v2"
+)
+
+func EncodeEnvelope(envelope Envelope) ([]byte, error) {
+ encoded, err := cbor.Marshal(envelope)
+ if err != nil {
+ return nil, fmt.Errorf("encode envelope: %w", err)
+ }
+ if len(encoded) > MaxEnvelopeBytes {
+ return nil, fmt.Errorf("encoded envelope is too large")
+ }
+ return encoded, nil
+}
+
+func DecodeEnvelope(encoded []byte) (Envelope, error) {
+ if len(encoded) == 0 || len(encoded) > MaxEnvelopeBytes {
+ return Envelope{}, fmt.Errorf("invalid encoded envelope size")
+ }
+ var envelope Envelope
+ if err := cbor.Unmarshal(encoded, &envelope); err != nil {
+ return Envelope{}, fmt.Errorf("decode envelope: %w", err)
+ }
+ return envelope, nil
+}
diff --git a/katzenpost/dispatcher/envelope.go b/katzenpost/dispatcher/envelope.go
new file mode 100644
index 0000000..b9ff7b8
--- /dev/null
+++ b/katzenpost/dispatcher/envelope.go
@@ -0,0 +1,45 @@
+package dispatcher
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "net/mail"
+ "strings"
+)
+
+const MaxEnvelopeBytes = 64 * 1024
+
+// Envelope is the only application payload accepted by the dispatcher. Message
+// must already be a complete YAMN transport envelope, not user supplied mail.
+type Envelope struct {
+ EntryAddress string `json:"entry_address"`
+ Message []byte `json:"message"`
+}
+
+func (e Envelope) Validate(allowedDomains map[string]struct{}) error {
+ if len(e.Message) == 0 || len(e.Message) > MaxEnvelopeBytes {
+ return fmt.Errorf("invalid YAMN envelope size")
+ }
+ if bytes.Contains(e.Message, []byte("\r")) {
+ return errors.New("YAMN envelope must use LF line endings")
+ }
+ address, err := mail.ParseAddress(e.EntryAddress)
+ if err != nil || address.Address != e.EntryAddress {
+ return errors.New("invalid entry remailer address")
+ }
+ parts := strings.Split(address.Address, "@")
+ if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
+ return errors.New("invalid entry remailer address")
+ }
+ if _, ok := allowedDomains[strings.ToLower(parts[1])]; !ok {
+ return errors.New("entry remailer domain is not allowed")
+ }
+ if !bytes.HasPrefix(e.Message, []byte("To: "+e.EntryAddress+"\n")) {
+ return errors.New("YAMN envelope recipient does not match entry remailer")
+ }
+ if !bytes.Contains(e.Message, []byte("-----BEGIN REMAILER MESSAGE-----\n")) {
+ return errors.New("invalid YAMN envelope")
+ }
+ return nil
+}
diff --git a/katzenpost/dispatcher/envelope_test.go b/katzenpost/dispatcher/envelope_test.go
new file mode 100644
index 0000000..712071c
--- /dev/null
+++ b/katzenpost/dispatcher/envelope_test.go
@@ -0,0 +1,21 @@
+package dispatcher
+
+import "testing"
+
+func TestEnvelopeValidate(t *testing.T) {
+ allowed := map[string]struct{}{"remailer.example": {}}
+ valid := Envelope{
+ EntryAddress: "entry@remailer.example",
+ Message: []byte("To: entry@remailer.example\nFrom: mix@nowhere.invalid\n\n-----BEGIN REMAILER MESSAGE-----\nabc\n"),
+ }
+ if err := valid.Validate(allowed); err != nil {
+ t.Fatalf("expected valid envelope: %v", err)
+ }
+
+ invalidDomain := valid
+ invalidDomain.EntryAddress = "entry@other.example"
+ invalidDomain.Message = []byte("To: entry@other.example\n-----BEGIN REMAILER MESSAGE-----\n")
+ if err := invalidDomain.Validate(allowed); err == nil {
+ t.Fatal("expected unknown domain rejection")
+ }
+}
diff --git a/katzenpost/dispatcher/frame.go b/katzenpost/dispatcher/frame.go
new file mode 100644
index 0000000..43f547c
--- /dev/null
+++ b/katzenpost/dispatcher/frame.go
@@ -0,0 +1,84 @@
+package dispatcher
+
+import (
+ "crypto/rand"
+ "crypto/sha256"
+ "errors"
+ "fmt"
+
+ "github.com/fxamacker/cbor/v2"
+)
+
+const (
+ ProtocolVersion = 1
+ MaxSphinxPayload = 2000
+ FrameDataBytes = 1500
+ MaxFrames = (MaxEnvelopeBytes + FrameDataBytes - 1) / FrameDataBytes
+)
+
+type Frame struct {
+ Version uint8 `cbor:"1,keyasint"`
+ ID [16]byte `cbor:"2,keyasint"`
+ Digest [32]byte `cbor:"3,keyasint"`
+ Index uint16 `cbor:"4,keyasint"`
+ Total uint16 `cbor:"5,keyasint"`
+ Data []byte `cbor:"6,keyasint"`
+}
+
+func SplitEnvelope(encoded []byte) ([][]byte, error) {
+ if len(encoded) == 0 || len(encoded) > MaxEnvelopeBytes {
+ return nil, errors.New("invalid encoded envelope size")
+ }
+
+ var id [16]byte
+ if _, err := rand.Read(id[:]); err != nil {
+ return nil, fmt.Errorf("generate message id: %w", err)
+ }
+ digest := sha256.Sum256(encoded)
+ total := (len(encoded) + FrameDataBytes - 1) / FrameDataBytes
+ if total > MaxFrames {
+ return nil, errors.New("encoded envelope requires too many frames")
+ }
+
+ frames := make([][]byte, 0, total)
+ for index, start := 0, 0; start < len(encoded); index, start = index+1, start+FrameDataBytes {
+ end := min(start+FrameDataBytes, len(encoded))
+ frame := Frame{
+ Version: ProtocolVersion,
+ ID: id,
+ Digest: digest,
+ Index: uint16(index),
+ Total: uint16(total),
+ Data: encoded[start:end],
+ }
+ payload, err := cbor.Marshal(frame)
+ if err != nil {
+ return nil, fmt.Errorf("encode frame %d: %w", index, err)
+ }
+ if len(payload) > MaxSphinxPayload {
+ return nil, fmt.Errorf("frame %d exceeds Sphinx payload limit", index)
+ }
+ frames = append(frames, payload)
+ }
+ return frames, nil
+}
+
+func DecodeFrame(payload []byte) (Frame, error) {
+ if len(payload) == 0 || len(payload) > MaxSphinxPayload {
+ return Frame{}, errors.New("invalid frame size")
+ }
+ var frame Frame
+ if err := cbor.Unmarshal(payload, &frame); err != nil {
+ return Frame{}, fmt.Errorf("decode frame: %w", err)
+ }
+ if frame.Version != ProtocolVersion {
+ return Frame{}, errors.New("unsupported frame version")
+ }
+ if frame.Total == 0 || int(frame.Total) > MaxFrames || frame.Index >= frame.Total {
+ return Frame{}, errors.New("invalid frame sequence")
+ }
+ if len(frame.Data) == 0 || len(frame.Data) > FrameDataBytes {
+ return Frame{}, errors.New("invalid frame data size")
+ }
+ return frame, nil
+}
diff --git a/katzenpost/dispatcher/frame_test.go b/katzenpost/dispatcher/frame_test.go
new file mode 100644
index 0000000..21c84ba
--- /dev/null
+++ b/katzenpost/dispatcher/frame_test.go
@@ -0,0 +1,46 @@
+package dispatcher
+
+import (
+ "bytes"
+ "testing"
+ "time"
+)
+
+func TestSplitAndReassembleEnvelope(t *testing.T) {
+ encoded := bytes.Repeat([]byte("yamn-envelope\n"), 900)
+ payloads, err := SplitEnvelope(encoded)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(payloads) < 2 {
+ t.Fatal("expected multiple frames")
+ }
+
+ reassembler := NewReassembler(time.Minute)
+ var result []byte
+ for _, payload := range payloads {
+ if len(payload) > MaxSphinxPayload {
+ t.Fatalf("payload exceeds limit: %d", len(payload))
+ }
+ frame, err := DecodeFrame(payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assembled, complete, err := reassembler.Add(frame)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if complete {
+ result = assembled
+ }
+ }
+ if !bytes.Equal(result, encoded) {
+ t.Fatal("reassembled envelope differs")
+ }
+}
+
+func TestDecodeFrameRejectsOversizedPayload(t *testing.T) {
+ if _, err := DecodeFrame(make([]byte, MaxSphinxPayload+1)); err == nil {
+ t.Fatal("expected oversized payload rejection")
+ }
+}
diff --git a/katzenpost/dispatcher/reassembler.go b/katzenpost/dispatcher/reassembler.go
new file mode 100644
index 0000000..87af98c
--- /dev/null
+++ b/katzenpost/dispatcher/reassembler.go
@@ -0,0 +1,91 @@
+package dispatcher
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "errors"
+ "sync"
+ "time"
+)
+
+const MaxPendingMessages = 64
+
+type pendingMessage struct {
+ digest [32]byte
+ total uint16
+ parts map[uint16][]byte
+ created time.Time
+}
+
+type Reassembler struct {
+ mu sync.Mutex
+ pending map[[16]byte]*pendingMessage
+ ttl time.Duration
+ now func() time.Time
+}
+
+func NewReassembler(ttl time.Duration) *Reassembler {
+ return &Reassembler{
+ pending: make(map[[16]byte]*pendingMessage),
+ ttl: ttl,
+ now: time.Now,
+ }
+}
+
+func (r *Reassembler) Add(frame Frame) ([]byte, bool, error) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ r.expireLocked()
+ pending, ok := r.pending[frame.ID]
+ if !ok {
+ if len(r.pending) >= MaxPendingMessages {
+ return nil, false, errors.New("too many pending messages")
+ }
+ pending = &pendingMessage{
+ digest: frame.Digest,
+ total: frame.Total,
+ parts: make(map[uint16][]byte, frame.Total),
+ created: r.now(),
+ }
+ r.pending[frame.ID] = pending
+ }
+ if pending.total != frame.Total || pending.digest != frame.Digest {
+ delete(r.pending, frame.ID)
+ return nil, false, errors.New("inconsistent frame metadata")
+ }
+ if _, duplicate := pending.parts[frame.Index]; !duplicate {
+ pending.parts[frame.Index] = bytes.Clone(frame.Data)
+ }
+ if len(pending.parts) != int(pending.total) {
+ return nil, false, nil
+ }
+
+ var assembled bytes.Buffer
+ for index := uint16(0); index < pending.total; index++ {
+ part, exists := pending.parts[index]
+ if !exists {
+ return nil, false, nil
+ }
+ if assembled.Len()+len(part) > MaxEnvelopeBytes {
+ delete(r.pending, frame.ID)
+ return nil, false, errors.New("reassembled envelope is too large")
+ }
+ assembled.Write(part)
+ }
+ delete(r.pending, frame.ID)
+ result := assembled.Bytes()
+ if sha256.Sum256(result) != pending.digest {
+ return nil, false, errors.New("reassembled envelope digest mismatch")
+ }
+ return result, true, nil
+}
+
+func (r *Reassembler) expireLocked() {
+ cutoff := r.now().Add(-r.ttl)
+ for id, pending := range r.pending {
+ if pending.created.Before(cutoff) {
+ delete(r.pending, id)
+ }
+ }
+}
diff --git a/katzenpost/go.mod b/katzenpost/go.mod
new file mode 100644
index 0000000..09a9aca
--- /dev/null
+++ b/katzenpost/go.mod
@@ -0,0 +1,35 @@
+module git.virebent.art/virebent/yamnweb/katzenpost
+
+go 1.26.2
+
+require (
+ github.com/fxamacker/cbor/v2 v2.9.1
+ github.com/katzenpost/hpqc v0.0.84-0.20260603082753-be60338910ec
+ github.com/katzenpost/katzenpost v0.0.90
+)
+
+require (
+ codeberg.org/vula/highctidh v1.0.2025051200 // indirect
+ filippo.io/edwards25519 v1.2.0 // indirect
+ filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e // indirect
+ filippo.io/mlkem768 v0.0.0-20260214141301-2e7bebc7d88d // indirect
+ github.com/BurntSushi/toml v1.6.0 // indirect
+ github.com/agl/gcmsiv v0.0.0-20190418185415-e8dcd2f151dc // indirect
+ github.com/katzenpost/chacha20 v0.0.1 // indirect
+ github.com/katzenpost/chacha20poly1305 v0.0.1 // indirect
+ github.com/katzenpost/circl v1.3.8-0.20260413165442-e2d217fd59f5 // indirect
+ github.com/katzenpost/falcon v0.1.0 // indirect
+ github.com/katzenpost/sntrup4591761 v0.0.0-20231024131303-8755eb1986b8 // indirect
+ github.com/katzenpost/sphincsplus v0.0.2 // indirect
+ github.com/katzenpost/sqisign/bindings/go v0.0.0-20260520215053-47e926d8264d // indirect
+ github.com/mattn/go-pointer v0.0.1 // indirect
+ github.com/shurlinet/go-hqc v0.1.1 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ gitlab.com/yawning/aez.git v0.0.0-20211027044916-e49e68abd344 // indirect
+ gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
+ golang.org/x/crypto v0.51.0 // indirect
+ golang.org/x/net v0.53.0 // indirect
+ golang.org/x/sys v0.44.0 // indirect
+ golang.org/x/text v0.37.0 // indirect
+ gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // indirect
+)
diff --git a/katzenpost/go.sum b/katzenpost/go.sum
new file mode 100644
index 0000000..d8d6abd
--- /dev/null
+++ b/katzenpost/go.sum
@@ -0,0 +1,63 @@
+codeberg.org/vula/highctidh v1.0.2025051200 h1:4xG2pxCCYKvI2HICy5YQf+44d8pEibuOg4u86PvX4OM=
+codeberg.org/vula/highctidh v1.0.2025051200/go.mod h1:admvznk7GhsrDih/BMy7jbIv9Y86U9vj7S5FVoquF/g=
+filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
+filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
+filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e h1:VsUbObBMxXlc23Eb9VeeJYE4jvTs87qa5RqSN2U5FJU=
+filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e/go.mod h1:32qQ5yj3R24Eu03iWFWchdC3OB653wPvoepWejkefbY=
+filippo.io/mlkem768 v0.0.0-20260214141301-2e7bebc7d88d h1:YyLyABjdrdt2l/E6JAnku4BjhEDXhxQD2bPOnOvy8/M=
+filippo.io/mlkem768 v0.0.0-20260214141301-2e7bebc7d88d/go.mod h1:ym4egWKLpazdho3bHx0xuQlCq02ttP+vhxxKO8LgO9c=
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/agl/gcmsiv v0.0.0-20190418185415-e8dcd2f151dc h1:MB338WDPuyQ8qXRiSRK2NVTv0EmLYsHXZAevEiQO1+c=
+github.com/agl/gcmsiv v0.0.0-20190418185415-e8dcd2f151dc/go.mod h1:5joDAvk82M2Cx1X8mAL5Orvhy5lfW4BjrTCW65wbvRo=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
+github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/katzenpost/chacha20 v0.0.1 h1:Scu6Pqyginw083FhypKMIBmCI2gQTC1RFD6vqpWwI2Y=
+github.com/katzenpost/chacha20 v0.0.1/go.mod h1:/LIJK/8cUXVJrCh5NypZ8So3gDfQCoQT8lRvy1rYQZA=
+github.com/katzenpost/chacha20poly1305 v0.0.1 h1:yFPxgu/VSWsoCNQ2x15az8xBKKc/9GDY9Pz42MvY9xg=
+github.com/katzenpost/chacha20poly1305 v0.0.1/go.mod h1:5sv//C8pWaXrnnP3CNiJXqAlA1/yfUZOl1tR9xsL9bI=
+github.com/katzenpost/circl v1.3.8-0.20260413165442-e2d217fd59f5 h1:n+9aUwSmnz97MNS9duYeHSq+s42CG9wydFECOjqAI9A=
+github.com/katzenpost/circl v1.3.8-0.20260413165442-e2d217fd59f5/go.mod h1:9KxLMK17ZLjofnmpVp4Sm315uIF3BAWHVk32EFDRicU=
+github.com/katzenpost/falcon v0.1.0 h1:+WkID41Y6CaWAQE3ijCIfZHu7eHHy8DvAFWCnrW1XDw=
+github.com/katzenpost/falcon v0.1.0/go.mod h1:HtOnkNvwpvTJ3fuXmy4BNw4A8Jg2QetpmkvOPOnOwyU=
+github.com/katzenpost/hpqc v0.0.84-0.20260603082753-be60338910ec h1:6YnGxqJXmAXt9maeTudPdNmEjG4hLJkRj8ISpih9TJU=
+github.com/katzenpost/hpqc v0.0.84-0.20260603082753-be60338910ec/go.mod h1:C/MaI/ke/uHR5FCKuqB/GNa/JLoFk9PeBnFoP23dh6Q=
+github.com/katzenpost/katzenpost v0.0.90 h1:z3bBMpDK3eC65kqbXGwlIjIo1IccHTWytgCCtX+PL2M=
+github.com/katzenpost/katzenpost v0.0.90/go.mod h1:tKMbNtHwBxv2qk9uX5hfNueFnxpQPDvr6NjkW5AUchY=
+github.com/katzenpost/sntrup4591761 v0.0.0-20231024131303-8755eb1986b8 h1:TsKxH0x2RUwf5rBw67k15bqVM3oVbexA9oaTZQLIy3Y=
+github.com/katzenpost/sntrup4591761 v0.0.0-20231024131303-8755eb1986b8/go.mod h1:Hmcrwom7jcEmGdo0CsyuJNnldPeyS+M07FuCbo7I8fw=
+github.com/katzenpost/sphincsplus v0.0.2 h1:W1UWejLK62Lk0uK2R08H/sWEaQrRHWCaMEKO181SoOE=
+github.com/katzenpost/sphincsplus v0.0.2/go.mod h1:ChO9+ojgCH1yEuplGgW4mSI1FwZWtyEmEkG1xL3w264=
+github.com/katzenpost/sqisign/bindings/go v0.0.0-20260520215053-47e926d8264d h1:mjXmMwB8NRO0+NoUGj9zys2vm02Ia9on0lZE5gVr4X8=
+github.com/katzenpost/sqisign/bindings/go v0.0.0-20260520215053-47e926d8264d/go.mod h1:CcVAF5BHgQtHH3lFCmwl3t2KKcXih+8ZcjLbxg8z3D0=
+github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
+github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/schwarmco/go-cartesian-product v0.0.0-20230921023625-e02d1c150053 h1:h7EwPM2KjupG0zVAG+EYxbR2cHnbiP1d4DTAZ+G09LY=
+github.com/schwarmco/go-cartesian-product v0.0.0-20230921023625-e02d1c150053/go.mod h1:/TRiIlxvQQAtfnBXEqqbnYBYPmE6XT5iZxSx+hJ9zGw=
+github.com/shurlinet/go-hqc v0.1.1 h1:p0ZSHW+Spulp0pBnuyYTjgqmqm0CkILZWyOEZeKRXl0=
+github.com/shurlinet/go-hqc v0.1.1/go.mod h1:3pQdozY8+F4AcOpiyj/ztKR90TwMDMBjbiNHNbejPtw=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+gitlab.com/yawning/aez.git v0.0.0-20211027044916-e49e68abd344 h1:eICJMpqnDkO4nQv+GWtXcE45CFum/ni3jhcE+acBjQk=
+gitlab.com/yawning/aez.git v0.0.0-20211027044916-e49e68abd344/go.mod h1:/WDFxZLKGy+NQc+nqvQg2O0rW1HeWNHSelVv5fwEL8s=
+gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
+gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
+golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
+golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
+golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
+golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
+gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE=
+gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=