Omitly โ Public Threat Model
Status: Draft for review ยท Last updated: 2026-07-01 ยท Applies to: Omitly desktop (macOS; Windows at launch)
This document is Omitly's Public Threat Model (PTM). It exists so that customers, security reviewers, and the wider community can evaluate our security claims against the threats they care about โ rather than being asked to "trust us." It describes how Omitly is secured; it is not a guide to attacking it.
Why we publish this
Omitly's value proposition is itself a security claim: we remove the underlying data from a PDF, independently verify it is gone, and never send your documents anywhere. A claim like that should be falsifiable. Following the argument made by Loren Kohnfelder and Adam Shostack in "Publish Your Threat Models!" (2025) โ and the principle of Open Design (Saltzer & Schroeder) โ we believe the benefits of publishing far outweigh the risks. A threat model describes how a product is secured, not how to break it; if any part of it felt too dangerous to publish, that would be a signal of a real weakness to fix rather than hide.
This is also where the industry is heading: the EU Cyber Resilience Act will effectively require documented cybersecurity risk assessments, and CISA's Secure by Design and Australia's Secure by Demand both push toward this kind of transparency.
This model is organized around Shostack's Four Question Framework:
- What are we working on?
- What can go wrong?
- What are we going to do about it?
- Did we do a good enough job?
1. What are we working on?
1.1 Product summary
Omitly is a local-only desktop PDF reader and editor. Its headline capability is verifiable redaction: it removes the underlying text and image data for a redacted region (it does not merely draw a black box), then independently verifies on the output that the data is unrecoverable.
By default, nothing is uploaded and the application makes no network calls. As of 2026-07, there is exactly one opt-in exception: requesting a trusted RFC 3161 timestamp while digitally signing a document (ยง1.3). It is off unless the user turns it on for that specific signature, and it sends only a cryptographic hash โ never document content. See "Network surface" below ยง1.3 for the complete, exhaustive list of what is and isn't local; nothing outside that list reaches the network.
1.2 Components
| Component | Tech | Responsibility | Trust level |
|---|---|---|---|
| Webview UI | React + TypeScript, runs in the OS webview | Rendering (PDF.js), editing (pdf-lib), drawing redaction regions, email auto-detection, certificate import + sign UI. No direct filesystem access. | Lower-trust; treats PDF content as untrusted |
| Tauri/Rust core | Rust | The privileged process. Exposes IPC commands for file I/O, redaction, audit verification, and PAdES signing (read_file_bytes, write_file_bytes, redact_pdf, verify_audit, export_certificate, import_signing_certificate, sign_pdf, verify_signature). Owns all file I/O and the app's one network boundary. |
Trust boundary owner |
redaction-core crate |
Rust (lopdf) + bundled qpdf |
The redaction engine plus the PDF-structural half of PAdES signing (/Sig placeholder embedding and byte-range patching). Tauri-independent, clock/env-free, so it can be unit-tested in isolation and never itself touches the network. |
Trusted core logic |
PAdES signing (src-tauri/src/sign.rs) |
Rust (cryptographic-message-syntax, x509-certificate) |
Builds a CMS/PAdES signature over a user-supplied (BYOC) certificate; validates algorithm floors and certificate/key pairing; optionally contacts an RFC 3161 timestamp authority over HTTPS. This is the only code path in Omitly that can reach the network, and only when the user explicitly opts in per signature. | Trusted core logic + the app's sole network boundary |
licensing crate |
Rust (ed25519-dalek, sha2) |
Offline Ed25519 license verification. Present and tested but not yet wired into the shipping app. | N/A at runtime today |
omitly-mcp server |
Node/TypeScript (MCP SDK) | Stub. Intended to expose redact_pdf / verify_redaction to local AI agents over stdio. Not functional yet. |
Future boundary โ see ยง4 |
1.3 Trust boundaries and data flow
flowchart TD
user([User]) -->|chooses file via OS dialog| webview
subgraph device[User's device]
webview[Webview UI<br/>PDF.js / pdf-lib<br/>no FS access]
core[Tauri/Rust core<br/>file/redact/verify/sign commands]
engine[redaction-core<br/>lopdf + Sig placeholder]
qpdf[qpdf sidecar<br/>native C++]
disk[(Local disk)]
webview -->|B1: IPC โ paths, PDF bytes, regions| core
core -->|B2: untrusted PDF bytes| engine
engine -->|process boundary| qpdf
qpdf -->|sandboxed: temp-only writes, no network| engine
core -->|redacted/signed PDF + audit.json| disk
end
tsa[(RFC 3161 Timestamp<br/>Authority โ third party)]
core -.->|B3: OPT-IN ONLY โ<br/>signature hash alone,<br/>never document content| tsa
classDef boundary fill:#fef3c7,stroke:#b45309;
classDef network fill:#fee2e2,stroke:#b91c1c,stroke-dasharray: 5 5;
class core,qpdf boundary;
class tsa network;
- B1 โ Webview โ Rust core (IPC). The lower-trust webview cannot touch the filesystem; it can only ask the Rust core to act on paths the user selected through an OS dialog. Enforced by Tauri capabilities (no
fs:capability granted to the webview) and a restrictive Content Security Policy. - B2 โ Rust core โ qpdf sidecar (process). Untrusted PDF bytes are processed by a native C++ binary โ the largest native attack surface. On macOS this runs under a deny-by-default seatbelt sandbox (read anywhere, write only to temp, no network). Arguments are passed as explicit argv, never through a shell.
- B3 โ the app's only network boundary, opt-in and disclosed. Added 2026-07 alongside PAdES digital signing: if (and only if) the user turns on "add a trusted timestamp" for a specific signature, Omitly sends an RFC 3161 request (a SHA-256/384 digest of the signature, plus the digest algorithm identifier โ never the document, filename, or any PDF bytes) to a Time-Stamp Authority over HTTPS. The TSA endpoint is user-configurable (default: a public TSA; an organization can point this at its own internal TSA to keep the request off third-party infrastructure entirely). Signing without a timestamp remains fully available and makes zero network calls. See "Network surface" below for the exhaustive local-vs-network map.
- Redacted/signed PDFs and the audit sidecar are written only to the user-chosen location on local disk โ this has not changed.
Network surface โ the complete local-vs-network map
Everything Omitly does, and whether any of it reaches the network. This table is exhaustive by construction: B3 above is the only network-capable code path anywhere in the app, so anything not listed as reaching it is, structurally, local-only.
| Feature | Network call? | What's sent | When |
|---|---|---|---|
| Open / view / edit a PDF | No | โ | always |
| Manual or auto (email) redaction + verification | No | โ | always |
| Audit report (JSON/HTML) + Ed25519 tamper-evidence seal | No | โ | always |
| Verify a redacted PDF's audit report / export the certificate | No | โ | always |
| Import a signing certificate (PEM + PKCS#8) | No | โ | always โ validated and stored (OS keystore preferred) entirely on-device |
| PAdES-sign a document, timestamp off | No | โ | default; zero network calls |
| PAdES-sign a document, timestamp on | Yes โ the only case | A SHA-256/384 digest of the CMS signature value + the digest algorithm OID. Never: document content, filename, page count, certificate holder's name/PII beyond what the digest algebraically can't avoid revealing (nothing), or the private key. | Only for that one signature, only if the user opted in |
Verify a PAdES signature (verify_signature) |
No | โ | always. Revocation (OCSP/CRL) checking is deliberately not implemented in this build โ so there is also no OCSP network call; this is a coverage gap (ยง4.2) and a network-minimalism property at the same time, and both are stated here rather than picking one framing |
Why the minimum can't be lower than this: a timestamp that actually means something under RFC 3161 / PAdES-B-T requires an independent third party (the TSA) to attest to the time โ that attestation is the entire value of the feature, and it cannot be produced without a network round trip to some TSA. What Omitly controls, and has minimized: (1) the request carries a digest only, never content; (2) it is per-signature opt-in, not a background/automatic call; (3) the TSA is user-configurable, so the destination of that one request is the user's choice, not Omitly's; (4) declining the timestamp is a fully supported, fully local (PAdES-B-B) alternative โ the feature degrades gracefully to zero network calls rather than requiring one.
1.4 Assets we protect
The user's document content (especially the data being redacted); the integrity of the redaction result; the integrity of the audit record; the integrity of the Omitly binary itself.
2 & 3. What can go wrong, and what we do about it (STRIDE)
Threats are enumerated with STRIDE against the elements and boundaries above. Mitigations marked โ Gap are not yet implemented and are tracked in ยง4.2.
Redaction engine (the core asset)
| STRIDE | Threat | Mitigation |
|---|---|---|
| Information disclosure | "Redacted" data remains recoverable underneath a black box (the category's defining failure). | Engine removes the glyphs and image XObjects, not just covers them; normalizes/flattens incremental updates and object streams via qpdf so prior revisions can't retain the data. |
| Information disclosure | Data survives in metadata, XMP, thumbnails, comments/annotations. | Metadata scrub removes /Info, XMP /Metadata, /PieceInfo, and markup/comment annotations; unreferenced objects are garbage-collected on finalize. |
| Information disclosure | Engine claims success it cannot actually prove (e.g. content it can't parse). | Independent two-part verification on the output: (1) re-interpret the page and fail if any text/image still renders into a redacted region; (2) decompress the output and substring-search for the removed fragments. Unparseable pages and inline images (BI/ID/EI) are flagged as warnings that force an overall fail rather than a false pass. |
| Tampering / Spoofing | Over-redaction (removing neighboring content) or under-redaction. | Spatial, per-glyph content-stream editing computes each glyph box; covered by ~20 correctness tests asserting both removal and survival of neighbors, including rotated/multi-page cases. |
Audit log
| STRIDE | Threat | Mitigation |
|---|---|---|
| Tampering | The *.audit.json sidecar can be edited after the fact and still appears valid. |
โ Gap โ no integrity protection today. The audit file is plain JSON. A cryptographic signature is planned (see ยง4.2). |
| Repudiation | The recorded timestamp is taken from the local clock and is trivially spoofable. | โ Partial. The audit log's own created_at is still local-clock. A trusted-timestamp mechanism now exists (RFC 3161, added 2026-07) but today it's wired to the PAdES signature, not yet to the audit-log timestamp itself โ closing that gap is tracked in ยง4.2. |
Boundary B1 โ Webview โ Rust core
| STRIDE | Threat | Mitigation |
|---|---|---|
| Elevation of privilege | Compromised/hostile webview content reads or writes arbitrary files. | Webview has no filesystem capability; only a small, fixed set of narrow IPC commands exist (ยง1.2). File I/O is restricted to user-dialog-selected paths; non-regular files (symlinks/devices) are rejected; writes are atomic (temp + rename). |
| Tampering | Injected script alters behavior or exfiltrates content. | Strict CSP (script-src 'self', object-src 'none', connect-src limited to local IPC, frame-ancestors 'none'). No remote code; PDF.js worker is bundled locally (no CDN fetch). |
Viewer text layer (selectable text in the preview โ added 2026-07)
The preview now materializes the document's text as invisible DOM spans (a PDF.js text layer) so text can be selected and copied with the native OS gesture. This changes the in-webview surface and is bounded as follows:
| STRIDE | Threat | Mitigation |
|---|---|---|
| Information disclosure | Copy/paste leaks text that a redaction box visually covers โ the classic "black box you can copy-paste under," now via the preview rather than the output file. | Runs covered by a redaction are emptied from the DOM, not just visually hidden, so native selection/copy has nothing to pick up. Exclusion is whole-run and fail-safe: partial coverage excludes the entire run rather than trimming it (src/viewer/textSelection.ts, unit-tested). The layer may only ever expose what the canvas visibly shows. The output-file guarantee is unchanged โ actual removal still happens in the Rust engine on the real bytes (ยง Redaction engine), never in the preview. |
| Information disclosure | Document text now exists as DOM nodes, enlarging what hostile in-webview script could read. | No new trust level: the webview already holds the full document bytes and PDF.js text content in JS memory (find, OCR, auto-detect all read it); DOM spans add a representation, not access. Exfiltration remains blocked by the same B1 mitigations (strict CSP, connect-src limited to local IPC, no remote script). |
| Information disclosure | Copied text lands in the OS clipboard, subject to clipboard history/managers/cross-device sync. | Inherent to copy, same as any editor, and user-initiated: the app itself never calls a clipboard API โ the only clipboard write is the user's own OS copy gesture on a native selection. No auto-copy, no programmatic writes. |
Boundary B2 โ Native PDF processing (qpdf / lopdf)
| STRIDE | Threat | Mitigation |
|---|---|---|
| Elevation of privilege / DoS | A malicious PDF exploits a parser bug in native code. | qpdf runs under a macOS seatbelt sandbox (no network, writes confined to temp). Pure-Rust lopdf provides memory safety for the Rust-side parsing. โ Partial: on non-macOS platforms the sandbox is a no-op and relies on ambient OS sandboxing โ hardening tracked in ยง4.2. |
Application / supply chain
| STRIDE | Threat | Mitigation |
|---|---|---|
| Spoofing / Tampering | User runs a trojaned build of "Omitly." | โ Gap. Code signing / notarization config is not yet present. Planned for release (see ยง4.2). |
| Tampering | Compromised dependency in the build. | Pure-Rust core with a small dependency set; SBOM (and a cryptography bill of materials) planned for publication per release. |
| Information disclosure | Hidden/undisclosed network egress or telemetry contradicts the local-only claim. | Verified and scoped, not "zero": the app's only network-capable code path is the opt-in RFC 3161 timestamp request during PAdES signing (added 2026-07, boundary B3 in ยง1.3) โ it is disclosed in the signing UI before it fires, carries a signature digest only, and is skippable per signature. No telemetry, no auto-updater, no license-server call exist anywhere. The complete, exhaustive list of what is and isn't local is the "Network surface" table in ยง1.3 โ anything not in that table making a network call would be a bug against this threat model. |
4. Did we do a good enough job?
4.1 How we validate (and how you can check)
- The redaction-correctness tests are treated as the product. ~20 tests assert that secrets (e.g. SSNs) are unrecoverable from the decompressed output, that neighboring content survives, that metadata and comment annotations are scrubbed, that image markers are removed, and that pages the engine can't fully reason about (inline images) are flagged rather than silently passed. These run with plain
cargo testagainst the Tauri-independent engine. - Verification is independent of redaction. The verify step re-derives its result from the output file, not from the engine's own bookkeeping, so a bug in removal does not produce a false "verified."
- The local-only claim is scoped and structurally enforced, not just asserted. There is exactly one network-capable code path in the entire app (boundary B3, opt-in RFC 3161 timestamping โ see the "Network surface" table in ยง1.3), and no updater to invoke. Every other feature is checkable by the same method used before: there is no other network code path to find.
4.2 Known gaps and roadmap (stated honestly)
Per Shostack's guidance, a published threat model should include the complete set of threats โ including those not yet fully mitigated. The following are known and tracked:
- Signed audit log โ not yet implemented. The audit sidecar is currently unsigned JSON with a local-clock timestamp. Planned: cryptographic signing (Ed25519 capability already exists in the
licensingcrate) and a tamper-evident format, ideally with a post-quantum / hybrid signature option. Until shipped, Omitly does not claim a "signed" audit log. - Code signing / notarization for distributed binaries โ to be configured before public release.
- Sandbox parity off macOS โ extend the qpdf sandboxing approach to Windows.
- MCP server (
omitly-mcp) is a non-functional stub. When built, it would widen the trust boundary from "a human selecting files through an OS dialog" to "any local AI agent able to drive redaction over arbitrary file paths." It will need its own threat-model section and access controls before release. - Published SBOM / CBOM to substantiate the supply-chain and cryptography claims.
- PAdES signing (added 2026-07) has two stated scope boundaries, not silent gaps: (a) no OCSP/CRL revocation checking โ a signature that was valid when checked could have since been revoked and this build won't know; (b) no CA trust-chain validation โ this is a bring-your-own-certificate (BYOC) self-signing feature, so a signer's reported identity is read directly from the certificate and is not confirmed against any external trust authority (the same "integrity, not identity" scope as the existing Ed25519 seal). Both are surfaced in every
verify_signatureresult's warnings, not just here. Planned: PAdES-B-LT/LTA (embeds revocation evidence) and, longer-term, hardware-backed signing keys (YubiKey/Secure Enclave/Windows CNG) so the private key never exists as an exportable file at all.
4.3 Scope and assumptions (non-goals)
This model assumes a trustworthy host: Omitly does not defend against a compromised operating system, malware already running with the user's privileges, physical access to an unlocked device, or screen-capture of content displayed on screen. It covers the desktop application and redaction engine; it does not cover the marketing website (separate codebase and trust boundary) or any future cloud service (none exists today).
Methodology & precedent
This document follows the Threat Modeling Manifesto (2020) and Shostack's Four Question Framework, with threats enumerated via STRIDE. We publish in the spirit of peer privacy-and-security tools that publish their own threat models, including SecureDrop, Tor, cURL, and Kubernetes. We welcome scrutiny: if you find a threat we've missed or a mitigation that doesn't hold, please contact us.
This is a living document and will be revised on architecture changes, new features, security incidents, and at least annually.