cloudflare/access-crl-worker-template
Publicmirrored fromhttps://github.com/cloudflare/access-crl-worker-template
index.js
135lines · modecode
| 1 | /** |
| 2 | * This worker script will handle loading a CRL and doing a cert revocation check |
| 3 | * |
| 4 | * You can force a refresh of the CRL by adding a `force-crl-refresh: 1` header to the original request |
| 5 | */ |
| 6 | |
| 7 | import * as asn1js from 'asn1js' |
| 8 | import { CertificateRevocationList } from 'pkijs' |
| 9 | |
| 10 | // The URL where your CRL is located. We will fetch it from here. |
| 11 | // Uncomment this line if you can't use wrangler >= 1.8.0 |
| 12 | // const CRL_URL = '<REPLACE_ME>' |
| 13 | |
| 14 | // The key we store your crl under in your namespace |
| 15 | const CRL_KV_KEY = `CRL_${btoa(CRL_URL)}` |
| 16 | |
| 17 | // Optional header that will force the worker to get an updated CRL |
| 18 | const FORCE_CRL_REFRESH_HEADER = 'force-crl-refresh' |
| 19 | |
| 20 | /** |
| 21 | * Worker entry point |
| 22 | */ |
| 23 | addEventListener('fetch', event => { |
| 24 | event.respondWith(handleRequest(event)) |
| 25 | }) |
| 26 | |
| 27 | /** |
| 28 | * Return a 403 response |
| 29 | */ |
| 30 | function forbidden() { |
| 31 | return new Response('client certificate was revoked', { status: 403 }) |
| 32 | } |
| 33 | |
| 34 | /** |
| 35 | * Helper function that converts a buffer to a hex string |
| 36 | * @param {*} inputBuffer |
| 37 | */ |
| 38 | function bufToHex(inputBuffer) { |
| 39 | let result = '' |
| 40 | for (const item of new Uint8Array(inputBuffer, 0, inputBuffer.byteLength)) { |
| 41 | const str = item.toString(16).toUpperCase() |
| 42 | if (str.length === 1) result += '0' |
| 43 | result += str |
| 44 | } |
| 45 | return result.trim() |
| 46 | } |
| 47 | |
| 48 | /** |
| 49 | * Fetchs a CRL list, parses out the serial numbers, and stores them into workers kv |
| 50 | */ |
| 51 | async function updateCRL() { |
| 52 | const crlResp = await fetch(CRL_URL) |
| 53 | if (crlResp.status == 200) { |
| 54 | const buf = await crlResp.arrayBuffer() |
| 55 | const asn1 = asn1js.fromBER(buf) |
| 56 | const crlSimpl = new CertificateRevocationList({ |
| 57 | schema: asn1.result, |
| 58 | }) |
| 59 | const newCRL = { |
| 60 | nextUpdate: crlSimpl.nextUpdate.value, |
| 61 | thisUpdate: crlSimpl.thisUpdate.value, |
| 62 | revokedSerialNumbers: crlSimpl.revokedCertificates.reduce( |
| 63 | (revokedSerialNums, cert) => { |
| 64 | let serialNum = bufToHex(cert.userCertificate.valueBlock.valueHex) |
| 65 | revokedSerialNums[serialNum] = true |
| 66 | return revokedSerialNums |
| 67 | }, |
| 68 | {}, |
| 69 | ), |
| 70 | } |
| 71 | CRL_NAMESPACE.put(CRL_KV_KEY, JSON.stringify(newCRL)) |
| 72 | return newCRL |
| 73 | } |
| 74 | throw new Error(`failed to fetch crl with status ${crlResp.status}`) |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Load a CRL from workers kv. Handles refreshing the crl as needed. |
| 79 | */ |
| 80 | async function loadCRL(event, forceCRLRefresh = false) { |
| 81 | // Force a refresh of the CRL list if needed |
| 82 | if (forceCRLRefresh) { |
| 83 | return await updateCRL() |
| 84 | } |
| 85 | |
| 86 | // attempt to get the CRL from workers kv first |
| 87 | let crl = await CRL_NAMESPACE.get(CRL_KV_KEY, 'json') |
| 88 | if (!crl) { |
| 89 | // the CRL wasn't in workers kv, so go fetch it from the source |
| 90 | crl = await updateCRL() |
| 91 | } |
| 92 | |
| 93 | // Check to see if we should refresh the CRL |
| 94 | const nextUpdate = Date.parse(crl.next_update) |
| 95 | const now = new Date() |
| 96 | if (now > nextUpdate) { |
| 97 | // it is time to update the CRL. Out of band send a request to update the workers kv key |
| 98 | event.waitUntil(updateCRL()) |
| 99 | } |
| 100 | |
| 101 | return crl |
| 102 | } |
| 103 | |
| 104 | async function handleRequest(event) { |
| 105 | try { |
| 106 | const request = event.request |
| 107 | // Ensure the request has the Cloudflare cf object and certificate headers and that the certificate was successfully presented |
| 108 | // If so, then check the CRL to see if the cert was revoked. |
| 109 | if ( |
| 110 | request.cf && |
| 111 | request.cf.tlsClientAuth && |
| 112 | request.cf.tlsClientAuth.certPresented && |
| 113 | request.cf.tlsClientAuth.certVerified === 'SUCCESS' |
| 114 | ) { |
| 115 | // Check to see if we were asked to force a CRL refresh |
| 116 | const forceCRLRefresh = request.headers.get(FORCE_CRL_REFRESH_HEADER) |
| 117 | ? true |
| 118 | : false |
| 119 | |
| 120 | // Load the crl |
| 121 | const crl = await loadCRL(event, forceCRLRefresh) |
| 122 | if (!crl) { |
| 123 | return new Response('failed to load CRL', { status: 500 }) |
| 124 | } |
| 125 | |
| 126 | // Check to see if the certificate the user presented is in the crl |
| 127 | if (crl.revokedSerialNumbers[request.cf.tlsClientAuth.certSerial]) { |
| 128 | return forbidden() |
| 129 | } |
| 130 | } |
| 131 | return await fetch(request) |
| 132 | } catch (e) { |
| 133 | return new Response(`failed to load CRL ${e}`, { status: 500 }) |
| 134 | } |
| 135 | } |