Skip to content

Commit

Permalink
feat: ESSR tunneling with KERIA API
Browse files Browse the repository at this point in the history
  • Loading branch information
iFergal committed Jan 17, 2025
1 parent cddb007 commit a80e9ff
Show file tree
Hide file tree
Showing 13 changed files with 1,041 additions and 626 deletions.
3 changes: 3 additions & 0 deletions examples/integration-scripts/test-setup-single-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
97 changes: 47 additions & 50 deletions src/keri/app/clienting.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Authenticater } from '../core/authing';
import { Authenticator } from '../core/authing';
import { HEADER_SIG_TIME } from '../core/httping';
import { ExternalModule, KeyManager } from '../core/keeping';
import { Tier } from '../core/salter';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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!
);
Expand All @@ -172,63 +172,60 @@ export class SignifyClient {
data: any,
extraHeaders?: Headers
): Promise<Response> {
const headers = new Headers();
let signed_headers = new Headers();
const final_headers = new Headers();
if (!this.authn) {
throw new Error('Client needs to call connect first');
}

const 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');
}

signed_headers.forEach((value, key) => {
final_headers.set(key, value);
});
if (extraHeaders !== undefined) {
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;
}

/**
Expand All @@ -253,7 +250,7 @@ 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
);
Expand Down
208 changes: 205 additions & 3 deletions src/keri/core/authing.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import libsodium from 'libsodium-wrappers-sumo';
import { Signer } from './signer';
import { Verfer } from './verfer';
import {
Expand All @@ -10,19 +11,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 {
Expand Down Expand Up @@ -94,7 +115,7 @@ export class Authenticater {
fields?: Array<string>
): Headers {
if (fields == undefined) {
fields = Authenticater.DefaultFields;
fields = Authenticator.DefaultFields;
}

const [header, sig] = siginput(this._csig, {
Expand All @@ -121,4 +142,185 @@ export class Authenticater {

return headers;
}

async wrap(
request: Request,
baseUrl: string,
sender: string,
receiver: string
): Promise<Request> {
const dt = new Date().toISOString().replace('Z', '000+00:00');

const headers = new Headers();
headers.set('Signify-Resource', sender);
headers.set('Signify-Receiver', receiver);
headers.set('Signify-Timestamp', 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<string, Siger | Cigar>();
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<Response> {
const signature = wrapper.headers.get('Signature');
if (!signature) {
throw new Error('Signature is missing from ESSR payload');
}

if (wrapper.headers.get('Signify-Resource') !== sender) {
throw new Error('Message from a different remote agent');
}

if (wrapper.headers.get('Signify-Receiver') !== receiver) {
throw new Error(
'Invalid ESSR payload, missing or incorrect destination prefix'
);
}

const dt = wrapper.headers.get('Signify-Timestamp');
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<string, Siger | Cigar>;
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('Signify-Resource') !== 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,
});
}
}
Loading

0 comments on commit a80e9ff

Please sign in to comment.