signify-ts 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,42 @@
1
- import { desiginput, HEADER_SIG_INPUT, HEADER_SIG_TIME, normalize, siginput, } from "./httping.js";
1
+ import libsodium from 'libsodium-wrappers-sumo';
2
+ import { desiginput, HEADER_SIG, HEADER_SIG_DESTINATION, HEADER_SIG_INPUT, HEADER_SIG_SENDER, HEADER_SIG_TIME, normalize, siginput, } from "./httping.js";
2
3
  import { Signage, signature, designature } from "../end/ending.js";
3
- export class Authenticater {
4
+ import { Diger } from "./diger.js";
5
+ import { MtrDex } from "./matter.js";
6
+ import { b, d } from "./core.js";
7
+ export class Authenticator {
4
8
  constructor(csig, verfer) {
5
- this._csig = csig;
6
- this._verfer = verfer;
9
+ this.csig = csig;
10
+ this.verfer = verfer;
7
11
  }
8
- verify(headers, method, path) {
12
+ }
13
+ export class SignedHeaderAuthenticator extends Authenticator {
14
+ async prepare(request, _local, _remote) {
15
+ const headers = request.headers;
16
+ const signedHeaders = this.sign(request.headers, request.method, new URL(request.url).pathname);
17
+ signedHeaders.forEach((value, key) => {
18
+ headers.set(key, value);
19
+ });
20
+ return request;
21
+ }
22
+ async verify(request, response, _local, remote) {
23
+ if (response.status === 401) {
24
+ throw new Error(`HTTP ${request.method} ${new URL(request.url).pathname} - ${response.status} ${response.statusText}`);
25
+ }
26
+ if (remote !== response.headers.get(HEADER_SIG_SENDER)) {
27
+ throw new Error('message from a different remote agent');
28
+ }
29
+ if (!this.verifyHeaders(response.headers, request.method, new URL(request.url).pathname)) {
30
+ throw new Error('response verification failed');
31
+ }
32
+ return response;
33
+ }
34
+ verifyHeaders(headers, method, path) {
9
35
  const siginput = headers.get(HEADER_SIG_INPUT);
10
36
  if (siginput == null) {
11
37
  return false;
12
38
  }
13
- const signature = headers.get('Signature');
39
+ const signature = headers.get(HEADER_SIG);
14
40
  if (signature == null) {
15
41
  return false;
16
42
  }
@@ -61,24 +87,21 @@ export class Authenticater {
61
87
  const signage = designature(signature);
62
88
  const markers = signage[0].markers;
63
89
  const cig = markers.get(input.name);
64
- if (!cig || !this._verfer.verify(cig.raw, ser)) {
90
+ if (!cig || !this.verfer.verify(cig.raw, ser)) {
65
91
  throw new Error(`Signature for ${input.keyid} invalid.`);
66
92
  }
67
93
  });
68
94
  return true;
69
95
  }
70
- sign(headers, method, path, fields) {
71
- if (fields == undefined) {
72
- fields = Authenticater.DefaultFields;
73
- }
74
- const [header, sig] = siginput(this._csig, {
96
+ sign(headers, method, path) {
97
+ const [header, sig] = siginput(this.csig, {
75
98
  name: 'signify',
76
99
  method,
77
100
  path,
78
101
  headers,
79
- fields,
102
+ fields: SignedHeaderAuthenticator.DefaultFields,
80
103
  alg: 'ed25519',
81
- keyid: this._csig.verfer.qb64,
104
+ keyid: this.csig.verfer.qb64,
82
105
  });
83
106
  header.forEach((value, key) => {
84
107
  headers.append(key, value);
@@ -93,9 +116,159 @@ export class Authenticater {
93
116
  return headers;
94
117
  }
95
118
  }
96
- Authenticater.DefaultFields = [
119
+ SignedHeaderAuthenticator.DefaultFields = [
97
120
  '@method',
98
121
  '@path',
99
122
  'signify-resource',
100
123
  HEADER_SIG_TIME.toLowerCase(),
101
124
  ];
125
+ export class EssrAuthenticator extends Authenticator {
126
+ constructor(csig, verfer) {
127
+ super(csig, verfer);
128
+ const sigkey = new Uint8Array(this.csig.raw.length + this.csig.verfer.raw.length);
129
+ sigkey.set(this.csig.raw);
130
+ sigkey.set(this.csig.verfer.raw, this.csig.raw.length);
131
+ this.cx25519Priv =
132
+ libsodium.crypto_sign_ed25519_sk_to_curve25519(sigkey);
133
+ this.cx25519Pub = libsodium.crypto_scalarmult_base(this.cx25519Priv);
134
+ this.vx25519Pub = libsodium.crypto_sign_ed25519_pk_to_curve25519(this.verfer.raw);
135
+ }
136
+ async prepare(request, local, remote) {
137
+ const dt = new Date().toISOString().replace('Z', '000+00:00');
138
+ const headers = new Headers();
139
+ headers.set(HEADER_SIG_SENDER, local);
140
+ headers.set(HEADER_SIG_DESTINATION, remote);
141
+ headers.set(HEADER_SIG_TIME, dt);
142
+ headers.set('Content-Type', 'application/octet-stream');
143
+ const requestStr = await EssrAuthenticator.serializeRequest(request);
144
+ const raw = libsodium.crypto_box_seal(requestStr, this.vx25519Pub);
145
+ const diger = new Diger({ code: MtrDex.Blake3_256 }, raw);
146
+ const payload = {
147
+ src: local,
148
+ dest: remote,
149
+ d: diger.qb64,
150
+ dt,
151
+ };
152
+ const sig = this.csig.sign(b(JSON.stringify(payload)));
153
+ const markers = new Map();
154
+ markers.set('signify', sig);
155
+ const signage = new Signage(markers, false);
156
+ const signed = signature([signage]);
157
+ signed.forEach((value, key) => {
158
+ headers.append(key, value);
159
+ });
160
+ return new Request(new URL(request.url).origin + '/', {
161
+ method: 'POST',
162
+ body: raw,
163
+ headers,
164
+ });
165
+ }
166
+ async verify(request, response, local, remote) {
167
+ if (response.status === 401) {
168
+ throw new Error(`HTTP ${request.method} ${new URL(request.url).pathname} - ${response.status} ${response.statusText}`);
169
+ }
170
+ return await this.unwrap(response, remote, local);
171
+ }
172
+ static async serializeRequest(request) {
173
+ let headers = '';
174
+ request.headers.forEach((value, name) => {
175
+ headers += `${name}: ${value}\r\n`;
176
+ });
177
+ let body = '';
178
+ if (request.method !== 'GET' && request.body) {
179
+ const bytes = await this.streamToBytes(request.body);
180
+ try {
181
+ body = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
182
+ }
183
+ catch (e) {
184
+ throw new Error('Failed to serialize ESSR request - body is not valid UTF-8', { cause: e });
185
+ }
186
+ }
187
+ return `${request.method} ${request.url} HTTP/1.1\r\n${headers}\r\n${body}`;
188
+ }
189
+ static async streamToBytes(stream) {
190
+ const reader = stream.getReader();
191
+ const chunks = [];
192
+ let done, value;
193
+ while ((({ done, value } = await reader.read()), !done)) {
194
+ if (value)
195
+ chunks.push(value);
196
+ }
197
+ reader.releaseLock();
198
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
199
+ const result = new Uint8Array(totalLength);
200
+ let offset = 0;
201
+ for (const chunk of chunks) {
202
+ result.set(chunk, offset);
203
+ offset += chunk.length;
204
+ }
205
+ return result;
206
+ }
207
+ async unwrap(wrapper, sender, receiver) {
208
+ const signature = wrapper.headers.get(HEADER_SIG);
209
+ if (!signature) {
210
+ throw new Error('Signature is missing from ESSR payload');
211
+ }
212
+ if (wrapper.headers.get(HEADER_SIG_SENDER) !== sender) {
213
+ throw new Error('Message from a different remote agent');
214
+ }
215
+ if (wrapper.headers.get(HEADER_SIG_DESTINATION) !== receiver) {
216
+ throw new Error('Invalid ESSR payload, missing or incorrect destination prefix');
217
+ }
218
+ const dt = wrapper.headers.get(HEADER_SIG_TIME);
219
+ if (!dt) {
220
+ throw new Error('Timestamp is missing from ESSR payload');
221
+ }
222
+ const ciphertext = new Uint8Array(await wrapper.arrayBuffer());
223
+ const diger = new Diger({ code: MtrDex.Blake3_256 }, ciphertext);
224
+ const payload = {
225
+ src: sender,
226
+ dest: receiver,
227
+ d: diger.qb64,
228
+ dt,
229
+ };
230
+ const signages = designature(signature);
231
+ const markers = signages[0].markers;
232
+ const cig = markers.get('signify');
233
+ if (!cig) {
234
+ throw new Error('Invalid signature format - missing "signify" marker');
235
+ }
236
+ const verified = this.verfer.verify(cig.raw, b(JSON.stringify(payload)));
237
+ if (!verified) {
238
+ throw new Error('Invalid signature');
239
+ }
240
+ const response = EssrAuthenticator.deserializeResponse(d(this.decrypt(ciphertext)));
241
+ if (response.headers.get(HEADER_SIG_SENDER) !== sender) {
242
+ throw new Error('Invalid ESSR payload, missing or incorrect encrypted sender');
243
+ }
244
+ return response;
245
+ }
246
+ // KERIA seals the response to the client key it has in the controller's KEL
247
+ decrypt(ciphertext) {
248
+ try {
249
+ return libsodium.crypto_box_seal_open(ciphertext, this.cx25519Pub, this.cx25519Priv);
250
+ }
251
+ catch (e) {
252
+ throw new Error('Failed to decrypt ESSR response - sealed to a different client key', { cause: e });
253
+ }
254
+ }
255
+ static deserializeResponse(httpString) {
256
+ const sep = httpString.indexOf('\r\n\r\n');
257
+ const head = sep === -1 ? httpString.trimEnd() : httpString.slice(0, sep);
258
+ const [statusLine, ...headerLines] = head.split('\r\n');
259
+ const [, statusCode, ...statusTextArr] = statusLine.split(' ');
260
+ const body = sep === -1 ? '' : httpString.slice(sep + 4);
261
+ const headers = new Headers();
262
+ for (const line of headerLines) {
263
+ const i = line.indexOf(':');
264
+ if (i !== -1) {
265
+ headers.append(line.slice(0, i).trim(), line.slice(i + 1).trim());
266
+ }
267
+ }
268
+ return new Response(body.length ? body : null, {
269
+ status: Number(statusCode),
270
+ statusText: statusTextArr.join(' '),
271
+ headers,
272
+ });
273
+ }
274
+ }
@@ -286,7 +286,7 @@ export function messagize(serder, sigers, seal, wigers, cigars, pipelined = fals
286
286
  atc = concat(atc, new Counter({ code: CtrDex.TransIdxSigGroups, count: 1 })
287
287
  .qb64b);
288
288
  atc = concat(atc, new TextEncoder().encode(seal[1].i));
289
- atc = concat(atc, new Seqner({ sn: parseInt(seal[1].s) }).qb64b);
289
+ atc = concat(atc, new Seqner({ snh: seal[1].s }).qb64b);
290
290
  atc = concat(atc, new TextEncoder().encode(seal[1].d));
291
291
  }
292
292
  else if (seal[0] == 'SealLast') {
@@ -1,8 +1,11 @@
1
1
  import { Signer } from './signer.ts';
2
2
  import { Cigar } from './cigar.ts';
3
3
  import { Siger } from './siger.ts';
4
+ export declare const HEADER_SIG: string;
4
5
  export declare const HEADER_SIG_INPUT: string;
5
6
  export declare const HEADER_SIG_TIME: string;
7
+ export declare const HEADER_SIG_SENDER: string;
8
+ export declare const HEADER_SIG_DESTINATION: string;
6
9
  export declare function normalize(header: string): string;
7
10
  export interface SiginputArgs {
8
11
  name: string;
@@ -2,8 +2,11 @@ import { serializeDictionary, parseDictionary, } from 'structured-headers';
2
2
  import { b } from "./core.js";
3
3
  import { nowUTC } from "./utils.js";
4
4
  import { encodeBase64Url } from "./base64.js";
5
+ export const HEADER_SIG = normalize('Signature');
5
6
  export const HEADER_SIG_INPUT = normalize('Signature-Input');
6
7
  export const HEADER_SIG_TIME = normalize('Signify-Timestamp');
8
+ export const HEADER_SIG_SENDER = normalize('Signify-Resource');
9
+ export const HEADER_SIG_DESTINATION = normalize('Signify-Receiver');
7
10
  export function normalize(header) {
8
11
  return header.trim();
9
12
  }
@@ -57,7 +57,7 @@ export interface IdentifierManager<T extends IdentifierManagerParams = Identifie
57
57
  * `ondex` matches its `index`. `ondices[j] === undefined` is the explicit
58
58
  * current-only case for that signature, so no `ondex` is emitted.
59
59
  */
60
- sign(ser: Uint8Array, indexed?: boolean, indices?: number[], ondices?: Array<number | undefined>, rotated?: boolean): Promise<SignResult>;
60
+ sign(ser: Uint8Array, indexed?: boolean, indices?: number[], ondices?: Ondex[], rotated?: boolean): Promise<SignResult>;
61
61
  }
62
62
  /**
63
63
  * Creates IdentifierManager instances based on the algorithm and key indexes.
@@ -113,7 +113,7 @@ export declare class SaltyIdentifierManager implements IdentifierManager {
113
113
  params(): SaltyManagerParams;
114
114
  incept(transferable: boolean): Promise<IdentifierManagerResult>;
115
115
  rotate(ncodes: string[], transferable: boolean): Promise<[string[], string[]]>;
116
- sign(ser: Uint8Array, indexed?: boolean, indices?: number[] | undefined, ondices?: Array<number | undefined> | undefined, _rotated?: boolean): Promise<SignResult>;
116
+ sign(ser: Uint8Array, indexed?: boolean, indices?: number[] | undefined, ondices?: Ondex[] | undefined, _rotated?: boolean): Promise<SignResult>;
117
117
  }
118
118
  export declare class RandyIdentifierManager implements IdentifierManager {
119
119
  private salter;
@@ -137,8 +137,9 @@ export declare class RandyIdentifierManager implements IdentifierManager {
137
137
  params(): RandyManagerParams;
138
138
  incept(transferable: boolean): Promise<IdentifierManagerResult>;
139
139
  rotate(ncodes: string[], transferable: boolean): Promise<IdentifierManagerResult>;
140
- sign(ser: Uint8Array, indexed?: boolean, indices?: number[] | undefined, ondices?: Array<number | undefined> | undefined, _rotated?: boolean): Promise<SignResult>;
140
+ sign(ser: Uint8Array, indexed?: boolean, indices?: number[] | undefined, ondices?: Ondex[] | undefined, _rotated?: boolean): Promise<SignResult>;
141
141
  }
142
+ type Ondex = number | undefined;
142
143
  /**
143
144
  * Identifier manager for group identifiers.
144
145
  *
@@ -225,7 +226,7 @@ export declare class GroupIdentifierManager implements IdentifierManager {
225
226
  /**
226
227
  * Signs for the group by delegating to the local member keeper.
227
228
  */
228
- sign(ser: Uint8Array, indexed?: boolean, _indices?: number[] | undefined, _ondices?: Array<number | undefined> | undefined, rotated?: boolean): Promise<SignResult>;
229
+ sign(ser: Uint8Array, indexed?: boolean, _indices?: number[] | undefined, _ondices?: Ondex[] | undefined, rotated?: boolean): Promise<SignResult>;
229
230
  /**
230
231
  * Finds the prior next digest position that precommitted the current key.
231
232
  *
@@ -236,23 +237,9 @@ export declare class GroupIdentifierManager implements IdentifierManager {
236
237
  */
237
238
  private priorNextIndexForKey;
238
239
  params(): {
239
- mhab: {
240
- name: string;
241
- prefix: string;
242
- icp_dt: string;
243
- state: import("../../types/keria-api-schema.ts").components["schemas"]["KeyStateRecord"];
244
- transferable: boolean;
245
- windexes: string[];
246
- } & ({
247
- salty: import("../../types/keria-api-schema.ts").components["schemas"]["SaltyState"];
248
- } | {
249
- randy: import("../../types/keria-api-schema.ts").components["schemas"]["RandyKeyState"];
250
- } | {
251
- group: import("../../types/keria-api-schema.ts").components["schemas"]["GroupKeyState"];
252
- } | {
253
- extern: import("../../types/keria-api-schema.ts").components["schemas"]["ExternState"];
254
- });
240
+ mhab: HabState;
255
241
  keys: string[];
256
242
  ndigs: string[];
257
243
  };
258
244
  }
245
+ export {};
@@ -162,9 +162,11 @@ export class SaltyIdentifierManager {
162
162
  const sigers = [];
163
163
  let i = 0;
164
164
  for (const [j, signer] of signers.signers.entries()) {
165
- if (indices != undefined) {
166
- i = indices[j];
167
- if (typeof i != 'number' || i < 0) {
165
+ if (indices &&
166
+ indices.length > 0 &&
167
+ indices.length === signers.signers.length) {
168
+ i = indices[j]; // default indexing behavior of pulling index order from signer order
169
+ if (!Number.isInteger(i) || i < 0) {
168
170
  throw new Error(`Invalid signing index = ${i}, not whole number.`);
169
171
  }
170
172
  }
@@ -172,7 +174,10 @@ export class SaltyIdentifierManager {
172
174
  i = j;
173
175
  }
174
176
  let o = 0;
175
- if (ondices != undefined) {
177
+ // if ondices are specified then the count and order should match the indices list
178
+ // even if a given ondex is "undefined" which means to ignore the ondex (dual indexed sig)
179
+ // for that given index.
180
+ if (ondices && ondices.length > 0) {
176
181
  o = ondices[j];
177
182
  if (o !== undefined && (!Number.isInteger(o) || o < 0)) {
178
183
  throw new Error(`Invalid ondex = ${o}, not whole number.`);
@@ -17,15 +17,39 @@ export type RandyKeyState = components['schemas']['RandyKeyState'];
17
17
  /**
18
18
  * Defining properties a multi-signature group identifier manager.
19
19
  */
20
- export type GroupKeyState = components['schemas']['GroupKeyState'];
20
+ export type GroupKeyState = Omit<components['schemas']['GroupKeyState'], 'mhab'> & {
21
+ mhab: HabState;
22
+ };
21
23
  /**
22
24
  * Defining properties for an external module identifier manager that uses externally managed keys such as in an HSM or a KMS system.
23
25
  */
24
26
  export type ExternState = components['schemas']['ExternState'];
27
+ type SchemaHabState = components['schemas']['HabState'];
25
28
  /**
26
- * Defining properties of an identifier habitat, know as a Hab in KERIpy.
29
+ * Defining properties of an identifier habitat, known as a Hab in KERIpy.
30
+ *
31
+ * Omitting `state` alone would erase the algo variants — `keyof` a union of disjoint objects is
32
+ * `never` — so they are re-spelled, which also points `group` at the local GroupKeyState above.
27
33
  */
28
- export type HabState = components['schemas']['HabState'];
34
+ type BaseHabState = Omit<SchemaHabState, 'state' | 'salty' | 'randy' | 'group' | 'extern'> & ({
35
+ salty: SaltyKeyState;
36
+ } | {
37
+ randy: RandyKeyState;
38
+ } | {
39
+ group: GroupKeyState;
40
+ } | {
41
+ extern: ExternState;
42
+ });
43
+ /** Multisig group whose inception is still collecting member signatures; no key state yet. */
44
+ export type PendingHabState = BaseHabState & {
45
+ state?: undefined;
46
+ };
47
+ /** Fully inceptioned identifier; key state present. */
48
+ export type HabState = BaseHabState & {
49
+ state: KeyState;
50
+ };
51
+ export declare function isAccepted(hab: HabState | PendingHabState): hab is HabState;
52
+ export declare function requireKeyState(hab: HabState | PendingHabState): KeyState;
29
53
  export type Icp = components['schemas']['ICP_V_1'] | components['schemas']['ICP_V_2'];
30
54
  export type Ixn = components['schemas']['IXN_V_1'] | components['schemas']['IXN_V_2'];
31
55
  export type ExnV1 = components['schemas']['EXN_V_1'];
@@ -50,6 +74,7 @@ export type DoneOperation = components['schemas']['DoneOperation'];
50
74
  export type CredentialOperation = components['schemas']['CredentialOperation'];
51
75
  export type GroupOperation = components['schemas']['GroupOperation'];
52
76
  export type DelegatorOperation = components['schemas']['DelegatorOperation'];
77
+ export type KelOperation = components['schemas']['KelOperation'];
53
78
  export type PendingOOBIOperation = components['schemas']['PendingOOBIOperation'];
54
79
  export type PendingQueryOperation = components['schemas']['PendingQueryOperation'];
55
80
  export type PendingEndRoleOperation = components['schemas']['PendingEndRoleOperation'];
@@ -97,3 +122,4 @@ export type FailedDelegatorOperation = components['schemas']['FailedDelegatorOpe
97
122
  export type FailedOperation = FailedOOBIOperation | FailedQueryOperation | FailedEndRoleOperation | FailedWitnessOperation | FailedDelegationOperation | FailedRegistryOperation | FailedLocSchemeOperation | FailedChallengeOperation | FailedExchangeOperation | FailedSubmitOperation | FailedDoneOperation | FailedCredentialOperation | FailedGroupOperation | FailedDelegatorOperation;
98
123
  export type GenericOperation = OOBIOperation | QueryOperation | EndRoleOperation | WitnessOperation | DelegationOperation | RegistryOperation | LocSchemeOperation | ChallengeOperation | ExchangeOperation | SubmitOperation | DoneOperation | CredentialOperation | GroupOperation | DelegatorOperation;
99
124
  export type EndRole = components['schemas']['EndRole'];
125
+ export {};
@@ -1 +1,9 @@
1
- export {};
1
+ export function isAccepted(hab) {
2
+ return hab.state !== undefined;
3
+ }
4
+ export function requireKeyState(hab) {
5
+ if (!isAccepted(hab)) {
6
+ throw new Error(`No key state for identifier ${hab.name}`);
7
+ }
8
+ return hab.state;
9
+ }
@@ -1,5 +1,6 @@
1
1
  import { Siger } from "../core/siger.js";
2
2
  import { Cigar } from "../core/cigar.js";
3
+ import { HEADER_SIG } from "../core/httping.js";
3
4
  export const FALSY = [false, 0, '?0', 'no', 'false', 'False', 'off'];
4
5
  export const TRUTHY = [true, 1, '?1', 'yes', 'true', 'True', 'on'];
5
6
  export class Signage {
@@ -77,7 +78,7 @@ export function signature(signages) {
77
78
  }));
78
79
  values.push(items.join(';'));
79
80
  }
80
- return new Headers([['Signature', values.join(',')]]);
81
+ return new Headers([[HEADER_SIG, values.join(',')]]);
81
82
  }
82
83
  export function designature(value) {
83
84
  const values = value.replace(' ', '').split(',');