Signing Workers

Build and test a signing worker

The parts every signing worker shares, how to test one against the sandbox worker, and the checklist before you go live

Private beta. Qualified signatures are available on request. Request access and we enable them for your account. SWP/1 is in beta and may change.

Build a signing worker when nobody runs one for your provider yet. If your provider already runs one, skip to Connect a signing worker.

Before you start

  • Qualified signatures enabled on your SignatureAPI account. Test mode works right away with the sandbox worker.
  • A provider account with API access, ideally with a test environment.
  • A public HTTPS host on port 443, with a certificate from a public certificate authority.
  • A store for idempotency outcomes and seen token IDs, shared by all instances.

1. Choose your model

Answer three questions about your provider: does the signer already have a certificate, when does the provider identify them, and does its signing call return a bare value or a finished CMS? Choose your provider’s model turns the answers into a signature format and the calls your worker pauses.

Then follow the closest recipe for the provider calls behind each operation:

RecipeProviderFormatPauses
CSC remote certificateLong-term certificate behind the CSC API, approval with an OTPrawauthorize, redirect
One-shot certificateIdentifies, issues a certificate for one signature, returns a CMScmsauthorize, redirect
App approvalLong-term certificate, approval in the provider’s appraw or cmsauthorize, poll

2. Build the endpoints

1

Serve the health check

Answer GET /v1/health without authentication, and list the connection tokens you accept:

app.get("/v1/health", async (req, res) => {
    const providerUp = await provider.ping();
    res.status(200).json({
        status: providerUp ? "ok" : "degraded",
        connection_tokens: [...ALLOWED_CONNECTION_TOKENS],
    });
});
2

Authenticate every POST

Verify the token, body hash and connection token first, then refuse a missing Idempotency-Key with 400. See Verify a request for the order and a Node.js example.

3

Make every operation idempotent

Wrap each operation in one handler keyed by the Idempotency-Key:

async function withIdempotency(key, bodySha256, run) {
    const stored = await store.get(`idem:${key}`);
    if (stored) {
        return stored.bodySha256 === bodySha256
            ? stored.outcome
            : conflict(`Idempotency-Key ${key} was used with a different payload`);
    }
    if (!(await store.reserve(`lock:${key}`))) {
        return conflict(`Idempotency-Key ${key} is currently executing`);
    }
    try {
        const outcome = await run();
        const final = outcome.status === "completed"
            || (outcome.status === "failed" && outcome.retryable !== true);
        if (final) await store.set(`idem:${key}`, { bodySha256, outcome });
        return outcome;
    } finally {
        await store.release(`lock:${key}`); // Only after the outcome is stored
    }
}

conflict(message) returns { refusal: { error: "idempotency_conflict", error_description: message } }, which the handler in the next step answers with 409. The rules are in Idempotency.

4

Implement credential, authorize and sign

Follow your recipe and each endpoint page. Every worker has the same skeleton:

const operations = { credential, authorize, sign };

app.post("/v1/:operation", express.raw({ type: "application/json" }), async (req, res) => {
    const run = operations[req.params.operation];
    if (!run) return res.status(404).json({ error: "invalid_request", error_description: "unknown operation" });

    const auth = await authenticate(req.headers, req.body, req.params.operation);
    if (auth.error) return res.status(auth.status).json({ error: auth.error, error_description: auth.error_description });

    const outcome = await withIdempotency(auth.idempotencyKey, auth.bodySha256, () => run(auth.body));
    if (outcome.refusal) return res.status(409).json(outcome.refusal);
    res.status(200).json(outcome); // completed, pending or failed
});

Two details trip up most first versions of a raw sign:

  • Don't hash again. The digest is already a hash.
  • Return DER for ECDSA, not the compact r‖s form many APIs return.

For cms, return the provider's CMS and the certificate chain, signer first. Leave out CRLs, OCSP responses and timestamps: SignatureAPI adds its own.

3. Configure connection tokens

Your worker accepts only the accounts whose connection tokens you list. For each account you serve, including your own, open Connect Test Worker or Connect Live Worker (or a connected worker’s card), copy the Connection token, and add it to your worker’s configuration.

4. Test in test mode

1

Try the sandbox worker first

Before you connect anything, send a test envelope with a qualified signer, as in the Quickstart. The sandbox worker at https://sandbox-signer.signatureapi.com signs it, and it's a working SWP/1 worker you can watch from the outside:

  • credential pauses with an action_url to its Identify yourself page. On the resumed call, it issues a short-lived ECDSA P-256 certificate and returns it, in raw.
  • authorize pauses with an action_url to its Authorize your signature page, which lists the digest's label.
  • sign returns a DER-encoded ECDSA signature.

It pauses twice on purpose, to show both ceremony screens. Your worker pauses only where your provider needs the signer. See Choose your provider's model.

The sandbox worker's Identify yourself page with Jane Doe's name and email

2

Connect your worker for test mode

Connect your worker with Connect Test Worker. Fix any failing connection check.

3

Sign a test envelope

Send the same test envelope. Your worker receives credential, authorize and sign. Check the deliverable's signature in a PDF signature validator.

Also test the paths that are easy to miss:

TestExpected
The signer refuses at the providerYour worker answers failed with access_denied, or redirects to callback_url with error=access_denied. The ceremony shows your error_description and Try again.
The signer walks awayAfter the authorization timeout, the session fails with authorization window expired.
The browser comes back twiceThe second callback has no effect, because your code is single-use.
A poll arrives while the signer decidesYour worker answers from the provider’s current state and doesn’t start the act again.
A repeated signIn your own tests, the second call with the same Idempotency-Key returns the stored signatures without calling your provider.
A token for another worker401 with invalid_token. The connection checks test this for you.
cms only: the largest real signatureIt fits your max_signature_size.

Disconnect your test worker to go back to the sandbox worker for comparison.

5. Go live

When every test passes:

  1. Switch your worker to your provider’s production environment, with qualified certificates.
  2. Add the account’s live connection token to your worker.
  3. Connect the worker with Connect Live Worker.
  4. Send a live envelope and check the signature in a validator. The signer should now show as trusted.

See Go live for the account requirements.

Go-live checklist

Check every line before you connect for live mode.

  • Serve POST /v1/credential, /v1/authorize, /v1/sign and GET /v1/health on a public HTTPS URL, port 443, without redirects.
  • List the connection tokens you accept in /v1/health, and refuse requests carrying any other.
  • Verify ES256, iss, aud, exp, swp.operation, swp.session and swp.body_sha256. Remember each jti until exp plus your clock tolerance.
  • Refuse a request without an Idempotency-Key with 400, after verifying its token.
  • Answer every operation with 200 and completed, pending or failed.
  • Return the full chain, signer certificate first, with honest key_algo_oids and expires_at. For cms, return the chain in sign, and in credential too if you have it.
  • Declare "signature_format": "cms" if your provider returns a complete CMS, with a max_signature_size that covers its largest output.
  • In key_algo_oids, list a SHA-256 algorithm before any other full signature algorithm.
  • Pause credential or authorize only where your provider needs the signer, and set expires_at on a pending answer when the provider’s request expires sooner than the connection’s timeout.
  • Answer each poll from the provider’s current state. Never send a second push for one session.
  • Show each digest’s label on any approval screen you host.
  • End a redirect step at callback_url, with a single-use code. Never answer a request carrying callback.code with pending.
  • Follow the idempotency rules. Never sign twice for one key.
  • Sign the right input and return the right encoding. For cms, meet the CMS requirements.
  • Use only the listed error codes, or x_-prefixed ones.
  • Write error_description for the signer. The ceremony shows it when the session fails.
  • Never log request bodies.