Files
esign/pkg/api/handlers_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

170 lines
4.5 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"
"testing"
"time"
"github.com/luxfi/esign/pkg/envelope"
"github.com/luxfi/esign/pkg/sign"
)
// End-to-end happy path: create envelope → transition Sent → sign → Verify
// passes → audit chain intact. Single-signer flow.
func TestHandler_E2E_SingleSigner(t *testing.T) {
ctx := context.Background()
now := time.Unix(1_700_000_000, 0).UTC()
h := &Handler{Store: NewMemStore(), Now: func() time.Time { return now }}
signer, err := sign.NewEd25519Signer()
if err != nil {
t.Fatalf("NewEd25519Signer: %v", err)
}
signerHash := sha256.Sum256(signer.Public())
// Create.
doc := []byte("Subscription Agreement v1\n")
cr, err := h.Create(ctx, CreateRequest{
Document: doc,
Signers: []envelope.SignerSlot{
{SignerHash: signerHash, Method: envelope.MethodEd25519, Required: true},
},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
if cr.ID == "" {
t.Fatal("Create returned empty ID")
}
// Sent.
if _, err := h.Transition(ctx, TransitionRequest{
EnvelopeID: cr.ID, Target: envelope.StateSent, Actor: [32]byte{0xaa},
}); err != nil {
t.Fatalf("Transition→Sent: %v", err)
}
// Sign.
docHash := sha256.Sum256(doc)
rawSig, err := signer.Sign(docHash[:])
if err != nil {
t.Fatalf("Sign: %v", err)
}
sr, err := h.Sign(ctx, SignRequest{
EnvelopeID: cr.ID,
SignerHash: signerHash,
Method: envelope.MethodEd25519,
Sig: rawSig,
PubKey: signer.Public(),
})
if err != nil {
t.Fatalf("Sign: %v", err)
}
if !sr.Completed {
t.Errorf("expected completed, got state=%s", sr.State)
}
// Verify.
vr, err := h.Verify(ctx, cr.ID)
if err != nil {
t.Fatalf("Verify: %v", err)
}
if !vr.Verified {
t.Fatalf("Verify returned not-verified: %+v", vr)
}
if vr.BrokenAuditEvent != -1 || vr.FailedSignature != -1 {
t.Errorf("expected -1/-1, got %d/%d", vr.BrokenAuditEvent, vr.FailedSignature)
}
}
// Tampering with a signature byte in the store is detected by Verify.
func TestHandler_Verify_DetectsSigTampering(t *testing.T) {
ctx := context.Background()
store := NewMemStore()
h := &Handler{Store: store, Now: time.Now}
signer, _ := sign.NewEd25519Signer()
signerHash := sha256.Sum256(signer.Public())
doc := []byte("doc")
cr, _ := h.Create(ctx, CreateRequest{
Document: doc,
Signers: []envelope.SignerSlot{
{SignerHash: signerHash, Method: envelope.MethodEd25519, Required: true},
},
})
_, _ = h.Transition(ctx, TransitionRequest{
EnvelopeID: cr.ID, Target: envelope.StateSent, Actor: [32]byte{0xaa},
})
docHash := sha256.Sum256(doc)
rawSig, _ := signer.Sign(docHash[:])
_, err := h.Sign(ctx, SignRequest{
EnvelopeID: cr.ID, SignerHash: signerHash,
Method: envelope.MethodEd25519, Sig: rawSig, PubKey: signer.Public(),
})
if err != nil {
t.Fatalf("Sign: %v", err)
}
// Tamper directly in the store: flip a sig byte.
e, _ := store.Get(ctx, cr.ID)
e.Signatures[0].Sig[0] ^= 0x01
_ = store.Put(ctx, e)
vr, err := h.Verify(ctx, cr.ID)
if err != nil {
t.Fatalf("Verify: %v", err)
}
if vr.Verified || vr.FailedSignature != 0 {
t.Errorf("expected failed-sig=0, got verified=%v failedSig=%d",
vr.Verified, vr.FailedSignature)
}
}
// Trying to sign with a mismatched (pubKey, sig) pair fails the crypto
// check BEFORE the envelope's signature list is touched.
func TestHandler_Sign_CryptoFailsBeforeStore(t *testing.T) {
ctx := context.Background()
store := NewMemStore()
h := &Handler{Store: store, Now: time.Now}
signer, _ := sign.NewEd25519Signer()
imposter, _ := sign.NewEd25519Signer()
signerHash := sha256.Sum256(signer.Public())
doc := []byte("doc")
cr, _ := h.Create(ctx, CreateRequest{
Document: doc,
Signers: []envelope.SignerSlot{
{SignerHash: signerHash, Method: envelope.MethodEd25519, Required: true},
},
})
_, _ = h.Transition(ctx, TransitionRequest{
EnvelopeID: cr.ID, Target: envelope.StateSent, Actor: [32]byte{0xaa},
})
docHash := sha256.Sum256(doc)
imposterSig, _ := imposter.Sign(docHash[:])
// signer's pubKey + imposter's sig → must fail crypto.
_, err := h.Sign(ctx, SignRequest{
EnvelopeID: cr.ID, SignerHash: signerHash,
Method: envelope.MethodEd25519,
Sig: imposterSig,
PubKey: signer.Public(),
})
if err != sign.ErrBadSignature {
t.Fatalf("got %v, want ErrBadSignature", err)
}
// Envelope was never updated.
e, _ := store.Get(ctx, cr.ID)
if len(e.Signatures) != 0 {
t.Errorf("expected 0 sigs in store, got %d", len(e.Signatures))
}
}