Skip to content

About

LedgerLock is a secure, multi-tenant SaaS application designed to provide isolated tenant environments, secure authentication, role-based access control, and tamper-evident audit logging. The system integrates a hash-linked ledger and Merkle-based verification mechanisms to ensure data integrity and detect unauthorized modifications.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

README.md

LedgerLock

LedgerLock is a secure, multi-tenant SaaS application designed to provide isolated tenant environments, secure authentication, role-based access control, and tamper-evident audit logging. The system integrates a hash-linked ledger and Merkle-based verification mechanisms to ensure data integrity and detect unauthorized modifications.

Secure Multi-Tenant SaaS with a Verifiable Audit Ledger

A multi-tenant SaaS application whose audit log is tamper-evident. Access control decides who may read a record; the ledger decides whether anyone can quietly rewrite the record of what happened. Those are different problems, and only the second one needs a chain.


The idea in one paragraph

Every SaaS product has an audit log, and every audit log has the same weakness: it lives in the provider's database, and the provider's own administrator can edit it. No amount of tenant isolation, RBAC, or encryption fixes that, because the attacker in this threat model is the person the access control answers to. Here, every security event is hashed, batched into a Merkle tree, and sealed into a block that commits to the hash of the block before it. Rewriting one row now breaks that block's Merkle root and every block hash after it, and the application will say exactly which event stopped matching.


Running it

pip install -r requirements.txt
python app.py

Runs on macOS, Windows, and Linux. Pure Python plus SQLite, no external services, no network calls, no wallet, no gas.

Tests

pytest test_app.py -v

67 tests. Each gets its own throwaway database, so the suite is safe to run repeatedly and in any order.

Demo accounts

Account Workspace Role
alice@acme.com Acme Corporation admin
carol@acme.com Acme Corporation user
dave@globex.com Globex Inc. admin
bob@globex.com Globex Inc. user

Password for all four: 1234.


What's in the box

Layer What it does
Tenant isolation Every query is filtered by the tenant_id inside the verified JWT, never by anything the client sends
Record ownership You may edit what you created; admins may edit anything in their own workspace
RBAC Deleting records, listing teammates, and sealing blocks are admin-only, and denials are themselves logged
Rate limiting Per-IP and per-account throttling on sign-in and tenant creation
Input validation Pydantic models reject bad input with 422 before it reaches the database
Verifiable ledger Events are hashed, Merkle-batched, proof-of-worked, and hash-linked into blocks
Inclusion proofs Any event yields a standalone proof that can be re-checked without this server
Notary The browser hashes a file locally and sends only the digest, which is sealed into the ledger
Tamper detection Verification recomputes every leaf and root from current data, and names the exact corrupted event

Demo script (about five minutes)

Run through this in order. The last three steps are the ones that distinguish this from an ordinary audit table.

  1. Sign in as alice@acme.com. The sign-in page shows the real chain: block heights and hashes read live from /api/ledger/headers, an endpoint that needs no authentication because block headers carry no tenant data.

  2. Create a record. Watch it appear in the audit trail as a pending event with its leaf hash already computed.

  3. Show isolation. Open a private window, sign in as bob@globex.com, and confirm Acme's records are invisible. In /docs, try fetching Alice's record ID with Bob's token: 404, not 403, because the record does not exist as far as Bob's tenant is concerned.

  4. Show RBAC. Sign in as carol@acme.com and click Team. She's refused, and the refusal is written to her own audit trail as RBAC_DENIED.

  5. Notarise a file. Drop any file onto the Notary screen. Point out that the file never uploads — the browser computes the SHA-256 with the Web Crypto API and only the 64-character digest is sent. Then edit one character of the file and check it again: no match.

  6. Open a proof. Click any sealed event in the audit trail. The drawer shows the leaf hash, the sibling path, the Merkle root, and the block hash. Note that other tenants' leaves in the same block stay opaque hashes — a proof reveals membership and nothing else.

  7. Break the log. On the Ledger screen, click Tamper with the log. This runs a plain UPDATE against audit_logs and deliberately does not touch the ledger — exactly what an administrator with database access would do.

  8. Catch it. The integrity pill in the header flips to Ledger broken and the overview names the event, the block, the hash that was sealed, and the hash the row produces now. Click Undo tampering to restore.

For step 7 you can also do it by hand, which is more convincing than a button:

