mirror of
https://github.com/luxfi/esign.git
synced 2026-10-30 16:58:18 +00:00
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.
177 lines
5.0 KiB
Go
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
|