WebeSign

For developers

Let people sign on your website with the certificate they already have. You build the document and its hash; WebeSign returns the signature and the certificate.

Quickstart: your website

Install the package. It has no dependencies, is fully typed and adds under 5 KB to your page.

bun add @websign/sdk     # or: npm install @websign/sdk

Then sign. prepare receives the certificate the person chose and returns the hash to sign:

import { sign } from "@websign/sdk";

const { signature, certificate } = await sign({
  hash: "SHA-256",
  prepare: (certificate, { hash }) => crypto.subtle.digest(hash, signedAttributesFor(certificate)),
});

result.signature is the raw signature (ECDSA r‖s, or the RSA block). Put it in your CMS or PDF together with result.certificate.der and result.certificate.chain.

How prepare works

  1. Your page calls sign(). The WebeSign window opens and the person chooses a certificate.
  2. prepare(certificate) runs in your page. PAdES and CAdES put the certificate inside the signed data, so this is where you build the signed attributes and return their hash (32, 48 or 64 bytes).
  3. The person compares the verification code and confirms, typing the PIN in the app if needed. The document never leaves your page.
  4. sign() resolves with the signature. If the person switches certificate, prepare runs again and only the last hash is signed.

The types follow your options: with hash: "SHA-384" and algorithm: "ECDSA", prepare's context and the result are typed with exactly those values.

Show the Sign button only when WebeSign is ready

status() never opens a window and never fails. problem says what is missing; onChange tells you when the person installs or updates something while your page is open.

import { installUrl, onChange, status } from "@websign/sdk";
import { errorText } from "@websign/sdk/messages";

function show({ ready, problem }) {
  signButton.hidden = !ready;
  installLink.hidden = problem !== "ExtensionMissing";
  installLink.href = installUrl();             // the extension store of this browser
  notice.textContent = errorText(problem)?.body ?? "";
}
show(await status());
onChange(show);

Errors

Every failure is a WebSignError with a stable code, a message and a hint for you, and a docsUrl that points to this table. For the person, errorText(error) from @websign/sdk/messages gives a title and the next step in 7 languages.

import { isWebSignError, sign } from "@websign/sdk";
import { errorText } from "@websign/sdk/messages";

try {
  await sign({ hash: "SHA-256", prepare });
} catch (error) {
  if (!isWebSignError(error)) throw error;
  if (error.code === "UserCancelled") return;          // nothing to show
  console.warn(error.code, error.hint, error.docsUrl);  // for you
  const text = errorText(error);                        // for the person, localized
  if (text) showBanner(text.title, text.body);
}
CodeWhat happened and what to do
ExtensionMissingThe WebeSign extension did not answer: it is not installed, or it is disabled for this site. Show a link to installUrl().
AppMissingThe extension is there but the WebeSign app is not. The person installs the app; the extension guides them.
AppOutdatedThe app is older than the extension needs (details has both versions). The person updates the app.
ExtensionOutdatedThe extension is older than the app needs. The person restarts the browser so it updates.
ClientOutdatedThe extension only speaks a newer protocol than your copy of @websign/sdk. Update the package in your site.
InsecureOriginThe page is not served over https. Serve it over https (http://localhost also works).
AbortedYour AbortSignal fired, or prepare threw (your error is in cause). Nothing to show the person.
UserCancelledThe person closed the window. Leave the page as it was; no message is needed.
TimeoutNobody confirmed in time, or prepare took more than 60 seconds. Offer to try again.
NoCertificatesNo usable certificate was found. The person plugs in the token or card and tries again.
CertificateUnavailableThe chosen certificate is gone (the token was removed). The person plugs it back in or chooses another one.
CertificateNotValidThe certificate has expired or is not valid yet. The person chooses another one.
InvalidRequestA bug in the call: a wrong option, or a hash of the wrong length. The message says which.
UnsupportedAlgorithmThe key cannot produce the algorithm you asked for. Accept more algorithms, or ask for another certificate.
PinIncorrectA wrong PIN. The app asks again, so a website rarely sees this.
PinLockedThe PIN is blocked. The person unlocks it with the PUK, using the issuer's tools.
TokenRemovedThe token was removed while signing. The person plugs it back in and tries again.
DriverFailureThe token driver failed; details.native has its status. Try again; if it keeps failing, the person reconnects the token.
BusyOther requests are waiting in the WebeSign window. Try again once they are done.
InternalA bug in WebeSign. Please report it with the diagnostics from the app.

Test without the extension

@websign/sdk/testing installs a fake extension and app on window. Your code runs unchanged: status(), certificates() and sign() work, prepare runs, and the signatures are real ECDSA or RSA made with public test keys, so you can verify them. Script every outcome: the person cancels, no certificates, app missing, outdated, driver errors.

import { installFakeWebSign, type FakeWebSign } from "@websign/sdk/testing";

let fake: FakeWebSign;
beforeEach(async () => (fake = await installFakeWebSign()));   // in jsdom or happy-dom
afterEach(() => fake.uninstall());

it("signs the contract", async () => {
  const result = await signContract();                         // your code, calling sign()
  expect(await fake.verify(result, signedAttributes)).toBe(true);
});

it("stays calm when the person cancels", async () => {
  fake.failNext("UserCancelled");
  await expect(signContract()).rejects.toMatchObject({ code: "UserCancelled" });
});

// Other setups: installFakeWebSign({ scenario: "app-missing" | "app-outdated" | "extension-missing" | … }),
// installFakeWebSign({ certificates: [] }), fake.switchDuringNextSign(1), fake.requests.

The same fake lets you build your page before installing anything. Load it only in development:

if (import.meta.env.DEV) {
  const { installFakeWebSign } = await import("@websign/sdk/testing");
  await installFakeWebSign();
}

Never ship the fake. Anyone can sign with its keys. It refuses to run outside localhost-like addresses and warns on the console when installed.

React and Vue

The SDK has no framework code. The examples include a copy-paste useWebSign() hook for React and the same as a Vue composable: live status, progress, error, and cancelling when the component goes away.

const { status, signing, error, sign } = useWebSign();

<button disabled={!status?.ready || signing} onClick={() => sign({ hash: "SHA-256", prepare })}>
  Sign
</button>

Signing PDFs (PAdES)

Your PDF library reserves room for the signature and hashes the rest of the file. In prepare you build the signed attributes for the chosen certificate and return their hash; the signature you get back goes into the CMS inside the PDF. The PAdES example does this with pdf-lib and PKI.js, and its test checks the result with OpenSSL.

See the examplesTest your setup

API reference

Every function and type of @websign/sdk, @websign/sdk/messages and @websign/sdk/testing, with examples, generated from the source.

Open the API reference

Desktop programs

Desktop program (Node, Bun, Electron): @websign/desktop

Connect to the app, then call sign(). The app names your program in its confirmation window.

import { createHash } from "node:crypto";
import { WebSign } from "@websign/desktop";

const websign = await WebSign.connect(); // AppMissing if not installed
try {
  const { signature, certificate } = await websign.sign({
    hash: "SHA-256",
    prepare: () => createHash("sha256").update("my document").digest(),
  });
  console.log(`${certificate.displayName}: ${signature.byteLength} bytes`);
} finally {
  await websign.close();
}

Desktop program (Rust): websign-client

Client::connect(), then certificates() or sign(). The app is started on demand.

API guides and contracts in the repository: docs/architecture/web-api.md and docs/architecture/desktop-api.md