Signer Plugin
Sign PDF and Word documents with a visible signature and a digital seal
The Signer plugin places a visible signature on a document and, by default, seals it with a digital signature that a PDF reader can verify. It signs PDF and Word (.docx) files. The visible signature is always a PNG image: drawn, typed, or uploaded by the signer.
| Surface | Use it when |
|---|---|
DocumentSigner (C#) | You sign on the server, with no browser involved |
Drop-in widget (doconutSigner.js) | You want a ready-made upload, sign and download screen in your page |
HTTP endpoints (?sign=open|run|download) | You build your own signing UI against a documented server contract |
Each signature is placed once, on one page. The signer's name, the reason and the location can travel with it.
Install the package
Install the latest stable Signer plugin:
dotnet add package Doconut.NET8.SignerTo pin the plugin to the release this documentation describes, pass the version separately:
dotnet add package Doconut.NET8.Signer --version 26.10.0Keep the Signer package at the same version as Doconut.NET8. The package ID is Doconut.NET8.Signer; .<version> appears only in the downloaded .nupkg filename.
The plugin's license file, Doconut.Viewer.Signer.lic, goes in the same folder as Doconut.Viewer.lic; it is read together with the viewer license.
Platforms. The plugin runs wherever Doconut.NET8 runs: Windows, and glibc-based Linux on x64 (not Alpine or other musl-based distributions). On Linux, install the font configuration library before the first document opens:
apt-get update && apt-get install -y libfontconfig1On RHEL, Fedora and similar distributions the package is fontconfig. Without it, opening a document fails with The system library libfontconfig1 is missing. Install it (Debian/Ubuntu: apt-get install -y libfontconfig1) and see the Linux section of the Doconut documentation.
Register the plugin
Like every Doconut plugin, Signer registers with AddPlugin<TPlugin>() inside AddDoconut():
builder.Services.AddDoconut(options =>
{
options.LicensePath = "Doconut.Viewer.lic";
options.AddPlugin<Doconut.Plugins.Signer.SignerPlugin>();
});That is all the C# facade needs. The widget and its HTTP endpoints are a separate opt-in, AddSignerWidget() (see Drop-in widget).
The plugin needs a license that grants the Signer capability. Without one, AddDoconut() throws at startup, before the app serves a request:
The Signer plugin is registered via AddPlugin but no active license grants Signer. Remove the AddPlugin<...>() call or install a license (or trial/demo) that includes Signer.It throws for no license, a legacy TRIAL file, a license without Signer, and a paid license used outside its version window. Temporary Demo/NFR licenses are accepted. See License Setup for how licenses are loaded.
Licensing & watermark
| License state | Startup | Certified signature | Visual signature |
|---|---|---|---|
Paid license granting Signer, certificate configured | Starts | Sealed with your certificate, clean (watermarked: false) | Clean |
Paid license granting Signer, no certificate | Starts | Refused: No signing certificate configured. | Clean |
| Paid license, certificate expired or not yet valid | Starts (with the widget enabled, a Warning is logged) | Refused: The signing certificate expired on yyyy-MM-dd. | Clean |
| Temporary Demo/NFR license, active or expired | Starts | Sealed with the built-in demo certificate, watermarked (watermarked: true) | Watermarked |
No license, a legacy TRIAL file, a license without Signer, or a paid license outside its version window | AddDoconut() throws | — | — |
The rule is the same for the C# facade and the widget: output is watermarked when the license is not a paid viewer license or is temporary. Under a temporary license the plugin always signs with its built-in demo certificate, and any certificate you configure is not used.
The demo certificate produces a genuine seal, but PDF readers do not trust it. An integration can be built and tested end to end on an evaluation license; to get clean, trusted output you need both a paid license and your own signing certificate.
Use from C#
DocumentSigner is registered by AddDoconut() and resolved from dependency injection. Its constructor is internal, so never new it. Resolving it without AddPlugin<SignerPlugin>() throws No IDocumentSigner is registered. Add the signing plugin: options.AddPlugin<Doconut.Plugins.Signer.SignerPlugin>().
using Doconut;
using Doconut.Core.Models.Signing;
public sealed class ContractSigning
{
private readonly DocumentSigner _signer;
public ContractSigning(DocumentSigner signer) => _signer = signer;
public async Task<byte[]> SignAsync(Stream pdf, byte[] signaturePng, SigningCertificate certificate, CancellationToken ct)
{
var request = new SignRequest(
new VisibleSignature(SignatureKind.Drawn, signaturePng),
new SignaturePlacement(Page: 1, X: 0.58, Y: 0.80, W: 0.32, H: 0.12),
SignerName: "Ada Lovelace",
Reason: "I approve this document",
Location: "London, UK");
// Mode defaults to Certified (visible signature + digital seal); WordOutput defaults to Pdf.
SignResult result = await _signer.SignAsync(pdf, ".pdf", request, certificate, ct);
using (result.Stream)
using (var output = new MemoryStream())
{
await result.Stream.CopyToAsync(output, ct);
return output.ToArray();
}
}
}SignAsync takes the source stream, its extension (".pdf" or ".docx"), the request, and the certificate for this call. The facade does not read SignerWidgetOptions.SigningCertificate; that setting is for the widget. Pass null under a demo license, or in Visual mode.
SignResult holds the signed document as a seekable stream, plus its Extension (".pdf" or ".docx") so you can name the file. You own the stream: dispose it when you are done.
A Visual signature stamps the image only and needs no certificate:
var signer = app.Services.GetRequiredService<DocumentSigner>();
byte[] signaturePng = File.ReadAllBytes("signature.png");
var request = new SignRequest(
new VisibleSignature(SignatureKind.Image, signaturePng),
new SignaturePlacement(Page: 1, X: 0.58, Y: 0.80, W: 0.32, H: 0.12),
Mode: SignatureMode.Visual);
SignResult result = await signer.SignAsync(stream, ".pdf", request, ct: ct);A Word source is signed to PDF unless you ask for Word output. Name the result from result.Extension:
var signer = app.Services.GetRequiredService<DocumentSigner>();
byte[] signaturePng = File.ReadAllBytes("signature.png");
var request = new SignRequest(
new VisibleSignature(SignatureKind.Drawn, signaturePng),
new SignaturePlacement(Page: 1, X: 0.58, Y: 0.80, W: 0.32, H: 0.12),
WordOutput: WordOutput.Docx);
SignResult result = await signer.SignAsync(stream, ".docx", request, ct: ct);
string downloadName = "contract-signed" + result.Extension; // ".docx"SignAsync throws instead of returning when the call cannot succeed:
| Exception | Message | When |
|---|---|---|
InvalidOperationException | No signing certificate configured. | Certified mode, a paid license, and no certificate passed |
InvalidOperationException | The signing certificate expired on yyyy-MM-dd. / The signing certificate is not valid before yyyy-MM-dd. | Certified mode with a certificate outside its validity window |
ArgumentException | The sign request is invalid: the visible signature needs PNG bytes and the placement must be a normalized rectangle on a 1-based page. | Empty PNG bytes, or a placement that is not valid |
NotSupportedException | The signer supports PDF and Word (.docx) documents; got '{extension}'. | Any other source type |
Signing modes, Word output and certificates
Signing modes
SignRequest.Mode chooses whether the signature is sealed.
| Mode | What it produces | Certificate | Verifiable |
|---|---|---|---|
SignatureMode.Certified (default) | The visible signature plus a digital seal | Required under a paid license | Yes |
SignatureMode.Visual | The visible signature only | Never needed | No: it is a picture |
Use Certified when the signature must be provable: approvals, contracts, anything a verifier needs to check for identity and integrity. Use Visual when you only need the appearance of a signature, or have no certificate. A temporary license watermarks both.
PDF seals are PKCS#7 detached signatures (adbe.pkcs7.detached) with SHA-256, for RSA or ECDSA keys. They carry the issuer certificates found in your PFX, so a reader that trusts your root can build the chain. Word-native seals are OOXML package signatures and carry the signer certificate only.
Word documents
For a .docx source, SignRequest.WordOutput chooses the output. It is ignored for PDF sources, which always produce a PDF.
| Output | What happens | Verifier support |
|---|---|---|
WordOutput.Pdf (default) | The document is converted to PDF, then signed and sealed | Broad: a signed PDF verifies everywhere |
WordOutput.Docx | The document stays Word: the signature image is placed in it, plus an OOXML package signature | Limited: not every viewer shows OOXML package signatures |
Keep PDF unless the recipient needs an editable Word file and accepts the limited verifier support.
Certificate setup
To seal with your own identity, give the plugin a PFX (PKCS#12) file that includes the private key. For the widget, set SignerWidgetOptions.SigningCertificate; for the C# facade, pass the certificate to SignAsync. Provide either the bytes or a path, plus the password:
builder.Services.AddDoconut(options =>
{
options.LicensePath = "Doconut.Viewer.lic";
options.AddPlugin<Doconut.Plugins.Signer.SignerPlugin>();
options.AddSignerWidget(widget =>
{
widget.SigningCertificate = new SigningCertificate
{
PfxBytes = File.ReadAllBytes("/secure/signing.pfx"),
Password = builder.Configuration["Signing:PfxPassword"],
};
});
});builder.Services.AddDoconut(options =>
{
options.LicensePath = "Doconut.Viewer.lic";
options.AddPlugin<Doconut.Plugins.Signer.SignerPlugin>();
options.AddSignerWidget(widget =>
{
widget.SigningCertificate = new SigningCertificate
{
PfxPath = "/secure/signing.pfx",
Password = builder.Configuration["Signing:PfxPassword"],
};
});
});The certificate is held in memory only. It is never logged, returned to a client, or persisted, and the password never appears in a log line or an error message.
Requirements:
- A PFX / PKCS#12 bundle with its private key. A public-key-only file (
.cer,.crt) cannot sign. Export the issuer chain with it, so PDF seals can carry it. - The correct password.
- Inside its validity window. An expired or not-yet-valid certificate does not stop the app: it is logged as a Warning at startup, and Certified signatures are refused until it is replaced. Visual signatures are unaffected.
- Issued for document signing: a key usage with digital signature or non-repudiation, and an extended key usage, if present, that allows document signing, e-mail protection or client authentication. A certificate issued for something else, such as a TLS server certificate, still signs, but it is logged as a Warning and PDF readers flag the signature.
- Hardware tokens, smart cards, HSMs and the Windows certificate store are not supported. Export the certificate, with its private key and chain, to a PFX.
Create a PFX from a certificate and its private key with OpenSSL:
openssl pkcs12 -export -out signing.pfx -inkey private.key -in certificate.crt
# The export password you are prompted for is SigningCertificate.Password.Export one from the Windows certificate store: open certmgr.msc and find the certificate (its icon shows a key when the private key is present). Right-click it, choose All Tasks → Export…, select Yes, export the private key, choose Personal Information Exchange – PKCS #12 (.PFX), and set a password.
Startup checks
When the widget is enabled, the certificate in SignerWidgetOptions.SigningCertificate is checked once, inside AddDoconut(), so a misconfigured PFX fails at startup rather than on the first signature. Three problems throw an InvalidOperationException:
| Message | Fix |
|---|---|
Doconut Signer: the certificate file was not found at '{path}'. | Correct PfxPath; make sure the file is deployed and readable by the app's account |
Doconut Signer: the signing certificate could not be opened — the password is incorrect or the file is not a valid .pfx. | Check the password; make sure the file is a real .pfx, not a .cer or .pem |
Doconut Signer: the signing certificate has no private key. Re-export the .pfx INCLUDING the private key. | Export the PFX again with the private key |
Otherwise the host writes one line to the log when it starts. Information when the certificate is ready:
Doconut Signer: using certificate '{subject}' (valid until yyyy-MM-dd). Signatures are digitally sealed.A Warning when it is expired, not yet valid, or not issued for document signing:
Doconut Signer: the signing certificate '{subject}' expired on yyyy-MM-dd. Certified signatures will be refused until it is replaced.
Doconut Signer: the signing certificate '{subject}' is not valid before yyyy-MM-dd. Certified signatures will be refused until then.
Doconut Signer: the signing certificate '{subject}' is not issued for document signing (its key usage excludes it). PDF readers will flag these signatures.And Information when no certificate is configured:
Doconut Signer: no signing certificate configured — output uses the built-in demo certificate and is watermarked. Set SignerWidgetOptions.SigningCertificate to sign with your own certificate.That last line is the same under every license. Under a paid license the demo certificate is never used: with no certificate, only Visual signatures work.
The C# facade is not checked at startup. The certificate you pass to SignAsync is read when you sign, so a wrong path or password fails that call.
Drop-in widget
Enable the endpoints
The widget's ?sign=open|run|download endpoints are off by default. Enable them next to the plugin registration:
builder.Services.AddDoconut(options =>
{
options.LicensePath = "Doconut.Viewer.lic";
options.AddPlugin<Doconut.Plugins.Signer.SignerPlugin>();
options.AddSignerWidget(widget =>
{
widget.MaxUploadMb = 25; // the default
widget.MaxSignatureImageKb = 512; // the default
// widget.SigningCertificate: see Certificate setup; leave it null for demo (watermarked) signing
});
});AddSignerWidget() with no argument enables the widget with the defaults. It does not grant signing by itself: it needs AddPlugin<SignerPlugin>(), or AddDoconut() throws:
AddSignerWidget() was called but the Signer plugin is not registered. Add options.AddPlugin<Doconut.Plugins.Signer.SignerPlugin>() (the widget needs it), or remove the AddSignerWidget() call.Without AddSignerWidget(), every ?sign= request answers 404. The script itself is still served, so the widget loads and paints its drop zone, then reports Signing is not enabled on this server. on its first request.
Embed it
UseDoconutResources() serves the script at {ResourcesPath}/js/doconutSigner.js (default /doconut-res/js/doconutSigner.js). It is a single, self-contained file with no dependencies:
<div id="host"></div>
<script src="/doconut-res/js/doconutSigner.js"></script>
<script>
const handle = Doconut.signer('#host', { basePath: '/doconut' });
</script>Doconut.signer(selectorOrElement, options) takes a CSS selector or an element, throws if the mount is not found, and returns a handle.
The signer uploads a PDF or .docx file, builds a signature (Draw on a canvas, Type a name in a signature font, or Image from a PNG, JPG or GIF), drags and resizes it onto the page, and signs and downloads the result. The widget always sends the signature as a PNG; an uploaded image is converted in the browser to a PNG that fits 520×160.
Options
| Option | Type | Default | Notes |
|---|---|---|---|
basePath | string | '/doconut' | Where the ?sign= endpoints and preview images are served |
resPath | string | '/doconut-res' | Reserved; the widget bundles its own CSS |
maxUploadMb | number | 25 | Client-side check only, so an oversized file fails fast. The server cap is MaxUploadMb in AddSignerWidget |
maxSignatureKb | number | 512 | Client-side cap for the Image mode. Keep it equal to the server's MaxSignatureImageKb |
previewWidth | number | 560 | Pixel width of the page preview |
licenseUrl | string | null | null | Turns the demo-watermark notice into a link to this URL |
signatureMode | string | 'certified' | Initial signature type: 'certified' or 'visual'. Any other value becomes 'certified' |
showSignatureModeToggle | boolean | true | Shows the Digital signature / Visual only choice. With false, the widget signs with signatureMode |
wordOutput | string | 'pdf' | Initial output for .docx sources: 'pdf' or 'docx'. Any other value becomes 'pdf' |
showOutputFormatToggle | boolean | true | Shows the PDF / Word choice, for .docx sources only. With false, the widget uses wordOutput |
labels | object | {} | Overrides any subset of the English strings |
Hiding a toggle does not disable the feature; it fixes the value. Two common configurations:
// Both toggles shown, digital signature by default: a full self-service signer.
Doconut.signer('#host', { basePath: '/doconut', licenseUrl: '/pricing' });// Visual only, no toggle: a simple stamp with no digital signature offered.
Doconut.signer('#host', {
basePath: '/doconut',
signatureMode: 'visual',
showSignatureModeToggle: false
});Callbacks
| Callback | Fires when | Payload |
|---|---|---|
onReady | Once, at mount (not again on reset()) | — |
onSourceLoaded | The file is opened and its preview shown | { file: { token, name, pages, ext } } |
onSigned | Signing succeeds | { downloadToken, downloadName, watermarked } |
onDownload | On each Download click | { downloadName, downloadToken } |
onError | An open, sign or download step fails, or an Image-mode file is too large | { message } |
A callback that throws is caught and logged; it never breaks the widget's flow.
Imperative handle
| Method | Behavior |
|---|---|
reset() | Returns to the drop screen and clears the current flow. Does not fire onReady again. No-op after destroy() |
loadFile(file) | Starts the flow with a File. No-op unless the widget is at its drop screen; call reset() first if needed |
destroy() | Removes the listeners, empties the mount, and freezes the instance |
const signerWidget = Doconut.signer('#host', {
basePath: '/doconut',
onSigned: (result) => console.log('signed', result.downloadName, result.watermarked),
onError: (error) => console.warn(error.message)
});
document.getElementById('start-over').addEventListener('click', () => signerWidget.reset());Labels
Every string has an English default; labels overrides any subset and the rest stay English. Fragments that change at runtime use {placeholders}, such as uploading: 'Uploading {name}…' and pages: '{n} pages'.
| Key | English default |
|---|---|
dropPrompt | Drag & drop a PDF or Word file here |
dropHint | PDF or Word (.docx) |
chooseFile | Choose file |
signatureHeading | Your signature |
modeDraw / modeType / modeImage | Draw / Type / Image |
metaSummary | Signature details (optional) |
metaSignerName / metaReason / metaLocation | Signer name / Reason / Location |
signatureType | Signature type |
modeCertified / modeVisual | Digital signature / Visual only |
modeVisualNote | No certificate — a visible mark only, not cryptographically verifiable. |
outputFormat | Output format |
outputPdf / outputWord | PDF / Word (.docx) |
signAction | Sign document |
download | Download |
demoNotice | Demo — signed with the Doconut demo certificate; output is watermarked. |
signingNotEnabled | Signing is not enabled on this server. |
page / pages | 1 page / {n} pages |
Further keys cover the placement step, the error screen, the linked form of the demo notice (demoNoticePrefix, demoNoticeLink, demoNoticeSuffix) and the aria-live announcements.
When open or run answers 404 with no JSON error, the widget shows signingNotEnabled. A JSON error from the server always wins, so messages such as an expired upload stay visible.
Theming
The widget adds the dsw class to its mount element and reads its colors, radius and fonts from CSS custom properties, so you can theme it without touching its internals:
#host.dsw {
--dsw-accent: #b0522b; /* links, focus, active mode */
--dsw-bg: #fffaf3;
--dsw-fg: #3b2a1a;
--dsw-muted: #8a7a68;
--dsw-border: #e0cbb0;
--dsw-radius: 14px;
--dsw-font: Georgia, "Times New Roman", serif;
--dsw-signature-font: "Segoe Script", "Brush Script MT", cursive; /* the Type mode */
}Defaults: --dsw-accent: #2b6cb0, --dsw-bg: #ffffff, --dsw-fg: #1a202c, --dsw-muted: #718096, --dsw-border: #cbd5e0, --dsw-radius: 8px, a system UI font, and a cursive signature font.
Build your own frontend
The widget is a client for this HTTP contract. All three routes sit under the path where UseDoconut() is mounted (normally /doconut), and they answer 404 until AddSignerWidget() is called. Errors carry a JSON body, { "error": "..." }, except where the table says "status only". An unknown ?sign= value answers 400.
| Route | Purpose | Success response |
|---|---|---|
POST ?sign=open (multipart, one file) | Upload a PDF or .docx and open it for preview | 200 — { token, name, pages, ext } |
POST ?sign=run (JSON body) | Sign the uploaded file | 200 — { downloadToken, downloadName, watermarked } |
GET ?sign=download&token=<downloadToken> | Stream the signed file | 200 — the file, Content-Disposition: attachment, Cache-Control: no-store |
ext in the open response includes the leading dot (".docx"). The page preview uses the standard viewer image route, {basePath}?token={token}&page={n}&thumb=1&width={px}; the page shown becomes placement.page.
The run body:
{
"token": "GUID-from-open",
"signature": { "kind": "Drawn", "payload": "<base64 PNG, no data: prefix>" },
"placement": { "page": 1, "x": 0.58, "y": 0.80, "w": 0.32, "h": 0.12 },
"meta": { "signerName": "Ada Lovelace", "reason": "I approve", "location": "London, UK" },
"wordOutput": "pdf",
"mode": "certified"
}| Field | Notes |
|---|---|
token | The token from open |
signature.kind | Drawn, Typed or Image, case-insensitive; anything else becomes Drawn. A hint only: the payload is always a PNG |
signature.payload | A bare base64 PNG, with no data: prefix. It must be a real PNG and no larger than MaxSignatureImageKb once decoded |
placement.page | 1-based |
placement.x / y / w / h | Normalized 0..1 from the top-left corner, with x + w ≤ 1 and y + h ≤ 1 |
meta.signerName / reason / location | Optional. Each is trimmed, a blank value is dropped, and each is cut at 256 characters |
wordOutput | pdf (default) or docx; ignored for PDF sources. Anything else becomes pdf |
mode | certified (default) or visual. Anything else becomes certified |
downloadName follows the output: <source>-signed.pdf or <source>-signed.docx, and download sends the matching Content-Type.
The uploaded source and the signed result are each kept for 30 minutes. After that, run and download answer 404 and the file must be opened again. All tokens are GUIDs.
| Route | Status | When | Body |
|---|---|---|---|
| any | 404 | The widget is not enabled | status only |
| any | 405 | Wrong verb (open and run need POST, download needs GET) | status only |
open | 400 | Not a multipart upload | Expected a multipart file upload. |
open | 400 | No file, or an empty one | No file uploaded. |
open | 400 | Anything other than .pdf or .docx | Signing supports PDF and Word (.docx) files only. |
open | 413 | The upload exceeds MaxUploadMb | File is too large. |
run | 400 | The body is not valid JSON, or is empty | Malformed request. / Empty request. |
run | 400 | The token is missing or not a GUID | Invalid token. |
run | 404 | The upload expired or was never opened | Upload expired — please re-open the file. |
run | 400 | No signature, or no placement | No signature supplied. / No placement supplied. |
run | 400 | The payload is not base64, is empty, too large, or not a PNG | Signature image is not valid base64. / Signature image is empty. / Signature image is too large. / The signature image must be a PNG. |
run | 400 | The placement is outside the page | The signature or its placement is invalid. |
run | 400 | Certified mode, a paid license and no certificate | No signing certificate configured. |
run | 400 | Certified mode with a certificate outside its validity window | The signing certificate expired on yyyy-MM-dd. / The signing certificate is not valid before yyyy-MM-dd. |
open, run | 500 | Processing failed | A sanitized message |
download | 400 | The token is missing or not a GUID | status only |
download | 404 | Unknown or expired token | status only |
A complete round trip with fetch:
// file: the File to sign; pngBase64: the signature as a bare base64 PNG, with no data: prefix,
// for example canvas.toDataURL('image/png').split(',')[1].
async function signFile(file, pngBase64) {
const base = '/doconut';
// 1. open: upload the file
const form = new FormData();
form.append('file', file, file.name);
const opened = await (await fetch(base + '?sign=open', { method: 'POST', body: form })).json();
// 2. run: sign it
const signed = await (await fetch(base + '?sign=run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: opened.token,
signature: { kind: 'Drawn', payload: pngBase64 },
placement: { page: 1, x: 0.58, y: 0.80, w: 0.32, h: 0.12 },
meta: { signerName: 'Ada Lovelace', reason: 'I approve', location: 'London, UK' },
wordOutput: 'pdf',
mode: 'certified'
})
})).json();
// 3. download: fetch the signed file
const blob = await (await fetch(base + '?sign=download&token=' + encodeURIComponent(signed.downloadToken))).blob();
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = signed.downloadName;
link.click();
URL.revokeObjectURL(link.href);
}The endpoints are same-origin: this version has no CORS support. Serve your UI and the widget script from the same origin as Doconut, or reverse-proxy both /doconut paths (the middleware path and the resources path) under your UI's origin.
In production
-
Install the plugin next to the core package, at the same version:
dotnet add package Doconut.NET8.Signer. -
Add the license file. Put
Doconut.Viewer.Signer.licin the same folder asDoconut.Viewer.lic. -
Get your certificate as a PFX with its private key and issuer chain, from a certificate authority or your organization's own PKI. To try things first, use a demo license (the built-in demo certificate, watermarked) or a self-signed PFX.
-
Read the certificate from configuration, so changing it never needs a code change:
json"DoconutSigner": { "PfxPath": "certs/your-certificate.pfx" }csharpvar signerSettings = builder.Configuration.GetSection("DoconutSigner"); builder.Services.AddDoconut(options => { options.LicensePath = "Doconut.Viewer.lic"; options.AddPlugin<Doconut.Plugins.Signer.SignerPlugin>(); options.AddSignerWidget(widget => widget.SigningCertificate = new SigningCertificate { PfxPath = Path.Combine(builder.Environment.ContentRootPath, signerSettings["PfxPath"]), Password = signerSettings["Password"] ?? "", }); });Keep the
.pfxoutsidewwwroot, and keep the password out ofappsettings.json: user secrets in development (dotnet user-secrets set "DoconutSigner:Password" "<password>"), an environment variable (DoconutSigner__Password) or a vault in production. -
Check the startup log. It should read
using certificate '{subject}' (valid until …). Signatures are digitally sealed.A Warning instead means the certificate is expired, not yet valid, or not issued for document signing.
Trust in PDF readers. A reader shows the signature as valid when it trusts the certificate's root. With your organization's PKI, that holds on every machine that trusts your root, which is usually every machine inside the organization. Certificates that Adobe trusts on any machine (the Adobe Approved Trust List) are, as far as we know, issued with the private key on a hardware token or HSM, which this plugin cannot use: it signs from a PFX only. Confirm with your certificate provider before relying on that kind of trust.
Web farms
The widget keeps each upload and its result in memory on the node that served open. Behind a load balancer without sticky sessions, a later run, download or preview request can reach another node and not find the upload. Register the shared, encrypted widget store once, after AddDoconut():
byte[] farmKey = Convert.FromBase64String(builder.Configuration["Doconut:FarmKey"]);
builder.Services.AddDoconutDistributedWidgets(store =>
{
store.RootPath = @"\\fileshare\doconut"; // a folder every node can read and write
store.FarmKey = farmKey; // at least 32 bytes, identical on every node
// store.Ttl = TimeSpan.FromMinutes(30); // how long an upload is kept (the default)
});- A single server needs nothing: without this call the in-memory store is used.
- Uploads and results are encrypted at rest on the share (AES-256, encrypt-then-MAC), and each file is bound to its token, so one token can never read another's file.
FarmKeymust be at least 32 bytes and the same on every node. Load it from a secret store; never hard-code it. Registration throws anArgumentExceptionwhenRootPathis missing orFarmKeyis shorter than 32 bytes.- Point
RootPathat the same shared location the distributed viewer uses, and give every node the same signing certificate.
API reference
The facade and the widget options are in the Doconut namespace; the request and result types are in Doconut.Core.Models.Signing. SignerPlugin is in Doconut.Plugins.Signer.
DocumentSigner
public sealed class DocumentSigner
{
public Task<SignResult> SignAsync(
Stream source, string sourceExtension, SignRequest request,
SigningCertificate? cert = null, CancellationToken ct = default);
}Resolved from dependency injection; the constructor is internal. sourceExtension is ".pdf" or ".docx". Under a paid license cert is used and the output is clean; under a temporary license the built-in demo certificate is used, cert is ignored, and the output is watermarked.
SignRequest
public sealed record SignRequest(
VisibleSignature Signature,
SignaturePlacement Placement,
string? SignerName = null,
string? Reason = null,
string? Location = null,
WordOutput WordOutput = WordOutput.Pdf,
SignatureMode Mode = SignatureMode.Certified)
{
public bool IsValid { get; } // Signature.IsValid && Placement.IsValid
}One visible signature placed once, plus optional signer metadata.
VisibleSignature and SignatureKind
public sealed record VisibleSignature(SignatureKind Kind, byte[] PngBytes)
{
public bool IsValid { get; } // PngBytes is not empty
}
public enum SignatureKind { Drawn, Typed, Image }PngBytes is a PNG for every kind. A drawn or typed signature is transparent; an uploaded image keeps the transparency it had, so an opaque JPG stays opaque. Kind is a hint only.
SignaturePlacement
public readonly record struct SignaturePlacement(int Page, double X, double Y, double W, double H)
{
public bool IsValid { get; } // Page >= 1; X, Y >= 0; W, H > 0; X + W <= 1; Y + H <= 1
}Page is 1-based; X, Y, W and H are normalized 0..1 from the top-left corner.
SignatureMode and WordOutput
public enum SignatureMode { Certified, Visual } // Certified is the default
public enum WordOutput { Pdf, Docx } // Pdf is the default; ignored for PDF sourcesSignResult
public sealed record SignResult(Stream Stream, string Extension);The signed document as a seekable stream, and its extension with the dot (".pdf" or ".docx").
SigningCertificate
public sealed class SigningCertificate
{
public byte[]? PfxBytes { get; init; }
public string? PfxPath { get; init; }
public string Password { get; init; } = "";
public bool HasMaterial { get; } // PfxBytes is not empty, or PfxPath is not blank
}Set either PfxBytes or PfxPath, plus Password.
SignerWidgetOptions
public sealed class SignerWidgetOptions
{
public int MaxUploadMb { get; set; } = 25;
public int MaxSignatureImageKb { get; set; } = 512;
public SigningCertificate? SigningCertificate { get; set; }
}Set through DoconutOptions.AddSignerWidget(Action<SignerWidgetOptions>? configure = null), which returns the DoconutOptions so calls can be chained. A null SigningCertificate means the demo certificate under a temporary license, and Visual signatures only under a paid one.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
AddDoconut() throws The Signer plugin is registered via AddPlugin but no active license grants Signer. | No license, a TRIAL file, a license without Signer, or a paid license outside its version window | Install a license that grants Signer, or remove AddPlugin<SignerPlugin>() |
AddDoconut() throws AddSignerWidget() was called but the Signer plugin is not registered. | The widget is enabled without its plugin | Add options.AddPlugin<Doconut.Plugins.Signer.SignerPlugin>() |
AddDoconut() throws Doconut Signer: the certificate file was not found at '{path}'. | PfxPath points at a missing file | Fix the path; deploy the file where the app's account can read it |
AddDoconut() throws Doconut Signer: the signing certificate could not be opened — the password is incorrect or the file is not a valid .pfx. | Wrong password, or not a PKCS#12 file | Check the password; use a real .pfx |
AddDoconut() throws Doconut Signer: the signing certificate has no private key. Re-export the .pfx INCLUDING the private key. | The PFX was exported without its private key | Export it again with the private key |
Resolving DocumentSigner throws No IDocumentSigner is registered. | SignerPlugin is not registered | Add AddPlugin<SignerPlugin>() inside AddDoconut() |
| The output is watermarked | The license is temporary (Demo/NFR) | Use a paid license that grants Signer |
run answers 400 No signing certificate configured. | A paid license, Certified mode, and no certificate | Configure SigningCertificate, or sign in Visual mode |
run answers 400 The signing certificate expired on yyyy-MM-dd. | The certificate is out of date | Replace it; Visual signatures keep working meanwhile |
| The log says the output "uses the built-in demo certificate", yet a paid license refuses Certified signatures | That line is written whenever no certificate is configured, whatever the license | Configure SigningCertificate |
The widget loads but says Signing is not enabled on this server. | AddSignerWidget() was not called, or basePath does not match where UseDoconut() is mounted | Call AddSignerWidget(); fix basePath |
run answers 400 The signature image must be a PNG., Signature image is not valid base64. or Signature image is too large. | A custom UI sent a data: URL, a JPG, or more than MaxSignatureImageKb | Send a bare base64 PNG within the cap |
open answers 413 File is too large. | The upload exceeds MaxUploadMb | Raise MaxUploadMb in AddSignerWidget; the widget's maxUploadMb is only a client-side check |
run or download answers 404 Upload expired — please re-open the file. | The 30-minute window elapsed, or a web-farm node does not have the upload | Open the file again; in a farm, register AddDoconutDistributedWidgets |
A signed .docx shows no signature in some viewers | Word output carries an OOXML package signature, which many viewers do not show | Sign to PDF, the default |
| PDF readers say the signer's identity is unknown | The reader does not trust the certificate's root (the demo certificate is never trusted) | Use a certificate from a root your readers trust |
| The widget fails when the UI is on another origin | The endpoints have no CORS support | Serve the UI from the same origin, or reverse-proxy the Doconut paths |
| The theme does not apply | The selector targets an element inside the mount | Target the mount itself: #host.dsw or #host |
War diese Seite hilfreich?