Signatures
A digital signature has two parts: the mark, what people see in the signature field, such as a drawn signature; and the key, which seals the document so any later change shows. The signature plugin signs with both, checks the signatures a document already has, and warns before an edit would break one.
Register signaturePlugin() next to formPlugin() and stampPlugin(), with the key to sign with:
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
import { stampPlugin } from '@embedpdf/svelte/stamp';
import { createTestSigner, signaturePlugin } from '@embedpdf/svelte/signature';
import { localEngine } from '@embedpdf/engine';
import SignBar from './SignBar.svelte';
import '../basic.css';
const engine = localEngine();
const assetEngine = engine; // a person's marks are stamps, kept as PDFs; they open here too
// A throwaway key for the demo. Bring your own with `webCryptoSigner`, a
// service with `remoteSigner`, or a person's own with `personalSigner`.
const signer = createTestSigner({ commonName: 'Ada Lovelace' });
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
formPlugin(),
stampPlugin({ assetEngine }),
signaturePlugin({
key: () => signer,
// Trust the demo key itself, so its signatures check out as 'valid'.
trust: { anchors: async () => [(await signer).certificate] },
}),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<SignBar />
<Stage class="stage">
<RenderLayer />
<!-- An empty signature field is "sign here": a click makes it the target -->
<FormLayer />
</Stage>
</DocumentGate>
</Viewer>import { formPlugin } from '@embedpdf/svelte/form';
import { indexedDbKeyStore, personalSigner, signaturePlugin } from '@embedpdf/svelte/signature';
import { stampPlugin } from '@embedpdf/svelte/stamp';
import { rootCertificate } from './certificates';
export const plugins = [
/* … */
formPlugin(),
stampPlugin(),
signaturePlugin({
key: () => personalSigner({ subject: 'Ada Lovelace', store: indexedDbKeyStore('my-app-keys') }),
trust: { anchors: async () => [rootCertificate] }, // the certificates you trust
}),
];Sign a field#
const { version } = await signature.sign({
field: toFieldRef('approval'),
mark: { assetId },
signer: { reason: 'Approved', location: 'Amsterdam' },
});The mark becomes what the field shows, the key seals the document, and the signed document is saved as a new version. If anything fails along the way, nothing is left half-signed.
A mark is a stamp from a person’s signature library, or image bytes you bring:
{ assetId } // a stamp, drawn, typed or uploaded; see "A person's signatures" below
{ source: pngBytes } // a PNG, a JPEG, or a one-page PDFLet people click to sign#
People can also arm their signature and click where it goes, as in Preview. With the stamp plugin, arm a mark from a signature library:
- over an empty signature field, the click signs it;
- over a signed field, nothing happens;
- anywhere else, the mark is placed as a stamp.
placeMark() does the same from code:
await signature.placeMark({ assetId }, { field: toFieldRef('approval') }); // signs the field
await signature.placeMark({ assetId }, { page: page.ref, center: { x: 120, y: 90 } }); // a stampWhat happens in a field depends on mode: 'sign' signs it, 'visual' only draws the mark in,
and 'ask' asks your UI first through onSignRequested, so you can show your own sign dialog.
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
import { stampPlugin } from '@embedpdf/svelte/stamp';
import { createTestSigner, signaturePlugin } from '@embedpdf/svelte/signature';
import { localEngine } from '@embedpdf/engine';
import ArmBar from './ArmBar.svelte';
import '../click-to-sign.css';
const engine = localEngine();
const assetEngine = engine; // a person's marks are stamps, kept as PDFs; they open here too
const signer = createTestSigner({ commonName: 'Ada Lovelace' });
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
annotationPlugin(),
formPlugin(),
stampPlugin({ assetEngine }),
signaturePlugin({ key: () => signer }),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<ArmBar />
<Stage class="stage">
<RenderLayer />
<AnnotationLayer />
<FormLayer />
</Stage>
</DocumentGate>
</Viewer>Fill a field without signing#
Sometimes a signature only needs to look signed, such as for a form that gets printed. Draw the mark into the field without sealing anything:
await signature.fillField(toFieldRef('approval'), { source: pngBytes });
await signature.clearField(toFieldRef('approval'));A signed field can’t be filled or cleared: what it shows is part of what was signed.
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { formPlugin } from '@embedpdf/svelte/form';
import { stampPlugin } from '@embedpdf/svelte/stamp';
import { signaturePlugin } from '@embedpdf/svelte/signature';
import { localEngine } from '@embedpdf/engine';
import FillBar from './FillBar.svelte';
import '../fill.css';
const engine = localEngine();
const assetEngine = engine; // a person's marks are stamps, kept as PDFs; they open here too
// No key: a mark is only drawn in, nothing is sealed.
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
formPlugin(),
stampPlugin({ assetEngine }),
signaturePlugin(),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<FillBar />
<Stage class="stage">
<RenderLayer />
</Stage>
</DocumentGate>
</Viewer>The key#
The engine never holds a key. You give the plugin a signer, which does the signing.
@embedpdf/svelte/signature has one for each case:
// Your own key and certificate chain
webCryptoSigner({ privateKey, certificateChain: [leaf, issuer] });
// Your signing service or hardware module: only the document's fingerprint leaves the browser
remoteSigner({ sign: ({ digest, algorithm, subFilter }) => api.sign(digest, algorithm, subFilter) });
// A key per person, created in the browser and kept there, never readable
personalSigner({ subject: 'Ada Lovelace', store: indexedDbKeyStore('my-app-keys') });
// For tests and demos
createTestSigner({ commonName: 'Demo' });A personal signer works like signing in Preview, with real cryptography: a reader who trusts that
person’s certificate sees a valid signature, and everyone else sees “validity unknown”, the honest
answer for a key the person made themselves. For signatures everyone can check, sign with a
certificate from a trusted provider, through webCryptoSigner or remoteSigner.
Signing in two steps#
When the key signs somewhere else, such as on a phone or in a signing service that asks for approval, split the signing in two:
const { signingId, digest } = await signature.prepareSignature({ field, mark: { assetId } });
const cms = await myService.sign(digest); // may take minutes
await signature.completeSignature(signingId, cms);While a signature is prepared, the document can’t be changed, so nothing can slip in between.
signature.cancelPending() gives up.
Check the signatures#
<script lang="ts">
import { useSignatureState } from '@embedpdf/svelte/signature';
const state = useSignatureState();
</script>
<ul>
{#each state.signatures as sig (sig.fieldName)}
<li>{sig.fieldName}: {sig.verdict?.summary ?? 'checking…'}</li>
{/each}
</ul>A verdict’s summary is:
| Summary | Means |
|---|---|
'valid' | Nothing changed since it was signed, and you trust the signer |
'valid-untrusted' | Nothing changed, but the signer’s certificate isn’t one you trust |
'invalid' | The document changed in a way the signature doesn’t allow |
'indeterminate' | It couldn’t be checked, for example because a certificate is missing |
The plugin checks the signatures when the document opens, and again after every change. For a
detailed report of what changed since a signature, rule by rule, use signature.analyzeChanges().
Warn before an edit breaks a signature#
After an approval signature, filling in the form and adding more signatures keep it valid, but an annotation doesn’t. The plugin warns you the moment a change would break a signature once it’s saved, as Acrobat does:
<script lang="ts">
import { useSignature, useSignatureEvent } from '@embedpdf/svelte/signature';
import { toast } from './toast';
const signature = useSignature();
useSignatureEvent(
(s) => s.onInvalidationPredicted,
({ field }) => {
const name = signature.getSignature(field)?.fieldName;
toast(`This change will break the signature in "${name}" when saved.`);
},
);
</script>signature.getProtection() tells you what the document’s signatures allow, before anyone tries. A
signer who wants comments to stay allowed signs with a certification that allows them.
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
import { formPlugin } from '@embedpdf/svelte/form';
import { stampPlugin } from '@embedpdf/svelte/stamp';
import { createTestSigner, signaturePlugin } from '@embedpdf/svelte/signature';
import { localEngine } from '@embedpdf/engine';
import WarningBar from './WarningBar.svelte';
import '../break-warning.css';
const engine = localEngine();
const assetEngine = engine; // a person's marks are stamps, kept as PDFs; they open here too
const signer = createTestSigner({ commonName: 'Ada Lovelace' });
const plugins = [
stagePlugin(),
renderPlugin(),
interactionPlugin(),
annotationPlugin(),
formPlugin(),
stampPlugin({ assetEngine }),
signaturePlugin({
key: () => signer,
// Trust the demo key itself, so its signature checks out as 'valid'.
trust: { anchors: async () => [(await signer).certificate] },
}),
];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<WarningBar />
<Stage class="stage">
<RenderLayer />
<AnnotationLayer />
</Stage>
</DocumentGate>
</Viewer>A person’s signatures#
A person’s signature and initials are stamps, in a library of kind 'signatures', drawn, typed or
uploaded with the stamp plugin:
const { library } = await stamp.createLibrary('Ada Lovelace', { kind: 'signatures' });
await stamp.createAsset({ libraryId: library.id, name: 'signature', label: 'Signature', mark: { kind: 'ink', strokes } });
await stamp.createAsset({
libraryId: library.id,
name: 'initials',
label: 'Initials',
mark: { kind: 'text', text: 'AL', fontFamily: 'times-italic' },
});useSignerRows() lists each person with their signature and initials, for a picker. The library is
a normal PDF, so Acrobat can open it too.
”Sign here” in a form#
With the form layer, an empty signature field shows a “sign here” button. Clicking it makes the field the target: the next mark the person picks goes there. A signed field, clicked, asks your UI to show its details:
signature.setTarget(toFieldRef('approval')); // the next mark goes here; null clears| Event | Fires when | Show |
|---|---|---|
onTargetChanged | A field became the target | your signatures panel |
onSignRequested | In 'ask' mode, a mark was dropped on a field | your sign dialog |
onInspectionRequested | A signed field was clicked | the signature’s details |
Methods#
Everything useSignature() gives you.
| Method | What it does |
|---|---|
sign({ field, mark }) | Signs a field: the mark becomes what it shows, and the key seals the document. Also takes signer, a per-call key, certify and lock, and resolves { signature, version, protection }. Sign a field |
placeMark(mark, { field }), placeMark(mark, { page, center }) | Does what a click with an armed mark does: in a field, signs, fills or asks, by mode; anywhere else, places a stamp. Resolves what it did as kind: 'signed', 'filled', 'requested' or 'placed'. Let people click to sign |
fillField(field, mark), clearField(field) | Draws a mark into a field without signing, or takes it out again. Refused on a signed field. Fill a field without signing |
prepareSignature({ field, mark }), completeSignature(signingId, cms) | Signs in two steps: the first resolves { signingId, digest } for your key to sign, the second seals the document with what the key returned. Signing in two steps |
cancelPending() | Gives up a signature prepared in two steps, so the document can be changed again |
validate({ until? }) | Checks every signature again and resolves the verdicts. until: 'persisted' checks the file as it was opened. Which bytes are checked |
validateField(field) | Checks one signed field and resolves its verdict |
getVerdict(field), listVerdicts() | One signed field’s verdict from the last check, or every verdict; null before the first check. Check the signatures |
getSignature(field) | One signature field’s facts, signed or not, such as fieldName, signed and signer, or null |
listUnsignedFields() | The signature fields still to sign, in document order |
analyzeChanges({ since }) | What changed after a signature ({ signatureIndex }) or a version of the file ({ revisionIndex }), rule by rule |
readRevision(field), readRevision({ revisionIndex }) | The exact bytes of the file version a signature covers. The signed bytes |
setTarget(field) | Makes a field the target, where the next mark the person picks goes; null clears. “Sign here” in a form |
requestInspection(field) | Asks your UI to show a signed field’s details, as clicking it does: onInspectionRequested fires |
getMode() | The mode in effect: the mode setting, or its default. Settings |
getSnapshot() | Everything at once, as the engine reads it: every signature field, signed or not, the file’s versions, and protection |
refresh() | Reads the signatures again. You rarely need it: the plugin reads and checks them again after every change. |
canSign(field), canCertify(), canFill(field), canReadRevision() | Whether signing a field, certifying, drawing a mark into a field without signing, and reading the signed bytes are allowed. Permissions |
getSettings(), updateSettings(changes), resetSettings() | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use useSignatureSettings(). Settings |
field is a field’s ref, such as toFieldRef('approval'); the methods that only read also take the
ref of the field’s box. Every method that returns a promise also takes a signal in its options, to
cancel it.
State#
Everything useSignatureState() gives you, as one reactive object: read its fields, and your markup updates when they change. The method in the last column reads the same value once.
| Field | What it is | Read once |
|---|---|---|
signatures | The signed fields, in document order, each with its verdict once checked | listSignatures() |
protection | What the document’s signatures allow, or null before they’re read | getProtection() |
target | The field the next mark goes to, or null | getTarget() |
busy | Whether a signing or a fill is running | isBusy() |
pending | A signature prepared in two steps and not completed yet, as { signingId, field }, or null. Until it’s completed or cancelled, the document can’t be changed. | getPending() |
status | Whether the signatures are read: 'idle', 'loading', 'ready', 'forbidden' or 'error' | getStatus() |
Settings#
Set them when you register the plugin, and change them while the app runs with
signature.updateSettings({ … }); signature.resetSettings() goes back to what you registered, and
useSignatureSettings() gives you the current settings.
| Setting | What it does | Default |
|---|---|---|
key | The signer, or a function that returns one when someone signs | none |
mode | What a mark does in a field: 'sign', 'visual' or 'ask' | 'sign' with a key, else 'visual' |
trust | The certificates you trust, for checking signatures | none |
allowCertify | Let a first signature be a certification, which sets what stays allowed | false |
Events#
<script lang="ts">
import { useSignatureEvent } from '@embedpdf/svelte/signature';
import { showWarning } from './warnings';
useSignatureEvent(
(signature) => signature.onInvalidationPredicted,
() => showWarning('Saving this change will break a signature.'),
);
</script>| Event | Fires when | Carries |
|---|---|---|
onSigned | A field was signed, whoever signed it | field, result, origin |
onFilled, onCleared | A mark was drawn into a field, or removed | field |
onValidated | The signatures were checked | verdicts |
onProtectionChanged | What the signatures allow changed | protection |
onInvalidationPredicted | A change would break a signature once saved | field, detail |
onTargetChanged | A field became the target, or the target was cleared | field (null when cleared) |
onSignRequested | In 'ask' mode, a mark was dropped on a field | field, mark |
onInspectionRequested | A signed field was clicked | field |
onSettingsChanged | The settings changed | settings, changed |
result is what sign() resolves. “Sign here” in a form says what to show
for the last three.
Permissions#
| Permission | What it allows | Check |
|---|---|---|
doc.sign | Signing | signature.canSign(field) |
doc.sign.certify | Signing a certification | signature.canCertify() |
doc.forms.fill | Drawing a mark into a field without signing | signature.canFill(field) |
doc.download | Reading the exact bytes a signature covers | signature.canReadRevision() |
A user with a fields:sign (fields:fill) permission for one group signs (fills in) that group’s
fields only, so the checks take the field. See Signers and groups. On a
signed document, the engine also refuses what its signatures forbid.
Good to know#
Which bytes are checked#
The plugin checks the document as it is in the viewer, with changes that haven’t been downloaded yet,
so the verdict is the one the downloaded file will get. validate({ until: 'persisted' }) checks the
file as it was opened.
The signed bytes#
signature.readRevision({ revisionIndex }) gives you the exact version of the file a signature
covers, for an audit trail.
How signatures are stored, and what each certification allows, is on Digital signatures.
Your feedback goes directly to the documentation team.