Skip to content

Signatures

The Signatures capability is Lukeflow's native e-signature stack: place signing fields on a PDF, send a per-recipient signing link, capture a drawn/typed mark in a guided ceremony, then produce a cryptographically PAdES-sealed PDF with an embedded audit trail. Partial

It has a deliberately clean split of responsibilities:

  • The frontend contract + the ceremony UI live in the library (luke-signatures@lukeflow/sign-core + @lukeflow/sign-react). Types, schema validation, PDF geometry, transport-agnostic clients, and the React designer/ceremony components. This layer stamps intent onto a PDF; it performs no cryptography.
  • The cryptographic PAdES signing is ENGINE-side (Core Engine, package com.luke.engine.capability.signature). The engine flips coordinates, draws the signature PNG with Apache PDFBox, appends a Certificate of Completion, and applies a detached CAdES/PAdES CMS seal with an RFC-3161 timestamp using BouncyCastle.

The two halves meet at an HTTP contract: sign-core's clients call the engine's /api/signature-definitions, /api/signature-instances, and /api/public/sign-instance routes.

Components at a glance

Two-phase model

Signatures mirror the Forms lifecycle 1:1 (FormDefinition → FormVersion → FormInstance). A SignatureDefinition is the editable, versioned, legally-signed-off template; a SignatureInstance is one signing event that pins a published version so the document can never shift mid-ceremony.

The design-time schema (SignatureSchema)

Defined in sign-core/schema.ts, validated by validateSignatureSchema, and coerced by repairSignatureSchema so a malformed stored draft never crashes the builder. The engine reads it back through the tolerant SignatureSchemaModel.parse.

ElementShapeMeaning
document{ key?, name, pageCount? }Source PDF referenced by an opaque DocumentStore key — bytes never live in the schema or DB
signersSignerRole[]{ id, label, order, verify?, color? }Signing parties; equal order sign in parallel, ascending order sign sequentially
fieldsSchemaField[]{ id, signerId, type, page, x, y, w, h }Placed fields, one signerId each. Types: SIGNATURE, INITIALS, DATE, NAME, TEXT
variablesSchemaVariable[]{ key, label, required? }Declared merge attributes the campaign supplies at start
routing"sequential" | "parallel"Whether recipients are armed one-at-a-time or all at once

Geometry (sign-core/geometry.ts)

All field coordinates are PDF points with a TOP-LEFT origin; the engine flips Y to PDF bottom-left at stamp time. The library and engine agree on this convention exactly.

HelperResponsibility
cssToPdf(g, cssX, cssY)Map a click in rendered CSS pixels to PDF points
placeField(g, cssX, cssY, size, page)Center a fixed-size field on a click, clamped to the page
fieldToCssRect(g, field)Project a PDF-point field rect back to a CSS overlay rect
isRotated(g)g.rotate % 360 !== 0a rotated page is refused on both sides

The engine enforces the same guard in NativeSignatureProvider.pageFor: a page with /Rotate ≠ 0 throws "Rotated pages are not supported" because it stamps into the un-rotated mediabox, and field.page out of range is rejected.

Design flow (place fields)

The designer runs entirely in the browser on SignatureBuilder, which renders the PDF via DocumentViewer and lets the user drop fields per signer. The host debounces onChange into saveDraft, then checks in a snapshot, gets legal sign-off, and publishes.

Function / methodWhereResponsibility
SignatureBuildersign-reactUncontrolled designer: per-type field sizing, signer palette, click-to-place, live validation; exposes getSchema / setSchema via ref
validateSignatureSchemasign-coreProduces SchemaProblem[]; error-level findings gate sign-off & publish
repairSignatureSchema / parseSignatureSchemasign-coreCoerce unknown/partial JSON into a valid schema, dropping fields that reference missing signers
SignatureDefinitionService.saveDraftenginePersist the draft schema on the definition
.checkInengineSnapshot the draft into an immutable SignatureVersion
.signOffengineRecord signedOffAt — the legal review gate (no separate Java validator; sign-off carries it)
.publishengineSet publishedVersion; blocked unless the version is signed off
.uploadDocument / .fetchDocumentengineStore/serve the source PDF bytes through the DocumentStore

The design-time lock model (checkout / release / discard / retire) matches Forms exactly — see Forms design-time lifecycle.

Signing ceremony → signed PDF

Starting a campaign binds a published definition to recipients (one per signer role) and the supplied values, mints per-recipient signing tokens, and writes a transactional outbox row that drives the Camunda ceremony process. Each recipient opens their unguessable token link, adopts a signature, and consents; when the last recipient signs, the engine seals the finished PDF.

Function / endpointWhereResponsibility
startCampaignSignatureInstanceServiceValidate roles + required variables, mint tokens, arm first (sequential) or all (parallel) recipients, write outbox row
SignatureProcessOutboxConsumer.drainengineQUEUED→STARTED launches the ceremony process; STARTED→CLOSED correlates closure on terminal state
SignatureCeremonyService.start / .correlateClosureengineIn-process Camunda start (idempotent on businessKey) + SignatureClosure message correlation
viewForSigning(token)SignatureInstanceServiceReturn the recipient's session (PDF base64 + their fields + merged values); marks R_OPENED / OPENED
sign(token, png, consent)SignatureInstanceServiceStore the PNG in the DocumentStore (never the DB), mark R_SIGNED, advance routing, seal on last signer
PublicInstanceSignController.signenginePublic route; decodes a bounded (≤512 KB), magic-byte-checked base64 PNG
sealClosureSignatureInstanceServiceBuild stamps from the pinned schema and call the provider; sets sealStatus SEALED|FAILED — never throws (retryable via reseal)
NativeSignatureProvider.sealEnvelopeengineDraw each recipient's PNG / DATE / NAME at its field, append the multi-signer Certificate of Completion
SelfSealTrustProvider.sealengineApply the detached PAdES CMS signature + embed the RFC-3161 timestamp; saveIncremental

