SignLocal

SignLocal setup guide

This guide is for the person who makes SignLocal available to others: IT, a SharePoint administrator, or a team lead. People who sign documents only need the user guide.

  1. Choose a way to provide SignLocal
  2. Option A: Hosted website (no setup)
  3. Option B: Single file
  4. Option C: SharePoint Online and Microsoft Teams
  5. Option D: Host it on your own web server
  6. Signing identities and trust

  7. Security and data handling (for IT review)
  8. Developer setup: build, test and deploy
  9. Updating SignLocal
  10. Troubleshooting

1. Choose a way to provide SignLocal

All four options run the same app with the same features. Pick what suits your users.

OptionBest forNeedsWorks offline
A. Hosted website: https://signlocal.pages.devAnyone, any device, phonesA browserYes, after the first visit (or when installed as an app)
B. Single file: SignLocal.htmlLocked-down PCs, air-gapped networks, USB, emailA browserAlways
C. SharePoint / Teams web part: signlocal.sppkgMicrosoft 365 organisationsSharePoint admin rights, onceNeeds SharePoint to load the page
D. Your own web serverYour own domain or intranetA static web serverYes, after the first visit

Browser requirements (all options): a current version of Microsoft Edge, Google Chrome, Mozilla Firefox, or Safari (macOS, iOS and iPadOS 16.4 or later). Internet Explorer is not supported.

Downloads

FileLinkIn the repository
Single-file apphttps://signlocal.pages.dev/download/SignLocal.htmldist/SignLocal.html
SharePoint packagehttps://signlocal.pages.dev/download/signlocal.sppkgdist/signlocal.sppkg

Option A: Hosted website (no setup)

Send users the address https://signlocal.pages.dev.

After the first visit the app is cached on the device and works offline.

Documents are never sent to the website. The site serves only the program files, and its security policy (see section 7) prevents the page from contacting any server.


Option B: Single file

  1. Download SignLocal.html (about 2.4 MB) from the link above or from dist/ in the repository.
  2. Give it to users however suits you: a network share, intranet download, email attachment, Teams files, a USB stick, or a desktop shortcut deployed by Intune or Group Policy.
  3. Users double-click it. It opens in their default browser.

Notes:


Option C: SharePoint Online and Microsoft Teams

SignLocal is packaged as a SharePoint Framework (SPFx 1.24) web part:

Why not upload the HTML file to a document library? SharePoint Online opens .html files as downloads rather than running them, and modern sites block .aspx uploads ("NoScript"). An SPFx package is the supported way to run custom code in SharePoint Online.

C1. Prerequisites

