@raytio/core 11.1.0 → 11.2.0

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.
package/CHANGELOG.md CHANGED
@@ -5,13 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ <!--
9
+ If you think there are missing entries, go to:
10
+ https://gitlab.com/raytio/raytio-client-v2/-/commits/master/packages/core
11
+ to see all commits that touched this package.
12
+ -->
13
+
8
14
  ## [Unreleased]
9
15
 
10
- ## 11.1.0 (2022-04-20)
16
+ ## 11.2.0 (2023-07-03)
17
+
18
+ - Support key rotation
19
+ - Support the new Schema API format
20
+
21
+ ## 11.1.0 (2023-04-20)
11
22
 
12
23
  - Move `expandSchema` into core
13
24
 
14
- ## 11.0.0 (2022-04-20)
25
+ ## 11.0.0 (2023-04-20)
15
26
 
16
27
  - 💥 BREAKING CHANGE: Changed the input arguments of `convertInstanceToRuleInput` to only require a list of `ProfileObject`s, instead of the whole `Instance` object.
17
28
  - Any profile object with an expired field is now considered `Expired`, even if other fields are verified.
package/README.md CHANGED
@@ -138,7 +138,7 @@ ___
138
138
 
139
139
  ### checkJsonSignature
140
140
 
141
- ▸ **checkJsonSignature**(`data`, `signature`): `Promise`<`boolean`\>
141
+ ▸ **checkJsonSignature**(`data`, `signature`, `keyId`): `Promise`<`boolean`\>
142
142
 
143
143
  checks that a json object was signed by the provided signature. Unless you're
144
144
  dealing with bundled verifications, you should use `getOwnRealVerifications`
@@ -150,6 +150,7 @@ or `getSomeoneElsesRealVerifications` instead.
150
150
  | :------ | :------ |
151
151
  | `data` | `unknown` |
152
152
  | `signature` | `string` |
153
+ | `keyId` | `undefined` \| `string` |
153
154
 
154
155
  #### Returns
155
156
 
@@ -393,7 +394,7 @@ two overloads - if you provide undefined, you might get undefined back
393
394
 
394
395
  | Name | Type |
395
396
  | :------ | :------ |
