A simple, multi-stage, WebAssembly-based security framework to protect web APIs from automated threats.
Sigilus is a simple multi-stage, WebAssembly-based security framework designed to protect web application APIs from automated threats, bots, and tampering. It establishes a dynamic, short-lived cryptographic trust between the client and server, ensuring that API requests originate from a legitimate, unmodified frontend application.
Sigilus employs a two-stage initialization process to provision a client-side request-signing VM. This ensures that only clients capable of executing the initial challenge can receive the primary security module.
sequenceDiagram
autonumber
participant Client as Browser Client
participant Server as Server
participant Redis as Redis
Note over Client, Server: Stage 1: Proof of Execution & Provisioning
Client ->> Server: GET /sigilus.js
Server -->> Client: Dynamic JS Bundle (los.js, sigilus_client.js, etc.)
Client ->> Client: 1. Initialize Loom of Secrets VM (los.wasm)
Client ->> Client: 2. Execute bytecode → Generate "Woven Sigil" (Proof of Execution)
Client ->> Server: POST /sigilus/meta (Headers: X-Sigilus-Dynamic-Key, Chrono, Phantom)
Server ->> Server: 3. validate_woven_sigil() mirrors VM logic
Server ->> Redis: 4. Check if Woven Sigil is already used
alt Sigil is valid and unused
Server ->> Redis: Burn Woven Sigil (prevent replay)
Server -->> Client: 200 OK (Provisioning Data: PhantomID, Encrypted VM URL, Decryption Runes)
else Sigil is invalid or replayed
Server -->> Client: 403 Forbidden
end
Note over Client, Server: Stage 2: Main VM Operation (Request Signing)
Client ->> Server: 5. GET /.../frame.enc (Encrypted VM)
Server -->> Client: Encrypted WASM + Secret Seed
Client ->> Client: 6. Decrypt frame.enc using Runes
Client ->> Client: 7. Initialize Main Sigilus VM (sigilus_vm.wasm) with Secret Seed
loop For every protected API request
Client ->> Client: 8. Generate headers (Sigilus, OblivionSig, ChronoMark)
Client ->> Server: POST /api/data (Headers: Sigilus, PhantomID, etc.)
Server ->> Server: 9. Validate all headers
Server ->> Redis: 10. Check if PhantomID is already used
alt Request is valid and not replayed
Server ->> Redis: Burn PhantomID
Server -->> Client: 200 OK (API Data)
else Invalid or replayed request
Server -->> Client: 403 Forbidden
end
end
- WASM-Based Obfuscation: Moves critical security logic from easily readable JavaScript into a sandboxed, low-level WebAssembly binary, making reverse-engineering significantly more difficult.
- Mini-VM Execution: Both WASM modules are minimalist, custom-built virtual machines with a very small, purpose-built instruction set, making static analysis and instrumentation challenging.
- Multi-Stage VM Provisioning: A lightweight "Loom of Secrets" VM provides proof-of-execution before the server provisions the main, more powerful request-signing VM.
- Dynamic VM Encryption & Key Rotation: The main WASM VM is encrypted on the server with a daily-rotating key. The client decrypts it at runtime, preventing static analysis of the security module.
- Replay Attack Prevention: Utilizes a Redis-backed cache to "burn" unique identifiers (
PhantomIDfor requests,Woven Sigilfor provisioning), ensuring each cryptographic token is used only once. - Payload Integrity (OblivionSig): API request bodies are cryptographically signed using AES-GCM, preventing man-in-the-middle tampering between the client and server.
- Timestamp Validation (ChronoMark): Requests are timestamped and validated against a server-defined time-drift tolerance, mitigating simple record-and-playback attacks.
- Drop-in Integration: Provides simple integrators for Flask and Quart, along with an automatic JavaScript hook for
fetchandXMLHttpRequest, requiring minimal changes to existing application code. - Automated Build & Packing: Includes Python scripts to compile the JS bundle and to pack/encrypt the WASM frame, simplifying the development and deployment workflow.
- When the client first loads, it fetches the Sigilus JavaScript bundle, which includes the logic for a small "Loom of Secrets" WASM module (
los.wasm). - The client executes a predefined bytecode program (
rune_pattern.js) inside this module. This bytecode is processed by a minimalist, custom-built virtual machine with a very small instruction set. The opcodes are designed specifically for the required cryptographic operations (e.g., string manipulation, HMAC, Base64 encoding), with a few decoy instructions to hinder analysis. - The result is a Woven Sigil, a unique, short-lived token sent in the
X-Sigilus-Dynamic-Keyheader. This serves as proof that the client successfully executed the initial WASM challenge.
- The client sends the Woven Sigil to the
/sigilus/metaendpoint. - The server's
sigilus_vm_validator.pymeticulously simulates the exact same bytecode logic on the server side, mirroring the mini-VM's behavior. - If the server's calculated Sigil matches the client's submitted Sigil (and it hasn't been seen before), the server trusts the client.
- The server then responds with provisioning data, which includes:
- A server-authoritative
PhantomIDfor the session. - The URL for the main, encrypted VM (
frame.enc). - The necessary metadata ("runes") to decrypt it (date and salt).
- A server-authoritative
- The client fetches the encrypted
frame.enc. - Using the Web Crypto API, it derives the AES decryption key from the runes and decrypts the frame.
- The decrypted payload is a JSON object containing the main
sigilus_vm.wasmbinary (as a Base64 string) and the secretrune_seed(the HMAC key). - The client initializes the main Sigilus VM instance with this binary and securely stores the
rune_seedin memory.
- The
SigilusAutoHookautomatically intercepts all outgoingfetchorXHRcalls to protected routes. - For each request, it calls the main WASM VM to generate a fresh set of security headers:
ChronoMark: A current timestamp.Sigilus: An HMAC-based signature of the request path, PhantomID, and ChronoMark.OblivionSig: An AES-GCM encrypted signature of the JSON request body, preventing tampering.PhantomID: The persistent session identifier provided by the server.
- The server validates these headers on every request, checking signatures and using Redis to prevent
PhantomIDreuse.
Sigilus/
├── backend/
│ ├── sigilus_flask_integrator.py # Drop-in Flask support
│ ├── sigilus_quart_integrator.py # Drop-in Quart support
│ ├── sigilus_vm_validator.py # Server-side validation of the Woven Sigil
│ ├── phantom_cache.py # Redis/in-memory cache for replay protection
│ ├── phantom_core.py # Core Python crypto logic
│ ├── wasm_packer.py # Script to encrypt and pack the main VM
│ └── vm/
│ ├── los.cpp # C++ source for the Loom of Secrets VM
│ ├── sigilus_vm.cpp # C++ source for the main request-signing VM
│ └── wasm_dist/ # Compiled WASM binaries
│
└── frontend/
├── js/
│ ├── sigilus_client.js # Main client orchestrator
│ ├── sigilus_autohook.js # Intercepts fetch/XHR
│ ├── los.js # Emscripten wrapper for los.wasm
│ ├── sigilus_vm.js # Emscripten wrapper for sigilus_vm.wasm
│ ├── aes_decrypt.js # Web Crypto API decryption logic
│ ├── frame_loader.js # Logic to fetch and decrypt frame.enc
│ ├── rune_pattern.js # Bytecode for the Loom of Secrets
│ └── phantom.bundle.min.js # Final compiled JS bundle
│
├── wasm/
│ ├── frame.enc # The encrypted main VM package
│ └── runeset.json # Metadata for decrypting frame.enc
│
└── build_phantom_bundle.py # Script to build and minify the JS bundle
- Python 3.8+
- Node.js and
npx(for JS bundling/obfuscation) - Emscripten SDK (for compiling C++ to WASM)
- A running Redis server
-
Install dependencies:
pip install -r requirements.txt # requirements.txt should include: # Flask / Quart, python-dotenv, redis, pycryptodome, importlib-resources
-
Configure Environment: Create a
.envfile in your project root with the following keys. These must be strong, randomly generated secrets.# Secret for generating PhantomIDs and other server-side tokens SIGILUS_KEY=your_strong_random_32_char_secret_key # The HMAC secret seed embedded inside the encrypted main VM SIGILUS_SEED=another_strong_random_32_char_secret # (Optional) An API key for future administrative functions SIGILUS_API_KEY=your_admin_api_key # Your Redis connection string REDIS_URL_URI=redis://localhost:6379/0
This process must be run during your application's build/deployment pipeline.
-
Compile WASM Modules (if not pre-compiled): Navigate to
Sigilus/backend/vm/and use Emscripten to compilelos.cppandsigilus_vm.cppinto their respective.wasmand.jswrappers in thewasm_dist/directory. -
Pack the Main VM Frame: This script encrypts
sigilus_vm.wasmand theSIGILUS_SEEDintoframe.enc.# From your project root python -m Sigilus.backend.wasm_packerThis generates
Sigilus/frontend/wasm/frame.encandruneset.json. -
Build the JavaScript Bundle: This script concatenates, minifies, and optionally obfuscates all necessary JS files into a single bundle.
# From your project root, navigate to the frontend directory cd Sigilus/frontend/ # Basic build (minify only) and compress for serving python build_phantom_bundle.py --compress # For production (obfuscate, silence logs, and compress) python build_phantom_bundle.py --obs --silent --compress
# In your app.py
from quart import Quart, jsonify
from Sigilus.backend.sigilus_quart_integrator import SigilusQuartIntegrator
app = Quart(__name__)
# Initialize Sigilus and automatically protect all routes
# The integrator handles serving /sigilus.js, /sigilus/meta, and WASM assets.
sigilus = SigilusQuartIntegrator(app, protect_all_routes=True)
# Add the dynamic JS boot URL to your template context
@app.context_processor
def inject_sigilus():
return dict(sigilus_js_boot_url=sigilus.js_boot_url)
# This route is now automatically protected by Sigilus
@app.route("/api/v1/user", methods=["POST"])
async def update_user():
data = await request.get_json()
# Your API logic here...
return jsonify({"status": "ok", "user_id": data.get("id")})
# To protect a specific route, set protect_all_routes=False
# and use the decorator.
# sigilus_manual = SigilusQuartIntegrator(app, protect_all_routes=False)
# @app.route("/api/protected")
# @sigilus_manual.sigilus_guard
# async def protected_endpoint():
# return jsonify({"secret": "data"})Note: The sigilus_flask_integrator provides an identical API for Flask applications.
In your main HTML template (e.g., index.html), simply include the script tag pointing to the dynamic JS endpoint.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Secure App</title>
</head>
<body>
<h1>Welcome</h1>
<script src="{{ sigilus_js_boot_url }}"></script>
<script src="/path/to/your/app.js"></script>
</body>
</html>The Sigilus client will initialize automatically. The SigilusAutoHook will intercept fetch and XMLHttpRequest calls, adding the necessary security headers.
For advanced use cases where you need to ensure Sigilus is ready before making a manual request, you can use the ready() promise:
// in your app.js
async function doSomethingSecure() {
try {
await SigilusClient.ready(); // Wait for initialization to complete
const response = await fetch('/api/v1/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 123, name: 'Alice' })
});
const data = await response.json();
console.log('Server response:', data);
} catch (error) {
console.error('Sigilus failed to initialize or request failed:', error);
}
}
doSomethingSecure();This project is licensed under the MIT License - see the LICENSE.md file for details.
