Skip to content

Commit

Permalink
signedFetch more generic/configurable aligning it with fetch api (#259)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: signedFetch renamed to to createSignedRequest with changed parameters. It now returns the request that can be passed to `fetch`
  • Loading branch information
2byrds authored May 16, 2024
1 parent 2d23251 commit 6113497
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 68 deletions.
6 changes: 3 additions & 3 deletions examples/integration-scripts/singlesig-vlei-issuance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
74 changes: 28 additions & 46 deletions src/keri/app/clienting.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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<Response>} 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<Request>} A promise to the created Request
*/
async signedFetch(
async createSignedRequest(
aidName: string,
url: string,
path: string,
method: string,
data: any,
aidName: string
): Promise<Response> {
req: RequestInit
): Promise<Request> {
const hab = await this.identifiers().get(aidName);
const keeper = this.manager!.get(hab);

Expand All @@ -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);
}

/**
Expand Down
12 changes: 9 additions & 3 deletions src/keri/core/authing.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,7 +15,7 @@ export class Authenticater {
'@method',
'@path',
'signify-resource',
'signify-timestamp',
HEADER_SIG_TIME.toLowerCase(),
];
private _verfer: Verfer;
private readonly _csig: Signer;
Expand All @@ -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;
}
Expand Down
5 changes: 4 additions & 1 deletion src/keri/core/httping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -107,7 +110,7 @@ export function siginput(

return [
new Map<string, string>([
['Signature-Input', `${serializeDictionary(sid as Dictionary)}`],
[HEADER_SIG_INPUT, `${serializeDictionary(sid as Dictionary)}`],
]),
sig,
];
Expand Down
76 changes: 61 additions & 15 deletions test/app/clienting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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"',
Expand All @@ -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"',
Expand Down Expand Up @@ -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 () => {
Expand Down

0 comments on commit 6113497

Please sign in to comment.