Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ x-python-env: &python-env

services:
vlei-server:
image: gleif/vlei
image: gleif/vlei:0.2.0
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kentbull There seems to be a problem with the latest vLEI docker image - see here: https://github.com/WebOfTrust/signify-ts/actions/runs/15855574402/job/44699896180 in Print logs section.

Server seems to be returning application/json instead of application/schema+json and it's causing KERIA to crash. (which sounds like another unhandled issue)

For now, locking it to the old version here works to get the integration tests to pass.

environment:
<<: *python-env
command: vLEI-server -s ./schema/acdc -c ./samples/acdc/ -o ./samples/oobis/
Expand Down
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

186 changes: 83 additions & 103 deletions src/keri/app/clienting.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Authenticater } from '../core/authing.ts';
import { HEADER_SIG_TIME } from '../core/httping.ts';
import {
EssrAuthenticator,
SignedHeaderAuthenticator,
Authenticator,
} from '../core/authing.ts';
import { HEADER_SIG_SENDER, HEADER_SIG_TIME } from '../core/httping.ts';
import { ExternalModule, IdentifierManagerFactory } from '../core/keeping.ts';
import { Tier } from '../core/salter.ts';

Expand Down Expand Up @@ -30,21 +34,27 @@ class State {
}
}

export enum AuthMode {
SignedHeaders = 'SIGNED_HEADERS',
ESSR = 'ESSR',
}

/**
* An in-memory key manager that can connect to a KERIA Agent and use it to
* receive messages and act as a proxy for multi-signature operations and delegation operations.
*/
export class SignifyClient {
public controller: Controller;
public url: string;
public bran: string;
public pidx: number;
public agent: Agent | null;
public authn: Authenticater | null;
public manager: IdentifierManagerFactory | null;
public tier: Tier;
public bootUrl: string;
public exteralModules: ExternalModule[];
controller: Controller;
url: string;
bran: string;
pidx: number;
agent: Agent | null;
authn: Authenticator | null;
manager: IdentifierManagerFactory | null;
tier: Tier;
bootUrl: string;
exteralModules: ExternalModule[];
authMode: AuthMode;

/**
* SignifyClient constructor
Expand All @@ -59,7 +69,8 @@ export class SignifyClient {
bran: string,
tier: Tier = Tier.low,
bootUrl: string = DEFAULT_BOOT_URL,
externalModules: ExternalModule[] = []
externalModules: ExternalModule[] = [],
authMode: AuthMode = AuthMode.SignedHeaders
) {
this.url = url;
if (bran.length < 21) {
Expand All @@ -74,6 +85,7 @@ export class SignifyClient {
this.tier = tier;
this.bootUrl = bootUrl;
this.exteralModules = externalModules;
this.authMode = authMode;
}

get data() {
Expand Down Expand Up @@ -154,10 +166,18 @@ export class SignifyClient {
this.controller.salter,
this.exteralModules
);
this.authn = new Authenticater(
this.controller.signer,
this.agent.verfer!
);

if (this.authMode === AuthMode.SignedHeaders) {
this.authn = new SignedHeaderAuthenticator(
this.controller.signer,
this.agent.verfer!
);
} else {
this.authn = new EssrAuthenticator(
this.controller.signer,
this.agent.verfer!
);
}
}

/**
Expand All @@ -175,63 +195,55 @@ 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');
}

headers.set('Signify-Resource', this.controller.pre);
const headers = new Headers();
headers.set(HEADER_SIG_SENDER, 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);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any reason to not sign extraHeaders

});
}
const res = await fetch(this.url + path, {
method: method,
body: _body,
headers: final_headers,
});
if (!res.ok) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo signature should be verified for non 2xx responses too

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 baseRequest = new Request(this.url + path, {
method,
path.split('?')[0]
body,
headers,
});
const request = await this.authn.prepare(
baseRequest,
this.controller.pre,
this.agent!.pre
);
if (verification) {
return res;
} else {
throw new Error('response verification failed');

const res = await this.authn.verify(
baseRequest,
await fetch(request),
this.controller.pre,
this.agent!.pre
);

if (!res.ok) {
const error = await res.text();
throw new Error(
`HTTP ${method} ${path} - ${res.status} ${res.statusText} - ${error}`
);
}

return res;
}

/**
Expand All @@ -256,34 +268,35 @@ export class SignifyClient {
const hab = await this.identifiers().get(aidName);
const keeper = this.manager!.get(hab);

const authenticator = new Authenticater(
const authenticator = new SignedHeaderAuthenticator(
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')
);

const signed_headers = authenticator.sign(
new Headers(headers),
req.method ?? 'GET',
new URL(url).pathname
return await authenticator.prepare(
new Request(url, {
headers,
method: req.method ?? 'GET',
body: req.body,
}),
hab.prefix,
hab.prefix
);
req.headers = signed_headers;

return new Request(url, req);
}

/**
* Approve the delegation of the client AID to the KERIA agent
* @async
* @returns {Promise<Response>} A promise to the result of the approval
*/
async approveDelegation(): Promise<Response> {
private async approveDelegation(): Promise<Response> {
const sigs = this.controller.approveDelegation(this.agent!);

const data = {
Expand All @@ -303,39 +316,6 @@ export class SignifyClient {
);
}

/**
* Save old client passcode in KERIA agent
* @async
* @param {string} passcode Passcode to be saved
* @returns {Promise<Response>} A promise to the result of the save
*/
async saveOldPasscode(passcode: string): Promise<Response> {
const caid = this.controller?.pre;
const body = { salt: passcode };
return await fetch(this.url + '/salt/' + caid, {
method: 'PUT',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
}

/**
* Delete a saved passcode from KERIA agent
* @async
* @returns {Promise<Response>} A promise to the result of the deletion
*/
async deletePasscode(): Promise<Response> {
const caid = this.controller?.pre;
return await fetch(this.url + '/salt/' + caid, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
}

/**
* Rotate the client AID
* @async
Expand Down
Loading
Loading