├── 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 |
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 |
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 |
--------------------------------------------------------------------------------