Files
esign/pkg/api/zap_server_test.go
zeekay 165ae41805 luxfi/esign v0.2: Ed25519 sign/verify + ZAP RPC server + esignd
This commit lands the first end-to-end signable surface:

  * pkg/sign — Signer/Verifier interfaces, Ed25519 implementation
    (stdlib crypto/ed25519), method-dispatch helper. 8 unit tests:
    round-trip, deterministic-from-seed, bad-seed-length, tampered
    payload + sig, bad pubkey, bad sig length, zero-value verifier.

  * pkg/envelope — added Signature.SignerHashHex() and SeqString
    helper used by the verifier-side error reporting.

  * pkg/api — Handler with pluggable Store, in-memory MemStore for
    tests + small deployments. CreateRequest / SignRequest /
    TransitionRequest / VerifyResponse types form the wire surface.
    Handler.Sign runs cryptographic verify BEFORE calling
    envelope.AddSignature so a malformed sig never lands in the
    envelope. 3 e2e tests cover happy path, sig tampering detection,
    and crypto-fails-before-store ordering.

  * pkg/api — opcode table (Create/Get/Sign/Transition/Verify/Audit
    + Ping). ZAPServer serves [4B BE length][2B LE opcode][JSON body]
    framing with per-conn pipelining + idle timeout. Tests: ping,
    end-to-end create→sent→sign→verify, unknown opcode error,
    bad-JSON error.

  * cmd/esignd — standalone daemon. `--zap :9650` by default.
    SIGINT/SIGTERM shutdown drains inflight conns before exit.

Test summary: 3 packages, 22 tests, all green.
2026-06-07 11:28:46 -07:00

177 lines
5.0 KiB
Go

// Copyright 2026 Lux Industries Inc. and the luxfi/esign authors.
// Licensed under the Apache License, Version 2.0 (see LICENSE).
package api
import (
"context"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"io"
"net"
"testing"
"time"
"github.com/luxfi/esign/pkg/envelope"
"github.com/luxfi/esign/pkg/sign"
)
// rawCall opens a TCP connection to addr, sends one opcode-prefixed
// length-framed JSON body, reads the reply, and returns the response
// payload. Frames the way ZAPServer expects on the wire.
func rawCall(t *testing.T, addr string, op uint16, body []byte) []byte {
t.Helper()
conn, err := net.Dial("tcp", addr)
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer conn.Close()
inner := make([]byte, 2+len(body))
binary.LittleEndian.PutUint16(inner[:2], op)
copy(inner[2:], body)
frame := make([]byte, 4+len(inner))
binary.BigEndian.PutUint32(frame[:4], uint32(len(inner)))
copy(frame[4:], inner)
_ = conn.SetWriteDeadline(time.Now().Add(2 * time.Second))
if _, err := conn.Write(frame); err != nil {
t.Fatalf("write: %v", err)
}
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
var lenBuf [4]byte
if _, err := io.ReadFull(conn, lenBuf[:]); err != nil {
t.Fatalf("read length: %v", err)
}
respLen := binary.BigEndian.Uint32(lenBuf[:])
resp := make([]byte, respLen)
if _, err := io.ReadFull(conn, resp); err != nil {
t.Fatalf("read body: %v", err)
}
// Skip the echoed opcode (2 bytes).
return resp[2:]
}
// Spin up a ZAPServer on an ephemeral port, return its address + a
// teardown.
func startServer(t *testing.T) (string, func()) {
t.Helper()
h := New(NewMemStore())
srv, err := NewZAPServer("127.0.0.1:0", h)
if err != nil {
t.Fatalf("NewZAPServer: %v", err)
}
go func() { _ = srv.Serve() }()
return srv.Addr(), func() { _ = srv.Close() }
}
// Ping round-trip — basic wire format sanity.
func TestZAP_Ping(t *testing.T) {
addr, stop := startServer(t)
defer stop()
resp := rawCall(t, addr, OpPing, nil)
if string(resp) != `{"ok":true}` {
t.Fatalf("ping: got %q want %q", string(resp), `{"ok":true}`)
}
}
// End-to-end over ZAP: create, transition Sent, sign with Ed25519, verify.
func TestZAP_E2E_SingleSigner(t *testing.T) {
addr, stop := startServer(t)
defer stop()
signer, err := sign.NewEd25519Signer()
if err != nil {
t.Fatalf("NewEd25519Signer: %v", err)
}
signerHash := sha256.Sum256(signer.Public())
// Create.
doc := []byte("hello zap")
creq, _ := json.Marshal(CreateRequest{
Document: doc,
Signers: []envelope.SignerSlot{
{SignerHash: signerHash, Method: envelope.MethodEd25519, Required: true},
},
})
cresp := rawCall(t, addr, OpEnvelopeCreate, creq)
var cr CreateResponse
if err := json.Unmarshal(cresp, &cr); err != nil {
t.Fatalf("create resp: %v body=%s", err, string(cresp))
}
if cr.ID == "" {
t.Fatalf("create returned empty ID: %s", string(cresp))
}
// Transition Sent.
treq, _ := json.Marshal(TransitionRequest{
EnvelopeID: cr.ID, Target: envelope.StateSent, Actor: [32]byte{0xaa},
})
_ = rawCall(t, addr, OpEnvelopeTransition, treq)
// Sign.
docHash := sha256.Sum256(doc)
rawSig, _ := signer.Sign(docHash[:])
sreq, _ := json.Marshal(SignRequest{
EnvelopeID: cr.ID, SignerHash: signerHash,
Method: envelope.MethodEd25519,
Sig: rawSig, PubKey: signer.Public(),
})
sresp := rawCall(t, addr, OpEnvelopeSign, sreq)
var sr SignResponse
if err := json.Unmarshal(sresp, &sr); err != nil {
t.Fatalf("sign resp: %v body=%s", err, string(sresp))
}
if !sr.Completed {
t.Fatalf("expected completed, got %+v", sr)
}
// Verify.
vreq, _ := json.Marshal(struct{ ID string `json:"id"` }{ID: cr.ID})
vresp := rawCall(t, addr, OpEnvelopeVerify, vreq)
var vr VerifyResponse
if err := json.Unmarshal(vresp, &vr); err != nil {
t.Fatalf("verify resp: %v body=%s", err, string(vresp))
}
if !vr.Verified {
t.Fatalf("verify returned not-verified: %+v", vr)
}
}
// Unknown opcode → error response, not crash.
func TestZAP_UnknownOpcode(t *testing.T) {
addr, stop := startServer(t)
defer stop()
resp := rawCall(t, addr, 0x7777, []byte(`{}`))
if string(resp) == `{"ok":true}` {
t.Fatalf("unknown opcode returned ok body, expected error")
}
// Just need ANY response shape that's an error envelope; substring check.
var e struct{ Error string `json:"error"` }
if err := json.Unmarshal(resp, &e); err != nil {
t.Fatalf("expected error envelope, got %q", string(resp))
}
if e.Error == "" {
t.Fatalf("error field empty, body=%s", string(resp))
}
}
// Bad JSON in Create → error, not crash.
func TestZAP_BadJSON(t *testing.T) {
addr, stop := startServer(t)
defer stop()
resp := rawCall(t, addr, OpEnvelopeCreate, []byte("not json"))
var e struct{ Error string `json:"error"` }
if err := json.Unmarshal(resp, &e); err != nil {
t.Fatalf("expected error envelope, got %q", string(resp))
}
if e.Error == "" {
t.Fatalf("error field empty, body=%s", string(resp))
}
}
// Context is unused in the test but keep the import live for follow-on
// per-request cancellation handling.
var _ = context.TODO