├── functions ├── _middleware.ts └── api │ ├── _middleware.ts │ ├── submit.js │ ├── list.js │ └── verifiers.js ├── public ├── verifier.html └── index.html ├── .gitignore ├── .github └── workflows │ └── publish.yml ├── package.json └── README.md /functions/_middleware.ts: -------------------------------------------------------------------------------- 1 | import honeycombPlugin from "@cloudflare/pages-plugin-honeycomb"; 2 | 3 | export const onRequest: PagesFunction<{ 4 | HONEYCOMB_API_KEY: string 5 | HONEYCOMB_DATASET: string 6 | }> = (context) => { 7 | return honeycombPlugin({ 8 | apiKey: context.env.HONEYCOMB_API_KEY, 9 | dataset: context.env.HONEYCOMB_DATASET, 10 | })(context); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /public/verifier.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Verifier 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /npm-shrinkwrap.json.ember-try 27 | /package.json.ember-try 28 | /package-lock.json.ember-try 29 | /yarn.lock.ember-try 30 | 31 | # broccoli-debug 32 | /DEBUG/ 33 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | jobs: 4 | publish: 5 | runs-on: ubuntu-latest 6 | permissions: 7 | contents: read 8 | deployments: write 9 | name: Publish to Cloudflare Pages 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Build 14 | run: npm install 15 | 16 | - name: Publish to Cloudflare Pages 17 | uses: cloudflare/pages-action@v1 18 | with: 19 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 20 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT }} 21 | projectName: ${{ secrets.CLOUDFLARE_PAGES }} 22 | directory: public 23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "verifier-key-rcv", 3 | "version": "0.0.22", 4 | "description": "can cloudflare act as a proxy to harvest public key used by pkcs1 verify?", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/patterns/verifier-key-rcv.git" 12 | }, 13 | "keywords": [ 14 | "verifier" 15 | ], 16 | "author": "興怡", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/patterns/verifier-key-rcv/issues" 20 | }, 21 | "homepage": "https://github.com/patterns/verifier-key-rcv#readme", 22 | "dependencies": { 23 | "@cloudflare/pages-plugin-honeycomb": "^1.0.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # verifier-key-rcv 2 | 3 | Purpose, harvest the public key specified by `keyId`. 4 | Cache the public key up to 48hr in CF KV namespace. 5 | 6 | 7 | 8 | ## Credits 9 | HTML [min/no JS](https://developers.cloudflare.com/pages/tutorials/forms/) 10 | 11 | KV [plain JS](https://developers.cloudflare.com/workers/tutorials/build-a-jamstack-app/) 12 | 13 | Astro [for CF Pages](https://docs.astro.build/en/guides/deploy/cloudflare/#using-pages-functions) 14 | 15 | Hono [pages-stack](https://github.com/honojs/examples/tree/main/pages-stack) 16 | 17 | D1 [worker in Hono](https://blog.cloudflare.com/making-static-sites-dynamic-with-cloudflare-d1/) 18 | 19 | Honeycomb [pages plugin](https://developers.cloudflare.com/pages/platform/functions/plugins/honeycomb/) 20 | 21 | 22 | -------------------------------------------------------------------------------- /functions/api/_middleware.ts: -------------------------------------------------------------------------------- 1 | import type { PluginData } from "@cloudflare/pages-plugin-honeycomb"; 2 | 3 | export const onRequest: PagesFunction = async ({ 4 | data, 5 | next, 6 | request 7 | }) => { 8 | // debug trace input fields (of verifier that we're proxying for the consumer) 9 | data.honeycomb.tracer.addData({ 10 | method: request.method, 11 | url: request.url, 12 | body: request.body, 13 | host: request.headers.get('host'), 14 | date: request.headers.get('date'), 15 | contenttype: request.headers.get('content-type'), 16 | digest: request.headers.get('digest'), 17 | contentlength: request.headers.get('content-length'), 18 | signature: request.headers.get('signature'), 19 | }); 20 | return next(); 21 | }; 22 | 23 | -------------------------------------------------------------------------------- /functions/api/submit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * POST /api/submit 3 | */ 4 | export async function onRequestPost(context) { 5 | try { 6 | let input = await context.request.formData(); 7 | 8 | // Convert FormData to JSON 9 | // NOTE: Allows multiple values per key 10 | let output = {}; 11 | for (let [key, value] of input) { 12 | let tmp = output[key]; 13 | if (tmp === undefined) { 14 | output[key] = value; 15 | } else { 16 | output[key] = [].concat(tmp, value); 17 | } 18 | } 19 | 20 | let pretty = JSON.stringify(output, null, 2); 21 | return new Response(pretty, { 22 | headers: { 23 | 'Content-Type': 'application/json;charset=utf-8', 24 | }, 25 | }); 26 | } catch (err) { 27 | return new Response('Error parsing JSON content', { status: 400 }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /functions/api/list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GET /api/list 3 | */ 4 | export async function onRequestGet(context) { 5 | try { 6 | ////let input = await context.request.formData(); 7 | const KV = context.env.VERIFIER_KEYS; 8 | const list = await KV.list({ limit: 10 }); 9 | if (!list) { 10 | return new Response('zero data list', { status: 400 }); 11 | } 12 | 13 | let output = {}; 14 | for (let [key, value] of list) { 15 | let tmp = output[key]; 16 | if (tmp === undefined) { 17 | output[key] = value; 18 | } else { 19 | output[key] = [].concat(tmp, value); 20 | } 21 | } 22 | 23 | 24 | let pretty = JSON.stringify(output, null, 2); 25 | return new Response(pretty, { 26 | headers: { 27 | 'Content-Type': 'application/json;charset=utf-8', 28 | }, 29 | }); 30 | } catch (err) { 31 | return new Response('Error parsing JSON content', { status: 400 }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Form Demo 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 | 19 |
20 | 21 | 29 |
30 | 31 |
32 | 33 |
    34 |
  • 35 | 36 | 37 |
  • 38 |
  • 39 | 40 | 41 |
  • 42 |
  • 43 | 44 | 45 |
  • 46 |
  • 47 | 48 | 49 |
  • 50 |
51 |
52 | 53 | 54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /functions/api/verifiers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * POST /api/verifiers 3 | */ 4 | export async function onRequestPost(context) { 5 | 6 | /** 7 | * gatherResponse awaits and returns a response body as a string. 8 | * Use await gatherResponse(..) in an async function to get the response body 9 | * @param {Response} response 10 | */ 11 | async function gatherResponse(response) { 12 | const { headers } = response; 13 | const contentType = headers.get("content-type") || ""; 14 | if (contentType.includes("application/json")) { 15 | return JSON.stringify(await response.json()); 16 | } 17 | return response.text(); 18 | } 19 | 20 | try { 21 | //TODO refactor to use HMAC after e2e 22 | const hdr_field = context.env.PRESHARED_AUTH_HEADER_KEY; 23 | const psk = context.request.headers.get(hdr_field); 24 | if (psk !== context.env.PRESHARED_AUTH_HEADER_VALUE) { 25 | return new Response('Required auth value', { status: 403 }); 26 | } 27 | 28 | let input = await context.request.json(); 29 | if (!input.locator) { 30 | return new Response('Missing locator input field', { status: 400 }); 31 | } 32 | // TypeError exception is thrown on invalid URLs 33 | const destination = new URL(input.locator); 34 | 35 | // use the SHA256 sum as our internal sequence 36 | const enc = new TextEncoder().encode(destination.href); 37 | const sum = await crypto.subtle.digest({name: 'SHA-256'}, enc); 38 | const internal_seq = btoa(String.fromCharCode(...new Uint8Array(sum))); 39 | 40 | // prefer local copy and saving a trip 41 | const KV = context.env.VERIFIER_KEYS; 42 | const from_cache = await KV.get(internal_seq); 43 | if (from_cache != null) { 44 | // TODO short-circuit when wrong format from public key (save consumer grief) 45 | return new Response(from_cache, { 46 | headers:{'Content-Type': 'application/json;charset=utf-8'}, 47 | }); 48 | } 49 | 50 | // retrieve a fresh version (as specified by keyId) 51 | const response = await fetch(destination, { 52 | headers: {'Accept': 'application/activity+json'}, 53 | cf: {cacheTtl: 5, cacheEverything: true}, 54 | }); 55 | const results = await gatherResponse(response); 56 | 57 | // keep a local copy of the public key 58 | await KV.put(internal_seq, results, {expirationTtl: 3600}); 59 | 60 | // pass back fresh (keyId) to the consumer 61 | return new Response(results, { 62 | headers: { 63 | 'Content-Type': 'application/json;charset=utf-8', 64 | }, 65 | }); 66 | } catch (err) { 67 | return new Response('Error parsing JSON content', { status: 400 }); 68 | } 69 | } 70 | --------------------------------------------------------------------------------