From 6113497b1ef73b6c6cb0b4ca3c688313116f02da Mon Sep 17 00:00:00 2001 From: Lance <2byrds@gmail.com> Date: Thu, 16 May 2024 10:31:24 -0400 Subject: [PATCH] signedFetch more generic/configurable aligning it with fetch api (#259) BREAKING CHANGE: signedFetch renamed to to createSignedRequest with changed parameters. It now returns the request that can be passed to `fetch` --- .../singlesig-vlei-issuance.test.ts | 6 +- src/keri/app/clienting.ts | 74 +++++++----------- src/keri/core/authing.ts | 12 ++- src/keri/core/httping.ts | 5 +- test/app/clienting.test.ts | 76 +++++++++++++++---- 5 files changed, 105 insertions(+), 68 deletions(-) diff --git a/examples/integration-scripts/singlesig-vlei-issuance.test.ts b/examples/integration-scripts/singlesig-vlei-issuance.test.ts index 6734d68a..4d807860 100644 --- a/examples/integration-scripts/singlesig-vlei-issuance.test.ts +++ b/examples/integration-scripts/singlesig-vlei-issuance.test.ts @@ -108,9 +108,9 @@ const OOR_AUTH_RULES = LE_RULES; const CRED_RETRY_DEFAULTS = { maxSleep: 1000, - minSleep: 10, - maxRetries: 5, - timeout: 10000, + minSleep: 100, + maxRetries: undefined, + timeout: 30000, }; interface Aid { diff --git a/src/keri/app/clienting.ts b/src/keri/app/clienting.ts index de2e5986..4c7d5c0f 100644 --- a/src/keri/app/clienting.ts +++ b/src/keri/app/clienting.ts @@ -1,16 +1,17 @@ -import { Agent, Controller } from './controller'; -import { Tier } from '../core/salter'; import { Authenticater } from '../core/authing'; +import { HEADER_SIG_TIME } from '../core/httping'; import { ExternalModule, KeyManager } from '../core/keeping'; +import { Tier } from '../core/salter'; import { Identifier } from './aiding'; import { Contacts, Challenges } from './contacting'; +import { Agent, Controller } from './controller'; import { Oobis, Operations, KeyEvents, KeyStates } from './coring'; import { Credentials, Ipex, Registries, Schemas } from './credentialing'; -import { Notifications } from './notifying'; import { Escrows } from './escrowing'; -import { Groups } from './grouping'; import { Exchanges } from './exchanging'; +import { Groups } from './grouping'; +import { Notifications } from './notifying'; const DEFAULT_BOOT_URL = 'http://localhost:3903'; @@ -176,7 +177,7 @@ export class SignifyClient { headers.set('Signify-Resource', this.controller.pre); headers.set( - 'Signify-Timestamp', + HEADER_SIG_TIME, new Date().toISOString().replace('Z', '000+00:00') ); headers.set('Content-Type', 'application/json'); @@ -230,22 +231,24 @@ export class SignifyClient { } /** - * Fetch a resource from from an external URL with headers signed by an AID + * Create a Signed Request to fetch a resource from an external URL with headers signed by an AID * @async - * @param {string} url URL of the resource - * @param {string} path Path to the resource - * @param {string} method HTTP method - * @param {any} data Data to be sent in the body of the resource * @param {string} aidName Name or alias of the AID to be used for signing - * @returns {Promise} A promise to the result of the fetch + * @param {string} url URL of the requested resource + * @param {RequestInit} req Request options should include: + * - method: HTTP method + * - data Data to be sent in the body of the resource. + * If the data is a CESR JSON string then you should also set contentType to 'application/json+cesr' + * If the data is a FormData object then you should not set the contentType and the browser will set it to 'multipart/form-data' + * If the data is an object then you should use JSON.stringify to convert it to a string and set the contentType to 'application/json' + * - contentType Content type of the request. + * @returns {Promise} A promise to the created Request */ - async signedFetch( + async createSignedRequest( + aidName: string, url: string, - path: string, - method: string, - data: any, - aidName: string - ): Promise { + req: RequestInit + ): Promise { const hab = await this.identifiers().get(aidName); const keeper = this.manager!.get(hab); @@ -254,42 +257,21 @@ export class SignifyClient { keeper.signers[0].verfer ); - const headers = new Headers(); - headers.set('Signify-Resource', hab.prefix); + const headers = new Headers(req.headers); + headers.set('Signify-Resource', hab['prefix']); headers.set( - 'Signify-Timestamp', + HEADER_SIG_TIME, new Date().toISOString().replace('Z', '000+00:00') ); - if (data !== null) { - headers.set('Content-Length', data.length); - } else { - headers.set('Content-Length', '0'); - } const signed_headers = authenticator.sign( - headers, - method, - path.split('?')[0] + new Headers(headers), + req.method ?? 'GET', + new URL(url).pathname ); - let _body = null; - if (method != 'GET') { - if (data instanceof FormData) { - _body = data; - // do not set the content type, let the browser do it - // headers.set('Content-Type', 'multipart/form-data') - } else { - _body = JSON.stringify(data); - headers.set('Content-Type', 'application/json'); - } - } else { - headers.set('Content-Type', 'application/json'); - } + req.headers = signed_headers; - return await fetch(url + path, { - method: method, - body: _body, - headers: signed_headers, - }); + return new Request(url, req); } /** diff --git a/src/keri/core/authing.ts b/src/keri/core/authing.ts index 5f816f0e..3b1650d6 100644 --- a/src/keri/core/authing.ts +++ b/src/keri/core/authing.ts @@ -1,6 +1,12 @@ import { Signer } from './signer'; import { Verfer } from './verfer'; -import { desiginput, normalize, siginput } from './httping'; +import { + desiginput, + HEADER_SIG_INPUT, + HEADER_SIG_TIME, + normalize, + siginput, +} from './httping'; import { Signage, signature, designature } from '../end/ending'; import { Cigar } from './cigar'; import { Siger } from './siger'; @@ -9,7 +15,7 @@ export class Authenticater { '@method', '@path', 'signify-resource', - 'signify-timestamp', + HEADER_SIG_TIME.toLowerCase(), ]; private _verfer: Verfer; private readonly _csig: Signer; @@ -20,7 +26,7 @@ export class Authenticater { } verify(headers: Headers, method: string, path: string): boolean { - const siginput = headers.get('Signature-Input'); + const siginput = headers.get(HEADER_SIG_INPUT); if (siginput == null) { return false; } diff --git a/src/keri/core/httping.ts b/src/keri/core/httping.ts index d83694ff..46e3ac14 100644 --- a/src/keri/core/httping.ts +++ b/src/keri/core/httping.ts @@ -13,6 +13,9 @@ import { Siger } from './siger'; import { Buffer } from 'buffer'; import { encodeBase64Url } from './base64'; +export const HEADER_SIG_INPUT = normalize('Signature-Input'); +export const HEADER_SIG_TIME = normalize('Signify-Timestamp'); + export function normalize(header: string) { return header.trim(); } @@ -107,7 +110,7 @@ export function siginput( return [ new Map([ - ['Signature-Input', `${serializeDictionary(sid as Dictionary)}`], + [HEADER_SIG_INPUT, `${serializeDictionary(sid as Dictionary)}`], ]), sig, ]; diff --git a/test/app/clienting.test.ts b/test/app/clienting.test.ts index ede08d6f..a7a0e23b 100644 --- a/test/app/clienting.test.ts +++ b/test/app/clienting.test.ts @@ -19,6 +19,7 @@ import { Groups } from '../../src/keri/app/grouping'; import { Notifications } from '../../src/keri/app/notifying'; import { Authenticater } from '../../src/keri/core/authing'; +import { HEADER_SIG_INPUT, HEADER_SIG_TIME } from '../../src/keri/core/httping'; import { Salter, Tier } from '../../src/keri/core/salter'; import libsodium from 'libsodium-wrappers-sumo'; import fetchMock from 'jest-fetch-mock'; @@ -142,7 +143,7 @@ fetchMock.mockResponse((req) => { 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' ); headers.set( - 'Signify-Timestamp', + HEADER_SIG_TIME, new Date().toISOString().replace('Z', '000+00:00') ); headers.set('Content-Type', 'application/json'); @@ -290,8 +291,8 @@ describe('SignifyClient', () => { // Headers in error let badAgentHeaders = { 'signify-resource': 'bad_resource', - 'signify-timestamp': '2023-08-20T15:34:31.534673+00:00', - 'signature-input': + [HEADER_SIG_TIME]: '2023-08-20T15:34:31.534673+00:00', + [HEADER_SIG_INPUT]: 'signify=("signify-resource" "@method" "@path" "signify-timestamp");created=1692545671;keyid="EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei";alg="ed25519"', signature: 'indexed="?0";signify="0BDiSoxCv42h2BtGMHy_tpWAqyCgEoFwRa8bQy20mBB2D5Vik4gRp3XwkEHtqy6iy6SUYAytMUDtRbewotAfkCgN"', @@ -307,7 +308,7 @@ describe('SignifyClient', () => { badAgentHeaders = { 'signify-resource': 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', 'signify-timestamp': '2023-08-20T15:34:31.534673+00:00', - 'signature-input': + [HEADER_SIG_INPUT]: 'signify=("signify-resource" "@method" "@path" "signify-timestamp");created=1692545671;keyid="EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei";alg="ed25519"', signature: 'indexed="?0";signify="0BDiSoxCv42h2BtGMHy_tpWAqyCgEoFwRa8bQy20mBB2D5Vik4gRp3XwkEHtqy6iy6SUYAytMUDtRbewotAfkCbad"', @@ -359,23 +360,68 @@ describe('SignifyClient', () => { 'EGFi9pCcRaLK8dPh5S7JP9Em62fBMiR1l4gW1ZazuuAO' ); - resp = await client.signedFetch( - 'http://example.com', - '/test', - 'POST', - { foo: true }, - 'aid1' - ); + let heads = new Headers(); + heads.set('Content-Type', 'application/json'); + let treqInit = { + headers: heads, + method: 'POST', + body: JSON.stringify({ foo: true }), + }; + let turl = 'http://example.com/test'; + let treq = await client.createSignedRequest('aid1', turl, treqInit); + let tres = await fetch(treq); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, 'http://example.com/test'); - assert.equal(lastCall[1]!.method, 'POST'); - lastBody = JSON.parse(lastCall[1]!.body!); + let resReq = lastCall[0] as Request; + assert.equal(resReq.url, 'http://example.com/test'); + assert.equal(resReq.method, 'POST'); + lastBody = await resReq.json(); assert.deepEqual(lastBody.foo, true); - lastHeaders = new Headers(lastCall[1]!.headers!); + lastHeaders = new Headers(resReq.headers); assert.equal( lastHeaders.get('signify-resource'), 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK' ); + assert.equal( + lastHeaders + .get(HEADER_SIG_INPUT) + ?.startsWith( + 'signify=("@method" "@path" "signify-resource" "signify-timestamp");created=' + ), + true + ); + assert.equal( + lastHeaders + .get(HEADER_SIG_INPUT) + ?.endsWith( + ';keyid="BPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9";alg="ed25519"' + ), + true + ); + + let aid = await client.identifiers().get('aid1'); + const keeper = client.manager!.get(aid); + const signer = keeper.signers[0]; + const created = lastHeaders + .get(HEADER_SIG_INPUT) + ?.split(';created=')[1] + .split(';keyid=')[0]; + const data = `"@method": POST\n"@path": /test\n"signify-resource": ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK\n"signify-timestamp": ${lastHeaders.get( + HEADER_SIG_TIME + )}\n"@signature-params: (@method @path signify-resource signify-timestamp);created=${created};keyid=BPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9;alg=ed25519"`; + + if (data) { + const raw = new TextEncoder().encode(data); + const sig = signer.sign(raw); + assert.equal( + sig.qb64, + lastHeaders + .get('signature') + ?.split('signify="')[1] + .split('"')[0] + ); + } else { + fail(`${HEADER_SIG_INPUT} is empty`); + } }); test('includes HTTP status info in error message', async () => {