From 650430660e4a65b8b998dd1ee30d753e383438d4 Mon Sep 17 00:00:00 2001 From: Benjamin Goering <171782+gobengo@users.noreply.github.com> Date: Thu, 8 Dec 2022 14:50:23 -0800 Subject: [PATCH 1/9] access-api uses DID env variable when building its ucanto server signer id --- packages/access-api/src/bindings.d.ts | 5 +++ packages/access-api/src/config.js | 37 +++++++++++++++++ packages/access-api/src/utils/context.js | 7 ++-- packages/access-api/test/config.test.js | 28 +++++++++++++ packages/access-api/test/helpers/context.js | 44 ++++++++++++++++----- packages/access-api/test/ucan.test.js | 25 ++++++++++++ 6 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 packages/access-api/test/config.test.js diff --git a/packages/access-api/src/bindings.d.ts b/packages/access-api/src/bindings.d.ts index 50adbe61c..38c2de5d7 100644 --- a/packages/access-api/src/bindings.d.ts +++ b/packages/access-api/src/bindings.d.ts @@ -23,6 +23,11 @@ export interface Env { // vars ENV: string DEBUG: string + /** + * publicly advertised decentralized identifier of the running api service + * * this may be used to filter incoming ucanto invocations + */ + DID: string // secrets PRIVATE_KEY: string SENTRY_DSN: string diff --git a/packages/access-api/src/config.js b/packages/access-api/src/config.js index afa519ae2..8f332ca38 100644 --- a/packages/access-api/src/config.js +++ b/packages/access-api/src/config.js @@ -1,3 +1,5 @@ +import { Signer } from '@ucanto/principal/ed25519' + /** * Loads configuration variables from the global environment and returns a JS object * keyed by variable names. @@ -49,6 +51,8 @@ export function loadConfig(env) { // eslint-disable-next-line no-undef COMMITHASH: ACCOUNT_COMMITHASH, + DID: env.DID, + // bindings METRICS: /** @type {import("./bindings").AnalyticsEngine} */ ( @@ -105,3 +109,36 @@ export function createAnalyticsEngine() { _store: store, } } + +/** + * Given a config, return a ucanto Signer object representing the service + * + * @param {object} config + * @param {string} [config.DID] - public identifier of the running service. e.g. a did:key or a did:web + * @param {string} config.PRIVATE_KEY - multiformats private key of primary signing key + */ +export function configureSigner(config) { + const signer = Signer.parse(config.PRIVATE_KEY) + if (config.DID) { + if (!isDID(config.DID)) { + throw new Error(`Invalid DID: ${config.DID}`) + } + return signer.withDID(config.DID) + } + return signer +} + +/** + * Return whether or not the provided object looks like a decentralized identifier (aka DID) + * + * @see https://www.w3.org/TR/did-core/#did-syntax + * @param {any} object + * @returns {object is `did:${string}:${string}`} + */ +function isDID(object) { + if (typeof object !== 'string') return false + const parts = object.split(':') + if (parts.length <= 2) return false + if (parts[0] !== 'did') return false + return true +} diff --git a/packages/access-api/src/utils/context.js b/packages/access-api/src/utils/context.js index 747889c57..2e11e7744 100644 --- a/packages/access-api/src/utils/context.js +++ b/packages/access-api/src/utils/context.js @@ -1,8 +1,7 @@ -import { Signer } from '@ucanto/principal/ed25519' import { Logging } from '@web3-storage/worker-utils/logging' import Toucan from 'toucan-js' import pkg from '../../package.json' -import { loadConfig } from '../config.js' +import { configureSigner, loadConfig } from '../config.js' import { Spaces } from '../kvs/spaces.js' import { Validations } from '../kvs/validations.js' import { Email } from './email.js' @@ -42,12 +41,12 @@ export function getContext(request, env, ctx) { env: config.ENV, }) - const keypair = Signer.parse(config.PRIVATE_KEY) + const signer = configureSigner(config) const url = new URL(request.url) const db = new D1QB(config.DB) return { log, - signer: keypair, + signer, config, url, kvs: { diff --git a/packages/access-api/test/config.test.js b/packages/access-api/test/config.test.js new file mode 100644 index 000000000..48b2b3d8d --- /dev/null +++ b/packages/access-api/test/config.test.js @@ -0,0 +1,28 @@ +import assert from 'assert' +import * as configModule from '../src/config.js' + +describe('@web3-storage/access-api/src/config configureSigner', () => { + it('configureSigner creates a signer using config.{DID,PRIVATE_KEY}', async () => { + const config = { + PRIVATE_KEY: + 'MgCYWjE6vp0cn3amPan2xPO+f6EZ3I+KwuN1w2vx57vpJ9O0Bn4ci4jn8itwc121ujm7lDHkCW24LuKfZwIdmsifVysY=', + DID: 'did:web:example.com', + } + const signer = configModule.configureSigner(config) + assert.ok(signer) + assert.equal(signer.did().toString(), config.DID) + }) + it('configureSigner infers did from config.PRIVATE_KEY when config.DID is omitted', async () => { + const testKey = { + PRIVATE_KEY: + 'MgCYWjE6vp0cn3amPan2xPO+f6EZ3I+KwuN1w2vx57vpJ9O0Bn4ci4jn8itwc121ujm7lDHkCW24LuKfZwIdmsifVysY=', + didKey: 'did:key:z6MkqBzPG7oNu7At8fktasQuS7QR7Tj7CujaijPMAgzdmAxD', + } + const config = { + PRIVATE_KEY: testKey.PRIVATE_KEY, + } + const signer = configModule.configureSigner(config) + assert.ok(signer) + assert.equal(signer.did().toString(), testKey.didKey) + }) +}) diff --git a/packages/access-api/test/helpers/context.js b/packages/access-api/test/helpers/context.js index b59b4f073..fb6ae1465 100644 --- a/packages/access-api/test/helpers/context.js +++ b/packages/access-api/test/helpers/context.js @@ -14,20 +14,46 @@ dotenv.config({ path: path.join(__dirname, '..', '..', '..', '..', '.env.tpl'), }) -export const bindings = { - ENV: 'test', - DEBUG: 'false', - PRIVATE_KEY: process.env.PRIVATE_KEY || '', - POSTMARK_TOKEN: process.env.POSTMARK_TOKEN || '', - SENTRY_DSN: process.env.SENTRY_DSN || '', - LOGTAIL_TOKEN: process.env.LOGTAIL_TOKEN || '', - W3ACCESS_METRICS: createAnalyticsEngine(), +/** + * @typedef {Omit} AccessApiBindings - bindings object expected by access-api workers + */ + +/** + * Given a map of environment vars, return a map of bindings that can be passed with access-api worker invocations. + * + * @param {{ [key: string]: string | undefined }} env - environment variables + * @returns {AccessApiBindings} - env bindings expected by access-api worker objects + */ +function createBindings(env) { + return { + ENV: 'test', + DEBUG: 'false', + DID: env.DID || '', + PRIVATE_KEY: env.PRIVATE_KEY || '', + POSTMARK_TOKEN: env.POSTMARK_TOKEN || '', + SENTRY_DSN: env.SENTRY_DSN || '', + LOGTAIL_TOKEN: env.LOGTAIL_TOKEN || '', + W3ACCESS_METRICS: createAnalyticsEngine(), + } } +/** + * Good default bindings useful for tests - configured via process.env + */ +export const bindings = createBindings(process.env) + export const serviceAuthority = Signer.parse(bindings.PRIVATE_KEY) -export async function context() { +/** + * @param {object} [options] + * @param {Record} options.environment - environment variables to use when configuring access-api. Defaults to process.env. + */ +export async function context(options) { + const environment = options?.environment || process.env const principal = await Signer.generate() + const bindings = createBindings({ + ...environment, + }) const mf = new Miniflare({ packagePath: true, wranglerConfigPath: true, diff --git a/packages/access-api/test/ucan.test.js b/packages/access-api/test/ucan.test.js index 09d7a9dc8..498dadecc 100644 --- a/packages/access-api/test/ucan.test.js +++ b/packages/access-api/test/ucan.test.js @@ -144,6 +144,31 @@ describe('ucan', function () { t.deepEqual(rsp, ['test pass']) }) + test('should support ucan invoking to a did:web aud', async function () { + const serviceDidWeb = 'did:web:web3.storage' + const { mf, issuer, service } = await context({ + environment: { + ...process.env, + PRIVATE_KEY: + 'MgCYWjE6vp0cn3amPan2xPO+f6EZ3I+KwuN1w2vx57vpJ9O0Bn4ci4jn8itwc121ujm7lDHkCW24LuKfZwIdmsifVysY=', + DID: serviceDidWeb, + }, + }) + const ucan = await UCAN.issue({ + issuer, + audience: service.withDID('did:web:web3.storage'), + capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], + }) + const res = await mf.dispatchFetch('http://localhost:8787/raw', { + method: 'POST', + headers: { + Authorization: `Bearer ${UCAN.format(ucan)}`, + }, + }) + const rsp = await res.json() + t.deepEqual(rsp, ['test pass']) + }) + test('should handle exception in route handler', async function () { const { mf, service, issuer } = ctx From a4785ddba774927fd00a988c5f2636a3ac5f887c Mon Sep 17 00:00:00 2001 From: Benjamin Goering <171782+gobengo@users.noreply.github.com> Date: Thu, 8 Dec 2022 15:04:47 -0800 Subject: [PATCH 2/9] access-api add test ensuring error when non-did config.DID provided to configureSigner --- packages/access-api/test/config.test.js | 43 ++++++++++++++++++------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/access-api/test/config.test.js b/packages/access-api/test/config.test.js index 48b2b3d8d..ab95ce102 100644 --- a/packages/access-api/test/config.test.js +++ b/packages/access-api/test/config.test.js @@ -1,28 +1,47 @@ import assert from 'assert' import * as configModule from '../src/config.js' +/** keypair that can be used for testing */ +const testKeypair = { + private: { + /** + * Private key encoded as multiformats + */ + multiformats: + 'MgCYWjE6vp0cn3amPan2xPO+f6EZ3I+KwuN1w2vx57vpJ9O0Bn4ci4jn8itwc121ujm7lDHkCW24LuKfZwIdmsifVysY=', + }, + public: { + /** + * Public key encoded as a did:key + */ + did: 'did:key:z6MkqBzPG7oNu7At8fktasQuS7QR7Tj7CujaijPMAgzdmAxD', + }, +} + describe('@web3-storage/access-api/src/config configureSigner', () => { - it('configureSigner creates a signer using config.{DID,PRIVATE_KEY}', async () => { + it('creates a signer using config.{DID,PRIVATE_KEY}', async () => { const config = { - PRIVATE_KEY: - 'MgCYWjE6vp0cn3amPan2xPO+f6EZ3I+KwuN1w2vx57vpJ9O0Bn4ci4jn8itwc121ujm7lDHkCW24LuKfZwIdmsifVysY=', - DID: 'did:web:example.com', + PRIVATE_KEY: testKeypair.private.multiformats, + DID: testKeypair.public.did, } const signer = configModule.configureSigner(config) assert.ok(signer) assert.equal(signer.did().toString(), config.DID) }) - it('configureSigner infers did from config.PRIVATE_KEY when config.DID is omitted', async () => { - const testKey = { - PRIVATE_KEY: - 'MgCYWjE6vp0cn3amPan2xPO+f6EZ3I+KwuN1w2vx57vpJ9O0Bn4ci4jn8itwc121ujm7lDHkCW24LuKfZwIdmsifVysY=', - didKey: 'did:key:z6MkqBzPG7oNu7At8fktasQuS7QR7Tj7CujaijPMAgzdmAxD', - } + it('errors if config.DID is provided but not a did', () => { + assert.throws(() => { + configModule.configureSigner({ + DID: 'not a did', + PRIVATE_KEY: testKeypair.private.multiformats, + }) + }, 'Invalid DID') + }) + it('infers did from config.PRIVATE_KEY when config.DID is omitted', async () => { const config = { - PRIVATE_KEY: testKey.PRIVATE_KEY, + PRIVATE_KEY: testKeypair.private.multiformats, } const signer = configModule.configureSigner(config) assert.ok(signer) - assert.equal(signer.did().toString(), testKey.didKey) + assert.equal(signer.did().toString(), testKeypair.public.did) }) }) From 91edd06ec724fece211610723aeb331f28d181a3 Mon Sep 17 00:00:00 2001 From: Benjamin Goering <171782+gobengo@users.noreply.github.com> Date: Thu, 8 Dec 2022 15:49:13 -0800 Subject: [PATCH 3/9] access-api update isDID type guard to use @ucanto/validator instead of new custom code --- packages/access-api/package.json | 1 + packages/access-api/src/config.js | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/access-api/package.json b/packages/access-api/package.json index 2549de000..08a3d9def 100644 --- a/packages/access-api/package.json +++ b/packages/access-api/package.json @@ -22,6 +22,7 @@ "@ucanto/principal": "^4.0.2", "@ucanto/server": "^4.0.2", "@ucanto/transport": "^4.0.2", + "@ucanto/validator": "^4.0.2", "@web3-storage/access": "workspace:^", "@web3-storage/capabilities": "workspace:^", "@web3-storage/worker-utils": "0.4.3-dev", diff --git a/packages/access-api/src/config.js b/packages/access-api/src/config.js index 8f332ca38..6e771428b 100644 --- a/packages/access-api/src/config.js +++ b/packages/access-api/src/config.js @@ -1,3 +1,4 @@ +import { DID } from '@ucanto/validator' import { Signer } from '@ucanto/principal/ed25519' /** @@ -119,13 +120,14 @@ export function createAnalyticsEngine() { */ export function configureSigner(config) { const signer = Signer.parse(config.PRIVATE_KEY) - if (config.DID) { - if (!isDID(config.DID)) { - throw new Error(`Invalid DID: ${config.DID}`) - } - return signer.withDID(config.DID) + const did = config.DID + if (!did) { + return signer + } + if (!isDID(did)) { + throw new Error(`Invalid DID: ${did}`) } - return signer + return signer.withDID(did) } /** @@ -136,9 +138,8 @@ export function configureSigner(config) { * @returns {object is `did:${string}:${string}`} */ function isDID(object) { - if (typeof object !== 'string') return false - const parts = object.split(':') - if (parts.length <= 2) return false - if (parts[0] !== 'did') return false - return true + try { + return Boolean(DID.match({}).from(object)) + } catch {} + return false } From 795582af677052b557c7f0f9ea82a16888055243 Mon Sep 17 00:00:00 2001 From: Benjamin Goering <171782+gobengo@users.noreply.github.com> Date: Thu, 8 Dec 2022 15:50:16 -0800 Subject: [PATCH 4/9] pnpm-lock --- pnpm-lock.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95e6d0920..afee130ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,7 @@ importers: '@ucanto/principal': ^4.0.2 '@ucanto/server': ^4.0.2 '@ucanto/transport': ^4.0.2 + '@ucanto/validator': ^4.0.2 '@web3-storage/access': workspace:^ '@web3-storage/capabilities': workspace:^ '@web3-storage/worker-utils': 0.4.3-dev @@ -71,6 +72,7 @@ importers: '@ucanto/principal': 4.0.2 '@ucanto/server': 4.0.2 '@ucanto/transport': 4.0.2 + '@ucanto/validator': 4.0.2 '@web3-storage/access': link:../access-client '@web3-storage/capabilities': link:../capabilities '@web3-storage/worker-utils': 0.4.3-dev From b9533c92d0b8080f9f6d14c240a89f370d6ad7f7 Mon Sep 17 00:00:00 2001 From: Benjamin Goering <171782+gobengo@users.noreply.github.com> Date: Fri, 9 Dec 2022 09:39:02 -0800 Subject: [PATCH 5/9] access-api configureSigner happens inside loadConfig and sets config.signer --- packages/access-api/src/config.js | 8 ++++++-- packages/access-api/src/utils/context.js | 6 ++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/access-api/src/config.js b/packages/access-api/src/config.js index 6e771428b..647ca9fa0 100644 --- a/packages/access-api/src/config.js +++ b/packages/access-api/src/config.js @@ -32,11 +32,14 @@ export function loadConfig(env) { } } + const DID = env.DID + const PRIVATE_KEY = vars.PRIVATE_KEY + const signer = configureSigner({ DID, PRIVATE_KEY }) return { DEBUG: boolValue(vars.DEBUG), ENV: parseRuntimeEnv(vars.ENV), - PRIVATE_KEY: vars.PRIVATE_KEY, + PRIVATE_KEY, POSTMARK_TOKEN: vars.POSTMARK_TOKEN, SENTRY_DSN: vars.SENTRY_DSN, LOGTAIL_TOKEN: vars.LOGTAIL_TOKEN, @@ -52,7 +55,8 @@ export function loadConfig(env) { // eslint-disable-next-line no-undef COMMITHASH: ACCOUNT_COMMITHASH, - DID: env.DID, + DID, + signer, // bindings METRICS: diff --git a/packages/access-api/src/utils/context.js b/packages/access-api/src/utils/context.js index 2e11e7744..d9e5c9490 100644 --- a/packages/access-api/src/utils/context.js +++ b/packages/access-api/src/utils/context.js @@ -1,7 +1,7 @@ import { Logging } from '@web3-storage/worker-utils/logging' import Toucan from 'toucan-js' import pkg from '../../package.json' -import { configureSigner, loadConfig } from '../config.js' +import { loadConfig } from '../config.js' import { Spaces } from '../kvs/spaces.js' import { Validations } from '../kvs/validations.js' import { Email } from './email.js' @@ -40,13 +40,11 @@ export function getContext(request, env, ctx) { commit: config.COMMITHASH, env: config.ENV, }) - - const signer = configureSigner(config) const url = new URL(request.url) const db = new D1QB(config.DB) return { log, - signer, + signer: config.signer, config, url, kvs: { From d20433140493ecf5408c0e6edb49ce654ec01f12 Mon Sep 17 00:00:00 2001 From: Benjamin Goering <171782+gobengo@users.noreply.github.com> Date: Fri, 9 Dec 2022 09:43:49 -0800 Subject: [PATCH 6/9] remove unnecessary PRIVATE_KEY environment var from access-api/test/ucan.test --- packages/access-api/test/ucan.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/access-api/test/ucan.test.js b/packages/access-api/test/ucan.test.js index 498dadecc..b16265d00 100644 --- a/packages/access-api/test/ucan.test.js +++ b/packages/access-api/test/ucan.test.js @@ -145,18 +145,16 @@ describe('ucan', function () { }) test('should support ucan invoking to a did:web aud', async function () { - const serviceDidWeb = 'did:web:web3.storage' + const serviceDidWeb = 'did:web:example.com' const { mf, issuer, service } = await context({ environment: { ...process.env, - PRIVATE_KEY: - 'MgCYWjE6vp0cn3amPan2xPO+f6EZ3I+KwuN1w2vx57vpJ9O0Bn4ci4jn8itwc121ujm7lDHkCW24LuKfZwIdmsifVysY=', DID: serviceDidWeb, }, }) const ucan = await UCAN.issue({ issuer, - audience: service.withDID('did:web:web3.storage'), + audience: service.withDID(serviceDidWeb), capabilities: [{ can: 'testing/pass', with: 'mailto:admin@dag.house' }], }) const res = await mf.dispatchFetch('http://localhost:8787/raw', { From 5184a554ee80f29ddb74ddd9c78e3b883c4c8119 Mon Sep 17 00:00:00 2001 From: Benjamin Goering <171782+gobengo@users.noreply.github.com> Date: Fri, 9 Dec 2022 09:47:16 -0800 Subject: [PATCH 7/9] access-api/src/config remove isDID function --- packages/access-api/src/config.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/access-api/src/config.js b/packages/access-api/src/config.js index 647ca9fa0..0d502adde 100644 --- a/packages/access-api/src/config.js +++ b/packages/access-api/src/config.js @@ -128,22 +128,5 @@ export function configureSigner(config) { if (!did) { return signer } - if (!isDID(did)) { - throw new Error(`Invalid DID: ${did}`) - } - return signer.withDID(did) -} - -/** - * Return whether or not the provided object looks like a decentralized identifier (aka DID) - * - * @see https://www.w3.org/TR/did-core/#did-syntax - * @param {any} object - * @returns {object is `did:${string}:${string}`} - */ -function isDID(object) { - try { - return Boolean(DID.match({}).from(object)) - } catch {} - return false + return signer.withDID(DID.match({}).from(did)) } From a629263c52ebb6969ce5ddcdebe306a3a86f6c76 Mon Sep 17 00:00:00 2001 From: Benjamin Goering <171782+gobengo@users.noreply.github.com> Date: Fri, 9 Dec 2022 15:17:23 -0800 Subject: [PATCH 8/9] improve configureSigner test --- packages/access-api/test/config.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/access-api/test/config.test.js b/packages/access-api/test/config.test.js index ab95ce102..8707c8134 100644 --- a/packages/access-api/test/config.test.js +++ b/packages/access-api/test/config.test.js @@ -22,11 +22,14 @@ describe('@web3-storage/access-api/src/config configureSigner', () => { it('creates a signer using config.{DID,PRIVATE_KEY}', async () => { const config = { PRIVATE_KEY: testKeypair.private.multiformats, - DID: testKeypair.public.did, + DID: 'did:web:exampe.com', } const signer = configModule.configureSigner(config) assert.ok(signer) assert.equal(signer.did().toString(), config.DID) + const { keys } = signer.toArchive() + const didKeys = Object.keys(keys) + assert.deepEqual(didKeys, [testKeypair.public.did]) }) it('errors if config.DID is provided but not a did', () => { assert.throws(() => { From 852951f932d97fc651c08258fdc6f4a7b15ce658 Mon Sep 17 00:00:00 2001 From: Benjamin Goering <171782+gobengo@users.noreply.github.com> Date: Fri, 9 Dec 2022 15:18:28 -0800 Subject: [PATCH 9/9] config loadConfig no longer has DID or PRIVATE_KEY on it --- packages/access-api/src/config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/access-api/src/config.js b/packages/access-api/src/config.js index 0d502adde..65d0061ad 100644 --- a/packages/access-api/src/config.js +++ b/packages/access-api/src/config.js @@ -39,7 +39,6 @@ export function loadConfig(env) { DEBUG: boolValue(vars.DEBUG), ENV: parseRuntimeEnv(vars.ENV), - PRIVATE_KEY, POSTMARK_TOKEN: vars.POSTMARK_TOKEN, SENTRY_DSN: vars.SENTRY_DSN, LOGTAIL_TOKEN: vars.LOGTAIL_TOKEN, @@ -55,7 +54,6 @@ export function loadConfig(env) { // eslint-disable-next-line no-undef COMMITHASH: ACCOUNT_COMMITHASH, - DID, signer, // bindings