C2. Upload and enable the app (once per tenant)

  1. Download signlocal.sppkg.
  2. Open the SharePoint admin center (https://<your-tenant>-admin.sharepoint.com).
  3. Go to More features → Apps → Open. The Manage apps page opens.
  4. Choose Upload and select signlocal.sppkg.
  5. In the Enable app panel:

    • Tick Enable this app and add it to all sites to make it available everywhere. Otherwise, only enable it, and site owners add it themselves (step C4).
    • Choose Enable.
  6. (Optional) The panel offers Add to Teams. Choose it to make SignLocal available as a Teams app (see C5).

The app appears in the list as SignLocal, version 1.1.0.0.

C3. Add SignLocal to a page

  1. Go to the SharePoint site, open the page (or + New → Page), and choose Edit.
  2. Hover where you want it and choose + (Add a new web part).
  3. Search for SignLocal (it's listed under Advanced) and select it.
  4. Choose Publish (or Republish).

Recommendations:

Anyone who can view the page can use it. Editing the page requires the usual page-edit permissions.

C4. If you didn't add it to all sites

Site owners add it once per site: Settings (⚙) → Add an app → SignLocal. Then follow C3.

C5. Microsoft Teams

If you chose Add to Teams in C2, or later selected the app in the app catalog and chose Sync to Teams:

C6. Remove or roll back

C7. Content Security Policy notes

SignLocal's code is part of the package and runs from SharePoint's own storage, which SharePoint's CSP trusts by default, so no "Trusted script sources" entry is needed. To test before CSP enforcement reaches a page, add ?csp=enforce to the page URL and confirm SignLocal works (browser developer tools should show no CSP errors).


Option D: Host it on your own web server

SignLocal is a static website: plain files, no server code, no database.

  1. Build the site folder (see section 8): npm run build:site. This writes site/:

    site/
      index.html, sw.js, manifest.webmanifest, icon.svg
      css/  js/  vendor/
      download/SignLocal.html, download/signlocal.sppkg
      _headers            (security headers, used by Cloudflare)
  2. Copy the contents of site/ to your web server or static host (IIS, nginx, Apache, Azure Static Web Apps, Amazon S3 + CloudFront, Netlify…).
  3. Serve it over HTTPS. Offline use and installing as an app need HTTPS (or localhost).
  4. Add the security headers that site/_headers lists. Examples follow.

nginx

location / {
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' data: blob:; worker-src 'self' blob:; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self' https://*.sharepoint.com https://teams.microsoft.com" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
}
location /download/ {
    add_header Content-Disposition "attachment" always;
}
types { application/manifest+json webmanifest; }

IIS (web.config in the site folder)

<configuration>
  <system.webServer>
    <staticContent>
      <remove fileExtension=".webmanifest" />
      <mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" />
      <remove fileExtension=".sppkg" />
      <mimeMap fileExtension=".sppkg" mimeType="application/octet-stream" />
    </staticContent>
    <httpProtocol>
      <customHeaders>
        <add name="Content-Security-Policy" value="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' data: blob:; worker-src 'self' blob:; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self' https://*.sharepoint.com https://teams.microsoft.com" />
        <add name="X-Content-Type-Options" value="nosniff" />
        <add name="Referrer-Policy" value="no-referrer" />
        <add name="Permissions-Policy" value="camera=(), microphone=(), geolocation=(), payment=(), usb=()" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

Your own Cloudflare account: see 8.4.

If you host SignLocal at your own address, also change the ? help link in index.html (https://signlocal.pages.dev/guide.html) to point at your copy, then rebuild.


6. Signing identities and trust

How keys work by default

This proves integrity (the document hasn't changed) and key continuity (same device and browser as before). It does not prove legal identity, because anyone can type any name.

Making signatures trusted in Adobe Acrobat

Pick one approach:

  1. Per signer: the signer exports their certificate (Identity → Export certificate). Recipients import it in Acrobat as a trusted certificate. The steps are in the user guide.
  2. Organisation-wide, recommended for formal use: issue each signer a certificate from your certificate authority (for example Active Directory Certificate Services, or a public CA's document-signing certificate) as a .p12/.pfx file with a password. Signers load it in Identity → Use your own certificate each session. The certificate chain is embedded in each signature, and devices that trust your CA (for example through the Windows certificate store, or Acrobat's AATL or EUTL lists for public CAs) show the signature as trusted.

    • The certificate needs an RSA key and the Digital Signature (and ideally Non-Repudiation) key usage.
    • Very long certificate chains (over about 12 KB) can't be embedded; SignLocal reports this.

Trusted certificates on users' devices

Each user's Identity → Trusted certificates list decides which signatures show as Verified. To roll out your organisation's CA, send users its root certificate (.cer) with instructions to add it there. They can also choose Trust this signer's certificate on a Verify result.

For what this means legally, see Legal validity.

Timestamp relay

Signatures get a trusted RFC 3161 timestamp from a public timestamp authority (TSA). Public TSAs don't allow browser requests (no CORS), so SignLocal sends its request through a tiny relay:

Firewalls and proxies: allow HTTPS to signlocal.pages.dev. All three versions (web, single file and SharePoint) use this relay.

Using your own relay or a different TSA (for example an EU-qualified TSA):

  1. Change the TSAS list in functions/api/timestamp.js.
  2. Deploy it with the site to your own Cloudflare Pages project (npm run deploy deploys functions/ too). Alternatively, implement the same small POST relay on any server, returning Access-Control-Allow-Origin: *.
  3. Point SignLocal at it:

    • Hosted on your own Pages project: if the site's domain isn't signlocal.pages.dev, change the default URL in ST.tsa.url() in js/cms.js.
    • Without rebuilding: set window.SIGNLOCAL_TSA_URL = 'https://your.host/api/timestamp' in a script that loads before js/cms.js, and allow that host in the page's connect-src.
  4. Add your TSA's root certificate to vendor/tsa-roots.js, then rebuild (npm run build:site, npm run build:sharepoint).

Time without a trusted timestamp

If trusted timestamps are turned off or unavailable, times come from the signing device's clock. Keep automatic date and time on for managed devices (Windows Time service or NTP, and MDM settings on phones).


7. Security and data handling (for IT review)

TopicDetail
Where documents goNowhere. Files are read with the browser's File API, signed in memory, and downloaded.
NetworkOne request per signature, only when trusted timestamp is ticked: an RFC 3161 request (a SHA-256 fingerprint of the signature plus a nonce) to https://signlocal.pages.dev/api/timestamp, relayed to DigiCert, Sectigo or GlobalSign (see Timestamp relay). Nothing else is sent. The web version's Content Security Policy is default-src 'self', connect-src 'self' data: blob: and form-action 'none', so the page cannot send data anywhere else.
Data stored on the deviceBrowser localStorage for the SignLocal origin only: st.identity.v1 (the signer's private key, PEM, unencrypted), st.name, st.email, st.sig.signature and st.sig.initials (signature images, only if "Remember on this device" is ticked), and st.trust.v1 (certificates the user chose to trust). Imported .p12 certificates are never stored.
Signature formatsPDF: PAdES baseline B-T (ETSI EN 319 142), ETSI.CAdES.detached. Other files: CAdES B-T (ETSI EN 319 122) .p7s. Signed attributes are content-type, message-digest, signing-certificate-v2 and commitment-type-indication; the unsigned attribute is the RFC 3161 signature-time-stamp. Optional DocMDP certification (P=1).
Consent and intentEach signer must tick agreement to a fixed consent statement before signing. The statement and agreement times go into the certificate page or package README; the chosen intent goes into the signature (commitment type) and /Reason.
Removing stored dataIdentity → Reset identity removes the key. Clearing site data for the SignLocal address (or the SharePoint origin) removes everything. On shared or kiosk machines, use private browsing or clear browser data on sign-out.
CryptographyRSA-2048 keys (Web Crypto when available, otherwise node-forge); SHA-256; CMS/PKCS#7 detached signatures; PDF adbe.pkcs7.detached signatures over the whole file, added as incremental updates.
ValidationOutput validated with pyHanko (signature intact and valid; later signers classified as permitted form filling), OpenSSL cms -verify, and MuPDF.
Third-party codeBundled and pinned, with no CDN: pdf.js 3.11.174 (Apache-2.0), pdf-lib 1.17.1 (MIT), node-forge 1.3.1 (BSD-3-Clause option), JSZip 3.10.1 (MIT option).
SharePoint packageNo API permission requests; requiresCustomScript: false; assets served from the tenant (includeClientSideAssets: true); tenant-wide deployment (skipFeatureDeployment: true).
Known limitsDefault identities are self-signed, so use CA-issued .p12 certificates for strong identity. There are no hardware tokens or EU qualified signatures (QES), and no long-term validation data (PAdES-LT/LTA). Signing together on one device trusts the people present. There is no routing, reminders or central audit log. See Legal validity.

8. Developer setup: build, test and deploy

8.1 Prerequisites

8.2 Get the code and run it

git clone https://github.com/lipk-stack/signature-tool.git
cd signature-tool
npm install

There is no compile step for the web app. Open index.html in a browser to run it from source. To test offline mode and installation, serve the folder over HTTP instead, for example:

npx --yes http-server -p 8080 -c-1

Then open http://localhost:8080.

Source layout

PathWhat
index.htmlApp markup. The block between <!-- APP:START --> and <!-- APP:END --> is reused by every build.
css/app.cssStyles, all scoped under .signlocal so the app can sit inside SharePoint pages.
js/core.jsHelpers, signing identities, CMS sign/verify.
js/pdfsign.jsMarks, certificate page, incremental PDF signing, verification, conversions, Word signature blocks.
js/app.jsUser interface. window.SignLocalMount(element) starts the app inside any element.
vendor/Pinned third-party libraries.
tools/build.jsBuilds dist/SignLocal.html, the SharePoint bundle, and (with --site) the site/ folder.
sharepoint/SPFx 1.24 project. SignLocalWebPart.ts mounts the app; appBundle.ts is generated.
js/cms.jsPAdES/CAdES signing, RFC 3161 timestamp client, signature and certificate-chain verification, trusted-certificate list.
vendor/tsa-roots.jsRoot certificates of the timestamp authorities, used to check timestamp tokens.
functions/api/timestamp.jsThe timestamp relay (Cloudflare Pages Function).
docs/This guide, the user guide and the legal notes (also published as /setup, /guide and /legal).

8.3 Build commands

CommandOutput
npm run builddist/SignLocal.html and sharepoint/src/webparts/signLocal/appBundle.ts
npm run build:siteAlso site/, ready for any static host, including the two guides as guide.html and setup.html
npm run build:sharepointAlso dist/signlocal.sppkg (first run npm install inside sharepoint/, which takes a few minutes)
npm run deployBuilds site/ and uploads it to Cloudflare Pages

To build the SharePoint package:

npm --prefix sharepoint install
npm run build:sharepoint

Before releasing a new SharePoint version, raise version in sharepoint/config/package-solution.json (for example 1.2.0.0) and in package.json.

8.4 Deploy to Cloudflare Pages

  1. Log in once:

    npx wrangler login
  2. First time only, create the Pages project. With Wrangler 4.162 or later, --force is needed once to create a classic Pages project; leave it out on later deploys:

    npx wrangler pages project create signlocal --production-branch main --force
  3. Deploy (this also builds site/):

    npm run deploy

The site is published at https://<project>.pages.dev. To use your own domain, go to the Cloudflare dashboard → Workers & Pages → signlocal → Custom domains → Set up a custom domain.

To use a different project name, change --project-name in the deploy script in package.json.

8.5 Testing a change

Check at least these before releasing:

  1. Single signer: open a PDF, place a signature and a date, tick consent, finish, download. Verify shows Valid — unchanged since signing, with Signed at … trusted timestamp by DigiCert … (trusted authority) and PAdES baseline B-T.
  2. Several signers: add a second signer on the same device and a Sign here box for a third. Open the result in a different browser profile, fill the box, finish. Verify shows three valid signatures.
  3. Tamper check: change any byte of the signed PDF. Verify shows NOT valid.
  4. Word file: sign a .docx with two signers. Verify on the .zip shows every signature valid, and the .docx opens in Word with the signature blocks at the end.
  5. Phone size: repeat step 1 at phone width with touch.
  6. Independent checks (optional):

    pip install pyhanko-cli
    pyhanko sign validate --pretty-print --trust signer.cer signed.pdf

    signer.cer is the certificate the signer exported from Identity → Export certificate. Expect "All modifications relate to signing and form filling operations" and "The signature is judged VALID". Without --trust, pyHanko reports self-signed certificates as untrusted and judges them INVALID even when the document is intact.

    openssl cms -verify -binary -noverify -inform DER -in file.docx.p7s -content file.docx -out /dev/null

9. Updating SignLocal

OptionHow users get updates
A. Hosted websiteAutomatic. Installed apps pick up the new version the next time they're opened online (the service worker cache name changes with each version in sw.js).
B. Single fileReplace SignLocal.html wherever you distributed it.
C. SharePointUpload the new .sppkg over the old one in Manage apps and choose Replace/Enable. Pages update automatically; users may need to refresh.
D. Own serverRebuild site/ and copy it over the old files.

Documents signed with older versions stay valid; the signature format doesn't change between versions.


10. Troubleshooting

SymptomCause and fix
SignLocal isn't in the SharePoint web part listThe app isn't enabled for this site. Check Manage apps (C2); if you didn't add it to all sites, a site owner adds it (C4). New uploads can take a few minutes to appear.
The web part shows an error or stays blankRefresh with Ctrl+F5. Check the app is still enabled. Open the browser developer tools (F12 → Console) and look for errors; send them to whoever maintains SignLocal.
The .html file downloads instead of opening in SharePoint or OneDriveExpected: SharePoint never runs HTML files. Open the downloaded copy, or use the web part.
The app doesn't work offline on a phoneOpen it once while online over HTTPS, then reopen. Private browsing doesn't keep the offline cache.
"Signer's identity is unknown" in AcrobatExpected for self-signed certificates. See section 6.
A user lost their saved signature or keyBrowser data was cleared, or they use a different browser or device. Nothing to restore; they create a new one. Earlier signed documents stay valid.
"Certificate chain is too large to embed"The imported .p12 has a very long chain. Export it with only the signer and intermediate certificates.
"This PDF has no cross-reference table…"The PDF is damaged. Re-save it from its original program (or Print to PDF) and sign the new copy.
Wrangler: "No access to the specified resource" on deployYour login can't deploy Workers. Use the Pages commands in 8.4 (pages deploy), not wrangler deploy.
Wrangler: "Missing entry-point to Worker script" when creating the projectAdd --force to pages project create (first time only), as in 8.4.