396
- | `urn` | \`urn:user:${string}\` \| \`urn:profile\_object:${string}\` \| \`urn:instance:${string}\` \| \`urn:schema:${string}\` \| \`urn:temp\_object:${string}\` \| \`urn:document:${string}\` |
397
+ | `urn` | `Urn` |
397
398
 
398
399
  #### Returns
399
400
 
@@ -407,7 +408,7 @@ two overloads - if you provide undefined, you might get undefined back
407
408
 
408
409
  | Name | Type |
409
410
  | :------ | :------ |
410
- | `urn` | `undefined` \| \`urn:user:${string}\` \| \`urn:profile\_object:${string}\` \| \`urn:instance:${string}\` \| \`urn:schema:${string}\` \| \`urn:temp\_object:${string}\` \| \`urn:document:${string}\` |
411
+ | `urn` | `undefined` \| `Urn` |
411
412
 
412
413
  #### Returns
413
414
 
@@ -2,16 +2,44 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getAADecryptor = void 0;
4
4
  const util_1 = require("../util");
5
+ /**
6
+ * Fetches the Public Key Information for an Access Application
7
+ * @returns the id and Key information of the Applications Public Key
8
+ */
9
+ async function fetchPublicKey({ apiUrl, apiToken, aId, }) {
10
+ const [publicKey] = await fetch(`${apiUrl}/db/v1/dsm_access_application_public_keys?aa_id=eq.${aId}`, {
11
+ headers: { Authorization: `Bearer ${apiToken}` },
12
+ }).then(util_1.handleResponse);
13
+ if (!publicKey) {
14
+ throw new Error("Could not Find Encryption Key");
15
+ }
16
+ const { id, public_key } = publicKey;
17
+ return { n_id: id, properties: public_key };
18
+ }
19
+ /**
20
+ * Fetches the Private Key Information from a Public Key
21
+ * @returns An encrypted Private Key
22
+ */
23
+ async function fetchPrivateKey({ apiUrl, apiToken, publicKeyNId, }) {
24
+ const [privateKey] = await fetch(`${apiUrl}/db/v1/dsm_access_application_private_keys?aack_id=eq.${publicKeyNId}`, { headers: { Authorization: `Bearer ${apiToken}` } }).then(util_1.handleResponse);
25
+ return privateKey.encrypted_private_key;
26
+ }
5
27
  /**
6
28
  * Fetchs the public and private keys for an Access Application, then initializes
7
29
  * the {@link https://npm.im/@raytio/maxcryptor|Maxcryptor}'s `ApplicationEncryptor`.
8
30
  * @returns an `ApplicationEncryptor` and the public key of the Access Application
9
31
  */
10
32
  async function getAADecryptor({ aId, apiUrl, maxcryptor, apiToken, }) {
11
- const { n_id: publicKeyNId, properties: publicKey } = (await fetch(`${apiUrl}/share/v2/access_application/${aId}/public_key`, {
12
- headers: { Authorization: `Bearer ${apiToken}` },
13
- }).then(util_1.handleResponse));
14
- const privateKey = await fetch(`${apiUrl}/share/v2/access_application/public_key/${publicKeyNId}/private_key`, { headers: { Authorization: `Bearer ${apiToken}` } }).then(util_1.handleResponse);
33
+ const { n_id: publicKeyNId, properties: publicKey } = (await fetchPublicKey({
34
+ apiUrl,
35
+ apiToken,
36
+ aId,
37
+ }));
38
+ const privateKey = await fetchPrivateKey({
39
+ apiUrl,
40
+ apiToken,
41
+ publicKeyNId,
42
+ });
15
43
  return {
16
44
  decryptor: await maxcryptor.loadApplicationEncryptorForDecryption(publicKey, privateKey),
17
45
  publicKeyNId,
@@ -3,13 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const expandSchema_1 = require("../expandSchema");
4
4
  const fromAPI = [
5
5
  {
6
- name: "ps_Vessel",
6
+ schema_name: "ps_Vessel",
7
7
  start_date: "1970-01-01T00:00:00+00:00",
8
8
  end_date: "2222-12-31T23:59:59+00:00",
9
9
  active: true,
10
10
  version_current: true,
11
- version: "0.0.7",
12
- type: "ps",
11
+ schema_version: "0.0.7",
12
+ schema_type: "ps",
13
13
  schema: {
14
14
  $id: "https://api.rayt.io/graph/v1/schema/ps/ps_Vessel",
15
15
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -38,8 +38,8 @@ const fromAPI = [
38
38
  },
39
39
  },
40
40
  {
41
- name: "ss_birth_year_integer",
42
- version: "0.0.1",
41
+ schema_name: "ss_birth_year_integer",
42
+ schema_version: "0.0.1",
43
43
  schema: {
44
44
  $id: "this field often has typos. in this case its blatantly wrong",
45
45
  $schema: "https://whatever",
@@ -61,6 +61,7 @@ describe("expandSchema", () => {
61
61
  start_date: "1970-01-01T00:00:00+00:00",
62
62
  end_date: "2222-12-31T23:59:59+00:00",
63
63
  version: "0.0.7",
64
+ schema_type: "ps",
64
65
  i18n: fromAPI[0].schema.i18n,
65
66
  isProfileSchema: true,
66
67
  isSpSchema: false,
@@ -25,7 +25,7 @@ describe("processSchema", () => {
25
25
  const allUnexpandedSchema = [
26
26
  mock,
27
27
  {
28
- name: "ss_first_name_01",
28
+ schema_name: "ss_first_name_01",
29
29
  schema: {
30
30
  properties: {},
31
31
  tags: ["a tag from the child"],
@@ -95,14 +95,14 @@ describe("processSchema", () => {
95
95
  const allUnexpandedSchema = [
96
96
  mock,
97
97
  {
98
- name: "ss_first_name_01",
98
+ schema_name: "ss_first_name_01",
99
99
  schema: {
100
100
  required: ["c", "d"],
101
101
  properties: { first_name: { type: "string" } },
102
102
  },
103
103
  },
104
104
  {
105
- name: "ss_last_name_01",
105
+ schema_name: "ss_last_name_01",
106
106
  schema: {
107
107
  required: ["e", "f"],
108
108
  properties: { last_name: { type: "string" } },
@@ -297,7 +297,7 @@ describe("processSchema", () => {
297
297
  ],
298
298
  };
299
299
  const moeSubSchema = {
300
- name: "ss_MOE_ID",
300
+ schema_name: "ss_MOE_ID",
301
301
  schema: {
302
302
  type: "number",
303
303
  properties: {},
@@ -1,4 +1,3 @@
1
1
  import { Schema, WrappedSchema } from "@raytio/types";
2
- export declare const processSchema: (schema: WrappedSchema["schema"] & {
3
- version: string;
4
- }, allUnexpandedSchemas: WrappedSchema[]) => Schema;
2
+ import type { unwrapSchema } from "./unwrapSchema";
3
+ export declare const processSchema: (schema: ReturnType<typeof unwrapSchema>, allUnexpandedSchemas: WrappedSchema[]) => Schema;
@@ -19,7 +19,7 @@ const processSchema = (schema, allUnexpandedSchemas) => {
19
19
  const subSchemaName = (0, sortSchemaProperties_1.getNidFromUrn)((_a = schema.definitions) === null || _a === void 0 ? void 0 : _a[$ref.split("/")[2]].$ref);
20
20
  if (!subSchemaName)
21
21
  throw new Error("Invalid schema");
22
- const subSchema = (_b = allUnexpandedSchemas.find(s => s.name === subSchemaName)) === null || _b === void 0 ? void 0 : _b.schema;
22
+ const subSchema = (_b = allUnexpandedSchemas.find(s => s.schema_name === subSchemaName)) === null || _b === void 0 ? void 0 : _b.schema;
23
23
  if (!subSchema) {
24
24
  throw new Error(`Could not resolve subschema '${subSchemaName}'`);
25
25
  }
@@ -44,7 +44,7 @@ const processSchema = (schema, allUnexpandedSchemas) => {
44
44
  const subSchemaName = (0, sortSchemaProperties_1.getNidFromUrn)((_a = schema.definitions) === null || _a === void 0 ? void 0 : _a[$ref.split("/")[2]].$ref);
45
45
  if (!subSchemaName)
46
46
  throw new Error("Invalid schema");
47
- const subSchema = (_b = allUnexpandedSchemas.find(s => s.name === subSchemaName)) === null || _b === void 0 ? void 0 : _b.schema;
47
+ const subSchema = (_b = allUnexpandedSchemas.find(s => s.schema_name === subSchemaName)) === null || _b === void 0 ? void 0 : _b.schema;
48
48
  if (!subSchema) {
49
49
  throw new Error(`Could not resolve subschema '${subSchemaName}'`);
50
50
  }
@@ -1,10 +1,10 @@
1
1
  import { Schema } from "@raytio/types";
2
2
  export declare const removePrivateFields: (schema: Schema) => {
3
3
  properties: Record<string, Omit<import("@raytio/types").SchemaField, "$id" | "allOf" | "$schema" | "definitions">>;
4
- type?: import("@raytio/types").DataTypes | undefined;
5
- name: string;
6
4
  title: string;
7
5
  description: string;
6
+ description_decorator?: "md" | undefined;
7
+ schema_type?: import("@raytio/types").SchemaType | undefined;
8
8
  title_plural?: string | undefined;
9
9
  schema_group?: string | undefined;
10
10
  tags?: import("@raytio/types").SchemaTag[] | undefined;
@@ -104,6 +104,8 @@ export declare const removePrivateFields: (schema: Schema) => {
104
104
  }[] | undefined;
105
105
  return_to?: string | undefined;
106
106
  } | undefined;
107
+ name: string;
108
+ type?: import("@raytio/types").DataTypes | undefined;
107
109
  group_title?: string | undefined;
108
110
  verified_fields?: (string | import("@raytio/types").ConditionallyRequired)[] | undefined;
109
111
  required?: (string | import("@raytio/types").ConditionallyRequired)[] | undefined;
@@ -1,6 +1,6 @@
1
1
  import { WrappedSchema } from "@raytio/types";
2
- export declare function unwrapSchema(wrapped: WrappedSchema): WrappedSchema["schema"] & {
2
+ export type ClientFields = {
3
3
  version: string;
4
- start_date?: string;
5
- end_date?: string;
4
+ name: string;
6
5
  };
6
+ export declare function unwrapSchema(wrapped: WrappedSchema): WrappedSchema["schema"] & Pick<WrappedSchema, "start_date" | "end_date" | "schema_type"> & ClientFields;
@@ -2,6 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.unwrapSchema = void 0;
4
4
  function unwrapSchema(wrapped) {
5
- return Object.assign(Object.assign({}, wrapped.schema), { name: wrapped.name, start_date: wrapped.start_date, end_date: wrapped.end_date, version: wrapped.version });
5
+ return Object.assign(Object.assign({}, wrapped.schema), { name: wrapped.schema_name, start_date: wrapped.start_date, end_date: wrapped.end_date, version: wrapped.schema_version, schema_type: wrapped.schema_type });
6
6
  }
7
7
  exports.unwrapSchema = unwrapSchema;
@@ -0,0 +1,9 @@
1
+ /// <reference types="jest" />
2
+ /** like {@link Partial} but applies to all deep properties */
3
+ export type DeepPartial<T> = T | (T extends object ? {
4
+ [K in keyof T]?: DeepPartial<T[K]>;
5
+ } : T extends (infer U)[] ? DeepPartial<U>[] : T);
6
+ /** tells typescript that this function is mocked */
7
+ export declare const m: (func: unknown) => jest.Mock<any, any, any>;
8
+ /** helper to let us define type-safe partial mocks */
9
+ export declare const deepPartial: <T>(partial: DeepPartial<T>) => T;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deepPartial = exports.m = void 0;
4
+ /** tells typescript that this function is mocked */
5
+ const m = (func) => func;
6
+ exports.m = m;
7
+ /** helper to let us define type-safe partial mocks */
8
+ const deepPartial = (partial) => partial;
9
+ exports.deepPartial = deepPartial;
@@ -55,10 +55,13 @@ function getPOVerification({ PO, schema, realVers, }) {
55
55
  if (pertainingVers.length && pertainingVers.every(x => x.expired)) {
56
56
  return types_1.FieldVerification.Expired;
57
57
  }
58
- if (pertainingVers.some(x => !x.verified)) {
58
+ // the if statement above checks if every ver is expired. It is possible
59
+ // that only some are expired, so filter out those ones.
60
+ const nonExpiredPertainingVers = pertainingVers.filter(x => !x.expired);
61
+ if (nonExpiredPertainingVers.some(x => !x.verified)) {
59
62
  return types_1.FieldVerification.VerifiedFalse;
60
63
  }
61
- return pertainingVers.length
64
+ return nonExpiredPertainingVers.length
62
65
  ? types_1.FieldVerification.Verified
63
66
  : types_1.FieldVerification.NotVerified;
64
67
  }, shouldBeVerifiedProps);
@@ -89,6 +92,7 @@ function getPOVerification({ PO, schema, realVers, }) {
89
92
  nId: PO.n_id,
90
93
  realVers,
91
94
  shouldBeVerifiedProps,
95
+ verificationStatus: status,
92
96
  });
93
97
  // find the earliest expiry date if there is one
94
98
  const maybeExpiryDate = ((_c = (0, ramda_1.sortBy)(ver => +ver.expired, realVers.filter(ver => ver.belongsToNId === PO.n_id && ver.expired))[0]) === null || _c === void 0 ? void 0 : _c.expired) || undefined;
@@ -1,13 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getVerifiedBy = void 0;
4
+ const types_1 = require("@raytio/types");
4
5
  const ramda_1 = require("ramda");
5
6
  const maybeRereference_1 = require("./maybeRereference");
6
7
  // we should probably document this
7
8
  /** @internal */
8
- const getVerifiedBy = ({ nId, realVers, shouldBeVerifiedProps, }) => {
9
+ const getVerifiedBy = ({ nId, realVers, shouldBeVerifiedProps, verificationStatus, }) => {
9
10
  const mayBeVerifiedFields = Object.keys(shouldBeVerifiedProps);
10
11
  const pertainingVers = realVers
12
+ // for valid verifications, we filter out the information that came from
13
+ // now expired sources. We only ever consider expired source if the verification
14
+ // itself is expired.
15
+ .filter(realVer => realVer.expired ? verificationStatus === types_1.POVerification.Expired : true)
11
16
  .filter(x => mayBeVerifiedFields.includes(x.fieldName) &&
12
17
  // using ramda's `equals` because this needs to work for objects/arrays
13
18
  (0, ramda_1.equals)((0, maybeRereference_1.maybeRereference)(shouldBeVerifiedProps[x.fieldName]), (0, maybeRereference_1.maybeRereference)(x.value)) &&
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const __1 = require("..");
4
4
  const operations_1 = require("../operations");
5
+ const testHelpers_1 = require("../../../testHelpers");
5
6
  jest.mock("../operations");
6
7
  /**
7
8
  * in this test case:
@@ -16,18 +17,21 @@ jest.mock("../operations");
16
17
  */
17
18
  describe("getOwnRealVerifications", () => {
18
19
  beforeAll(() => {
19
- operations_1.checkOwnVerification.mockImplementation(async ({ signature }) => {
20
+ jest.fn(() => 1).mockImplementation(() => 123);
21
+ (0, testHelpers_1.m)(operations_1.checkOwnVerification).mockImplementation(async ({ signature, keyId }) => {
20
22
  if (signature === "signatureForLastNameMustermannFromN2")
21
23
  return false;
24
+ if (keyId === "thisOneIsInvalid")
25
+ return false;
22
26
  return true;
23
27
  });
24
28
  });
25
29
  it("calls the verifyCheck function for each PO, keeping only the valid ones", async () => {
26
- const profileObjects = [
30
+ const profileObjects = (0, testHelpers_1.deepPartial)([
27
31
  { n_id: "n1", properties: { fName: "Max", lName: "Mustermann" } },
28
32
  { n_id: "n2", properties: { fName: "Erika", lName: "Mustermann" } },
29
- ];
30
- const verifications = [
33
+ ]);
34
+ const verifications = (0, testHelpers_1.deepPartial)([
31
35
  {
32
36
  n_id: "nv1",
33
37
  properties: {
@@ -65,6 +69,7 @@ describe("getOwnRealVerifications", () => {
65
69
  passed: false,
66
70
  source_n_id: "n2",
67
71
  },
72
+ // legacy: key_id is not specified
68
73
  },
69
74
  ],
70
75
  },
@@ -103,14 +108,56 @@ describe("getOwnRealVerifications", () => {
103
108
  ],
104
109
  },
105
110
  },
106
- ];
111
+ {
112
+ n_id: "nv6",
113
+ properties: {
114
+ verifications: [
115
+ {
116
+ signature: "signatureForFirstNameErika",
117
+ data: {
118
+ field: "fName",
119
+ verifier_source_id: "Ministry of pike river mine re-entry",
120
+ verifier_service_id: "Minister for pike river re-entry",
121
+ verifier_id: "v1",
122
+ verification_date: "2020-08-28T23:11:20.592912",
123
+ request_div: "x2",
124
+ passed: false,
125
+ source_n_id: "n2",
126
+ },
127
+ key_id: "thisOneIsInvalid", // signature & data is good, but this key will fail
128
+ },
129
+ ],
130
+ },
131
+ },
132
+ {
133
+ n_id: "nv7",
134
+ properties: {
135
+ verifications: [
136
+ {
137
+ signature: "signatureForFirstNameErika",
138
+ data: {
139
+ field: "fName",
140
+ verifier_source_id: "Ministry of pike river mine re-entry",
141
+ verifier_service_id: "Minister for pike river re-entry",
142
+ verifier_id: "v1",
143
+ verification_date: "2020-08-28T23:11:20.592912",
144
+ request_div: "x2",
145
+ passed: false,
146
+ source_n_id: "n2",
147
+ },
148
+ key_id: "raytio", // everything is valid, this is the right key_id
149
+ },
150
+ ],
151
+ },
152
+ },
153
+ ]);
107
154
  const realVers = await (0, __1.getOwnRealVerifications)({
108
155
  profileObjects,
109
156
  verifications,
110
157
  userId: "geesepolice2002",
111
158
  });
112
- expect(operations_1.checkOwnVerification).toHaveBeenCalledTimes(3);
113
- // 3 times, not 5. Because this new method doesn't need to build a big matrix of possible combinations
159
+ expect(operations_1.checkOwnVerification).toHaveBeenCalledTimes(5);
160
+ // 5 times. Because this new method doesn't need to build a big matrix of possible combinations
114
161
  expect(realVers).toStrictEqual([
115
162
  {
116
163
  fieldName: "fName",
@@ -147,6 +194,24 @@ describe("getOwnRealVerifications", () => {
147
194
  xId: "x2",
148
195
  },
149
196
  // note how lastName is not included since it's invalid
197
+ // nv6 is not included because the key is invalid
198
+ {
199
+ fieldName: "fName",
200
+ value: "Erika",
201
+ belongsToNId: "n2",
202
+ nID: "nv7",
203
+ expired: false,
204
+ metadata: undefined,
205
+ provider: {
206
+ dataSourceNId: "Ministry of pike river mine re-entry",
207
+ date: new Date("2020-08-28T23:11:20.592Z"),
208
+ serviceProviderNId: "Minister for pike river re-entry",
209
+ verifierNId: "v1",
210
+ },
211
+ signature: "signatureForFirstNameErika",
212
+ verified: false,
213
+ xId: "x2",
214
+ },
150
215
  ]);
151
216
  });
152
217
  });
@@ -17,7 +17,8 @@ const getOwnRealVerifications = async ({ verifications, profileObjects, userId,
17
17
  // because attempting hundreds of webcrypto operations simultaneously will
18
18
  // probably upset some heritage web browser.
19
19
  for (const ver of verifications) {
20
- for (const { data, signature } of ver.properties.verifications) {
20
+ for (const verPO of ver.properties.verifications) {
21
+ const { data, signature } = verPO;
21
22
  const sourcePO = profileObjects.find(PO => PO.n_id === data.source_n_id);
22
23
  if (!sourcePO)
23
24
  continue;
@@ -34,6 +35,7 @@ const getOwnRealVerifications = async ({ verifications, profileObjects, userId,
34
35
  userId,
35
36
  value: (0, maybeRereference_1.maybeRereference)(value),
36
37
  signature,
38
+ keyId: verPO.key_id,
37
39
  });
38
40
  if (!isGenuine)
39
41
  continue;
@@ -7,8 +7,9 @@ const __1 = require("..");
7
7
  const util_1 = require("../../../../util");
8
8
  const checkOwnVerification_1 = require("../checkOwnVerification");
9
9
  const sampleBundle_json_1 = __importDefault(require("./sampleBundle.json"));
10
- Reflect.set(global, "fetch", jest.fn().mockResolvedValue({
11
- text: async () => `-----BEGIN PUBLIC KEY-----
10
+ global.fetch = jest.fn().mockImplementation(async (url) => ({
11
+ text: async () => url.endsWith("raytio.pem")
12
+ ? `-----BEGIN PUBLIC KEY-----
12
13
  MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAn9QtCqYa3H3ipFFU0xP3
13
14
  n6r7KHS3GMbh0h/xzel57HhCIaXDYjUeUtgUNtzm+uElb/qzGn50xQRzVqO32vKB
14
15
  ZAW2kYyZ2+R5ruk9CSxr7K4Vk1FtDMcUCzqxm0eycFD2xbLsN3feRc3BMjfdaQ7P
@@ -21,10 +22,29 @@ Gut0BoM+DIwDu0uZaUprz7fSgNmYHHEiIFbOMVHiOn8oZAZbJXXbUbFIUYXA8u9+
21
22
  J1Z+QEpgw+rhGzOf/TSeHfMC9nNbWgYglluAJusWf2XwG/t/VlhtzviHCVGEL7HQ
22
23
  jQE5DrM7vaTg6Gu9bjKuoeLIRzbOYK6qAWFoa0CLcN84PLjhDSRw2duatP08hcWg
23
24
  jTgOkLWnBFE7NyRU93uPp68CAwEAAQ==
25
+ -----END PUBLIC KEY-----`
26
+ : `-----BEGIN PUBLIC KEY-----
27
+ MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgee+uBOgOsbwjMvGN1/H
28
+ qpGXGJLol0Pc2KhI1fh1NBq+UGhk8PqgDd5wHZikbmrtVkvp/maIh+mbIdehY/RC
29
+ ftMylvebCf4Qf+5SWzQsmB1o6nBUbwJzYE2XyvxRiNLhdIeE+GgfdpA5S3l0cDJ5
30
+ B/1TagITmQUjThwTxDYZ6jlGJJ4NSjqlqeQrHhGWRLVQPWU8bYysX3jt3/uiv4tS
31
+ n3TheLGY1TMlbFrVF2Spv1WxuqMZ4bX1mIotK3yEB3TaZSZaOwlUcEZ4xY+J4Vl+
32
+ ZlrOgYbmzFd7UFh9UZbYZUkNSEfddEnFNFlFG3YQVt8UAumPBVJELdjiaRjTj/K4
33
+ 62GAFyOcbcw9wcl69fPBnieBo2m2Dqf6U3wcrnvTnkMwjCewWXCH6FdbC4OllBZV
34
+ Nrfn6zf9yX0J8ZEDEcw9ZsNLVkyl2U+Ya/h5CQt43ip/1eNM5LpTbfqBTtAH7iUO
35
+ 4L9rxuSJFA2Q9kZfof6kYO9EGgMFB+GM47/Q068+IiTpifvPno4ilnowyS4hbLiy
36
+ Os2yudW79flaz0a7rq5dLdSg9Mm5k7ETBm7WguDcocaiETmuYTJT2PozAGOC3+EP
37
+ w5N7mQff4ecx/880FWYKQmU9Asav1V49DWSnG0ZXQ7U24dG8ANeAgKddDviJGlsh
38
+ SsjKrz8LJqeoNQu30iSGZhUCAwEAAQ==
24
39
  -----END PUBLIC KEY-----`,
25
40
  }));
