diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 72b7dd5b5..ad87c9b82 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: npm ci - run: npm run static-checks - - run: npm run test:ci:unit + - run: npm run test:unit - uses: azure/setup-helm@v1 with: @@ -61,7 +61,6 @@ jobs: kubectl port-forward --address 127.0.0.1 svc/public-gateway-pohttp 8081:8080 & kubectl port-forward --address 127.0.0.1 svc/public-gateway-cogrpc 8082:8081 & kubectl port-forward --address 127.0.0.1 svc/relaynet-pong-pohttp 8083:80 & - kubectl port-forward --address 127.0.0.1 svc/public-gateway-vault 8200:8200 & kubectl port-forward --address 127.0.0.1 svc/nats 4222:4222 & kubectl port-forward --address 127.0.0.1 svc/minio 9000:9000 & diff --git a/.gitignore b/.gitignore index b2aa9c28b..ddad907bf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ docs/vendor chart/charts chart/tmpcharts + +# Work around https://github.com/shelfio/jest-mongodb/issues/214 +/globalConfig.json diff --git a/chart/templates/cogrpc-deploy.yml b/chart/templates/cogrpc-deploy.yml index f8c7f0a5c..9906e4a15 100644 --- a/chart/templates/cogrpc-deploy.yml +++ b/chart/templates/cogrpc-deploy.yml @@ -18,7 +18,6 @@ spec: {{- toYaml .Values.podAnnotations | nindent 8 }} {{- end }} global-cm-digest: {{ include "relaynet-internet-gateway.resourceDigest" (merge (dict "fileName" "global-cm.yml") .) }} - generated-cm-digest: {{ include "relaynet-internet-gateway.resourceDigest" (merge (dict "fileName" "generated-cm.yml") .) }} mongo-cm-digest: {{ include "relaynet-internet-gateway.resourceDigest" (merge (dict "fileName" "mongo-cm.yml") .) }} global-secret-digest: {{ include "relaynet-internet-gateway.resourceDigest" (merge (dict "fileName" "global-secret.yml") .) }} labels: @@ -50,8 +49,6 @@ spec: envFrom: - configMapRef: name: {{ include "relaynet-internet-gateway.fullname" . }} - - configMapRef: - name: {{ include "relaynet-internet-gateway.fullname" . }}-generated - configMapRef: name: {{ include "relaynet-internet-gateway.fullname" . }}-mongo - secretRef: diff --git a/chart/templates/generated-cm.yml b/chart/templates/generated-cm.yml deleted file mode 100644 index 50f96bf45..000000000 --- a/chart/templates/generated-cm.yml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "relaynet-internet-gateway.fullname" . }}-generated - annotations: - "helm.sh/hook": "pre-install" - "helm.sh/hook-weight": "-5" - labels: - {{- include "relaynet-internet-gateway.labels" . | nindent 4 }} -data: - GATEWAY_KEY_ID: {{ default (randAlphaNum 12 | b64enc) .Values.gatewayKeyId | quote }} diff --git a/chart/templates/keygen-job.yml b/chart/templates/keygen-job.yml index f6250f08c..e31f48ba6 100644 --- a/chart/templates/keygen-job.yml +++ b/chart/templates/keygen-job.yml @@ -29,8 +29,6 @@ spec: envFrom: - configMapRef: name: {{ include "relaynet-internet-gateway.fullname" . }} - - configMapRef: - name: {{ include "relaynet-internet-gateway.fullname" . }}-generated - configMapRef: name: {{ include "relaynet-internet-gateway.fullname" . }}-mongo - secretRef: diff --git a/chart/templates/poweb-deploy.yml b/chart/templates/poweb-deploy.yml index 89ce05d97..9319b082f 100644 --- a/chart/templates/poweb-deploy.yml +++ b/chart/templates/poweb-deploy.yml @@ -18,7 +18,6 @@ spec: {{- toYaml .Values.podAnnotations | nindent 8 }} {{- end }} global-cm-digest: {{ include "relaynet-internet-gateway.resourceDigest" (merge (dict "fileName" "global-cm.yml") .) }} - generated-cm-digest: {{ include "relaynet-internet-gateway.resourceDigest" (merge (dict "fileName" "generated-cm.yml") .) }} mongo-cm-digest: {{ include "relaynet-internet-gateway.resourceDigest" (merge (dict "fileName" "mongo-cm.yml") .) }} global-secret-digest: {{ include "relaynet-internet-gateway.resourceDigest" (merge (dict "fileName" "global-secret.yml") .) }} labels: @@ -44,8 +43,6 @@ spec: envFrom: - configMapRef: name: {{ include "relaynet-internet-gateway.fullname" . }} - - configMapRef: - name: {{ include "relaynet-internet-gateway.fullname" . }}-generated - configMapRef: name: {{ include "relaynet-internet-gateway.fullname" . }}-mongo - secretRef: diff --git a/chart/values.schema.json b/chart/values.schema.json index 3ec6ff0c4..1ad631dec 100644 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -73,9 +73,6 @@ } } }, - "gatewayKeyId": { - "type": "string" - }, "proxyRequestIdHeader": { "type": "string" }, diff --git a/jest-mongodb-config.js b/jest-mongodb-config.js new file mode 100644 index 000000000..ac564d5ae --- /dev/null +++ b/jest-mongodb-config.js @@ -0,0 +1,11 @@ +module.exports = { + mongodbMemoryServerOptions: { + binary: { + version: '4.2.17', + skipMD5: false, + }, + instance: {}, + autoStart: false, + }, + useSharedDBForAllJestWorkers: false, +}; diff --git a/jest.config.ci.js b/jest.config.ci.js deleted file mode 100644 index eddfca3e2..000000000 --- a/jest.config.ci.js +++ /dev/null @@ -1,11 +0,0 @@ -const mainJestConfig = require('./jest.config'); - -module.exports = Object.assign({}, mainJestConfig, { - collectCoverageFrom: ['services/**/*.js'], - moduleFileExtensions: ['js'], - preset: null, - roots: ['build/main'], - testPathIgnorePatterns: [ - "build/main/functionalTests" - ], -}); diff --git a/jest.config.js b/jest.config.js index 1eed54567..6067d5016 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,4 @@ -// For a detailed explanation regarding each configuration property, visit: -// https://jestjs.io/docs/en/configuration.html +const { defaults: tsjPreset } = require('ts-jest/presets'); module.exports = { // All imported modules in your tests should be mocked automatically @@ -90,7 +89,7 @@ module.exports = { // notifyMode: "failure-change", // A preset that is used as a base for Jest's configuration - preset: "ts-jest", + preset: "@shelf/jest-mongodb", // Run tests from one or more projects // projects: null, @@ -130,8 +129,8 @@ module.exports = { // A list of paths to snapshot serializer modules Jest should use for snapshot testing // snapshotSerializers: [], - // The test environment that will be used for testing - testEnvironment: "node", + // Work around https://github.com/shelfio/jest-mongodb/issues/109 + // testEnvironment: "node", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, @@ -167,7 +166,7 @@ module.exports = { // timers: "real", // A map from regular expressions to paths to transformers - // transform: null, + transform: tsjPreset.transform, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // transformIgnorePatterns: [ diff --git a/package-lock.json b/package-lock.json index 7ba492d0f..64edb5754 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1295,15 +1295,15 @@ } }, "@peculiar/webcrypto": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.2.2.tgz", - "integrity": "sha512-xb8MEgfq93TAkIb70kn+llZgIFQwhdiCiOJHzekVTAS74Y+ae5bZn8KEsuycop/LXAm1kx+Kad/v9eTDTWuY/w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.2.3.tgz", + "integrity": "sha512-q7wDfZy3k/tpnsYB23/MyyDkjn6IdHh8w+xwoVMS5cu6CjVoFzngXDZEOOuSE4zus2yO6ciQhhHxd4XkLpwVnQ==", "requires": { - "@peculiar/asn1-schema": "^2.0.38", + "@peculiar/asn1-schema": "^2.0.44", "@peculiar/json-schema": "^1.1.12", "pvtsutils": "^1.2.1", "tslib": "^2.3.1", - "webcrypto-core": "^1.3.0" + "webcrypto-core": "^1.4.0" }, "dependencies": { "tslib": { @@ -1382,11 +1382,11 @@ } }, "@relaycorp/keystore-vault": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@relaycorp/keystore-vault/-/keystore-vault-1.2.12.tgz", - "integrity": "sha512-7JpMMI5ST2q4jLqMI5X5jHnyfuHlk8m2+IQHSikM1b9NiyDeSsg7+VEmsoJkXHHPka1a7zfLzDG230EmnoVqfQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@relaycorp/keystore-vault/-/keystore-vault-2.0.2.tgz", + "integrity": "sha512-PQYSqia0kCV6L+ruoT4t1l1pldX2UdHGir9iXzkH45psOW+lErwjLAbqWhVznVDY6/sQ1QypXyMH5BQNZYUyVQ==", "requires": { - "@relaycorp/relaynet-core": "^1.54.4", + "@relaycorp/relaynet-core": "^1.56.1", "axios": "^0.24.0" }, "dependencies": { @@ -1418,14 +1418,14 @@ } }, "@relaycorp/relaynet-core": { - "version": "1.54.7", - "resolved": "https://registry.npmjs.org/@relaycorp/relaynet-core/-/relaynet-core-1.54.7.tgz", - "integrity": "sha512-K1Uzc7RolJuPQ4sEIG8JBPZvrbfm37JXD3J7TLX6ZPeTI1I6V4ReCRhFbvkiMwEGzK1fmbrOZTa79WEAWxJ6iA==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@relaycorp/relaynet-core/-/relaynet-core-1.57.0.tgz", + "integrity": "sha512-nAUK8UPHmWCmJIpJlE+u8lGUw/t91wRvJV5V2aaJ0GmZmIGbBDl7Aj9tdCjAJ47BFR8zXx8al/tsNF1HN7fCuA==", "requires": { - "@peculiar/webcrypto": "^1.2.2", + "@peculiar/webcrypto": "^1.2.3", + "@stablelib/aes-kw": "^1.0.1", "@types/verror": "^1.10.5", "asn1js": "^2.1.1", - "binary-parser": "^2.0.1", "buffer-to-arraybuffer": "0.0.6", "dohdec": "^3.1.0", "is-valid-domain": "^0.1.4", @@ -1433,7 +1433,8 @@ "pkijs": "^2.2.1", "smart-buffer": "^4.2.0", "uuid4": "^2.0.2", - "verror": "^1.10.1" + "verror": "^1.10.1", + "webcrypto-core": "^1.4.0" }, "dependencies": { "pkijs": { @@ -1858,6 +1859,34 @@ } } }, + "@shelf/jest-mongodb": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@shelf/jest-mongodb/-/jest-mongodb-2.1.0.tgz", + "integrity": "sha512-sSSS/QnU8sKJUpVqKvcR4CR5ca8Mb1MpQY49H/we9bzyg/UsBhEcVehwaRSEZ13dOd8orGrNa8OW8438l2Gt1g==", + "dev": true, + "requires": { + "debug": "4.3.2", + "mongodb-memory-server": "7.3.6", + "uuid": "8.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "@sindresorhus/df": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@sindresorhus/df/-/df-3.1.1.tgz", @@ -1913,6 +1942,56 @@ "@sinonjs/commons": "^1.7.0" } }, + "@stablelib/aes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/aes/-/aes-1.0.1.tgz", + "integrity": "sha512-bMiezJDeFONDHbMEa+Kic26962+bwkZfsHPAmcqTjLaHCAhEQuK3i1H0POPOkcHCdj75oVRIqFCraCA0cyHPvw==", + "requires": { + "@stablelib/binary": "^1.0.1", + "@stablelib/blockcipher": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, + "@stablelib/aes-kw": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/aes-kw/-/aes-kw-1.0.1.tgz", + "integrity": "sha512-KrOkiRex1tQTbWk+hFB5fFw4vqKhNnTUtlCRf1bhUEOFp7hadWe49/sLa/P4X4FBQVoh3Z9Lj0zS1OWu/AHA1w==", + "requires": { + "@stablelib/aes": "^1.0.1", + "@stablelib/binary": "^1.0.1", + "@stablelib/blockcipher": "^1.0.1", + "@stablelib/constant-time": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, + "@stablelib/binary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", + "integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==", + "requires": { + "@stablelib/int": "^1.0.1" + } + }, + "@stablelib/blockcipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/blockcipher/-/blockcipher-1.0.1.tgz", + "integrity": "sha512-4bkpV8HUAv0CgI1fUqkPUEEvv3RXQ3qBkuZaSWhshXGAz1JCpriesgiO9Qs4f0KzBJkCtvcho5n7d/RKvnHbew==" + }, + "@stablelib/constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/constant-time/-/constant-time-1.0.1.tgz", + "integrity": "sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg==" + }, + "@stablelib/int": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", + "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==" + }, + "@stablelib/wipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", + "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==" + }, "@stroncium/procfs": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@stroncium/procfs/-/procfs-1.2.1.tgz", @@ -2216,6 +2295,12 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "@types/tmp": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.2.tgz", + "integrity": "sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A==", + "dev": true + }, "@types/verror": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.5.tgz", @@ -2504,6 +2589,23 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "async-mutex": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", + "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", + "dev": true, + "requires": { + "tslib": "^2.3.1" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + } + } + }, "async-retry": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz", @@ -2825,11 +2927,6 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "binary-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-2.0.1.tgz", - "integrity": "sha512-7kZx8vZCq+V/0XkaOjLpCkBdOYCzjau+yt1PDMM04Q73WgtNYSoM6gOpl/Ky7qbANrEkcbX9ya+6Fdmj1WeU+w==" - }, "bl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", @@ -2946,6 +3043,12 @@ "isarray": "^1.0.0" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3219,6 +3322,12 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, "compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -3460,10 +3569,9 @@ "integrity": "sha512-7u+uNfnjWkX+YFQfivvW24TjaJG6ahvTrfw1auq7KlC7osuGcZBIWGBvB9UcENjH6JnLVhMqlRripk1dSHjAUA==" }, "date-fns": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz", - "integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==", - "dev": true + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.27.0.tgz", + "integrity": "sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q==" }, "dateformat": { "version": "3.0.3", @@ -4408,6 +4516,15 @@ "bser": "2.1.1" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -4426,6 +4543,17 @@ "to-regex-range": "^5.0.1" } }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, "find-my-way": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-4.3.3.tgz", @@ -4552,6 +4680,12 @@ } } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "fs-extra": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", @@ -4638,6 +4772,12 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, + "get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true + }, "get-stream": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", @@ -5401,9 +5541,9 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-valid-domain": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-valid-domain/-/is-valid-domain-0.1.4.tgz", - "integrity": "sha512-Caa6rwGze6pihA29wy3T1yNXzd53caGHvL0OfJ8RLtv0tVVzVZGlxFcQ0W8kls/uG0QUrv2B3J9xi/YB5/cfUQ==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/is-valid-domain/-/is-valid-domain-0.1.5.tgz", + "integrity": "sha512-ilzfGo1kXzoVpSLplJWOexoiuAc6mRK+vPlNAeEPVJ29RagETpCz0izg6CZfY72DCuA+PCrEAEJeaecRLMNq5Q==", "requires": { "punycode": "^2.1.1" } @@ -7662,6 +7802,12 @@ } } }, + "md5-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", + "integrity": "sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw==", + "dev": true + }, "memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", @@ -7872,6 +8018,93 @@ "saslprep": "^1.0.0" } }, + "mongodb-memory-server": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-7.3.6.tgz", + "integrity": "sha512-JfuY7uJD9TZxq6C4YVuCjiP2v0V/NYb19Wki6rFovdJvkgxGp5/KXKaazu47leECYAtJc2ajW1c009M3hRM+6A==", + "dev": true, + "requires": { + "mongodb-memory-server-core": "7.3.6", + "tslib": "^2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + } + } + }, + "mongodb-memory-server-core": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-7.3.6.tgz", + "integrity": "sha512-dxprJ5Xqb0y/9nqv709BGf8Cz4dnInhG3sTuYvMJijK4cAHYxk0xkKrvZX2X9PrYiCQqKix59hHMBGyBetN3hg==", + "dev": true, + "requires": { + "@types/tmp": "^0.2.0", + "async-mutex": "^0.3.0", + "camelcase": "^6.1.0", + "debug": "^4.2.0", + "find-cache-dir": "^3.3.1", + "get-port": "^5.1.1", + "https-proxy-agent": "^5.0.0", + "md5-file": "^5.0.0", + "mkdirp": "^1.0.4", + "mongodb": "^3.6.9", + "new-find-package-json": "^1.1.0", + "semver": "^7.3.5", + "tar-stream": "^2.1.4", + "tmp": "^0.2.1", + "tslib": "^2.3.0", + "uuid": "^8.3.1", + "yauzl": "^2.10.0" + }, + "dependencies": { + "camelcase": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", + "dev": true + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + } + } + }, "mongoose": { "version": "5.13.7", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.7.tgz", @@ -8018,6 +8251,39 @@ "integrity": "sha1-5tq3/r9a2Bbqgc9cYpxaDr3nLBo=", "dev": true }, + "new-find-package-json": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-1.1.0.tgz", + "integrity": "sha512-KOH3BNZcTKPzEkaJgG2iSUaurxKmefqRKmCOYH+8xqJytNIgjqU4J88BHfK+gy/UlEzlhccLyuJDJAcCgexSwA==", + "dev": true, + "requires": { + "debug": "^4.3.2", + "tslib": "^2.3.0" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + } + } + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -10566,6 +10832,12 @@ "pify": "^3.0.0" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -12275,6 +12547,42 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + } + } + }, "teeny-request": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.1.1.tgz", @@ -12367,6 +12675,26 @@ "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==" }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -13058,11 +13386,11 @@ } }, "webcrypto-core": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.3.0.tgz", - "integrity": "sha512-/+Hz+uNM6T8FtizWRYMNdGTXxWaljLFzQ5GKU4WqCTZKpaki94YqDA39h/SpWxEZfgkVMZzrqqtPlfy2+BloQw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.4.0.tgz", + "integrity": "sha512-HY3Zo0GcRIQUUDnlZ/shGjN+4f7LVMkdJZoGPog+oHhJsJdMz6iM8Za5xZ0t6qg7Fx/JXXz+oBv2J2p982hGTQ==", "requires": { - "@peculiar/asn1-schema": "^2.0.38", + "@peculiar/asn1-schema": "^2.0.44", "@peculiar/json-schema": "^1.1.12", "asn1js": "^2.1.1", "pvtsutils": "^1.2.0", @@ -13336,6 +13664,16 @@ "decamelize": "^1.2.0" } }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index ca16145e8..a0a567922 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,6 @@ "static-checks": "run-p static-checks:*", "static-checks:lint": "tslint --project .", "static-checks:prettier": "prettier \"src/**/*.ts\" --list-different", - "test:ci:unit": "run-s build test:ci:unit:jest", - "test:ci:unit:jest": "jest --config jest.config.ci.js --coverage", "cov": "run-s build test:unit && opn coverage/lcov-report/index.html", "clean": "trash build test coverage" }, @@ -27,6 +25,7 @@ "@relaycorp/shared-config": "^1.6.0", "@relaycorp/ws-mock": "^4.2.0", "@semantic-release/exec": "^5.0.0", + "@shelf/jest-mongodb": "^2.1.0", "@types/jest": "^26.0.24", "@types/mongoose": "^5.10.5", "@types/pino": "^6.3.11", @@ -34,7 +33,6 @@ "@types/split2": "^3.2.1", "@types/verror": "^1.10.5", "@types/ws": "^7.4.7", - "date-fns": "^2.23.0", "fastify-plugin": "^3.0.0", "jest": "^26.6.3", "jest-extended": "^0.11.5", @@ -71,15 +69,16 @@ "dependencies": { "@grpc/grpc-js": "^1.3.7", "@relaycorp/cogrpc": "^1.3.23", - "@relaycorp/keystore-vault": "^1.2.12", + "@relaycorp/keystore-vault": "^2.0.2", "@relaycorp/object-storage": "^1.4.5", "@relaycorp/pino-cloud": "^1.0.4", - "@relaycorp/relaynet-core": "^1.54.7", + "@relaycorp/relaynet-core": "^1.57.0", "@relaycorp/relaynet-pohttp": "^1.7.6", "@typegoose/typegoose": "^8.1.1", "abort-controller": "^3.0.0", "abortable-iterator": "^3.0.0", "buffer-to-arraybuffer": "0.0.6", + "date-fns": "^2.27.0", "env-var": "^7.0.1", "fastify": "^3.20.2", "fastify-mongoose": "^0.3.0", diff --git a/skaffold.yaml b/skaffold.yaml index c147ed064..d56e0c349 100644 --- a/skaffold.yaml +++ b/skaffold.yaml @@ -16,7 +16,6 @@ deploy: artifactOverrides: image.repository: public-gateway setValues: - gatewayKeyId: MTM1NzkK relaynet-pong.current_endpoint_key_id: aGVsbG8K relaynet-pong.current_endpoint_session_key_id: OTc1MzEK wait: true diff --git a/src/_test_utils.ts b/src/_test_utils.ts index 8ce7b0d26..179c5b308 100644 --- a/src/_test_utils.ts +++ b/src/_test_utils.ts @@ -9,6 +9,7 @@ import { } from '@relaycorp/relaynet-core'; import bufferToArray from 'buffer-to-arraybuffer'; import { BinaryLike, createHash, Hash } from 'crypto'; +import { Connection, createConnection } from 'mongoose'; import pino, { symbols as PinoSymbols } from 'pino'; import split2 from 'split2'; @@ -186,6 +187,7 @@ export async function generateCDAChain(pdaChain: ExternalPdaChain): Promise Connection { + let connection: Connection; + + const connect = () => + createConnection((global as any).__MONGO_URI__, { + useCreateIndex: true, + useNewUrlParser: true, + useUnifiedTopology: true, + }); + + beforeAll(async () => { + connection = await connect(); + }); + + beforeEach(async () => { + if (connection.readyState === 0) { + connection = await connect(); + } + }); + + afterEach(async () => { + Object.values(connection.collections).map((c) => c.deleteMany({})); + }); + + afterAll(async () => { + await connection.close(true); + }); + + return () => connection; +} diff --git a/src/backingServices/mongo.spec.ts b/src/backingServices/mongo.spec.ts index 0f25aaa47..166d087b2 100644 --- a/src/backingServices/mongo.spec.ts +++ b/src/backingServices/mongo.spec.ts @@ -2,7 +2,7 @@ import { EnvVarError } from 'env-var'; import mongoose, { Connection } from 'mongoose'; import { mockSpy, MONGO_ENV_VARS } from '../_test_utils'; -import { MongoPublicKeyStore } from '../MongoPublicKeyStore'; +import { MongoPublicKeyStore } from '../keystores/MongoPublicKeyStore'; import { configureMockEnvVars } from '../services/_test_utils'; import { createMongooseConnectionFromEnv, @@ -10,7 +10,7 @@ import { initMongoDBKeyStore, } from './mongo'; -const MOCK_MONGOOSE_CONNECTION = { what: 'The connection' }; +const MOCK_MONGOOSE_CONNECTION = { model: { bind: mockSpy(jest.fn()) } } as any as Connection; const MOCK_MONGOOSE_CREATE_CONNECTION = mockSpy( jest.spyOn(mongoose, 'createConnection'), jest.fn().mockResolvedValue(MOCK_MONGOOSE_CONNECTION), @@ -125,10 +125,8 @@ describe('createMongooseConnectionFromEnv', () => { }); describe('initMongoDBKeyStore', () => { - const mongooseConnection = { model: { bind: jest.fn() } } as any as Connection; - test('MongoPublicKeyStore instance should be returned', () => { - const keyStore = initMongoDBKeyStore(mongooseConnection); + const keyStore = initMongoDBKeyStore(MOCK_MONGOOSE_CONNECTION); expect(keyStore).toBeInstanceOf(MongoPublicKeyStore); }); diff --git a/src/backingServices/mongo.ts b/src/backingServices/mongo.ts index fa564139c..5a575344a 100644 --- a/src/backingServices/mongo.ts +++ b/src/backingServices/mongo.ts @@ -1,7 +1,8 @@ import { PublicKeyStore } from '@relaycorp/relaynet-core'; import { get as getEnvVar } from 'env-var'; import { Connection, ConnectionOptions, createConnection } from 'mongoose'; -import { MongoPublicKeyStore } from '../MongoPublicKeyStore'; + +import { MongoPublicKeyStore } from '../keystores/MongoPublicKeyStore'; export function getMongooseConnectionArgsFromEnv(): { readonly uri: string; @@ -15,6 +16,7 @@ export function getMongooseConnectionArgsFromEnv(): { options: { dbName: mongoDb, pass: mongoPassword, + useCreateIndex: true, useNewUrlParser: true, useUnifiedTopology: true, user: mongoUser, @@ -29,7 +31,7 @@ export async function createMongooseConnectionFromEnv(): Promise { } /** - * Return the public key store used by the gateway. + * Return the public key store. * * @param connection * diff --git a/src/bin/generate-keypairs.ts b/src/bin/generate-keypairs.ts index e7ae53e9d..b7eae21eb 100644 --- a/src/bin/generate-keypairs.ts +++ b/src/bin/generate-keypairs.ts @@ -1,11 +1,14 @@ import { Certificate, generateRSAKeyPair, issueGatewayCertificate } from '@relaycorp/relaynet-core'; import { getModelForClass } from '@typegoose/typegoose'; import bufferToArray from 'buffer-to-arraybuffer'; -import { get as getEnvVar } from 'env-var'; +import { addDays } from 'date-fns'; +import { Connection } from 'mongoose'; import { createMongooseConnectionFromEnv } from '../backingServices/mongo'; import { initVaultKeyStore } from '../backingServices/vault'; +import { MongoCertificateStore } from '../keystores/MongoCertificateStore'; import { OwnCertificate } from '../models'; +import { Config, ConfigKey } from '../utilities/config'; import { configureExitHandling } from '../utilities/exitHandling'; import { makeLogger } from '../utilities/logging'; @@ -14,57 +17,68 @@ configureExitHandling(LOGGER); const NODE_CERTIFICATE_TTL_DAYS = 360; -const KEY_ID_BASE64 = getEnvVar('GATEWAY_KEY_ID').required().asString(); - const privateKeyStore = initVaultKeyStore(); async function main(): Promise { - const keyId = Buffer.from(KEY_ID_BASE64, 'base64'); + const connection = await createMongooseConnectionFromEnv(); try { - await privateKeyStore.fetchNodeKey(keyId); - LOGGER.warn(`Gateway key ${KEY_ID_BASE64} already exists`); + const certificateStore = new MongoCertificateStore(connection); + + await generateKeyPair(connection, certificateStore); + await migrateDeprecatedCertificates(connection, certificateStore); + } finally { + await connection.close(); + } +} + +async function generateKeyPair( + connection: Connection, + certificateStore: MongoCertificateStore, +): Promise { + const config = new Config(connection); + const currentPrivateAddress = await config.get(ConfigKey.CURRENT_PRIVATE_ADDRESS); + if (currentPrivateAddress) { + LOGGER.info({ privateAddress: currentPrivateAddress }, `Gateway key pair already exists`); return; - } catch (error) { - LOGGER.info(`Gateway key will be created because it doesn't already exist`); } + LOGGER.info(`Gateway key will be created because it doesn't already exist`); const gatewayKeyPair = await generateRSAKeyPair(); + const privateAddress = await privateKeyStore.saveIdentityKey(gatewayKeyPair.privateKey); - const nodeCertEndDate = new Date(); - nodeCertEndDate.setDate(nodeCertEndDate.getDate() + NODE_CERTIFICATE_TTL_DAYS); const gatewayCertificate = await issueGatewayCertificate({ issuerPrivateKey: gatewayKeyPair.privateKey, subjectPublicKey: gatewayKeyPair.publicKey, - validityEndDate: nodeCertEndDate, + validityEndDate: addDays(new Date(), NODE_CERTIFICATE_TTL_DAYS), }); - // Force the certificate to have the serial number specified in GATEWAY_KEY_ID. This nasty - // hack won't be necessary once https://github.com/relaycorp/relaynet-internet-gateway/issues/49 - // is done. - // tslint:disable-next-line:no-object-mutation - (gatewayCertificate as any).pkijsCertificate.serialNumber.valueBlock.valueHex = - bufferToArray(keyId); + await certificateStore.save(gatewayCertificate); - await privateKeyStore.saveNodeKey(gatewayKeyPair.privateKey, gatewayCertificate); - await saveOwnCertificate(gatewayCertificate); + await config.set(ConfigKey.CURRENT_PRIVATE_ADDRESS, privateAddress); - LOGGER.info( - { - gatewayCertificate: base64Encode(gatewayCertificate.serialize()), - keyPairId: KEY_ID_BASE64, - }, - 'Identity key pair was successfully generated', - ); + LOGGER.info({ privateAddress }, 'Identity key pair was successfully generated'); } -async function saveOwnCertificate(certificate: Certificate): Promise { - const connection = await createMongooseConnectionFromEnv(); - const ownCertificateModel = getModelForClass(OwnCertificate, { existingConnection: connection }); - await ownCertificateModel.create({ serializationDer: Buffer.from(certificate.serialize()) }); - await connection.close(); -} +// TODO: Delete once we've deployed it to Frankfurt +async function migrateDeprecatedCertificates( + connection: Connection, + certificateStore: MongoCertificateStore, +): Promise { + const deprecatedCertificateModel = getModelForClass(OwnCertificate, { + existingConnection: connection, + }); + const deprecatedCertificateRecords = await deprecatedCertificateModel.find({}); + const deprecatedCertificates = deprecatedCertificateRecords.map((c) => + Certificate.deserialize(bufferToArray(c.serializationDer)), + ); -function base64Encode(payload: ArrayBuffer | Buffer): string { - return Buffer.from(payload).toString('base64'); + await Promise.all(deprecatedCertificates.map((c) => certificateStore.save(c))); + + await deprecatedCertificateModel.deleteMany({}); + + LOGGER.info( + { deprecatedCertificates: deprecatedCertificates.length }, + 'Migrated deprecated certificates', + ); } main(); diff --git a/src/certs.spec.ts b/src/certs.spec.ts index 7a43ed72f..91283b89f 100644 --- a/src/certs.spec.ts +++ b/src/certs.spec.ts @@ -1,81 +1,66 @@ -/* tslint:disable:no-object-mutation */ +import { Certificate, generateRSAKeyPair, issueGatewayCertificate } from '@relaycorp/relaynet-core'; +import { addMinutes } from 'date-fns'; -import { generateRSAKeyPair } from '@relaycorp/relaynet-core'; -import * as typegoose from '@typegoose/typegoose'; -import bufferToArray from 'buffer-to-arraybuffer'; -import { Connection } from 'mongoose'; - -import { mockSpy } from './_test_utils'; +import { setUpTestDBConnection } from './_test_utils'; import { retrieveOwnCertificates } from './certs'; -import { OwnCertificate } from './models'; -import { expectBuffersToEqual, generateStubEndpointCertificate } from './services/_test_utils'; - -const stubConnection: Connection = { whoAreYou: 'the-stub-connection' } as any; +import { MongoCertificateStore } from './keystores/MongoCertificateStore'; +import { Config, ConfigKey } from './utilities/config'; -const stubFind = mockSpy(jest.fn(), () => []); -const stubGetModelForClass = mockSpy(jest.spyOn(typegoose, 'getModelForClass'), () => ({ - find: stubFind, -})); +const getMongooseConnection = setUpTestDBConnection(); -let stubOwnCerts: readonly OwnCertificate[]; +let certificate1: Certificate; +let certificate2: Certificate; beforeAll(async () => { - const keyPair1 = await generateRSAKeyPair(); - const ownCert1 = new OwnCertificate(); - ownCert1.serializationDer = Buffer.from( - (await generateStubEndpointCertificate(keyPair1)).serialize(), - ); - - const keyPair2 = await generateRSAKeyPair(); - const ownCert2 = new OwnCertificate(); - ownCert2.serializationDer = Buffer.from( - (await generateStubEndpointCertificate(keyPair2)).serialize(), - ); - - stubOwnCerts = [ownCert1, ownCert2]; + const identityKeyPair = await generateRSAKeyPair(); + certificate1 = await issueGatewayCertificate({ + issuerPrivateKey: identityKeyPair.privateKey, + subjectPublicKey: identityKeyPair.publicKey, + validityEndDate: addMinutes(new Date(), 1), + }); + certificate2 = await issueGatewayCertificate({ + issuerPrivateKey: identityKeyPair.privateKey, + subjectPublicKey: identityKeyPair.publicKey, + validityEndDate: addMinutes(new Date(), 3), + }); }); -describe('retrieveOwnCertificates', () => { - test('The specified connection should be used', async () => { - await retrieveOwnCertificates(stubConnection); - - expect(stubGetModelForClass).toBeCalledTimes(1); - expect(stubGetModelForClass).toBeCalledWith(OwnCertificate, { - existingConnection: stubConnection, - }); - }); +let certificateStore: MongoCertificateStore; +beforeEach(async () => { + const connection = getMongooseConnection(); - test('All records should be queried', async () => { - await retrieveOwnCertificates(stubConnection); + certificateStore = new MongoCertificateStore(connection); - expect(stubFind).toBeCalledTimes(1); - expect(stubFind).toBeCalledWith({}); - }); + const config = new Config(connection); + await config.set( + ConfigKey.CURRENT_PRIVATE_ADDRESS, + await certificate1.calculateSubjectPrivateAddress(), + ); +}); +describe('retrieveOwnCertificates', () => { test('An empty array should be returned when there are no certificates', async () => { - const certs = await retrieveOwnCertificates(stubConnection); + const certs = await retrieveOwnCertificates(getMongooseConnection()); expect(certs).toEqual([]); }); test('A single certificate should be returned when there is one certificate', async () => { - stubFind.mockReset(); - stubFind.mockResolvedValueOnce([stubOwnCerts[0]]); + await certificateStore.save(certificate1); - const certs = await retrieveOwnCertificates(stubConnection); + const certs = await retrieveOwnCertificates(getMongooseConnection()); expect(certs).toHaveLength(1); - expectBuffersToEqual(certs[0].serialize(), bufferToArray(stubOwnCerts[0].serializationDer)); + expect(certificate1.isEqual(certs[0])).toBeTrue(); }); test('Multiple certificates should be retuned when there are multiple certificates', async () => { - stubFind.mockReset(); - stubFind.mockResolvedValueOnce(stubOwnCerts); + await certificateStore.save(certificate1); + await certificateStore.save(certificate2); - const certs = await retrieveOwnCertificates(stubConnection); + const certs = await retrieveOwnCertificates(getMongooseConnection()); - expect(certs).toHaveLength(stubOwnCerts.length); - for (let i = 0; i < stubOwnCerts.length; i++) { - expectBuffersToEqual(certs[i].serialize(), bufferToArray(stubOwnCerts[i].serializationDer)); - } + expect(certs).toHaveLength(2); + expect(certs.filter((c) => certificate1.isEqual(c))).toHaveLength(1); + expect(certs.filter((c) => certificate2.isEqual(c))).toHaveLength(1); }); }); diff --git a/src/certs.ts b/src/certs.ts index 55350cf97..1c6f34a9d 100644 --- a/src/certs.ts +++ b/src/certs.ts @@ -1,14 +1,15 @@ import { Certificate } from '@relaycorp/relaynet-core'; -import { getModelForClass } from '@typegoose/typegoose'; -import bufferToArray from 'buffer-to-arraybuffer'; import { Connection } from 'mongoose'; -import { OwnCertificate } from './models'; +import { MongoCertificateStore } from './keystores/MongoCertificateStore'; +import { Config, ConfigKey } from './utilities/config'; export async function retrieveOwnCertificates( connection: Connection, ): Promise { - const ownCertificateModel = getModelForClass(OwnCertificate, { existingConnection: connection }); - const ownCerts = await ownCertificateModel.find({}); - return ownCerts.map((c) => Certificate.deserialize(bufferToArray(c.serializationDer))); + const store = new MongoCertificateStore(connection); + const config = new Config(connection); + + const privateAddress = await config.get(ConfigKey.CURRENT_PRIVATE_ADDRESS); + return store.retrieveAll(privateAddress!!); } diff --git a/src/functionalTests/jest.config.js b/src/functionalTests/jest.config.js index 5b0f5739b..100faa01a 100644 --- a/src/functionalTests/jest.config.js +++ b/src/functionalTests/jest.config.js @@ -2,11 +2,8 @@ const mainJestConfig = require('../../jest.config'); module.exports = { moduleFileExtensions: mainJestConfig.moduleFileExtensions, - preset: mainJestConfig.preset, + preset: 'ts-jest', roots: ['.'], - testEnvironment: mainJestConfig.testEnvironment, - setupFilesAfterEnv: [ - ...mainJestConfig.setupFilesAfterEnv, - './jest.setup.ts', - ], + testEnvironment: 'node', + setupFilesAfterEnv: [...mainJestConfig.setupFilesAfterEnv, './jest.setup.ts'], }; diff --git a/src/functionalTests/jest.setup.ts b/src/functionalTests/jest.setup.ts index b2a5155f0..fa52194dd 100644 --- a/src/functionalTests/jest.setup.ts +++ b/src/functionalTests/jest.setup.ts @@ -2,7 +2,6 @@ jest.setTimeout(10_000); const TEST_ENV_VARS = { COGRPC_REQUIRE_TLS: 'false', - GATEWAY_KEY_ID: 'MTM1NzkK', NATS_CLUSTER_ID: 'stan', NATS_SERVER_URL: 'nats://127.0.0.1:4222', OBJECT_STORE_ACCESS_KEY_ID: 'test-key', @@ -11,9 +10,6 @@ const TEST_ENV_VARS = { OBJECT_STORE_SECRET_KEY: 'test-secret', OBJECT_STORE_TLS_ENABLED: 'false', POHTTP_TLS_REQUIRED: 'false', - VAULT_KV_PREFIX: 'gw-keys', - VAULT_TOKEN: 'root', - VAULT_URL: 'http://127.0.0.1:8200', }; // tslint:disable-next-line:no-object-mutation Object.assign(process.env, TEST_ENV_VARS); diff --git a/src/functionalTests/poweb_server.test.ts b/src/functionalTests/poweb_server.test.ts index 50d2c2f9a..7a37e6d49 100644 --- a/src/functionalTests/poweb_server.test.ts +++ b/src/functionalTests/poweb_server.test.ts @@ -19,12 +19,7 @@ import pipe from 'it-pipe'; import { asyncIterableToArray, ExternalPdaChain, iterableTake } from '../_test_utils'; import { expectBuffersToEqual } from '../services/_test_utils'; import { GW_POWEB_LOCAL_PORT } from './services'; -import { - createAndRegisterPrivateGateway, - getPublicGatewayCertificate, - registerPrivateGateway, - sleep, -} from './utils'; +import { createAndRegisterPrivateGateway, registerPrivateGateway, sleep } from './utils'; describe('PoWeb server', () => { describe('Node registration', () => { @@ -38,9 +33,6 @@ describe('PoWeb server', () => { derSerializePublicKey(await registration.privateNodeCertificate.getPublicKey()), ).resolves.toEqual(await derSerializePublicKey(privateGatewayKeyPair.publicKey)); - const actualPublicGatewayCertificate = await getPublicGatewayCertificate(); - expect(actualPublicGatewayCertificate.isEqual(registration.gatewayCertificate)).toBeTrue(); - await expect( registration.privateNodeCertificate.getCertificationPath( [], diff --git a/src/functionalTests/utils.ts b/src/functionalTests/utils.ts index c3d4666a8..2d409518c 100644 --- a/src/functionalTests/utils.ts +++ b/src/functionalTests/utils.ts @@ -1,13 +1,11 @@ import { initObjectStoreClient, ObjectStoreClient } from '@relaycorp/object-storage'; import { - Certificate, generateRSAKeyPair, issueDeliveryAuthorization, issueEndpointCertificate, PrivateNodeRegistration, PrivateNodeRegistrationRequest, SessionKey, - UnboundKeyPair, } from '@relaycorp/relaynet-core'; import { PoWebClient } from '@relaycorp/relaynet-poweb'; import { get as getEnvVar } from 'env-var'; @@ -15,7 +13,6 @@ import { connect as stanConnect, Stan } from 'node-nats-streaming'; import uuid from 'uuid-random'; import { ExternalPdaChain } from '../_test_utils'; -import { initVaultKeyStore } from '../backingServices/vault'; import { GW_POWEB_LOCAL_PORT } from './services'; export const IS_GITHUB = getEnvVar('IS_GITHUB').asBool(); @@ -47,20 +44,6 @@ export function connectToNatsStreaming(): Promise { }); } -async function getPublicGatewayKeyPair(): Promise { - const privateKeyStore = initVaultKeyStore(); - const publicGatewayKeyId = Buffer.from( - getEnvVar('GATEWAY_KEY_ID').required().asString(), - 'base64', - ); - return privateKeyStore.fetchNodeKey(publicGatewayKeyId); -} - -export async function getPublicGatewayCertificate(): Promise { - const keyPair = await getPublicGatewayKeyPair(); - return keyPair.certificate; -} - export interface PrivateGatewayRegistration { readonly pdaChain: ExternalPdaChain; readonly publicGatewaySessionKey: SessionKey; diff --git a/src/keystores/MongoCertificateStore.spec.ts b/src/keystores/MongoCertificateStore.spec.ts new file mode 100644 index 000000000..b585ce4fd --- /dev/null +++ b/src/keystores/MongoCertificateStore.spec.ts @@ -0,0 +1,159 @@ +import { + Certificate, + generateRSAKeyPair, + getPrivateAddressFromIdentityKey, + issueGatewayCertificate, +} from '@relaycorp/relaynet-core'; +import { getModelForClass, ReturnModelType } from '@typegoose/typegoose'; +import { addDays, addSeconds, subSeconds } from 'date-fns'; + +import { setUpTestDBConnection } from '../_test_utils'; +import { Certificate as CertificateModel } from '../models'; +import { MongoCertificateStore } from './MongoCertificateStore'; + +const getConnection = setUpTestDBConnection(); +let certificateModel: ReturnModelType; +let store: MongoCertificateStore; +beforeAll(async () => { + const connection = getConnection(); + certificateModel = getModelForClass(CertificateModel, { existingConnection: connection }); + store = new MongoCertificateStore(connection); +}); + +let identityKeyPair: CryptoKeyPair; +let subjectPrivateAddress: string; +let validCertificate: Certificate; +let expiredCertificate: Certificate; +beforeAll(async () => { + identityKeyPair = await generateRSAKeyPair(); + subjectPrivateAddress = await getPrivateAddressFromIdentityKey(identityKeyPair.publicKey); + + validCertificate = await issueGatewayCertificate({ + issuerPrivateKey: identityKeyPair.privateKey, + subjectPublicKey: identityKeyPair.publicKey, + validityEndDate: addSeconds(new Date(), 15), + }); + expiredCertificate = await issueGatewayCertificate({ + issuerPrivateKey: identityKeyPair.privateKey, + subjectPublicKey: identityKeyPair.publicKey, + validityEndDate: subSeconds(new Date(), 1), + validityStartDate: subSeconds(new Date(), 2), + }); +}); + +describe('saveData', () => { + test('All attributes should be saved', async () => { + await store.save(validCertificate); + + const certificateStored = await certificateModel.findOne({ subjectPrivateAddress }).exec(); + expect(certificateStored).toMatchObject>({ + certificateSerialized: Buffer.from(validCertificate.serialize()), + expiryDate: validCertificate.expiryDate, + }); + }); + + test('The same subject should be allowed to have multiple certificates', async () => { + const certificate2 = await issueGatewayCertificate({ + issuerPrivateKey: identityKeyPair.privateKey, + subjectPublicKey: identityKeyPair.publicKey, + validityEndDate: addDays(validCertificate.expiryDate, 1), + }); + + await store.save(validCertificate); + await store.save(certificate2); + + const certificateRecords = await certificateModel.find({ subjectPrivateAddress }).exec(); + expect(certificateRecords).toHaveLength(2); + expect(certificateRecords[0]).toMatchObject>({ + certificateSerialized: Buffer.from(validCertificate.serialize()), + expiryDate: validCertificate.expiryDate, + }); + expect(certificateRecords[1]).toMatchObject>({ + certificateSerialized: Buffer.from(certificate2.serialize()), + expiryDate: certificate2.expiryDate, + }); + }); + + test('Certificates with the same subject and expiry date should be deduped', async () => { + const certificate2 = await issueGatewayCertificate({ + issuerPrivateKey: identityKeyPair.privateKey, + subjectPublicKey: identityKeyPair.publicKey, + validityEndDate: validCertificate.expiryDate, + }); + + await store.save(validCertificate); + await store.save(certificate2); + + const certificateRecords = await certificateModel.find({ subjectPrivateAddress }).exec(); + expect(certificateRecords).toHaveLength(1); + expect(certificateRecords[0]).toMatchObject>({ + certificateSerialized: Buffer.from(certificate2.serialize()), + expiryDate: certificate2.expiryDate, + }); + }); +}); + +describe('retrieveLatestSerialization', () => { + test('Nothing should be returned if subject has no certificates', async () => { + await expect(store.retrieveLatest(subjectPrivateAddress)).resolves.toBeNull(); + }); + + test('Expired certificates should not be returned', async () => { + await store.save(expiredCertificate); + + await expect(store.retrieveLatest(subjectPrivateAddress)).resolves.toBeNull(); + }); + + test('The latest valid certificate should be returned', async () => { + await store.save(validCertificate); + const newestCertificate = await issueGatewayCertificate({ + issuerPrivateKey: identityKeyPair.privateKey, + subjectPublicKey: identityKeyPair.publicKey, + validityEndDate: addSeconds(validCertificate.expiryDate, 1), + }); + await store.save(newestCertificate); + + await expect(store.retrieveLatest(subjectPrivateAddress)).resolves.toSatisfy((c) => + newestCertificate.isEqual(c), + ); + }); +}); + +describe('retrieveAllSerializations', () => { + test('Nothing should be returned if there are no certificates', async () => { + await expect(store.retrieveAll(subjectPrivateAddress)).resolves.toBeEmpty(); + }); + + test('Expired certificates should not be returned', async () => { + await store.save(expiredCertificate); + + await expect(store.retrieveAll(subjectPrivateAddress)).resolves.toBeEmpty(); + }); + + test('All valid certificates should be returned', async () => { + await store.save(expiredCertificate); + await store.save(validCertificate); + const newestCertificate = await issueGatewayCertificate({ + issuerPrivateKey: identityKeyPair.privateKey, + subjectPublicKey: identityKeyPair.publicKey, + validityEndDate: addSeconds(validCertificate.expiryDate, 1), + }); + await store.save(newestCertificate); + + const allCertificates = await store.retrieveAll(subjectPrivateAddress); + + expect(allCertificates).toHaveLength(2); + expect(allCertificates.filter((c) => c.isEqual(validCertificate))).not.toBeEmpty(); + expect(allCertificates.filter((c) => c.isEqual(newestCertificate))).not.toBeEmpty(); + }); +}); + +describe('deleteExpired', () => { + test('Valid certificates should not be deleted', async () => { + await store.save(validCertificate); + + await store.deleteExpired(); + + await expect(store.retrieveAll(subjectPrivateAddress)).resolves.toHaveLength(1); + }); +}); diff --git a/src/keystores/MongoCertificateStore.ts b/src/keystores/MongoCertificateStore.ts new file mode 100644 index 000000000..200a49f4d --- /dev/null +++ b/src/keystores/MongoCertificateStore.ts @@ -0,0 +1,59 @@ +import { CertificateStore } from '@relaycorp/relaynet-core'; +import { getModelForClass, ReturnModelType } from '@typegoose/typegoose'; +import bufferToArray from 'buffer-to-arraybuffer'; +import { Connection } from 'mongoose'; + +import { Certificate } from '../models'; + +export class MongoCertificateStore extends CertificateStore { + private readonly certificateModel: ReturnModelType; + + constructor(connection: Connection) { + super(); + + this.certificateModel = getModelForClass(Certificate, { existingConnection: connection }); + } + + public async deleteExpired(): Promise { + // Do nothing. Trust that the model will delete expired records. + } + + protected async saveData( + subjectPrivateAddress: string, + subjectCertificateSerialized: ArrayBuffer, + subjectCertificateExpiryDate: Date, + ): Promise { + const record: Certificate = { + certificateSerialized: Buffer.from(subjectCertificateSerialized), + expiryDate: subjectCertificateExpiryDate, + subjectPrivateAddress, + }; + await this.certificateModel + .updateOne( + { + expiryDate: subjectCertificateExpiryDate, + subjectPrivateAddress, + }, + record, + { upsert: true }, + ) + .exec(); + } + + protected async retrieveLatestSerialization( + subjectPrivateAddress: string, + ): Promise { + const record = await this.certificateModel + .findOne({ subjectPrivateAddress }) + .sort({ expiryDate: -1 }) + .exec(); + return record ? bufferToArray(record.certificateSerialized) : null; + } + + protected async retrieveAllSerializations( + subjectPrivateAddress: string, + ): Promise { + const records = await this.certificateModel.find({ subjectPrivateAddress }).exec(); + return records.map((r) => bufferToArray(r.certificateSerialized)); + } +} diff --git a/src/MongoPublicKeyStore.spec.ts b/src/keystores/MongoPublicKeyStore.spec.ts similarity index 97% rename from src/MongoPublicKeyStore.spec.ts rename to src/keystores/MongoPublicKeyStore.spec.ts index e564807ed..068d905d6 100644 --- a/src/MongoPublicKeyStore.spec.ts +++ b/src/keystores/MongoPublicKeyStore.spec.ts @@ -10,8 +10,8 @@ import { import * as typegoose from '@typegoose/typegoose'; import { Connection } from 'mongoose'; -import { mockSpy } from './_test_utils'; -import { PeerPublicKeyData } from './models'; +import { mockSpy } from '../_test_utils'; +import { PeerPublicKeyData } from '../models'; import { MongoPublicKeyStore } from './MongoPublicKeyStore'; const STUB_CONNECTION: Connection = { what: 'the-stub-connection' } as any; diff --git a/src/MongoPublicKeyStore.ts b/src/keystores/MongoPublicKeyStore.ts similarity index 96% rename from src/MongoPublicKeyStore.ts rename to src/keystores/MongoPublicKeyStore.ts index 1b0c5a859..517ba0cbd 100644 --- a/src/MongoPublicKeyStore.ts +++ b/src/keystores/MongoPublicKeyStore.ts @@ -2,7 +2,7 @@ import { PublicKeyStore, SessionPublicKeyData } from '@relaycorp/relaynet-core'; import { getModelForClass } from '@typegoose/typegoose'; import { Connection, Model } from 'mongoose'; -import { PeerPublicKeyData } from './models'; +import { PeerPublicKeyData } from '../models'; export class MongoPublicKeyStore extends PublicKeyStore { protected readonly keyDataModel: Model; diff --git a/src/models.ts b/src/models.ts index 77dc13e62..87271983f 100644 --- a/src/models.ts +++ b/src/models.ts @@ -4,11 +4,34 @@ import { index, prop } from '@typegoose/typegoose'; const SECONDS_IN_A_DAY = 86400; +export class ConfigItem { + @prop({ required: true, unique: true }) + public key!: string; + + @prop({ required: true }) + public value!: string; +} + +/** + * @deprecated Use [Certificate] instead + */ export class OwnCertificate { @prop({ required: true }) public serializationDer!: Buffer; } +@index({ subjectPrivateAddress: 1 }) +export class Certificate { + @prop({ required: true }) + public subjectPrivateAddress!: string; + + @prop({ required: true }) + public certificateSerialized!: Buffer; + + @prop({ required: true, expires: 0 }) + public expiryDate!: Date; +} + export class PeerPublicKeyData { protected static TTL_DAYS = 30; diff --git a/src/queueWorkers/crcIncoming.spec.ts b/src/queueWorkers/crcIncoming.spec.ts index a72ce994d..d27bfaef6 100644 --- a/src/queueWorkers/crcIncoming.spec.ts +++ b/src/queueWorkers/crcIncoming.spec.ts @@ -28,7 +28,7 @@ import * as mongo from '../backingServices/mongo'; import { NatsStreamingClient } from '../backingServices/natsStreaming'; import * as objectStorage from '../backingServices/objectStorage'; import * as vault from '../backingServices/vault'; -import * as mongoPublicKeyStore from '../MongoPublicKeyStore'; +import * as mongoPublicKeyStore from '../keystores/MongoPublicKeyStore'; import { ParcelStore } from '../parcelStore'; import { castMock, @@ -115,11 +115,8 @@ beforeAll(async () => { }); beforeEach(async () => { - await mockPrivateKeyStore.registerNodeKey( - certificateChain.publicGatewayPrivateKey, - certificateChain.publicGatewayCert, - ); - await mockPrivateKeyStore.saveInitialSessionKey( + await mockPrivateKeyStore.saveIdentityKey(certificateChain.publicGatewayPrivateKey); + await mockPrivateKeyStore.saveUnboundSessionKey( publicGatewaySessionPrivateKey, publicGatewaySessionKey.keyId, ); diff --git a/src/queueWorkers/crcIncoming.ts b/src/queueWorkers/crcIncoming.ts index 5d9c20fb3..b88e9400c 100644 --- a/src/queueWorkers/crcIncoming.ts +++ b/src/queueWorkers/crcIncoming.ts @@ -18,7 +18,7 @@ import { createMongooseConnectionFromEnv } from '../backingServices/mongo'; import { NatsStreamingClient } from '../backingServices/natsStreaming'; import { initObjectStoreFromEnv } from '../backingServices/objectStorage'; import { initVaultKeyStore } from '../backingServices/vault'; -import { MongoPublicKeyStore } from '../MongoPublicKeyStore'; +import { MongoPublicKeyStore } from '../keystores/MongoPublicKeyStore'; import { ParcelStore } from '../parcelStore'; import { configureExitHandling } from '../utilities/exitHandling'; import { makeLogger } from '../utilities/logging'; diff --git a/src/services/_test_utils.ts b/src/services/_test_utils.ts index 71e0c3411..33735b1b0 100644 --- a/src/services/_test_utils.ts +++ b/src/services/_test_utils.ts @@ -194,11 +194,11 @@ export function testDisallowedMethods( }); } -export function mockFastifyMongoose(mockMongoProperty: { readonly db: Connection }): void { +export function mockFastifyMongoose(mockMongoProperty: () => { readonly db: Connection }): void { const mockFastifyPlugin = fastifyPlugin; jest.mock('fastify-mongoose', () => { function mockFunc(fastify: FastifyInstance, _options: any, next: () => void): void { - fastify.decorate('mongo', mockMongoProperty); + fastify.decorate('mongo', mockMongoProperty()); next(); } diff --git a/src/services/cogrpc/server.spec.ts b/src/services/cogrpc/server.spec.ts index 3e37b47a2..5ffe16b2d 100644 --- a/src/services/cogrpc/server.spec.ts +++ b/src/services/cogrpc/server.spec.ts @@ -5,6 +5,7 @@ import { Logger } from 'pino'; import selfsigned from 'selfsigned'; import { makeMockLogging, mockSpy, partialPinoLog } from '../../_test_utils'; +import { createMongooseConnectionFromEnv } from '../../backingServices/mongo'; import { MAX_RAMF_MESSAGE_SIZE } from '../../constants'; import * as exitHandling from '../../utilities/exitHandling'; import * as logging from '../../utilities/logging'; @@ -37,7 +38,6 @@ const mockSelfSigned = mockSpy(jest.spyOn(selfsigned, 'generate'), () => mockSel const mockExitHandler = mockSpy(jest.spyOn(exitHandling, 'configureExitHandling')); const BASE_ENV_VARS = { - GATEWAY_KEY_ID: 'base64-encoded key id', NATS_CLUSTER_ID: 'nats-cluster-id', NATS_SERVER_URL: 'nats://example.com', OBJECT_STORE_BUCKET: 'bucket-name', @@ -58,7 +58,6 @@ describe('runServer', () => { }); test.each([ - 'GATEWAY_KEY_ID', 'NATS_SERVER_URL', 'NATS_CLUSTER_ID', 'OBJECT_STORE_BUCKET', @@ -129,7 +128,7 @@ describe('runServer', () => { debug: expect.anything(), error: expect.anything(), }), - gatewayKeyIdBase64: BASE_ENV_VARS.GATEWAY_KEY_ID, + getMongooseConnection: createMongooseConnectionFromEnv, natsClusterId: BASE_ENV_VARS.NATS_CLUSTER_ID, natsServerUrl: BASE_ENV_VARS.NATS_SERVER_URL, parcelStoreBucket: BASE_ENV_VARS.OBJECT_STORE_BUCKET, diff --git a/src/services/cogrpc/server.ts b/src/services/cogrpc/server.ts index 2df6e0fcf..c33346672 100644 --- a/src/services/cogrpc/server.ts +++ b/src/services/cogrpc/server.ts @@ -4,6 +4,7 @@ import { get as getEnvVar } from 'env-var'; import * as grpcHealthCheck from 'grpc-js-health-check'; import { Logger } from 'pino'; import * as selfsigned from 'selfsigned'; +import { createMongooseConnectionFromEnv } from '../../backingServices/mongo'; import { configureExitHandling } from '../../utilities/exitHandling'; import { MAX_RAMF_MESSAGE_SIZE } from '../../constants'; @@ -23,7 +24,6 @@ export async function runServer(logger?: Logger): Promise { const baseLogger = logger ?? makeLogger(); configureExitHandling(baseLogger); - const gatewayKeyIdBase64 = getEnvVar('GATEWAY_KEY_ID').required().asString(); const publicAddress = getEnvVar('PUBLIC_ADDRESS').required().asString(); const parcelStoreBucket = getEnvVar('OBJECT_STORE_BUCKET').required().asString(); const natsServerUrl = getEnvVar('NATS_SERVER_URL').required().asString(); @@ -40,7 +40,7 @@ export async function runServer(logger?: Logger): Promise { const serviceImplementation = await makeServiceImplementation({ baseLogger, - gatewayKeyIdBase64, + getMongooseConnection: createMongooseConnectionFromEnv, natsClusterId, natsServerUrl, parcelStoreBucket, diff --git a/src/services/cogrpc/service.spec.ts b/src/services/cogrpc/service.spec.ts index 8bb04d886..ad9a4f531 100644 --- a/src/services/cogrpc/service.spec.ts +++ b/src/services/cogrpc/service.spec.ts @@ -9,7 +9,6 @@ import { InvalidMessageError, issueEndpointCertificate, MockPrivateKeyStore, - MockPublicKeyStore, Parcel, ParcelCollectionAck, RAMFSyntaxError, @@ -18,10 +17,9 @@ import { SessionKeyPair, UnknownKeyError, } from '@relaycorp/relaynet-core'; -import * as typegoose from '@typegoose/typegoose'; import bufferToArray from 'buffer-to-arraybuffer'; +import { addDays } from 'date-fns'; import { EventEmitter } from 'events'; -import mongoose from 'mongoose'; import { arrayBufferFrom, @@ -34,15 +32,17 @@ import { partialPinoLog, partialPinoLogger, PdaChain, + setUpTestDBConnection, UUID4_REGEX, } from '../../_test_utils'; -import * as mongo from '../../backingServices/mongo'; import * as natsStreaming from '../../backingServices/natsStreaming'; import * as vault from '../../backingServices/vault'; -import * as ccaFulfillments from '../../ccaFulfilments'; +import { recordCCAFulfillment, wasCCAFulfilled } from '../../ccaFulfilments'; import * as certs from '../../certs'; +import { MongoCertificateStore } from '../../keystores/MongoCertificateStore'; import * as parcelCollectionAck from '../../parcelCollection'; import { ParcelStore } from '../../parcelStore'; +import { Config, ConfigKey } from '../../utilities/config'; import { configureMockEnvVars, generatePdaChain, getMockInstance } from '../_test_utils'; import { MockGrpcBidiCall } from './_test_utils'; import { makeServiceImplementation, ServiceImplementationOptions } from './service'; @@ -55,8 +55,7 @@ const OBJECT_STORE_BUCKET = 'parcels-bucket'; const NATS_SERVER_URL = 'nats://example.com'; const NATS_CLUSTER_ID = 'nats-cluster-id'; -const TOMORROW = new Date(); -TOMORROW.setDate(TOMORROW.getDate() + 1); +const TOMORROW = addDays(new Date(), 1); let pdaChain: PdaChain; let cdaChain: CDAChain; @@ -67,12 +66,7 @@ beforeAll(async () => { privateGatewayAddress = await pdaChain.privateGatewayCert.calculateSubjectPrivateAddress(); }); -const MOCK_MONGOOSE_CONNECTION: mongoose.Connection = new EventEmitter() as any; -const MOCK_CREATE_MONGOOSE_CONNECTION = mockSpy( - jest.spyOn(mongo, 'createMongooseConnectionFromEnv'), - jest.fn().mockResolvedValue(MOCK_MONGOOSE_CONNECTION), -); -beforeEach(() => MOCK_MONGOOSE_CONNECTION.removeAllListeners()); +const getMongooseConnection = setUpTestDBConnection(); configureMockEnvVars({ OBJECT_STORE_ACCESS_KEY_ID: 'id', @@ -91,7 +85,7 @@ beforeEach(() => { MOCK_LOGS = mockLogging.logs; SERVICE_IMPLEMENTATION_OPTIONS = { baseLogger: mockLogging.logger, - gatewayKeyIdBase64: pdaChain.publicGatewayCert.getSerialNumber().toString('base64'), + getMongooseConnection: jest.fn().mockImplementation(getMongooseConnection), natsClusterId: NATS_CLUSTER_ID, natsServerUrl: NATS_SERVER_URL, parcelStoreBucket: OBJECT_STORE_BUCKET, @@ -104,16 +98,18 @@ beforeEach(() => { describe('makeServiceImplementation', () => { describe('Mongoose connection', () => { test('Connection should be created preemptively before any RPC', async () => { - expect(MOCK_CREATE_MONGOOSE_CONNECTION).not.toBeCalled(); + expect(SERVICE_IMPLEMENTATION_OPTIONS.getMongooseConnection).not.toBeCalled(); await makeServiceImplementation(SERVICE_IMPLEMENTATION_OPTIONS); - expect(MOCK_CREATE_MONGOOSE_CONNECTION).toBeCalledTimes(1); + expect(SERVICE_IMPLEMENTATION_OPTIONS.getMongooseConnection).toBeCalled(); }); test('Errors while establishing connection should be propagated', async () => { const error = new Error('Database credentials are wrong'); - MOCK_CREATE_MONGOOSE_CONNECTION.mockRejectedValue(error); + getMockInstance(SERVICE_IMPLEMENTATION_OPTIONS.getMongooseConnection).mockRejectedValue( + error, + ); await expect(makeServiceImplementation(SERVICE_IMPLEMENTATION_OPTIONS)).rejects.toEqual( error, @@ -121,11 +117,15 @@ describe('makeServiceImplementation', () => { }); test('Errors after establishing connection should be logged', async (cb) => { + const mockConnection = new EventEmitter(); + getMockInstance(SERVICE_IMPLEMENTATION_OPTIONS.getMongooseConnection).mockResolvedValue( + mockConnection, + ); await makeServiceImplementation(SERVICE_IMPLEMENTATION_OPTIONS); const error = new Error('Database credentials are wrong'); - MOCK_MONGOOSE_CONNECTION.on('error', (err) => { + mockConnection.on('error', (err) => { expect(MOCK_LOGS).toContainEqual( partialPinoLog('error', 'Mongoose connection error', { err: expect.objectContaining({ message: err.message }), @@ -133,7 +133,7 @@ describe('makeServiceImplementation', () => { ); cb(); }); - MOCK_MONGOOSE_CONNECTION.emit('error', error); + mockConnection.emit('error', error); }); }); }); @@ -405,28 +405,32 @@ describe('deliverCargo', () => { describe('collectCargo', () => { const PRIVATE_KEY_STORE = new MockPrivateKeyStore(); - mockSpy(jest.spyOn(vault, 'initVaultKeyStore'), () => PRIVATE_KEY_STORE); let publicGatewaySessionKeyPair: SessionKeyPair; beforeAll(async () => { publicGatewaySessionKeyPair = await SessionKeyPair.generate(); }); beforeEach(async () => { PRIVATE_KEY_STORE.clear(); - await PRIVATE_KEY_STORE.registerNodeKey( - pdaChain.publicGatewayPrivateKey, - pdaChain.publicGatewayCert, - ); - await PRIVATE_KEY_STORE.saveSubsequentSessionKey( + await PRIVATE_KEY_STORE.saveIdentityKey(pdaChain.publicGatewayPrivateKey); + await PRIVATE_KEY_STORE.saveBoundSessionKey( publicGatewaySessionKeyPair.privateKey, publicGatewaySessionKeyPair.sessionKey.keyId, privateGatewayAddress, ); }); + mockSpy(jest.spyOn(vault, 'initVaultKeyStore'), () => PRIVATE_KEY_STORE); + + beforeEach(async () => { + const connection = getMongooseConnection(); - const PUBLIC_KEYS_STORE = new MockPublicKeyStore(); - mockSpy(jest.spyOn(mongo, 'initMongoDBKeyStore'), (connection) => { - expect(connection).toBe(MOCK_MONGOOSE_CONNECTION); - return PUBLIC_KEYS_STORE; + const certificateStore = new MongoCertificateStore(connection); + await certificateStore.save(pdaChain.publicGatewayCert); + + const config = new Config(connection); + await config.set( + ConfigKey.CURRENT_PRIVATE_ADDRESS, + await pdaChain.publicGatewayCert.calculateSubjectPrivateAddress(), + ); }); let SERVICE: CargoRelayServerMethodSet; @@ -450,17 +454,6 @@ describe('collectCargo', () => { }, ); - mockSpy(jest.spyOn(typegoose, 'getModelForClass')); - - const MOCK_WAS_CCA_FULFILLED = mockSpy( - jest.spyOn(ccaFulfillments, 'wasCCAFulfilled'), - async () => false, - ); - const MOCK_RECORD_CCA_FULFILLMENT = mockSpy( - jest.spyOn(ccaFulfillments, 'recordCCAFulfillment'), - async () => null, - ); - let DUMMY_PARCEL: Parcel; let DUMMY_PARCEL_SERIALIZED: Buffer; beforeAll(async () => { @@ -478,6 +471,7 @@ describe('collectCargo', () => { DUMMY_PARCEL_SERIALIZED = Buffer.from(await DUMMY_PARCEL.serialize(keyPair.privateKey)); }); + let cca: CargoCollectionAuthorization; let ccaSerialized: Buffer; let privateGatewaySessionPrivateKey: CryptoKey; beforeAll(async () => { @@ -487,6 +481,7 @@ describe('collectCargo', () => { publicGatewaySessionKeyPair.sessionKey, pdaChain.privateGatewayPrivateKey, ); + cca = generatedCCA.cca; ccaSerialized = generatedCCA.ccaSerialized; privateGatewaySessionPrivateKey = generatedCCA.sessionPrivateKey; }); @@ -646,7 +641,7 @@ describe('collectCargo', () => { }); test('INVALID_ARGUMENT should be returned if CCA recipient is malformed', async (cb) => { - const cca = new CargoCollectionAuthorization( + const malformedCCA = new CargoCollectionAuthorization( '0deadbeef', pdaChain.privateGatewayCert, Buffer.from([]), @@ -654,7 +649,7 @@ describe('collectCargo', () => { CALL.on('error', (error) => { expect(MOCK_LOGS).toContainEqual( partialPinoLog('info', 'Refusing CCA with malformed recipient', { - ccaRecipientAddress: cca.recipientAddress, + ccaRecipientAddress: malformedCCA.recipientAddress, grpcClient: CALL.getPeer(), grpcMethod: 'collectCargo', peerGatewayAddress: privateGatewayAddress, @@ -669,7 +664,7 @@ describe('collectCargo', () => { }); const invalidCCASerialized = Buffer.from( - await cca.serialize(pdaChain.privateGatewayPrivateKey), + await malformedCCA.serialize(pdaChain.privateGatewayPrivateKey), ); CALL.metadata.add('Authorization', `Relaynet-CCA ${invalidCCASerialized.toString('base64')}`); @@ -677,7 +672,7 @@ describe('collectCargo', () => { }); test('INVALID_ARGUMENT should be returned if CCA is not bound for current gateway', async (cb) => { - const cca = new CargoCollectionAuthorization( + const invalidCCA = new CargoCollectionAuthorization( `https://different-${PUBLIC_ADDRESS}`, pdaChain.privateGatewayCert, Buffer.from([]), @@ -685,7 +680,7 @@ describe('collectCargo', () => { CALL.on('error', (error) => { expect(MOCK_LOGS).toContainEqual( partialPinoLog('info', 'Refusing CCA bound for another gateway', { - ccaRecipientAddress: cca.recipientAddress, + ccaRecipientAddress: invalidCCA.recipientAddress, grpcClient: CALL.getPeer(), grpcMethod: 'collectCargo', peerGatewayAddress: privateGatewayAddress, @@ -700,7 +695,7 @@ describe('collectCargo', () => { }); const invalidCCASerialized = Buffer.from( - await cca.serialize(pdaChain.privateGatewayPrivateKey), + await invalidCCA.serialize(pdaChain.privateGatewayPrivateKey), ); CALL.metadata.add('Authorization', `Relaynet-CCA ${invalidCCASerialized.toString('base64')}`); @@ -708,6 +703,8 @@ describe('collectCargo', () => { }); test('PERMISSION_DENIED should be returned if CCA was already fulfilled', async (cb) => { + await recordCCAFulfillment(cca, getMongooseConnection()); + CALL.on('error', (error) => { expect(MOCK_LOGS).toContainEqual( partialPinoLog('info', 'Refusing CCA that was already fulfilled', { @@ -724,7 +721,6 @@ describe('collectCargo', () => { cb(); }); - MOCK_WAS_CCA_FULFILLED.mockResolvedValue(true); CALL.metadata.add('Authorization', serializeAuthzMetadata(ccaSerialized)); await SERVICE.collectCargo(CALL.convertToGrpcStream()); @@ -830,7 +826,7 @@ describe('collectCargo', () => { expect(MOCK_GENERATE_PCAS).toBeCalledTimes(1); expect(MOCK_GENERATE_PCAS).toBeCalledWith( await pdaChain.privateGatewayCert.calculateSubjectPrivateAddress(), - MOCK_MONGOOSE_CONNECTION, + getMongooseConnection(), ); }); @@ -884,12 +880,7 @@ describe('collectCargo', () => { await SERVICE.collectCargo(CALL.convertToGrpcStream()); - expect(MOCK_RECORD_CCA_FULFILLMENT).toBeCalledTimes(1); - const cca = await CargoCollectionAuthorization.deserialize(bufferToArray(ccaSerialized)); - expect(MOCK_RECORD_CCA_FULFILLMENT).toBeCalledWith( - expect.objectContaining({ id: cca.id }), - MOCK_MONGOOSE_CONNECTION, - ); + await expect(wasCCAFulfilled(cca, getMongooseConnection())).resolves.toBeTrue(); }); test('CCA fulfillment should be logged and end the call', async () => { @@ -951,7 +942,7 @@ describe('collectCargo', () => { cb(); }); - await SERVICE.collectCargo(CALL.convertToGrpcStream()); + SERVICE.collectCargo(CALL.convertToGrpcStream()); }); test('Call should end with an error for the client', async (cb) => { @@ -964,16 +955,16 @@ describe('collectCargo', () => { cb(); }); - await SERVICE.collectCargo(CALL.convertToGrpcStream()); + SERVICE.collectCargo(CALL.convertToGrpcStream()); }); test('CCA should not be marked as fulfilled', async (cb) => { - CALL.on('error', () => { - expect(MOCK_RECORD_CCA_FULFILLMENT).not.toBeCalled(); + CALL.on('error', async () => { + await expect(wasCCAFulfilled(cca, getMongooseConnection())).resolves.toBeFalse(); cb(); }); - await SERVICE.collectCargo(CALL.convertToGrpcStream()); + SERVICE.collectCargo(CALL.convertToGrpcStream()); }); }); @@ -985,12 +976,12 @@ describe('collectCargo', () => { recipientAddress: string, payload: ArrayBuffer, ): Promise { - const cca = new CargoCollectionAuthorization( + const auth = new CargoCollectionAuthorization( recipientAddress, pdaChain.privateGatewayCert, Buffer.from(payload), ); - return Buffer.from(await cca.serialize(pdaChain.privateGatewayPrivateKey)); + return Buffer.from(await auth.serialize(pdaChain.privateGatewayPrivateKey)); } async function validateCargoDelivery( diff --git a/src/services/cogrpc/service.ts b/src/services/cogrpc/service.ts index 286ca650c..a13caff83 100644 --- a/src/services/cogrpc/service.ts +++ b/src/services/cogrpc/service.ts @@ -14,7 +14,7 @@ import { Connection } from 'mongoose'; import { Logger } from 'pino'; import uuid from 'uuid-random'; -import { createMongooseConnectionFromEnv, initMongoDBKeyStore } from '../../backingServices/mongo'; +import { initMongoDBKeyStore } from '../../backingServices/mongo'; import { NatsStreamingClient, PublisherMessage } from '../../backingServices/natsStreaming'; import { initObjectStoreFromEnv } from '../../backingServices/objectStorage'; import { initVaultKeyStore } from '../../backingServices/vault'; @@ -22,6 +22,7 @@ import { recordCCAFulfillment, wasCCAFulfilled } from '../../ccaFulfilments'; import { retrieveOwnCertificates } from '../../certs'; import { generatePCAs } from '../../parcelCollection'; import { ParcelObject, ParcelStore } from '../../parcelStore'; +import { Config, ConfigKey } from '../../utilities/config'; const INTERNAL_SERVER_ERROR = { code: grpc.status.UNAVAILABLE, @@ -30,7 +31,7 @@ const INTERNAL_SERVER_ERROR = { export interface ServiceImplementationOptions { readonly baseLogger: Logger; - readonly gatewayKeyIdBase64: string; + readonly getMongooseConnection: () => Promise; readonly parcelStoreBucket: string; readonly natsServerUrl: string; readonly natsClusterId: string; @@ -43,11 +44,9 @@ export async function makeServiceImplementation( const objectStoreClient = initObjectStoreFromEnv(); const parcelStore = new ParcelStore(objectStoreClient, options.parcelStoreBucket); - const currentKeyId = Buffer.from(options.gatewayKeyIdBase64, 'base64'); - const vaultKeyStore = initVaultKeyStore(); - const mongooseConnection = await createMongooseConnectionFromEnv(); + const mongooseConnection = await options.getMongooseConnection(); mongooseConnection.on('error', (err) => options.baseLogger.error({ err }, 'Mongoose connection error'), ); @@ -79,7 +78,6 @@ export async function makeServiceImplementation( call, mongooseConnection, options.publicAddress, - currentKeyId, parcelStore, vaultKeyStore, logger, @@ -149,7 +147,6 @@ async function collectCargo( call: grpc.ServerDuplexStream, mongooseConnection: Connection, ownPublicAddress: string, - currentKeyId: Buffer, parcelStore: ParcelStore, vaultKeyStore: VaultPrivateKeyStore, logger: Logger, @@ -224,10 +221,12 @@ async function collectCargo( let cargoesCollected = 0; async function* encapsulateMessagesInCargo(messages: CargoMessageStream): AsyncIterable { - const { privateKey } = await vaultKeyStore.fetchNodeKey(currentKeyId); + const config = new Config(mongooseConnection); + const privateAddress = await config.get(ConfigKey.CURRENT_PRIVATE_ADDRESS); + const privateKey = await vaultKeyStore.retrieveIdentityKey(privateAddress!!); yield* await gateway.generateCargoes( messages, - cca.senderCertificate, + await cca.senderCertificate.calculateSubjectPrivateAddress(), privateKey, ccr.cargoDeliveryAuthorization, ); diff --git a/src/services/pohttp/routes.spec.ts b/src/services/pohttp/routes.spec.ts index a69a415ae..5e6c011a5 100644 --- a/src/services/pohttp/routes.spec.ts +++ b/src/services/pohttp/routes.spec.ts @@ -18,7 +18,7 @@ import { makeServer } from './server'; jest.mock('../../utilities/exitHandling'); const mockFastifyMongooseObject = { db: { what: 'The mongoose.Connection' } as any, ObjectId: {} }; -mockFastifyMongoose(mockFastifyMongooseObject); +mockFastifyMongoose(() => mockFastifyMongooseObject); const validRequestOptions: InjectOptions = { headers: { diff --git a/src/services/poweb/RouteOptions.ts b/src/services/poweb/RouteOptions.ts index f6c451119..34d16f845 100644 --- a/src/services/poweb/RouteOptions.ts +++ b/src/services/poweb/RouteOptions.ts @@ -1,5 +1 @@ -import { UnboundKeyPair } from '@relaycorp/relaynet-core'; - -export default interface RouteOptions { - readonly keyPairRetriever: () => Promise; -} +export default interface RouteOptions {} diff --git a/src/services/poweb/_test_utils.ts b/src/services/poweb/_test_utils.ts index 0dff1eea4..a735be0b7 100644 --- a/src/services/poweb/_test_utils.ts +++ b/src/services/poweb/_test_utils.ts @@ -1,20 +1,28 @@ import { MockPrivateKeyStore, Parcel } from '@relaycorp/relaynet-core'; import { Connection } from 'mongoose'; -import { arrayToAsyncIterable, mockSpy, MONGO_ENV_VARS, PdaChain } from '../../_test_utils'; +import { + arrayToAsyncIterable, + mockSpy, + MONGO_ENV_VARS, + PdaChain, + setUpTestDBConnection, +} from '../../_test_utils'; import * as vault from '../../backingServices/vault'; +import { MongoCertificateStore } from '../../keystores/MongoCertificateStore'; import { ParcelStore } from '../../parcelStore'; +import { Config, ConfigKey } from '../../utilities/config'; import { configureMockEnvVars, generatePdaChain, mockFastifyMongoose } from '../_test_utils'; export interface FixtureSet extends PdaChain { - readonly mongooseConnection: Connection; + readonly getMongooseConnection: () => Connection; readonly parcelStore: ParcelStore; readonly privateKeyStore: MockPrivateKeyStore; } export function setUpCommonFixtures(): () => FixtureSet { - const mockMongooseConnection: Connection = { whatIsThis: 'The Mongoose connection' } as any; - mockFastifyMongoose({ db: mockMongooseConnection }); + const getMongooseConnection = setUpTestDBConnection(); + mockFastifyMongoose(() => ({ db: getMongooseConnection() })); const mockParcelStore: ParcelStore = { liveStreamActiveParcelsForGateway: mockSpy( @@ -45,25 +53,33 @@ export function setUpCommonFixtures(): () => FixtureSet { let mockPrivateKeyStore: MockPrivateKeyStore; beforeEach(async () => { mockPrivateKeyStore = new MockPrivateKeyStore(); - await mockPrivateKeyStore.registerNodeKey( - certificatePath.publicGatewayPrivateKey, - certificatePath.publicGatewayCert, - ); + await mockPrivateKeyStore.saveIdentityKey(certificatePath.publicGatewayPrivateKey); }); mockSpy(jest.spyOn(vault, 'initVaultKeyStore'), () => mockPrivateKeyStore); + beforeEach(async () => { + const connection = getMongooseConnection(); + + const certificateStore = new MongoCertificateStore(connection); + await certificateStore.save(certificatePath.publicGatewayCert); + + const config = new Config(connection); + await config.set( + ConfigKey.CURRENT_PRIVATE_ADDRESS, + await certificatePath.publicGatewayCert.calculateSubjectPrivateAddress(), + ); + }); + const mockEnvVars = configureMockEnvVars(MONGO_ENV_VARS); beforeEach(() => { - const gatewayCertificate = certificatePath.publicGatewayCert; mockEnvVars({ ...MONGO_ENV_VARS, - GATEWAY_KEY_ID: gatewayCertificate.getSerialNumber().toString('base64'), GATEWAY_VERSION: '1.0.2', }); }); return () => ({ - mongooseConnection: mockMongooseConnection, + getMongooseConnection, parcelStore: mockParcelStore, privateKeyStore: mockPrivateKeyStore, ...certificatePath, diff --git a/src/services/poweb/parcelCollection.spec.ts b/src/services/poweb/parcelCollection.spec.ts index 26c8857a4..7afc68819 100644 --- a/src/services/poweb/parcelCollection.spec.ts +++ b/src/services/poweb/parcelCollection.spec.ts @@ -51,7 +51,7 @@ let mockLogging: MockLogging; beforeEach(() => { mockLogging = makeMockLogging(); mockWSServer = makeWebSocketServer( - getFixtures().mongooseConnection, + getFixtures().getMongooseConnection(), REQUEST_ID_HEADER, mockLogging.logger, ); @@ -68,7 +68,7 @@ beforeAll(async () => { const MOCK_RETRIEVE_OWN_CERTIFICATES = mockSpy( jest.spyOn(certs, 'retrieveOwnCertificates'), async (connection) => { - expect(connection).toBe(getFixtures().mongooseConnection); + expect(connection).toBe(getFixtures().getMongooseConnection()); const fixtures = getFixtures(); return [fixtures.publicGatewayCert]; }, @@ -90,7 +90,7 @@ mockSpy(jest.spyOn(WS, 'createWebSocketStream'), createMockWebSocketStream); describe('WebSocket server configuration', () => { test('Path should be /v1/parcel-collection', () => { const wsServer = makeWebSocketServer( - getFixtures().mongooseConnection, + getFixtures().getMongooseConnection(), REQUEST_ID_HEADER, mockLogging.logger, ); @@ -100,7 +100,7 @@ describe('WebSocket server configuration', () => { test('Maximum incoming payload size should be 2 kib', () => { const wsServer = makeWebSocketServer( - getFixtures().mongooseConnection, + getFixtures().getMongooseConnection(), REQUEST_ID_HEADER, mockLogging.logger, ); @@ -110,7 +110,7 @@ describe('WebSocket server configuration', () => { test('Clients should not be tracked', () => { const wsServer = makeWebSocketServer( - getFixtures().mongooseConnection, + getFixtures().getMongooseConnection(), REQUEST_ID_HEADER, mockLogging.logger, ); diff --git a/src/services/poweb/parcelDelivery.spec.ts b/src/services/poweb/parcelDelivery.spec.ts index 3e2cd2acb..a69773584 100644 --- a/src/services/poweb/parcelDelivery.spec.ts +++ b/src/services/poweb/parcelDelivery.spec.ts @@ -192,7 +192,7 @@ test('Valid parcels should result in an HTTP 202 response', async () => { expect.objectContaining({ id: PARCEL.id }), Buffer.from(PARCEL_SERIALIZED), await fixtures.privateGatewayCert.calculateSubjectPrivateAddress(), - fixtures.mongooseConnection, + fixtures.getMongooseConnection(), mockNatsStreamingConnection, expect.objectContaining({ debug: expect.toBeFunction(), info: expect.toBeFunction() }), ); diff --git a/src/services/poweb/preRegistration.ts b/src/services/poweb/preRegistration.ts index 84426227f..c42067cb1 100644 --- a/src/services/poweb/preRegistration.ts +++ b/src/services/poweb/preRegistration.ts @@ -1,20 +1,20 @@ import { PrivateNodeRegistrationAuthorization } from '@relaycorp/relaynet-core'; import bufferToArray from 'buffer-to-arraybuffer'; import { FastifyInstance, FastifyReply } from 'fastify'; +import { initVaultKeyStore } from '../../backingServices/vault'; +import { Config, ConfigKey } from '../../utilities/config'; import { registerDisallowedMethods } from '../fastify'; import { CONTENT_TYPES } from './contentTypes'; -import RouteOptions from './RouteOptions'; const ENDPOINT_URL = '/v1/pre-registrations'; const SHA256_HEX_DIGEST_LENGTH = 64; -export default async function registerRoutes( - fastify: FastifyInstance, - options: RouteOptions, -): Promise { +export default async function registerRoutes(fastify: FastifyInstance): Promise { registerDisallowedMethods(['POST'], ENDPOINT_URL, fastify); + const privateKeyStore = initVaultKeyStore(); + fastify.route<{ readonly Body: string }>({ method: 'POST', url: ENDPOINT_URL, @@ -28,10 +28,12 @@ export default async function registerRoutes( return reply.code(400).send({ message: 'Payload is not a SHA-256 digest' }); } - const publicGatewayKeyPair = await options.keyPairRetriever(); + const config = new Config((fastify as any).mongo.db); + const privateAddress = await config.get(ConfigKey.CURRENT_PRIVATE_ADDRESS); + const privateKey = await privateKeyStore.retrieveIdentityKey(privateAddress!!); const authorizationSerialized = await generateAuthorization( privateGatewayPublicKeyDigest, - publicGatewayKeyPair.privateKey, + privateKey, ); return reply .header('Content-Type', CONTENT_TYPES.GATEWAY_REGISTRATION.AUTHORIZATION) diff --git a/src/services/poweb/registration.spec.ts b/src/services/poweb/registration.spec.ts index 97eeb4644..a72e7199d 100644 --- a/src/services/poweb/registration.spec.ts +++ b/src/services/poweb/registration.spec.ts @@ -4,7 +4,7 @@ import { PrivateNodeRegistration, PrivateNodeRegistrationAuthorization, PrivateNodeRegistrationRequest, - SubsequentSessionPrivateKeyData, + SessionPrivateKeyData, } from '@relaycorp/relaynet-core'; import bufferToArray from 'buffer-to-arraybuffer'; import { FastifyInstance } from 'fastify'; @@ -218,12 +218,11 @@ describe('Successful registration', () => { const registration = await PrivateNodeRegistration.deserialize( bufferToArray(response.rawPayload), ); - const keyData = fixtures.privateKeyStore.keys[registration.sessionKey!!.keyId.toString('hex')]; - expect(keyData).toBeTruthy(); - expect(keyData.type).toEqual('session-subsequent'); - expect((keyData as SubsequentSessionPrivateKeyData).peerPrivateAddress).toEqual( - await fixtures.privateGatewayCert.calculateSubjectPrivateAddress(), - ); + const keyData = + fixtures.privateKeyStore.sessionKeys[registration.sessionKey!!.keyId.toString('hex')]; + expect(keyData).toMatchObject>({ + peerPrivateAddress: await fixtures.privateGatewayCert.calculateSubjectPrivateAddress(), + }); }); async function completeRegistration(fixtures: FixtureSet): Promise { diff --git a/src/services/poweb/registration.ts b/src/services/poweb/registration.ts index 24d60f54d..7340b8e92 100644 --- a/src/services/poweb/registration.ts +++ b/src/services/poweb/registration.ts @@ -10,11 +10,12 @@ import { import bufferToArray from 'buffer-to-arraybuffer'; import { FastifyInstance, FastifyReply } from 'fastify'; import { initVaultKeyStore } from '../../backingServices/vault'; +import { MongoCertificateStore } from '../../keystores/MongoCertificateStore'; +import { Config, ConfigKey } from '../../utilities/config'; import { sha256 } from '../../utilities/crypto'; import { registerDisallowedMethods } from '../fastify'; import { CONTENT_TYPES } from './contentTypes'; -import RouteOptions from './RouteOptions'; const ENDPOINT_URL = '/v1/nodes'; @@ -27,10 +28,7 @@ const PRIVATE_GATEWAY_CERTIFICATE_START_OFFSET_HOURS = 3; const PRIVATE_GATEWAY_CERTIFICATE_VALIDITY_YEARS = 1; -export default async function registerRoutes( - fastify: FastifyInstance, - options: RouteOptions, -): Promise { +export default async function registerRoutes(fastify: FastifyInstance): Promise { registerDisallowedMethods(['POST'], ENDPOINT_URL, fastify); fastify.addContentTypeParser( @@ -61,13 +59,20 @@ export default async function registerRoutes( .send({ message: 'Payload is not a valid Private Node Registration Request' }); } - const publicGatewayKeyPair = await options.keyPairRetriever(); + const mongooseConnection = (fastify as any).mongo.db; + const config = new Config(mongooseConnection); + const privateAddress = await config.get(ConfigKey.CURRENT_PRIVATE_ADDRESS); + const privateKey = await privateKeyStore.retrieveIdentityKey(privateAddress!!); + + const certificateStore = new MongoCertificateStore(mongooseConnection); + const publicGatewayCertificate = await certificateStore.retrieveLatest(privateAddress!!); + const gatewayPublicKey = await publicGatewayCertificate!!.getPublicKey(); let registrationAuthorization: PrivateNodeRegistrationAuthorization; try { registrationAuthorization = await PrivateNodeRegistrationAuthorization.deserialize( registrationRequest.pnraSerialized, - await publicGatewayKeyPair.certificate.getPublicKey(), + gatewayPublicKey, ); } catch (err) { request.log.info({ err }, 'PNRR contains invalid authorization'); @@ -88,18 +93,18 @@ export default async function registerRoutes( const privateGatewayCertificate = await issuePrivateGatewayCertificate( registrationRequest.privateNodePublicKey, - publicGatewayKeyPair.privateKey, - publicGatewayKeyPair.certificate, + privateKey, + publicGatewayCertificate!!, ); const sessionKeyPair = await SessionKeyPair.generate(); - await privateKeyStore.saveSubsequentSessionKey( + await privateKeyStore.saveBoundSessionKey( sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, await privateGatewayCertificate.calculateSubjectPrivateAddress(), ); const registration = new PrivateNodeRegistration( privateGatewayCertificate, - publicGatewayKeyPair.certificate, + publicGatewayCertificate!!, sessionKeyPair.sessionKey, ); return reply diff --git a/src/services/poweb/server.spec.ts b/src/services/poweb/server.spec.ts index eb9754821..fe9d8cfc7 100644 --- a/src/services/poweb/server.spec.ts +++ b/src/services/poweb/server.spec.ts @@ -3,12 +3,11 @@ import pino from 'pino'; import { mockSpy } from '../../_test_utils'; import * as fastifyUtils from '../fastify'; import { setUpCommonFixtures } from './_test_utils'; -import RouteOptions from './RouteOptions'; import { makeServer } from './server'; jest.mock('../../utilities/exitHandling'); -const getFixtures = setUpCommonFixtures(); +setUpCommonFixtures(); const mockFastifyInstance = {}; const mockConfigureFastify = mockSpy( @@ -17,15 +16,6 @@ const mockConfigureFastify = mockSpy( ); describe('makeServer', () => { - test('Function to retrieve the key pair should be added to the options', async () => { - await makeServer(); - - const routeOptions = mockConfigureFastify.mock.calls[0][1] as RouteOptions; - const retriever = routeOptions.keyPairRetriever; - const retrieverCertificate = (await retriever()).certificate; - expect(retrieverCertificate.isEqual(getFixtures().publicGatewayCert)).toBeTrue(); - }); - test('No logger should be passed by default', async () => { await makeServer(); diff --git a/src/services/poweb/server.ts b/src/services/poweb/server.ts index b3f3e468a..07777a396 100644 --- a/src/services/poweb/server.ts +++ b/src/services/poweb/server.ts @@ -1,9 +1,6 @@ -import { UnboundKeyPair } from '@relaycorp/relaynet-core'; -import { get as getEnvVar } from 'env-var'; import { FastifyInstance, FastifyPluginCallback } from 'fastify'; import { Logger } from 'pino'; -import { initVaultKeyStore } from '../../backingServices/vault'; import { configureFastify } from '../fastify'; import healthcheck from './healthcheck'; import parcelCollection from './parcelCollection'; @@ -26,18 +23,5 @@ const ROUTES: ReadonlyArray> = [ * This function doesn't call .listen() so we can use .inject() for testing purposes. */ export async function makeServer(logger?: Logger): Promise { - return configureFastify( - ROUTES, - { - keyPairRetriever: makeKeyPairRetriever(), - }, - logger, - ); -} - -function makeKeyPairRetriever(): () => Promise { - const gatewayKeyIdBase64 = getEnvVar('GATEWAY_KEY_ID').required().asString(); - const gatewayKeyId = Buffer.from(gatewayKeyIdBase64, 'base64'); - const privateKeyStore = initVaultKeyStore(); - return () => privateKeyStore.fetchNodeKey(gatewayKeyId); + return configureFastify(ROUTES, {}, logger); } diff --git a/src/utilities/config.spec.ts b/src/utilities/config.spec.ts new file mode 100644 index 000000000..ceeecd3bc --- /dev/null +++ b/src/utilities/config.spec.ts @@ -0,0 +1,56 @@ +import { getModelForClass, ReturnModelType } from '@typegoose/typegoose'; + +import { setUpTestDBConnection } from '../_test_utils'; +import { ConfigItem } from '../models'; +import { Config, ConfigKey } from './config'; + +const getConnection = setUpTestDBConnection(); +let configItemModel: ReturnModelType; +beforeAll(async () => { + configItemModel = getModelForClass(ConfigItem, { existingConnection: getConnection() }); +}); + +const PRIVATE_ADDRESS = '0deafbeef'; + +describe('Config', () => { + describe('set', () => { + test('Item should be created if it does not already exist', async () => { + const config = new Config(getConnection()); + + await config.set(ConfigKey.CURRENT_PRIVATE_ADDRESS, PRIVATE_ADDRESS); + + await expect( + configItemModel.countDocuments({ + key: ConfigKey.CURRENT_PRIVATE_ADDRESS, + value: PRIVATE_ADDRESS, + }), + ).resolves.toEqual(1); + }); + + test('Item should be updated if it already exists', async () => { + const config = new Config(getConnection()); + await config.set(ConfigKey.CURRENT_PRIVATE_ADDRESS, PRIVATE_ADDRESS); + const newValue = `new ${PRIVATE_ADDRESS}`; + await config.set(ConfigKey.CURRENT_PRIVATE_ADDRESS, newValue); + + await expect( + configItemModel.countDocuments({ key: ConfigKey.CURRENT_PRIVATE_ADDRESS, value: newValue }), + ).resolves.toEqual(1); + }); + }); + + describe('get', () => { + test('Null should be returned for non-existing item', async () => { + const config = new Config(getConnection()); + + await expect(config.get(ConfigKey.CURRENT_PRIVATE_ADDRESS)).resolves.toBeNull(); + }); + + test('Value should be returned if item exists', async () => { + const config = new Config(getConnection()); + await config.set(ConfigKey.CURRENT_PRIVATE_ADDRESS, PRIVATE_ADDRESS); + + await expect(config.get(ConfigKey.CURRENT_PRIVATE_ADDRESS)).resolves.toEqual(PRIVATE_ADDRESS); + }); + }); +}); diff --git a/src/utilities/config.ts b/src/utilities/config.ts new file mode 100644 index 000000000..ec8f05a8b --- /dev/null +++ b/src/utilities/config.ts @@ -0,0 +1,26 @@ +import { getModelForClass, ReturnModelType } from '@typegoose/typegoose'; + +import { Connection } from 'mongoose'; +import { ConfigItem } from '../models'; + +export enum ConfigKey { + CURRENT_PRIVATE_ADDRESS = 'current_private_address', +} + +export class Config { + private readonly configItemModel: ReturnModelType; + + constructor(connection: Connection) { + this.configItemModel = getModelForClass(ConfigItem, { existingConnection: connection }); + } + + public async set(key: ConfigKey, value: string): Promise { + const record: ConfigItem = { key, value }; + await this.configItemModel.updateOne({ key }, record, { upsert: true }); + } + + public async get(key: ConfigKey): Promise { + const record = await this.configItemModel.findOne({ key }).exec(); + return record?.value ?? null; + } +}