mirror of
https://github.com/luxfi/esign.git
synced 2026-10-30 16:58:18 +00:00
Replace 'Liquid EVM tx hash' with 'Lux EVM tx hash' — Lux esign notarization is on Lux's chain, not the Liquidity-flavored EVM.
152 lines
5.1 KiB
Go
152 lines
5.1 KiB
Go
// Copyright 2026 Lux Industries Inc. and the luxfi/esign authors.
|
|
// Licensed under the Apache License, Version 2.0 (see LICENSE).
|
|
|
|
// Package envelope defines the canonical eSign envelope state machine.
|
|
//
|
|
// An envelope is the unit of signing: it bundles one or more PDF documents,
|
|
// a list of required signers, and an append-only audit chain. Envelopes
|
|
// progress through a fixed set of states; any unknown transition is an
|
|
// error. The state machine is independent of crypto and transport, so it
|
|
// can be exercised by unit tests without a signer or a network.
|
|
//
|
|
// Versions
|
|
//
|
|
// Every state-changing operation produces a new envelope version. Version
|
|
// N's hash is included in version N+1's audit event, forming a chain that
|
|
// makes any historical tamper detectable by comparing the current chain
|
|
// tip to the version most-recently observed by a third party.
|
|
package envelope
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// State enumerates the legal positions in the envelope lifecycle.
|
|
//
|
|
// Draft — created, not yet sent for signature
|
|
// Sent — signers notified, awaiting first action
|
|
// Viewed — at least one signer has opened the document
|
|
// Signing — at least one signer has signed; not all required signers complete
|
|
// Completed — every required signer has signed successfully
|
|
// Voided — terminal: creator revoked the envelope before completion
|
|
// Expired — terminal: envelope passed its ExpiresAt without completing
|
|
// Declined — terminal: a required signer declined to sign
|
|
type State uint8
|
|
|
|
const (
|
|
StateDraft State = iota
|
|
StateSent
|
|
StateViewed
|
|
StateSigning
|
|
StateCompleted
|
|
StateVoided
|
|
StateExpired
|
|
StateDeclined
|
|
)
|
|
|
|
var stateNames = map[State]string{
|
|
StateDraft: "draft",
|
|
StateSent: "sent",
|
|
StateViewed: "viewed",
|
|
StateSigning: "signing",
|
|
StateCompleted: "completed",
|
|
StateVoided: "voided",
|
|
StateExpired: "expired",
|
|
StateDeclined: "declined",
|
|
}
|
|
|
|
// String renders the state in lowercase wire form ("draft", "sent", …).
|
|
func (s State) String() string {
|
|
if n, ok := stateNames[s]; ok {
|
|
return n
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// IsTerminal reports whether s is an end state — no further transitions allowed.
|
|
func (s State) IsTerminal() bool {
|
|
switch s {
|
|
case StateCompleted, StateVoided, StateExpired, StateDeclined:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SignMethod tags how a signer's signature was produced. The choice affects
|
|
// which Signer/Verifier in pkg/sign is used; envelope.go is agnostic.
|
|
type SignMethod uint8
|
|
|
|
const (
|
|
MethodUnspecified SignMethod = iota
|
|
MethodEd25519 // stdlib crypto/ed25519
|
|
MethodMLDSA65 // post-quantum Dilithium (luxfi/keys)
|
|
MethodFROST // Schnorr threshold (luxfi/threshold)
|
|
MethodMPCECDSA // ECDSA via CGGMP21 (luxfi/mpc)
|
|
MethodECDSAP256 // stdlib crypto/ecdsa, P-256
|
|
MethodRSAPSS // stdlib crypto/rsa, PSS — DocuSign/Adobe interop
|
|
)
|
|
|
|
// SignerSlot describes one party who must sign before the envelope completes.
|
|
// SignerHash is the canonical 32-byte identifier — typically SHA-256 of the
|
|
// signer's public key, or for FROST/MPC, the hash of the group public key.
|
|
// Order=0 means "any sequence"; Order>=1 enforces strict sequential signing.
|
|
type SignerSlot struct {
|
|
SignerHash [32]byte
|
|
Method SignMethod
|
|
Required bool
|
|
Order uint8
|
|
}
|
|
|
|
// Signature is one signer's contribution to the envelope.
|
|
type Signature struct {
|
|
SignerHash [32]byte
|
|
Method SignMethod
|
|
SignedAt time.Time
|
|
Sig []byte // raw signature bytes; format depends on Method
|
|
PubKey []byte // signer pubkey (or group pubkey for threshold)
|
|
DocHash [32]byte
|
|
}
|
|
|
|
// AuditEvent records every observable transition. Hash chains backward to
|
|
// PrevHash so any state tampering is detected by re-hashing forward.
|
|
type AuditEvent struct {
|
|
Seq uint32
|
|
At time.Time
|
|
Actor [32]byte // hash of the principal causing the transition
|
|
From State
|
|
To State
|
|
Note string
|
|
PrevHash [32]byte // chain-back hash; zero on Seq=0
|
|
EventHash [32]byte // SHA-256 of canonical encoding of all fields above
|
|
}
|
|
|
|
// Envelope is the working representation of an eSign job.
|
|
type Envelope struct {
|
|
ID string
|
|
Version uint32
|
|
Document []byte // canonical PDF bytes
|
|
DocHash [32]byte
|
|
Signers []SignerSlot
|
|
Signatures []Signature
|
|
State State
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
ExpiresAt time.Time // zero = no expiry
|
|
AuditChain []AuditEvent
|
|
// OnchainTx is the optional Lux EVM tx hash where the final envelope
|
|
// hash was anchored. Set only on completed envelopes that opted into
|
|
// on-chain notarization.
|
|
OnchainTx string
|
|
}
|
|
|
|
// Errors surfaced from the state machine and signature collection.
|
|
var (
|
|
ErrInvalidTransition = errors.New("envelope: invalid state transition")
|
|
ErrAlreadyTerminal = errors.New("envelope: already in terminal state")
|
|
ErrUnknownSigner = errors.New("envelope: signer not in slot list")
|
|
ErrSignerOutOfOrder = errors.New("envelope: signer signed out of declared order")
|
|
ErrDuplicateSignature = errors.New("envelope: signer already signed")
|
|
ErrDocHashMismatch = errors.New("envelope: signature dochash differs from envelope dochash")
|
|
)
|