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.

SurfaceUse 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:

bash
dotnet add package Doconut.NET8.Signer

To pin the plugin to the release this documentation describes, pass the version separately:

bash
dotnet add package Doconut.NET8.Signer --version 26.10.0

Keep 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:

bash
apt-get update && apt-get install -y libfontconfig1

On 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():

csharp
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:

text
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 stateStartupCertified signatureVisual signature
Paid license granting Signer, certificate configuredStartsSealed with your certificate, clean (watermarked: false)Clean
Paid license granting Signer, no certificateStartsRefused: No signing certificate configured.Clean
Paid license, certificate expired or not yet validStarts (with the widget enabled, a Warning is logged)Refused: The signing certificate expired on yyyy-MM-dd.Clean
Temporary Demo/NFR license, active or expiredStartsSealed 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 windowAddDoconut() 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>().

csharp
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:

csharp
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:

csharp
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:

ExceptionMessageWhen
InvalidOperationExceptionNo signing certificate configured.Certified mode, a paid license, and no certificate passed
InvalidOperationExceptionThe 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
ArgumentExceptionThe 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
NotSupportedExceptionThe 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.

ModeWhat it producesCertificateVerifiable
SignatureMode.Certified (default)The visible signature plus a digital sealRequired under a paid licenseYes
SignatureMode.VisualThe visible signature onlyNever neededNo: 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.

OutputWhat happensVerifier support
WordOutput.Pdf (default)The document is converted to PDF, then signed and sealedBroad: a signed PDF verifies everywhere
WordOutput.DocxThe document stays Word: the signature image is placed in it, plus an OOXML package signatureLimited: 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:

csharp
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"],
        };
    });
});
csharp
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:

bash
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:

MessageFix
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:

text
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:

text
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:

text
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:

csharp
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:

text
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:

html
<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

OptionTypeDefaultNotes
basePathstring'/doconut'Where the ?sign= endpoints and preview images are served
resPathstring'/doconut-res'Reserved; the widget bundles its own CSS
maxUploadMbnumber25Client-side check only, so an oversized file fails fast. The server cap is MaxUploadMb in AddSignerWidget
maxSignatureKbnumber512Client-side cap for the Image mode. Keep it equal to the server's MaxSignatureImageKb
previewWidthnumber560Pixel width of the page preview
licenseUrlstring | nullnullTurns the demo-watermark notice into a link to this URL
signatureModestring'certified'Initial signature type: 'certified' or 'visual'. Any other value becomes 'certified'
showSignatureModeTogglebooleantrueShows the Digital signature / Visual only choice. With false, the widget signs with signatureMode
wordOutputstring'pdf'Initial output for .docx sources: 'pdf' or 'docx'. Any other value becomes 'pdf'
showOutputFormatTogglebooleantrueShows the PDF / Word choice, for .docx sources only. With false, the widget uses wordOutput
labelsobject{}Overrides any subset of the English strings

Hiding a toggle does not disable the feature; it fixes the value. Two common configurations:

javascript
// Both toggles shown, digital signature by default: a full self-service signer.
Doconut.signer('#host', { basePath: '/doconut', licenseUrl: '/pricing' });
javascript
// Visual only, no toggle: a simple stamp with no digital signature offered.
Doconut.signer('#host', {
  basePath: '/doconut',
  signatureMode: 'visual',
  showSignatureModeToggle: false
});

Callbacks

CallbackFires whenPayload
onReadyOnce, at mount (not again on reset())—
onSourceLoadedThe file is opened and its preview shown{ file: { token, name, pages, ext } }
onSignedSigning succeeds{ downloadToken, downloadName, watermarked }
onDownloadOn each Download click{ downloadName, downloadToken }
onErrorAn 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

MethodBehavior
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
javascript
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'.

KeyEnglish default
dropPromptDrag & drop a PDF or Word file here
dropHintPDF or Word (.docx)
chooseFileChoose file
signatureHeadingYour signature
modeDraw / modeType / modeImageDraw / Type / Image
metaSummarySignature details (optional)
metaSignerName / metaReason / metaLocationSigner name / Reason / Location
signatureTypeSignature type
modeCertified / modeVisualDigital signature / Visual only
modeVisualNoteNo certificate — a visible mark only, not cryptographically verifiable.
outputFormatOutput format
outputPdf / outputWordPDF / Word (.docx)
signActionSign document
downloadDownload
demoNoticeDemo — signed with the Doconut demo certificate; output is watermarked.
signingNotEnabledSigning is not enabled on this server.
page / pages1 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:

css
#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.

RoutePurposeSuccess response
POST ?sign=open (multipart, one file)Upload a PDF or .docx and open it for preview200 — { token, name, pages, ext }
POST ?sign=run (JSON body)Sign the uploaded file200 — { downloadToken, downloadName, watermarked }
GET ?sign=download&token=<downloadToken>Stream the signed file200 — 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:

