Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

signedFetch more generic/configurable aligning it with fetch api #259

Merged
merged 10 commits into from
May 16, 2024
62 changes: 23 additions & 39 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 @@ -232,19 +233,23 @@ export class SignifyClient {
/**
* Fetch a resource from from an external URL with headers signed by an AID
* @async
2byrds marked this conversation as resolved.
Show resolved Hide resolved
* @param {string} aidName Name or alias of the AID to be used for signing
* @param {string} url URL of the resource
* @param {string} path Path to the resource
* @param {string} method HTTP method
2byrds marked this conversation as resolved.
Show resolved Hide resolved
* @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
* @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<Response>} A promise to the result of the fetch
*/
async signedFetch(
aidName: string,
url: string,
path: string,
method: string,
2byrds marked this conversation as resolved.
Show resolved Hide resolved
data: any,
aidName: string
req: RequestInit
): Promise<Response> {
const hab = await this.identifiers().get(aidName);
const keeper = this.manager!.get(hab);
Expand All @@ -254,42 +259,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,
new Headers(headers),
req.method ?? 'GET',
path.split('?')[0]
);
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 await fetch(url + path, 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
64 changes: 57 additions & 7 deletions test/app/clienting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import { Groups } from '../../src/keri/app/grouping';
import { Notifications } from '../../src/keri/app/notifying';

import { Authenticater } from '../../src/keri/core/authing';
import { Cigar } from '../../src/keri/core/cigar';
import { HEADER_SIG_INPUT, HEADER_SIG_TIME } from '../../src/keri/core/httping';
import { SaltyKeeper } from '../../src/keri/core/keeping';
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 +145,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 +293,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 +310,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,12 +362,18 @@ describe('SignifyClient', () => {
'EGFi9pCcRaLK8dPh5S7JP9Em62fBMiR1l4gW1ZazuuAO'
);

let heads = new Headers();
heads.set('Content-Type', 'application/json');
let reqInit = {
headers: heads,
method: 'POST',
body: JSON.stringify({ foo: true }),
};
resp = await client.signedFetch(
'aid1',
'http://example.com',
'/test',
2byrds marked this conversation as resolved.
Show resolved Hide resolved
'POST',
{ foo: true },
'aid1'
reqInit
);
lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!;
assert.equal(lastCall[0]!, 'http://example.com/test');
Expand All @@ -376,6 +385,47 @@ describe('SignifyClient', () => {
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) as SaltyKeeper;
2byrds marked this conversation as resolved.
Show resolved Hide resolved
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, null) as Cigar;
2byrds marked this conversation as resolved.
Show resolved Hide resolved
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
Loading