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
4 changes: 2 additions & 2 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,
minSleep: 1000,
2byrds marked this conversation as resolved.
Show resolved Hide resolved
maxRetries: 5,
timeout: 10000,
timeout: 30000,
};

interface Aid {
Expand Down
72 changes: 27 additions & 45 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,20 +233,22 @@ 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} 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 result of the fetch
*/
2byrds marked this conversation as resolved.
Show resolved Hide resolved
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
Loading