26
41
  describe("checkOwnVerification", () => {
27
- it("works for a real PO", async () => {
42
+ it.each `
43
+ keyId | result
44
+ ${undefined} | ${true}
45
+ ${"whatever/raytio"} | ${true}
46
+ ${"whatever/somethingElse"} | ${false}
47
+ `("returns $result for a PO with keyId=$keyId", async ({ keyId, result }) => {
28
48
  //
29
49
  // This is an important integration test from ca. 2021-12-02.
30
50
  // If it is failing, it means our code no longer matches the
@@ -54,7 +74,13 @@ describe("checkOwnVerification", () => {
54
74
  };
55
75
  const value = "NZ Limited Company";
56
76
  const userId = "fd9c4903-65c2-4b75-b454-4fda6b682f3e"; // for 27july21 🦈
57
- expect(await (0, checkOwnVerification_1.checkOwnVerification)({ value, signature, userId, verObject })).toBe(true);
77
+ expect(await (0, checkOwnVerification_1.checkOwnVerification)({
78
+ value,
79
+ signature,
80
+ userId,
81
+ verObject,
82
+ keyId,
83
+ })).toBe(result);
58
84
  });
59
85
  it("errors if you forget to supply the uId", async () => {
60
86
  await expect(() => (0, checkOwnVerification_1.checkOwnVerification)({
@@ -62,8 +88,22 @@ describe("checkOwnVerification", () => {
62
88
  signature: "",
63
89
  userId: "",
64
90
  verObject: {},
91
+ keyId: undefined,
65
92
  })).rejects.toThrow(new Error("No userId supplied"));
66
93
  });
94
+ it.each `
95
+ keyId
96
+ ${"../../malicious.pem"}
97
+ ${"https://example.com/malicious.pem"}
98
+ `("errors if you supply an invalid keyId", async ({ keyId }) => {
99
+ await expect(() => (0, checkOwnVerification_1.checkOwnVerification)({
100
+ value: "whatever",
101
+ signature: "whatever",
102
+ userId: "whatever",
103
+ verObject: {},
104
+ keyId,
105
+ })).rejects.toThrow(new Error("Invalid key ID"));
106
+ });
67
107
  });
68
108
  describe("checkSignature", () => {
69
109
  it("works", async () => {
@@ -86,6 +126,6 @@ describe("checkSignature", () => {
86
126
  });
87
127
  describe("checkJsonSignature", () => {
88
128
  it("can verify a bundled verification", async () => {
89
- expect(await (0, __1.checkJsonSignature)(sampleBundle_json_1.default.data, sampleBundle_json_1.default.signature)).toBe(true);
129
+ expect(await (0, __1.checkJsonSignature)(sampleBundle_json_1.default.data, sampleBundle_json_1.default.signature, sampleBundle_json_1.default.key_id)).toBe(true);
90
130
  });
91
131
  });
@@ -39,5 +39,6 @@
39
39
  },
40
40
  "valid_until": "2022-03-13T02:26:20.468171"
41
41
  },
42
+ "key_id": "any string/raytio",
42
43
  "signature": "AiWWrL+S1paYOqJiOtU3qwLTCkkZjwDq3FuHl7oy14IATYOhCeHLf+ca44X1Wc6pYpTQckjKnJZL\nkfgiwNE97aymWIOc+ZZGEb5YhXRNO+inTV4k5zppaDN3n3YAGzn7zMxleh3+opzJqncNaJtpZ0Wv\na9Pu/m4WjyT5ee3Myz6VOOMuVkcaTL4FD8XT7NdCh0ybRevAZ5R9xl0YuWMhvNpf3P6ieTikHXYN\nkKbPTnAhNdBmqV4njSIR66M82Ek0d9VcsX4zhmlhpdCmGRlXLgHEyMCF4iHlCIxSeKtGaOm2QK2R\nOV/lN3VScDNWyD8lPBipcj++5ZGII6BnFFG8LlT3gY/Y/wt8KeH/xgdu0a7Lt6J/BOiGLFfscUmb\nH5K5t48gnQ5BQS+Cf/yhayMV49LlGiK9m1iPlbmuJH1L2/ZM+iLsIrSTGCU0Rpbkw7qvm0dkUNYf\nhvlj/RnUxcy0Lr/84CzLvBhFMmBX+RHlcPrCWpIiibsdaD81kRyvLY2TASLFTeHajfr+UvtP3LVs\n8NGwRQHd6c2/ptxv3ERRUnDtNASatsLe67ZHg9SeF3BDhMHZwU1neYyrBI1TMECasFli5rP5gviq\nC8ZwFQ9lnDDTidWBF8GjRl6ope4wIuNBBkOsIIeyqIJE5BRUH4LhVUnN1be696uCKnWOyOo7fkc=\n"
43
44
  }
@@ -4,12 +4,13 @@ type SingleVerToCheck = {
4
4
  signature: string;
5
5
  userId: UId;
6
6
  value: unknown;
7
+ keyId: string | undefined;
7
8
  };
8
9
  /**
9
10
  * checks that a json object was signed by the provided signature. Unless you're
10
11
  * dealing with bundled verifications, you should use `getOwnRealVerifications`
11
12
  * or `getSomeoneElsesRealVerifications` instead.
12
13
  */
13
- export declare const checkJsonSignature: (data: unknown, signature: string) => Promise<boolean>;
14
- export declare const checkOwnVerification: ({ verObject, signature, userId, value, }: SingleVerToCheck) => Promise<boolean>;
14
+ export declare const checkJsonSignature: (data: unknown, signature: string, keyId: string | undefined) => Promise<boolean>;
15
+ export declare const checkOwnVerification: ({ verObject, signature, userId, value, keyId, }: SingleVerToCheck) => Promise<boolean>;
15
16
  export {};
@@ -2,14 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkOwnVerification = exports.checkJsonSignature = exports.checkSignature = void 0;
4
4
  const util_1 = require("../../../util");
5
- let cache;
5
+ const cache = {};
6
6
  const base64ToArrayBuffer = (str) => Uint8Array.from(atob(str), c => c.charCodeAt(0));
7
- async function getJwk() {
7
+ async function getJwk(keyUrl) {
8
8
  // eslint-disable-next-line fp/no-mutation
9
- cache || (cache = fetch("https://api-docs.rayt.io/lookups/raytio.pem")
9
+ cache[keyUrl] || (cache[keyUrl] = fetch(keyUrl)
10
10
  .then(r => r.text())
11
11
  .then(pem => crypto.subtle.importKey("spki", base64ToArrayBuffer(pem.split("-----")[2].trim()), { name: "RSA-PSS", hash: "SHA-512" }, false, ["verify"])));
12
- return cache;
12
+ return cache[keyUrl];
13
13
  }
14
14
  /** @internal exported only for tests */
15
15
  async function checkSignature(publicCryptoKey, signature, data) {
@@ -24,16 +24,23 @@ exports.checkSignature = checkSignature;
24
24
  * dealing with bundled verifications, you should use `getOwnRealVerifications`
25
25
  * or `getSomeoneElsesRealVerifications` instead.
26
26
  */
27
- const checkJsonSignature = async (data, signature) => {
28
- const jwk = await getJwk();
27
+ const checkJsonSignature = async (data, signature, keyId) => {
28
+ const keyFileName = keyId ? keyId.split("/")[1] : "raytio";
29
+ // don't allow any special characters, e.g. to prevent
30
+ // someone using a keyID of "../../someOtherFile"
31
+ if (!keyFileName || /[^\w-]/.test(keyFileName)) {
32
+ throw new Error("Invalid key ID");
33
+ }
34
+ const keyUrl = `https://api-docs.rayt.io/lookups/${keyFileName}.pem`;
35
+ const jwk = await getJwk(keyUrl);
29
36
  const stringified = (0, util_1.canonicalJsonify)(data);
30
37
  const result = await checkSignature(jwk, signature, stringified);
31
38
  return result;
32
39
  };
33
40
  exports.checkJsonSignature = checkJsonSignature;
34
- const checkOwnVerification = async ({ verObject, signature, userId, value, }) => {
41
+ const checkOwnVerification = async ({ verObject, signature, userId, value, keyId, }) => {
35
42
  if (!userId)
36
43
  throw new Error("No userId supplied");
37
- return (0, exports.checkJsonSignature)(Object.assign(Object.assign({}, verObject), { sub: userId, value }), signature);
44
+ return (0, exports.checkJsonSignature)(Object.assign(Object.assign({}, verObject), { sub: userId, value }), signature, keyId);
38
45
  };
39
46
  exports.checkOwnVerification = checkOwnVerification;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raytio/core",
3
- "version": "11.1.0",
3
+ "version": "11.2.0",
4
4
  "license": "MIT",
5
5
  "main": "index",
6
6
  "types": "index",
@@ -17,13 +17,13 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@raytio/maxcryptor": "3.1.0",
20
- "@raytio/types": "7.1.0",
20
+ "@raytio/types": "7.2.0",
21
21
  "ramda": "0.29.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@types/ramda": "0.29.0",
25
- "jest": "29.5.0",
26
- "ts-jest": "29.1.0"
24
+ "@types/ramda": "0.29.2",
25
+ "jest": "29.6.1",
26
+ "ts-jest": "29.1.1"
27
27
  },
28
28
  "jest": {
29
29
  "transform": {