diff --git a/examples/integration-scripts/test-setup-single-client.test.ts b/examples/integration-scripts/test-setup-single-client.test.ts index bd957743..ad93226e 100644 --- a/examples/integration-scripts/test-setup-single-client.test.ts +++ b/examples/integration-scripts/test-setup-single-client.test.ts @@ -74,6 +74,9 @@ describe('test-setup-single-client', () => { 'http://127.0.0.1:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness', 'http://127.0.0.1:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness', 'http://127.0.0.1:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness', + 'http://127.0.0.1:5645/oobi/BM35JN8XeJSEfpxopjn5jr7tAHCE5749f0OobhMLCorE/controller?name=Wit&tag=witness', + 'http://127.0.0.1:5646/oobi/BIj15u5V11bkbtAxMA7gcNJZcax-7TgaBMLsQnMHpYHP/controller?name=Wub&tag=witness', + 'http://127.0.0.1:5647/oobi/BF2rZTW79z4IXocYRQnjjsOuvFUQv-ptCf8Yltd7PfsM/controller?name=Wyz&tag=witness', ], }); break; diff --git a/src/keri/app/clienting.ts b/src/keri/app/clienting.ts index df1d7356..eb314042 100644 --- a/src/keri/app/clienting.ts +++ b/src/keri/app/clienting.ts @@ -1,5 +1,5 @@ -import { Authenticater } from '../core/authing'; -import { HEADER_SIG_TIME } from '../core/httping'; +import { Authenticator } from '../core/authing'; +import { HEADER_SIG_SENDER, HEADER_SIG_TIME } from '../core/httping'; import { ExternalModule, KeyManager } from '../core/keeping'; import { Tier } from '../core/salter'; @@ -37,7 +37,7 @@ export class SignifyClient { public bran: string; public pidx: number; public agent: Agent | null; - public authn: Authenticater | null; + public authn: Authenticator | null; public manager: KeyManager | null; public tier: Tier; public bootUrl: string; @@ -151,7 +151,7 @@ export class SignifyClient { this.controller.salter, this.exteralModules ); - this.authn = new Authenticater( + this.authn = new Authenticator( this.controller.signer, this.agent.verfer! ); @@ -172,63 +172,60 @@ export class SignifyClient { data: any, extraHeaders?: Headers ): Promise { - const headers = new Headers(); - let signed_headers = new Headers(); - const final_headers = new Headers(); - - headers.set('Signify-Resource', this.controller.pre); - headers.set( - HEADER_SIG_TIME, - new Date().toISOString().replace('Z', '000+00:00') - ); - headers.set('Content-Type', 'application/json'); - - const _body = method == 'GET' ? null : JSON.stringify(data); - - if (this.authn) { - signed_headers = this.authn.sign( - headers, - method, - path.split('?')[0] - ); - } else { - throw new Error('client need to call connect first'); + if (!this.authn) { + throw new Error('Client needs to call connect first'); } - signed_headers.forEach((value, key) => { - final_headers.set(key, value); - }); - if (extraHeaders !== undefined) { + const headers = new Headers(); + headers.set(HEADER_SIG_SENDER, this.controller.pre); + + if (extraHeaders) { extraHeaders.forEach((value, key) => { - final_headers.append(key, value); + headers.append(key, value); }); } - const res = await fetch(this.url + path, { - method: method, - body: _body, - headers: final_headers, - }); - if (!res.ok) { - const error = await res.text(); - const message = `HTTP ${method} ${path} - ${res.status} ${res.statusText} - ${error}`; - throw new Error(message); - } - const isSameAgent = - this.agent?.pre === res.headers.get('signify-resource'); - if (!isSameAgent) { - throw new Error('message from a different remote agent'); + + const body = method == 'GET' ? null : JSON.stringify(data); + if (body) { + headers.set('Content-Type', 'application/json'); + headers.set('Content-Length', body.length.toString()); } - const verification = this.authn.verify( - res.headers, + const request = new Request(this.url + path, { method, - path.split('?')[0] + body, + headers, + }); + + const wrappedRequest = await this.authn.wrap( + request, + this.url, + this.controller.pre, + this.agent!.pre + ); + const wrappedResponse = await fetch(wrappedRequest); + + // Any other error will be wrapped in an ESSR response + if (wrappedResponse.status === 401) { + throw new Error( + `HTTP ${method} ${path} - ${wrappedResponse.status} ${wrappedResponse.statusText}` + ); + } + + const response = await this.authn.unwrap( + wrappedResponse, + this.agent!.pre, + this.controller.pre ); - if (verification) { - return res; - } else { - throw new Error('response verification failed'); + + if (!response.ok) { + const error = await response.text(); + throw new Error( + `HTTP ${method} ${path} - ${response.status} ${response.statusText} - ${error}` + ); } + + return response; } /** @@ -253,13 +250,13 @@ export class SignifyClient { const hab = await this.identifiers().get(aidName); const keeper = this.manager!.get(hab); - const authenticator = new Authenticater( + const authenticator = new Authenticator( keeper.signers[0], keeper.signers[0].verfer ); const headers = new Headers(req.headers); - headers.set('Signify-Resource', hab['prefix']); + headers.set(HEADER_SIG_SENDER, hab['prefix']); headers.set( HEADER_SIG_TIME, new Date().toISOString().replace('Z', '000+00:00') diff --git a/src/keri/core/authing.ts b/src/keri/core/authing.ts index f372032f..d8587680 100644 --- a/src/keri/core/authing.ts +++ b/src/keri/core/authing.ts @@ -1,8 +1,12 @@ +import libsodium from 'libsodium-wrappers-sumo'; import { Signer } from './signer'; import { Verfer } from './verfer'; import { desiginput, + HEADER_SIG, + HEADER_SIG_DESTINATION, HEADER_SIG_INPUT, + HEADER_SIG_SENDER, HEADER_SIG_TIME, normalize, siginput, @@ -10,19 +14,39 @@ import { import { Signage, signature, designature } from '../end/ending'; import { Cigar } from './cigar'; import { Siger } from './siger'; -export class Authenticater { +import { Diger } from './diger'; +import { MtrDex } from './matter'; +import { b, d } from './core'; + +export class Authenticator { static DefaultFields = [ '@method', '@path', 'signify-resource', HEADER_SIG_TIME.toLowerCase(), ]; - private _verfer: Verfer; private readonly _csig: Signer; + private readonly _cx25519Pub: Uint8Array; + private readonly _cx25519Priv: Uint8Array; + + private readonly _verfer: Verfer; + private readonly _vx25519Pub: Uint8Array; constructor(csig: Signer, verfer: Verfer) { this._csig = csig; + const sigkey = new Uint8Array( + this._csig.raw.length + this._csig.verfer.raw.length + ); + sigkey.set(this._csig.raw); + sigkey.set(this._csig.verfer.raw, this._csig.raw.length); + this._cx25519Priv = + libsodium.crypto_sign_ed25519_sk_to_curve25519(sigkey); + this._cx25519Pub = libsodium.crypto_scalarmult_base(this._cx25519Priv); + this._verfer = verfer; + this._vx25519Pub = libsodium.crypto_sign_ed25519_pk_to_curve25519( + this._verfer.raw + ); } verify(headers: Headers, method: string, path: string): boolean { @@ -30,7 +54,7 @@ export class Authenticater { if (siginput == null) { return false; } - const signature = headers.get('Signature'); + const signature = headers.get(HEADER_SIG); if (signature == null) { return false; } @@ -94,7 +118,7 @@ export class Authenticater { fields?: Array ): Headers { if (fields == undefined) { - fields = Authenticater.DefaultFields; + fields = Authenticator.DefaultFields; } const [header, sig] = siginput(this._csig, { @@ -121,4 +145,185 @@ export class Authenticater { return headers; } + + async wrap( + request: Request, + baseUrl: string, + sender: string, + receiver: string + ): Promise { + const dt = new Date().toISOString().replace('Z', '000+00:00'); + + const headers = new Headers(); + headers.set(HEADER_SIG_SENDER, sender); + headers.set(HEADER_SIG_DESTINATION, receiver); + headers.set(HEADER_SIG_TIME, dt); + headers.set('Content-Type', 'application/octet-stream'); + + const requestStr = await Authenticator.serializeRequest(request); + const raw = libsodium.crypto_box_seal(requestStr, this._vx25519Pub); + + const diger = new Diger({ code: MtrDex.Blake3_256 }, raw); + const payload = { + src: sender, + dest: receiver, + d: diger.qb64, + dt, + }; + + const sig = this._csig.sign(b(JSON.stringify(payload))); + const markers = new Map(); + markers.set('signify', sig); + const signage = new Signage(markers, false); + const signed = signature([signage]); + + signed.forEach((value, key) => { + headers.append(key, value); + }); + + return new Request(baseUrl + '/', { + method: 'POST', + body: raw, + headers, + }); + } + + static async serializeRequest(request: Request) { + let headers = ''; + request.headers.forEach((value, name) => { + headers += `${name}: ${value}\r\n`; + }); + + let body = ''; + if (request.method !== 'GET' && request.body) { + body = Buffer.from(await this.streamToBytes(request.body)).toString( + 'utf-8' + ); + } + + return `${request.method} ${request.url} HTTP/1.1\r\n${headers}\r\n${body}`; + } + + private static async streamToBytes(stream: ReadableStream) { + const reader = stream.getReader(); + const chunks = []; + let done, value; + + while ((({ done, value } = await reader.read()), !done)) { + if (value) chunks.push(value); + } + reader.releaseLock(); + + const totalLength = chunks.reduce( + (acc, chunk) => acc + chunk.length, + 0 + ); + const result = new Uint8Array(totalLength); + let offset = 0; + + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + + return result; + } + + async unwrap( + wrapper: Response, + sender: string, + receiver: string + ): Promise { + const signature = wrapper.headers.get(HEADER_SIG); + if (!signature) { + throw new Error('Signature is missing from ESSR payload'); + } + + if (wrapper.headers.get(HEADER_SIG_SENDER) !== sender) { + throw new Error('Message from a different remote agent'); + } + + if (wrapper.headers.get(HEADER_SIG_DESTINATION) !== receiver) { + throw new Error( + 'Invalid ESSR payload, missing or incorrect destination prefix' + ); + } + + const dt = wrapper.headers.get(HEADER_SIG_TIME); + if (!dt) { + throw new Error('Timestamp is missing from ESSR payload'); + } + + const ciphertext = new Uint8Array(await wrapper.arrayBuffer()); + const diger = new Diger({ code: MtrDex.Blake3_256 }, ciphertext); + + const payload = { + src: sender, + dest: receiver, + d: diger.qb64, + dt, + }; + + const signages = designature(signature); + const markers = signages[0].markers as Map; + const cig = markers.get('signify'); + + const verified = this._verfer.verify( + cig?.raw, + Buffer.from(JSON.stringify(payload)) + ); + if (!verified) { + throw new Error('Invalid signature'); + } + + const plaintext = d( + libsodium.crypto_box_seal_open( + ciphertext, + this._cx25519Pub, + this._cx25519Priv + ) + ); + const response = this.deserializeResponse(plaintext); + + if (response.headers.get(HEADER_SIG_SENDER) !== sender) { + throw new Error( + 'Invalid ESSR payload, missing or incorrect encrypted sender' + ); + } + + return response; + } + + private deserializeResponse(httpString: string) { + const lines = httpString.split('\r\n'); + + const [_, statusCode, ...statusTextArr] = lines[0].split(' '); + const statusText = statusTextArr.join(' '); + const status = Number(statusCode); + + const headers = new Headers(); + let body = ''; + let bodyStart = false; + + for (let i = 1; i < lines.length; i++) { + if (lines[i] === '') { + bodyStart = true; + continue; + } + + if (bodyStart) { + body += lines[i] + '\n'; + continue; + } + + const [key, value] = lines[i].split(': '); + headers.append(key, value); + } + + return new Response(body ? body.trim() : null, { + status, + statusText, + headers, + }); + } } diff --git a/src/keri/core/httping.ts b/src/keri/core/httping.ts index 46e3ac14..6b8f16f3 100644 --- a/src/keri/core/httping.ts +++ b/src/keri/core/httping.ts @@ -13,8 +13,11 @@ import { Siger } from './siger'; import { Buffer } from 'buffer'; import { encodeBase64Url } from './base64'; +export const HEADER_SIG = normalize('Signature'); export const HEADER_SIG_INPUT = normalize('Signature-Input'); export const HEADER_SIG_TIME = normalize('Signify-Timestamp'); +export const HEADER_SIG_SENDER = normalize('Signify-Resource'); +export const HEADER_SIG_DESTINATION = normalize('Signify-Receiver'); export function normalize(header: string) { return header.trim(); diff --git a/src/keri/end/ending.ts b/src/keri/end/ending.ts index ea9d26d0..0923d33e 100644 --- a/src/keri/end/ending.ts +++ b/src/keri/end/ending.ts @@ -1,5 +1,6 @@ import { Siger } from '../core/siger'; import { Cigar } from '../core/cigar'; +import { HEADER_SIG } from '../core/httping'; export const FALSY = [false, 0, '?0', 'no', 'false', 'False', 'off']; export const TRUTHY = [true, 1, '?1', 'yes', 'true', 'True', 'on']; @@ -98,7 +99,7 @@ export function signature(signages: Signage[]): Headers { values.push(items.join(';')); } - return new Headers([['Signature', values.join(',')]]); + return new Headers([[HEADER_SIG, values.join(',')]]); } export function designature(value: string) { diff --git a/test/app/clienting.test.ts b/test/app/clienting.test.ts index cd8a8f06..891d697e 100644 --- a/test/app/clienting.test.ts +++ b/test/app/clienting.test.ts @@ -17,13 +17,18 @@ import { Escrows } from '../../src/keri/app/escrowing'; import { Exchanges } from '../../src/keri/app/exchanging'; 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 { + HEADER_SIG, + HEADER_SIG_DESTINATION, + HEADER_SIG_INPUT, + HEADER_SIG_SENDER, + 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'; import 'whatwg-fetch'; +import { Authenticator, Cigar, Siger, Signage, signature } from '../../src'; fetchMock.enableMocks(); @@ -128,6 +133,24 @@ const mockCredential = { et: 'iss', }, }; +// prettier-ignore +const essrPayload = new Uint8Array([117,25,216,177,230,114,125,73,6,221,25,123,124,67,78,188,248,165,196,158,243,206,130,147,102,156,228,138,222,39,133,63,171,67,121,182,77,211,170,157,244,131,48,73,202,165,117,156,16,157,70,102,201,62,231,10,246,138,114,58,207,154,91,112,110,246,233,72,55,254,87,77,203,235,159,142,158,25,3,178,52,30,235,96,136,193,163,209,239,98,213,94,6,255,249,103,110,237,215,10,181,35,158,70,204,16,99,238,176,156,237,64,154,141,94,207,139,176,240,63,91,198,105,49,126,234,140,54,155,145,33,120,222,27,232,87,13,208,232,11,15,119,179,36,87,93,11,69,67,198,18,18,51,66,115,39,193,180,92,169,26,161,214,44,4,149,50,209,5,234,186,74,248,194,55,76,168,169,235,207,250,34,8,198,206,13,142,210,27,112,48,235,63,94,49,45,93,194,68,25,171,117,5,4,150,93,210,236,86,81,9,189,226,94,5,34,202,74,64,219,246,101,52,177,211,194,71,20,86,108,138,62,5,240,213,17,162,247,27,236,106,70,127,58,78,251,205,141,233,168,120,248,206,49,158,78,2,47,74,41,83,170,209,163,84,148,177,95,48,57,139,218,39,73,32,14,11,120,23,114,74,100,81,237,68,54,197,49,186,249,22,156,68,8,201,101,217,80,180,78,95,226,127,213,68,235,75,139,141,30,146,107,135,69,195,161,37,131,145,233,180,162,204,217,225,133,237,34,242,52,112,140,11,39,33,128,244,24,107,39,232,43,238,59,173,61,129,56,47,123,47,88,148,167,181,2,93,76,148,235,188,208,136,164,27,199,210,203,43,111,70,229,64,227,183,34,31,143,200,97,255,145,211,116,200,197,137,78,5,180,39,226,212,218,253,192,19,180,154,32,87,207,162,52,247,141,69,85,33,118,9,90,143,152,149,135,255,231,34,52,175,88,193,148,238,135,225,132,51,101,16,15,187,30,86,15,255,65,26,93,110,90,135,123,35,200,47,184,244,73,50,160,41,203,202,105,70,10,158,243,245,113,65,64,147,113,141,75,84,174,53,150,222,208,176,133,10,74,102,253,57,216,123,7,241,180,59,210,18,44,200,232,1,218,204,128,131,166,3,121,207,105,164,55,253,155,56,143,129,171,181,53,249,14,178,27,109,224,21,180,128,66,232,49,56,116,210,102,191,49,82,124,49,193,128,103,127,44,16,50,164,227,231,142,51,62,184,199,155,77,219,68,59,234,164,143,182,187,255,68,135,224,254,162,70,91,184,219,36,82,233,50,63,92,231,125,209,44,111,214,249,230,112,127,225,34,138,246,194,221,248,154,252,15,62,168,121,164,26,5,239,11,159,79,37,100,77,237,239,147,149,25,55,254,191,198,105,91,235,72,19,103,153,200,140,63,158,21,253,116,120,83,100,85,55,246,172,193,106,127,151,16,22,212,124,175,253,5,147,108,33,229,47,129,9,23,83,235,248,21,9,108,70,3,60,112,254,152,185,153,222,80,184,43,8,65,96,29,115,58,87,35,212,0,231,190,170,163,137,182,165,254,122,212,185,145,174,111,122,85,238,61,253,222,230,152,51,247,29,5,165,46,70,16,161,200,197,200,206,208,52,235,70,214,5,225,70,233,152,55,77,23,235,142,97,234,32,224,52,69,9,226,137,134,225,65,175,30,255,125,166,182,250,140,121,169,57,145,118,245,76,64,154,51,25,78,164,234,101,8]); +// prettier-ignore +const essrPayloadWrongSender = new Uint8Array([16,216,221,116,161,207,29,16,56,119,38,162,132,222,109,10,210,98,11,197,57,200,145,126,31,253,229,255,103,238,171,121,169,100,71,250,190,57,210,72,114,69,40,19,216,173,154,198,185,34,42,215,148,208,6,45,141,25,192,207,97,10,31,27,226,101,73,124,201,121,10,201,60,163,206,97,97,175,253,5,180,218,232,170,113,227,105,150,65,96,165,61,250,199,185,18,205,157,41,60,239,176,30,46,126,100,105,192,35,224,52,63,208,30,45,51,252,34,93,42,227,166,89,163,244,111,121,202,99,128,60,74,157,88,224,11,25,122,195]); +// prettier-ignore +const essrPayloadNoSender = new Uint8Array([119,138,58,107,131,131,130,130,136,117,95,96,48,74,71,43,135,116,175,134,222,207,167,251,22,116,64,94,254,151,14,38,181,7,111,123,147,187,68,32,151,21,183,245,137,134,58,139,173,58,45,121,137,232,136,63,252,64,247,24,18,203,49,141,210,198,99,210,117,56,18,104,213,202,160,9,218,228,254,102,118,66,29,54,183,118,49,148,112,249,9,31,74,41,0,163,254,173,111,230,147,146,223,106,160,8,23,188,1,62,32,108,116,190,43,95,238,186,240,197,52,84,123,105,249,172,252,251,235,38,29,79,113,243,104,216,124,94,148,87,45,30,249,122,87,83,228,53,168,184,140,76,193,166,8,211,226,45,217,50,33,52,53,0,153,42,70,163,154,1,26,5,34,197,168,30,253,48,45,247,203,221,66,5,18,135,114,228,8,13,178,19,95,184,93,140,236,101,233,78,234,134,250,50,222,142,72,108,43,161,162,80,199,83,23,72,254,215,171,42,91,214,83,218,203,48,84,38,148,33,186,8,141,42,69,62,205,177,195,224,197,139,105,109,85,165,189,240,90,139,4,102,89,111,87,250,155,154,79,227,112,155,249,174,155,73,151,39,159,146,170,26,226,35,123,230,143,105,132,163,100,71,25,136,24,232,156,156,111,51,96,71,7,9,78,66,138,6,25,193,136,19,116,43,190,141,107,74,105,247,132,48,64,135,161,101,140,241,31,125,139,4,82,149,202,18,124,240,238,204,188,247,50,27,113,63,133,199,34,170,88,149,121,118,30,11,35,48,142,138,193,210,125,212,229,199,78,150,153,166,93,63,213,144,160,37,185,193,164,116,137,212,120,141,233,36,147,31,200,95,97,206,118,114,8,19,78,104,57,95,136,66,250,99,31,188,85,218,223,143,219,6,61,144,74,49,255,169,229,148,153,43,21,188,73,24,220,121,151,249,69,59,228,94,7,172,17,39,217,170,46,219,228,153,220,200,172,64,187,10,249,220,101,206,157,8,129,105,91,77,93,35,242,171,165,242,47,173,196,137,185,168,90,104,220,194,73,172,207,172,253,204,187,96,138,122,96,62,191,81,225,160,138,1,228,26,87,249,124,47,76,142,197,250,204,125,14,76,181,208,11,106,239,134,180,93,123,176,178,156,202,72,170,79,81,30,130,46,26,231,147,194,16,165,9,117,66,162,139,229,59,254,95,51,250,252,114,199,5,171,56,184,171,251,233,88,118,203,166,219,241,27,236,112,16,250,165,129,192,8,42,177,108,94,203,135,181,61,186,135,209,173,240,224,58,81,51,94,223,223,234,104,127,169,184,140,44,224,162,70,245,80,117,152,178,63,123,74,102,136,226,253,207,206,49,148,119,235,59,36,14,54,201,210,68,231,53,102,196,68,34,83,252,90,0,51,53,33,250,219,105,140,212,55,11,118,140,132,176,224,137,202,145,13,112,159,166,177,23,163,56,69,37,108,148,235,135,247,127,69,187,88,14,135,230,147,142,213,25,223,173,205,234,45,139,93,193,39,8,47,247,254,226,253,97,120,123,230,24,165,61,236,33,232,113,199,105,95,51,163,57,227,153,224,29,81]); +// prettier-ignore +const essrPayload400Response = new Uint8Array([231,194,204,45,36,145,131,193,37,69,248,198,58,106,28,176,65,80,47,240,163,36,141,29,86,236,43,254,210,90,134,4,119,232,193,33,131,5,16,42,214,126,143,23,111,6,8,99,246,85,147,29,76,39,155,73,243,12,37,91,60,214,220,95,160,0,118,9,233,183,76,191,29,69,119,87,38,243,77,219,96,253,169,64,46,121,49,247,146,160,233,186,8,108,100,112,151,75,45,250,111,55,139,109,107,112,12,81,0,82,224,11,112,24,110,122,224,253,108,175,188,102,182,169,133,149,133,215,138,26,180,198,244,115,8,3,40,202,134,89,104,184,206,130,22,25,15,142,75,236,52,101,158,252,175,121,126,40,120,79,98,141,224,122,54,166,122,99,77,75,186,195,45,235,43,37,98,25,73,136,252,17,254,224,126,73,10,227,73,162,103,41,200,115,17,96,24,175,226,191,158,139,68,83,121,28,108,247,49,27,12,4,34,49,220,119,4,45,152,52,133,145,202,180,65,80,52,83,196,145,184,174,9,74,176,179,42,103,174,192,115,115,170,140,190,19,194,83,27,73,240,170,82,237,219,241,150,44,6,241,206,96,91,230,252,16,99,230,20,161,225,166,133,85,42,66,13,167,155,202,89,25,5,245,215,63,60,48,232,56,80,2,107]); + +// jest-fetch-mock doesn't work well with byte-stream bodies, so mocking this out. Tested in authing.test.ts. +jest.spyOn(Authenticator, 'serializeRequest').mockResolvedValue(` +POST http://127.0.0.1:3901/oobis HTTP/1.1 +content-length: 99 +content-type: application/json +signify-resource: ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose + +{"url":"http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","oobialias":"wit"} +`); fetchMock.mockResponse((req) => { if (req.url.startsWith(url + '/agent')) { @@ -135,42 +158,7 @@ fetchMock.mockResponse((req) => { } else if (req.url == boot_url + '/boot') { return Promise.resolve({ body: '', init: { status: 202 } }); } else { - const headers = new Headers(); - let signed_headers = new Headers(); - - headers.set( - 'Signify-Resource', - 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' - ); - headers.set( - HEADER_SIG_TIME, - new Date().toISOString().replace('Z', '000+00:00') - ); - headers.set('Content-Type', 'application/json'); - - const requrl = new URL(req.url); - const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); - const signer = salter.signer( - 'A', - true, - 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', - Tier.low - ); - - const authn = new Authenticater(signer!, signer!.verfer); - signed_headers = authn.sign( - headers, - req.method, - requrl.pathname.split('?')[0] - ); - const body = req.url.startsWith(url + '/identifiers/aid1/credentials') - ? mockCredential - : mockGetAID; - - return Promise.resolve({ - body: JSON.stringify(body), - init: { status: 202, headers: signed_headers }, - }); + return Promise.resolve({ body: '', init: { status: 202 } }); } }); @@ -269,66 +257,30 @@ describe('SignifyClient', () => { assert.equal(client.groups() instanceof Groups, true); }); - it('Signed fetch', async () => { - await libsodium.ready; + it('Signed headers fetch', async () => { + const clientFetchSpy = jest + .spyOn(SignifyClient.prototype, 'fetch') + .mockImplementation(async (rurl) => { + const body = rurl.startsWith('/identifiers/aid1/credentials') + ? mockCredential + : mockGetAID; + + return Promise.resolve( + new Response(JSON.stringify(body), { + status: 202, + }) + ); + }); + await libsodium.ready; const bran = '0123456789abcdefghijk'; const client = new SignifyClient(url, bran, Tier.low, boot_url); await client.connect(); - let resp = await client.fetch('/contacts', 'GET', undefined); + let resp = await client.saveOldPasscode('1234'); assert.equal(resp.status, 202); let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/contacts'); - assert.equal(lastCall[1]!.method, 'GET'); - let lastHeaders = new Headers(lastCall[1]!.headers!); - assert.equal( - lastHeaders.get('signify-resource'), - 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' - ); - - // Headers in error - let badAgentHeaders = { - 'signify-resource': 'bad_resource', - [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"', - 'content-type': 'application/json', - }; - fetchMock.mockResponseOnce('[]', { - status: 202, - headers: badAgentHeaders, - }); - let t = async () => await client.fetch('/contacts', 'GET', undefined); - await expect(t).rejects.toThrowError( - 'message from a different remote agent' - ); - - badAgentHeaders = { - 'signify-resource': 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', - 'signify-timestamp': '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_tpWAqyCgEoFwRa8bQy20mBB2D5Vik4gRp3XwkEHtqy6iy6SUYAytMUDtRbewotAfkCbad"', - 'content-type': 'application/json', - }; - fetchMock.mockResponseOnce('[]', { - status: 202, - headers: badAgentHeaders, - }); - t = async () => await client.fetch('/contacts', 'GET', undefined); - await expect(t).rejects.toThrowError( - 'Signature for EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei invalid.' - ); - - // Other calls - resp = await client.saveOldPasscode('1234'); - assert.equal(resp.status, 202); - lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( lastCall[0]!, url + '/salt/ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' @@ -378,9 +330,9 @@ describe('SignifyClient', () => { assert.equal(resReq.method, 'POST'); lastBody = await resReq.json(); assert.deepEqual(lastBody.foo, true); - lastHeaders = new Headers(resReq.headers); + const lastHeaders = new Headers(resReq.headers); assert.equal( - lastHeaders.get('signify-resource'), + lastHeaders.get(HEADER_SIG_SENDER), 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK' ); assert.equal( @@ -416,38 +368,361 @@ describe('SignifyClient', () => { const sig = signer.sign(raw); assert.equal( sig.qb64, - lastHeaders - .get('signature') - ?.split('signify="')[1] - .split('"')[0] + lastHeaders.get(HEADER_SIG)?.split('signify="')[1].split('"')[0] ); } else { fail(`${HEADER_SIG_INPUT} is empty`); } + + clientFetchSpy.mockRestore(); }); - test('includes HTTP status info in error message', async () => { + it('ESSR protected fetch', async () => { await libsodium.ready; const bran = '0123456789abcdefghijk'; const client = new SignifyClient(url, bran, Tier.low, boot_url); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow('Client needs to call connect first'); + await client.connect(); - fetchMock.mockResolvedValue( - new Response('Error info', { - status: 400, - statusText: 'Bad Request', + const headers = new Headers(); + fetchMock.mockResolvedValueOnce( + new Response(null, { + status: 200, + headers, + }) + ); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow('Signature is missing from ESSR payload'); + + headers.set( + HEADER_SIG, + 'indexed="?0";signify="0BB50Boq4s2xcFNjskRLziD-dmw443Y3ObeKfd1xjmNTLBQEXkT3Vj67xVD9Fv7OKZysD7xN6sQ_SxWLM8DaCyXX"' + ); + fetchMock.mockResolvedValueOnce( + new Response(null, { + status: 200, + headers, + }) + ); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow('Message from a different remote agent'); + + headers.set( + HEADER_SIG, + 'indexed="?0";signify="0BB50Boq4s2xcFNjskRLziD-dmw443Y3ObeKfd1xjmNTLBQEXkT3Vj67xVD9Fv7OKZysD7xN6sQ_SxWLM8DaCyXX"' + ); + fetchMock.mockResolvedValueOnce( + new Response(null, { + status: 200, + headers, + }) + ); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow('Message from a different remote agent'); + + headers.set( + HEADER_SIG_SENDER, + 'EPHceqLKZg1o95PuA-_47ffBOkpTjVWGQ9LsYf9M8Cs6' + ); // Wrong + fetchMock.mockResolvedValueOnce( + new Response(null, { + status: 200, + headers, + }) + ); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow('Message from a different remote agent'); + + headers.set( + HEADER_SIG_SENDER, + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); // Right + fetchMock.mockResolvedValueOnce( + new Response(null, { + status: 200, + headers, + }) + ); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow( + 'Invalid ESSR payload, missing or incorrect destination prefix' + ); + + headers.set( + HEADER_SIG_DESTINATION, + 'EPHceqLKZg1o95PuA-_47ffBOkpTjVWGQ9LsYf9M8Cs6' + ); // Wrong + fetchMock.mockResolvedValueOnce( + new Response(null, { + status: 200, + headers, + }) + ); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow( + 'Invalid ESSR payload, missing or incorrect destination prefix' + ); + + headers.set( + HEADER_SIG_DESTINATION, + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); // Right + fetchMock.mockResolvedValueOnce( + new Response(null, { + status: 200, + headers, + }) + ); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow('Timestamp is missing from ESSR payload'); + + headers.set(HEADER_SIG_TIME, '2025-01-16T16:37:10.345000+00:00'); + fetchMock.mockResolvedValueOnce( + new Response(null, { + status: 200, + headers, + }) + ); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow('Invalid signature'); + + let signed = signWithAgent( + new Uint8Array( + Buffer.from( + JSON.stringify({ + src: 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + dest: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + d: 'ECgtcE3D9hvXYqvsMKLxIfj8-nmOM6XOy4mArqxDWIR8', + dt: '2025-01-16T16:37:10.345000+00:00', + }) + ) + ) + ); + signed.forEach((value, key) => { + headers.set(key, value); + }); + fetchMock.mockResolvedValueOnce( + new Response(essrPayloadNoSender, { + status: 200, + headers, + }) + ); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow( + 'Invalid ESSR payload, missing or incorrect encrypted sender' + ); + + signed = signWithAgent( + new Uint8Array( + Buffer.from( + JSON.stringify({ + src: 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + dest: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + d: 'EKVuGrO8K7Yi5uV03HMn5Q_LqfbCJvvLzFixClN_QVLN', + dt: '2025-01-16T16:37:10.345000+00:00', + }) + ) + ) + ); + signed.forEach((value, key) => { + headers.set(key, value); + }); + fetchMock.mockResolvedValueOnce( + new Response(essrPayloadWrongSender, { + status: 200, + headers, }) ); + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow( + 'Invalid ESSR payload, missing or incorrect encrypted sender' + ); - const error = await client - .fetch('/somepath', 'GET', undefined) - .catch((e) => e); + signed = signWithAgent( + new Uint8Array( + Buffer.from( + JSON.stringify({ + src: 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + dest: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + d: 'EABLUXFJKkV9ey8_-yNnQDhuDkiJ_s5tPZNwYg2g21C5', + dt: '2025-01-16T16:37:10.345000+00:00', + }) + ) + ) + ); + signed.forEach((value, key) => { + headers.set(key, value); + }); + fetchMock.mockResolvedValueOnce( + new Response(essrPayload, { + status: 200, + headers, + }) + ); - assert(error instanceof Error); + const response = await client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }); + assert.equal(response.status, 202); + assert.equal( + response.headers.get(HEADER_SIG_SENDER), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); assert.equal( - error.message, - 'HTTP GET /somepath - 400 Bad Request - Error info' + (await response.text()).replace(/ /g, ''), + JSON.stringify({ + name: 'oobi.0ABZPhjVcllT3Sa2u61PRpqd', + metadata: { + oobi: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + }, + done: true, + error: null, + response: { + vn: [1, 0], + i: 'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + s: '0', + p: '', + d: 'EIkO4CUmYXukX4auGU9yaFoQaIicfVZkazQ0A3IO5biT', + f: '0', + dt: '2025-01-16T16:29:47.586818+00:00', + et: 'icp', + kt: '1', + k: ['BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha'], + nt: '0', + n: [], + bt: '0', + b: [], + c: [], + ee: { + s: '0', + d: 'EIkO4CUmYXukX4auGU9yaFoQaIicfVZkazQ0A3IO5biT', + br: [], + ba: [], + }, + di: '', + }, + }) + ); + }); + + it('HTTP errors are protected except 401', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.connect(); + + fetchMock.mockResolvedValueOnce( + new Response(null, { + status: 401, + statusText: 'Unauthorized', + }) + ); + + await expect( + client.fetch('/somepath', 'GET', undefined) + ).rejects.toThrow('HTTP GET /somepath - 401 Unauthorized'); + + const headers = new Headers({ + [HEADER_SIG_SENDER]: 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + [HEADER_SIG_DESTINATION]: + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + [HEADER_SIG_TIME]: '2025-01-16T16:37:10.345000+00:00', + }); + + const signed = signWithAgent( + new Uint8Array( + Buffer.from( + JSON.stringify({ + src: 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + dest: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + d: 'EM5HKVnEIAzAXdasQ85zSgN5i2ZQEBVI-GoejNUwk0cy', + dt: '2025-01-16T16:37:10.345000+00:00', + }) + ) + ) + ); + signed.forEach((value, key) => { + headers.set(key, value); + }); + + fetchMock.mockResolvedValueOnce( + new Response(essrPayload400Response, { + status: 200, + headers, + }) + ); + + await expect( + client.fetch('/oobis', 'POST', { + url: 'http://localhost:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha', + alias: 'wit', + }) + ).rejects.toThrow( + 'HTTP POST /oobis - 400 Bad Request - {"title": "400 Bad Request", "description": "invalid OOBI request body, either \'rpy\' or \'url\' is required"}' ); }); }); + +function signWithAgent(payload: Uint8Array): Headers { + const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); + const signer = salter.signer( + 'A', + true, + 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', + Tier.low + ); + const sig = signer.sign(payload); + const markers = new Map(); + markers.set('signify', sig); + const signage = new Signage(markers, false); + return signature([signage]); +} diff --git a/test/app/contacting.test.ts b/test/app/contacting.test.ts index 858d6e79..a6e3f0b1 100644 --- a/test/app/contacting.test.ts +++ b/test/app/contacting.test.ts @@ -1,12 +1,11 @@ import { strict as assert } from 'assert'; import { SignifyClient } from '../../src/keri/app/clienting'; -import { Authenticater } from '../../src/keri/core/authing'; -import { Salter, Tier } from '../../src/keri/core/salter'; +import { Tier } from '../../src/keri/core/salter'; import libsodium from 'libsodium-wrappers-sumo'; -import fetchMock from 'jest-fetch-mock'; +import jsFetchMock from 'jest-fetch-mock'; import 'whatwg-fetch'; -fetchMock.enableMocks(); +jsFetchMock.enableMocks(); const url = 'http://127.0.0.1:3901'; const boot_url = 'http://127.0.0.1:3903'; @@ -71,46 +70,24 @@ const mockGetAID = { windexes: [], }; -fetchMock.mockResponse((req) => { +const fetchMock = jest + .spyOn(SignifyClient.prototype, 'fetch') + .mockImplementation(async () => { + const body = mockGetAID; + + return Promise.resolve( + new Response(JSON.stringify(body), { + status: 202, + }) + ); + }); +jsFetchMock.mockResponse((req) => { if (req.url.startsWith(url + '/agent')) { return Promise.resolve({ body: mockConnect, init: { status: 202 } }); } else if (req.url == boot_url + '/boot') { return Promise.resolve({ body: '', init: { status: 202 } }); } else { - const headers = new Headers(); - let signed_headers = new Headers(); - - headers.set( - 'Signify-Resource', - 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' - ); - headers.set( - 'Signify-Timestamp', - new Date().toISOString().replace('Z', '000+00:00') - ); - headers.set('Content-Type', 'application/json'); - - const requrl = new URL(req.url); - const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); - const signer = salter.signer( - 'A', - true, - 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', - Tier.low - ); - - const authn = new Authenticater(signer!, signer!.verfer); - signed_headers = authn.sign( - headers, - req.method, - requrl.pathname.split('?')[0] - ); - const body = mockGetAID; - - return Promise.resolve({ - body: JSON.stringify(body), - init: { status: 202, headers: signed_headers }, - }); + throw new Error('Wrong fetch used'); } }); @@ -129,19 +106,18 @@ describe('Contacting', () => { await contacts.list('mygroup', 'company', 'mycompany'); let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + - '/contacts?group=mygroup&filter_field=company&filter_value=mycompany' + lastCall[0], + '/contacts?group=mygroup&filter_field=company&filter_value=mycompany' ); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[1], 'GET'); await contacts.get('EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + lastCall[0], + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' ); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[1], 'GET'); const info = { name: 'John Doe', @@ -152,12 +128,12 @@ describe('Contacting', () => { info ); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - let lastBody = JSON.parse(lastCall[1]!.body!.toString()); + let lastBody = lastCall[2]; assert.equal( - lastCall[0]!, - url + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + lastCall[0], + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' ); - assert.equal(lastCall[1]!.method, 'POST'); + assert.equal(lastCall[1], 'POST'); assert.deepEqual(lastBody, info); await contacts.update( @@ -165,22 +141,22 @@ describe('Contacting', () => { info ); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - lastBody = JSON.parse(lastCall[1]!.body!.toString()); + lastBody = lastCall[2]; assert.equal( - lastCall[0]!, - url + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + lastCall[0], + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' ); - assert.equal(lastCall[1]!.method, 'PUT'); + assert.equal(lastCall[1], 'PUT'); assert.deepEqual(lastBody, info); await contacts.delete('EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + lastCall[0], + '/contacts/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' ); - assert.equal(lastCall[1]!.method, 'DELETE'); - assert.equal(lastCall[1]!.body, undefined); + assert.equal(lastCall[1], 'DELETE'); + assert.equal(lastCall[2], undefined); }); it('Challenges', async () => { @@ -196,8 +172,8 @@ describe('Contacting', () => { await challenges.generate(128); let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/challenges?strength=128'); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[0], '/challenges?strength=128'); + assert.equal(lastCall[1], 'GET'); const words = [ 'shell', @@ -219,9 +195,9 @@ describe('Contacting', () => { words ); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/identifiers/aid1/exchanges'); - assert.equal(lastCall[1]!.method, 'POST'); - let lastBody = JSON.parse(lastCall[1]!.body!.toString()); + let lastBody = lastCall[2]; + assert.equal(lastCall[0], '/identifiers/aid1/exchanges'); + assert.equal(lastCall[1], 'POST'); assert.equal(lastBody.tpc, 'challenge'); assert.equal(lastBody.exn.r, '/challenge/response'); assert.equal( @@ -237,12 +213,11 @@ describe('Contacting', () => { ); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + - '/challenges_verify/EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p' + lastCall[0], + '/challenges_verify/EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p' ); - assert.equal(lastCall[1]!.method, 'POST'); - lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal(lastCall[1], 'POST'); + lastBody = lastCall[2]; assert.deepEqual(lastBody.words, words); await challenges.responded( @@ -251,12 +226,11 @@ describe('Contacting', () => { ); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + - '/challenges_verify/EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p' + lastCall[0], + '/challenges_verify/EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p' ); - assert.equal(lastCall[1]!.method, 'PUT'); - lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal(lastCall[1], 'PUT'); + lastBody = lastCall[2]; assert.equal( lastBody.said, 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' diff --git a/test/app/coring.test.ts b/test/app/coring.test.ts index 1325b945..3bf5abba 100644 --- a/test/app/coring.test.ts +++ b/test/app/coring.test.ts @@ -7,18 +7,15 @@ import { OperationsDeps, } from '../../src/keri/app/coring'; import { SignifyClient } from '../../src/keri/app/clienting'; -import { Authenticater } from '../../src/keri/core/authing'; -import { Salter, Tier } from '../../src/keri/core/salter'; -import fetchMock from 'jest-fetch-mock'; +import { Tier } from '../../src/keri/core/salter'; +import jsFetchMock from 'jest-fetch-mock'; import 'whatwg-fetch'; import { randomUUID } from 'crypto'; -fetchMock.enableMocks(); - const url = 'http://127.0.0.1:3901'; const boot_url = 'http://127.0.0.1:3903'; -fetchMock.enableMocks(); +jsFetchMock.enableMocks(); const mockConnect = '{"agent":{"vn":[1,0],"i":"EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei",' + @@ -118,48 +115,26 @@ const mockCredential = { }, }; -fetchMock.mockResponse((req) => { +const fetchMock = jest + .spyOn(SignifyClient.prototype, 'fetch') + .mockImplementation(async (rurl) => { + const body = rurl.startsWith('/identifiers/aid1/credentials') + ? mockCredential + : mockGetAID; + + return Promise.resolve( + new Response(JSON.stringify(body), { + status: 202, + }) + ); + }); +jsFetchMock.mockResponse((req) => { if (req.url.startsWith(url + '/agent')) { return Promise.resolve({ body: mockConnect, init: { status: 202 } }); } else if (req.url == boot_url + '/boot') { return Promise.resolve({ body: '', init: { status: 202 } }); } else { - const headers = new Headers(); - let signed_headers = new Headers(); - - headers.set( - 'Signify-Resource', - 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' - ); - headers.set( - 'Signify-Timestamp', - new Date().toISOString().replace('Z', '000+00:00') - ); - headers.set('Content-Type', 'application/json'); - - const requrl = new URL(req.url); - const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); - const signer = salter.signer( - 'A', - true, - 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', - Tier.low - ); - - const authn = new Authenticater(signer!, signer!.verfer); - signed_headers = authn.sign( - headers, - req.method, - requrl.pathname.split('?')[0] - ); - const body = req.url.startsWith(url + '/identifiers/aid1/credentials') - ? mockCredential - : mockGetAID; - - return Promise.resolve({ - body: JSON.stringify(body), - init: { status: 202, headers: signed_headers }, - }); + throw new Error('Wrong fetch used'); } }); @@ -189,21 +164,21 @@ describe('Coring', () => { await oobis.get('aid', 'agent'); let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/identifiers/aid/oobis?role=agent'); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[0], '/identifiers/aid/oobis?role=agent'); + assert.equal(lastCall[1], 'GET'); await oobis.resolve('http://oobiurl.com'); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - let lastBody = JSON.parse(lastCall[1]!.body!.toString()); - assert.equal(lastCall[0]!, url + '/oobis'); - assert.equal(lastCall[1]!.method, 'POST'); + let lastBody = lastCall[2]; + assert.equal(lastCall[0], '/oobis'); + assert.equal(lastCall[1], 'POST'); assert.deepEqual(lastBody.url, 'http://oobiurl.com'); await oobis.resolve('http://oobiurl.com', 'witness'); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - lastBody = JSON.parse(lastCall[1]!.body!.toString()); - assert.equal(lastCall[0]!, url + '/oobis'); - assert.equal(lastCall[1]!.method, 'POST'); + lastBody = lastCall[2]; + assert.equal(lastCall[0], '/oobis'); + assert.equal(lastCall[1], 'POST'); assert.deepEqual(lastBody.url, 'http://oobiurl.com'); assert.deepEqual(lastBody.oobialias, 'witness'); }); @@ -223,18 +198,18 @@ describe('Coring', () => { await keyEvents.get('EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX'); let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + '/events?pre=EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX' + lastCall[0], + '/events?pre=EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX' ); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[1], 'GET'); await keyStates.get('EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX'); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( lastCall[0]!, - url + '/states?pre=EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX' + '/states?pre=EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX' ); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[1], 'GET'); await keyStates.list([ 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX', @@ -242,11 +217,10 @@ describe('Coring', () => { ]); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + - '/states?pre=EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX&pre=ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK' + lastCall[0], + '/states?pre=EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX&pre=ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK' ); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[1], 'GET'); await keyStates.query( 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX', @@ -254,9 +228,9 @@ describe('Coring', () => { 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' ); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - const lastBody = JSON.parse(lastCall[1]!.body!.toString()); - assert.equal(lastCall[0]!, url + '/queries'); - assert.equal(lastCall[1]!.method, 'POST'); + const lastBody = lastCall[2]; + assert.equal(lastCall[0], '/queries'); + assert.equal(lastCall[1], 'POST'); assert.equal( lastBody.pre, 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX' @@ -281,8 +255,8 @@ describe('Coring', () => { await config.get(); const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/config'); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[0], '/config'); + assert.equal(lastCall[1], 'GET'); }); }); diff --git a/test/app/credentialing.test.ts b/test/app/credentialing.test.ts index 1cd5bea3..b864de28 100644 --- a/test/app/credentialing.test.ts +++ b/test/app/credentialing.test.ts @@ -1,10 +1,8 @@ import { strict as assert } from 'assert'; import { SignifyClient } from '../../src/keri/app/clienting'; - -import { Authenticater } from '../../src/keri/core/authing'; -import { Salter, Tier } from '../../src/keri/core/salter'; +import { Tier } from '../../src/keri/core/salter'; import libsodium from 'libsodium-wrappers-sumo'; -import fetchMock from 'jest-fetch-mock'; +import jsFetchMock from 'jest-fetch-mock'; import 'whatwg-fetch'; import { d, @@ -19,7 +17,7 @@ import { versify, } from '../../src'; -fetchMock.enableMocks(); +jsFetchMock.enableMocks(); const url = 'http://127.0.0.1:3901'; const boot_url = 'http://127.0.0.1:3903'; @@ -173,7 +171,20 @@ const mockCredential = { }, }; -fetchMock.mockResponse((req) => { +const fetchMock = jest + .spyOn(SignifyClient.prototype, 'fetch') + .mockImplementation(async (rurl) => { + const body = rurl.startsWith('/credentials') + ? mockCredential + : mockGetAID; + + return Promise.resolve( + new Response(JSON.stringify(body), { + status: 202, + }) + ); + }); +jsFetchMock.mockResponse((req) => { if (req.url.startsWith(url + '/agent')) { return Promise.resolve({ body: JSON.stringify(mockConnect), @@ -182,42 +193,7 @@ fetchMock.mockResponse((req) => { } else if (req.url == boot_url + '/boot') { return Promise.resolve({ body: '', init: { status: 202 } }); } else { - const headers = new Headers(); - let signed_headers = new Headers(); - - headers.set( - 'Signify-Resource', - 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' - ); - headers.set( - 'Signify-Timestamp', - new Date().toISOString().replace('Z', '000+00:00') - ); - headers.set('Content-Type', 'application/json'); - - const requrl = new URL(req.url); - const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); - const signer = salter.signer( - 'A', - true, - 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', - Tier.low - ); - - const authn = new Authenticater(signer!, signer!.verfer); - signed_headers = authn.sign( - headers, - req.method, - requrl.pathname.split('?')[0] - ); - const body = req.url.startsWith(url + '/credentials') - ? mockCredential - : mockGetAID; - - return Promise.resolve({ - body: JSON.stringify(body), - init: { status: 202, headers: signed_headers }, - }); + throw new Error('Wrong fetch used'); } }); @@ -243,9 +219,9 @@ describe('Credentialing', () => { }; await credentials.list(kargs); let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - let lastBody = JSON.parse(lastCall[1]!.body!.toString()); - assert.equal(lastCall[0]!, url + '/credentials/query'); - assert.equal(lastCall[1]!.method, 'POST'); + let lastBody = lastCall[2]; + assert.equal(lastCall[0], '/credentials/query'); + assert.equal(lastCall[1], 'POST'); assert.deepEqual(lastBody, kargs); await credentials.get( @@ -254,10 +230,10 @@ describe('Credentialing', () => { ); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + '/credentials/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + lastCall[0], + '/credentials/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' ); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[1], 'GET'); const registry = 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX'; const schema = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'; @@ -268,9 +244,9 @@ describe('Credentialing', () => { a: { i: isuee, LEI: '1234' }, }); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - lastBody = JSON.parse(lastCall[1]!.body!.toString()); - assert.equal(lastCall[0]!, url + '/identifiers/aid1/credentials'); - assert.equal(lastCall[1]!.method, 'POST'); + lastBody = lastCall[2]; + assert.equal(lastCall[0], '/identifiers/aid1/credentials'); + assert.equal(lastCall[1], 'POST'); assert.equal(lastBody.acdc.ri, registry); assert.equal(lastBody.acdc.s, schema); assert.equal(lastBody.acdc.a.i, isuee); @@ -285,15 +261,16 @@ describe('Credentialing', () => { assert.equal(lastBody.sigs[0].substring(0, 2), 'AA'); assert.equal(lastBody.sigs[0].length, 88); + console.log(`lastbbody is ${JSON.stringify(lastBody, null, 2)}`); const credential = lastBody.acdc.i; await credentials.revoke('aid1', credential); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - lastBody = JSON.parse(lastCall[1]!.body!.toString()); + lastBody = lastCall[2]; assert.equal( - lastCall[0]!, - url + '/identifiers/aid1/credentials/' + credential + lastCall[0], + '/identifiers/aid1/credentials/' + credential ); - assert.equal(lastCall[1]!.method, 'DELETE'); + assert.equal(lastCall[1], 'DELETE'); assert.equal(lastBody.rev.s, '1'); assert.equal(lastBody.rev.t, 'rev'); assert.equal( @@ -320,20 +297,19 @@ describe('Credentialing', () => { lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( lastCall[0]!, - url + - '/registries/EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df/EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo' + '/registries/EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df/EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo' ); - assert.equal(lastCall[1]!.method, 'GET'); - assert.equal(lastCall[1]!.body, null); + assert.equal(lastCall[1], 'GET'); + assert.equal(lastCall[2], null); await credentials.delete(mockCredential.sad.d); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + '/credentials/EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo' + lastCall[0], + '/credentials/EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo' ); - assert.equal(lastCall[1]!.method, 'DELETE'); - assert.equal(lastCall[1]!.body, undefined); + assert.equal(lastCall[1], 'DELETE'); + assert.equal(lastCall[2], undefined); }); }); @@ -462,10 +438,7 @@ describe('Ipex', () => { await ipex.submitGrant('multisig', ng, ngsigs, ngend, [holder]); let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal( - lastCall[0], - 'http://127.0.0.1:3901/identifiers/multisig/ipex/grant' - ); + assert.equal(lastCall[0], '/identifiers/multisig/ipex/grant'); const [admit, asigs, aend] = await ipex.admit({ senderName: 'holder', @@ -495,10 +468,7 @@ describe('Ipex', () => { await ipex.submitAdmit('multisig', admit, asigs, aend, [holder]); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal( - lastCall[0], - 'http://127.0.0.1:3901/identifiers/multisig/ipex/admit' - ); + assert.equal(lastCall[0], '/identifiers/multisig/ipex/admit'); assert.equal(aend, ''); }); @@ -575,10 +545,7 @@ describe('Ipex', () => { await ipex.submitApply('multisig', apply, applySigs, [holder]); let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal( - lastCall[0], - 'http://127.0.0.1:3901/identifiers/multisig/ipex/apply' - ); + assert.equal(lastCall[0], '/identifiers/multisig/ipex/apply'); const [offer, offerSigs, offerEnd] = await ipex.offer({ senderName: 'multisig', @@ -630,10 +597,7 @@ describe('Ipex', () => { holder, ]); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal( - lastCall[0], - 'http://127.0.0.1:3901/identifiers/multisig/ipex/offer' - ); + assert.equal(lastCall[0], '/identifiers/multisig/ipex/offer'); const [agree, agreeSigs, agreeEnd] = await ipex.agree({ senderName: 'multisig', @@ -667,10 +631,7 @@ describe('Ipex', () => { await ipex.submitAgree('multisig', agree, agreeSigs, [holder]); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal( - lastCall[0], - 'http://127.0.0.1:3901/identifiers/multisig/ipex/agree' - ); + assert.equal(lastCall[0], '/identifiers/multisig/ipex/agree'); const [grant, gsigs, end] = await ipex.grant({ senderName: 'multisig', @@ -762,10 +723,7 @@ describe('Ipex', () => { await ipex.submitGrant('multisig', ng, ngsigs, ngend, [holder]); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal( - lastCall[0], - 'http://127.0.0.1:3901/identifiers/multisig/ipex/grant' - ); + assert.equal(lastCall[0], '/identifiers/multisig/ipex/grant'); const [admit, asigs, aend] = await ipex.admit({ senderName: 'holder', @@ -795,10 +753,7 @@ describe('Ipex', () => { await ipex.submitAdmit('multisig', admit, asigs, aend, [holder]); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal( - lastCall[0], - 'http://127.0.0.1:3901/identifiers/multisig/ipex/admit' - ); + assert.equal(lastCall[0], '/identifiers/multisig/ipex/admit'); assert.equal(aend, ''); }); @@ -865,9 +820,6 @@ describe('Ipex', () => { holder, ]); const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal( - lastCall[0], - 'http://127.0.0.1:3901/identifiers/multisig/ipex/offer' - ); + assert.equal(lastCall[0], '/identifiers/multisig/ipex/offer'); }); }); diff --git a/test/app/delegating.test.ts b/test/app/delegating.test.ts index 6f7336fe..736cd7d7 100644 --- a/test/app/delegating.test.ts +++ b/test/app/delegating.test.ts @@ -1,12 +1,11 @@ import { strict as assert } from 'assert'; -import { Salter, Tier } from '../../src'; +import { Tier } from '../../src'; import libsodium from 'libsodium-wrappers-sumo'; import { SignifyClient } from '../../src/keri/app/clienting'; -import { Authenticater } from '../../src/keri/core/authing'; -import fetchMock from 'jest-fetch-mock'; +import jsFetchMock from 'jest-fetch-mock'; import 'whatwg-fetch'; -fetchMock.enableMocks(); +jsFetchMock.enableMocks(); const url = 'http://127.0.0.1:3901'; const boot_url = 'http://127.0.0.1:3903'; @@ -71,46 +70,24 @@ const mockGetAID = { windexes: [], }; -fetchMock.mockResponse((req) => { +const fetchMock = jest + .spyOn(SignifyClient.prototype, 'fetch') + .mockImplementation(async () => { + const body = mockGetAID; + + return Promise.resolve( + new Response(JSON.stringify(body), { + status: 202, + }) + ); + }); +jsFetchMock.mockResponse((req) => { if (req.url.startsWith(url + '/agent')) { return Promise.resolve({ body: mockConnect, init: { status: 202 } }); } else if (req.url == boot_url + '/boot') { return Promise.resolve({ body: '', init: { status: 202 } }); } else { - const headers = new Headers(); - let signed_headers = new Headers(); - - headers.set( - 'Signify-Resource', - 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' - ); - headers.set( - 'Signify-Timestamp', - new Date().toISOString().replace('Z', '000+00:00') - ); - headers.set('Content-Type', 'application/json'); - - const requrl = new URL(req.url); - const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); - const signer = salter.signer( - 'A', - true, - 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', - Tier.low - ); - - const authn = new Authenticater(signer!, signer!.verfer); - signed_headers = authn.sign( - headers, - req.method, - requrl.pathname.split('?')[0] - ); - const body = mockGetAID; - - return Promise.resolve({ - body: JSON.stringify(body), - init: { status: 202, headers: signed_headers }, - }); + throw new Error('Wrong fetch used'); } }); @@ -127,11 +104,10 @@ describe('delegate', () => { ); const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + - '/identifiers/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao/delegation' + lastCall[0], + '/identifiers/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao/delegation' ); - assert.equal(lastCall[1]!.method, 'POST'); + assert.equal(lastCall[1], 'POST'); const expectedBody = { ixn: { v: 'KERI10JSON0000cf_', @@ -157,9 +133,6 @@ describe('delegate', () => { transferable: true, }, }; - assert.equal( - lastCall[1]?.body?.toString(), - JSON.stringify(expectedBody) - ); + assert.equal(JSON.stringify(lastCall[2]), JSON.stringify(expectedBody)); }); }); diff --git a/test/app/escrowing.test.ts b/test/app/escrowing.test.ts index 10049123..db3beef9 100644 --- a/test/app/escrowing.test.ts +++ b/test/app/escrowing.test.ts @@ -1,12 +1,11 @@ import { strict as assert } from 'assert'; import { SignifyClient } from '../../src/keri/app/clienting'; -import { Authenticater } from '../../src/keri/core/authing'; -import { Salter, Tier } from '../../src/keri/core/salter'; -import fetchMock from 'jest-fetch-mock'; +import { Tier } from '../../src/keri/core/salter'; +import jsFetchMock from 'jest-fetch-mock'; import libsodium from 'libsodium-wrappers-sumo'; import 'whatwg-fetch'; -fetchMock.enableMocks(); +jsFetchMock.enableMocks(); const url = 'http://127.0.0.1:3901'; const boot_url = 'http://127.0.0.1:3903'; @@ -109,48 +108,26 @@ const mockCredential = { }, }; -fetchMock.mockResponse((req) => { +const fetchMock = jest + .spyOn(SignifyClient.prototype, 'fetch') + .mockImplementation(async (rurl) => { + const body = rurl.startsWith('/identifiers/aid1/credentials') + ? mockCredential + : mockGetAID; + + return Promise.resolve( + new Response(JSON.stringify(body), { + status: 202, + }) + ); + }); +jsFetchMock.mockResponse((req) => { if (req.url.startsWith(url + '/agent')) { return Promise.resolve({ body: mockConnect, init: { status: 202 } }); } else if (req.url == boot_url + '/boot') { return Promise.resolve({ body: '', init: { status: 202 } }); } else { - const headers = new Headers(); - let signed_headers = new Headers(); - - headers.set( - 'Signify-Resource', - 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' - ); - headers.set( - 'Signify-Timestamp', - new Date().toISOString().replace('Z', '000+00:00') - ); - headers.set('Content-Type', 'application/json'); - - const requrl = new URL(req.url); - const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); - const signer = salter.signer( - 'A', - true, - 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', - Tier.low - ); - - const authn = new Authenticater(signer!, signer!.verfer); - signed_headers = authn.sign( - headers, - req.method, - requrl.pathname.split('?')[0] - ); - const body = req.url.startsWith(url + '/identifiers/aid1/credentials') - ? mockCredential - : mockGetAID; - - return Promise.resolve({ - body: JSON.stringify(body), - init: { status: 202, headers: signed_headers }, - }); + throw new Error('Wrong fetch used'); } }); @@ -170,9 +147,9 @@ describe('SignifyClient', () => { await escrows.listReply('/presentation/request'); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + '/escrows/rpy?route=%2Fpresentation%2Frequest' + lastCall[0], + '/escrows/rpy?route=%2Fpresentation%2Frequest' ); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[1], 'GET'); }); }); diff --git a/test/app/exchanging.test.ts b/test/app/exchanging.test.ts index e1c01d40..098b0325 100644 --- a/test/app/exchanging.test.ts +++ b/test/app/exchanging.test.ts @@ -12,11 +12,10 @@ import { } from '../../src'; import libsodium from 'libsodium-wrappers-sumo'; import { SignifyClient } from '../../src/keri/app/clienting'; -import { Authenticater } from '../../src/keri/core/authing'; -import fetchMock from 'jest-fetch-mock'; +import jsFetchMock from 'jest-fetch-mock'; import 'whatwg-fetch'; -fetchMock.enableMocks(); +jsFetchMock.enableMocks(); const url = 'http://127.0.0.1:3901'; const boot_url = 'http://127.0.0.1:3903'; @@ -119,48 +118,26 @@ const mockCredential = { }, }; -fetchMock.mockResponse((req) => { +const fetchMock = jest + .spyOn(SignifyClient.prototype, 'fetch') + .mockImplementation(async (rurl) => { + const body = rurl.startsWith('/identifiers/aid1/credentials') + ? mockCredential + : mockGetAID; + + return Promise.resolve( + new Response(JSON.stringify(body), { + status: 202, + }) + ); + }); +jsFetchMock.mockResponse((req) => { if (req.url.startsWith(url + '/agent')) { return Promise.resolve({ body: mockConnect, init: { status: 202 } }); } else if (req.url == boot_url + '/boot') { return Promise.resolve({ body: '', init: { status: 202 } }); } else { - const headers = new Headers(); - let signed_headers = new Headers(); - - headers.set( - 'Signify-Resource', - 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' - ); - headers.set( - 'Signify-Timestamp', - new Date().toISOString().replace('Z', '000+00:00') - ); - headers.set('Content-Type', 'application/json'); - - const requrl = new URL(req.url); - const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); - const signer = salter.signer( - 'A', - true, - 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', - Tier.low - ); - - const authn = new Authenticater(signer!, signer!.verfer); - signed_headers = authn.sign( - headers, - req.method, - requrl.pathname.split('?')[0] - ); - const body = req.url.startsWith(url + '/identifiers/aid1/credentials') - ? mockCredential - : mockGetAID; - - return Promise.resolve({ - body: JSON.stringify(body), - init: { status: 202, headers: signed_headers }, - }); + throw new Error('Wrong fetch used'); } }); @@ -379,8 +356,8 @@ describe('exchange', () => { let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; await exchange.sendFromEvents('aid1', '', serder, [''], '', []); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/identifiers/aid1/exchanges'); - assert.equal(lastCall[1]!.method, 'POST'); + assert.equal(lastCall[0], '/identifiers/aid1/exchanges'); + assert.equal(lastCall[1], 'POST'); }); it('Get exchange', async () => { @@ -393,9 +370,9 @@ describe('exchange', () => { await exchanges.get('EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'); const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + '/exchanges/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' + lastCall[0], + '/exchanges/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao' ); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[1], 'GET'); }); }); diff --git a/test/app/grouping.test.ts b/test/app/grouping.test.ts index 9685af03..e2709280 100644 --- a/test/app/grouping.test.ts +++ b/test/app/grouping.test.ts @@ -1,12 +1,11 @@ import { strict as assert } from 'assert'; import { SignifyClient } from '../../src/keri/app/clienting'; -import { Authenticater } from '../../src/keri/core/authing'; -import { Salter, Tier } from '../../src/keri/core/salter'; -import fetchMock from 'jest-fetch-mock'; +import { Tier } from '../../src/keri/core/salter'; +import jsFetchMock from 'jest-fetch-mock'; import libsodium from 'libsodium-wrappers-sumo'; import 'whatwg-fetch'; -fetchMock.enableMocks(); +jsFetchMock.enableMocks(); const url = 'http://127.0.0.1:3901'; const boot_url = 'http://127.0.0.1:3903'; @@ -109,48 +108,26 @@ const mockCredential = { }, }; -fetchMock.mockResponse((req) => { +const fetchMock = jest + .spyOn(SignifyClient.prototype, 'fetch') + .mockImplementation(async (rurl) => { + const body = rurl.startsWith('/identifiers/aid1/credentials') + ? mockCredential + : mockGetAID; + + return Promise.resolve( + new Response(JSON.stringify(body), { + status: 202, + }) + ); + }); +jsFetchMock.mockResponse((req) => { if (req.url.startsWith(url + '/agent')) { return Promise.resolve({ body: mockConnect, init: { status: 202 } }); } else if (req.url == boot_url + '/boot') { return Promise.resolve({ body: '', init: { status: 202 } }); } else { - const headers = new Headers(); - let signed_headers = new Headers(); - - headers.set( - 'Signify-Resource', - 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' - ); - headers.set( - 'Signify-Timestamp', - new Date().toISOString().replace('Z', '000+00:00') - ); - headers.set('Content-Type', 'application/json'); - - const requrl = new URL(req.url); - const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); - const signer = salter.signer( - 'A', - true, - 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', - Tier.low - ); - - const authn = new Authenticater(signer!, signer!.verfer); - signed_headers = authn.sign( - headers, - req.method, - requrl.pathname.split('?')[0] - ); - const body = req.url.startsWith(url + '/identifiers/aid1/credentials') - ? mockCredential - : mockGetAID; - - return Promise.resolve({ - body: JSON.stringify(body), - init: { status: 202, headers: signed_headers }, - }); + throw new Error('Wrong fetch used'); } }); @@ -168,19 +145,18 @@ describe('Grouping', () => { let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; await groups.sendRequest('aid1', {}, [], ''); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/identifiers/aid1/multisig/request'); - assert.equal(lastCall[1]!.method, 'POST'); + assert.equal(lastCall[0], '/identifiers/aid1/multisig/request'); + assert.equal(lastCall[1], 'POST'); await groups.getRequest( 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00' ); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; assert.equal( - lastCall[0]!, - url + - '/multisig/request/ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00' + lastCall[0], + '/multisig/request/ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00' ); - assert.equal(lastCall[1]!.method, 'GET'); + assert.equal(lastCall[1], 'GET'); await groups.join( 'aid1', @@ -191,7 +167,7 @@ describe('Grouping', () => { ['a', 'b', 'c'] ); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/identifiers/aid1/multisig/join'); - assert.equal(lastCall[1]!.method, 'POST'); + assert.equal(lastCall[0], '/identifiers/aid1/multisig/join'); + assert.equal(lastCall[1], 'POST'); }); }); diff --git a/test/app/notifying.test.ts b/test/app/notifying.test.ts index b40b481b..ca063f90 100644 --- a/test/app/notifying.test.ts +++ b/test/app/notifying.test.ts @@ -1,12 +1,11 @@ import { strict as assert } from 'assert'; -import { Authenticater } from '../../src/keri/core/authing'; -import { Salter, Tier } from '../../src/keri/core/salter'; +import { Tier } from '../../src/keri/core/salter'; import { SignifyClient } from '../../src/keri/app/clienting'; import libsodium from 'libsodium-wrappers-sumo'; -import fetchMock from 'jest-fetch-mock'; +import jsFetchMock from 'jest-fetch-mock'; import 'whatwg-fetch'; -fetchMock.enableMocks(); +jsFetchMock.enableMocks(); const url = 'http://127.0.0.1:3901'; const boot_url = 'http://127.0.0.1:3903'; @@ -109,48 +108,26 @@ const mockCredential = { }, }; -fetchMock.mockResponse((req) => { +const fetchMock = jest + .spyOn(SignifyClient.prototype, 'fetch') + .mockImplementation(async (rurl) => { + const body = rurl.startsWith('/identifiers/aid1/credentials') + ? mockCredential + : mockGetAID; + + return Promise.resolve( + new Response(JSON.stringify(body), { + status: 202, + }) + ); + }); +jsFetchMock.mockResponse((req) => { if (req.url.startsWith(url + '/agent')) { return Promise.resolve({ body: mockConnect, init: { status: 202 } }); } else if (req.url == boot_url + '/boot') { return Promise.resolve({ body: '', init: { status: 202 } }); } else { - const headers = new Headers(); - let signed_headers = new Headers(); - - headers.set( - 'Signify-Resource', - 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' - ); - headers.set( - 'Signify-Timestamp', - new Date().toISOString().replace('Z', '000+00:00') - ); - headers.set('Content-Type', 'application/json'); - - const requrl = new URL(req.url); - const salter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); - const signer = salter.signer( - 'A', - true, - 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', - Tier.low - ); - - const authn = new Authenticater(signer!, signer!.verfer); - signed_headers = authn.sign( - headers, - req.method, - requrl.pathname.split('?')[0] - ); - const body = req.url.startsWith(url + '/identifiers/aid1/credentials') - ? mockCredential - : mockGetAID; - - return Promise.resolve({ - body: JSON.stringify(body), - init: { status: 202, headers: signed_headers }, - }); + throw new Error('Wrong fetch used'); } }); @@ -168,19 +145,19 @@ describe('SignifyClient', () => { await notifications.list(20, 40); let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/notifications'); - assert.equal(lastCall[1]!.method, 'GET'); - const lastHeaders = new Headers(lastCall[1]!.headers!); + assert.equal(lastCall[0], '/notifications'); + assert.equal(lastCall[1], 'GET'); + const lastHeaders = new Headers(lastCall[3]); assert.equal(lastHeaders.get('Range'), 'notes=20-40'); await notifications.mark('notificationSAID'); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/notifications/notificationSAID'); - assert.equal(lastCall[1]!.method, 'PUT'); + assert.equal(lastCall[0], '/notifications/notificationSAID'); + assert.equal(lastCall[1], 'PUT'); await notifications.delete('notificationSAID'); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/notifications/notificationSAID'); - assert.equal(lastCall[1]!.method, 'DELETE'); + assert.equal(lastCall[0], '/notifications/notificationSAID'); + assert.equal(lastCall[1], 'DELETE'); }); }); diff --git a/test/core/authing.test.ts b/test/core/authing.test.ts index e1ad5069..41610278 100644 --- a/test/core/authing.test.ts +++ b/test/core/authing.test.ts @@ -2,11 +2,32 @@ import { strict as assert } from 'assert'; import libsodium from 'libsodium-wrappers-sumo'; import { Salter } from '../../src/keri/core/salter'; import { b } from '../../src/keri/core/core'; -import { Authenticater } from '../../src/keri/core/authing'; +import { Authenticator } from '../../src/keri/core/authing'; import * as utilApi from '../../src/keri/core/utils'; import { Verfer } from '../../src/keri/core/verfer'; +import { + Cigar, + Diger, + HEADER_SIG_TIME, + HEADER_SIG, + HEADER_SIG_DESTINATION, + HEADER_SIG_SENDER, + MtrDex, + Siger, + Tier, + d, + designature, + HEADER_SIG_INPUT, +} from '../../src'; -describe('Authenticater.verify', () => { +// prettier-ignore +const essrPayload = new Uint8Array([134,89,250,128,50,135,60,33,214,52,216,194,200,42,118,33,91,130,129,141,158,102,96,66,95,163,32,235,6,239,150,82,59,67,100,70,116,25,10,180,189,26,104,114,166,121,247,185,12,105,147,232,68,248,238,58,53,200,129,173,34,216,228,153,190,240,53,53,134,194,69,152,21,209,3,225,5,221,57,220,159,249,90,85,73,197,64,155,168,217,24,111,211,100,129,18,21,57,70,152,77,65,156,71,84,186,222,81,82,204,120,176,67,173,207,149,39,180,129,192,22,194,84,57,226,15,4,48,240,133,54,170,34,211,204,141,15,204,78]); +// prettier-ignore +const essrPayloadWrongSender = new Uint8Array([226,12,182,1,251,73,45,83,28,139,226,10,38,143,81,108,254,153,187,150,224,12,78,189,13,202,196,57,112,107,169,10,254,92,196,213,107,81,206,11,140,157,195,207,55,32,26,203,6,131,80,156,192,249,122,254,58,126,184,87,134,17,40,55,147,76,74,17,222,50,38,154,22,81,157,74,239,179,251,103,180,95,236,122,143,94,215,233,179,227,239,95,156,220,248,230,219,243,220,247,132,181,159,210,138,132,185,96,58,155,41,189,71,233,28,171,149,25,58,42,13,13,13,109,60,39,224,39,112,145,58,220,0,239,224,10,23]); +// prettier-ignore +const essrPayloadNoSender = new Uint8Array([211,17,77,180,175,67,71,163,82,144,48,142,91,91,10,103,94,105,147,205,199,227,247,67,90,111,35,140,32,123,217,84,18,58,68,206,7,132,222,70,220,110,73,116,30,5,40,45,108,247,129,190,211,112,159,123,207,246,231,0,1,27,188,210,135,4,238,102,130,218,20,5,60]); + +describe('Authenticator.verify', () => { it('verify signature on Response', async () => { await libsodium.ready; const salt = '0123456789abcdef'; @@ -41,7 +62,7 @@ describe('Authenticater.verify', () => { ['Signify-Timestamp', '2023-05-22T00:37:00.248708+00:00'], ]); - const authn = new Authenticater(signer, verfer); + const authn = new Authenticator(signer, verfer); assert.notEqual(authn, undefined); assert.equal( @@ -51,7 +72,7 @@ describe('Authenticater.verify', () => { }); }); -describe('Authenticater.sign', () => { +describe('Authenticator.sign', () => { it('Create signed headers for a request', async () => { await libsodium.ready; const salt = '0123456789abcdef'; @@ -74,11 +95,11 @@ describe('Authenticater.sign', () => { new Date('2021-01-01T00:00:00.000000+00:00') ); - const authn = new Authenticater(signer, verfer); + const authn = new Authenticator(signer, verfer); const result = authn.sign(headers, 'POST', '/boot'); - assert.equal(result.has('Signature-Input'), true); - assert.equal(result.has('Signature'), true); + assert.equal(result.has(HEADER_SIG_INPUT), true); + assert.equal(result.has(HEADER_SIG), true); const expectedSignatureInput = [ 'signify=("@method" "@path" "signify-resource" "signify-timestamp")', @@ -92,6 +113,306 @@ describe('Authenticater.sign', () => { 'indexed="?0"', 'signify="0BChvN_BWAf-mgEuTnWfNnktgHdWOuOh9cWc4o0GFWuZOwra3DyJT5dJ_6BX7AANDOTnIlAKh5Sg_9qGQXHjj5oJ"', ].join(';'); - assert.equal(result.get('Signature'), expectedSignature); + assert.equal(result.get(HEADER_SIG), expectedSignature); + }); +}); + +describe('ESSR', () => { + it('Can wrap a HTTP request with ESSR', async () => { + await libsodium.ready; + const salt = '0123456789abcdef'; + const salter = new Salter({ raw: b(salt) }); + const signer = salter.signer(); + + const agentSalter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); + const agentSigner = agentSalter.signer( + 'A', + true, + 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', + Tier.low + ); + + const sigkey = new Uint8Array( + agentSigner.raw.length + agentSigner.verfer.raw.length + ); + sigkey.set(agentSigner.raw); + sigkey.set(agentSigner.verfer.raw, agentSigner.raw.length); + const agentPriv = + libsodium.crypto_sign_ed25519_sk_to_curve25519(sigkey); + const agentPub = libsodium.crypto_scalarmult_base(agentPriv); + + const authn = new Authenticator(signer, agentSigner.verfer); + + const getReq = new Request('http://127.0.0.1:3901/oobis', { + method: 'GET', + }); + + const wrapperGet = await authn.wrap( + getReq, + 'http://127.0.0.1:3901', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); + assert.equal(wrapperGet.url, 'http://127.0.0.1:3901/'); + assert.equal(wrapperGet.method, 'POST'); + assert.equal( + wrapperGet.headers.get(HEADER_SIG_SENDER), + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); + assert.equal( + wrapperGet.headers.get(HEADER_SIG_DESTINATION), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); + + const dt = wrapperGet.headers.get(HEADER_SIG_TIME); + assert.notEqual(dt, null); + assert.equal( + wrapperGet.headers.get('Content-Type'), + 'application/octet-stream' + ); + + const signature = wrapperGet.headers.get('Signature'); + assert.notEqual(signature, null); + + const ciphertextGet = new Uint8Array(await wrapperGet.arrayBuffer()); + const diger = new Diger({ code: MtrDex.Blake3_256 }, ciphertextGet); + + const payload = { + src: 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + dest: 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + d: diger.qb64, + dt, + }; + + const signages = designature(signature!); + const markers = signages[0].markers as Map; + const cig = markers.get('signify'); + + assert.equal( + signer.verfer.verify( + cig!.raw, + Buffer.from(JSON.stringify(payload)) + ), + true + ); + + const plaintextGet = d( + libsodium.crypto_box_seal_open(ciphertextGet, agentPub, agentPriv) + ); + assert.equal( + plaintextGet, + `GET http://127.0.0.1:3901/oobis HTTP/1.1\r +\r +` + ); + + const postReq = new Request('http://127.0.0.1:3901/oobis', { + method: 'POST', + body: JSON.stringify({ + a: 1, + }), + }); + const wrapperPost = await authn.wrap( + postReq, + 'http://127.0.0.1:3901', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); + const ciphertextPost = new Uint8Array(await wrapperPost.arrayBuffer()); + const plaintextPost = d( + libsodium.crypto_box_seal_open(ciphertextPost, agentPub, agentPriv) + ); + assert.equal( + plaintextPost, + `POST http://127.0.0.1:3901/oobis HTTP/1.1\r +content-type: text/plain;charset=UTF-8\r +\r +{"a":1}` + ); + }); + + it('Can unwrap HTTP requests', async () => { + await libsodium.ready; + const salt = '0123456789abcdef'; + const salter = new Salter({ raw: b(salt) }); + const signer = salter.signer(); + + const agentSalter = new Salter({ qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm' }); + const agentSigner = agentSalter.signer( + 'A', + true, + 'agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00', + Tier.low + ); + + const authn = new Authenticator(signer, agentSigner.verfer); + + const headers = new Headers(); + await expect( + authn.unwrap( + new Response(null, { headers }), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ) + ).rejects.toThrow('Signature is missing from ESSR payload'); + + headers.set( + HEADER_SIG, + 'indexed="?0";signify="0BB50Boq4s2xcFNjskRLziD-dmw443Y3ObeKfd1xjmNTLBQEXkT3Vj67xVD9Fv7OKZysD7xN6sQ_SxWLM8DaCyXX' + ); + await expect( + authn.unwrap( + new Response(null, { headers }), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ) + ).rejects.toThrow('Message from a different remote agent'); + + // Wrong + headers.set( + HEADER_SIG_SENDER, + 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1' + ); + await expect( + authn.unwrap( + new Response(null, { headers }), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ) + ).rejects.toThrow('Message from a different remote agent'); + + // Right + headers.set( + HEADER_SIG_SENDER, + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei' + ); + await expect( + authn.unwrap( + new Response(null, { headers }), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ) + ).rejects.toThrow( + 'Invalid ESSR payload, missing or incorrect destination prefix' + ); + + // Wrong + headers.set( + HEADER_SIG_DESTINATION, + 'EMQQpnSkgfUOgWdzQTWfrgiVHKIDAhvAZIPQ6z3EAfz1' + ); + await expect( + authn.unwrap( + new Response(null, { headers }), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ) + ).rejects.toThrow( + 'Invalid ESSR payload, missing or incorrect destination prefix' + ); + + // Right + headers.set( + HEADER_SIG_DESTINATION, + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); + await expect( + authn.unwrap( + new Response(null, { headers }), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ) + ).rejects.toThrow('Timestamp is missing from ESSR payload'); + + headers.set(HEADER_SIG_TIME, '2025-01-17T11:57:56.415000+00:00'); + await expect( + authn.unwrap( + new Response(null, { headers }), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ) + ).rejects.toThrow('Invalid signature'); + + headers.set( + 'Signature', + 'indexed="?0";signify="0BBLnK_-YI-sV4pZYe2kUkyPsuEvrnwKID__0t-kHD9p7pVxJEosxsClFUok4qgt1ULjl_irj13zUd-JqQQQx3MN' + ); + await expect( + authn.unwrap( + new Response(essrPayloadNoSender, { status: 200, headers }), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ) + ).rejects.toThrow( + 'Invalid ESSR payload, missing or incorrect encrypted sender' + ); + + headers.set(HEADER_SIG_TIME, '2025-01-17T12:00:18.260000+00:00'); + headers.set( + HEADER_SIG, + 'indexed="?0";signify="0BC4LCV6ZqPOzAVpyjPpi2v0AJOVwE7o3qnL2PAJ56ReMizfgzbo3DQK3HiKHkIJ2N5G5R0fno6Nhs6QTrB8CMII' + ); + await expect( + authn.unwrap( + new Response(essrPayloadWrongSender, { status: 200, headers }), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ) + ).rejects.toThrow( + 'Invalid ESSR payload, missing or incorrect encrypted sender' + ); + + headers.set(HEADER_SIG_TIME, '2025-01-17T12:04:16.254000+00:00'); + headers.set( + HEADER_SIG, + 'indexed="?0";signify="0BBQZQrG5mhWU2w9nSC45Dd-PIOYKjtD3KFY-arNKj0whNrUhdlmW0_m_Y487uOdDBR6_XbR0Ey2TqXNt9gAvEMB' + ); + const unwrapped = await authn.unwrap( + new Response(essrPayload, { status: 200, headers }), + 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose' + ); + assert.equal(await unwrapped.text(), JSON.stringify({ a: 1 })); + assert.equal(unwrapped.status, 200); + }); +}); + +describe('Authenticator.serializeRequest', () => { + it('Can serialise a GET request', async () => { + const request = new Request('http://127.0.0.1:3901/oobis', { + method: 'GET', + headers: { + [HEADER_SIG_SENDER]: + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + }, + }); + assert.equal( + await Authenticator.serializeRequest(request), + `GET http://127.0.0.1:3901/oobis HTTP/1.1\r +signify-resource: ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose\r +\r +` + ); + }); + + it('Can serialise a POST request', async () => { + const request = new Request('http://127.0.0.1:3901/oobis', { + method: 'POST', + body: JSON.stringify({ + a: 1, + }), + headers: { + [HEADER_SIG_SENDER]: + 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + }, + }); + assert.equal( + await Authenticator.serializeRequest(request), + `POST http://127.0.0.1:3901/oobis HTTP/1.1\r +content-type: text/plain;charset=UTF-8\r +signify-resource: ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose\r +\r +{"a":1}` + ); }); });