sqlite3 saas.db "UPDATE audit_logs SET details='Payment declined' WHERE id=5;"

Then re-verify in the app.


How the ledger works

Leaf. Each audit event is serialised to canonical JSON (sorted keys, no incidental whitespace) and hashed with SHA-256. The row's autoincrement id is inside the hash, so two identical events never produce the same leaf.

Tree. Pending leaves are folded into a Merkle tree, pairing an odd node with itself at each level. The root commits to every leaf, so changing, inserting, or deleting any event changes the root.

Block. The block header holds the index, the previous block's hash, the Merkle root, the event count, the seal timestamp, and a nonce. It is mined until the hash starts with four zeroes. The timestamp is fixed before mining, so the nonce is the only free variable.

Chain. Every block commits to the previous block's hash, starting from a genesis block sealed at first startup. Rewriting block n breaks blocks n+1 onward.

Verification. /api/ledger/verify re-derives everything from the current database: it recomputes each leaf from the row as it stands now, rebuilds each Merkle root, re-hashes each header, and re-checks each link. Nothing is read from a cached "valid" flag — that's what makes the check meaningful.

Why tenants can share a block

A block stores hashes and nothing else. A tenant holding an inclusion proof learns that their own event is in the tree and learns nothing about the other leaves. That's why /api/ledger/blocks/{index} expands the caller's own events and returns everyone else's as bare hashes, and why block headers can be served unauthenticated.


What this is, and what it is not

Stating the gap is a strength in the write-up, not a weakness, so it is worth saying out loud in the presentation:

  • It is a permissioned, single-writer ledger. One server writes blocks. Proof-of-work here demonstrates the mechanism and paces sealing; it is not a Sybil defence, because there are no competing miners.
  • The last bit of trust has not been removed. An attacker who edits audit_logs and re-seals every block from that point forward would produce a self-consistent chain. What stops that in a real deployment is publishing block hashes somewhere the provider does not control. chain.anchor_receipt() is the single function where that happens; it currently returns a local receipt and says so. The two real options are an RFC 3161 timestamp from an accredited authority (cheap, court-recognised, still trusts the authority) or a transaction writing the block hash to a public chain (no trusted third party, costs gas, needs key custody). Nothing else in the system changes either way.
  • It is a single process. The rate limiter is in-memory and SQLite is the store, so neither survives a restart or scales across workers. In production these would be a shared cache and a managed database.
  • Auth is hand-rolled JWT plus PBKDF2 rather than a managed identity provider.

Files

app.py               FastAPI backend: auth, tenancy, records, notary, ledger API
chain.py             The ledger: canonical hashing, Merkle trees, sealing, verification
templates/
  login.html         Sign-in and workspace creation, with the live chain spine
  dashboard.html     Overview, records, notary, audit trail, ledger explorer, team, account
test_app.py          67 tests across access control and the ledger
requirements.txt     Pinned dependencies

chain.py has no dependency on FastAPI, SQLite, or the web layer. It is pure functions over hashes, which is why the last seven tests in the suite can exercise it directly.


API surface

Method Path Notes
POST /api/login Rate limited
POST /api/register Creates a tenant and its first admin
POST /api/auth/change-password
GET /api/me
GET /api/users Admin only
GET /api/dashboard Tenant-scoped stats plus ledger status
GET/POST /api/records
GET/PUT/DELETE /api/records/{id} Edit needs ownership or admin; delete needs admin
GET /api/audit Tenant-scoped, with leaf hashes and block numbers
POST /api/notary/anchor Accepts a digest, never a file
GET /api/notary/documents
POST /api/notary/verify Returns the inclusion proof
GET /api/ledger/status
GET /api/ledger/headers Unauthenticated by design
GET /api/ledger/blocks
GET /api/ledger/blocks/{index} Own events expanded, others as hashes
POST /api/ledger/seal Admin only
GET /api/ledger/verify Recomputes the whole chain
GET /api/ledger/proof/{event_id} Standalone inclusion proof
POST /api/ledger/demo/tamper Admin only, demo
POST /api/ledger/demo/restore Admin only, demo
GET /api/health Unauthenticated

About

LedgerLock is a secure, multi-tenant SaaS application designed to provide isolated tenant environments, secure authentication, role-based access control, and tamper-evident audit logging. The system integrates a hash-linked ledger and Merkle-based verification mechanisms to ensure data integrity and detect unauthorized modifications.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages