X.509 Encryption Service
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
│ POST /api/encrypt { plaintext }
└──► RSA-OAEP encrypt → ciphertext (base64)
[ /api/decrypt ] ──► Load server private key
└──► RSA-OAEP decrypt → plaintext
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.
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:
- RSA encrypts small payloads only. RSA-OAEP-2048 with SHA-256 has a hard message-size limit of ~190 bytes. Real systems use hybrid encryption (encrypt a symmetric key with RSA, encrypt the bulk payload with AES). I built that pattern in the MQTT gateway; here I deliberately kept it simple to focus on PKI.
- Self-signed CA is not trusted by anyone. The certificate chain is structurally complete but no public trust store knows the CA. That's fine for a demo; not for a real system.
- Private key lives in the runtime. The server private key is loaded into the function's environment. For real deployments, key material should live in a managed KMS (GCP KMS, AWS KMS, HashiCorp Vault) — never in deployment artefacts.
What I Took From It
Three things this project taught me concretely, beyond what I'd have picked up reading the specs:
- Reading X.509 by eye — knowing which OIDs are which, what the Basic Constraints extension does, why Subject and Issuer matter to chain validation.
- Why every modern crypto library defaults to OAEP, and why "PKCS#1 v1.5 is fine for our use case" is a phrase that should make a security reviewer pause.
- The mechanics of bundling ESM Node code into serverless targets — a problem a lot of engineers hit, very few document, and which my next deeper crypto-platform projects benefited from solving once.
Try it: encryption.yaqoobahmed.com · the encrypt/decrypt endpoints round-trip in the browser.