json
{
  "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"
}
FieldNotes
tokenThe token from open
signature.kindDrawn, Typed or Image, case-insensitive; anything else becomes Drawn. A hint only: the payload is always a PNG
signature.payloadA bare base64 PNG, with no data: prefix. It must be a real PNG and no larger than MaxSignatureImageKb once decoded
placement.page1-based
placement.x / y / w / hNormalized 0..1 from the top-left corner, with x + w ≤ 1 and y + h ≤ 1
meta.signerName / reason / locationOptional. Each is trimmed, a blank value is dropped, and each is cut at 256 characters
wordOutputpdf (default) or docx; ignored for PDF sources. Anything else becomes pdf
modecertified (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.

RouteStatusWhenBody
any404The widget is not enabledstatus only
any405Wrong verb (open and run need POST, download needs GET)status only
open400Not a multipart uploadExpected a multipart file upload.
open400No file, or an empty oneNo file uploaded.
open400Anything other than .pdf or .docxSigning supports PDF and Word (.docx) files only.
open413The upload exceeds MaxUploadMbFile is too large.
run400The body is not valid JSON, or is emptyMalformed request. / Empty request.
run400The token is missing or not a GUIDInvalid token.
run404The upload expired or was never openedUpload expired — please re-open the file.
run400No signature, or no placementNo signature supplied. / No placement supplied.
run400The payload is not base64, is empty, too large, or not a PNGSignature image is not valid base64. / Signature image is empty. / Signature image is too large. / The signature image must be a PNG.
run400The placement is outside the pageThe signature or its placement is invalid.
run400Certified mode, a paid license and no certificateNo signing certificate configured.
run400Certified mode with a certificate outside its validity windowThe signing certificate expired on yyyy-MM-dd. / The signing certificate is not valid before yyyy-MM-dd.
open, run500Processing failedA sanitized message
download400The token is missing or not a GUIDstatus only
download404Unknown or expired tokenstatus only

A complete round trip with fetch:

javascript
// 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

  1. Install the plugin next to the core package, at the same version: dotnet add package Doconut.NET8.Signer.

  2. Add the license file. Put Doconut.Viewer.Signer.lic in the same folder as Doconut.Viewer.lic.

  3. 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.

  4. Read the certificate from configuration, so changing it never needs a code change:

    json
    "DoconutSigner": { "PfxPath": "certs/your-certificate.pfx" }
    csharp
    var 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 .pfx outside wwwroot, and keep the password out of appsettings.json: user secrets in development (dotnet user-secrets set "DoconutSigner:Password" "<password>"), an environment variable (DoconutSigner__Password) or a vault in production.

  5. 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():

csharp
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.
  • FarmKey must be at least 32 bytes and the same on every node. Load it from a secret store; never hard-code it. Registration throws an ArgumentException when RootPath is missing or FarmKey is shorter than 32 bytes.
  • Point RootPath at 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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
public enum SignatureMode { Certified, Visual }   // Certified is the default
public enum WordOutput { Pdf, Docx }              // Pdf is the default; ignored for PDF sources

SignResult

csharp
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

csharp
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

csharp
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

SymptomCauseFix
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 windowInstall 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 pluginAdd options.AddPlugin<Doconut.Plugins.Signer.SignerPlugin>()
AddDoconut() throws Doconut Signer: the certificate file was not found at '{path}'.PfxPath points at a missing fileFix 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 fileCheck 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 keyExport it again with the private key
Resolving DocumentSigner throws No IDocumentSigner is registered.SignerPlugin is not registeredAdd AddPlugin<SignerPlugin>() inside AddDoconut()
The output is watermarkedThe 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 certificateConfigure SigningCertificate, or sign in Visual mode
run answers 400 The signing certificate expired on yyyy-MM-dd.The certificate is out of dateReplace it; Visual signatures keep working meanwhile
The log says the output "uses the built-in demo certificate", yet a paid license refuses Certified signaturesThat line is written whenever no certificate is configured, whatever the licenseConfigure 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 mountedCall 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 MaxSignatureImageKbSend a bare base64 PNG within the cap
open answers 413 File is too large.The upload exceeds MaxUploadMbRaise 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 uploadOpen the file again; in a farm, register AddDoconutDistributedWidgets
A signed .docx shows no signature in some viewersWord output carries an OOXML package signature, which many viewers do not showSign to PDF, the default
PDF readers say the signer's identity is unknownThe 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 originThe endpoints have no CORS supportServe the UI from the same origin, or reverse-proxy the Doconut paths
The theme does not applyThe selector targets an element inside the mountTarget the mount itself: #host.dsw or #host

此页面对您有帮助吗?