diff --git a/Tokenization/backend/wrapper/package-lock.json b/Tokenization/backend/wrapper/package-lock.json index e04e08cea..83324580a 100644 --- a/Tokenization/backend/wrapper/package-lock.json +++ b/Tokenization/backend/wrapper/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", + "express": "^5.1.0", + "jose": "^6.1.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" }, @@ -1827,6 +1829,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2111,6 +2126,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2198,6 +2237,44 @@ "dev": true, "license": "MIT" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2391,6 +2468,28 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2398,6 +2497,24 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -2445,7 +2562,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2491,6 +2607,15 @@ "node": ">=0.10.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2533,6 +2658,26 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -2575,6 +2720,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2585,6 +2739,36 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2594,6 +2778,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2895,6 +3085,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2945,6 +3144,49 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3062,6 +3304,27 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -3097,6 +3360,24 @@ "dev": true, "license": "ISC" }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3108,7 +3389,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3133,6 +3413,30 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -3143,6 +3447,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3235,6 +3552,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3259,11 +3588,22 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3296,6 +3636,26 @@ "dev": true, "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3306,6 +3666,22 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3389,9 +3765,17 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3480,6 +3864,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4199,6 +4589,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4429,6 +4828,36 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4460,6 +4889,31 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4487,7 +4941,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mylas": { @@ -4511,6 +4964,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4555,11 +5017,34 @@ "dev": true, "license": "MIT" }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -4703,6 +5188,15 @@ "dev": true, "license": "MIT" }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4740,6 +5234,16 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4882,6 +5386,19 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4909,6 +5426,21 @@ ], "license": "MIT" }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-lit": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", @@ -4940,6 +5472,30 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5057,6 +5613,22 @@ "node": ">=0.10.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5081,6 +5653,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5091,6 +5669,49 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5114,6 +5735,78 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -5214,6 +5907,15 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -5365,6 +6067,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -5558,6 +6269,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -5601,6 +6326,15 @@ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -5663,6 +6397,15 @@ "node": ">=10.12.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -5720,7 +6463,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/Tokenization/backend/wrapper/package.json b/Tokenization/backend/wrapper/package.json index 000944e17..e41284638 100644 --- a/Tokenization/backend/wrapper/package.json +++ b/Tokenization/backend/wrapper/package.json @@ -26,6 +26,8 @@ "dependencies": { "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", + "express": "^5.1.0", + "jose": "^6.1.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" } diff --git a/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts index 20f5e2457..f120048ec 100644 --- a/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts +++ b/Tokenization/backend/wrapper/src/central/CentralSystemWrapper.ts @@ -15,6 +15,8 @@ import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; import { LogManager } from '@aliceo2/web-ui'; +import * as fs from 'fs'; +import type { CentralSystemConfig } from '../models/config.model'; import type { DuplexMessageModel } from '../models/message.model'; /** @@ -24,11 +26,15 @@ export class CentralSystemWrapper { // Config private _protoPath: string; - // Utilities - private _logger = LogManager.getLogger('CentralSystemWrapper'); - // Class properties private _server: grpc.Server; + private _port: number; + + // Certificates paths + private _serverCerts: CentralSystemConfig['serverCerts']; + + // Utilities + private _logger = LogManager.getLogger('CentralSystemWrapper'); // Clients management private _clients = new Map>(); @@ -38,8 +44,15 @@ export class CentralSystemWrapper { * Initializes the Wrapper for CentralSystem. * @param port The port number to bind the gRPC server to. */ - constructor(protoPath: string, private port: number) { - this._protoPath = protoPath; + constructor(config: CentralSystemConfig) { + if (!config.protoPath || !config.serverCerts?.caCertPath || !config.serverCerts?.certPath || !config.serverCerts?.keyPath) { + throw new Error('Invalid CentralSystemConfig provided'); + } + + this._protoPath = config.protoPath; + this._serverCerts = config.serverCerts; + this._port = config.port ?? 50051; + this._server = new grpc.Server(); this.setupService(); } @@ -115,7 +128,7 @@ export class CentralSystemWrapper { // Handle stream error event call.on('error', (err) => { - this._logger.infoMessage(`Stream error from client ${clientIp}:`, err); + this._logger.errorMessage(`Stream error from client ${clientIp}:`, err); this.cleanupClient(peer); }); } @@ -168,10 +181,27 @@ export class CentralSystemWrapper { * Starts the gRPC server and binds it to the specified in class port. */ public listen() { - const addr = `localhost:${this.port}`; - this._server.bindAsync(addr, grpc.ServerCredentials.createInsecure(), (err, _port) => { + const addr = `localhost:${this._port}`; + + // Create mTLS secure gRPC server + const caCert = fs.readFileSync(this._serverCerts.caCertPath); + const centralKey = fs.readFileSync(this._serverCerts.keyPath); + const centralCert = fs.readFileSync(this._serverCerts.certPath); + + const sslCreds = grpc.ServerCredentials.createSsl( + caCert, + [ + { + private_key: centralKey, + cert_chain: centralCert, + }, + ], + true + ); + + this._server.bindAsync(addr, sslCreds, (err, _port) => { if (err) { - this._logger.infoMessage('Server bind error:', err); + this._logger.errorMessage('Server bind error:', err); return; } this._logger.infoMessage(`CentralSytem started listening on ${addr}`); diff --git a/Tokenization/backend/wrapper/src/client/connection/Connection.ts b/Tokenization/backend/wrapper/src/client/connection/Connection.ts index 3f27c31b5..fbd63f3b1 100644 --- a/Tokenization/backend/wrapper/src/client/connection/Connection.ts +++ b/Tokenization/backend/wrapper/src/client/connection/Connection.ts @@ -12,52 +12,124 @@ * or submit itself to any jurisdiction. */ -import type { ConnectionDirection } from '../../models/message.model'; -import type { ConnectionHeaders, FetchOptions, FetchResponse } from '../../models/connection.model'; +import { ConnectionDirection } from '../../models/message.model'; +import type { ConnectionHeaders, FetchOptions, FetchResponse, TokenPayload } from '../../models/connection.model'; import { ConnectionStatus } from '../../models/connection.model'; import * as grpc from '@grpc/grpc-js'; +import { LogManager } from '@aliceo2/web-ui'; + +type ConnectionCerts = { + caCert: Buffer; + clientCert: Buffer; + clientKey: Buffer; +}; /** * @description This class represents a connection to a target client and manages sending messages to it. */ export class Connection { - private _token: string; - private _targetAddress: string; + private _jweToken: string; private _status: ConnectionStatus; - private _peerClient: any; + private _peerClient?: any; // A client grpc connection instance + + // Security management variables + private _clientSerialNumber?: string; // The certificate SN used to uniquely identify the peer. + private _lastActiveTimestamp: number; // Timestamp of the last successful request (for garbage collection). + private _authFailures: number; // Counter for consecutive authentication failures (for anti-DDoS/throttling). + private _cachedTokenPayload?: TokenPayload; // Cache of the successfully verified token payload. + + public targetAddress: string; public direction: ConnectionDirection; + // Utils + private _logger; + /** * Creates a new Connection instance with the given token, target address, and connection direction. * - * @param token - The authentication token for the connection. + * @param jweToken - The encrypted JWE token for the connection. * @param targetAddress - The unique address of the target client. * @param direction - The direction of the connection (e.g., sending or receiving). + * @param clientSN - Optional serial number of the peer's certificate (used for lookups). */ - constructor(token: string, targetAddress: string, direction: ConnectionDirection, peerCtor: any) { - this._token = token; - this._targetAddress = targetAddress; - this._peerClient = new peerCtor(targetAddress, grpc.credentials.createInsecure()); + constructor(jweToken: string, targetAddress: string, direction: ConnectionDirection, clientSN?: string) { + this._jweToken = jweToken; + this.targetAddress = targetAddress; this.direction = direction; + // Initialize state fields + this._clientSerialNumber = clientSN; + this._lastActiveTimestamp = Date.now(); + this._authFailures = 0; this._status = ConnectionStatus.CONNECTED; + + this._logger = LogManager.getLogger(`Connection ${targetAddress}`); + } + + /** + * Creates the mTLS gRPC client and attaches it to the connection. + * This method is REQUIRED ONLY for outbound (SENDING) connections. + * * @param peerCtor - The constructor for the gRPC client to be used for communication. + * @param connectionCerts - Required sending client certificates for mTLS. + */ + public createSslTunnel(peerCtor: any, connectionCerts: ConnectionCerts): void { + if (this.direction !== ConnectionDirection.SENDING) { + this._logger.warnMessage('Attempted to create SSL tunnel on a RECEIVING connection. This is usually unnecessary.'); + } + + if (!connectionCerts.caCert || !connectionCerts.clientCert || !connectionCerts.clientKey) { + throw new Error('Connection certificates are required to create an mTLS tunnel.'); + } + + // Create grpc credentials + const sslCreds = grpc.credentials.createSsl(connectionCerts.caCert, connectionCerts.clientKey, connectionCerts.clientCert); + + this._peerClient = new peerCtor(this.targetAddress, sslCreds); + this.status = ConnectionStatus.CONNECTED; } /** * Replace newly generated token - * @param token New token to be replaced + * @param jweToken New token to be replaced */ - public set token(token: string) { - this._token = token; + public set token(jweToken: string) { + this._jweToken = jweToken; } /** - * Revokes the token of the connection, effectively invalidating it. - * The connection status is set to UNAUTHORIZED. + * Revoke current token and set status of unauthorized connection */ public handleRevokeToken(): void { - this._token = ''; - this._status = ConnectionStatus.UNAUTHORIZED; + this._jweToken = ''; + this.status = ConnectionStatus.UNAUTHORIZED; + } + + /** + * Handles a successful authentication event. Updates the active timestamp, + * resets the failure counter, and caches the new token payload. + * This is crucial for high-performance applications to avoid re-validating the same token. + * @param payload The decoded and verified token payload. + */ + public handleSuccessfulAuth(payload: TokenPayload): void { + this._lastActiveTimestamp = Date.now(); + this._authFailures = 0; + this._cachedTokenPayload = payload; + this.status = ConnectionStatus.CONNECTED; + } + + /** + * Handles an authentication failure. Increments the failure counter. + * If the failure count exceeds a local threshold, the connection is locally marked as BLOCKED. + * @returns The new count of consecutive failures. + */ + public handleFailedAuth(): number { + this._authFailures += 1; + + // Local throttling mechanism + if (this._authFailures >= 5) { + this.status = ConnectionStatus.BLOCKED; + } + return this._authFailures; } /** @@ -65,7 +137,7 @@ export class Connection { * @returns Connection token */ public get token(): string { - return this._token; + return this._jweToken; } /** @@ -77,19 +149,51 @@ export class Connection { } /** - * Sets the status of this connection. - * @param status The new status of the connection. + * Updates the status of the connection. + * @param status New status */ public set status(status: ConnectionStatus) { this._status = status; } /** - * Returns target address for this Connection object - * @returns Target address + * Returns the client's Serial Number (SN). + * @returns The client's serial number or undefined. + */ + public get serialNumber(): string | undefined { + return this._clientSerialNumber; + } + + /** + * Sets the client's Serial Number. Primarily used for RECEIVING connections + * where the SN is extracted during the first mTLS handshake in the interceptor. + * @param serialNumber The serial number string. + */ + public set serialNumber(serialNumber: string | undefined) { + this._clientSerialNumber = serialNumber; + } + + /** + * Returns the timestamp of the last successful interaction. + * @returns UNIX timestamp in milliseconds. + */ + public get lastActiveTimestamp(): number { + return this._lastActiveTimestamp; + } + + /** + * Returns the cached token payload. + * @returns The cached payload or undefined. */ - public get targetAddress(): string { - return this._targetAddress; + public get cachedTokenPayload(): TokenPayload | undefined { + return this._cachedTokenPayload; + } + + /** + * Attaches gRPC client to that connection + */ + public attachGrpcClient(client: any): void { + this._peerClient = client; } /** @@ -106,6 +210,11 @@ export class Connection { const path = options.path ?? '/'; const headers: ConnectionHeaders = { ...(options.headers ?? {}) }; + // Set mandatory grpc metadata + const metadata = new grpc.Metadata(); + metadata.set('jwetoken', this._jweToken); + + // Build body buffer let bodyBuf: Buffer = Buffer.alloc(0); const b = options.body; if (b != null) { @@ -119,7 +228,7 @@ export class Connection { // Return promise with response return new Promise((resolve, reject) => { - this._peerClient.Fetch(req, (err: any, resp: any) => { + this._peerClient.Fetch(req, metadata, (err: any, resp: any) => { if (err) return reject(err); const resBody = resp?.body ? Buffer.from(resp.body) : Buffer.alloc(0); diff --git a/Tokenization/backend/wrapper/src/client/connectionManager/ConnectionManager.ts b/Tokenization/backend/wrapper/src/client/connectionManager/ConnectionManager.ts index fdbd20263..3abec22a9 100644 --- a/Tokenization/backend/wrapper/src/client/connectionManager/ConnectionManager.ts +++ b/Tokenization/backend/wrapper/src/client/connectionManager/ConnectionManager.ts @@ -22,8 +22,12 @@ import type { Command, CommandHandler } from 'models/commands.model'; import type { DuplexMessageEvent } from '../../models/message.model'; import { ConnectionDirection } from '../../models/message.model'; import { ConnectionStatus } from '../../models/connection.model'; +import type { SecurityContext } from '../../utils/security/SecurityContext'; import { peerListener } from '../../utils/connection/peerListener'; +/** + * @description Manages all the connection between clients and central system. + */ /** * Manages the lifecycle and connection logic for a gRPC client communicating with the central system. * @@ -40,14 +44,16 @@ import { peerListener } from '../../utils/connection/peerListener'; */ export class ConnectionManager { private _logger = LogManager.getLogger('ConnectionManager'); + private _wrapper: any; // GRPC wrapper file + private _centralDispatcher: CentralCommandDispatcher; private _centralConnection: CentralConnection; private _sendingConnections = new Map(); + private _receivingConnections = new Map(); - private _wrapper: any; - private _peerCtor: any; - private _peerServer: grpc.Server | undefined; - private _baseAPIPath: string = ''; + private _peerCtor: any; // P2P gRPC constructor + private _peerServer?: grpc.Server; + private _baseAPIPath: string = 'localhost:40041/api/'; /** * Initializes a new instance of the ConnectionManager class. @@ -56,8 +62,9 @@ export class ConnectionManager { * * @param protoPath - The file path to the gRPC proto definition. * @param centralAddress - The address of the central gRPC server (default: "localhost:50051"). + * @param securityContext - The security context containing certificates and keys for secure communication. */ - constructor(protoPath: string, centralAddress: string = 'localhost:50051') { + constructor(protoPath: string, centralAddress: string = 'localhost:50051', private readonly securityContext: SecurityContext) { const packageDef = protoLoader.loadSync(protoPath, { keepCase: true, longs: String, @@ -70,11 +77,17 @@ export class ConnectionManager { this._wrapper = proto.webui.tokenization; this._peerCtor = this._wrapper.Peer2Peer; - const client = new this._wrapper.CentralSystem(centralAddress, grpc.credentials.createInsecure()); + // Create grpc credentials + const sslCreds = grpc.credentials.createSsl( + this.securityContext.caCert, + this.securityContext.clientPrivateKey, + this.securityContext.clientSenderCert + ); + const centralClient = new this._wrapper.CentralSystem(centralAddress, sslCreds); // Event dispatcher for central system events this._centralDispatcher = new CentralCommandDispatcher(); - this._centralConnection = new CentralConnection(client, this._centralDispatcher, centralAddress); + this._centralConnection = new CentralConnection(centralClient, this._centralDispatcher, centralAddress); } /** @@ -110,14 +123,35 @@ export class ConnectionManager { * Creates new connection * @param address Target (external) address of the connection * @param direction Direction of connection - * @param token Optional token for connection + * @param jweToken Optional encrypted JWE token for connection */ - createNewConnection(address: string, direction: ConnectionDirection, token?: string) { - const conn = new Connection(token ?? '', address, direction, this._peerCtor); + public async createNewConnection(address: string, direction: ConnectionDirection, jweToken?: string) { + let conn: Connection | undefined; + + // Checks if connection already exists + conn = direction === ConnectionDirection.RECEIVING ? this._receivingConnections.get(address) : this._sendingConnections.get(address); + + // Return existing connection if found + if (conn) { + if (jweToken) { + conn.token = jweToken; + } + return conn; + } + + // Create new connection + conn = new Connection(jweToken ?? '', address, direction); + conn.status = ConnectionStatus.CONNECTING; if (direction === ConnectionDirection.RECEIVING) { this._receivingConnections.set(address, conn); } else { + // Open tunnel only on sending connections + conn.createSslTunnel(this._peerCtor, { + caCert: this.securityContext.caCert, + clientCert: this.securityContext.clientSenderCert, + clientKey: this.securityContext.clientPrivateKey, + }); this._sendingConnections.set(address, conn); } conn.status = ConnectionStatus.CONNECTED; @@ -143,9 +177,29 @@ export class ConnectionManager { } /** - * Returns all saved connections. - * - * @returns An object containing the sending and receiving connections. + * Searches through all receiving and sending connections to find a connection by its client Serial Number (SN). + * @param serialNumber The unique serial number of the peer's certificate. + * @returns The matching Connection object or undefined. + */ + getConnectionBySerialNumber(serialNumber: string): Connection | undefined { + // Check receiving connections first + for (const conn of this._receivingConnections.values()) { + if (conn.serialNumber === serialNumber) { + return conn; + } + } + // Check sending connections + for (const conn of this._sendingConnections.values()) { + if (conn.serialNumber === serialNumber) { + return conn; + } + } + return undefined; + } + + /** + * Returns object with all connections + * @returns Object of all connections */ public get connections(): { sending: Connection[]; @@ -161,6 +215,11 @@ export class ConnectionManager { public async listenForPeers(port: number, baseAPIPath?: string): Promise { if (baseAPIPath) this._baseAPIPath = baseAPIPath; + if (!this.securityContext.clientListenerCert) { + this._logger.errorMessage('Listener certificate not provided in gRPCWrapper. Cannot start peer listener.'); + return; + } + if (this._peerServer) { this._peerServer.forceShutdown(); this._peerServer = undefined; @@ -169,11 +228,30 @@ export class ConnectionManager { this._peerServer = new grpc.Server(); this._peerServer.addService(this._wrapper.Peer2Peer.service, { Fetch: async (call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => - peerListener(call, callback, this._logger, this._receivingConnections, this._peerCtor, this._baseAPIPath), + peerListener( + call, + callback, + this._logger, + this._receivingConnections, + this.createNewConnection.bind(this), + this.securityContext, + this._baseAPIPath + ), }); + const sslCreds = grpc.ServerCredentials.createSsl( + this.securityContext.caCert, + [ + { + private_key: this.securityContext.clientPrivateKey, + cert_chain: this.securityContext.clientListenerCert, + }, + ], + true + ); + await new Promise((resolve, reject) => { - this._peerServer?.bindAsync(`localhost:${port}`, grpc.ServerCredentials.createInsecure(), (err) => (err ? reject(err) : resolve())); + this._peerServer?.bindAsync(`localhost:${port}`, sslCreds, (err) => (err ? reject(err) : resolve())); }); this._logger.infoMessage(`Peer server listening on localhost:${port}`); diff --git a/Tokenization/backend/wrapper/src/client/connectionManager/interceptors/grpc.auth.interceptor.ts b/Tokenization/backend/wrapper/src/client/connectionManager/interceptors/grpc.auth.interceptor.ts new file mode 100644 index 000000000..c3a353eb2 --- /dev/null +++ b/Tokenization/backend/wrapper/src/client/connectionManager/interceptors/grpc.auth.interceptor.ts @@ -0,0 +1,289 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import * as grpc from '@grpc/grpc-js'; +import { Connection } from '../../connection/Connection'; +import { importPKCS8, importJWK, compactDecrypt, compactVerify } from 'jose'; +import type { TokenPayload } from '../../../models/connection.model'; +import { ConnectionStatus } from '../../../models/connection.model'; +import { ConnectionDirection } from '../../../models/message.model'; +import type { SecurityContext } from '../../../utils/security/SecurityContext'; + +/** + * Interceptor function responsible for JWE decryption, JWS verification, + * certificate serial number matching (mTLS binding), and basic authorization. + */ +export const gRPCAuthInterceptor = async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + clientConnections: Map, + securityContext: SecurityContext +): Promise<{ isAuthenticated: boolean; conn: Connection | null }> => { + const metadata = call.metadata.getMap(); + const jweToken = metadata.jwetoken as string; + const clientAddress = call.getPeer(); + let conn = clientConnections.get(clientAddress); + const peerCert = getPeerCertFromCall(call); + + // Check if token exists + if (!jweToken) { + const error = { + name: 'AuthenticationError', + message: 'No token provided', + code: grpc.status.UNAUTHENTICATED, + }; + callback(error, null); + return { isAuthenticated: false, conn: null }; + } + + // Check if connection exists + if (conn) { + // Check if connection is blocked + if (conn.status === ConnectionStatus.BLOCKED) { + const error = { + name: 'AuthenticationError', + message: 'Connection is blocked. Contact administrator.', + code: grpc.status.UNAUTHENTICATED, + }; + callback(error, null); + return { isAuthenticated: false, conn }; + } + + if (conn.token === jweToken) { + // Check for allowed requests and serial number match if token is the same + if (!isRequestAllowed(conn.cachedTokenPayload, call.request, callback)) { + return { isAuthenticated: false, conn }; + } + + if (!isSerialNumberMatching(conn.cachedTokenPayload, peerCert, callback)) { + conn.handleFailedAuth(); + return { isAuthenticated: false, conn }; + } + + return { isAuthenticated: true, conn }; + } + } else { + conn = new Connection(jweToken, clientAddress, ConnectionDirection.RECEIVING); + clientConnections.set(clientAddress, conn); + } + + // New connection - need to authenticate + // JWE decryption (RSA-OAEP-256) -> JWS (Plaintext) + let privateKey: any; + let jwsToken: string; + try { + // Importing RSA private key for decryption + privateKey = await importPKCS8(securityContext.clientPrivateKey.toString('utf-8'), 'RSA-OAEP-256'); + + const { plaintext } = await compactDecrypt(jweToken, privateKey); + jwsToken = new TextDecoder().decode(plaintext).trim(); + } catch (e) { + void e; + const error = { + name: 'AuthenticationError', + message: 'Incorrect token provided (JWE Decryption failed)', + code: grpc.status.UNAUTHENTICATED, + }; + + // TODO: Consider logging or informing a central security system about potential attack/misconfiguration. + callback(error, null); + conn.handleFailedAuth(); + return { isAuthenticated: false, conn }; + } + + // Verify JWS (With signature) and payload extraction + let pub: any; + let payload: TokenPayload; + + try { + // Convert a raw Base64 Ed25519 public key to JWK format + const jwk = { + kty: 'OKP', + crv: 'Ed25519', + x: Buffer.from(securityContext.JWS_PUBLIC_KEY, 'base64').toString('base64url'), + }; + + // Importing the Ed25519 public key for verification - using "EdDSA" algorithm + pub = await importJWK(jwk, 'EdDSA'); + + // Compact verify - verify with key and decode the JWS token in one step + const { payload: jwtPayload, protectedHeader } = await compactVerify(jwsToken, pub); + + // Additional check to ensure correct signing algorithm was used + if (protectedHeader.alg !== 'EdDSA' && protectedHeader.alg !== 'Ed25519') { + const error = { + name: 'AuthenticationError', + message: 'Incorrect signing algorithm for JWS.', + code: grpc.status.UNAUTHENTICATED, + }; + + callback(error, null); + return { isAuthenticated: false, conn }; + } + + // Decode and parse the JWT payload + const payloadString = new TextDecoder().decode(jwtPayload); + payload = JSON.parse(payloadString); + } catch { + const error = { + name: 'AuthenticationError', + message: `JWS Verification error: Invalid signature`, + code: grpc.status.PERMISSION_DENIED, + }; + // TODO: Consider logging or informing a central security system about failed verification. + callback(error, null); + + conn.handleFailedAuth(); + return { isAuthenticated: false, conn }; + } + + // Binding mTLS check and authorization + // Connection tunnel verification with serialNumber (mTLS SN vs Token SN) + if (!isSerialNumberMatching(payload, peerCert, callback)) { + conn.handleFailedAuth(); + return { isAuthenticated: false, conn }; + } + + // Validate permission for request method (Authorization check) + if (!isRequestAllowed(payload, call.request, callback)) { + return { isAuthenticated: false, conn }; + } + + // Authentication and Authorization successful + // Update Connection state with SN and status + conn.handleSuccessfulAuth(payload); + return { isAuthenticated: true, conn }; +}; + +/** + * Checks if the request method is allowed based on the token permissions. + * @param tokenPayload payload extracted from the token + * @param request gRPC request object containing method information + * @param callback callback to return gRPC error if needed + * @returns true if request method is allowed, false otherwise + */ +export const isRequestAllowed = (tokenPayload: TokenPayload | undefined, request: any, callback: grpc.sendUnaryData): boolean => { + const method = String(request?.method ?? 'POST').toUpperCase(); + const isValidPayload = validateTokenPayload(tokenPayload, method); + let isExpired = false; + + if (isValidPayload) { + isExpired = isPermissionExpired(tokenPayload.iat[method], tokenPayload.exp[method]); + } + + if (!isValidPayload || isExpired) { + const error = { + name: 'AuthorizationError', + code: isExpired ? grpc.status.UNAUTHENTICATED : grpc.status.PERMISSION_DENIED, + message: isExpired + ? `Request of type ${method}, permission has expired.` + : `Request of type ${method} is not allowed by the token policy.`, + } as any; + + callback(error, null); + return false; + } + + return true; +}; + +/** + * Validates the structure and types of the token payload. + * @returns true if token payload is valid, false otherwise + */ +const validateTokenPayload = (tokenPayload: TokenPayload | undefined, method: string): tokenPayload is TokenPayload => { + if (!tokenPayload) { + return false; + } + + if ( + typeof tokenPayload.iat !== 'object' || + typeof tokenPayload.exp !== 'object' || + typeof tokenPayload.sub !== 'string' || + typeof tokenPayload.aud !== 'string' || + typeof tokenPayload.iss !== 'string' || + typeof tokenPayload.jti !== 'string' || + Object.keys(tokenPayload.iat).length === 0 || + Object.keys(tokenPayload.exp).length === 0 || + !Object.prototype.hasOwnProperty.call(tokenPayload.iat, method) || + !Object.prototype.hasOwnProperty.call(tokenPayload.exp, method) + ) { + return false; + } + + return true; +}; + +/** + * Checks if the permissions granted in the token have expired. + * @param iat issued-at timestamp for the specific method + * @param exp expiration timestamp for the specific method + * @returns true if permission is expired + */ +export const isPermissionExpired = (iat: number, exp: number): boolean => { + const nowInSeconds = Math.floor(Date.now() / 1000); + + if (nowInSeconds >= exp) { + return true; + } + + if (iat > nowInSeconds) { + return true; + } + + return false; +}; + +/** + * Checks if the serial number from the peer certificate matches the one in the token payload. + * @param tokenPayload payload extracted from the token + * @param peerCert certificate object retrieved from the gRPC call + * @param callback callback to return gRPC error if needed + * @returns true if serial numbers match, false otherwise + */ +export const isSerialNumberMatching = (tokenPayload: TokenPayload | undefined, peerCert: any, callback: grpc.sendUnaryData): boolean => { + const clientSN = normalizeSerial(peerCert?.serialNumber); + const tokenSN = normalizeSerial(tokenPayload?.sub); + + if (!clientSN || clientSN !== tokenSN) { + const error = { + name: 'AuthenticationError', + code: grpc.status.PERMISSION_DENIED, + message: 'Serial number mismatch (mTLS binding failure).', + } as any; + callback(error, null); + return false; + } + return true; +}; + +/** + * Normalizes a certificate serial number by removing colons and converting to uppercase. + * @param sn serial number string possibly containing colons or being null/undefined + * @returns normalized serial number string + */ +const normalizeSerial = (sn?: string | null): string => + // Node retrieves serial number as hex string, without leading 0x and with possible colons so we need to normalize it + (sn ?? '').replace(/[^0-9a-f]/gi, '').toUpperCase(); + +/** + * Retrieves the peer certificate from the gRPC call object. + * @param call gRPC call object + * @returns peer certificate object from the gRPC call + */ +export const getPeerCertFromCall = (call: any) => { + const session = call?.call?.stream?.session; + const sock = session?.socket as any; + return sock?.getPeerCertificate(true); // Whole certificate info from TLS socket +}; diff --git a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts index 0622afe48..db6282625 100644 --- a/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts +++ b/Tokenization/backend/wrapper/src/client/gRPCWrapper.ts @@ -15,11 +15,15 @@ import { ConnectionManager } from './connectionManager/ConnectionManager'; import { RevokeTokenHandler } from './commands/revokeToken/revokeToken.handler'; import { ConnectionDirection, DuplexMessageEvent } from '../models/message.model'; -import { NewTokenHandler } from './commands/newToken/newToken.handler'; import type { Connection } from './connection/Connection'; +import { NewTokenHandler } from './commands/newToken/newToken.handler'; +import type { gRPCWrapperConfig } from '../models/config.model'; +import { SecurityContext } from '../utils/security/SecurityContext'; +import * as fs from 'fs'; +import { LogManager } from '@aliceo2/web-ui'; /** - * @description Wrapper class for managing secure gRPC wrapper. + * Wrapper class for managing secure gRPC wrapper. * * @remarks * This class serves as a high-level abstraction over the underlying @@ -29,20 +33,51 @@ import type { Connection } from './connection/Connection'; * @example * ```typescript * const grpcWrapper = new gRPCWrapper(PROTO_PATH, CENTRAL_SYSTEM_ADDRESS); - * // Use grpcWrapper to interact with gRPC services + * Use grpcWrapper to interact with gRPC services * ``` */ export class gRPCWrapper { private _connectionManager: ConnectionManager; + private _listenerKey?: NonSharedBuffer; + private _listenerCert?: NonSharedBuffer; + private _logger = LogManager.getLogger('gRPCWrapper'); + private _securityContext: SecurityContext; /** * Initializes an instance of gRPCWrapper class. * + * @param config - External configuration object containing necessary paths and addresses. * @param protoPath - The file path to the gRPC proto definition. * @param centralAddress - The address of the central gRPC server (default: "localhost:4100"). */ - constructor(protoPath: string, centralAddress: string = 'localhost:4100') { - this._connectionManager = new ConnectionManager(protoPath, centralAddress); + constructor(config: gRPCWrapperConfig) { + if ( + !config.protoPath || + !config.centralAddress || + !config.clientCerts?.caCertPath || + !config.clientCerts?.certPath || + !config.clientCerts?.publicKeyPath || + !config.clientCerts?.privateKeyPath + ) { + throw new Error('Invalid gRPCWrapper configuration provided. Missing required paths.'); + } + + let clientListenerCert: Buffer = Buffer.alloc(0); + + // Sender keys and certs are mandatory + const caCert = fs.readFileSync(config.clientCerts.caCertPath); + const clientSenderCert = fs.readFileSync(config.clientCerts.certPath); + const clientPublicKey = fs.readFileSync(config.clientCerts.publicKeyPath); + const clientPrivateKey = fs.readFileSync(config.clientCerts.privateKeyPath); + + if (config.listenerCertPath) { + // If we have dedicated listener cert, use it + clientListenerCert = fs.readFileSync(config.listenerCertPath); + } + + this._securityContext = new SecurityContext(caCert, clientSenderCert, clientPrivateKey, clientPublicKey, clientListenerCert); + + this._connectionManager = new ConnectionManager(config.protoPath, config.centralAddress, this._securityContext); this._connectionManager.registerCommandHandlers([ { event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, diff --git a/Tokenization/backend/wrapper/src/models/config.model.ts b/Tokenization/backend/wrapper/src/models/config.model.ts new file mode 100644 index 000000000..3167065d1 --- /dev/null +++ b/Tokenization/backend/wrapper/src/models/config.model.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +export interface CentralSystemConfig { + /** Path to the proto file defining the services. */ + protoPath: string; + /** Host/IP to bind the gRPC server on. Defaults to "0.0.0.0" which is docker-friendly. */ + host?: string; + /** Port to bind. Defaults to 50051. */ + port?: number; + + /** Central TLS certificates paths. */ + serverCerts: { + caCertPath: string; + certPath: string; + keyPath: string; + }; +} + +export interface gRPCWrapperConfig { + /** Path to the proto file defining the services. */ + protoPath: string; + /** Address of the CentralSystem server. */ + centralAddress: string; + + /** Client TLS certificates paths. */ + clientCerts: { + caCertPath: string; + publicKeyPath: string; + privateKeyPath: string; + certPath: string; + }; + + /** Optional listener TLS certificate path. If provided, the gRPCWrapper will be able to accept incoming connections. */ + listenerCertPath?: string; +} diff --git a/Tokenization/backend/wrapper/src/models/connection.model.ts b/Tokenization/backend/wrapper/src/models/connection.model.ts index eb9cb07c5..d44affb24 100644 --- a/Tokenization/backend/wrapper/src/models/connection.model.ts +++ b/Tokenization/backend/wrapper/src/models/connection.model.ts @@ -43,10 +43,13 @@ export enum ConnectionStatus { RECONNECTING = 'RECONNECTING', // The connection is refreshing its authentication token TOKEN_REFRESH = 'TOKEN_REFRESH', + // The connection has been blocked + BLOCKED = 'BLOCKED', } export type ConnectionHeaders = Record; +// Options for making fetch-like requests over a connection export type FetchOptions = { method?: string; path?: string; @@ -54,6 +57,7 @@ export type FetchOptions = { body?: string | Buffer | Uint8Array | null; }; +// A more specific type for fetch responses, including status, headers, and body export type FetchResponse = { status: number; headers: ConnectionHeaders; @@ -76,3 +80,21 @@ export type HttpLikeResponse = { headers: Headers; body: Buffer; }; + +/** + * @description Payload structure for authentication tokens + * @sub {string} sub - Subject: Client's certificate serial number + * @aud {string} aud - Audience: Listener's certificate serial number + * @iss {string} iss - Issuer: Central system's certificate serial number + * @iat {Object} iat - Issued At: Permissions granted to the client (e.g., allowed HTTP methods with timestamps) + * @exp {number} exp - Expiration: Expiry timestamps for the granted permissions + * @jti {string} jti - JWT ID: Unique identifier for the token + */ +export type TokenPayload = { + sub: string; + aud: string; + iss: string; + iat: { [method: string]: number }; + exp: { [method: string]: number }; + jti: string; +}; diff --git a/Tokenization/backend/wrapper/src/models/message.model.ts b/Tokenization/backend/wrapper/src/models/message.model.ts index a1819ceb1..947fa8ce5 100644 --- a/Tokenization/backend/wrapper/src/models/message.model.ts +++ b/Tokenization/backend/wrapper/src/models/message.model.ts @@ -35,7 +35,6 @@ export enum DuplexMessageEvent { * @property DUPLEX: Indicates a connection that can both send and receive messages. */ export enum ConnectionDirection { - UNSPECIFIED = 'UNSPECIFIED', SENDING = 'SENDING', RECEIVING = 'RECEIVING', DUPLEX = 'DUPLEX', @@ -70,6 +69,5 @@ export interface TokenMessage { */ export interface DuplexMessageModel { event: DuplexMessageEvent; - newToken?: TokenMessage; - revokeToken?: TokenMessage; + payload: TokenMessage; } diff --git a/Tokenization/backend/wrapper/src/test/central/CentralSystemWrapper.test.ts b/Tokenization/backend/wrapper/src/test/central/CentralSystemWrapper.test.ts index 4c7476cbb..9b5be6277 100644 --- a/Tokenization/backend/wrapper/src/test/central/CentralSystemWrapper.test.ts +++ b/Tokenization/backend/wrapper/src/test/central/CentralSystemWrapper.test.ts @@ -21,6 +21,7 @@ const mockServerInstance = { const logger = { infoMessage: jest.fn(), + errorMessage: jest.fn(), }; jest.mock( @@ -45,7 +46,7 @@ jest.mock('@grpc/grpc-js', () => { ...original, Server: jest.fn(() => mockServerInstance), ServerCredentials: { - createInsecure: jest.fn(() => 'mock-credentials'), + createSsl: jest.fn(() => 'mock-credentials'), }, loadPackageDefinition: jest.fn(() => ({ webui: { @@ -61,21 +62,26 @@ jest.mock('@grpc/grpc-js', () => { import { CentralSystemWrapper } from '../../central/CentralSystemWrapper'; import * as grpc from '@grpc/grpc-js'; +import { getTestCentralCertPaths } from '../testCerts/testCerts'; describe('CentralSystemWrapper', () => { let wrapper: CentralSystemWrapper; + const testCentralCertPaths = getTestCentralCertPaths(); beforeEach(() => { jest.clearAllMocks(); - wrapper = new CentralSystemWrapper('dummy.proto', 12345); + wrapper = new CentralSystemWrapper({ + protoPath: 'dummy.proto', + port: 12345, + serverCerts: testCentralCertPaths, + }); }); test('should set up gRPC service and add it to the server', () => { - const testWrapper = new CentralSystemWrapper('dummy.proto', 12345); expect(grpc.Server).toHaveBeenCalled(); expect(grpc.loadPackageDefinition).toHaveBeenCalled(); - expect(grpc.ServerCredentials.createInsecure).not.toHaveBeenCalled(); - expect(testWrapper).toBeDefined(); + expect(grpc.ServerCredentials.createSsl).not.toHaveBeenCalled(); + expect(wrapper).toBeDefined(); }); test('should call listen and bind the server', () => { @@ -92,7 +98,7 @@ describe('CentralSystemWrapper', () => { wrapper.listen(); - expect(logger.infoMessage).toHaveBeenCalledWith('Server bind error:', error); + expect(logger.errorMessage).toHaveBeenCalledWith('Server bind error:', error); }); test('should handle client stream events', () => { @@ -118,6 +124,6 @@ describe('CentralSystemWrapper', () => { expect(logger.infoMessage).toHaveBeenCalledWith(expect.stringContaining('Client client123')); expect(logger.infoMessage).toHaveBeenCalledWith('Client client123 ended stream.'); - expect(logger.infoMessage).toHaveBeenCalledWith('Stream error from client client123:', expect.any(Error)); + expect(logger.errorMessage).toHaveBeenCalledWith('Stream error from client client123:', expect.any(Error)); }); }); diff --git a/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts new file mode 100644 index 000000000..70d2b86f7 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/client/ConnectionManager/Interceptors/grpc.auth.interceptor.test.ts @@ -0,0 +1,634 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import * as grpc from '@grpc/grpc-js'; +import * as jose from 'jose'; +import * as interceptor from '../../../../client/connectionManager/interceptors/grpc.auth.interceptor'; + +// Connection class mock +jest.mock( + '../../../../client/connection/Connection', + () => { + return { + Connection: jest.fn().mockImplementation((jweToken: string, address: string, direction: any) => { + return { + jweToken, + address, + direction, + status: 1, + payload: { subSerialNumber: 'AABBCC', perm: { POST: true } }, + getStatus: jest.fn(function () { + return this.status; + }), + getToken: jest.fn(function () { + return this.jweToken; + }), + getCachedTokenPayload: jest.fn(function () { + return this.payload; + }), + handleFailedAuth: jest.fn(), + handleSuccessfulAuth: jest.fn(function (p: any) { + this.payload = p; + this.status = 1; + }), + }; + }), + }; + }, + { virtual: true } +); + +import { Connection } from '../../../../client/connection/Connection'; + +jest.mock('jose', () => ({ + importPKCS8: jest.fn(), + importJWK: jest.fn(), + compactDecrypt: jest.fn(), + compactVerify: jest.fn(), +})); + +import { ConnectionStatus, TokenPayload } from '../../../../models/connection.model'; +import { SecurityContext } from '../../../../utils/security/SecurityContext'; +import { ConnectionDirection } from '../../../../models/message.model'; + +const mockSecurityContext = { + clientPrivateKey: Buffer.from('mock_private_key_rsa'), + JWS_PUBLIC_KEY: 'mock_public_key_ed25519', +} as unknown as SecurityContext; + +let isRequestAllowedSpy: jest.SpyInstance; +let isSerialNumberMatchingSpy: jest.SpyInstance; +let getPeerCertFromCallSpy: jest.SpyInstance; + +const mockCall = { + metadata: { getMap: jest.fn(() => ({})) }, + getPeer: jest.fn(() => 'ipv4:127.0.0.1:12345'), + request: { method: 'POST' }, +} as unknown as grpc.ServerUnaryCall; + +const mockCallback = jest.fn(); +const mockClientConnections = new Map(); + +describe('gRPCAuthInterceptor', () => { + const MOCK_ADDRESS = 'ipv4:127.0.0.1:12345'; + const VALID_JWE = 'valid.jwe.token'; + const VALID_JWS = 'valid.jws.token'; + const DECRYPTED_PAYLOAD: TokenPayload = { + subSerialNumber: 'DDEEFF', + perm: { POST: true, GET: false }, + } as any; + + beforeEach(() => { + jest.clearAllMocks(); + mockClientConnections.clear(); + + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({ + jwetoken: VALID_JWE, + }); + (mockCall.getPeer as unknown as jest.Mock).mockReturnValue(MOCK_ADDRESS); + (mockCall as any).request = { method: 'POST' }; + + (jose.importPKCS8 as jest.Mock).mockResolvedValue('mock_priv_key'); + (jose.compactDecrypt as jest.Mock).mockResolvedValue({ + plaintext: Buffer.from(VALID_JWS), + }); + (jose.importJWK as jest.Mock).mockResolvedValue('mock_pub_key'); + (jose.compactVerify as jest.Mock).mockResolvedValue({ + payload: Buffer.from(JSON.stringify(DECRYPTED_PAYLOAD)), + protectedHeader: { alg: 'EdDSA' }, + }); + + // mocks of internal functions + isRequestAllowedSpy = jest.spyOn(interceptor, 'isRequestAllowed').mockImplementation((_p, _r, _cb) => true); + + isSerialNumberMatchingSpy = jest.spyOn(interceptor, 'isSerialNumberMatching').mockImplementation((_p, _pc, _cb) => true); + + getPeerCertFromCallSpy = jest.spyOn(interceptor, 'getPeerCertFromCall').mockReturnValue({ serialNumber: 'DDEEFF' }); + }); + + const getCreatedConn = () => { + const instances = (Connection as jest.Mock).mock?.instances ?? []; + return instances.find((i: any) => i.address === MOCK_ADDRESS) ?? mockClientConnections.get(MOCK_ADDRESS); + }; + + it('should fail if no JWE token is provided in the metadata', async () => { + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({}); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(result.conn).toBe(null); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.UNAUTHENTICATED, + message: 'No token provided', + }), + null + ); + }); + + it("should authenticate instantly if connection exists and token hasn't changed", async () => { + const existingConn = new (Connection as jest.Mock)(VALID_JWE, MOCK_ADDRESS, ConnectionDirection.RECEIVING); + existingConn.getToken.mockReturnValue(VALID_JWE); + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(true); + expect(result.conn).toBe(existingConn); + expect(isRequestAllowedSpy).toHaveBeenCalledTimes(1); + expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); + expect(jose.compactDecrypt as jest.Mock).toHaveBeenCalledTimes(1); + expect(jose.compactDecrypt as jest.Mock).toHaveBeenCalledWith(VALID_JWE, 'mock_priv_key'); + }); + + it('should reject if connection exists but is BLOCKED', async () => { + const existingConn = new (Connection as jest.Mock)(VALID_JWE, MOCK_ADDRESS, ConnectionDirection.RECEIVING); + existingConn.status = ConnectionStatus.BLOCKED; + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.UNAUTHENTICATED, + message: 'Connection is blocked. Contact administrator.', + }), + null + ); + }); + + it('should reject existing connection on serial number mismatch', async () => { + const existingConn = new (Connection as jest.Mock)(VALID_JWE, MOCK_ADDRESS, ConnectionDirection.RECEIVING); + existingConn.getToken.mockReturnValue(VALID_JWE); + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + // mock serial number mismatch + isSerialNumberMatchingSpy.mockImplementation((_p, _pc, cb) => { + cb( + { + name: 'AuthenticationError', + code: grpc.status.PERMISSION_DENIED, + message: 'Serial number mismatch (mTLS binding failure).', + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(existingConn.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Serial number mismatch (mTLS binding failure).', + }), + null + ); + }); + + it('should successfully authenticate a NEW connection', async () => { + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({ + jwetoken: 'NEW.JWE.TOKEN', + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(true); + expect(created).toBeDefined(); + expect(created!.handleSuccessfulAuth).toHaveBeenCalledWith(DECRYPTED_PAYLOAD); + expect(jose.compactDecrypt as jest.Mock).toHaveBeenCalledTimes(1); + expect(jose.compactVerify as jest.Mock).toHaveBeenCalledTimes(1); + expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); + expect(isRequestAllowedSpy).toHaveBeenCalledTimes(1); + }); + + it('should fail if JWE decryption fails', async () => { + (jose.compactDecrypt as jest.Mock).mockRejectedValue(new Error('Decryption failed')); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(false); + expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Incorrect token provided (JWE Decryption failed)', + }), + null + ); + }); + + it('should fail if JWS verification fails', async () => { + (jose.compactVerify as jest.Mock).mockRejectedValue(new Error('Invalid signature')); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(false); + expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'JWS Verification error: Invalid signature', + }), + null + ); + }); + + it('should fail if mTLS serial number mismatch occurs after decryption', async () => { + isSerialNumberMatchingSpy.mockImplementation((_p, _pc, cb) => { + cb( + { + name: 'AuthenticationError', + code: grpc.status.PERMISSION_DENIED, + message: 'Serial number mismatch (mTLS binding failure).', + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(false); + expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Serial number mismatch (mTLS binding failure).', + }), + null + ); + }); + + it('should fail if request authorization check fails', async () => { + isRequestAllowedSpy.mockImplementation((_p, _r, cb) => { + cb( + { + name: 'AuthorizationError', + code: grpc.status.PERMISSION_DENIED, + message: 'Request of type POST is not allowed by the token policy.', + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ code: grpc.status.PERMISSION_DENIED }), null); + expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); + }); + it('should reject if existing connection has request not allowed', async () => { + const existingConn = new (Connection as jest.Mock)(VALID_JWE, MOCK_ADDRESS, ConnectionDirection.RECEIVING); + existingConn.getToken.mockReturnValue(VALID_JWE); + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + // mock request not allowed + isRequestAllowedSpy.mockImplementation((_p, _r, cb) => { + cb( + { + name: 'AuthorizationError', + code: grpc.status.PERMISSION_DENIED, + message: 'Request of type POST is not allowed by the token policy.', + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(result.conn).toBe(existingConn); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Request of type POST is not allowed by the token policy.', + }), + null + ); + }); + + it('should re-authenticate when existing connection has different token', async () => { + const existingConn = new (Connection as jest.Mock)('OLD.TOKEN', MOCK_ADDRESS, ConnectionDirection.RECEIVING); + existingConn.getToken.mockReturnValue('OLD.TOKEN'); + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({ + jwetoken: 'NEW.TOKEN', + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(true); + expect(existingConn.handleSuccessfulAuth).toHaveBeenCalledWith(DECRYPTED_PAYLOAD); + expect(jose.compactDecrypt as jest.Mock).toHaveBeenCalledTimes(1); + expect(jose.compactVerify as jest.Mock).toHaveBeenCalledTimes(1); + }); + + it('should fail if JWS has incorrect signing algorithm', async () => { + (jose.compactVerify as jest.Mock).mockResolvedValue({ + payload: Buffer.from(JSON.stringify(DECRYPTED_PAYLOAD)), + protectedHeader: { alg: 'RS256' }, // Wrong algorithm + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Incorrect signing algorithm for JWS.', + code: grpc.status.UNAUTHENTICATED, + }), + null + ); + }); +}); + +describe('isRequestAllowed', () => { + const mockCallback = jest.fn(); + + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('should return true for valid payload with unexpired permission', () => { + const now = Math.floor(Date.now() / 1000); + const payload: TokenPayload = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + iat: { POST: now - 100 }, + exp: { POST: now + 3600 }, + } as any; + + const request = { method: 'POST' }; + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(true); + expect(mockCallback).not.toHaveBeenCalled(); + }); + + it('should return false for expired permission', () => { + const now = Math.floor(Date.now() / 1000); + const payload: TokenPayload = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + iat: { POST: now - 7200 }, + exp: { POST: now - 3600 }, // Expired 1 hour ago + } as any; + + const request = { method: 'POST' }; + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.UNAUTHENTICATED, + message: 'Request of type POST, permission has expired.', + }), + null + ); + }); + + it('should return false for method not in token permissions', () => { + const now = Math.floor(Date.now() / 1000); + const payload: TokenPayload = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + iat: { POST: now - 100 }, + exp: { POST: now + 3600 }, + } as any; + + const request = { method: 'DELETE' }; // Not in permissions + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.PERMISSION_DENIED, + message: 'Request of type DELETE is not allowed by the token policy.', + }), + null + ); + }); + + it('should handle missing request method with default POST', () => { + const now = Math.floor(Date.now() / 1000); + const payload: TokenPayload = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + iat: { POST: now - 100 }, + exp: { POST: now + 3600 }, + } as any; + + const request = {}; // No method + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(true); + }); + + it('should return false for invalid payload structure (missing iat)', () => { + const now = Math.floor(Date.now() / 1000); + const payload: any = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + exp: { POST: now + 3600 }, + // iat missing + }; + + const request = { method: 'POST' }; + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(false); + }); + + it('should return false for invalid payload structure (empty iat)', () => { + const now = Math.floor(Date.now() / 1000); + const payload: any = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + iat: {}, // Empty + exp: { POST: now + 3600 }, + }; + + const request = { method: 'POST' }; + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(false); + }); +}); + +describe('isPermissionExpired', () => { + it('should return false for valid unexpired permission', () => { + const now = Math.floor(Date.now() / 1000); + const iat = now - 100; + const exp = now + 3600; + + const result = interceptor.isPermissionExpired(iat, exp); + + expect(result).toBe(false); + }); + + it('should return true when permission has expired', () => { + const now = Math.floor(Date.now() / 1000); + const iat = now - 7200; + const exp = now - 3600; // Expired 1 hour ago + + const result = interceptor.isPermissionExpired(iat, exp); + + expect(result).toBe(true); + }); + + it('should return true when iat is in the future', () => { + const now = Math.floor(Date.now() / 1000); + const iat = now + 100; // Issued in the future + const exp = now + 3600; + + const result = interceptor.isPermissionExpired(iat, exp); + + expect(result).toBe(true); + }); +}); + +describe('isSerialNumberMatching', () => { + const mockCallback = jest.fn(); + + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('should return true when serial numbers match', () => { + const payload: TokenPayload = { + sub: 'AABBCCDDEE', + } as any; + const peerCert = { serialNumber: 'AA:BB:CC:DD:EE' }; + + const result = interceptor.isSerialNumberMatching(payload, peerCert, mockCallback); + + expect(result).toBe(true); + expect(mockCallback).not.toHaveBeenCalled(); + }); + + it('should return true when serial numbers match (different formats)', () => { + const payload: TokenPayload = { + sub: 'aabbccddee', + } as any; + const peerCert = { serialNumber: 'AA:BB:CC:DD:EE' }; + + const result = interceptor.isSerialNumberMatching(payload, peerCert, mockCallback); + + expect(result).toBe(true); + }); + + it('should return false when serial numbers do not match', () => { + const payload: TokenPayload = { + sub: 'AABBCCDDEE', + } as any; + const peerCert = { serialNumber: '11:22:33:44:55' }; + + const result = interceptor.isSerialNumberMatching(payload, peerCert, mockCallback); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.PERMISSION_DENIED, + message: 'Serial number mismatch (mTLS binding failure).', + }), + null + ); + }); + + it('should return false when peerCert is null', () => { + const payload: TokenPayload = { + sub: 'AABBCCDDEE', + } as any; + + const result = interceptor.isSerialNumberMatching(payload, null, mockCallback); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalled(); + }); + + it('should return false when payload is undefined', () => { + const peerCert = { serialNumber: 'AA:BB:CC:DD:EE' }; + + const result = interceptor.isSerialNumberMatching(undefined, peerCert, mockCallback); + + expect(result).toBe(false); + }); + + it('should normalize serial numbers with special characters', () => { + const payload: TokenPayload = { + sub: 'AA-BB-CC-DD-EE', + } as any; + const peerCert = { serialNumber: 'AA:BB:CC:DD:EE' }; + + const result = interceptor.isSerialNumberMatching(payload, peerCert, mockCallback); + + expect(result).toBe(true); + }); +}); + +describe('getPeerCertFromCall', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('should return peer certificate from call', () => { + const mockCert = { serialNumber: 'AABBCC', subject: 'CN=test' }; + const mockCall = { + call: { + stream: { + session: { + socket: { + getPeerCertificate: jest.fn().mockReturnValue(mockCert), + }, + }, + }, + }, + }; + + const result = interceptor.getPeerCertFromCall(mockCall); + + expect(result).toBe(mockCert); + expect(mockCall.call.stream.session.socket.getPeerCertificate).toHaveBeenCalledWith(true); + }); + + it('should handle missing call structure gracefully', () => { + const mockCall = {}; + + const result = interceptor.getPeerCertFromCall(mockCall); + + expect(result).toBeUndefined(); + }); +}); diff --git a/Tokenization/backend/wrapper/src/test/client/commands/newToken.test.ts b/Tokenization/backend/wrapper/src/test/client/commands/newToken.test.ts index fd5c23215..d87d08344 100644 --- a/Tokenization/backend/wrapper/src/test/client/commands/newToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/commands/newToken.test.ts @@ -21,11 +21,27 @@ import { ConnectionDirection, DuplexMessageEvent } from '../../../models/message import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; import path from 'path'; +import { getTestCerts } from '../../testCerts/testCerts'; + +// Mock logger +jest.mock( + '@aliceo2/web-ui', + () => ({ + LogManager: { + getLogger: () => ({ + infoMessage: jest.fn(), + debugMessage: jest.fn(), + errorMessage: jest.fn(), + }), + }, + }), + { virtual: true } +); /** * Helper to create a new token command with given address, direction, and token. */ -function createEventMessage(targetAddress: string, connectionDirection: ConnectionDirection): Command { +const createEventMessage = (targetAddress: string, connectionDirection: ConnectionDirection): Command => { return { event: DuplexMessageEvent.MESSAGE_EVENT_NEW_TOKEN, payload: { @@ -34,7 +50,7 @@ function createEventMessage(targetAddress: string, connectionDirection: Connecti token: 'test-token', }, } as Command; -} +}; describe('NewTokenHandler', () => { let manager: ConnectionManager; @@ -65,9 +81,10 @@ describe('NewTokenHandler', () => { return undefined; }), createNewConnection: jest.fn(function (this: any, address: string, dir: ConnectionDirection, token: string) { - const conn = new Connection(token, address, dir, peerCtor); + const conn = new Connection(token, address, dir); if (dir === ConnectionDirection.SENDING) { this.sendingConnections.set(address, conn); + conn.createSslTunnel(peerCtor, getTestCerts()); } else { this.receivingConnections.set(address, conn); } @@ -78,7 +95,9 @@ describe('NewTokenHandler', () => { it('should update token on existing SENDING connection', async () => { const targetAddress = 'peer-123'; - const conn = new Connection('old-token', targetAddress, ConnectionDirection.SENDING, peerCtor); + const conn = new Connection('old-token', targetAddress, ConnectionDirection.SENDING); + conn.createSslTunnel(peerCtor, getTestCerts()); + (manager as any).sendingConnections.set(targetAddress, conn); const handler = new NewTokenHandler(manager); diff --git a/Tokenization/backend/wrapper/src/test/client/commands/revokeToken.test.ts b/Tokenization/backend/wrapper/src/test/client/commands/revokeToken.test.ts index 3764d1724..b5c8fb75e 100644 --- a/Tokenization/backend/wrapper/src/test/client/commands/revokeToken.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/commands/revokeToken.test.ts @@ -22,6 +22,22 @@ import { Command } from 'models/commands.model'; import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; import path from 'path'; +import { getTestCerts } from '../../testCerts/testCerts'; + +// Mock logger +jest.mock( + '@aliceo2/web-ui', + () => ({ + LogManager: { + getLogger: () => ({ + infoMessage: jest.fn(), + debugMessage: jest.fn(), + errorMessage: jest.fn(), + }), + }, + }), + { virtual: true } +); describe('RevokeToken', () => { const protoPath = path.join(__dirname, '..', '..', '..', '..', '..', 'proto', 'wrapper.proto'); @@ -37,7 +53,7 @@ describe('RevokeToken', () => { const wrapper = proto.webui.tokenization; const peerCtor = wrapper.Peer2Peer; - function createEventMessage(targetAddress: string) { + const createEventMessage = (targetAddress: string) => { return { event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, payload: { @@ -45,7 +61,7 @@ describe('RevokeToken', () => { token: 'test-token', }, } as Command; - } + }; let manager: ConnectionManager; @@ -61,7 +77,8 @@ describe('RevokeToken', () => { it('should revoke token when connection found in sendingConnections', async () => { const targetAddress = 'peer-123'; - const conn = new Connection('valid-token', targetAddress, ConnectionDirection.SENDING, peerCtor); + const conn = new Connection('valid-token', targetAddress, ConnectionDirection.SENDING); + conn.createSslTunnel(peerCtor, getTestCerts()); (manager as any).sendingConnections!.set(targetAddress, conn); const handler = new RevokeTokenHandler(manager); @@ -75,7 +92,7 @@ describe('RevokeToken', () => { it('should revoke token when connection found in receivingConnections', async () => { const targetAddress = 'peer-456'; - const conn = new Connection('valid-token', targetAddress, ConnectionDirection.RECEIVING, peerCtor); + const conn = new Connection('valid-token', targetAddress, ConnectionDirection.RECEIVING); (manager as any).receivingConnections.set(targetAddress, conn); const handler = new RevokeTokenHandler(manager); diff --git a/Tokenization/backend/wrapper/src/test/client/connectionManager/ConnectionManager.test.ts b/Tokenization/backend/wrapper/src/test/client/connectionManager/ConnectionManager.test.ts index dff46c2c6..637f22982 100644 --- a/Tokenization/backend/wrapper/src/test/client/connectionManager/ConnectionManager.test.ts +++ b/Tokenization/backend/wrapper/src/test/client/connectionManager/ConnectionManager.test.ts @@ -13,145 +13,135 @@ */ import * as grpc from '@grpc/grpc-js'; +import { ConnectionManager } from '../../../client/connectionManager/ConnectionManager'; +import { ConnectionDirection, DuplexMessageEvent } from '../../../models/message.model'; +import { SecurityContext } from '../../../utils/security/SecurityContext'; + +// Mock duplex stream +const mockStream = { + on: jest.fn(), + end: jest.fn(), +}; + +// Mock gRPC client +const mockClient = { + ClientStream: jest.fn(() => mockStream), +}; + +// Mock CentralSystem constructor +const CentralSystemMock = jest.fn(() => mockClient); + +// Mock gRPC auth interceptor +jest.mock('../../../client/connectionManager/interceptors/grpc.auth.interceptor', () => ({ + gRPCAuthInterceptor: jest.fn((call, callback) => { + return Promise.resolve({ + isAuthenticated: true, + conn: { + updateStatus: jest.fn(), + handleSuccessfulAuth: jest.fn(), + getSerialNumber: jest.fn(), + setSerialNumber: jest.fn(), + }, + }); + }), +})); -// Capture service impl registered on grpc.Server.addService -let capturedServerImpl: any | null = null; +// Mock dispatcher +const mockDispatch = jest.fn(); +jest.mock('../../../client/connectionManager/eventManagement/CentralCommandDispatcher', () => ({ + CentralCommandDispatcher: jest.fn(() => ({ + dispatch: mockDispatch, + register: jest.fn(), + })), +})); +// Mock logger +jest.mock( + '@aliceo2/web-ui', + () => ({ + LogManager: { + getLogger: () => ({ + infoMessage: jest.fn(), + debugMessage: jest.fn(), + errorMessage: jest.fn(), + }), + }, + }), + { virtual: true } +); + +// Mock gRPC proto loader and client jest.mock('@grpc/proto-loader', () => ({ - loadSync: jest.fn(() => ({})), + loadSync: jest.fn(() => { + return {}; + }), })); -const CentralSystemClientMock = jest.fn(); -const Peer2PeerCtorMock = jest.fn(); +let capturedServerImpl: any | null = null; -// Mock @grpc/grpc-js jest.mock('@grpc/grpc-js', () => { const original = jest.requireActual('@grpc/grpc-js'); + const Peer2PeerMock: any = jest.fn(() => ({ + Fetch: jest.fn(), + })); + // simulation of the service definition + Peer2PeerMock.service = { + Fetch: { + path: '/webui.tokenization.Peer2Peer/Fetch', + requestStream: false, + responseStream: false, + requestSerialize: (x: any) => x, + requestDeserialize: (x: any) => x, + responseSerialize: (x: any) => x, + responseDeserialize: (x: any) => x, + }, + }; + // Mock server const mockServer = { addService: jest.fn((_svc: any, impl: any) => { capturedServerImpl = impl; }), - bindAsync: jest.fn((_addr: string, _creds: any, cb: any) => cb(null)), + bindAsync: jest.fn((_addr: any, _creds: any, cb: any) => cb(null)), forceShutdown: jest.fn(), }; - const ServerCtor = jest.fn(() => mockServer); - - const loadPackageDefinition = jest.fn(() => ({ - webui: { - tokenization: { - CentralSystem: CentralSystemClientMock, - Peer2Peer: Object.assign(Peer2PeerCtorMock, { - service: { - Fetch: { - path: '/webui.tokenization.Peer2Peer/Fetch', - requestStream: false, - responseStream: false, - requestSerialize: (x: any) => x, - requestDeserialize: (x: any) => x, - responseSerialize: (x: any) => x, - responseDeserialize: (x: any) => x, - }, - }, - }), - }, - }, - })); + + const mockServerCtor = jest.fn(() => mockServer); return { ...original, - loadPackageDefinition, credentials: { - createInsecure: jest.fn(() => ({})), + createSsl: jest.fn(() => 'mock-credentials'), }, ServerCredentials: { - createInsecure: jest.fn(() => ({})), + createSsl: jest.fn(() => 'mock-credentials'), }, status: { ...original.status, INTERNAL: 13, }, - Server: ServerCtor, - }; -}); - -// Mock CentralCommandDispatcher -const dispatcherRegisterMock = jest.fn(); -jest.mock( - '../../../client/connectionManager/eventManagement/CentralCommandDispatcher', - () => ({ - CentralCommandDispatcher: jest.fn().mockImplementation(() => ({ - register: dispatcherRegisterMock, - })), - }), - { virtual: true } -); - -// Mock CentralConnection -const centralStartMock = jest.fn(); -const centralDisconnectMock = jest.fn(); -jest.mock( - '../../../client/connectionManager/CentralConnection', - () => ({ - CentralConnection: jest.fn().mockImplementation(() => ({ - start: centralStartMock, - disconnect: centralDisconnectMock, + loadPackageDefinition: jest.fn(() => ({ + webui: { + tokenization: { + CentralSystem: CentralSystemMock, + Peer2Peer: Peer2PeerMock, + }, + }, })), - }), - { virtual: true } -); - -// Track Connection instances and allow status changes -const createdConnections: any[] = []; -const connectionCtorMock = jest.fn().mockImplementation(function (this: any, token: string, address: string, direction: any, peerCtor: any) { - this._token = token; - this._address = address; - this.direction = direction; - this.status = undefined; - this.targetAddress = address; - this.token = token; - Object.defineProperty(this, 'status', { - get: () => this._status, - set: (v) => (this._status = v), - configurable: true, - }); - createdConnections.push({ token, address, direction, peerCtor, instance: this }); + Server: mockServerCtor, + }; }); -jest.mock( - '../../../client/connection/Connection', - () => ({ - Connection: connectionCtorMock, - }), - { virtual: true } -); - -const infoMessageMock = jest.fn(); -const errorMessageMock = jest.fn(); -jest.mock( - '@aliceo2/web-ui', - () => ({ - LogManager: { - getLogger: () => ({ - infoMessage: infoMessageMock, - errorMessage: errorMessageMock, - debugMessage: jest.fn(), - }), - }, - }), - { virtual: true } -); - -import { ConnectionManager } from '../../../client/connectionManager/ConnectionManager'; -import { ConnectionDirection } from '../../../models/message.model'; -import { ConnectionStatus } from '../../../models/connection.model'; describe('ConnectionManager', () => { + let conn: ConnectionManager; + const MOCK_CERT = Buffer.from('MOCK_CERT'); + const securityContext = new SecurityContext(MOCK_CERT, MOCK_CERT, MOCK_CERT, MOCK_CERT, MOCK_CERT); + beforeEach(() => { jest.clearAllMocks(); capturedServerImpl = null; - createdConnections.length = 0; - // @ts-ignore global.fetch = jest.fn(); + conn = new ConnectionManager('dummy.proto', 'localhost:12345', securityContext); }); afterAll(() => { @@ -159,241 +149,343 @@ describe('ConnectionManager', () => { delete global.fetch; }); - test('constructor: loads proto, builds wrapper/peerCtor and CentralSystem client', () => { - const cm = new ConnectionManager('proto/file.proto', 'central:5555'); - expect(cm).toBeDefined(); - - expect((grpc as any).loadPackageDefinition).toHaveBeenCalled(); - expect(CentralSystemClientMock).toHaveBeenCalledWith('central:5555', expect.any(Object)); - expect(grpc.credentials.createInsecure).toHaveBeenCalled(); + test('should initialize client with correct address', () => { + expect(conn).toBeDefined(); + expect(grpc.loadPackageDefinition).toHaveBeenCalled(); + expect(CentralSystemMock).toHaveBeenCalledWith('localhost:12345', 'mock-credentials'); }); - test('registerCommandHandlers: calls dispatcher.register for each item', () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - dispatcherRegisterMock.mockClear(); + test('connectToCentralSystem() should set up stream listeners', () => { + conn.connectToCentralSystem(); - const handlers = [ - { event: 1 as any, handler: { handle: jest.fn() } as any }, - { event: 2 as any, handler: { handle: jest.fn() } as any }, - ]; - - cm.registerCommandHandlers(handlers); - - expect(dispatcherRegisterMock).toHaveBeenCalledTimes(2); - expect(dispatcherRegisterMock).toHaveBeenCalledWith(handlers[0].event, handlers[0].handler); - expect(dispatcherRegisterMock).toHaveBeenCalledWith(handlers[1].event, handlers[1].handler); + expect(mockClient.ClientStream).toHaveBeenCalled(); + expect(mockStream.on).toHaveBeenCalledWith('data', expect.any(Function)); + expect(mockStream.on).toHaveBeenCalledWith('end', expect.any(Function)); + expect(mockStream.on).toHaveBeenCalledWith('error', expect.any(Function)); }); - test('connectToCentralSystem/disconnectFromCentralSystem delegate to CentralConnection', () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - // @ts-ignore - cm['_peerCtor'] = Peer2PeerCtorMock; - cm.connectToCentralSystem(); - expect(centralStartMock).toHaveBeenCalled(); + test('disconnectFromCentralSystem() should end stream', () => { + conn.connectToCentralSystem(); + conn.disconnectFromCentralSystem(); - cm.disconnectFromCentralSystem(); - expect(centralDisconnectMock).toHaveBeenCalled(); + expect(mockStream.end).toHaveBeenCalled(); }); - test('createNewConnection: adds to sending map, sets CONNECTED, logs', () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - // @ts-ignore - cm['_peerCtor'] = Peer2PeerCtorMock; - const conn = cm.createNewConnection('peer-A', ConnectionDirection.SENDING, 'tok123'); - - expect(connectionCtorMock).toHaveBeenCalledWith('tok123', 'peer-A', ConnectionDirection.SENDING, expect.any(Function)); - expect(conn.status).toBe(ConnectionStatus.CONNECTED); + test("should reconnect on stream 'end'", () => { + jest.useFakeTimers(); + conn.connectToCentralSystem(); + const onEnd = mockStream.on.mock.calls.find(([event]) => event === 'end')?.[1]; - // Exposed via connections getter - const { sending, receiving } = cm.connections; - expect(sending.length).toBe(1); - expect(receiving.length).toBe(0); + onEnd?.(); // simulate 'end' + jest.advanceTimersByTime(2000); - expect(infoMessageMock).toHaveBeenCalledWith(expect.stringContaining('Connection with peer-A has been estabilished')); + expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); + jest.useRealTimers(); }); - test('createNewConnection: adds to receiving map if direction is RECEIVING', () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - cm.createNewConnection('peer-B', ConnectionDirection.RECEIVING); + test("should reconnect on stream 'error'", () => { + jest.useFakeTimers(); + conn.connectToCentralSystem(); + const onError = mockStream.on.mock.calls.find(([event]) => event === 'error')?.[1]; - const { sending, receiving } = cm.connections; - expect(sending.length).toBe(0); - expect(receiving.length).toBe(1); - }); + onError?.(new Error('Simulated error')); + jest.advanceTimersByTime(2000); - test('getConnectionByAddress: returns by direction. Logs on invalid direction', () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - const s = cm.createNewConnection('s-1', ConnectionDirection.SENDING); - const r = cm.createNewConnection('r-1', ConnectionDirection.RECEIVING); + expect(mockClient.ClientStream).toHaveBeenCalledTimes(2); + jest.useRealTimers(); + }); - expect(cm.getConnectionByAddress('s-1', ConnectionDirection.SENDING)).toBe(s); - expect(cm.getConnectionByAddress('r-1', ConnectionDirection.RECEIVING)).toBe(r); + test("should dispatch event when 'data' is received", () => { + conn.connectToCentralSystem(); + const onData = mockStream.on.mock.calls.find(([event]) => event === 'data')?.[1]; - errorMessageMock.mockClear(); - const invalid = cm.getConnectionByAddress('x', 999 as any); - expect(invalid).toBeUndefined(); - expect(errorMessageMock).toHaveBeenCalledWith('Invalid connection direction: 999'); - }); + const mockMessage = { + event: DuplexMessageEvent.MESSAGE_EVENT_REVOKE_TOKEN, + data: { + revokeToken: { + token: 'abc123', + targetAddress: 'peer-123', + }, + }, + }; - test('connections getter: returns arrays (copies) of maps', () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - cm.createNewConnection('a', ConnectionDirection.SENDING); - cm.createNewConnection('b', ConnectionDirection.RECEIVING); + onData?.(mockMessage); - const { sending, receiving } = cm.connections; - expect(Array.isArray(sending)).toBe(true); - expect(Array.isArray(receiving)).toBe(true); - expect(sending.length).toBe(1); - expect(receiving.length).toBe(1); + expect(mockDispatch).toHaveBeenCalledWith(mockMessage); }); - test('listenForPeers: creates server, registers service, binds & logs', async () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - await cm.listenForPeers(50099, 'http://localhost:41000/api/'); + test('listenForPeers() should start server and register service', async () => { + await conn.listenForPeers(50055, 'http://localhost:40041/api/'); - const ServerCtor = (grpc.Server as any).mock; - expect(ServerCtor).toBeDefined(); - expect(ServerCtor.calls.length).toBeGreaterThan(0); + const serverCtor = (grpc.Server as any).mock; + expect(serverCtor).toBeDefined(); + expect(serverCtor.calls.length).toBeGreaterThan(0); - const serverInstance = ServerCtor.results[0].value; + const serverInstance = serverCtor.results[0].value; expect(serverInstance.addService).toHaveBeenCalled(); - expect(serverInstance.bindAsync).toHaveBeenCalledWith('localhost:50099', expect.anything(), expect.any(Function)); - expect(infoMessageMock).toHaveBeenCalledWith('Peer server listening on localhost:50099'); + expect(serverInstance.bindAsync).toHaveBeenCalledWith('localhost:50055', expect.anything(), expect.any(Function)); - // Service impl captured expect(capturedServerImpl).toBeTruthy(); expect(typeof capturedServerImpl.Fetch).toBe('function'); }); - test('listenForPeers: calling twice shuts previous server down', async () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - await cm.listenForPeers(50100, 'http://localhost:41000/api/'); - const firstServer = (grpc.Server as any).mock.results[0].value; - - await cm.listenForPeers(50101, 'http://localhost:41000/api/'); - expect(firstServer.forceShutdown).toHaveBeenCalled(); - }); - - test('p2p Fetch: registers new incoming receiving connection, forwards to local API, maps response', async () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - await cm.listenForPeers(50102, 'http://local/api/'); + test('p2p Fetch should register incoming receiving connection and forward request', async () => { + await conn.listenForPeers(50056, 'http://localhost:40041/api/'); - // Prepare incoming call and callback + // prepare data to call const call = { getPeer: () => 'client-42', request: { - method: 'post', + method: 'POST', path: 'echo', headers: { 'content-type': 'application/json' }, body: Buffer.from(JSON.stringify({ ping: true })), }, } as any; + const callback = jest.fn(); - // Mock fetch response - // @ts-ignore + // @ts-ignore - mock global.fetch response global.fetch.mockResolvedValue({ status: 202, headers: { forEach: (fn: (v: string, k: string) => void) => { fn('application/json', 'content-type'); - fn('abc', 'x-extra'); + fn('test', 'x-extra'); }, }, arrayBuffer: async () => Buffer.from(JSON.stringify({ ok: 1 })), }); - const before = cm.connections.receiving.length; + const before = conn.connections.receiving.length; await capturedServerImpl.Fetch(call, callback); - expect(global.fetch).toHaveBeenCalledWith('http://local/api/echo', { + expect(global.fetch).toHaveBeenCalledWith('http://localhost:40041/api/echo', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ping: true }), }); - // Response mapped back to gRPC + // callback with response from forwarded fetch expect(callback).toHaveBeenCalledWith(null, { status: 202, - headers: { 'content-type': 'application/json', 'x-extra': 'abc' }, + headers: { 'content-type': 'application/json', 'x-extra': 'test' }, body: expect.any(Buffer), }); - // Receiving connection was created & stored - const after = cm.connections.receiving.length; + // connection receiving should be registered + const after = conn.connections.receiving.length; expect(after).toBeGreaterThan(before); - const found = cm.getConnectionByAddress('client-42', ConnectionDirection.RECEIVING); - expect(found).toBeDefined(); - expect(infoMessageMock).toHaveBeenCalledWith(expect.stringContaining('Incoming request from client-42')); - expect(infoMessageMock).toHaveBeenCalledWith(expect.stringContaining('New incoming connection registered for: client-42')); + const rec = conn.getConnectionByAddress('client-42', ConnectionDirection.RECEIVING); + expect(rec).toBeDefined(); }); - test('p2p Fetch: uses existing receiving connection when present (no duplicate creation)', async () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - await cm.listenForPeers(50103, 'http://local/api/'); - - cm.createNewConnection('client-77', ConnectionDirection.RECEIVING); - - // @ts-ignore - global.fetch.mockResolvedValue({ - status: 200, - headers: { forEach: (fn: any) => fn('text/plain', 'content-type') }, - arrayBuffer: async () => Buffer.from('ok'), - }); + test('p2p Fetch should return INTERNAL on forward error', async () => { + await conn.listenForPeers(50057, 'http://localhost:40041/api/'); const call = { - getPeer: () => 'client-77', - request: { method: 'get', path: 'pong', headers: {}, body: undefined }, + getPeer: () => 'client-error', + request: { + method: 'GET', + path: 'fail', + headers: {}, + }, } as any; const callback = jest.fn(); - const before = cm.connections.receiving.length; - await capturedServerImpl.Fetch(call, callback); - - // No new receiving connection added - const after = cm.connections.receiving.length; - expect(after).toBe(before); + // @ts-ignore + global.fetch.mockRejectedValue(new Error('err')); - // Forwarded with GET and no body - expect(global.fetch).toHaveBeenCalledWith('http://local/api/pong', { - method: 'GET', - headers: {}, - body: undefined, - }); + await capturedServerImpl.Fetch(call, callback); expect(callback).toHaveBeenCalledWith( - null, expect.objectContaining({ - status: 200, - headers: { 'content-type': 'text/plain' }, - body: expect.any(Buffer), + code: grpc.status.INTERNAL, + message: 'err', }) ); }); - test('p2p Fetch: on forward error returns INTERNAL and logs error', async () => { - const cm = new ConnectionManager('p.proto', 'c:1'); - await cm.listenForPeers(50104, 'http://local/api/'); + describe('createNewConnection', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); - // @ts-ignore - global.fetch.mockRejectedValue(new Error('err')); + test('should create new RECEIVING connection', async () => { + const address = 'peer-receiving-1'; + const token = 'test-token-123'; - const call = { - getPeer: () => 'err-client', - request: { method: 'get', path: 'fail', headers: {} }, - } as any; - const callback = jest.fn(); + const connection = await conn.createNewConnection(address, ConnectionDirection.RECEIVING, token); - await capturedServerImpl.Fetch(call, callback); + expect(connection).toBeDefined(); + expect(connection.targetAddress).toBe(address); + expect(connection.direction).toBe(ConnectionDirection.RECEIVING); + expect(connection.token).toBe(token); + }); - expect(errorMessageMock).toHaveBeenCalledWith(expect.stringContaining('Error forwarding request')); - expect(callback).toHaveBeenCalledWith( - expect.objectContaining({ - code: grpc.status.INTERNAL, - message: 'err', - }) - ); + test('should create new SENDING connection with SSL tunnel', async () => { + const address = 'peer-sending-1'; + const token = 'test-token-456'; + + const connection = await conn.createNewConnection(address, ConnectionDirection.SENDING, token); + + expect(connection).toBeDefined(); + expect(connection.targetAddress).toBe(address); + expect(connection.direction).toBe(ConnectionDirection.SENDING); + expect(connection.token).toBe(token); + }); + + test('should return existing connection if already exists (RECEIVING)', async () => { + const address = 'peer-existing-receiving'; + const token1 = 'token-1'; + const token2 = 'token-2'; + + const conn1 = await conn.createNewConnection(address, ConnectionDirection.RECEIVING, token1); + const conn2 = await conn.createNewConnection(address, ConnectionDirection.RECEIVING, token2); + + expect(conn1).toBe(conn2); + expect(conn2.token).toBe(token2); // Token should be updated + }); + + test('should return existing connection if already exists (SENDING)', async () => { + const address = 'peer-existing-sending'; + const token1 = 'token-1'; + const token2 = 'token-2'; + + const conn1 = await conn.createNewConnection(address, ConnectionDirection.SENDING, token1); + const conn2 = await conn.createNewConnection(address, ConnectionDirection.SENDING, token2); + + expect(conn1).toBe(conn2); + expect(conn2.token).toBe(token2); // Token should be updated + }); + + test('should return existing connection without updating token if no token provided', async () => { + const address = 'peer-no-token-update'; + const token1 = 'token-1'; + + const conn1 = await conn.createNewConnection(address, ConnectionDirection.RECEIVING, token1); + const conn2 = await conn.createNewConnection(address, ConnectionDirection.RECEIVING); + + expect(conn1).toBe(conn2); + expect(conn2.token).toBe(token1); // Token should remain unchanged + }); + + test('should create connection without token', async () => { + const address = 'peer-no-token'; + + const connection = await conn.createNewConnection(address, ConnectionDirection.RECEIVING); + + expect(connection).toBeDefined(); + expect(connection.token).toBe(''); + }); + }); + + describe('getConnectionByAddress', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should get SENDING connection by address', async () => { + const address = 'peer-send-get'; + await conn.createNewConnection(address, ConnectionDirection.SENDING, 'token'); + + const connection = conn.getConnectionByAddress(address, ConnectionDirection.SENDING); + + expect(connection).toBeDefined(); + expect(connection?.targetAddress).toBe(address); + }); + + test('should get RECEIVING connection by address', async () => { + const address = 'peer-receive-get'; + await conn.createNewConnection(address, ConnectionDirection.RECEIVING, 'token'); + + const connection = conn.getConnectionByAddress(address, ConnectionDirection.RECEIVING); + + expect(connection).toBeDefined(); + expect(connection?.targetAddress).toBe(address); + }); + + test('should return undefined for non-existent connection', () => { + const connection = conn.getConnectionByAddress('non-existent', ConnectionDirection.SENDING); + + expect(connection).toBeUndefined(); + }); + }); + + describe('getConnectionBySerialNumber', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should find connection by serial number in receiving connections', async () => { + const address = 'peer-sn-receiving'; + const serialNumber = 'SN-12345'; + + const connection = await conn.createNewConnection(address, ConnectionDirection.RECEIVING, 'token'); + connection.serialNumber = serialNumber; + + const found = conn.getConnectionBySerialNumber(serialNumber); + + expect(found).toBe(connection); + expect(found?.serialNumber).toBe(serialNumber); + }); + + test('should find connection by serial number in sending connections', async () => { + const address = 'peer-sn-sending'; + const serialNumber = 'SN-67890'; + + const connection = await conn.createNewConnection(address, ConnectionDirection.SENDING, 'token'); + connection.serialNumber = serialNumber; + + const found = conn.getConnectionBySerialNumber(serialNumber); + + expect(found).toBe(connection); + expect(found?.serialNumber).toBe(serialNumber); + }); + + test('should return undefined if serial number not found', () => { + const found = conn.getConnectionBySerialNumber('non-existent-sn'); + + expect(found).toBeUndefined(); + }); + }); + + describe('listenForPeers edge cases', () => { + beforeEach(() => { + jest.clearAllMocks(); + capturedServerImpl = null; + }); + + test('should not start listener if clientListenerCert is missing', async () => { + const securityContextNoCert = new SecurityContext(MOCK_CERT, MOCK_CERT, MOCK_CERT, MOCK_CERT, undefined as any); + const connNoCert = new ConnectionManager('dummy.proto', 'localhost:12345', securityContextNoCert); + + await connNoCert.listenForPeers(50058); + + const serverCtor = (grpc.Server as any).mock; + // Server should not be created + expect(serverCtor.calls.length).toBe(0); + }); + + test('should shutdown existing server before starting new one', async () => { + await conn.listenForPeers(50059, 'http://localhost:40041/api/'); + + const serverCtor = (grpc.Server as any).mock; + const firstServer = serverCtor.results[0].value; + + // Start listener again + await conn.listenForPeers(50060, 'http://localhost:40041/api/'); + + expect(firstServer.forceShutdown).toHaveBeenCalled(); + }); + + test('should use default baseAPIPath if not provided', async () => { + await conn.listenForPeers(50061); + + expect(capturedServerImpl).toBeTruthy(); + expect(typeof capturedServerImpl.Fetch).toBe('function'); + }); }); }); diff --git a/Tokenization/backend/wrapper/src/test/client/connectionManager/interceptors/grpc.auth.interceptor.test.ts b/Tokenization/backend/wrapper/src/test/client/connectionManager/interceptors/grpc.auth.interceptor.test.ts new file mode 100644 index 000000000..70d2b86f7 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/client/connectionManager/interceptors/grpc.auth.interceptor.test.ts @@ -0,0 +1,634 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import * as grpc from '@grpc/grpc-js'; +import * as jose from 'jose'; +import * as interceptor from '../../../../client/connectionManager/interceptors/grpc.auth.interceptor'; + +// Connection class mock +jest.mock( + '../../../../client/connection/Connection', + () => { + return { + Connection: jest.fn().mockImplementation((jweToken: string, address: string, direction: any) => { + return { + jweToken, + address, + direction, + status: 1, + payload: { subSerialNumber: 'AABBCC', perm: { POST: true } }, + getStatus: jest.fn(function () { + return this.status; + }), + getToken: jest.fn(function () { + return this.jweToken; + }), + getCachedTokenPayload: jest.fn(function () { + return this.payload; + }), + handleFailedAuth: jest.fn(), + handleSuccessfulAuth: jest.fn(function (p: any) { + this.payload = p; + this.status = 1; + }), + }; + }), + }; + }, + { virtual: true } +); + +import { Connection } from '../../../../client/connection/Connection'; + +jest.mock('jose', () => ({ + importPKCS8: jest.fn(), + importJWK: jest.fn(), + compactDecrypt: jest.fn(), + compactVerify: jest.fn(), +})); + +import { ConnectionStatus, TokenPayload } from '../../../../models/connection.model'; +import { SecurityContext } from '../../../../utils/security/SecurityContext'; +import { ConnectionDirection } from '../../../../models/message.model'; + +const mockSecurityContext = { + clientPrivateKey: Buffer.from('mock_private_key_rsa'), + JWS_PUBLIC_KEY: 'mock_public_key_ed25519', +} as unknown as SecurityContext; + +let isRequestAllowedSpy: jest.SpyInstance; +let isSerialNumberMatchingSpy: jest.SpyInstance; +let getPeerCertFromCallSpy: jest.SpyInstance; + +const mockCall = { + metadata: { getMap: jest.fn(() => ({})) }, + getPeer: jest.fn(() => 'ipv4:127.0.0.1:12345'), + request: { method: 'POST' }, +} as unknown as grpc.ServerUnaryCall; + +const mockCallback = jest.fn(); +const mockClientConnections = new Map(); + +describe('gRPCAuthInterceptor', () => { + const MOCK_ADDRESS = 'ipv4:127.0.0.1:12345'; + const VALID_JWE = 'valid.jwe.token'; + const VALID_JWS = 'valid.jws.token'; + const DECRYPTED_PAYLOAD: TokenPayload = { + subSerialNumber: 'DDEEFF', + perm: { POST: true, GET: false }, + } as any; + + beforeEach(() => { + jest.clearAllMocks(); + mockClientConnections.clear(); + + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({ + jwetoken: VALID_JWE, + }); + (mockCall.getPeer as unknown as jest.Mock).mockReturnValue(MOCK_ADDRESS); + (mockCall as any).request = { method: 'POST' }; + + (jose.importPKCS8 as jest.Mock).mockResolvedValue('mock_priv_key'); + (jose.compactDecrypt as jest.Mock).mockResolvedValue({ + plaintext: Buffer.from(VALID_JWS), + }); + (jose.importJWK as jest.Mock).mockResolvedValue('mock_pub_key'); + (jose.compactVerify as jest.Mock).mockResolvedValue({ + payload: Buffer.from(JSON.stringify(DECRYPTED_PAYLOAD)), + protectedHeader: { alg: 'EdDSA' }, + }); + + // mocks of internal functions + isRequestAllowedSpy = jest.spyOn(interceptor, 'isRequestAllowed').mockImplementation((_p, _r, _cb) => true); + + isSerialNumberMatchingSpy = jest.spyOn(interceptor, 'isSerialNumberMatching').mockImplementation((_p, _pc, _cb) => true); + + getPeerCertFromCallSpy = jest.spyOn(interceptor, 'getPeerCertFromCall').mockReturnValue({ serialNumber: 'DDEEFF' }); + }); + + const getCreatedConn = () => { + const instances = (Connection as jest.Mock).mock?.instances ?? []; + return instances.find((i: any) => i.address === MOCK_ADDRESS) ?? mockClientConnections.get(MOCK_ADDRESS); + }; + + it('should fail if no JWE token is provided in the metadata', async () => { + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({}); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(result.conn).toBe(null); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.UNAUTHENTICATED, + message: 'No token provided', + }), + null + ); + }); + + it("should authenticate instantly if connection exists and token hasn't changed", async () => { + const existingConn = new (Connection as jest.Mock)(VALID_JWE, MOCK_ADDRESS, ConnectionDirection.RECEIVING); + existingConn.getToken.mockReturnValue(VALID_JWE); + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(true); + expect(result.conn).toBe(existingConn); + expect(isRequestAllowedSpy).toHaveBeenCalledTimes(1); + expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); + expect(jose.compactDecrypt as jest.Mock).toHaveBeenCalledTimes(1); + expect(jose.compactDecrypt as jest.Mock).toHaveBeenCalledWith(VALID_JWE, 'mock_priv_key'); + }); + + it('should reject if connection exists but is BLOCKED', async () => { + const existingConn = new (Connection as jest.Mock)(VALID_JWE, MOCK_ADDRESS, ConnectionDirection.RECEIVING); + existingConn.status = ConnectionStatus.BLOCKED; + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.UNAUTHENTICATED, + message: 'Connection is blocked. Contact administrator.', + }), + null + ); + }); + + it('should reject existing connection on serial number mismatch', async () => { + const existingConn = new (Connection as jest.Mock)(VALID_JWE, MOCK_ADDRESS, ConnectionDirection.RECEIVING); + existingConn.getToken.mockReturnValue(VALID_JWE); + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + // mock serial number mismatch + isSerialNumberMatchingSpy.mockImplementation((_p, _pc, cb) => { + cb( + { + name: 'AuthenticationError', + code: grpc.status.PERMISSION_DENIED, + message: 'Serial number mismatch (mTLS binding failure).', + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(existingConn.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Serial number mismatch (mTLS binding failure).', + }), + null + ); + }); + + it('should successfully authenticate a NEW connection', async () => { + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({ + jwetoken: 'NEW.JWE.TOKEN', + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(true); + expect(created).toBeDefined(); + expect(created!.handleSuccessfulAuth).toHaveBeenCalledWith(DECRYPTED_PAYLOAD); + expect(jose.compactDecrypt as jest.Mock).toHaveBeenCalledTimes(1); + expect(jose.compactVerify as jest.Mock).toHaveBeenCalledTimes(1); + expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); + expect(isRequestAllowedSpy).toHaveBeenCalledTimes(1); + }); + + it('should fail if JWE decryption fails', async () => { + (jose.compactDecrypt as jest.Mock).mockRejectedValue(new Error('Decryption failed')); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(false); + expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Incorrect token provided (JWE Decryption failed)', + }), + null + ); + }); + + it('should fail if JWS verification fails', async () => { + (jose.compactVerify as jest.Mock).mockRejectedValue(new Error('Invalid signature')); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(false); + expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'JWS Verification error: Invalid signature', + }), + null + ); + }); + + it('should fail if mTLS serial number mismatch occurs after decryption', async () => { + isSerialNumberMatchingSpy.mockImplementation((_p, _pc, cb) => { + cb( + { + name: 'AuthenticationError', + code: grpc.status.PERMISSION_DENIED, + message: 'Serial number mismatch (mTLS binding failure).', + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + const created = getCreatedConn(); + expect(result.isAuthenticated).toBe(false); + expect(created!.handleFailedAuth).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Serial number mismatch (mTLS binding failure).', + }), + null + ); + }); + + it('should fail if request authorization check fails', async () => { + isRequestAllowedSpy.mockImplementation((_p, _r, cb) => { + cb( + { + name: 'AuthorizationError', + code: grpc.status.PERMISSION_DENIED, + message: 'Request of type POST is not allowed by the token policy.', + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ code: grpc.status.PERMISSION_DENIED }), null); + expect(isSerialNumberMatchingSpy).toHaveBeenCalledTimes(1); + }); + it('should reject if existing connection has request not allowed', async () => { + const existingConn = new (Connection as jest.Mock)(VALID_JWE, MOCK_ADDRESS, ConnectionDirection.RECEIVING); + existingConn.getToken.mockReturnValue(VALID_JWE); + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + // mock request not allowed + isRequestAllowedSpy.mockImplementation((_p, _r, cb) => { + cb( + { + name: 'AuthorizationError', + code: grpc.status.PERMISSION_DENIED, + message: 'Request of type POST is not allowed by the token policy.', + } as any, + null + ); + return false; + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(result.conn).toBe(existingConn); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Request of type POST is not allowed by the token policy.', + }), + null + ); + }); + + it('should re-authenticate when existing connection has different token', async () => { + const existingConn = new (Connection as jest.Mock)('OLD.TOKEN', MOCK_ADDRESS, ConnectionDirection.RECEIVING); + existingConn.getToken.mockReturnValue('OLD.TOKEN'); + mockClientConnections.set(MOCK_ADDRESS, existingConn); + + (mockCall.metadata.getMap as unknown as jest.Mock).mockReturnValue({ + jwetoken: 'NEW.TOKEN', + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(true); + expect(existingConn.handleSuccessfulAuth).toHaveBeenCalledWith(DECRYPTED_PAYLOAD); + expect(jose.compactDecrypt as jest.Mock).toHaveBeenCalledTimes(1); + expect(jose.compactVerify as jest.Mock).toHaveBeenCalledTimes(1); + }); + + it('should fail if JWS has incorrect signing algorithm', async () => { + (jose.compactVerify as jest.Mock).mockResolvedValue({ + payload: Buffer.from(JSON.stringify(DECRYPTED_PAYLOAD)), + protectedHeader: { alg: 'RS256' }, // Wrong algorithm + }); + + const result = await interceptor.gRPCAuthInterceptor(mockCall, mockCallback, mockClientConnections as any, mockSecurityContext); + + expect(result.isAuthenticated).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Incorrect signing algorithm for JWS.', + code: grpc.status.UNAUTHENTICATED, + }), + null + ); + }); +}); + +describe('isRequestAllowed', () => { + const mockCallback = jest.fn(); + + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('should return true for valid payload with unexpired permission', () => { + const now = Math.floor(Date.now() / 1000); + const payload: TokenPayload = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + iat: { POST: now - 100 }, + exp: { POST: now + 3600 }, + } as any; + + const request = { method: 'POST' }; + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(true); + expect(mockCallback).not.toHaveBeenCalled(); + }); + + it('should return false for expired permission', () => { + const now = Math.floor(Date.now() / 1000); + const payload: TokenPayload = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + iat: { POST: now - 7200 }, + exp: { POST: now - 3600 }, // Expired 1 hour ago + } as any; + + const request = { method: 'POST' }; + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.UNAUTHENTICATED, + message: 'Request of type POST, permission has expired.', + }), + null + ); + }); + + it('should return false for method not in token permissions', () => { + const now = Math.floor(Date.now() / 1000); + const payload: TokenPayload = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + iat: { POST: now - 100 }, + exp: { POST: now + 3600 }, + } as any; + + const request = { method: 'DELETE' }; // Not in permissions + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.PERMISSION_DENIED, + message: 'Request of type DELETE is not allowed by the token policy.', + }), + null + ); + }); + + it('should handle missing request method with default POST', () => { + const now = Math.floor(Date.now() / 1000); + const payload: TokenPayload = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + iat: { POST: now - 100 }, + exp: { POST: now + 3600 }, + } as any; + + const request = {}; // No method + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(true); + }); + + it('should return false for invalid payload structure (missing iat)', () => { + const now = Math.floor(Date.now() / 1000); + const payload: any = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + exp: { POST: now + 3600 }, + // iat missing + }; + + const request = { method: 'POST' }; + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(false); + }); + + it('should return false for invalid payload structure (empty iat)', () => { + const now = Math.floor(Date.now() / 1000); + const payload: any = { + sub: 'AABBCC', + aud: 'test-audience', + iss: 'test-issuer', + jti: 'test-jti', + iat: {}, // Empty + exp: { POST: now + 3600 }, + }; + + const request = { method: 'POST' }; + const result = interceptor.isRequestAllowed(payload, request, mockCallback); + + expect(result).toBe(false); + }); +}); + +describe('isPermissionExpired', () => { + it('should return false for valid unexpired permission', () => { + const now = Math.floor(Date.now() / 1000); + const iat = now - 100; + const exp = now + 3600; + + const result = interceptor.isPermissionExpired(iat, exp); + + expect(result).toBe(false); + }); + + it('should return true when permission has expired', () => { + const now = Math.floor(Date.now() / 1000); + const iat = now - 7200; + const exp = now - 3600; // Expired 1 hour ago + + const result = interceptor.isPermissionExpired(iat, exp); + + expect(result).toBe(true); + }); + + it('should return true when iat is in the future', () => { + const now = Math.floor(Date.now() / 1000); + const iat = now + 100; // Issued in the future + const exp = now + 3600; + + const result = interceptor.isPermissionExpired(iat, exp); + + expect(result).toBe(true); + }); +}); + +describe('isSerialNumberMatching', () => { + const mockCallback = jest.fn(); + + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('should return true when serial numbers match', () => { + const payload: TokenPayload = { + sub: 'AABBCCDDEE', + } as any; + const peerCert = { serialNumber: 'AA:BB:CC:DD:EE' }; + + const result = interceptor.isSerialNumberMatching(payload, peerCert, mockCallback); + + expect(result).toBe(true); + expect(mockCallback).not.toHaveBeenCalled(); + }); + + it('should return true when serial numbers match (different formats)', () => { + const payload: TokenPayload = { + sub: 'aabbccddee', + } as any; + const peerCert = { serialNumber: 'AA:BB:CC:DD:EE' }; + + const result = interceptor.isSerialNumberMatching(payload, peerCert, mockCallback); + + expect(result).toBe(true); + }); + + it('should return false when serial numbers do not match', () => { + const payload: TokenPayload = { + sub: 'AABBCCDDEE', + } as any; + const peerCert = { serialNumber: '11:22:33:44:55' }; + + const result = interceptor.isSerialNumberMatching(payload, peerCert, mockCallback); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + code: grpc.status.PERMISSION_DENIED, + message: 'Serial number mismatch (mTLS binding failure).', + }), + null + ); + }); + + it('should return false when peerCert is null', () => { + const payload: TokenPayload = { + sub: 'AABBCCDDEE', + } as any; + + const result = interceptor.isSerialNumberMatching(payload, null, mockCallback); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalled(); + }); + + it('should return false when payload is undefined', () => { + const peerCert = { serialNumber: 'AA:BB:CC:DD:EE' }; + + const result = interceptor.isSerialNumberMatching(undefined, peerCert, mockCallback); + + expect(result).toBe(false); + }); + + it('should normalize serial numbers with special characters', () => { + const payload: TokenPayload = { + sub: 'AA-BB-CC-DD-EE', + } as any; + const peerCert = { serialNumber: 'AA:BB:CC:DD:EE' }; + + const result = interceptor.isSerialNumberMatching(payload, peerCert, mockCallback); + + expect(result).toBe(true); + }); +}); + +describe('getPeerCertFromCall', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('should return peer certificate from call', () => { + const mockCert = { serialNumber: 'AABBCC', subject: 'CN=test' }; + const mockCall = { + call: { + stream: { + session: { + socket: { + getPeerCertificate: jest.fn().mockReturnValue(mockCert), + }, + }, + }, + }, + }; + + const result = interceptor.getPeerCertFromCall(mockCall); + + expect(result).toBe(mockCert); + expect(mockCall.call.stream.session.socket.getPeerCertificate).toHaveBeenCalledWith(true); + }); + + it('should handle missing call structure gracefully', () => { + const mockCall = {}; + + const result = interceptor.getPeerCertFromCall(mockCall); + + expect(result).toBeUndefined(); + }); +}); diff --git a/Tokenization/backend/wrapper/src/test/connection/Connection.test.ts b/Tokenization/backend/wrapper/src/test/connection/Connection.test.ts index 8937e9d5e..69b328940 100644 --- a/Tokenization/backend/wrapper/src/test/connection/Connection.test.ts +++ b/Tokenization/backend/wrapper/src/test/connection/Connection.test.ts @@ -14,6 +14,8 @@ import { Connection } from '../../client/connection/Connection'; import { ConnectionStatus } from '../../models/connection.model'; +// opcjonalnie można też użyć prawdziwego enumu: +// import { ConnectionDirection } from '../../models/message.model'; const FAKE_DIRECTION: any = 'SENDING'; @@ -25,6 +27,21 @@ const PeerCtorMock = jest.fn((_addr: string, _creds: any) => { return lastPeerClient; }); +jest.mock( + '@aliceo2/web-ui', + () => ({ + LogManager: { + getLogger: () => ({ + infoMessage: jest.fn(), + debugMessage: jest.fn(), + warnMessage: jest.fn(), + errorMessage: jest.fn(), + }), + }, + }), + { virtual: true } +); + jest.mock( '@grpc/grpc-js', () => { @@ -32,7 +49,7 @@ jest.mock( return { ...original, credentials: { - createInsecure: jest.fn(() => ({ insecure: true })), + createSsl: jest.fn(() => ({ insecure: true })), }, }; }, @@ -40,6 +57,7 @@ jest.mock( ); import * as grpc from '@grpc/grpc-js'; +import { getTestCerts } from '../testCerts/testCerts'; describe('Connection', () => { beforeEach(() => { @@ -48,9 +66,11 @@ describe('Connection', () => { }); test('constructor should create connection and set base state correctly', () => { - const conn = new Connection('tok', 'peer:50051', FAKE_DIRECTION, PeerCtorMock); + const certs = getTestCerts(); + const conn = new Connection('tok', 'peer:50051', FAKE_DIRECTION); + conn.createSslTunnel(PeerCtorMock, certs); - expect(grpc.credentials.createInsecure).toHaveBeenCalled(); + expect(grpc.credentials.createSsl).toHaveBeenCalledWith(certs.caCert, certs.clientKey, certs.clientCert); expect(PeerCtorMock).toHaveBeenCalledWith('peer:50051', { insecure: true }); expect(conn.token).toBe('tok'); @@ -60,21 +80,21 @@ describe('Connection', () => { }); test('getter/setter for token should work', () => { - const conn = new Connection('old', 'peer:1', FAKE_DIRECTION, PeerCtorMock); + const conn = new Connection('old', 'peer:1', FAKE_DIRECTION); expect(conn.token).toBe('old'); conn.token = 'new-token'; expect(conn.token).toBe('new-token'); }); test('handleRevokeToken should clear token and status to UNAUTHORIZED', () => { - const conn = new Connection('secret', 'peer:x', FAKE_DIRECTION, PeerCtorMock); + const conn = new Connection('secret', 'peer:x', FAKE_DIRECTION); conn.handleRevokeToken(); expect(conn.token).toBe(''); expect(conn.status).toBe(ConnectionStatus.UNAUTHORIZED); }); test('getter/setter for status should work', () => { - const conn = new Connection('t', 'a', FAKE_DIRECTION, PeerCtorMock); + const conn = new Connection('t', 'a', FAKE_DIRECTION); conn.status = ConnectionStatus.UNAUTHORIZED; expect(conn.status).toBe(ConnectionStatus.UNAUTHORIZED); conn.status = ConnectionStatus.CONNECTED; @@ -82,12 +102,13 @@ describe('Connection', () => { }); test('getter for targetAddress should work', () => { - const conn = new Connection('t', 'host:1234', FAKE_DIRECTION, PeerCtorMock); + const conn = new Connection('t', 'host:1234', FAKE_DIRECTION); expect(conn.targetAddress).toBe('host:1234'); }); test('fetch should throw if peer client is not attached', async () => { - const conn = new Connection('t', 'addr', FAKE_DIRECTION, PeerCtorMock); + const conn = new Connection('t', 'addr', FAKE_DIRECTION); + // peerClient celowo nie jest ustawiany (brak createSslTunnel/attachGrpcClient) // @ts-ignore conn['_peerClient'] = undefined; @@ -95,9 +116,11 @@ describe('Connection', () => { }); test('fetch with defaults should work', async () => { - const conn = new Connection('t', 'addr', FAKE_DIRECTION, PeerCtorMock); + const certs = getTestCerts(); + const conn = new Connection('t', 'addr', FAKE_DIRECTION); + conn.createSslTunnel(PeerCtorMock, certs); - lastPeerClient.Fetch.mockImplementation((req: any, cb: any) => { + lastPeerClient.Fetch.mockImplementation((req: any, metadata: any, cb: any) => { try { expect(req).toEqual({ method: 'POST', @@ -105,6 +128,10 @@ describe('Connection', () => { headers: {}, body: Buffer.alloc(0), }); + + expect(metadata).toBeInstanceOf(grpc.Metadata); + expect(metadata.get('jwetoken')).toEqual(['t']); + cb(null, { status: 200, headers: {}, body: Buffer.alloc(0) }); } catch (e) { cb(e); @@ -116,16 +143,23 @@ describe('Connection', () => { }); test('fetch builds request correctly and returns response', async () => { - const conn = new Connection('t', 'addr', FAKE_DIRECTION, PeerCtorMock); + const certs = getTestCerts(); + const conn = new Connection('t', 'addr', FAKE_DIRECTION); + conn.createSslTunnel(PeerCtorMock, certs); + const body = Buffer.from('abc'); - lastPeerClient.Fetch.mockImplementation((req: any, cb: any) => { + lastPeerClient.Fetch.mockImplementation((req: any, metadata: any, cb: any) => { try { expect(req.method).toBe('PUT'); expect(req.path).toBe('/api/a'); expect(req.headers).toEqual({ 'x-a': '1' }); expect(Buffer.isBuffer(req.body)).toBe(true); expect(req.body.equals(body)).toBe(true); + + expect(metadata).toBeInstanceOf(grpc.Metadata); + expect(metadata.get('jwetoken')).toEqual(['t']); + cb(null, { status: 201, headers: { 'content-type': 'text/plain' }, @@ -142,10 +176,13 @@ describe('Connection', () => { }); test('fetch should convert Uint8Array to Buffer', async () => { - const conn = new Connection('t', 'addr', FAKE_DIRECTION, PeerCtorMock); + const certs = getTestCerts(); + const conn = new Connection('t', 'addr', FAKE_DIRECTION); + conn.createSslTunnel(PeerCtorMock, certs); + const body = new Uint8Array([1, 2, 3]); - lastPeerClient.Fetch.mockImplementation((req: any, cb: any) => { + lastPeerClient.Fetch.mockImplementation((req: any, _metadata: any, cb: any) => { try { expect(Buffer.isBuffer(req.body)).toBe(true); expect(req.body.equals(Buffer.from([1, 2, 3]))).toBe(true); @@ -160,10 +197,13 @@ describe('Connection', () => { }); test('fetch should convert string to Buffer', async () => { - const conn = new Connection('t', 'addr', FAKE_DIRECTION, PeerCtorMock); - const body = 'żółć & äöü'; // handling special chars + const certs = getTestCerts(); + const conn = new Connection('t', 'addr', FAKE_DIRECTION); + conn.createSslTunnel(PeerCtorMock, certs); - lastPeerClient.Fetch.mockImplementation((req: any, cb: any) => { + const body = 'żółć & äöü'; + + lastPeerClient.Fetch.mockImplementation((req: any, _metadata: any, cb: any) => { try { expect(req.body.equals(Buffer.from(body, 'utf8'))).toBe(true); cb(null, { status: 200, headers: {}, body: Buffer.from('{"ok":true}') }); @@ -177,24 +217,32 @@ describe('Connection', () => { }); test('fetch should reject if body is not allowed', async () => { - const conn = new Connection('t', 'addr', FAKE_DIRECTION, PeerCtorMock); - // @ts-ignore - await expect(conn.fetch({ body: { not: 'allowed' } })).rejects.toThrow('Body must be a string/Buffer/Uint8Array'); + const conn = new Connection('t', 'addr', FAKE_DIRECTION); + conn.createSslTunnel(PeerCtorMock, getTestCerts()); + await expect( + // @ts-ignore + conn.fetch({ body: { not: 'allowed' } }) + ).rejects.toThrow('Body must be a string/Buffer/Uint8Array'); }); test('fetch should propagate errors from peer', async () => { - const conn = new Connection('t', 'addr', FAKE_DIRECTION, PeerCtorMock); + const certs = getTestCerts(); + const conn = new Connection('t', 'addr', FAKE_DIRECTION); + conn.createSslTunnel(PeerCtorMock, certs); + const err = new Error('err'); - lastPeerClient.Fetch.mockImplementation((_req: any, cb: any) => cb(err)); + lastPeerClient.Fetch.mockImplementation((_req: any, _metadata: any, cb: any) => cb(err)); await expect(conn.fetch({ method: 'GET', path: '/x' })).rejects.toThrow('err'); }); test('fetch should map response', async () => { - const conn = new Connection('t', 'addr', FAKE_DIRECTION, PeerCtorMock); + const certs = getTestCerts(); + const conn = new Connection('t', 'addr', FAKE_DIRECTION); + conn.createSslTunnel(PeerCtorMock, certs); const payload = { a: 1, b: 'x' }; - lastPeerClient.Fetch.mockImplementation((_req: any, cb: any) => + lastPeerClient.Fetch.mockImplementation((_req: any, _metadata: any, cb: any) => cb(null, { headers: { 'x-k': 'v' }, body: Buffer.from(JSON.stringify(payload)), diff --git a/Tokenization/backend/wrapper/src/test/testCerts/ca.crt b/Tokenization/backend/wrapper/src/test/testCerts/ca.crt new file mode 100644 index 000000000..899799b3c --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/ca.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIUA3wFlpIAu9PcCYrsZQwml1VBbBIwDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDQyMDlaFw0zNTA5MjMyMDQyMDlaMDMxCzAJBgNV +BAYTAlBMMQ0wCwYDVQQKDARUZXN0MRUwEwYDVQQDDAxUZXN0IFJvb3QgQ0EwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDwQYsYLOQyRG5nXHLzTuAXIDJl +nV6eoAOsDItqGCiOVy9T6Y0g+4RcSf99i2DRsOVLGg/jQHpAmo765XVObPiEmE+f +rxgLclXqXNlRQdiaK9LRPIUF0TCcyFJGTVMzHTFt0jHEdiP/2++egoAA/pP1al4y +XwJjWkGgLU7GA/Eou2Gfdc7mxZ4ZtmxkEh7OEqSOVgR+qi1fTfXfTyXF+s1PG4pu +tGNCg/zTh1rlMS9kWcoTF4Bk/RU5nKZ1zOAWOgffd72JiWyoGSYy0Yk2e1fhGgG5 +cWMZCDiOCAdNgwTtVKn8IfTgoZJpXhoegMVH44CDkfb7bkp3ETGfMzKncv4v5C2P +11HjWYOrmQ1bgpy+lR9RR+7Oem9So49UsayqqAYmquOCycnGT9wOX+4qsalnZd/O +J4mHtmUGiK0Lkvfh3X5T7wE7yLuiYJtG4XYwREZkBlusxGyX0lRvWJq3lI93EFqt +p7UWtB+1OjabUpCadypzkbvA19DJ4fhzaPh+A3tfn42RnlVAYAazNRiAy90G4/Mh +MPZzKqe0DTp2i2WG5/NTEivKoSVD27vVKp5Tk4LhgAMmZ/F4uFT0TBGva4q+tEUW +jv6mLKpCtQzgqsjNCSKL5pWnymDme7UN/mGe1ttU8xrI9pyGA5tSb9kO0VkBCcVd +R1mOOjZKx0iR3ctG+QIDAQABo2YwZDASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQU41jbLgGadHiLx0921bLDou0kHQEwHwYDVR0j +BBgwFoAU41jbLgGadHiLx0921bLDou0kHQEwDQYJKoZIhvcNAQELBQADggIBAN+W +xYNn4nGtkEfNaVP46GckMQU8Lq6asXIj/L830JHq+R0gJeiURBqBs5snRqBSx6/W +3Xn+IfFGUttnEPlAkxXsg5R/JnQnggfyQX7Hk3SwedKH0uUqX7NLqAv7tfZJPGU9 +HSuvOzThvSX8N3Vr0zLVJDT94WqXX69HoSp6BZnriVss5RWMvM51QsNirj5379CX +iU7BCQdKBQfGSCQW0Qr4GWZYZHuhpXHcsfrrQ66krdqGLgkOxe3xQgvFQSWEf4OU +d/pqlRIOCnku4g2JR/ph+tuLtxmHdidNBjP27mrtrKx4MsaqimxAYOuHTkni8cLF +01IDq95txBs1fShWE5ritJh5b03ZVmDS0uiVH2IGPBmxz08ysJdUAm6uGJWg3D5X +nJBpJbqzYe6wrZDB48s0yZwo6FX5gfoAG6OR0iWfXMsOrpMOxFz/A739JjxcoFDT +P5qct/z92obgFqp0w/RN/8Dotaw00l5P1IenCE42fLuARelrS8jFKrrjUr2+0Occ +CJ/3us1j7Ln5gYWSlWHTjDRwSyaji3Gi4mnduQUsdkIpI7grh4FGULNOLOZZf3Rj +fKlP9kW5m7MB196MYjQrQXTZM1ZUY5yEeCspsb0UaD78Oq5qXSFfGFZ63BmxPMvi +RzP8neThIVB948nZ0GYMc3SIHBFvwQpFZgkuz2+0 +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.crt b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.crt new file mode 100644 index 000000000..dd67e5dac --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIExzCCAq+gAwIBAgIUG2HcUzPbDD8biqumq+ISyohyeYIwDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDQ5NDRaFw0yNjA5MjUyMDQ5NDRaMEkxCzAJBgNV +BAYTAlBMMRcwFQYDVQQKDA5FeGFtcGxlQ28gVGVzdDEhMB8GA1UEAwwYY2VudHJh +bC5zeXN0ZW0uc3ZjLmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA6PdQdC5ol4niS1fiXwiJjEhfvPIAhj+LLLaHv1VG71sS9VmnsoEVJMfGqrkO +FkHrHFRFc1UJKi1se1r4NSlPsSPmTOj9KCgp5WSTxrww6X9hniWLCgC1S2fbmrWQ +O07D/qOGpO1GRgqL1KbVdHDZxhLa4MXevRnlgd2VcY1KaXT15BTQcJRJR+I9iIJF +HkUCdrLPjoJvS2G8gRezrRVC+EgrxTfJOQ2rcUunDDhn+f//cTulWjZ/R/Jy9Byy +qFTGPiwwnLkVBQGLhBSRNEYSvzxpgxipVOTLKPZYHNKyITibno3cYiaS+qCI0GG2 +wY7jrh44I6yQ/dCYMyuu7yxT0QIDAQABo4G8MIG5MAkGA1UdEwQCMAAwDgYDVR0P +AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA9BgNVHREE +NjA0ghhjZW50cmFsLnN5c3RlbS5zdmMubG9jYWyCB2NlbnRyYWyCCWxvY2FsaG9z +dIcEfwAAATAdBgNVHQ4EFgQUYLT4W+0UJcnlfU4VQMjbSHS5gYkwHwYDVR0jBBgw +FoAU41jbLgGadHiLx0921bLDou0kHQEwDQYJKoZIhvcNAQELBQADggIBALXQVFHu +z1GDyGNUNN5fEonbv4V8MGO8RU6I0O4ccuYqVGBvDUWVSMFtT4Qc60CcXZF2V8p6 +3FP6PAmDK12mMswWuc7lvgAldxOC+PwC4K8fbu9fl+KbP8lsEikVwqiLWXWOGiRh +xlgFhzYTvgEwa3Ta5hqQPsCnY5+/ybF4l7yxZgL0Qp/OWQJgtYd8AeAWpVayhpzw +WojpJ6x56PIZI9vJ00RmMOQib5fl6e4fKKj2ACt8uorG9kL/sWId2BnCJKFjSl8d +4krZr4ocGYK+yK7KgrunqAXy/NPk1hC/oRryaSznC3oh+83P3emjuf//t0FYhSUQ +g8Urku1v9916ulTM8DsF/eSr8z6BMod60fDrpaDnSY+4hcpJuOMfN1pOWcmQVejW +TgX+pwyKpRnvIOlm7NRz+gv31xMEu/McMQQ/oC9qYh7frOsO5pHt1kI7bJ2X69Dj +rz+y7SoW4Ur/pbevfyWu4kBMdo8Dj1zF2GwYFHDzjU0R814fBHEnkN5Jxnk+ahYl +yNwWnjphabPSGRx3nIgCJ600HvgAK1uKgdTCRoiYhkDxcve3m5wEE9UCgLrcp8HL +ushY02iXu9TsPuIA/3bBeLVeI0JxnyxYjP1YuGF2i2fxF/rZxhVXl12xc2oJAytA +6CRc2j0K3JcjgG5jdt3H36LwPnvQ9HPfF200 +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.key b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.key new file mode 100644 index 000000000..22044072d --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/centralSystem/central-system.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDo91B0LmiXieJL +V+JfCImMSF+88gCGP4sstoe/VUbvWxL1WaeygRUkx8aquQ4WQescVEVzVQkqLWx7 +Wvg1KU+xI+ZM6P0oKCnlZJPGvDDpf2GeJYsKALVLZ9uatZA7TsP+o4ak7UZGCovU +ptV0cNnGEtrgxd69GeWB3ZVxjUppdPXkFNBwlElH4j2IgkUeRQJ2ss+Ogm9LYbyB +F7OtFUL4SCvFN8k5DatxS6cMOGf5//9xO6VaNn9H8nL0HLKoVMY+LDCcuRUFAYuE +FJE0RhK/PGmDGKlU5Mso9lgc0rIhOJuejdxiJpL6oIjQYbbBjuOuHjgjrJD90Jgz +K67vLFPRAgMBAAECggEABeW00KwYE7X214drAJLbwIRYgBT0NHHJWSFpwEstV4PL +sBBL8XXZDixMeCflFmUmyXnMpEXDzKCHvXupCtd33/kTrGC9f9W8ccUhBIfhCRgj +ZXh305H/BOClK35rH0U4KusCzov/GmjL718lyiPNL3lstwHrSIguSiJM1SoJdy/l +aCIif6v5l8/DItDSavQxgI97AC0u7lLJadB460XqeJi3vYPzBg6WxEMMdqRhzzOH +1XIsv+IzHabJmt5J3wFsv2lk1v0Irny+CtWtZTM5mTVr4FcgefNx0t8pVSxRXf9F +DjXXbTSrlPVjZVPENrAr6Sl5YyJeK/UABiRl/BLxDQKBgQD3SQ0qZw41JsCFj6W7 +DGyKwFVNvFibzO2Hb6grHwV3iJKHnppFFpanEMhLiZzgdeTGpQFpR4iM6Ne8ewFQ +zu2P91cGjMBP4HqP/RWtGStZ6X/Br2I66sra6BXTsXGNXSr330xzYiwbDrKtr7rv +Q79ySfRRlwWpTeT6RubbqxB3dwKBgQDxLRPlVYaALeJ1rxuds59NeC5bJGHS8+1h +kiXQgKmG1/5saLUyWXWAT5FJqG/xvtiN+vtmf8jL+KhUYpga0kJeuykR7loJRGdr +7h4uMYmzrP7+5P6tNOBqawGCDZzutXMq2TIJAwy1s+tVE4KWWcv0qE8op4on+/f2 +8A+5HWNw9wKBgHGjU3aJ92B7l3uJQMsNcY/txQW9KScn7HwR1sFCNzvwOg4y14gq +Uj8iGjmEWuBXrTOQPm7IHbtLgWCvUjJ1dXx0WLy8z9+lNA2Za22ppF9kS36Rf129 +6kzg3K70207wYr+YEUTw933Tqk7g89HiW0dFLw6TjVl5X2GYVZzbJu0PAoGBAL6b +BLVkIXeeS/L8YJQDSOx+BgzsNQ/2zm4lhhNCDDlQ7XgaTNItF4s/1zBimY5yaU3U +xOmeJkDmFYsTnOjdsaySuIO+X5QhZqdLOrkBV7YUDDfBHXIgbxhL15ZEUfnql8mO +fFfY/CuCtYO4dqWC9Ik4l88mki7FmZSk55hCnLvDAoGBAKEjQS4GVd+9Ku/HVi71 +OG2vfKEyGfTyyOB/3c99BMWNOkQfMNrxuHR8XhTPi2LvFE9nndBlMuCLcRS417iL +Gvd7FazAaO1lRO1tlqOym1z2gx/j2k+2BrIV+vOzg3PEJeXZAihLHTSsfhNIc4xW +ZeR3z5nE4Pz9AOr/YFKi9Xqm +-----END PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-client.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-client.crt new file mode 100644 index 000000000..08482178c --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-client.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEuDCCAqCgAwIBAgIURLvrnhcyzZ0UTbuI1tHGoXXFWc0wDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDU2MTdaFw0yNjA5MjUyMDU2MTdaMEYxCzAJBgNV +BAYTAlBMMRcwFQYDVQQKDA5FeGFtcGxlQ28gVGVzdDEeMBwGA1UEAwwVY2xpZW50 +LWItY2xpZW50LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +xxqgLBJFeQgLcRvkPj6S16guSJsJN/MJxGAnmWQF3utr9neUXeID2KUEMNUT2b73 +EO/LD3z+8JvDhfG4J9Y1/ny/CuaAbmHKXH/ot0nIwWjd11yXjhWyXQuNqGHiuniX +2KaDH5+rIPXYU6eUjx+V3VX0iiYEFUizPRUhzmJ8AONSu43OuMdhZo7Frb4qUc4/ +ioVqhiAb3Sdm/nKDWI9OQR6Ux8Mc2OaPY3wQA4r3ZBz9oJU3G7BL85bclmk1PDZh +R6T0/oo09FiTj8zTZ8vooarJH+TAD2EBsTwfIdBT1yuECAueztxaos2q2TUsJz3f +6hQaEnOqiz6nZ8FceXudgQIDAQABo4GwMIGtMAkGA1UdEwQCMAAwDgYDVR0PAQH/ +BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMDsGA1UdEQQ0MDKCFWNsaWVudC1i +LWNsaWVudC5sb2NhbIIIY2xpZW50LWKCCWxvY2FsaG9zdIcEfwAABTAdBgNVHQ4E +FgQUTzAFrkegQMRXfjt4djXC5N4YHnYwHwYDVR0jBBgwFoAU41jbLgGadHiLx092 +1bLDou0kHQEwDQYJKoZIhvcNAQELBQADggIBAB+vXi5ThGYQt7RUx92Vjy2++Yiz +WztajTms/ac2B2JZbW9dl1PYv+t5hrkiXp8Q21eWq7ZPbd+7QbzvQsOxSGEKNBQp +0A4CeI67YkbYjJPzxjpDxiYo8+wmOtGgWRok9P4FLN5dGVo9/3JQHrn+3m20/6DT +FqTqm+mC3FkSjAJzPnd1wOmPxS2xENE/L5x4KHR91xkCeaHXjQky9gQs7vzY4uto +kiuB/elxFq2l4XN4BI6A65261AyelB15qpTKBGICGNO56hIcrGIjoFyYQnKyJrk4 +yylH0LtACnEV0lzS9l7FtRNERQ5xDBRvcqK7X7XCwkKqXfYN04STiVEEqhSoYDF2 +INJeV2FN7CJQueCuiKqG1S+zd7uXJ9a7dx5Qk3boJVBfX2WjK5BbiSpjoq+x7tCG +zwtHpBDaB8K6Ee0XICFcDZlPB214XaGjrx6iavs5ppP6261f20QMI3xtTVPapMjT +bmC2AoARTdaIxoQaSyCE+QImVxkhHYBePpIHAnAZhBPzF46u+APRAOpklTJ6J+uv +VWCwm71ebXUTSZ7alsScqH+zDmYMYpGuar3qcPHnOJDo0XDoSMYl3kLDrK0UoHci +mtjRzuYBlKyEX8LcqY9teVuarnvxaNtYGbF98L3nFSLGnUfzaCMIbA4xtJcZ7DXu +J9l3OBcmHYWG52f2 +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-server.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-server.crt new file mode 100644 index 000000000..949b07297 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b-server.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEqjCCApKgAwIBAgIUSKJ0QtJi4yK32QhGXdgKbfADC3owDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDU2MDhaFw0yNjA5MjUyMDU2MDhaMD8xCzAJBgNV +BAYTAlBMMRcwFQYDVQQKDA5FeGFtcGxlQ28gVGVzdDEXMBUGA1UEAwwOY2xpZW50 +LWIubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHGqAsEkV5 +CAtxG+Q+PpLXqC5Imwk38wnEYCeZZAXe62v2d5Rd4gPYpQQw1RPZvvcQ78sPfP7w +m8OF8bgn1jX+fL8K5oBuYcpcf+i3ScjBaN3XXJeOFbJdC42oYeK6eJfYpoMfn6sg +9dhTp5SPH5XdVfSKJgQVSLM9FSHOYnwA41K7jc64x2FmjsWtvipRzj+KhWqGIBvd +J2b+coNYj05BHpTHwxzY5o9jfBADivdkHP2glTcbsEvzltyWaTU8NmFHpPT+ijT0 +WJOPzNNny+ihqskf5MAPYQGxPB8h0FPXK4QIC57O3FqizarZNSwnPd/qFBoSc6qL +PqdnwVx5e52BAgMBAAGjgakwgaYwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBaAw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwNAYDVR0RBC0wK4IOY2xpZW50LWIubG9jYWyC +CGNsaWVudC1igglsb2NhbGhvc3SHBH8AAAQwHQYDVR0OBBYEFE8wBa5HoEDEV347 +eHY1wuTeGB52MB8GA1UdIwQYMBaAFONY2y4BmnR4i8dPdtWyw6LtJB0BMA0GCSqG +SIb3DQEBCwUAA4ICAQAfNbYmzhNKTJT+e4VZaJdqxFmsm2oHUtRXHVKHcPKYZEd5 +ujKtIjbdhjQ82Rhfmof9cydvAK8qEm+ydwUBvN/9q7Dd4V3rafKbsrVizB63HbSl +AZujvRxwIKF9Gzc3Sqliy1/LZYfk+FHHooUtzmL/K5cTVlHaBqT8m4zmqp4djFjQ +YnshmdaMBmgmgluO4/JyPswFHpRlKcp29GA8n39/+25yFyiIunryypCwPAFb/owh +sXVshhs04+JUwEdWGHoesbhjbIik706poPOlvUf9xHDcB6PXIwPmo08+1u2QEaV3 +Dqw4TjNcUA7OJdxzKhF0J4tVXAD1Hg2yrOYedtTeXDPntjgb3Uq4DWnAAJ+fMF1U +T1vJogzgzq6y5jl0KClqpSA9dOKt4IG2hL5WcoudyTk0ao4wkVdOQwR1vNz25Fub +LSl582PpHxvK3GYk4PegoPnHzz02IN4B+AxUDvXPH4HIZ4QtubnX4x4j8Kk4bPCK +ZBBA3t8K/6W/bjOB0Xh+LyXf9dIndVmFHC0iOf8YQgyNHhXhzpbpM/LWdDk3aEiG +6y99wNdWuu7F3T7wFk7dDhxxroHqxUjsP8921LD0JDqdryWQK6wE23fSpiThOEDN +o2FnBG3514QI/v6zlUWY4LtqJO2UCwPLbWAsuXd+AxWOO3urg0ucW0r1UNzmIQ== +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.key b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.key new file mode 100644 index 000000000..b2fc830da --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHGqAsEkV5CAtx +G+Q+PpLXqC5Imwk38wnEYCeZZAXe62v2d5Rd4gPYpQQw1RPZvvcQ78sPfP7wm8OF +8bgn1jX+fL8K5oBuYcpcf+i3ScjBaN3XXJeOFbJdC42oYeK6eJfYpoMfn6sg9dhT +p5SPH5XdVfSKJgQVSLM9FSHOYnwA41K7jc64x2FmjsWtvipRzj+KhWqGIBvdJ2b+ +coNYj05BHpTHwxzY5o9jfBADivdkHP2glTcbsEvzltyWaTU8NmFHpPT+ijT0WJOP +zNNny+ihqskf5MAPYQGxPB8h0FPXK4QIC57O3FqizarZNSwnPd/qFBoSc6qLPqdn +wVx5e52BAgMBAAECggEAA0i8JZ3ziWiJj8cO/7vWfjom8UmlYEfg/F09qfkNY7zs +Xfdg+h91QsiOBiQtnKTavGvIJKxCJEPdeMMg739ICreSCyL8MVXpmZb+hq9v4UjS +h+/eDBjthT1gi8t5iuvcTVWJyia/Et8bP13/RFEYDruROgogfR1i33oOwbG8K+OM +iDSIRo9swBaVNqFuWZKqZr1pjY9KUfh4jnSA+x1hueVHREJYt6qvX2KNhHFhyfyo +RRuLSKmz7Xx3sTu7qQD1NRKsD/8lM4oZWGPTit0F6qx+FfzbI0nBn9In8czmrPcm +VqwdgT5aR5I6lfzAj6kMHlmY2n0f4J0Hk6Ha6MgkJQKBgQD6AnjeToUw6PcIbTrJ +18mDfzOCMhQjQHDXipHrJ06K65B+4XX8/oTkRahyeqn4sbKKHP11e6bsDPVNRK0P +9G3wYw7nPNcGtF1pojWvh0HxBTJ/iugDgZT5ngtwbydvAT7+m6De3DicEJo5ZUPP +IAweL+qf9Nh30PtKiOSgJ2GrRwKBgQDL3+eX0sQg+dfYb3zAJmsLwXoY4fAOGc1o +KI4UI26Bq4TPrJv9guaDjSYNE62M7+H7vWudlG+KPxt6jNaxxpIo5J1c7qVl+gx6 +kTDZ053peVLtWJzLrRT9/e238bCYnKfFpKwRzUGT9kEkMvmFqb56lnhKBXS/OPuP +3dAOWHvE9wKBgAJM4YXSHSGdEyDNuHvA84a1NekdwtesMR2alcsfGnbmwfaY5ngE +c36SMYGUJVo3cFga+i4JjDihyeQDHMCH1DchAjMYeTYDlNRy/KF30iCAlr1brtTR +bWh6jspjC27XCRhYoDtMtWyiLnkWuHAAcHwansMIArHfh2BhMBFVK23jAoGAGwrf +GFdfppQdWlsnbAFsj4mhXW2SvvwTL+65MdilTtPmcPmPU2gqlWaClpd2nMww6Ihu +nt9SkD7gsTe/PqN9PaldajdJfyZUw2lA1pPoTVDHfC4V1jpmH26wOob3ira01lWK +cW4NdcfjSh7s1Br45h/RYtgobTjsvV+Jum1oNW8CgYAeY82AEydYzqjZRH66xAjQ +pSxaT6B2YS5JqJFbRAtHA/ndApHbW7ALX5vqoJ6EkJ+aORQQMZUkX32n3Y0WFNE2 +FR9JEcnBjimwmG+JTBRCk1luamsZZpH/sHZohH1bsq9+dGo9xIeeBdi5DPxpdsKM +6NuWljVhDbuJeRiamlo5tg== +-----END PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.pub.pem b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.pub.pem new file mode 100644 index 000000000..cf74b90e7 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientListener/client-b.pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxxqgLBJFeQgLcRvkPj6S +16guSJsJN/MJxGAnmWQF3utr9neUXeID2KUEMNUT2b73EO/LD3z+8JvDhfG4J9Y1 +/ny/CuaAbmHKXH/ot0nIwWjd11yXjhWyXQuNqGHiuniX2KaDH5+rIPXYU6eUjx+V +3VX0iiYEFUizPRUhzmJ8AONSu43OuMdhZo7Frb4qUc4/ioVqhiAb3Sdm/nKDWI9O +QR6Ux8Mc2OaPY3wQA4r3ZBz9oJU3G7BL85bclmk1PDZhR6T0/oo09FiTj8zTZ8vo +oarJH+TAD2EBsTwfIdBT1yuECAueztxaos2q2TUsJz3f6hQaEnOqiz6nZ8FceXud +gQIDAQAB +-----END PUBLIC KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-client.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-client.crt new file mode 100644 index 000000000..72ddb5561 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-client.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEuDCCAqCgAwIBAgIUTk9RrYkrtmjMiKjWusjyURgmSUEwDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDU1MTVaFw0yNjA5MjUyMDU1MTVaMEYxCzAJBgNV +BAYTAlBMMRcwFQYDVQQKDA5FeGFtcGxlQ28gVGVzdDEeMBwGA1UEAwwVY2xpZW50 +LWEtY2xpZW50LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +nJySLgwWLpLbt8EqFDfhKciKJGlxRUfcn/2u2EyxnkpKwwiLDSzKguZZ50xKviDt +Jao2FuZ251uyJDTfuPYt549OklkKIQYFSmP4MxNDAz501TVSJ45a9WQugScGc9lk +invmIADGBEa0rj2keRkT4MYvnWT2IGQJ91N99g9tDoQVPem5naHU1PxIwyxVVRIv +6mVCaro6OULqx6iFvDffvL0ef/5lbt8+vqX4QWwPH8rF5CvaV1KYYMkvQEXSn615 +8FE2YUepEN6wGEEDAIr98D5vNlgabkpLzxDULFT+tdk7v6Shb7bKql1W0QlBkEh/ +mFDCEkqOesGXLuzPG8pVAwIDAQABo4GwMIGtMAkGA1UdEwQCMAAwDgYDVR0PAQH/ +BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMDsGA1UdEQQ0MDKCFWNsaWVudC1h +LWNsaWVudC5sb2NhbIIIY2xpZW50LWGCCWxvY2FsaG9zdIcEfwAAAzAdBgNVHQ4E +FgQUScQWL/kot0/d3H9JQ9sZsO3xniAwHwYDVR0jBBgwFoAU41jbLgGadHiLx092 +1bLDou0kHQEwDQYJKoZIhvcNAQELBQADggIBALVJfl1YWb3mrXT51+pZ4pYJfY0Z +iXCGAMVLUtdnhgMmX5GVJhWvCAt+vwHggOJ+EA+1vw5CRe/GLDa5QurOtLtH6uBt +OjenYmOKRZ5f+mG8ZR20PyhH70m8DZ01OGlfuieFb8KgyYtEFNLCFZWxVDlvdwNL +2HVPSjM0JYudBELnTs4N8YZUTLGDRDZ9sz/KQYMJrSojN45k05qqr/EXWHwVDzVL +LefvuKi6H6DLzmU+oDy9TRcCydV4/h6i3MUxWm/IBrgoKIg4fmd/Evnen9KSc6md +yMHoKR6iHcha526txtUu4w0/0Le45yYxE1/eNm5jfMNwTFuoTbmlk+lItXd5Upn7 +1Pk+TF81WLl8prdvVoZPVYaKbj80JQlleQlWu76SaaY9ofkbRwP85npJmdl0wDCu +Bmil+ziBxzK73TT+UMBbRKRmXdeEh3fjQ8X4qlRNVPEarj1f2UiZ+45G/eFogA2r +EtVTqQ5MetNtWssgK0GFf2KeUIfXRdvYuFvLOhcd7uccxThq+o9KDFIulPzhy6uq +nu2AS8NELydQeHh6GjKqsxNoMS5l+YSzTGPvFWTYqzfRH5+h2J0H8Oex2Grb5C9A +35F8f35zLViv8C9mU32W9bSgcJElKaOumgBLbRtfrzHesFBFyOkTbtWnKJJJwXQH +7QZyDraKRsXdHAMr +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-server.crt b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-server.crt new file mode 100644 index 000000000..23fc77a5d --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a-server.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEqjCCApKgAwIBAgIULDdlrJiTv9xZIx3QqggR9bBIgwswDQYJKoZIhvcNAQEL +BQAwMzELMAkGA1UEBhMCUEwxDTALBgNVBAoMBFRlc3QxFTATBgNVBAMMDFRlc3Qg +Um9vdCBDQTAeFw0yNTA5MjUyMDU1MDJaFw0yNjA5MjUyMDU1MDJaMD8xCzAJBgNV +BAYTAlBMMRcwFQYDVQQKDA5FeGFtcGxlQ28gVGVzdDEXMBUGA1UEAwwOY2xpZW50 +LWEubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcnJIuDBYu +ktu3wSoUN+EpyIokaXFFR9yf/a7YTLGeSkrDCIsNLMqC5lnnTEq+IO0lqjYW5nbn +W7IkNN+49i3nj06SWQohBgVKY/gzE0MDPnTVNVInjlr1ZC6BJwZz2WSKe+YgAMYE +RrSuPaR5GRPgxi+dZPYgZAn3U332D20OhBU96bmdodTU/EjDLFVVEi/qZUJqujo5 +QurHqIW8N9+8vR5//mVu3z6+pfhBbA8fysXkK9pXUphgyS9ARdKfrXnwUTZhR6kQ +3rAYQQMAiv3wPm82WBpuSkvPENQsVP612Tu/pKFvtsqqXVbRCUGQSH+YUMISSo56 +wZcu7M8bylUDAgMBAAGjgakwgaYwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBaAw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwNAYDVR0RBC0wK4IOY2xpZW50LWEubG9jYWyC +CGNsaWVudC1hgglsb2NhbGhvc3SHBH8AAAIwHQYDVR0OBBYEFEnEFi/5KLdP3dx/ +SUPbGbDt8Z4gMB8GA1UdIwQYMBaAFONY2y4BmnR4i8dPdtWyw6LtJB0BMA0GCSqG +SIb3DQEBCwUAA4ICAQAKtTGDurzAkajMDN+WqhiA6daqIstsRzLz9VnBwqIlWcOr +c1As4ah+YZSf2Qw1AMZ387fpk4oF2QZD4ZG7kigZdn5ricFVhBRMZUzJV1ommu2H +8Mub+oRyKQ/TtRqkq1JJqLKz7rDxBMM9LxSBPR4Nj2C4IVioxI5KYXYxlmqMeoYA +sMglGi8c3loRSy9LNwvcQu+UPI6kcFG+J0rfXJlWx10GRWIURudXt8oAAIVBLvSt +HR29TXWjOTULwqun0y5V4eksJek5jEhGTWuODAdPmCSSjAE4VSLECex/jql6jNFB +zmE9Q7vcss4zR9TASMeJYT3S+mXVb9sNf4ps+9rhx63tluSCH1vwtpMoQXucbIgo +tBUz+5gCIA7n1bMUJ8b1MajnTVH0nJa1ZWi0zTYnSd6WL0S0Se5exZ5Ws1ZWnFl9 +lVPCn2Mt8agRu0s0VAT7t4nY4VjHTDqjj9Z99tcfUWCO8gAAR28kkqdRYxrgVMkx +pv8IwTt0tBldDnpwdCqBnXP75sta4Gq7IOpe0oQB6kizWqbII84tYSxUch9SkkaH +rE7BhGtUywAJxc+dnAFuePuu6BE2ZsQK86FpuHYIxR6DU7hH1i8258qxGt0/EVBg +ekhyT6tFaAWl5N+OVmEu1JvNdqNiw6sJc+xy9AcviWlAOvkGd2Aw0eTRNQLboQ== +-----END CERTIFICATE----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.key b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.key new file mode 100644 index 000000000..7377803ae --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCcnJIuDBYuktu3 +wSoUN+EpyIokaXFFR9yf/a7YTLGeSkrDCIsNLMqC5lnnTEq+IO0lqjYW5nbnW7Ik +NN+49i3nj06SWQohBgVKY/gzE0MDPnTVNVInjlr1ZC6BJwZz2WSKe+YgAMYERrSu +PaR5GRPgxi+dZPYgZAn3U332D20OhBU96bmdodTU/EjDLFVVEi/qZUJqujo5QurH +qIW8N9+8vR5//mVu3z6+pfhBbA8fysXkK9pXUphgyS9ARdKfrXnwUTZhR6kQ3rAY +QQMAiv3wPm82WBpuSkvPENQsVP612Tu/pKFvtsqqXVbRCUGQSH+YUMISSo56wZcu +7M8bylUDAgMBAAECggEANyBDsjKt8inebjFvmttKhfchbQyygsT3S1ez3k4srT+Q +TlNpArOz+tyTY7+uhXs4jmv6CxiHXQuhSm5UG5qH8Py4FvqBfrtMTHGg8XWDvpYS +8OOKbgMFUGA5oFt4wXmRks9m4vfyu5mZysVG6htiLFoGc5wQqLkd6vF4Io8uf4+A +Dd/oY85K7x/JnrNQaF7LmpeqTMRlhtzSzGNVqEwT9b9FoQRADGHrg1ksY5xXixAw +Jf+wvJqd6AaoxzK0rDuZiQpVnjepJ6aeip61RL7coc+yAlkXhfDOWvzWCefRFUTf +Q/iTyOset8ejRg2mF8InLUKYC2kwKLeucVvFkijz2QKBgQDbgf5L2aK/lUEQVtQf +ueqCc89PJZHU34tEXRC0doGYLQnhZQPVvLeTv+CkZ7GibUxPiMLdtzzfyFxlLbs+ +gUxBXV540hI3acpDqdiiZmHeYtMPvyjUkkP1ymQSnLwwjOkBh7Wt/+zMBEHtO8hz +Mo5vfoV6JV+b+JQg2f7cyd1t2wKBgQC2pclNJ1Dj58Vgmfd2EZhZgG0XRVPfR7PT +QjEIcnFmmNvEiN83dYqYw8fVOegcXrCFMTP3aW6ONoGnk+owhTSBtEfYNCKOwIi9 +GHM+MJFNIgxxPM+xSvpxHtAF9meYFkMRqjMJPM8ICz04Uz3AsfbdveYwUNMudSap +znigNKfh+QKBgDCvvH+GXhqwOCYvnA0NZ35XwXuEkbvteS5IlhPw1P2zv6VGins1 +yGH1BRZyCWxFYc+iPdZ/dfkMr7GhWw6aDxfQZcvWjEPOKxam7W3X141DzhyIAb5k +Ur6JjXizWupJ1sSIHTvir9rwds7vm54xcHY6UdCtyW8Gy5QdxfGitIJRAoGAbJ2I +aT5RJ0bEJJ9K/saV39u0hBsxNl2QfbgmKozMDSQnxOdUPsnCgvgiVRXbh0t0E7Df +42iqWx3k2n/my7XbNKq98r+GMXgjmLf6iGgfcEwoNAriw97/sdeOA421q0bJ2a5q +LTshLvpoDJ/L4FS0psbwJZlbDIyUUnS7XSITGBkCgYBNvMefT86P85yN5nDyxRjO +lCitw08NjE+6WZWL6BTbRqVovFG2HAaWGEjG2+bpApy4S4AN3NmmOby38ZZDR7bJ +bQxCHRt61yqX6IphkRrzv8K7DbrN3jnKO2FN8TBWwcvFzx2d5hl8Nsv2OmEyixZZ +ySbU4WOBCdu/mJy/+Xb9gA== +-----END PRIVATE KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.pub.pem b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.pub.pem new file mode 100644 index 000000000..f565de4a2 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/clientSender/client-a.pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnJySLgwWLpLbt8EqFDfh +KciKJGlxRUfcn/2u2EyxnkpKwwiLDSzKguZZ50xKviDtJao2FuZ251uyJDTfuPYt +549OklkKIQYFSmP4MxNDAz501TVSJ45a9WQugScGc9lkinvmIADGBEa0rj2keRkT +4MYvnWT2IGQJ91N99g9tDoQVPem5naHU1PxIwyxVVRIv6mVCaro6OULqx6iFvDff +vL0ef/5lbt8+vqX4QWwPH8rF5CvaV1KYYMkvQEXSn6158FE2YUepEN6wGEEDAIr9 +8D5vNlgabkpLzxDULFT+tdk7v6Shb7bKql1W0QlBkEh/mFDCEkqOesGXLuzPG8pV +AwIDAQAB +-----END PUBLIC KEY----- diff --git a/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts new file mode 100644 index 000000000..3e2f3ef93 --- /dev/null +++ b/Tokenization/backend/wrapper/src/test/testCerts/testCerts.ts @@ -0,0 +1,112 @@ +import { CentralSystemConfig, gRPCWrapperConfig } from "models/config.model"; +import path from "path"; +import * as fs from "fs"; + +export const getTestCentralCertPaths = + (): CentralSystemConfig["serverCerts"] => { + const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); + const SERVER_CERT_PATH = path.join( + __dirname, + "./centralSystem/central-system.crt" + ); + const SERVER_KEY_PATH = path.join( + __dirname, + "./centralSystem/central-system.key" + ); + + return { + caCertPath: CA_CERT_PATH, + certPath: SERVER_CERT_PATH, + keyPath: SERVER_KEY_PATH, + }; + }; + +export const getTestClientListenerCertPaths = + (): gRPCWrapperConfig["clientCerts"] => { + const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); + const CLIENT_CERT_PATH = path.join( + __dirname, + "./clientListener/client-b-client.crt" + ); + const CLIENT_PRIVATE_KEY_PATH = path.join( + __dirname, + "./clientListener/client-b.key" + ); + const CLIENT_PUBLIC_KEY_PATH = path.join( + __dirname, + "./clientListener/client-b.pub.pem" + ); + + return { + caCertPath: CA_CERT_PATH, + certPath: CLIENT_CERT_PATH, + privateKeyPath: CLIENT_PRIVATE_KEY_PATH, + publicKeyPath: CLIENT_PUBLIC_KEY_PATH, + }; + }; + +export const getTestClientListenerServerCertPaths = + (): gRPCWrapperConfig["clientCerts"] => { + const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); + const CLIENT_CERT_PATH = path.join( + __dirname, + "./clientListenerServer/client-b-server.crt" + ); + const CLIENT_PRIVATE_KEY_PATH = path.join( + __dirname, + "./clientListener/client-b.key" + ); + const CLIENT_PUBLIC_KEY_PATH = path.join( + __dirname, + "./clientListener/client-b.pub.pem" + ); + + return { + caCertPath: CA_CERT_PATH, + certPath: CLIENT_CERT_PATH, + publicKeyPath: CLIENT_PUBLIC_KEY_PATH, + privateKeyPath: CLIENT_PRIVATE_KEY_PATH, + }; + }; + +export const getTestClientSenderCertPaths = + (): gRPCWrapperConfig["clientCerts"] => { + const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); + const CLIENT_CERT_PATH = path.join( + __dirname, + "./clientSender/client-a-client.crt" + ); + const CLIENT_PRIVATE_KEY_PATH = path.join( + __dirname, + "./clientSender/client-a.key" + ); + const CLIENT_PUBLIC_KEY_PATH = path.join( + __dirname, + "./clientSender/client-a.pub.pem" + ); + + return { + caCertPath: CA_CERT_PATH, + certPath: CLIENT_CERT_PATH, + privateKeyPath: CLIENT_PRIVATE_KEY_PATH, + publicKeyPath: CLIENT_PUBLIC_KEY_PATH, + }; + }; + +export const getTestCerts = () => { + const CA_CERT_PATH = path.join(__dirname, "./ca.crt"); + const SERVER_CERT_PATH = path.join( + __dirname, + "./centralSystem/central-system.crt" + ); + const SERVER_KEY_PATH = path.join( + __dirname, + "./centralSystem/central-system.key" + ); + + const caCert = fs.readFileSync(CA_CERT_PATH); + const clientCert = fs.readFileSync(SERVER_CERT_PATH); + const clientKey = fs.readFileSync(SERVER_KEY_PATH); + + return { caCert, clientCert, clientKey }; +}; diff --git a/Tokenization/backend/wrapper/src/utils/connection/peerListener.ts b/Tokenization/backend/wrapper/src/utils/connection/peerListener.ts index 5fafb4f1d..18a3238e0 100644 --- a/Tokenization/backend/wrapper/src/utils/connection/peerListener.ts +++ b/Tokenization/backend/wrapper/src/utils/connection/peerListener.ts @@ -12,9 +12,11 @@ * or submit itself to any jurisdiction. */ import * as grpc from '@grpc/grpc-js'; -import { Connection } from '../../client/connection/Connection'; -import { ConnectionDirection } from '../../models/message.model'; +import type { ConnectionDirection } from '../../models/message.model'; import { ConnectionStatus } from '../../models/connection.model'; +import type { Connection } from '../../client/connection/Connection'; +import { gRPCAuthInterceptor } from '../../client/connectionManager/interceptors/grpc.auth.interceptor'; +import type { SecurityContext } from '../security/SecurityContext'; /** * Listens for incoming gRPC requests and forwards them to the local API endpoint. @@ -24,7 +26,7 @@ import { ConnectionStatus } from '../../models/connection.model'; * @param callback - The callback function to be called with the response. * @param logger - The logger object to write info and error messages. * @param receivingConnections - The map of existing incoming connections. - * @param peerCtor - The constructor function for the peer client. + * @param createNewConnection - Function to create a new Connection instance. * @param baseAPIPath - The base path of the local API endpoint. */ export const peerListener = async ( @@ -32,21 +34,24 @@ export const peerListener = async ( callback: grpc.sendUnaryData, logger: any, receivingConnections: Map, - peerCtor: any, + createNewConnection: (address: string, direction: ConnectionDirection, token?: string | undefined) => Promise, + securityContext: SecurityContext, baseAPIPath: string ) => { + // Run auth interceptor + const { isAuthenticated, conn } = await gRPCAuthInterceptor(call, callback, receivingConnections, securityContext); + + if (!isAuthenticated || !conn) { + // Authentication failed - response already sent in interceptor + return; + } + try { const clientAddress = call.getPeer(); logger.infoMessage(`Incoming request from ${clientAddress}`); - let conn: Connection | undefined = receivingConnections.get(clientAddress); - - if (!conn) { - conn = new Connection('', clientAddress, ConnectionDirection.RECEIVING, peerCtor); - conn.status = ConnectionStatus.CONNECTED; - receivingConnections.set(clientAddress, conn); - logger.infoMessage(`New incoming connection registered for: ${clientAddress}`); - } + conn.status = ConnectionStatus.CONNECTED; + receivingConnections.set(clientAddress, conn); // Create request to forward to local API endpoint const method = String(call.request?.method ?? 'POST').toUpperCase(); diff --git a/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts b/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts new file mode 100644 index 000000000..88685a30c --- /dev/null +++ b/Tokenization/backend/wrapper/src/utils/security/SecurityContext.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * @description Stores every keys and certificates needed for gRPC mTLS communication and token verifications (JWE/JWS) + */ +export class SecurityContext { + // Keys for mTLS (RSA) + public readonly caCert: Buffer; + public readonly clientSenderCert: Buffer; + public readonly clientListenerCert?: Buffer; + public readonly clientPublicKey: Buffer; + // RSA Private Key (PKCS8) for JWE decryption + public readonly clientPrivateKey: Buffer; + + // Public Ed25519 key for JWS verification + public readonly JWS_PUBLIC_KEY: string; + + /** + * Initializes an instance of SecurityContext class. + * + * @param caCert - The root Certificate Authority (CA) certificate used for mTLS. + * @param clientSenderCert - The client certificate used for mTLS. + * @param clientPrivateKey - The client private key (PKCS8) used for JWE decryption. + * @param clientPublicKey - The client public key used for JWE encryption. + * @param clientListenerCert - The client listener certificate (optional) used for mTLS. + * @param JWS_PUBLIC_KEY - The public Ed25519 key used for JWS verification (optional, default value is provided if not set). + */ + constructor( + caCert: Buffer, + clientSenderCert: Buffer, + clientPrivateKey: Buffer, + clientPublicKey: Buffer, + clientListenerCert?: Buffer, + JWS_PUBLIC_KEY?: string + ) { + this.caCert = caCert; + this.clientSenderCert = clientSenderCert; + this.clientPrivateKey = clientPrivateKey; + this.clientPublicKey = clientPublicKey; + + if (clientListenerCert) { + this.clientListenerCert = clientListenerCert; + } + + if (JWS_PUBLIC_KEY) { + this.JWS_PUBLIC_KEY = JWS_PUBLIC_KEY; + } else { + this.JWS_PUBLIC_KEY = 'hTb3l5gwoIWISOLi6cQMwcultawKyA6vxnimXWtE6JI='; + } + } +}