The PAdES seal (engine-side cryptography)

SelfSealTrustProvider (selected unless LUKE_SIGN_TRUST_MODE=qtsp) is the legally-binding layer:

  • Adds a PDSignature with filter Adobe.PPKLite / subfilter ETSI.CAdES.detached (PAdES) and reserves 64 KB for the CMS.
  • The inner CadesSigner builds a BouncyCastle CMSSignedData over the PDF byte range: SHA256withRSA content signer, the org PKCS#12 chain (KeystoreSigningKeyProvider — loaded lazily from LUKE_SIGN_KEYSTORE_BASE64), and the CAdES-BES signing-certificate-v2 signed attribute.
  • HttpTsaClient fetches an RFC-3161 trusted timestamp (LUKE_SIGN_TSA_URL, default DigiCert), embedded as the signature-time-stamp unsigned attribute.

This yields a US ESIGN/UETA-binding signature at ≈ $0/signature. The Adobe "green check" additionally needs an AATL-member CA cert; EU eIDAS QES is a config swap to QtspTrustProvider (TRUST_MODE=qtsp), built post-V1. NativeSignatureProvider also appends a Certificate of Completion page rendering the IP-stamped audit trail (AuditService stamps each event with client IP, user-agent, IpRisk, and geo) plus the SHA-256 of the source.

Component reference

ComponentTypeResponsibilityKey methods / props
SignatureBuildersign-reactDesign-time PDF field designerinitialSchema, onUploadDocument, getSchema/setSchema (ref)
SigningCeremonysign-reactGuided review → adopt → confirm ceremonysession, onSubmit, adoptMethods
AdoptSignaturesign-reactDraw / type / upload tabs, per-method value retentiononChange(pngDataUrl), methods
SignaturePadsign-reactSelf-contained draw-to-sign canvas (mouse + touch)onChange, penColor
DocumentViewersign-reactMulti-page PDF viewer + zoom + click-capture + overlay slotonClick, renderOverlay, onGeometry
PdfViewsign-reactSingle-page renderer reporting PdfGeometryonGeometry, onClick, overlay
createSignatureDefinitionsClientsign-coreDesign-time HTTP clientsaveDraft, checkIn, publish, signOff
createSignatureInstancesClientsign-coreRuntime campaign clientstartCampaign, getRecipientSession, submit
SignatureDefinitionServiceengineDesign-time lifecycle + versions + document storecheckIn, signOff, publish, uploadDocument
SignatureInstanceServiceengineCampaign start, per-recipient signing, closure sealingstartCampaign, sign, sealClosure, reseal
NativeSignatureProviderengineDraw marks + Certificate of Completion (PDFBox)stampAndSign, sealEnvelope
SelfSealTrustProviderengineDetached PAdES CMS seal + RFC-3161 (BouncyCastle)seal
DocumentStoreenginePluggable PDF/PNG object storage; bytes never hit Postgresput, get, delete (LocalFs / S3 impls)

Endpoints

Design-time — /api/signature-definitions (authenticated, X-Tenant-Id):

Method · PathPurpose
GET / · POST /List / create a definition
GET·PATCH·DELETE /{id}Get / rename / soft-delete
PUT /{id}/draftSave the working schema draft
POST /{id}/versionsCheck in an immutable version snapshot
GET /{id}/versions · GET /{id}/versions/{v}List / fetch versions
POST /{id}/versions/{v}/publish · /restorePublish (gated on sign-off) / restore a version
POST /{id}/sign-offLegal review gate
POST /{id}/checkout · /release · /discard · /retire · /unretire · /restoreAdvisory-lock lifecycle
POST /{id}/document · GET /{id}/document/{key}Upload / fetch the source PDF
GET /{id}/audit · DELETE /{id}/purgeAudit trail / hard purge

Runtime — /api/signature-instances (authenticated):

Method · PathPurpose
POST /Start a campaign from a published definition
GET / · GET /{id}List / detail (recipients + tokens for delivery)
POST /{id}/cancelCancel a non-terminal instance
GET /{id}/signed.pdfStream the sealed PDF (409 until SEALED)
POST /{id}/sealRe-seal a completed instance whose seal failed
POST /{id}/recipients/{signerId}/remind(Delivery lands with SIG-9)

Public signing — /api/public/sign-instance/{token} (token-authenticated, no tenant header):

Method · PathPurpose
GET /{token}Load the recipient's signing session
POST /{token}Submit the drawn PNG + consent

A single-request legacy path also exists (/api/signatures + public /api/public/sign/{token}) for one-signer requests, backed by SignatureController / PublicSignatureController and NativeSignatureProvider.stampAndSign.

Status & gaps

Signatures are Partial: the two-phase model, designer, ceremony, PAdES sealing, IP-stamped audit, and multi-recipient routing are built and green, but email delivery of signing links (SIG-9, Postmark), EU eIDAS QES (QtspTrustProvider), and identity verification methods (EMAIL_OTP / SMS_OTP / IDV) beyond NONE are not yet wired. The engine module is folded into core but production rollout of the signing keystore/TSA is deferred.

See the Completeness Scorecard for the live status.

Related pages: Signatures library · Core Engine · File Proxy

Lukeflow Manual · documentation snapshot July 2026