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.
- Choose a way to provide SignLocal
- Option A: Hosted website (no setup)
- Option B: Single file
- Option C: SharePoint Online and Microsoft Teams
- Option D: Host it on your own web server
- Security and data handling (for IT review)
- Developer setup: build, test and deploy
- Updating SignLocal
- Troubleshooting
1. Choose a way to provide SignLocal
All four options run the same app with the same features. Pick what suits your users.
| Option | Best for | Needs | Works offline |
|---|---|---|---|
| A. Hosted website: https://signlocal.pages.dev | Anyone, any device, phones | A browser | Yes, after the first visit (or when installed as an app) |
B. Single file: SignLocal.html | Locked-down PCs, air-gapped networks, USB, email | A browser | Always |
C. SharePoint / Teams web part: signlocal.sppkg | Microsoft 365 organisations | SharePoint admin rights, once | Needs SharePoint to load the page |
| D. Your own web server | Your own domain or intranet | A static web server | Yes, 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
| File | Link | In the repository |
|---|---|---|
| Single-file app | https://signlocal.pages.dev/download/SignLocal.html | dist/SignLocal.html |
| SharePoint package | https://signlocal.pages.dev/download/signlocal.sppkg | dist/signlocal.sppkg |
Option A: Hosted website (no setup)
Send users the address https://signlocal.pages.dev.
- Desktop: it runs in the browser. In Edge or Chrome, users can choose Install (the icon in the address bar, or the browser menu → Apps → Install SignLocal) to get a desktop shortcut and a standalone window.
- iPhone / iPad (Safari): Share → Add to Home Screen.
- Android (Chrome): ⋮ → Install app.
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
- Download
SignLocal.html(about 2.4 MB) from the link above or fromdist/in the repository. - 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.
- Users double-click it. It opens in their default browser.
Notes:
- The single file contains everything, including all libraries. It never loads anything from the internet, so it suits air-gapped networks.
- SharePoint and OneDrive libraries: if users click an
.htmlfile there, SharePoint downloads it instead of running it (by design). Users then open the downloaded copy. For running SignLocal inside SharePoint, use Option C. - Signatures and keys are stored per browser. If users open the file from different locations, each copy keeps the same browser storage for a
file://page in Chrome and Edge, but this can differ between browsers. - Some email filters block
.htmlattachments. Zip it first, or share a link instead.
Option C: SharePoint Online and Microsoft Teams
SignLocal is packaged as a SharePoint Framework (SPFx 1.24) web part:
- All code is inside the package, and SharePoint serves it from your tenant's own app catalog storage. Nothing loads from outside your tenant.
- It does not require custom scripts to be enabled, and it complies with SharePoint Online's Content Security Policy enforcement.
- It requests no API permissions (no Microsoft Graph or SharePoint API access). It can't read or write your SharePoint content; users choose files from their device, and signed files are downloaded to their device.
- Its only outside connection is the optional timestamp relay, which receives a signature fingerprint, never documents.
- It is deployable tenant-wide, so there's no per-site installation.
Why not upload the HTML file to a document library? SharePoint Online opens
.htmlfiles as downloads rather than running them, and modern sites block.aspxuploads ("NoScript"). An SPFx package is the supported way to run custom code in SharePoint Online.
C1. Prerequisites
- SharePoint Administrator (or Global Administrator) rights in Microsoft 365, to upload the app.
- A tenant app catalog. Most tenants already have one. To check, go to the SharePoint admin center → More features → Apps → Open. If you're asked to create an app catalog, accept and wait a few minutes.
C2. Upload and enable the app (once per tenant)
- Download
signlocal.sppkg. - Open the SharePoint admin center (
https://<your-tenant>-admin.sharepoint.com). - Go to More features → Apps → Open. The Manage apps page opens.
- Choose Upload and select
signlocal.sppkg. 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.
- (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
- Go to the SharePoint site, open the page (or + New → Page), and choose Edit.
- Hover where you want it and choose + (Add a new web part).
- Search for SignLocal (it's listed under Advanced) and select it.
- Choose Publish (or Republish).
Recommendations:
- Put it in a full-width section (Edit → + Add section → Full-width section) or a one-column section, so documents have room.
- For a dedicated signing page, use SignLocal as a full-page app: on the site's home page choose + New → App, select SignLocal, give the page a name (for example Sign), and choose Create. The page then shows only SignLocal, with the most room for documents.
- Add a link to the page in the site navigation or a hub, for example Sign a document.
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:
- As a tab: in a channel or chat, choose + (Add a tab) → SignLocal.
- As a personal app: in Teams, choose Apps, search for SignLocal, and choose Add. You may need to allow it in the Teams admin center → Teams apps → Manage apps if your app permission policies block custom apps.
C6. Remove or roll back
- Remove from one page: edit the page, select the web part and choose the bin icon.
- Remove everywhere: in Manage apps, select SignLocal and choose Remove. Pages that use it then show an error, so remove it from pages first.
- Roll back: upload the previous
.sppkgversion over the current one.
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.
Build the site folder (see section 8):
npm run build:site. This writessite/:site/ index.html, sw.js, manifest.webmanifest, icon.svg css/ js/ vendor/ download/SignLocal.html, download/signlocal.sppkg _headers (security headers, used by Cloudflare)- Copy the contents of
site/to your web server or static host (IIS, nginx, Apache, Azure Static Web Apps, Amazon S3 + CloudFront, Netlify…). - Serve it over HTTPS. Offline use and installing as an app need HTTPS (or
localhost). - Add the security headers that
site/_headerslists. 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
- The first time someone seals a document, their browser creates an RSA-2048 key and a self-signed certificate with their name and email. The key is stored in that browser's local storage on that device.
- The key's fingerprint (the Key ID) stays the same for that person on that browser until they choose Reset identity.
- Extra signers on the same device (signing together) get a one-time key for that signing.
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:
- 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.
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/.pfxfile 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:
- What is sent: an RFC 3161 TimeStampReq, which contains a SHA-256 fingerprint of the signature value and a random number. No document content, names or emails.
- The relay:
https://signlocal.pages.dev/api/timestamp, a Cloudflare Pages Function (functions/api/timestamp.jsin the repository). It accepts only small DER timestamp requests, forwards them unchanged to DigiCert (with Sectigo, then GlobalSign as fallbacks), and stores and logs nothing. - Checking the result: SignLocal checks every token: that it covers this signature and uses this request's nonce, that the TSA's signature is valid, and that the TSA chains to one of the bundled roots (
vendor/tsa-roots.js: DigiCert, USERTrust/Sectigo, GlobalSign). - Offline or blocked: signing still works with the device's clock, and the result says that no trusted timestamp was added.
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):
- Change the
TSASlist infunctions/api/timestamp.js. - Deploy it with the site to your own Cloudflare Pages project (
npm run deploydeploysfunctions/too). Alternatively, implement the same small POST relay on any server, returningAccess-Control-Allow-Origin: *. Point SignLocal at it:
- Hosted on your own Pages project: if the site's domain isn't
signlocal.pages.dev, change the default URL inST.tsa.url()injs/cms.js. - Without rebuilding: set
window.SIGNLOCAL_TSA_URL = 'https://your.host/api/timestamp'in a script that loads beforejs/cms.js, and allow that host in the page'sconnect-src.
- Hosted on your own Pages project: if the site's domain isn't
- 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)
| Topic | Detail |
|---|---|
| Where documents go | Nowhere. Files are read with the browser's File API, signed in memory, and downloaded. |
| Network | One 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 device | Browser 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 formats | PDF: 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 intent | Each 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 data | Identity → 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. |
| Cryptography | RSA-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. |
| Validation | Output validated with pyHanko (signature intact and valid; later signers classified as permitted form filling), OpenSSL cms -verify, and MuPDF. |
| Third-party code | Bundled 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 package | No API permission requests; requiresCustomScript: false; assets served from the tenant (includeClientSideAssets: true); tenant-wide deployment (skipFeatureDeployment: true). |
| Known limits | Default 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
- Node.js 22.14+ or 24.x (
node --version) - Git
- For SharePoint builds: no extra tools; everything installs from npm.
- For Cloudflare deploys: a Cloudflare account.
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
| Path | What |
|---|---|
index.html | App markup. The block between <!-- APP:START --> and <!-- APP:END --> is reused by every build. |
css/app.css | Styles, all scoped under .signlocal so the app can sit inside SharePoint pages. |
js/core.js | Helpers, signing identities, CMS sign/verify. |
js/pdfsign.js | Marks, certificate page, incremental PDF signing, verification, conversions, Word signature blocks. |
js/app.js | User interface. window.SignLocalMount(element) starts the app inside any element. |
vendor/ | Pinned third-party libraries. |
tools/build.js | Builds 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.js | PAdES/CAdES signing, RFC 3161 timestamp client, signature and certificate-chain verification, trusted-certificate list. |
vendor/tsa-roots.js | Root certificates of the timestamp authorities, used to check timestamp tokens. |
functions/api/timestamp.js | The 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
| Command | Output |
|---|---|
npm run build | dist/SignLocal.html and sharepoint/src/webparts/signLocal/appBundle.ts |
npm run build:site | Also site/, ready for any static host, including the two guides as guide.html and setup.html |
npm run build:sharepoint | Also dist/signlocal.sppkg (first run npm install inside sharepoint/, which takes a few minutes) |
npm run deploy | Builds 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
Log in once:
npx wrangler loginFirst time only, create the Pages project. With Wrangler 4.162 or later,
--forceis needed once to create a classic Pages project; leave it out on later deploys:npx wrangler pages project create signlocal --production-branch main --forceDeploy (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:
- 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.
- 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.
- Tamper check: change any byte of the signed PDF. Verify shows NOT valid.
- Word file: sign a
.docxwith two signers. Verify on the.zipshows every signature valid, and the.docxopens in Word with the signature blocks at the end. - Phone size: repeat step 1 at phone width with touch.
Independent checks (optional):
pip install pyhanko-clipyhanko sign validate --pretty-print --trust signer.cer signed.pdfsigner.ceris 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
| Option | How users get updates |
|---|---|
| A. Hosted website | Automatic. 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 file | Replace SignLocal.html wherever you distributed it. |
| C. SharePoint | Upload the new .sppkg over the old one in Manage apps and choose Replace/Enable. Pages update automatically; users may need to refresh. |
| D. Own server | Rebuild 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
| Symptom | Cause and fix |
|---|---|
| SignLocal isn't in the SharePoint web part list | The 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 blank | Refresh 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 OneDrive | Expected: SharePoint never runs HTML files. Open the downloaded copy, or use the web part. |
| The app doesn't work offline on a phone | Open it once while online over HTTPS, then reopen. Private browsing doesn't keep the offline cache. |
| "Signer's identity is unknown" in Acrobat | Expected for self-signed certificates. See section 6. |
| A user lost their saved signature or key | Browser 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 deploy | Your 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 project | Add --force to pages project create (first time only), as in 8.4. |