VeriDoc
VeriDoc — Secure Document Verification System
What it is: a web-based system that lets authorized organizations issue digitally signed PDF documents, and lets anyone verify their authenticity and integrity via a QR code or a PDF upload — without contacting the issuing organization.
Also known as the final-year project: "Design and Implementation of a Secure Document Verification System Using QR Codes and Digital Signatures."
Don't pretend it's an AI project. Its value is that it proves I understand security fundamentals — authentication, authorization, cryptography, integrity, secure workflows, auditability, testing, backend engineering — before moving into AI security. It is version 0 of the security-engineering story, not an artifact to discard.
Source material: 00 PROJECT PROPOSAL · 01 COVER PAGE · 02 PRELIMINARY PAGES · 03 CHAPTER ONE · 04 CHAPTER TWO · 05 CHAPTER THREE · 06 CHAPTER FOUR · 07 CHAPTER FIVE · 08 APPENDICES
What it demonstrates
Application security: authentication → authorization → cryptography → integrity → secure workflows → auditability → testing → backend engineering.
Built with React, Node.js, Express, PostgreSQL, Prisma, better-auth, UploadThing, PDF-lib, Vitest, using SHA-256 hashing and RSA-2048 digital signatures, with AES-256-GCM protecting private keys.
System specification
Purpose
- Enable authorized organizations to issue, sign, manage and revoke digitally signed PDF documents.
- Enable third parties (verifiers) to check document authenticity and integrity without depending on manual confirmation from the issuer.
- Maintain verification records of verification attempts.
- Scope: PDF documents only; focus is authenticity + integrity, not confidentiality or archival.
Threat model
Defended against:
- Post-issuance modification of a document — detected by SHA-256 hash mismatch.
- Forgery / impersonation of the issuer — detected by RSA signature verification with the issuing organization's public key.
- Wrong / mismatched verification key — signature verification fails.
- Revoked documents presented as valid — explicit status check (
REVOKED). - Unregistered documents — no matching record (
NOT_FOUND). - Unauthorized access to protected operations — authentication + organization membership + role checks (tested: 401 / 403).
- Private key at rest — encrypted with AES-256-GCM.
Out of scope / not defended:
- Compromise of an organization's private signing key.
- Availability of the verification service, database, or document storage.
- Transport/TLS assumptions (not documented).
- Large-file, concurrent, and rate-limiting conditions (not fully tested).
- No formal independent security audit or penetration test was performed.
Trust model
- Each organization holds an RSA key pair; the private key signs, the public key verifies.
- The organization is trusted to issue the documents it registers, and the information supplied at issuance is assumed accurate.
- The system is the reference: QR verification checks the server-stored document, not a document submitted by the person scanning.
- Public verifiers are untrusted and unauthenticated.
- Cross-organization isolation: a document signed by one organization cannot be verified with another organization's public key (tested).
Actors
Two major external actors:
- Organization User (authenticated): create/access an organization; manage membership; invite users; assign roles; upload, issue, sign, manage and revoke documents; review verification activity.
- Verifier (public): scan a QR code; use the public verification interface; submit a PDF; view results. No account required.
Architecture
Layered web application on the PERN stack:
- Presentation — React frontend.
- Application — Node.js + Express.js backend (routes, controllers, middleware, services).
- Authentication & Organization — better-auth.
- Cryptographic — SHA-256 hashing + RSA signature services.
- Data — PostgreSQL accessed through Prisma.
- Document storage — external file-storage service (UploadThing); the DB stores metadata and references.
- Public verification interface — public-facing, unauthenticated.
General flow: Client → React → Express API → { Prisma/PostgreSQL, Crypto service, File storage }.
Authentication
Handled by better-auth: user registration, login, logout, session management, protected routes, and account management. Backed by the User, Session, Account and Verification entities.
Authorization
- Organization membership + roles + invitations via the better-auth organization plugin (
Organization,Member,Invitation). - Public verification is separated from protected organization operations.
- Protected document operations require the appropriate organization membership and permissions.
- Tested: unauthenticated signing → 401; missing membership or insufficient privileges → 403; unauthorized revocation rejected.
Key management
- Per-organization RSA-2048 key pair.
- Public key stored in the organization record.
- Private key stored encrypted (
privateKeyEncrypted) using AES-256-GCM, decrypted only when signing. - No HSM or enterprise key-management infrastructure (declared out of scope); key rotation/revocation/recovery are future work.
Signing process
- Authorized organization user uploads a PDF.
- The system validates the file.
- A unique verification token is generated —
crypto.randomUUID(). - A public verification URL is built:
{host}/verification?token={token}. - The URL is encoded into a QR code (PNG).
- The QR code is embedded into the first page of the PDF (PDF-lib).
- The SHA-256 hash is computed over the final QR-embedded PDF.
- The hash is signed with the organization's RSA private key:
crypto.createSign("SHA256")→ base64 signature (RSA with Node's default PKCS#1 v1.5 padding). - The final PDF is stored in the file-storage service.
- Metadata is recorded (title, filename, organization, hash, signature, token, status).
Ordering matters: the QR code is embedded before hashing and signing, so the cryptographic information corresponds to the final issued PDF.
Verification process
QR-code verification
- Verifier scans the QR code → URL containing the token.
- The token locates the document record and the issuing organization.
- The organization's public key and the stored PDF are retrieved.
- The required hash is computed and the digital signature verified with the public key; the document status is checked.
- A result is returned and the attempt is logged.
QR verification verifies the server-stored document associated with the token. The server does not receive a separate PDF from the scanner.
PDF-upload verification
- Verifier submits a PDF through the public interface.
- The server computes its SHA-256 hash and searches for an exact matching record.
- If matched: signature and status are verified.
- If not matched: fuzzy matching (TLSH) attempts to identify a possibly modified version (broad threshold, e.g.
diff < 300), refined by text extraction. - A result is returned and the attempt is logged.
Verification result states
The five modelled document states are:
- VERIFIED — corresponds to an issued document, cryptographic checks valid, document active (UI: "AUTHENTIC CREDENTIAL").
- REVOKED — identified but revoked by the issuing organization (UI: "REVOKED CREDENTIAL").
- TAMPERED_SIGNATURE — identified but the digital signature does not validate (UI: "TAMPERED / INVALID").
- MODIFIED_VERSION — no exact match but sufficiently similar to an issued document (UI: "MODIFIED VERSION DETECTED").
- NOT_FOUND — no corresponding issued document (UI: "NOT REGISTERED").
Separately, an operational failure to retrieve the stored document from storage surfaces as a STORAGE ERROR result (UI: "STORAGE ERROR"), rather than as one of the document states above.
QR token design
- Token = cryptographically random UUID (
crypto.randomUUID()). - Encoded as a URL (
{host}/verification?token={uuid}), not the document itself. - The QR code is an access mechanism, not the cryptographic proof — the proof is the hash + signature.
PDF processing
- PDF-lib loads the PDF, embeds the QR PNG on the first page, and saves the modified bytes.
- Only PDFs are accepted; non-PDF uploads are rejected.
- The hash is computed over the final QR-embedded PDF.
- Limitation: some PDF-processing tests used mocks, so not every aspect of real PDF processing was exercised.
Database model
PostgreSQL via Prisma. Nine major entities: User, Session, Account, Verification, Organization, Member, Invitation, SignedDocument, VerificationLog.
- Auth entities (
User,Session,Account,Verification) support authentication/session management. - Organization entities (
Organization,Member,Invitation) support membership and roles. SignedDocumentstores title, filename, organization, document hash, signature information, verification token, status, and a fuzzy hash.VerificationLogstores verification method, result, timestamp and request metadata; the verifier may be absent (public verification). One document → many logs.- The authentication
Verificationentity is deliberately separate from the application'sVerificationLog.
Security assumptions
- Organizations are authorized to issue the documents they register; supplied information is accurate.
- Cryptographic keys are properly protected; the server, database, storage and network are available during verification.
- Verifiers have a compatible device and internet access; submitted PDFs are processable by the implemented functions.
Known limitations
- PDF only — Word, spreadsheets, images are out of scope.
- No HSM / enterprise key management — private keys are protected in-application only.
- Verification depends on the service — if the service, DB or storage is unavailable, real-time verification is impossible.
- Requires internet connectivity.
- No external institutional database integration.
- DB stores references + metadata; PDFs live in the storage service; no large-scale archival.
- Crypto scope limited to SHA-256 + RSA.
- Not a formal audit or penetration test.
- Not all paths tested — large files, concurrency and rate-limiting need further evaluation.
- Test-coverage limits: frontend statement coverage 63.32%; some PDF tests mocked; no complete end-to-end signing→verification→revocation test.
Open decisions
Resolved (now documented from the implementation):
- Token design — random UUID in a verification URL.
- Hash/signing order — QR embedded before hashing and signing.
- Signature encoding — base64 RSA/SHA-256 signature (Node default PKCS#1 v1.5 padding).
- Key storage — private key encrypted with AES-256-GCM; public key stored plaintext.
Still open / not documented in the thesis:
- Whether PKCS#1 v1.5 padding is intentional or merely Node's default.
- Deployment / self-hosting model (the proposal suggested organizations could self-host; the implementation's deployment is not documented).
- TLS / transport assumptions.
- Duplicate / replay handling.
- Rate limiting.
- Key rotation, revocation and recovery.
Deployment
Not documented in the thesis. Known environment: Linux-based development, separate frontend and backend connected through the backend API, PostgreSQL, external file storage (UploadThing), and a verification host derived from the request origin. [TODO: document the actual deployment/staging/production setup.]
Backup / recovery
[TODO: not documented.] Recommendation from Chapter 5: organizations should maintain reliable document storage, backup and recovery procedures, because QR verification depends on retrieving the stored document associated with a verification record.
Incident response
[TODO: not documented.] Related Chapter 5 recommendations: regular security testing (especially after changes to auth, authorization, document processing or crypto), restrict access to private signing keys, and establish key backup, recovery and replacement procedures.
Future changes (Chapter 5 suggestions)
- Comprehensive end-to-end lifecycle testing (setup → invite → upload → QR → sign → verify → modify → revoke, in one flow).
- Performance and scalability evaluation (larger volumes, concurrent users).
- Advanced key management (rotation, revocation, secure recovery).
- Additional document formats.
- Usability evaluation.
- Additional security evaluation (compromised keys, unauthorized access, large files, concurrent verification).
Test evidence
- 128 automated tests passed — backend 53 (9 files), frontend 75 (10 files).
- Backend coverage: statements 78.18% · branches 74.14% · functions 76.00% · lines 78.18%.
- Frontend coverage: statements 63.32% · branches 63.49% · functions 53.73% · lines 65.00%.
Cryptographic tests (CT01–CT09, all passed): RSA key-pair generation; identical/different content hashing; valid signature verification; modified signed data fails; wrong public key fails; modified signature fails; private-key encrypt/decrypt; tampered ciphertext fails.
Document & authorization tests (DT01–DT09, all passed): validation rejection; non-PDF rejected; valid signing; signing without auth → 401; without membership / insufficient privileges → 403; missing organization keys rejected; unauthorized revocation rejected; authorized revocation succeeds.
Public verification tests (VT01–VT06, all passed): original → AUTHENTIC CREDENTIAL; revoked → REVOKED CREDENTIAL; modified → MODIFIED VERSION DETECTED; tampered → TAMPERED / INVALID; unregistered → NOT REGISTERED; storage failure → STORAGE ERROR.
Cross-organization key test: a document signed with one organization's private key could not be verified with another organization's public key.
Candidate next step
Turn VeriDoc into a Secure AI Document Verification Agent: receive a document → inspect → retrieve records → verify signatures → explain discrepancies → request evidence → produce a report.
Then attack it: PDF prompt injection, manipulated extraction, fake approvals, unauthorized tool calls, memory poisoning, exfiltration, approval bypass, confused-deputy. Document each as:
Attack → Vulnerability → Exploit → Impact → Mitigation → Test
That upgrades the signal from "I can build a web app" to "I understand how to secure an AI-enabled application."
Architecture decisions to record (ADRs)
Now that the decisions exist, capture the reasoning, not just the outcome:
001-rsa-signature-scheme— RSA-2048 / SHA-256, base64 signature, PKCS#1 v1.5 (Node default).002-document-hashing— SHA-256 over the final QR-embedded PDF; QR embedded before hashing.003-organization-key-management— per-organization RSA key pair; private key encrypted with AES-256-GCM.004-verification-token-design— random UUID in a verification URL; QR as access, not proof.005-self-hosting-model— still an open decision.
Goal of these docs: someone should be able to disagree with me without having to ask what I meant.