cd ..

X.509 Encryption Service

Live: encryption.yaqoobahmed.com | Runtime: Netlify Functions (Node.js, ESM) | Role: Cryptography / Serverless
Node.js (ESM) Netlify Functions X.509 (self-signed CA) RSA-OAEP node-forge Live deployment

Most engineers consume PKI: they accept a server certificate, trust a TLS handshake, and call it a day. I wanted to fully understand it. So I built a serverless service that issues its own certificate authority, signs a server certificate from it, and uses the resulting RSA keypair to do application-layer encryption on top of TLS — end-to-end, deployable, live in production.

The service exposes two endpoints — /api/encrypt and /api/decrypt — running on Netlify Functions. The interesting parts are below the surface: the X.509 chain construction, the RSA-OAEP padding choice, and the ESM/CommonJS interop required to bundle modern Node into a serverless target.

Architecture

Client
[ Browser / curl ]
    │ POST /api/encrypt  { plaintext }
Edge Function (Netlify)
[ /api/encrypt ]  ──►  Load server public key (X.509)
                      └──►  RSA-OAEP encrypt → ciphertext (base64)
[ /api/decrypt ]  ──►  Load server private key
                      └──►  RSA-OAEP decrypt → plaintext
PKI
[ Self-signed CA ]  ──►  [ Server certificate (signed) ]  ──►  [ RSA keypair ]
                     Generated locally · stored in environment · loaded at function init

Why Build It

This isn't a project I built because someone asked. I wanted to understand X.509 well enough to read a certificate without a parser — to know which OIDs map to which fields, why RSA-OAEP is preferred over PKCS#1 v1.5, and what serverless bundlers do to ESM imports under the hood. The fastest way to learn that is to build the smallest possible system that exercises every layer.

Public Key Infrastructure From Scratch

I generated a self-signed CA with a 2048-bit RSA keypair, used it to sign a server certificate, and then used the server's private key for application-layer encryption. The CA → server chain is the same conceptual structure as the public web's CA → intermediate → leaf chain — just with one less layer. Building it manually made the chain validation logic in the TLS Certificate Chain Inspector click in a way reading the spec alone hadn't.

RSA-OAEP, Not PKCS#1 v1.5

RSA encryption requires padding. The classical option is PKCS#1 v1.5; the modern option is OAEP (Optimal Asymmetric Encryption Padding). PKCS#1 v1.5 has known padding-oracle vulnerabilities — Bleichenbacher's attack and its variants demonstrated repeatedly that a network adversary observing decryption error responses can recover plaintext. OAEP closes that class of attack by design.

Decision: Use RSA-OAEP with SHA-256 as the hash function, even though PKCS#1 v1.5 would have been simpler to implement. Reason: cryptographic primitives have a one-way ratchet — the moment you build something with a deprecated mode, replacing it later requires coordinating with every client. Pick the right mode the first time.

ESM in a Bundled Serverless Target

Netlify Functions historically run CommonJS. The codebase wants to be ESM (import / export, top-level await, type: "module" in package.json). Bundling ESM into a serverless target reveals subtle pain: import.meta.url resolves differently in dev vs. bundled output; __dirname doesn't exist; node-forge's CommonJS exports need the right import shape.

I solved it with a small path-resolution shim that works in both ESM and bundled contexts, and structured the imports so node-forge loads cleanly through the bundler without needing require() escape hatches.

API

Encrypt

POST /api/encrypt

Request:

{ "plaintext": "hello world" }

Response:

{ "ciphertext": "<base64 RSA-OAEP ciphertext>", "algorithm": "RSA-OAEP", "hash": "SHA-256" }

Decrypt

POST /api/decrypt

Accepts the ciphertext returned from /encrypt, returns the original plaintext. Stateless — the server doesn't remember anything between calls.

Limitations Acknowledged

This is a learning artefact, not production cryptography for a real workload:

What I Took From It

Three things this project taught me concretely, beyond what I'd have picked up reading the specs:

Try it: encryption.yaqoobahmed.com · the encrypt/decrypt endpoints round-trip in the browser.