A tiny native macOS app that does exactly one thing: open a PDF and digitally
sign it with a certificate — the same kind of signature Adobe Acrobat produces
(adbe.pkcs7.detached, PKCS#7/CMS), without the bloat.
No dependencies. Pure Swift + Apple frameworks (SwiftUI, PDFKit, Security).
make app # builds PDFSign.app in the repo root
make run # builds and opens it
make test # runs the test suite- Open a PDF (⌘O, the toolbar button, or drop a file on the window).
- Pick a certificate from the toolbar menu — it lists signing identities from your Keychain, or use Import Certificate (.p12)… to load a PKCS#12 file (kept for the session only).
- Click Sign, then drag a rectangle on the page where the visible signature stamp should go — or choose Invisible Signature.
- Optionally fill in a reason/location and pick a handwritten signature
for the stamp: draw one with your trackpad/mouse (Draw New…), sign on
your phone (Sign on Phone… — scan the QR code with the phone's camera,
a full-screen signature pad opens in the browser; same Wi-Fi required,
works with any phone), or import an image (Import Image… — a transparent
PNG, or a photo/scan whose light background is removed automatically).
Apple's Continuity Sketch is still available inside the phone sheet.
Saved signatures are kept in
~/Library/Application Support/PDFSign/Signatures/for reuse. (Preview's own saved signatures are stored encrypted in a private format, so they can't be imported directly — redraw once and it's saved here.) - Sign & Save….
macOS will ask for permission the first time the app uses a private key from your Keychain — that's the Keychain access control doing its job.
Pushing a v* tag (or running the workflow manually) triggers
.github/workflows/release.yml on a macOS runner: it runs the tests, builds
the app signed with a Developer ID certificate (hardened runtime + secure
timestamp), packages a signed drag-to-Applications DMG, optionally notarizes
it, and attaches it to a GitHub Release.
Required repository secrets:
| Secret | Value |
|---|---|
MACOS_CERTIFICATE |
base64 of a .p12 containing your Developer ID Application certificate + private key (base64 -i cert.p12 | pbcopy) |
MACOS_CERTIFICATE_PASSWORD |
the .p12 password |
Optional (recommended — without notarization Gatekeeper blocks downloaded
apps): NOTARY_APPLE_ID (Apple ID email), NOTARY_PASSWORD (app-specific
password from appleid.apple.com), NOTARY_TEAM_ID (10-char team ID).
Local equivalent: make dmg VERSION=v1.0 SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" CODESIGN_FLAGS="--timestamp --options runtime".
- A standard incremental update: the original bytes are untouched, so any existing signatures remain valid (and you can sign an already-signed PDF).
- A signature field + widget annotation with an appearance stream
("Digitally signed by …, Date, Reason, Location"), registered in AcroForm
with
SigFlags 3. - A detached CMS/PKCS#7 SignedData blob (SHA-256, signing time, full
certificate chain), DER-encoded, over the
/ByteRangeof the file — verifiable by Adobe Acrobat,openssl smime -verify, and any PAdES-basic aware validator. - Matches the original file's cross-reference style (classic tables and cross-reference streams are both supported, including object streams).
- Encrypted (password-protected) PDFs are refused — remove the password first.
- No RFC 3161 trusted timestamps and no LTV/PAdES-LT (could be added later).
- Whether a validator shows the signer as trusted depends on the certificate's chain, exactly as in Acrobat: self-signed certificates validate as "signer unknown" unless the recipient trusts the certificate.
Sources/PDFSignCore— PDF parser (xref tables/streams, object streams), incremental-update signature writer, CMS signing, Keychain identity store.Sources/PDFSign— the SwiftUI app.Tests/PDFSignCoreTests— end-to-end tests: generate PDFs, sign with a throwaway certificate in a temporary keychain, verify independently withopenssl.