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.
170 lines
4.5 KiB
Go
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))
|
|
}
|
|
}
|