From 1c6c40fa8f9cd6a5a0ec858c8cbca8db2b371b00 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 28 Dec 2023 11:17:33 -0700 Subject: [PATCH 01/16] use ts-node 10.9.2 --- configs/mocha-config-devtools/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/mocha-config-devtools/package.json b/configs/mocha-config-devtools/package.json index 31d69def..afa33754 100644 --- a/configs/mocha-config-devtools/package.json +++ b/configs/mocha-config-devtools/package.json @@ -40,7 +40,7 @@ "jsdom": "^22.1.0", "react-16-node-hanging-test-fix": "^1.0.0", "sinon-chai": "^3.7.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "why-is-node-running": "^2.2.2" } } From 4096e976676f89f073b4724b638c53d467f1ebd6 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 28 Dec 2023 10:26:05 -0700 Subject: [PATCH 02/16] move signing utils into devtools-shared --- package-lock.json | 222 ++++++- packages/garasign-utils/.depcheckrc | 8 + packages/garasign-utils/.eslintignore | 2 + packages/garasign-utils/.eslintrc.js | 8 + packages/garasign-utils/.mocharc.js | 1 + packages/garasign-utils/.prettierignore | 3 + packages/garasign-utils/.prettierrc.json | 1 + packages/garasign-utils/LICENSE | 557 ++++++++++++++++++ packages/garasign-utils/bin/sign.js | 25 + packages/garasign-utils/package.json | 71 +++ packages/garasign-utils/src/garasign.sh | 54 ++ packages/garasign-utils/src/index.spec.ts | 0 packages/garasign-utils/src/index.ts | 18 + .../src/signing-clients/index.ts | 18 + .../local-signing-client.spec.ts | 48 ++ .../signing-clients/local-signing-client.ts | 48 ++ .../remote-signing-client.spec.ts | 95 +++ .../signing-clients/remote-signing-client.ts | 109 ++++ .../garasign-utils/src/ssh-client.spec.ts | 182 ++++++ packages/garasign-utils/src/ssh-client.ts | 110 ++++ packages/garasign-utils/src/utils.ts | 58 ++ packages/garasign-utils/tsconfig-lint.json | 5 + packages/garasign-utils/tsconfig.json | 9 + 23 files changed, 1644 insertions(+), 8 deletions(-) create mode 100644 packages/garasign-utils/.depcheckrc create mode 100644 packages/garasign-utils/.eslintignore create mode 100644 packages/garasign-utils/.eslintrc.js create mode 100644 packages/garasign-utils/.mocharc.js create mode 100644 packages/garasign-utils/.prettierignore create mode 100644 packages/garasign-utils/.prettierrc.json create mode 100644 packages/garasign-utils/LICENSE create mode 100755 packages/garasign-utils/bin/sign.js create mode 100644 packages/garasign-utils/package.json create mode 100644 packages/garasign-utils/src/garasign.sh create mode 100644 packages/garasign-utils/src/index.spec.ts create mode 100644 packages/garasign-utils/src/index.ts create mode 100644 packages/garasign-utils/src/signing-clients/index.ts create mode 100644 packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts create mode 100644 packages/garasign-utils/src/signing-clients/local-signing-client.ts create mode 100644 packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts create mode 100644 packages/garasign-utils/src/signing-clients/remote-signing-client.ts create mode 100644 packages/garasign-utils/src/ssh-client.spec.ts create mode 100644 packages/garasign-utils/src/ssh-client.ts create mode 100644 packages/garasign-utils/src/utils.ts create mode 100644 packages/garasign-utils/tsconfig-lint.json create mode 100644 packages/garasign-utils/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 1119e3b3..c5bff9f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "jsdom": "^22.1.0", "react-16-node-hanging-test-fix": "^1.0.0", "sinon-chai": "^3.7.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "why-is-node-running": "^2.2.2" }, "devDependencies": { @@ -2978,6 +2978,10 @@ "resolved": "configs/eslint-config-devtools", "link": true }, + "node_modules/@mongodb-js/garasign-utils": { + "resolved": "packages/garasign-utils", + "link": true + }, "node_modules/@mongodb-js/get-os-info": { "resolved": "packages/get-os-info", "link": true @@ -4113,6 +4117,24 @@ "integrity": "sha512-Y3Faje1Gi/l+tSjAo52Lpm3fLnpQtQLfYcmpInikSoHBxckOFKl1uGWVfRyI3LnX2+brcRUDox7T08eJ60UQlQ==", "dev": true }, + "node_modules/@types/ssh2": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.18.tgz", + "integrity": "sha512-7eH4ppQMFlzvn//zhwD54MWaITR1aSc1oFBye9vb76GZ2Y9PSFYdwVIwOlxRXWs5+1hifntXyt+8a6SUbOD7Hg==", + "dev": true, + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", + "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/ssri": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@types/ssri/-/ssri-7.1.1.tgz", @@ -5549,6 +5571,15 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/builtins": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", @@ -6500,6 +6531,20 @@ "node": ">=10" } }, + "node_modules/cpu-features": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz", + "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.17.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -13332,6 +13377,12 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "optional": true + }, "node_modules/nanoid": { "version": "3.1.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", @@ -17003,6 +17054,23 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/ssh2": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz", + "integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.9", + "nan": "^2.18.0" + } + }, "node_modules/sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", @@ -17654,9 +17722,9 @@ } }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -17940,6 +18008,12 @@ "through": "^2.3.8" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -18901,6 +18975,47 @@ "node": ">=14.17" } }, + "packages/garasign-utils": { + "name": "@mongodb-js/garasign-utils", + "version": "0.1.0", + "license": "SSPL", + "dependencies": { + "ssh2": "^1.15.0" + }, + "devDependencies": { + "@mongodb-js/eslint-config-devtools": "0.9.10", + "@mongodb-js/mocha-config-devtools": "^1.0.2", + "@mongodb-js/prettier-config-devtools": "^1.0.1", + "@mongodb-js/tsconfig-devtools": "^1.0.1", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.0.0", + "@types/node": "^17.0.35", + "@types/sinon-chai": "^3.2.5", + "@types/ssh2": "^1.11.18", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "mocha": "^8.4.0", + "nyc": "^15.1.0", + "prettier": "^2.3.2", + "sinon": "^9.2.3", + "typescript": "^5.0.4" + } + }, + "packages/garasign-utils/node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "packages/get-os-info": { "name": "@mongodb-js/get-os-info", "version": "0.3.22", @@ -21891,6 +22006,38 @@ "prettier": "2.3.2" } }, + "@mongodb-js/garasign-utils": { + "version": "file:packages/garasign-utils", + "requires": { + "@mongodb-js/eslint-config-devtools": "0.9.10", + "@mongodb-js/mocha-config-devtools": "^1.0.2", + "@mongodb-js/prettier-config-devtools": "^1.0.1", + "@mongodb-js/tsconfig-devtools": "^1.0.1", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.0.0", + "@types/node": "^17.0.35", + "@types/sinon-chai": "^3.2.5", + "@types/ssh2": "^1.11.18", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "mocha": "^8.4.0", + "nyc": "^15.1.0", + "prettier": "^2.3.2", + "sinon": "^9.2.3", + "ssh2": "^1.15.0", + "typescript": "^5.0.4" + }, + "dependencies": { + "typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true + } + } + }, "@mongodb-js/get-os-info": { "version": "file:packages/get-os-info", "requires": { @@ -21937,7 +22084,7 @@ "react": ">=16", "react-16-node-hanging-test-fix": "^1.0.0", "sinon-chai": "^3.7.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "why-is-node-running": "^2.2.2" }, "dependencies": { @@ -23341,6 +23488,26 @@ "integrity": "sha512-Y3Faje1Gi/l+tSjAo52Lpm3fLnpQtQLfYcmpInikSoHBxckOFKl1uGWVfRyI3LnX2+brcRUDox7T08eJ60UQlQ==", "dev": true }, + "@types/ssh2": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.18.tgz", + "integrity": "sha512-7eH4ppQMFlzvn//zhwD54MWaITR1aSc1oFBye9vb76GZ2Y9PSFYdwVIwOlxRXWs5+1hifntXyt+8a6SUbOD7Hg==", + "dev": true, + "requires": { + "@types/node": "^18.11.18" + }, + "dependencies": { + "@types/node": { + "version": "18.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", + "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + } + } + }, "@types/ssri": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@types/ssri/-/ssri-7.1.1.tgz", @@ -24455,6 +24622,12 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true + }, "builtins": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", @@ -25169,6 +25342,16 @@ "yaml": "^1.10.0" } }, + "cpu-features": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz", + "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==", + "optional": true, + "requires": { + "buildcheck": "~0.0.6", + "nan": "^2.17.0" + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -30445,6 +30628,12 @@ "thenify-all": "^1.0.0" } }, + "nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "optional": true + }, "nanoid": { "version": "3.1.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", @@ -33253,6 +33442,17 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "ssh2": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz", + "integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==", + "requires": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2", + "cpu-features": "~0.0.9", + "nan": "^2.18.0" + } + }, "sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", @@ -33745,9 +33945,9 @@ "dev": true }, "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -33943,6 +34143,12 @@ "through": "^2.3.8" } }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", diff --git a/packages/garasign-utils/.depcheckrc b/packages/garasign-utils/.depcheckrc new file mode 100644 index 00000000..48bf9af6 --- /dev/null +++ b/packages/garasign-utils/.depcheckrc @@ -0,0 +1,8 @@ +ignores: + - '@mongodb-js/prettier-config-devtools' + - '@mongodb-js/tsconfig-devtools' + - '@types/chai' + - '@types/sinon-chai' + - 'sinon' +ignore-patterns: + - 'dist' diff --git a/packages/garasign-utils/.eslintignore b/packages/garasign-utils/.eslintignore new file mode 100644 index 00000000..85a8a75e --- /dev/null +++ b/packages/garasign-utils/.eslintignore @@ -0,0 +1,2 @@ +.nyc-output +dist diff --git a/packages/garasign-utils/.eslintrc.js b/packages/garasign-utils/.eslintrc.js new file mode 100644 index 00000000..83296d73 --- /dev/null +++ b/packages/garasign-utils/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + root: true, + extends: ['@mongodb-js/eslint-config-devtools'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig-lint.json'], + }, +}; diff --git a/packages/garasign-utils/.mocharc.js b/packages/garasign-utils/.mocharc.js new file mode 100644 index 00000000..64afeb1f --- /dev/null +++ b/packages/garasign-utils/.mocharc.js @@ -0,0 +1 @@ +module.exports = require('@mongodb-js/mocha-config-devtools'); diff --git a/packages/garasign-utils/.prettierignore b/packages/garasign-utils/.prettierignore new file mode 100644 index 00000000..4d28df66 --- /dev/null +++ b/packages/garasign-utils/.prettierignore @@ -0,0 +1,3 @@ +.nyc_output +dist +coverage diff --git a/packages/garasign-utils/.prettierrc.json b/packages/garasign-utils/.prettierrc.json new file mode 100644 index 00000000..dfae21d0 --- /dev/null +++ b/packages/garasign-utils/.prettierrc.json @@ -0,0 +1 @@ +"@mongodb-js/prettier-config-devtools" diff --git a/packages/garasign-utils/LICENSE b/packages/garasign-utils/LICENSE new file mode 100644 index 00000000..63f6b6c1 --- /dev/null +++ b/packages/garasign-utils/LICENSE @@ -0,0 +1,557 @@ + Server Side Public License +VERSION 1, OCTOBER 16, 2018 + +Copyright © 2018 MongoDB, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to Server Side Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on +the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through a +computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the +extent that it includes a convenient and prominently visible feature that +(1) displays an appropriate copyright notice, and (2) tells the user that +there is no warranty for the work (except to the extent that warranties +are provided), that licensees may convey the work under this License, and +how to view a copy of this License. If the interface presents a list of +user commands or options, such as a menu, a prominent item in the list +meets this criterion. + +1. Source Code. + +The “source code” for a work means the preferred form of the work for +making modifications to it. “Object code” means any non-source form of a +work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that is +widely used among developers working in that language. The “System +Libraries” of an executable work include anything, other than the work as +a whole, that (a) is included in the normal form of packaging a Major +Component, but which is not part of that Major Component, and (b) serves +only to enable use of the work with that Major Component, or to implement +a Standard Interface for which an implementation is available to the +public in source code form. A “Major Component”, in this context, means a +major essential component (kernel, window system, and so on) of the +specific operating system (if any) on which the executable work runs, or +a compiler used to produce the work, or an object code interpreter used +to run it. + +The “Corresponding Source” for a work in object code form means all the +source code needed to generate, install, and (for an executable work) run +the object code and to modify the work, including scripts to control +those activities. However, it does not include the work's System +Libraries, or general-purpose tools or generally available free programs +which are used unmodified in performing those activities but which are +not part of the work. For example, Corresponding Source includes +interface definition files associated with source files for the work, and +the source code for shared libraries and dynamically linked subprograms +that the work is specifically designed to require, such as by intimate +data communication or control flow between those subprograms and other +parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program, subject to section 13. The +output from running a covered work is covered by this License only if the +output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by +copyright law. Subject to section 13, you may make, run and propagate +covered works that you do not convey, without conditions so long as your +license otherwise remains in force. You may convey covered works to +others for the sole purpose of having them make modifications exclusively +for you, or provide you with facilities for running those works, provided +that you comply with the terms of this License in conveying all +material for which you do not control copyright. Those thus making or +running the covered works for you must do so exclusively on your +behalf, under your direction and control, on terms that prohibit them +from making any copies of your copyrighted material outside their +relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes it +unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article 11 +of the WIPO copyright treaty adopted on 20 December 1996, or similar laws +prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention is +effected by exercising rights under this License with respect to the +covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's users, +your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; keep +intact all notices stating that this License and any non-permissive terms +added in accord with section 7 apply to the code; keep intact all notices +of the absence of any warranty; and give all recipients a copy of this +License along with the Program. You may charge any price or no price for +each copy that you convey, and you may offer support or warranty +protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the terms +of section 4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, +and giving a relevant date. + +b) The work must carry prominent notices stating that it is released +under this License and any conditions added under section 7. This +requirement modifies the requirement in section 4 to “keep intact all +notices”. + +c) You must license the entire work, as a whole, under this License to +anyone who comes into possession of a copy. This License will therefore +apply, along with any applicable section 7 additional terms, to the +whole of the work, and all its parts, regardless of how they are +packaged. This License gives no permission to license the work in any +other way, but it does not invalidate such permission if you have +separately received it. + +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your work +need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, and +which are not combined with it such as to form a larger program, in or on +a volume of a storage or distribution medium, is called an “aggregate” if +the compilation and its resulting copyright are not used to limit the +access or legal rights of the compilation's users beyond what the +individual works permit. Inclusion of a covered work in an aggregate does +not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium customarily +used for software interchange. + +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a written +offer, valid for at least three years and valid for as long as you +offer spare parts or customer support for that product model, to give +anyone who possesses the object code either (1) a copy of the +Corresponding Source for all the software in the product that is +covered by this License, on a durable physical medium customarily used +for software interchange, for a price no more than your reasonable cost +of physically performing this conveying of source, or (2) access to +copy the Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This alternative is +allowed only occasionally and noncommercially, and only if you received +the object code with such an offer, in accord with subsection 6b. + +d) Convey the object code by offering access from a designated place +(gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to copy +the object code is a network server, the Corresponding Source may be on +a different server (operated by you or a third party) that supports +equivalent copying facilities, provided you maintain clear directions +next to the object code saying where to find the Corresponding Source. +Regardless of what server hosts the Corresponding Source, you remain +obligated to ensure that it is available for as long as needed to +satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided you +inform other peers where the object code and Corresponding Source of +the work are being offered to the general public at no charge under +subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be included +in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as part +of a transaction in which the right of possession and use of the User +Product is transferred to the recipient in perpetuity or for a fixed term +(regardless of how the transaction is characterized), the Corresponding +Source conveyed under this section must be accompanied by the +Installation Information. But this requirement does not apply if neither +you nor any third party retains the ability to install modified object +code on the User Product (for example, the work has been installed in +ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access +to a network may be denied when the modification itself materially +and adversely affects the operation of the network or violates the +rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in +accord with this section must be in a format that is publicly documented +(and with an implementation available to the public in source code form), +and must require no special password or key for unpacking, reading or +copying. + +7. Additional Terms. + +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall be +treated as though they were included in this License, to the extent that +they are valid under applicable law. If additional permissions apply only +to part of the Program, that part may be used separately under those +permissions, but the entire Program remains governed by this License +without regard to the additional permissions. When you convey a copy of +a covered work, you may at your option remove any additional permissions +from that copy, or from any part of it. (Additional permissions may be +written to require their own removal in certain cases when you modify the +work.) You may place additional permissions on material, added by you to +a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add +to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or + +e) Declining to grant rights under trademark law for use of some trade +names, trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that material +by anyone who conveys the material (or modified versions of it) with +contractual assumptions of liability to the recipient, for any +liability that these contractual assumptions directly impose on those +licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further restriction, +you may remove that term. If a license document contains a further +restriction but permits relicensing or conveying under this License, you +may add to a covered work material governed by the terms of that license +document, provided that the further restriction does not survive such +relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must +place, in the relevant source files, a statement of the additional terms +that apply to those files, or a notice indicating where to find the +applicable terms. Additional terms, permissive or non-permissive, may be +stated in the form of a separately written license, or stated as +exceptions; the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or modify +it is void, and will automatically terminate your rights under this +License (including any patent licenses granted under the third paragraph +of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally terminates +your license, and (b) permanently, if the copyright holder fails to +notify you of the violation by some reasonable means prior to 60 days +after the cessation. + +Moreover, your license from a particular copyright holder is reinstated +permanently if the copyright holder notifies you of the violation by some +reasonable means, this is the first time you have received notice of +violation of this License (for any work) from that copyright holder, and +you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a +copy of the Program. Ancillary propagation of a covered work occurring +solely as a consequence of using peer-to-peer transmission to receive a +copy likewise does not require acceptance. However, nothing other than +this License grants you permission to propagate or modify any covered +work. These actions infringe copyright if you do not accept this License. +Therefore, by modifying or propagating a covered work, you indicate your +acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives +a license from the original licensors, to run, modify and propagate that +work, subject to this License. You are not responsible for enforcing +compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered work +results from an entity transaction, each party to that transaction who +receives a copy of the work also receives whatever licenses to the work +the party's predecessor in interest had or could give under the previous +paragraph, plus a right to possession of the Corresponding Source of the +work from the predecessor in interest, if the predecessor has it or can +get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted +under this License, and you may not initiate litigation (including a +cross-claim or counterclaim in a lawsuit) alleging that any patent claim +is infringed by making, using, selling, offering for sale, or importing +the Program or any portion of it. + +11. Patents. + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The work +thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or +controlled by the contributor, whether already acquired or hereafter +acquired, that would be infringed by some manner, permitted by this +License, of making, using, or selling its contributor version, but do not +include claims that would be infringed only as a consequence of further +modification of the contributor version. For purposes of this definition, +“control” includes the right to grant patent sublicenses in a manner +consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to make, +use, sell, offer for sale, import and otherwise run, modify and propagate +the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a party +means to make such an agreement or commitment not to enforce a patent +against the party. + +If you convey a covered work, knowingly relying on a patent license, and +the Corresponding Source of the work is not available for anyone to copy, +free of charge and under the terms of this License, through a publicly +available network server or other readily accessible means, then you must +either (1) cause the Corresponding Source to be so available, or (2) +arrange to deprive yourself of the benefit of the patent license for this +particular work, or (3) arrange, in a manner consistent with the +requirements of this License, to extend the patent license to downstream +recipients. “Knowingly relying” means you have actual knowledge that, but +for the patent license, your conveying the covered work in a country, or +your recipient's use of the covered work in a country, would infringe +one or more identifiable patents in that country that you have reason +to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties receiving +the covered work authorizing them to use, propagate, modify or convey a +specific copy of the covered work, then the patent license you grant is +automatically extended to all recipients of the covered work and works +based on it. + +A patent license is “discriminatory” if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you are +a party to an arrangement with a third party that is in the business of +distributing software, under which you make payment to the third party +based on the extent of your activity of conveying the work, and under +which the third party grants, to any of the parties who would receive the +covered work from you, a discriminatory patent license (a) in connection +with copies of the covered work conveyed by you (or copies made from +those copies), or (b) primarily for and in connection with specific +products or compilations that contain the covered work, unless you +entered into that arrangement, or that patent license was granted, prior +to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any +implied license or other defenses to infringement that may otherwise be +available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot use, +propagate or convey a covered work so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then +as a consequence you may not use, propagate or convey it at all. For +example, if you agree to terms that obligate you to collect a royalty for +further conveying from those to whom you convey the Program, the only way +you could satisfy both those terms and this License would be to refrain +entirely from conveying the Program. + +13. Offering the Program as a Service. + +If you make the functionality of the Program or a modified version +available to third parties as a service, you must make the Service Source +Code available via network download to everyone at no charge, under the +terms of this License. Making the functionality of the Program or +modified version available to third parties as a service includes, +without limitation, enabling third parties to interact with the +functionality of the Program or modified version remotely through a +computer network, offering a service the value of which entirely or +primarily derives from the value of the Program or modified version, or +offering a service that accomplishes for users the primary purpose of the +Program or modified version. + +“Service Source Code” means the Corresponding Source for the Program or +the modified version, and the Corresponding Source for all programs that +you use to make the Program or modified version available as a service, +including, without limitation, management software, user interfaces, +application program interfaces, automation software, monitoring software, +backup software, storage software and hosting software, all such that a +user could run an instance of the service using the Service Source Code +you make available. + +14. Revised Versions of this License. + +MongoDB, Inc. may publish revised and/or new versions of the Server Side +Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the Server Side Public +License “or any later version” applies to it, you have the option of +following the terms and conditions either of that numbered version or of +any later version published by MongoDB, Inc. If the Program does not +specify a version number of the Server Side Public License, you may +choose any version ever published by MongoDB, Inc. + +If the Program specifies that a proxy can decide which future versions of +the Server Side Public License can be used, that proxy's public statement +of acceptance of a version permanently authorizes you to choose that +version for the Program. + +Later license versions may give you additional or different permissions. +However, no additional obligations are imposed on any author or copyright +holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING +ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF +THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO +LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU +OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above +cannot be given local legal effect according to their terms, reviewing +courts shall apply local law that most closely approximates an absolute +waiver of all civil liability in connection with the Program, unless a +warranty or assumption of liability accompanies a copy of the Program in +return for a fee. + + END OF TERMS AND CONDITIONS diff --git a/packages/garasign-utils/bin/sign.js b/packages/garasign-utils/bin/sign.js new file mode 100755 index 00000000..80c2a8e6 --- /dev/null +++ b/packages/garasign-utils/bin/sign.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node +const { sign } = require('./../dist'); +const { program } = require('commander'); + +program + .arguments('file') + .option('-c, --client ', 'The signing client to use', 'local') + .option( + '-h, --host ', + 'The SSH host to use when signing with remote client.' + ) + .option('-u, --username ', 'The SSH host username.') + .option('-p, --port ', 'The SSH host port.') + .option( + '-k, --private-key ', + 'The SSH private key to use when signing with remote client.' + ) + .parse(process.argv); + +const { client, host, username, port, privateKey } = program.opts(); +const file = program.args[0]; + +const options = + client === 'remote' ? { host, username, port, privateKey } : undefined; +sign(file, client, options); diff --git a/packages/garasign-utils/package.json b/packages/garasign-utils/package.json new file mode 100644 index 00000000..67862095 --- /dev/null +++ b/packages/garasign-utils/package.json @@ -0,0 +1,71 @@ +{ + "name": "@mongodb-js/garasign-utils", + "description": "Utilities for signing packages in CI with Garasign.", + "author": { + "name": "MongoDB Inc", + "email": "compass@mongodb.com" + }, + "publishConfig": { + "access": "public" + }, + "bugs": { + "url": "https://jira.mongodb.org/projects/COMPASS/issues", + "email": "compass@mongodb.com" + }, + "homepage": "https://github.com/mongodb-js/devtools-shared", + "version": "0.1.0", + "repository": { + "type": "git", + "url": "https://github.com/mongodb-js/devtools-shared.git" + }, + "files": [ + "dist" + ], + "license": "SSPL", + "main": "dist/index.js", + "exports": { + "require": "./dist/index.js", + "import": "./dist/.esm-wrapper.mjs" + }, + "types": "./dist/index.d.ts", + "scripts": { + "bootstrap": "npm run compile", + "prepublishOnly": "npm run compile", + "compile": "tsc -p tsconfig.json && gen-esm-wrapper . ./dist/.esm-wrapper.mjs", + "typecheck": "tsc --noEmit", + "eslint": "eslint", + "prettier": "prettier", + "lint": "npm run eslint . && npm run prettier -- --check .", + "depcheck": "depcheck", + "check": "npm run typecheck && npm run lint && npm run depcheck", + "check-ci": "npm run check", + "test": "mocha", + "test-cov": "nyc -x \"**/*.spec.*\" --reporter=lcov --reporter=text --reporter=html npm run test", + "test-watch": "npm run test -- --watch", + "test-ci": "npm run test-cov", + "reformat": "npm run prettier -- --write ." + }, + "devDependencies": { + "@mongodb-js/eslint-config-devtools": "0.9.10", + "@mongodb-js/mocha-config-devtools": "^1.0.2", + "@mongodb-js/prettier-config-devtools": "^1.0.1", + "@mongodb-js/tsconfig-devtools": "^1.0.1", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.0.0", + "@types/node": "^17.0.35", + "@types/sinon-chai": "^3.2.5", + "@types/ssh2": "^1.11.18", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "mocha": "^8.4.0", + "nyc": "^15.1.0", + "prettier": "^2.3.2", + "sinon": "^9.2.3", + "typescript": "^5.0.4" + }, + "dependencies": { + "ssh2": "^1.15.0" + } +} diff --git a/packages/garasign-utils/src/garasign.sh b/packages/garasign-utils/src/garasign.sh new file mode 100644 index 00000000..5815beb0 --- /dev/null +++ b/packages/garasign-utils/src/garasign.sh @@ -0,0 +1,54 @@ +#! /usr/bin/env bash + +if [ -z "$1" ]; then + echo "Usage: garasign.sh " + exit 1 +fi + +echo "Checking environment variables" +echo "garasign_username: ${garasign_username}" +echo "garasign_password: ${garasign_password}" +echo "artifactory_username: ${artifactory_username}" +echo "artifactory_password: ${artifactory_password}" + +if [ -z ${garasign_username+omitted} ]; then echo "garasign_username is unset" && exit 1; fi +if [ -z ${garasign_password+omitted} ]; then echo "garasign_password is unset" && exit 1; fi +if [ -z ${artifactory_username+omitted} ]; then echo "artifactory_username is unset" && exit 1; fi +if [ -z ${artifactory_password+omitted} ]; then echo "artifactory_password is unset" && exit 1; fi + + +ARTIFACTORY_HOST="artifactory.corp.mongodb.com" + +logout_artifactory() { + docker logout "${ARTIFACTORY_HOST}" > /dev/null 2>&1 + echo "Logged out from artifactory" +} +trap logout_artifactory EXIT + +echo "Logging into docker artifactory" +echo "${artifactory_password}" | docker login --password-stdin --username ${artifactory_username} ${ARTIFACTORY_HOST} > /dev/null 2>&1 + +# If the docker login failed, exit +[ $? -ne 0 ] && exit $? + +cat < signing-envfile +GRS_CONFIG_USER1_USERNAME=${garasign_username} +GRS_CONFIG_USER1_PASSWORD=${garasign_password} +EOL + +directory=$(pwd) +file=$1 + +echo "File to be signed: $file" +echo "Working directory: $directory" + +docker run \ + --env-file=signing-envfile \ + --rm \ + -v $directory:$directory \ + -w $directory \ + ${ARTIFACTORY_HOST}/release-tools-container-registry-local/garasign-gpg \ + /bin/bash -c "gpgloader && gpg --yes -v --armor -o $file.sig --detach-sign $file" + +rm signing-envfile +echo "Finished signing $file" \ No newline at end of file diff --git a/packages/garasign-utils/src/index.spec.ts b/packages/garasign-utils/src/index.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/garasign-utils/src/index.ts b/packages/garasign-utils/src/index.ts new file mode 100644 index 00000000..bf285a89 --- /dev/null +++ b/packages/garasign-utils/src/index.ts @@ -0,0 +1,18 @@ +import type { ClientType, ClientOptions } from './signing-clients'; +import { assertRequiredVars, getSigningClient, debug } from './utils'; + +export async function sign( + file: string, + client: T, + options: ClientOptions +): Promise { + assertRequiredVars(); + debug(`Signing file: ${file} with client ${client} and options:`, options); + try { + const signingClient = await getSigningClient(client, options); + await signingClient.sign(file); + } catch (err) { + debug(`Error signing file: ${file}`, err); + throw err; + } +} diff --git a/packages/garasign-utils/src/signing-clients/index.ts b/packages/garasign-utils/src/signing-clients/index.ts new file mode 100644 index 00000000..f820e93a --- /dev/null +++ b/packages/garasign-utils/src/signing-clients/index.ts @@ -0,0 +1,18 @@ +import { type SSHClientOptions } from '../ssh-client'; + +export { LocalSigningClient } from './local-signing-client'; +export { RemoteSigningClient } from './remote-signing-client'; + +export type SigningClientOptions = { + rootDir: string; + signingScript: string; +}; + +export type ClientType = 'local' | 'remote'; +export type ClientOptions = T extends 'remote' + ? Pick + : undefined; + +export interface SigningClient { + sign(file: string): Promise; +} diff --git a/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts b/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts new file mode 100644 index 00000000..cfb1bca8 --- /dev/null +++ b/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts @@ -0,0 +1,48 @@ +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; +import { LocalSigningClient } from './local-signing-client'; +import { expect } from 'chai'; + +describe('LocalSigningClient', () => { + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'local-signing-client')); + }); + + it('signs the file correctly', async function () { + // In order to sign a file locally, we setup the following: + // 1. Create a tmp directory, tmp file to sign and a tmp signing script + // 2. Instantiate a LocalSigningClient with the tmp directory and the tmp signing script + // 3. Sign the file and assert that the file was modified correctly + // 5. Assert that the signing script was called with the correct arguments + // 6. Assert that the signed file was copied back to the original file + + const fileToSign = path.join(tmpDir, 'originals', 'file-to-sign.txt'); + const signingScript = path.join(tmpDir, 'originals', 'script.sh'); + + { + await fs.mkdir(path.dirname(fileToSign), { recursive: true }); + await fs.writeFile(fileToSign, 'original content'); + await fs.writeFile( + signingScript, + ` + #!/bin/bash + echo "Signing script called with arguments: $@" + echo "signed content" > $1 + ` + ); + } + + const localSigningClient = new LocalSigningClient({ + rootDir: tmpDir, + signingScript: signingScript, + }); + + await localSigningClient.sign(fileToSign); + + const signedFile = (await fs.readFile(fileToSign, 'utf-8')).trim(); + expect(signedFile).to.equal('signed content'); + }); +}); diff --git a/packages/garasign-utils/src/signing-clients/local-signing-client.ts b/packages/garasign-utils/src/signing-clients/local-signing-client.ts new file mode 100644 index 00000000..df6df601 --- /dev/null +++ b/packages/garasign-utils/src/signing-clients/local-signing-client.ts @@ -0,0 +1,48 @@ +import path from 'path'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { debug } from '../utils'; +import type { SigningClient, SigningClientOptions } from '.'; + +const execAsync = promisify(exec); + +export class LocalSigningClient implements SigningClient { + constructor(private options: SigningClientOptions) {} + + private async init() { + const remoteScript = `${this.options.rootDir}/garasign.sh`; + await execAsync(`mkdir -p ${this.options.rootDir}`); + await this.copyFile(this.options.signingScript, remoteScript); + await execAsync(`chmod +x ${remoteScript}`); + } + + private async copyFile(from: string, to: string): Promise { + await execAsync(`cp ${from} ${to}`); + } + + async sign(file: string): Promise { + debug('Signing file', file); + + const remotePath = path.join(this.options.rootDir, path.basename(file)); + + try { + await this.init(); + + await this.copyFile(file, remotePath); + debug(`LocalSigningClient: Copied file ${file} to ${remotePath}`); + + await execAsync( + `cd ${this.options.rootDir} && ./garasign.sh ${path.basename(file)}` + ); + debug(`LocalSigningClient: Signed file ${remotePath}`); + + await this.copyFile(remotePath, file); + debug( + `LocalSigningClient: Copied signed file back from ${remotePath} to ${file}` + ); + } finally { + // Clean up + void execAsync(`rm -f ${remotePath}`); + } + } +} diff --git a/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts b/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts new file mode 100644 index 00000000..69225d46 --- /dev/null +++ b/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts @@ -0,0 +1,95 @@ +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; +import { exec } from 'child_process'; +import { RemoteSigningClient } from './remote-signing-client'; +import { expect } from 'chai'; +import type { SSHClient } from '../ssh-client'; + +const getMockedSSHClient = () => { + return { + getSftpConnection: () => { + return { + fastPut: async ( + localFile: string, + remoteFile: string, + cb: (err?: Error) => void + ) => { + try { + await fs.copyFile(localFile, remoteFile); + cb(); + } catch (err) { + cb(err as Error); + } + }, + fastGet: async ( + remoteFile: string, + localFile: string, + cb: (err?: Error) => void + ) => { + try { + await fs.copyFile(remoteFile, localFile); + cb(); + } catch (err) { + cb(err as Error); + } + }, + unlink: async (remoteFile: string, cb: (err?: Error) => void) => { + try { + await fs.unlink(remoteFile); + cb(); + } catch (err) { + cb(err as Error); + } + }, + }; + }, + exec: (command: string) => { + return new Promise((resolve, reject) => { + exec(command, (err) => { + if (err) { + return reject(err); + } + return resolve('Ok'); + }); + }); + }, + disconnect: () => {}, + } as unknown as SSHClient; +}; + +describe('RemoteSigningClient', () => { + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-signing-client')); + }); + + it('signs the file correctly', async function () { + const fileToSign = path.join(tmpDir, 'originals', 'file-to-sign.txt'); + const signingScript = path.join(tmpDir, 'originals', 'script.sh'); + + { + await fs.mkdir(path.dirname(fileToSign), { recursive: true }); + await fs.writeFile(fileToSign, 'RemoteSigningClient: original content'); + await fs.writeFile( + signingScript, + ` + #!/bin/bash + echo "Signing script called with arguments: $@" + echo "RemoteSigningClient: signed content" > $1 + ` + ); + } + + const remoteSigningClient = new RemoteSigningClient(getMockedSSHClient(), { + rootDir: tmpDir, + signingScript: signingScript, + }); + + await remoteSigningClient.sign(fileToSign); + + const signedFile = (await fs.readFile(fileToSign, 'utf-8')).trim(); + expect(signedFile).to.equal('RemoteSigningClient: signed content'); + }); +}); diff --git a/packages/garasign-utils/src/signing-clients/remote-signing-client.ts b/packages/garasign-utils/src/signing-clients/remote-signing-client.ts new file mode 100644 index 00000000..f6f024a0 --- /dev/null +++ b/packages/garasign-utils/src/signing-clients/remote-signing-client.ts @@ -0,0 +1,109 @@ +import path from 'path'; +import type { SFTPWrapper } from 'ssh2'; +import type { SSHClient } from '../ssh-client'; +import { debug } from '../utils'; +import type { SigningClient, SigningClientOptions } from '.'; + +export class RemoteSigningClient implements SigningClient { + private sftpConnection!: SFTPWrapper; + + constructor( + private sshClient: SSHClient, + private options: SigningClientOptions + ) {} + + /** + * Initialize the signing client and setup remote machine to be ready for signing + * the files. This will do following things: + * - Create a working directory on the remote machine + * - Copy the signing script to the remote machine + */ + private async init() { + this.sftpConnection = await this.sshClient.getSftpConnection(); + await this.sshClient.exec(`mkdir -p ${this.options.rootDir}`); + + // Copy the signing script to the remote machine + { + const remoteScript = `${this.options.rootDir}/garasign.sh`; + await this.copyFile(this.options.signingScript, remoteScript); + await this.sshClient.exec(`chmod +x ${remoteScript}`); + } + } + + private getRemoteFilePath(file: string) { + return `${this.options.rootDir}/temp-${Date.now()}-${path.basename(file)}`; + } + + private async copyFile(file: string, remotePath: string): Promise { + return new Promise((resolve, reject) => { + this.sftpConnection.fastPut(file, remotePath, (err) => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); + } + + private async downloadFile(remotePath: string, file: string): Promise { + return new Promise((resolve, reject) => { + this.sftpConnection.fastGet(remotePath, file, (err) => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); + } + + private async removeFile(remotePath: string): Promise { + return new Promise((resolve, reject) => { + this.sftpConnection.unlink(remotePath, (err) => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); + } + + private async signRemoteFile(file: string) { + /** + * Passing env variables as an option to ssh.exec() doesn't work as ssh config + * (`sshd_config.AllowEnv`) does not allow to pass env variables by default. + * So, here we are passing the env variables as part of the command. + */ + const cmds = [ + `cd ${this.options.rootDir}`, + `export garasign_username=${process.env.GARASIGN_USERNAME}`, + `export garasign_password=${process.env.GARASIGN_PASSWORD}`, + `export artifactory_username=${process.env.ARTIFACTORY_USERNAME}`, + `export artifactory_password=${process.env.ARTIFACTORY_PASSWORD}`, + `./garasign.sh ${file}`, + ]; + const command = cmds.join(' && '); + const res = await this.sshClient.exec(command); + debug('Sign remote file response\n', res.trim()); + } + + async sign(file: string): Promise { + const remotePath = this.getRemoteFilePath(file); + try { + // establish connection + await this.init(); + + await this.copyFile(file, remotePath); + debug(`SFTP: Copied file ${file} to ${remotePath}`); + + await this.signRemoteFile(remotePath); + debug(`SFTP: Signed file ${file}`); + + await this.downloadFile(remotePath, file); + debug(`SFTP: Downloaded signed file to ${file}`); + } finally { + await this.removeFile(remotePath); + debug(`SFTP: Removed remote file ${remotePath}`); + this.sshClient.disconnect(); + } + } +} diff --git a/packages/garasign-utils/src/ssh-client.spec.ts b/packages/garasign-utils/src/ssh-client.spec.ts new file mode 100644 index 00000000..4e352a0a --- /dev/null +++ b/packages/garasign-utils/src/ssh-client.spec.ts @@ -0,0 +1,182 @@ +import sinon from 'sinon'; +import { SSHClient } from './ssh-client'; +import { expect } from 'chai'; + +describe('SSHClient', () => { + let sshClient: SSHClient; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + const sshClientOptions = { + host: 'example.com', + port: 22, + username: 'admin', + }; + sshClient = new SSHClient(sshClientOptions); + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('connects successfully on ready', async function () { + const connectStub = sandbox + .stub(sshClient['sshConnection'], 'connect') + .resolves(sshClient['sshConnection']); + await Promise.all([ + sshClient.connect(), + sshClient['sshConnection'].emit('ready'), + ]); + expect(connectStub.calledOnce).to.be.true; + expect(connectStub.firstCall.firstArg).to.deep.equal({ + host: 'example.com', + port: 22, + username: 'admin', + privateKey: undefined, + }); + expect(sshClient).to.have.property('connected', true); + }); + + it('does not called client.connect when connected', async function () { + const connectStub = sandbox + .stub(sshClient['sshConnection'], 'connect') + .resolves(sshClient['sshConnection']); + // Its connected here + sshClient['sshConnection'].emit('ready'); + await Promise.all([ + sshClient.connect(), + sshClient['sshConnection'].emit('ready'), + ]); + + expect(connectStub.calledOnce).to.be.false; + expect(sshClient).to.have.property('connected', true); + }); + + it('throws when connecting on error', async function () { + const connectStub = sandbox + .stub(sshClient['sshConnection'], 'connect') + .resolves(sshClient['sshConnection']); + try { + await Promise.all([ + sshClient.connect(), + sshClient['sshConnection'].emit('error', new Error('Connection error')), + ]); + expect.fail('Expected SSH Client to throw'); + } catch (err) { + expect(err).to.have.property('message', 'Connection error'); + expect(connectStub.calledOnce).to.be.true; + expect(connectStub.firstCall.firstArg).to.deep.equal({ + host: 'example.com', + port: 22, + username: 'admin', + privateKey: undefined, + }); + expect(sshClient).to.have.property('connected', false); + } + }); + + it('should disconnect from SSH server', function () { + const endStub = sandbox.stub(sshClient['sshConnection'], 'end').resolves(); + sshClient['sshConnection'].emit('ready'); + + sshClient.disconnect(); + expect(endStub.calledOnce).to.be.true; + }); + + context('exec', function () { + const COMMAND = 'echo "Hello World"'; + type ExecCallback = (err: Error | undefined, channel: any) => any; + it('should throw when exec fails', async function () { + const execStub = sandbox + .stub(sshClient['sshConnection'], 'exec') + .callsFake((_command: string, cb: ExecCallback) => { + return cb(new Error('Callback Error'), null); + }); + sshClient['sshConnection'].emit('ready'); + + try { + await sshClient.exec(COMMAND); + expect.fail('Expected SSH Client to throw'); + } catch (err) { + expect(err).to.have.property('message', 'Callback Error'); + expect(execStub.calledOnce).to.be.true; + expect(execStub.firstCall.firstArg).to.equal(COMMAND); + } + }); + + it('should throw when exec returns an error - code > 0', async function () { + const execStub = sandbox + .stub(sshClient['sshConnection'], 'exec') + .callsFake((_command: string, cb: ExecCallback) => { + return cb(undefined, { + stderr: { + on: (event: string, cb: (data: string) => void) => { + if (event === 'data') { + cb('Some Error'); + } + }, + }, + on: (event: string, cb: (code: number | string) => void) => { + if (event === 'close') { + cb(10); + } + }, + }); + }); + sshClient['sshConnection'].emit('ready'); + + try { + await sshClient.exec(COMMAND); + expect.fail('Expected SSH Client to throw'); + } catch (err) { + expect(err).to.have.a.property( + 'message', + 'Command failed with code 10. Error: Some Error' + ); + expect(execStub.calledOnce).to.be.true; + expect(execStub.firstCall.firstArg).to.equal(COMMAND); + } + }); + + it('should return stdout when exec succeeds', async function () { + const execStub = sandbox + .stub(sshClient['sshConnection'], 'exec') + .callsFake((_command: string, cb: ExecCallback) => { + return cb(undefined, { + stderr: { + on: () => {}, + }, + on: (event: string, cb: (code: number | string) => void) => { + if (event === 'data') { + cb('Hello World'); + } + if (event === 'close') { + cb(0); + } + }, + }); + }); + sshClient['sshConnection'].emit('ready'); + + const result = await sshClient.exec(COMMAND); + expect(result).to.equal('Hello World'); + expect(execStub.calledOnce).to.be.true; + expect(execStub.firstCall.firstArg).to.equal(COMMAND); + }); + }); + + it('should get SFTP connection', async function () { + const sftpStub = sandbox + .stub(sshClient['sshConnection'], 'sftp') + .callsFake((cb: (err: Error | undefined, sftp: any) => any) => { + return cb(undefined, 'mockedSFTP'); + }); + + sshClient['sshConnection'].emit('ready'); + + const result = await sshClient.getSftpConnection(); + expect(result).to.equal('mockedSFTP'); + expect(sftpStub.calledOnce).to.be.true; + }); +}); diff --git a/packages/garasign-utils/src/ssh-client.ts b/packages/garasign-utils/src/ssh-client.ts new file mode 100644 index 00000000..c67519c4 --- /dev/null +++ b/packages/garasign-utils/src/ssh-client.ts @@ -0,0 +1,110 @@ +import type { SFTPWrapper } from 'ssh2'; +import { Client, type ConnectConfig } from 'ssh2'; +import { readFile } from 'fs/promises'; +import { debug } from './utils'; + +export type SSHClientOptions = ConnectConfig & { + // Absolute path to private key file. We will read it when connecting. + privateKey?: string; +}; + +export class SSHClient { + private sshConnection: Client; + private sftpConnection?: SFTPWrapper; + + private connected = false; + + constructor(private sshClientOptions: SSHClientOptions) { + this.sshConnection = new Client(); + this.setupEventListeners(); + } + + setupEventListeners() { + this.sshConnection.on('ready', () => { + debug('SSH: Connection established'); + this.connected = true; + }); + this.sshConnection.on('error', (err) => { + debug('SSH: Connection error', err); + this.connected = false; + }); + this.sshConnection.on('close', () => { + debug('SSH: Connection closed'); + this.connected = false; + this.sshConnection.destroy(); + }); + } + + async connect() { + if (this.connected) { + return Promise.resolve(); + } + const privateKey = this.sshClientOptions.privateKey + ? await readFile(this.sshClientOptions.privateKey) + : undefined; + return new Promise((resolve, reject) => { + this.sshConnection.connect({ + ...this.sshClientOptions, + privateKey, + }); + this.sshConnection.on('error', reject); + this.sshConnection.on('ready' as any, resolve); + }); + } + + disconnect() { + this.sshConnection.end(); + this.connected = false; + } + + async exec(command: string): Promise { + if (!this.connected) { + throw new Error('Not connected to ssh server'); + } + return new Promise((resolve, reject) => { + this.sshConnection.exec(command, (err, stream) => { + if (err) { + return reject(err); + } + let data = ''; + stream.on('data', (chunk: string) => { + data += chunk; + }); + stream.stderr.on('data', (chunk) => { + data += chunk; + }); + stream.on('close', (code: number) => { + if (code === 0) { + return resolve(data); + } else { + return reject( + new Error(`Command failed with code ${code}. Error: ${data}`) + ); + } + }); + }); + }); + } + + async getSftpConnection(): Promise { + if (!this.connected) { + throw new Error('Not connected to ssh server'); + } + + if (this.sftpConnection) { + return Promise.resolve(this.sftpConnection); + } + + return new Promise((resolve, reject) => { + this.sshConnection.sftp((err, sftp) => { + if (err) { + debug('SFTP: Failed to setup connection', err); + return reject(err); + } + debug('SFTP: Connection established'); + this.sftpConnection = sftp; + return resolve(sftp); + }); + }); + } +} diff --git a/packages/garasign-utils/src/utils.ts b/packages/garasign-utils/src/utils.ts new file mode 100644 index 00000000..dc4ac3d7 --- /dev/null +++ b/packages/garasign-utils/src/utils.ts @@ -0,0 +1,58 @@ +import path from 'path'; +import { SSHClient, type SSHClientOptions } from './ssh-client'; +import { + LocalSigningClient, + RemoteSigningClient, + type SigningClient, + type ClientType, + type ClientOptions, +} from './signing-clients'; + +// eslint-disable-next-line no-console +export const debug = console.log; + +export function assertRequiredVars() { + [ + 'GARASIGN_USERNAME', + 'GARASIGN_PASSWORD', + 'ARTIFACTORY_USERNAME', + 'ARTIFACTORY_PASSWORD', + ].forEach((envVar) => { + if (!process.env[envVar]) { + throw new Error(`${envVar} is required`); + } + }); +} + +function getSigningScript() { + return path.join(__dirname, '..', 'src', './garasign.sh'); +} + +async function getSshClient(sshOptions: SSHClientOptions) { + const sshClient = new SSHClient(sshOptions); + await sshClient.connect(); + return sshClient; +} + +export async function getSigningClient( + client: T, + options: ClientOptions +): Promise { + if (client === 'remote') { + const sshClient = await getSshClient(options as SSHClientOptions); + // Currently only linux remote is supported to sign the artifacts + return new RemoteSigningClient(sshClient, { + rootDir: '~/garasign', + signingScript: getSigningScript(), + }); + } + if (client === 'local') { + // For local client, we put everything in a tmp directory to avoid + // polluting the user's working directory. + return new LocalSigningClient({ + rootDir: path.resolve(__dirname, '..', 'tmp'), + signingScript: getSigningScript(), + }); + } + throw new Error(`Unknown client type: ${client}`); +} diff --git a/packages/garasign-utils/tsconfig-lint.json b/packages/garasign-utils/tsconfig-lint.json new file mode 100644 index 00000000..6bdef84f --- /dev/null +++ b/packages/garasign-utils/tsconfig-lint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/garasign-utils/tsconfig.json b/packages/garasign-utils/tsconfig.json new file mode 100644 index 00000000..21899b27 --- /dev/null +++ b/packages/garasign-utils/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@mongodb-js/tsconfig-devtools/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist", + "allowJs": true + }, + "include": ["src/**/*"], + "exclude": ["./src/**/*.spec.*"] +} From e119980a88399c6d708618dfbea9ad888ac5b0a8 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 28 Dec 2023 11:33:12 -0700 Subject: [PATCH 03/16] fix linting, formatting and TS issues --- package-lock.json | 15 +++++++++++++++ packages/garasign-utils/package.json | 1 + .../garasign-utils/src/signing-clients/index.ts | 2 +- .../signing-clients/local-signing-client.spec.ts | 4 ++-- .../signing-clients/remote-signing-client.spec.ts | 4 ++-- .../src/signing-clients/remote-signing-client.ts | 4 ++++ packages/garasign-utils/src/ssh-client.spec.ts | 6 +++--- packages/garasign-utils/src/ssh-client.ts | 7 ++++--- packages/garasign-utils/src/utils.ts | 14 +++++++------- 9 files changed, 39 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index c5bff9f3..1082ce53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18980,6 +18980,7 @@ "version": "0.1.0", "license": "SSPL", "dependencies": { + "commander": "^11.1.0", "ssh2": "^1.15.0" }, "devDependencies": { @@ -19003,6 +19004,14 @@ "typescript": "^5.0.4" } }, + "packages/garasign-utils/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, "packages/garasign-utils/node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -22019,6 +22028,7 @@ "@types/sinon-chai": "^3.2.5", "@types/ssh2": "^1.11.18", "chai": "^4.3.6", + "commander": "^11.1.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", @@ -22030,6 +22040,11 @@ "typescript": "^5.0.4" }, "dependencies": { + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + }, "typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", diff --git a/packages/garasign-utils/package.json b/packages/garasign-utils/package.json index 67862095..198c9ea7 100644 --- a/packages/garasign-utils/package.json +++ b/packages/garasign-utils/package.json @@ -66,6 +66,7 @@ "typescript": "^5.0.4" }, "dependencies": { + "commander": "^11.1.0", "ssh2": "^1.15.0" } } diff --git a/packages/garasign-utils/src/signing-clients/index.ts b/packages/garasign-utils/src/signing-clients/index.ts index f820e93a..4cd3401f 100644 --- a/packages/garasign-utils/src/signing-clients/index.ts +++ b/packages/garasign-utils/src/signing-clients/index.ts @@ -1,4 +1,4 @@ -import { type SSHClientOptions } from '../ssh-client'; +import type { SSHClientOptions } from '../ssh-client'; export { LocalSigningClient } from './local-signing-client'; export { RemoteSigningClient } from './remote-signing-client'; diff --git a/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts b/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts index cfb1bca8..a15f1cc5 100644 --- a/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts +++ b/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts @@ -4,10 +4,10 @@ import os from 'os'; import { LocalSigningClient } from './local-signing-client'; import { expect } from 'chai'; -describe('LocalSigningClient', () => { +describe('LocalSigningClient', function () { let tmpDir: string; - beforeEach(async () => { + beforeEach(async function () { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'local-signing-client')); }); diff --git a/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts b/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts index 69225d46..8945985a 100644 --- a/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts +++ b/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts @@ -58,10 +58,10 @@ const getMockedSSHClient = () => { } as unknown as SSHClient; }; -describe('RemoteSigningClient', () => { +describe('RemoteSigningClient', function () { let tmpDir: string; - beforeEach(async () => { + beforeEach(async function () { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-signing-client')); }); diff --git a/packages/garasign-utils/src/signing-clients/remote-signing-client.ts b/packages/garasign-utils/src/signing-clients/remote-signing-client.ts index f6f024a0..520dbb12 100644 --- a/packages/garasign-utils/src/signing-clients/remote-signing-client.ts +++ b/packages/garasign-utils/src/signing-clients/remote-signing-client.ts @@ -75,9 +75,13 @@ export class RemoteSigningClient implements SigningClient { */ const cmds = [ `cd ${this.options.rootDir}`, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `export garasign_username=${process.env.GARASIGN_USERNAME}`, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `export garasign_password=${process.env.GARASIGN_PASSWORD}`, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `export artifactory_username=${process.env.ARTIFACTORY_USERNAME}`, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `export artifactory_password=${process.env.ARTIFACTORY_PASSWORD}`, `./garasign.sh ${file}`, ]; diff --git a/packages/garasign-utils/src/ssh-client.spec.ts b/packages/garasign-utils/src/ssh-client.spec.ts index 4e352a0a..7289c563 100644 --- a/packages/garasign-utils/src/ssh-client.spec.ts +++ b/packages/garasign-utils/src/ssh-client.spec.ts @@ -2,11 +2,11 @@ import sinon from 'sinon'; import { SSHClient } from './ssh-client'; import { expect } from 'chai'; -describe('SSHClient', () => { +describe('SSHClient', function () { let sshClient: SSHClient; let sandbox: sinon.SinonSandbox; - beforeEach(() => { + beforeEach(function () { const sshClientOptions = { host: 'example.com', port: 22, @@ -16,7 +16,7 @@ describe('SSHClient', () => { sandbox = sinon.createSandbox(); }); - afterEach(() => { + afterEach(function () { sandbox.restore(); }); diff --git a/packages/garasign-utils/src/ssh-client.ts b/packages/garasign-utils/src/ssh-client.ts index c67519c4..91b77e3e 100644 --- a/packages/garasign-utils/src/ssh-client.ts +++ b/packages/garasign-utils/src/ssh-client.ts @@ -1,5 +1,5 @@ -import type { SFTPWrapper } from 'ssh2'; -import { Client, type ConnectConfig } from 'ssh2'; +import type { ConnectConfig, SFTPWrapper } from 'ssh2'; +import { Client } from 'ssh2'; import { readFile } from 'fs/promises'; import { debug } from './utils'; @@ -48,7 +48,8 @@ export class SSHClient { privateKey, }); this.sshConnection.on('error', reject); - this.sshConnection.on('ready' as any, resolve); + // @ts-expect-error We expect an error here - why? + this.sshConnection.on('ready', resolve); }); } diff --git a/packages/garasign-utils/src/utils.ts b/packages/garasign-utils/src/utils.ts index dc4ac3d7..45c0be30 100644 --- a/packages/garasign-utils/src/utils.ts +++ b/packages/garasign-utils/src/utils.ts @@ -1,11 +1,11 @@ import path from 'path'; -import { SSHClient, type SSHClientOptions } from './ssh-client'; -import { - LocalSigningClient, - RemoteSigningClient, - type SigningClient, - type ClientType, - type ClientOptions, +import type { SSHClientOptions } from './ssh-client'; +import { SSHClient } from './ssh-client'; +import { LocalSigningClient, RemoteSigningClient } from './signing-clients'; +import type { + SigningClient, + ClientType, + ClientOptions, } from './signing-clients'; // eslint-disable-next-line no-console From fb43b1992166a928d959c1458f559cb771ca018a Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 28 Dec 2023 11:53:06 -0700 Subject: [PATCH 04/16] misc refactors - move client factory to the signing clients folder - use `debug` package instead of custom debug logic to prevent logs in tests --- package-lock.json | 20 ++++---- packages/garasign-utils/package.json | 3 +- packages/garasign-utils/src/garasign.sh | 7 --- packages/garasign-utils/src/index.ts | 4 +- .../src/signing-clients/index.ts | 46 +++++++++++++++++-- packages/garasign-utils/src/ssh-client.ts | 7 +-- packages/garasign-utils/src/utils.ts | 46 +------------------ 7 files changed, 62 insertions(+), 71 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1082ce53..2c59a2b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18980,7 +18980,8 @@ "version": "0.1.0", "license": "SSPL", "dependencies": { - "commander": "^11.1.0", + "commander": "^10.0.1", + "debug": "^4.3.4", "ssh2": "^1.15.0" }, "devDependencies": { @@ -19005,11 +19006,11 @@ } }, "packages/garasign-utils/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "engines": { - "node": ">=16" + "node": ">=14" } }, "packages/garasign-utils/node_modules/typescript": { @@ -22028,7 +22029,8 @@ "@types/sinon-chai": "^3.2.5", "@types/ssh2": "^1.11.18", "chai": "^4.3.6", - "commander": "^11.1.0", + "commander": "^10.0.1", + "debug": "^4.3.4", "depcheck": "^1.4.1", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", @@ -22041,9 +22043,9 @@ }, "dependencies": { "commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" }, "typescript": { "version": "5.3.3", diff --git a/packages/garasign-utils/package.json b/packages/garasign-utils/package.json index 198c9ea7..9dcf50a6 100644 --- a/packages/garasign-utils/package.json +++ b/packages/garasign-utils/package.json @@ -66,7 +66,8 @@ "typescript": "^5.0.4" }, "dependencies": { - "commander": "^11.1.0", + "commander": "^10.0.1", + "debug": "^4.3.4", "ssh2": "^1.15.0" } } diff --git a/packages/garasign-utils/src/garasign.sh b/packages/garasign-utils/src/garasign.sh index 5815beb0..3a110a58 100644 --- a/packages/garasign-utils/src/garasign.sh +++ b/packages/garasign-utils/src/garasign.sh @@ -5,18 +5,11 @@ if [ -z "$1" ]; then exit 1 fi -echo "Checking environment variables" -echo "garasign_username: ${garasign_username}" -echo "garasign_password: ${garasign_password}" -echo "artifactory_username: ${artifactory_username}" -echo "artifactory_password: ${artifactory_password}" - if [ -z ${garasign_username+omitted} ]; then echo "garasign_username is unset" && exit 1; fi if [ -z ${garasign_password+omitted} ]; then echo "garasign_password is unset" && exit 1; fi if [ -z ${artifactory_username+omitted} ]; then echo "artifactory_username is unset" && exit 1; fi if [ -z ${artifactory_password+omitted} ]; then echo "artifactory_password is unset" && exit 1; fi - ARTIFACTORY_HOST="artifactory.corp.mongodb.com" logout_artifactory() { diff --git a/packages/garasign-utils/src/index.ts b/packages/garasign-utils/src/index.ts index bf285a89..b89c0e39 100644 --- a/packages/garasign-utils/src/index.ts +++ b/packages/garasign-utils/src/index.ts @@ -1,5 +1,7 @@ import type { ClientType, ClientOptions } from './signing-clients'; -import { assertRequiredVars, getSigningClient, debug } from './utils'; + +import { getSigningClient } from './signing-clients'; +import { assertRequiredVars, debug } from './utils'; export async function sign( file: string, diff --git a/packages/garasign-utils/src/signing-clients/index.ts b/packages/garasign-utils/src/signing-clients/index.ts index 4cd3401f..219f1361 100644 --- a/packages/garasign-utils/src/signing-clients/index.ts +++ b/packages/garasign-utils/src/signing-clients/index.ts @@ -1,4 +1,9 @@ -import type { SSHClientOptions } from '../ssh-client'; +import type { ConnectConfig } from 'ssh2'; + +import * as path from 'path'; +import { SSHClient } from '../ssh-client'; +import { LocalSigningClient } from './local-signing-client'; +import { RemoteSigningClient } from './remote-signing-client'; export { LocalSigningClient } from './local-signing-client'; export { RemoteSigningClient } from './remote-signing-client'; @@ -10,9 +15,44 @@ export type SigningClientOptions = { export type ClientType = 'local' | 'remote'; export type ClientOptions = T extends 'remote' - ? Pick - : undefined; + ? Pick + : T extends 'local' + ? undefined + : never; export interface SigningClient { sign(file: string): Promise; } + +export async function getSigningClient( + clientType: T, + options: ClientOptions +): Promise { + async function getSshClient(sshOptions: ConnectConfig) { + const sshClient = new SSHClient(sshOptions); + await sshClient.connect(); + return sshClient; + } + + function getSigningScript() { + return path.join(__dirname, '..', 'src', './garasign.sh'); + } + + if (clientType === 'remote') { + const sshClient = await getSshClient(options as ConnectConfig); + // Currently only linux remote is supported to sign the artifacts + return new RemoteSigningClient(sshClient, { + rootDir: '~/garasign', + signingScript: getSigningScript(), + }); + } + if (clientType === 'local') { + // For local client, we put everything in a tmp directory to avoid + // polluting the user's working directory. + return new LocalSigningClient({ + rootDir: path.resolve(__dirname, '..', 'tmp'), + signingScript: getSigningScript(), + }); + } + throw new Error(`Unknown client type: ${clientType}`); +} diff --git a/packages/garasign-utils/src/ssh-client.ts b/packages/garasign-utils/src/ssh-client.ts index 91b77e3e..64d1e7a3 100644 --- a/packages/garasign-utils/src/ssh-client.ts +++ b/packages/garasign-utils/src/ssh-client.ts @@ -3,18 +3,13 @@ import { Client } from 'ssh2'; import { readFile } from 'fs/promises'; import { debug } from './utils'; -export type SSHClientOptions = ConnectConfig & { - // Absolute path to private key file. We will read it when connecting. - privateKey?: string; -}; - export class SSHClient { private sshConnection: Client; private sftpConnection?: SFTPWrapper; private connected = false; - constructor(private sshClientOptions: SSHClientOptions) { + constructor(private sshClientOptions: ConnectConfig) { this.sshConnection = new Client(); this.setupEventListeners(); } diff --git a/packages/garasign-utils/src/utils.ts b/packages/garasign-utils/src/utils.ts index 45c0be30..4a90c1a9 100644 --- a/packages/garasign-utils/src/utils.ts +++ b/packages/garasign-utils/src/utils.ts @@ -1,15 +1,6 @@ -import path from 'path'; -import type { SSHClientOptions } from './ssh-client'; -import { SSHClient } from './ssh-client'; -import { LocalSigningClient, RemoteSigningClient } from './signing-clients'; -import type { - SigningClient, - ClientType, - ClientOptions, -} from './signing-clients'; +import { debug as debugFn } from 'debug'; -// eslint-disable-next-line no-console -export const debug = console.log; +export const debug = debugFn('garasign-utils'); export function assertRequiredVars() { [ @@ -23,36 +14,3 @@ export function assertRequiredVars() { } }); } - -function getSigningScript() { - return path.join(__dirname, '..', 'src', './garasign.sh'); -} - -async function getSshClient(sshOptions: SSHClientOptions) { - const sshClient = new SSHClient(sshOptions); - await sshClient.connect(); - return sshClient; -} - -export async function getSigningClient( - client: T, - options: ClientOptions -): Promise { - if (client === 'remote') { - const sshClient = await getSshClient(options as SSHClientOptions); - // Currently only linux remote is supported to sign the artifacts - return new RemoteSigningClient(sshClient, { - rootDir: '~/garasign', - signingScript: getSigningScript(), - }); - } - if (client === 'local') { - // For local client, we put everything in a tmp directory to avoid - // polluting the user's working directory. - return new LocalSigningClient({ - rootDir: path.resolve(__dirname, '..', 'tmp'), - signingScript: getSigningScript(), - }); - } - throw new Error(`Unknown client type: ${client}`); -} From 71bbb5e8f78327e42bbff6bb3e3ecf838140b0d5 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 28 Dec 2023 13:56:40 -0700 Subject: [PATCH 05/16] add capibility to sign with jsign --- packages/garasign-utils/.mocharc.js | 6 +++- packages/garasign-utils/bin/sign.js | 28 +++++++++++++---- packages/garasign-utils/src/garasign.sh | 28 ++++++++++++++--- packages/garasign-utils/src/index.ts | 14 +++++---- packages/garasign-utils/src/mocha-hooks.ts | 11 +++++++ .../src/signing-clients/index.ts | 30 +++++++++++-------- .../local-signing-client.spec.ts | 5 ++++ .../signing-clients/local-signing-client.ts | 7 ++++- .../remote-signing-client.spec.ts | 5 ++++ .../signing-clients/remote-signing-client.ts | 1 + 10 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 packages/garasign-utils/src/mocha-hooks.ts diff --git a/packages/garasign-utils/.mocharc.js b/packages/garasign-utils/.mocharc.js index 64afeb1f..43001b2e 100644 --- a/packages/garasign-utils/.mocharc.js +++ b/packages/garasign-utils/.mocharc.js @@ -1 +1,5 @@ -module.exports = require('@mongodb-js/mocha-config-devtools'); +const config = require('@mongodb-js/mocha-config-devtools'); + +config.require.push(`./src/mocha-hooks.ts`); + +module.exports = config; diff --git a/packages/garasign-utils/bin/sign.js b/packages/garasign-utils/bin/sign.js index 80c2a8e6..1343ba33 100755 --- a/packages/garasign-utils/bin/sign.js +++ b/packages/garasign-utils/bin/sign.js @@ -1,10 +1,29 @@ #!/usr/bin/env node const { sign } = require('./../dist'); -const { program } = require('commander'); +const { program, InvalidArgumentError } = require('commander'); + +function parseEnumValue(name, values) { + return (value) => { + if (!values.includes(value)) { + throw new InvalidArgumentError( + `${name} must be one of ${values.join('|')}` + ); + } + }; +} program .arguments('file') - .option('-c, --client ', 'The signing client to use', 'local') + .requiredOption( + '-c, --client ', + 'The client to sign with. Can be `local` or `remote`.', + parseEnumValue('client', ['local', 'remote']) + ) + .requiredOption( + '--signing-method ', + 'The signing method to use. Can be `gpg` or `jsign`.', + parseEnumValue('signing method', ['gpg', 'jsign']) + ) .option( '-h, --host ', 'The SSH host to use when signing with remote client.' @@ -17,9 +36,6 @@ program ) .parse(process.argv); -const { client, host, username, port, privateKey } = program.opts(); const file = program.args[0]; -const options = - client === 'remote' ? { host, username, port, privateKey } : undefined; -sign(file, client, options); +sign(file, program.opts()); diff --git a/packages/garasign-utils/src/garasign.sh b/packages/garasign-utils/src/garasign.sh index 3a110a58..059cf739 100644 --- a/packages/garasign-utils/src/garasign.sh +++ b/packages/garasign-utils/src/garasign.sh @@ -9,6 +9,7 @@ if [ -z ${garasign_username+omitted} ]; then echo "garasign_username is unset" & if [ -z ${garasign_password+omitted} ]; then echo "garasign_password is unset" && exit 1; fi if [ -z ${artifactory_username+omitted} ]; then echo "artifactory_username is unset" && exit 1; fi if [ -z ${artifactory_password+omitted} ]; then echo "artifactory_password is unset" && exit 1; fi +if [ -z ${method+omitted} ]; then echo "method must either be gpg or jsign" && exit 1; fi ARTIFACTORY_HOST="artifactory.corp.mongodb.com" @@ -35,13 +36,32 @@ file=$1 echo "File to be signed: $file" echo "Working directory: $directory" -docker run \ +gpg_sign() { + docker run \ + --env-file=signing-envfile \ + --rm \ + -v $directory:$directory \ + -w $directory \ + ${ARTIFACTORY_HOST}/release-tools-container-registry-local/garasign-gpg \ + /bin/bash -c "gpgloader && gpg --yes -v --armor -o $file.sig --detach-sign $file" + + rm signing-envfile +} + +jsign_sign() { + podman run \ --env-file=signing-envfile \ --rm \ -v $directory:$directory \ -w $directory \ - ${ARTIFACTORY_HOST}/release-tools-container-registry-local/garasign-gpg \ - /bin/bash -c "gpgloader && gpg --yes -v --armor -o $file.sig --detach-sign $file" + artifactory.corp.mongodb.com/release-tools-container-registry-local/garasign-jsign \ + /bin/bash -c "jsign --tsaurl "timestamp.url" -a mongo-authenticode-2021 $file" +} + +if [[ "$method" -eq "gpg" ]]; then + gpg_sign +elif [[ "$method" -eq "jsign" ]]; then + jsign_sign +fi -rm signing-envfile echo "Finished signing $file" \ No newline at end of file diff --git a/packages/garasign-utils/src/index.ts b/packages/garasign-utils/src/index.ts index b89c0e39..2c51e69f 100644 --- a/packages/garasign-utils/src/index.ts +++ b/packages/garasign-utils/src/index.ts @@ -1,17 +1,19 @@ -import type { ClientType, ClientOptions } from './signing-clients'; +import type { ClientOptions } from './signing-clients'; import { getSigningClient } from './signing-clients'; import { assertRequiredVars, debug } from './utils'; -export async function sign( +export async function sign( file: string, - client: T, - options: ClientOptions + options: ClientOptions ): Promise { assertRequiredVars(); - debug(`Signing file: ${file} with client ${client} and options:`, options); + debug( + `Signing file: ${file} with client ${options.client} and options:`, + options + ); try { - const signingClient = await getSigningClient(client, options); + const signingClient = await getSigningClient(options); await signingClient.sign(file); } catch (err) { debug(`Error signing file: ${file}`, err); diff --git a/packages/garasign-utils/src/mocha-hooks.ts b/packages/garasign-utils/src/mocha-hooks.ts new file mode 100644 index 00000000..79ff2b7f --- /dev/null +++ b/packages/garasign-utils/src/mocha-hooks.ts @@ -0,0 +1,11 @@ +import * as os from 'os'; + +export const mochaHooks = { + beforeEach() { + // @ts-expect-error Stupid mocha binding properties to `this` + const test = this.currentTest ?? this.test; + if (os.platform().toLowerCase().includes('win32')) { + test.skip(); + } + }, +}; diff --git a/packages/garasign-utils/src/signing-clients/index.ts b/packages/garasign-utils/src/signing-clients/index.ts index 219f1361..acab73de 100644 --- a/packages/garasign-utils/src/signing-clients/index.ts +++ b/packages/garasign-utils/src/signing-clients/index.ts @@ -11,22 +11,22 @@ export { RemoteSigningClient } from './remote-signing-client'; export type SigningClientOptions = { rootDir: string; signingScript: string; + signingMethod: 'gpg' | 'jsign'; }; -export type ClientType = 'local' | 'remote'; -export type ClientOptions = T extends 'remote' - ? Pick - : T extends 'local' - ? undefined - : never; +export type ClientOptions = + | (Pick & { + signingMethod: 'gpg' | 'jsign'; + client: 'remote'; + }) + | { signingMethod: 'gpg' | 'jsign'; client: 'local' }; export interface SigningClient { sign(file: string): Promise; } -export async function getSigningClient( - clientType: T, - options: ClientOptions +export async function getSigningClient( + options: ClientOptions ): Promise { async function getSshClient(sshOptions: ConnectConfig) { const sshClient = new SSHClient(sshOptions); @@ -38,21 +38,25 @@ export async function getSigningClient( return path.join(__dirname, '..', 'src', './garasign.sh'); } - if (clientType === 'remote') { - const sshClient = await getSshClient(options as ConnectConfig); + if (options.client === 'remote') { + const sshClient = await getSshClient(options); // Currently only linux remote is supported to sign the artifacts return new RemoteSigningClient(sshClient, { rootDir: '~/garasign', signingScript: getSigningScript(), + signingMethod: options.signingMethod, }); } - if (clientType === 'local') { + if (options.client === 'local') { // For local client, we put everything in a tmp directory to avoid // polluting the user's working directory. return new LocalSigningClient({ rootDir: path.resolve(__dirname, '..', 'tmp'), signingScript: getSigningScript(), + signingMethod: options.signingMethod, }); } - throw new Error(`Unknown client type: ${clientType}`); + // @ts-expect-error `client` is a discriminated union - we should never reach here but we throw on the off-chance we do. + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unknown client type: ${options.client}`); } diff --git a/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts b/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts index a15f1cc5..20c25176 100644 --- a/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts +++ b/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts @@ -11,6 +11,10 @@ describe('LocalSigningClient', function () { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'local-signing-client')); }); + afterEach(async function () { + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + it('signs the file correctly', async function () { // In order to sign a file locally, we setup the following: // 1. Create a tmp directory, tmp file to sign and a tmp signing script @@ -38,6 +42,7 @@ describe('LocalSigningClient', function () { const localSigningClient = new LocalSigningClient({ rootDir: tmpDir, signingScript: signingScript, + signingMethod: 'gpg', }); await localSigningClient.sign(fileToSign); diff --git a/packages/garasign-utils/src/signing-clients/local-signing-client.ts b/packages/garasign-utils/src/signing-clients/local-signing-client.ts index df6df601..0ad023fd 100644 --- a/packages/garasign-utils/src/signing-clients/local-signing-client.ts +++ b/packages/garasign-utils/src/signing-clients/local-signing-client.ts @@ -32,7 +32,12 @@ export class LocalSigningClient implements SigningClient { debug(`LocalSigningClient: Copied file ${file} to ${remotePath}`); await execAsync( - `cd ${this.options.rootDir} && ./garasign.sh ${path.basename(file)}` + `cd ${this.options.rootDir} && ./garasign.sh ${path.basename(file)}`, + { + env: { + method: this.options.signingMethod, + }, + } ); debug(`LocalSigningClient: Signed file ${remotePath}`); diff --git a/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts b/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts index 8945985a..5031c02a 100644 --- a/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts +++ b/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts @@ -65,6 +65,10 @@ describe('RemoteSigningClient', function () { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-signing-client')); }); + afterEach(async function () { + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + it('signs the file correctly', async function () { const fileToSign = path.join(tmpDir, 'originals', 'file-to-sign.txt'); const signingScript = path.join(tmpDir, 'originals', 'script.sh'); @@ -85,6 +89,7 @@ describe('RemoteSigningClient', function () { const remoteSigningClient = new RemoteSigningClient(getMockedSSHClient(), { rootDir: tmpDir, signingScript: signingScript, + signingMethod: 'gpg', }); await remoteSigningClient.sign(fileToSign); diff --git a/packages/garasign-utils/src/signing-clients/remote-signing-client.ts b/packages/garasign-utils/src/signing-clients/remote-signing-client.ts index 520dbb12..c44cab41 100644 --- a/packages/garasign-utils/src/signing-clients/remote-signing-client.ts +++ b/packages/garasign-utils/src/signing-clients/remote-signing-client.ts @@ -83,6 +83,7 @@ export class RemoteSigningClient implements SigningClient { `export artifactory_username=${process.env.ARTIFACTORY_USERNAME}`, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `export artifactory_password=${process.env.ARTIFACTORY_PASSWORD}`, + `export method=${this.options.signingMethod}`, `./garasign.sh ${file}`, ]; const command = cmds.join(' && '); From 9038082cffa3b0c3f9b9534c6302546086de58b0 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 2 Jan 2024 07:46:34 -0700 Subject: [PATCH 06/16] rename package to signing-utils --- package-lock.json | 16 ++++++++-------- .../.depcheckrc | 0 .../.eslintignore | 0 .../.eslintrc.js | 0 .../.mocharc.js | 0 .../.prettierignore | 0 .../.prettierrc.json | 0 .../{garasign-utils => signing-utils}/LICENSE | 0 .../bin/sign.js | 0 .../package.json | 2 +- .../src/garasign.sh | 0 .../src/index.spec.ts | 0 .../src/index.ts | 4 ++-- .../src/mocha-hooks.ts | 0 .../src/signing-clients/index.ts | 0 .../signing-clients/local-signing-client.spec.ts | 0 .../src/signing-clients/local-signing-client.ts | 0 .../remote-signing-client.spec.ts | 0 .../src/signing-clients/remote-signing-client.ts | 0 .../src/ssh-client.spec.ts | 0 .../src/ssh-client.ts | 0 .../src/utils.ts | 2 +- .../tsconfig-lint.json | 0 .../tsconfig.json | 0 24 files changed, 12 insertions(+), 12 deletions(-) rename packages/{garasign-utils => signing-utils}/.depcheckrc (100%) rename packages/{garasign-utils => signing-utils}/.eslintignore (100%) rename packages/{garasign-utils => signing-utils}/.eslintrc.js (100%) rename packages/{garasign-utils => signing-utils}/.mocharc.js (100%) rename packages/{garasign-utils => signing-utils}/.prettierignore (100%) rename packages/{garasign-utils => signing-utils}/.prettierrc.json (100%) rename packages/{garasign-utils => signing-utils}/LICENSE (100%) rename packages/{garasign-utils => signing-utils}/bin/sign.js (100%) rename packages/{garasign-utils => signing-utils}/package.json (98%) rename packages/{garasign-utils => signing-utils}/src/garasign.sh (100%) rename packages/{garasign-utils => signing-utils}/src/index.spec.ts (100%) rename packages/{garasign-utils => signing-utils}/src/index.ts (82%) rename packages/{garasign-utils => signing-utils}/src/mocha-hooks.ts (100%) rename packages/{garasign-utils => signing-utils}/src/signing-clients/index.ts (100%) rename packages/{garasign-utils => signing-utils}/src/signing-clients/local-signing-client.spec.ts (100%) rename packages/{garasign-utils => signing-utils}/src/signing-clients/local-signing-client.ts (100%) rename packages/{garasign-utils => signing-utils}/src/signing-clients/remote-signing-client.spec.ts (100%) rename packages/{garasign-utils => signing-utils}/src/signing-clients/remote-signing-client.ts (100%) rename packages/{garasign-utils => signing-utils}/src/ssh-client.spec.ts (100%) rename packages/{garasign-utils => signing-utils}/src/ssh-client.ts (100%) rename packages/{garasign-utils => signing-utils}/src/utils.ts (86%) rename packages/{garasign-utils => signing-utils}/tsconfig-lint.json (100%) rename packages/{garasign-utils => signing-utils}/tsconfig.json (100%) diff --git a/package-lock.json b/package-lock.json index 2c59a2b5..d99d5a17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2978,8 +2978,8 @@ "resolved": "configs/eslint-config-devtools", "link": true }, - "node_modules/@mongodb-js/garasign-utils": { - "resolved": "packages/garasign-utils", + "node_modules/@mongodb-js/signing-utils": { + "resolved": "packages/signing-utils", "link": true }, "node_modules/@mongodb-js/get-os-info": { @@ -18975,8 +18975,8 @@ "node": ">=14.17" } }, - "packages/garasign-utils": { - "name": "@mongodb-js/garasign-utils", + "packages/signing-utils": { + "name": "@mongodb-js/signing-utils", "version": "0.1.0", "license": "SSPL", "dependencies": { @@ -19005,7 +19005,7 @@ "typescript": "^5.0.4" } }, - "packages/garasign-utils/node_modules/commander": { + "packages/signing-utils/node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", @@ -19013,7 +19013,7 @@ "node": ">=14" } }, - "packages/garasign-utils/node_modules/typescript": { + "packages/signing-utils/node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", @@ -22016,8 +22016,8 @@ "prettier": "2.3.2" } }, - "@mongodb-js/garasign-utils": { - "version": "file:packages/garasign-utils", + "@mongodb-js/signing-utils": { + "version": "file:packages/signing-utils", "requires": { "@mongodb-js/eslint-config-devtools": "0.9.10", "@mongodb-js/mocha-config-devtools": "^1.0.2", diff --git a/packages/garasign-utils/.depcheckrc b/packages/signing-utils/.depcheckrc similarity index 100% rename from packages/garasign-utils/.depcheckrc rename to packages/signing-utils/.depcheckrc diff --git a/packages/garasign-utils/.eslintignore b/packages/signing-utils/.eslintignore similarity index 100% rename from packages/garasign-utils/.eslintignore rename to packages/signing-utils/.eslintignore diff --git a/packages/garasign-utils/.eslintrc.js b/packages/signing-utils/.eslintrc.js similarity index 100% rename from packages/garasign-utils/.eslintrc.js rename to packages/signing-utils/.eslintrc.js diff --git a/packages/garasign-utils/.mocharc.js b/packages/signing-utils/.mocharc.js similarity index 100% rename from packages/garasign-utils/.mocharc.js rename to packages/signing-utils/.mocharc.js diff --git a/packages/garasign-utils/.prettierignore b/packages/signing-utils/.prettierignore similarity index 100% rename from packages/garasign-utils/.prettierignore rename to packages/signing-utils/.prettierignore diff --git a/packages/garasign-utils/.prettierrc.json b/packages/signing-utils/.prettierrc.json similarity index 100% rename from packages/garasign-utils/.prettierrc.json rename to packages/signing-utils/.prettierrc.json diff --git a/packages/garasign-utils/LICENSE b/packages/signing-utils/LICENSE similarity index 100% rename from packages/garasign-utils/LICENSE rename to packages/signing-utils/LICENSE diff --git a/packages/garasign-utils/bin/sign.js b/packages/signing-utils/bin/sign.js similarity index 100% rename from packages/garasign-utils/bin/sign.js rename to packages/signing-utils/bin/sign.js diff --git a/packages/garasign-utils/package.json b/packages/signing-utils/package.json similarity index 98% rename from packages/garasign-utils/package.json rename to packages/signing-utils/package.json index 9dcf50a6..dcb4b4ff 100644 --- a/packages/garasign-utils/package.json +++ b/packages/signing-utils/package.json @@ -1,5 +1,5 @@ { - "name": "@mongodb-js/garasign-utils", + "name": "@mongodb-js/signing-utils", "description": "Utilities for signing packages in CI with Garasign.", "author": { "name": "MongoDB Inc", diff --git a/packages/garasign-utils/src/garasign.sh b/packages/signing-utils/src/garasign.sh similarity index 100% rename from packages/garasign-utils/src/garasign.sh rename to packages/signing-utils/src/garasign.sh diff --git a/packages/garasign-utils/src/index.spec.ts b/packages/signing-utils/src/index.spec.ts similarity index 100% rename from packages/garasign-utils/src/index.spec.ts rename to packages/signing-utils/src/index.spec.ts diff --git a/packages/garasign-utils/src/index.ts b/packages/signing-utils/src/index.ts similarity index 82% rename from packages/garasign-utils/src/index.ts rename to packages/signing-utils/src/index.ts index 2c51e69f..c712721b 100644 --- a/packages/garasign-utils/src/index.ts +++ b/packages/signing-utils/src/index.ts @@ -1,11 +1,11 @@ -import type { ClientOptions } from './signing-clients'; +import type { ClientOptions as SigningOptions } from './signing-clients'; import { getSigningClient } from './signing-clients'; import { assertRequiredVars, debug } from './utils'; export async function sign( file: string, - options: ClientOptions + options: SigningOptions ): Promise { assertRequiredVars(); debug( diff --git a/packages/garasign-utils/src/mocha-hooks.ts b/packages/signing-utils/src/mocha-hooks.ts similarity index 100% rename from packages/garasign-utils/src/mocha-hooks.ts rename to packages/signing-utils/src/mocha-hooks.ts diff --git a/packages/garasign-utils/src/signing-clients/index.ts b/packages/signing-utils/src/signing-clients/index.ts similarity index 100% rename from packages/garasign-utils/src/signing-clients/index.ts rename to packages/signing-utils/src/signing-clients/index.ts diff --git a/packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts b/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts similarity index 100% rename from packages/garasign-utils/src/signing-clients/local-signing-client.spec.ts rename to packages/signing-utils/src/signing-clients/local-signing-client.spec.ts diff --git a/packages/garasign-utils/src/signing-clients/local-signing-client.ts b/packages/signing-utils/src/signing-clients/local-signing-client.ts similarity index 100% rename from packages/garasign-utils/src/signing-clients/local-signing-client.ts rename to packages/signing-utils/src/signing-clients/local-signing-client.ts diff --git a/packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts b/packages/signing-utils/src/signing-clients/remote-signing-client.spec.ts similarity index 100% rename from packages/garasign-utils/src/signing-clients/remote-signing-client.spec.ts rename to packages/signing-utils/src/signing-clients/remote-signing-client.spec.ts diff --git a/packages/garasign-utils/src/signing-clients/remote-signing-client.ts b/packages/signing-utils/src/signing-clients/remote-signing-client.ts similarity index 100% rename from packages/garasign-utils/src/signing-clients/remote-signing-client.ts rename to packages/signing-utils/src/signing-clients/remote-signing-client.ts diff --git a/packages/garasign-utils/src/ssh-client.spec.ts b/packages/signing-utils/src/ssh-client.spec.ts similarity index 100% rename from packages/garasign-utils/src/ssh-client.spec.ts rename to packages/signing-utils/src/ssh-client.spec.ts diff --git a/packages/garasign-utils/src/ssh-client.ts b/packages/signing-utils/src/ssh-client.ts similarity index 100% rename from packages/garasign-utils/src/ssh-client.ts rename to packages/signing-utils/src/ssh-client.ts diff --git a/packages/garasign-utils/src/utils.ts b/packages/signing-utils/src/utils.ts similarity index 86% rename from packages/garasign-utils/src/utils.ts rename to packages/signing-utils/src/utils.ts index 4a90c1a9..190b45bb 100644 --- a/packages/garasign-utils/src/utils.ts +++ b/packages/signing-utils/src/utils.ts @@ -1,6 +1,6 @@ import { debug as debugFn } from 'debug'; -export const debug = debugFn('garasign-utils'); +export const debug = debugFn('signing-utils'); export function assertRequiredVars() { [ diff --git a/packages/garasign-utils/tsconfig-lint.json b/packages/signing-utils/tsconfig-lint.json similarity index 100% rename from packages/garasign-utils/tsconfig-lint.json rename to packages/signing-utils/tsconfig-lint.json diff --git a/packages/garasign-utils/tsconfig.json b/packages/signing-utils/tsconfig.json similarity index 100% rename from packages/garasign-utils/tsconfig.json rename to packages/signing-utils/tsconfig.json From 49e11245240af8e212d305d343b2fd66b86ebdb6 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 3 Jan 2024 10:26:17 -0700 Subject: [PATCH 07/16] address comments --- package-lock.json | 188 +++++++++--------- packages/signing-utils/bin/sign.js | 40 +--- packages/signing-utils/src/cli.ts | 48 +++++ packages/signing-utils/src/garasign.sh | 2 +- packages/signing-utils/src/index.ts | 4 +- packages/signing-utils/src/mocha-hooks.ts | 7 +- .../src/signing-clients/index.ts | 16 +- .../signing-clients/local-signing-client.ts | 19 +- .../signing-clients/remote-signing-client.ts | 11 +- packages/signing-utils/src/utils.ts | 27 ++- 10 files changed, 188 insertions(+), 174 deletions(-) create mode 100755 packages/signing-utils/src/cli.ts diff --git a/package-lock.json b/package-lock.json index d99d5a17..19feec57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2978,10 +2978,6 @@ "resolved": "configs/eslint-config-devtools", "link": true }, - "node_modules/@mongodb-js/signing-utils": { - "resolved": "packages/signing-utils", - "link": true - }, "node_modules/@mongodb-js/get-os-info": { "resolved": "packages/get-os-info", "link": true @@ -3018,6 +3014,10 @@ "resolved": "packages/sbom-tools", "link": true }, + "node_modules/@mongodb-js/signing-utils": { + "resolved": "packages/signing-utils", + "link": true + }, "node_modules/@mongodb-js/tsconfig-devtools": { "resolved": "configs/tsconfig-devtools", "link": true @@ -18975,57 +18975,6 @@ "node": ">=14.17" } }, - "packages/signing-utils": { - "name": "@mongodb-js/signing-utils", - "version": "0.1.0", - "license": "SSPL", - "dependencies": { - "commander": "^10.0.1", - "debug": "^4.3.4", - "ssh2": "^1.15.0" - }, - "devDependencies": { - "@mongodb-js/eslint-config-devtools": "0.9.10", - "@mongodb-js/mocha-config-devtools": "^1.0.2", - "@mongodb-js/prettier-config-devtools": "^1.0.1", - "@mongodb-js/tsconfig-devtools": "^1.0.1", - "@types/chai": "^4.2.21", - "@types/mocha": "^9.0.0", - "@types/node": "^17.0.35", - "@types/sinon-chai": "^3.2.5", - "@types/ssh2": "^1.11.18", - "chai": "^4.3.6", - "depcheck": "^1.4.1", - "eslint": "^7.25.0", - "gen-esm-wrapper": "^1.1.0", - "mocha": "^8.4.0", - "nyc": "^15.1.0", - "prettier": "^2.3.2", - "sinon": "^9.2.3", - "typescript": "^5.0.4" - } - }, - "packages/signing-utils/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "engines": { - "node": ">=14" - } - }, - "packages/signing-utils/node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "packages/get-os-info": { "name": "@mongodb-js/get-os-info", "version": "0.3.22", @@ -19932,6 +19881,57 @@ "typescript": "^4.3.5" } }, + "packages/signing-utils": { + "name": "@mongodb-js/signing-utils", + "version": "0.1.0", + "license": "SSPL", + "dependencies": { + "commander": "^10.0.1", + "debug": "^4.3.4", + "ssh2": "^1.15.0" + }, + "devDependencies": { + "@mongodb-js/eslint-config-devtools": "0.9.10", + "@mongodb-js/mocha-config-devtools": "^1.0.2", + "@mongodb-js/prettier-config-devtools": "^1.0.1", + "@mongodb-js/tsconfig-devtools": "^1.0.1", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.0.0", + "@types/node": "^17.0.35", + "@types/sinon-chai": "^3.2.5", + "@types/ssh2": "^1.11.18", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "mocha": "^8.4.0", + "nyc": "^15.1.0", + "prettier": "^2.3.2", + "sinon": "^9.2.3", + "typescript": "^5.0.4" + } + }, + "packages/signing-utils/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "packages/signing-utils/node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "scripts": { "name": "@mongodb-js/devtools-scripts", "version": "0.2.11", @@ -22016,45 +22016,6 @@ "prettier": "2.3.2" } }, - "@mongodb-js/signing-utils": { - "version": "file:packages/signing-utils", - "requires": { - "@mongodb-js/eslint-config-devtools": "0.9.10", - "@mongodb-js/mocha-config-devtools": "^1.0.2", - "@mongodb-js/prettier-config-devtools": "^1.0.1", - "@mongodb-js/tsconfig-devtools": "^1.0.1", - "@types/chai": "^4.2.21", - "@types/mocha": "^9.0.0", - "@types/node": "^17.0.35", - "@types/sinon-chai": "^3.2.5", - "@types/ssh2": "^1.11.18", - "chai": "^4.3.6", - "commander": "^10.0.1", - "debug": "^4.3.4", - "depcheck": "^1.4.1", - "eslint": "^7.25.0", - "gen-esm-wrapper": "^1.1.0", - "mocha": "^8.4.0", - "nyc": "^15.1.0", - "prettier": "^2.3.2", - "sinon": "^9.2.3", - "ssh2": "^1.15.0", - "typescript": "^5.0.4" - }, - "dependencies": { - "commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" - }, - "typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true - } - } - }, "@mongodb-js/get-os-info": { "version": "file:packages/get-os-info", "requires": { @@ -22624,6 +22585,45 @@ } } }, + "@mongodb-js/signing-utils": { + "version": "file:packages/signing-utils", + "requires": { + "@mongodb-js/eslint-config-devtools": "0.9.10", + "@mongodb-js/mocha-config-devtools": "^1.0.2", + "@mongodb-js/prettier-config-devtools": "^1.0.1", + "@mongodb-js/tsconfig-devtools": "^1.0.1", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.0.0", + "@types/node": "^17.0.35", + "@types/sinon-chai": "^3.2.5", + "@types/ssh2": "^1.11.18", + "chai": "^4.3.6", + "commander": "^10.0.1", + "debug": "^4.3.4", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "mocha": "^8.4.0", + "nyc": "^15.1.0", + "prettier": "^2.3.2", + "sinon": "^9.2.3", + "ssh2": "^1.15.0", + "typescript": "^5.0.4" + }, + "dependencies": { + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" + }, + "typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true + } + } + }, "@mongodb-js/tsconfig-devtools": { "version": "file:configs/tsconfig-devtools", "requires": { diff --git a/packages/signing-utils/bin/sign.js b/packages/signing-utils/bin/sign.js index 1343ba33..96180310 100755 --- a/packages/signing-utils/bin/sign.js +++ b/packages/signing-utils/bin/sign.js @@ -1,41 +1,3 @@ #!/usr/bin/env node -const { sign } = require('./../dist'); -const { program, InvalidArgumentError } = require('commander'); -function parseEnumValue(name, values) { - return (value) => { - if (!values.includes(value)) { - throw new InvalidArgumentError( - `${name} must be one of ${values.join('|')}` - ); - } - }; -} - -program - .arguments('file') - .requiredOption( - '-c, --client ', - 'The client to sign with. Can be `local` or `remote`.', - parseEnumValue('client', ['local', 'remote']) - ) - .requiredOption( - '--signing-method ', - 'The signing method to use. Can be `gpg` or `jsign`.', - parseEnumValue('signing method', ['gpg', 'jsign']) - ) - .option( - '-h, --host ', - 'The SSH host to use when signing with remote client.' - ) - .option('-u, --username ', 'The SSH host username.') - .option('-p, --port ', 'The SSH host port.') - .option( - '-k, --private-key ', - 'The SSH private key to use when signing with remote client.' - ) - .parse(process.argv); - -const file = program.args[0]; - -sign(file, program.opts()); +require('../dist/cli.js'); diff --git a/packages/signing-utils/src/cli.ts b/packages/signing-utils/src/cli.ts new file mode 100755 index 00000000..fe072fb9 --- /dev/null +++ b/packages/signing-utils/src/cli.ts @@ -0,0 +1,48 @@ +import { InvalidArgumentError, program } from 'commander'; +import { sign } from '.'; + +function parseEnumValue(name: string, values: string[]) { + return (value: unknown) => { + if (typeof value !== 'string' || !values.includes(value)) { + throw new InvalidArgumentError( + `${name} must be one of ${values.join('|')}` + ); + } + }; +} + +program + .arguments('file') + .requiredOption( + '-c, --client ', + 'The client to sign with. Can be `local` or `remote`.', + parseEnumValue('client', ['local', 'remote']) + ) + .requiredOption( + '--signing-method ', + 'The signing method to use. Can be `gpg` or `jsign`.', + parseEnumValue('signing method', ['gpg', 'jsign']) + ) + .option( + '-h, --host ', + 'The SSH host to use when signing with remote client.' + ) + .option('-u, --username ', 'The SSH host username.') + .option('-p, --port ', 'The SSH host port.') + .option( + '-k, --private-key ', + 'The SSH private key to use when signing with remote client.' + ) + .parse(process.argv); + +const file = program.args[0]; + +sign(file, program.opts()).then( + () => { + // do nothing + }, + (error) => { + // eslint-disable-next-line no-console + console.error({ error }); + } +); diff --git a/packages/signing-utils/src/garasign.sh b/packages/signing-utils/src/garasign.sh index 059cf739..515000ac 100644 --- a/packages/signing-utils/src/garasign.sh +++ b/packages/signing-utils/src/garasign.sh @@ -49,7 +49,7 @@ gpg_sign() { } jsign_sign() { - podman run \ + docker run \ --env-file=signing-envfile \ --rm \ -v $directory:$directory \ diff --git a/packages/signing-utils/src/index.ts b/packages/signing-utils/src/index.ts index c712721b..666077b0 100644 --- a/packages/signing-utils/src/index.ts +++ b/packages/signing-utils/src/index.ts @@ -1,13 +1,13 @@ import type { ClientOptions as SigningOptions } from './signing-clients'; import { getSigningClient } from './signing-clients'; -import { assertRequiredVars, debug } from './utils'; +import { getEnv, debug } from './utils'; export async function sign( file: string, options: SigningOptions ): Promise { - assertRequiredVars(); + getEnv(); debug( `Signing file: ${file} with client ${options.client} and options:`, options diff --git a/packages/signing-utils/src/mocha-hooks.ts b/packages/signing-utils/src/mocha-hooks.ts index 79ff2b7f..f52c2619 100644 --- a/packages/signing-utils/src/mocha-hooks.ts +++ b/packages/signing-utils/src/mocha-hooks.ts @@ -1,11 +1,10 @@ import * as os from 'os'; -export const mochaHooks = { +export const mochaHooks: { beforeEach: Mocha.HookFunction } = { beforeEach() { - // @ts-expect-error Stupid mocha binding properties to `this` - const test = this.currentTest ?? this.test; if (os.platform().toLowerCase().includes('win32')) { - test.skip(); + // @ts-expect-error TS does not know mocha will properly set `this` when it invokes beforeEach + this.skip(); } }, }; diff --git a/packages/signing-utils/src/signing-clients/index.ts b/packages/signing-utils/src/signing-clients/index.ts index acab73de..71d99b68 100644 --- a/packages/signing-utils/src/signing-clients/index.ts +++ b/packages/signing-utils/src/signing-clients/index.ts @@ -8,18 +8,20 @@ import { RemoteSigningClient } from './remote-signing-client'; export { LocalSigningClient } from './local-signing-client'; export { RemoteSigningClient } from './remote-signing-client'; +export type SigningMethod = 'gpg' | 'jsign'; + export type SigningClientOptions = { rootDir: string; signingScript: string; - signingMethod: 'gpg' | 'jsign'; + signingMethod: SigningMethod; }; export type ClientOptions = | (Pick & { - signingMethod: 'gpg' | 'jsign'; + signingMethod: SigningMethod; client: 'remote'; }) - | { signingMethod: 'gpg' | 'jsign'; client: 'local' }; + | { signingMethod: SigningMethod; client: 'local' }; export interface SigningClient { sign(file: string): Promise; @@ -34,16 +36,14 @@ export async function getSigningClient( return sshClient; } - function getSigningScript() { - return path.join(__dirname, '..', 'src', './garasign.sh'); - } + const signingScript = path.join(__dirname, '..', 'src', './garasign.sh'); if (options.client === 'remote') { const sshClient = await getSshClient(options); // Currently only linux remote is supported to sign the artifacts return new RemoteSigningClient(sshClient, { rootDir: '~/garasign', - signingScript: getSigningScript(), + signingScript, signingMethod: options.signingMethod, }); } @@ -52,7 +52,7 @@ export async function getSigningClient( // polluting the user's working directory. return new LocalSigningClient({ rootDir: path.resolve(__dirname, '..', 'tmp'), - signingScript: getSigningScript(), + signingScript, signingMethod: options.signingMethod, }); } diff --git a/packages/signing-utils/src/signing-clients/local-signing-client.ts b/packages/signing-utils/src/signing-clients/local-signing-client.ts index 0ad023fd..72907c36 100644 --- a/packages/signing-utils/src/signing-clients/local-signing-client.ts +++ b/packages/signing-utils/src/signing-clients/local-signing-client.ts @@ -1,7 +1,7 @@ import path from 'path'; -import { exec } from 'child_process'; +import { exec, execFileSync } from 'child_process'; import { promisify } from 'util'; -import { debug } from '../utils'; +import { debug, getEnv } from '../utils'; import type { SigningClient, SigningClientOptions } from '.'; const execAsync = promisify(exec); @@ -31,14 +31,13 @@ export class LocalSigningClient implements SigningClient { await this.copyFile(file, remotePath); debug(`LocalSigningClient: Copied file ${file} to ${remotePath}`); - await execAsync( - `cd ${this.options.rootDir} && ./garasign.sh ${path.basename(file)}`, - { - env: { - method: this.options.signingMethod, - }, - } - ); + execFileSync('./garasign.sh', [path.basename(file)], { + cwd: this.options.rootDir, + env: { + ...getEnv(), + method: this.options.signingMethod, + }, + }); debug(`LocalSigningClient: Signed file ${remotePath}`); await this.copyFile(remotePath, file); diff --git a/packages/signing-utils/src/signing-clients/remote-signing-client.ts b/packages/signing-utils/src/signing-clients/remote-signing-client.ts index c44cab41..adeb7fcf 100644 --- a/packages/signing-utils/src/signing-clients/remote-signing-client.ts +++ b/packages/signing-utils/src/signing-clients/remote-signing-client.ts @@ -1,7 +1,7 @@ import path from 'path'; import type { SFTPWrapper } from 'ssh2'; import type { SSHClient } from '../ssh-client'; -import { debug } from '../utils'; +import { debug, getEnv } from '../utils'; import type { SigningClient, SigningClientOptions } from '.'; export class RemoteSigningClient implements SigningClient { @@ -68,6 +68,7 @@ export class RemoteSigningClient implements SigningClient { } private async signRemoteFile(file: string) { + const env = getEnv(); /** * Passing env variables as an option to ssh.exec() doesn't work as ssh config * (`sshd_config.AllowEnv`) does not allow to pass env variables by default. @@ -76,13 +77,13 @@ export class RemoteSigningClient implements SigningClient { const cmds = [ `cd ${this.options.rootDir}`, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `export garasign_username=${process.env.GARASIGN_USERNAME}`, + `export garasign_username=${env.garasign_username}`, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `export garasign_password=${process.env.GARASIGN_PASSWORD}`, + `export garasign_password=${env.garasign_password}`, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `export artifactory_username=${process.env.ARTIFACTORY_USERNAME}`, + `export artifactory_username=${env.artifactory_username}`, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `export artifactory_password=${process.env.ARTIFACTORY_PASSWORD}`, + `export artifactory_password=${env.artifactory_password}`, `export method=${this.options.signingMethod}`, `./garasign.sh ${file}`, ]; diff --git a/packages/signing-utils/src/utils.ts b/packages/signing-utils/src/utils.ts index 190b45bb..f4919932 100644 --- a/packages/signing-utils/src/utils.ts +++ b/packages/signing-utils/src/utils.ts @@ -2,15 +2,20 @@ import { debug as debugFn } from 'debug'; export const debug = debugFn('signing-utils'); -export function assertRequiredVars() { - [ - 'GARASIGN_USERNAME', - 'GARASIGN_PASSWORD', - 'ARTIFACTORY_USERNAME', - 'ARTIFACTORY_PASSWORD', - ].forEach((envVar) => { - if (!process.env[envVar]) { - throw new Error(`${envVar} is required`); - } - }); +export function getEnv() { + const garasign_username = + process.env['GARASIGN_USERNAME'] ?? process.env['garasign_username']; + const garasign_password = + process.env['GARASIGN_PASSWORD'] ?? process.env['garasign_password']; + const artifactory_username = + process.env['ARTIFACTORY_USERNAME'] ?? process.env['artifactory_username']; + const artifactory_password = + process.env['ARTIFACTORY_PASSWORD'] ?? process.env['artifactory_password']; + + return { + garasign_username, + garasign_password, + artifactory_username, + artifactory_password, + }; } From d6e63cb8ea12b30edd186f30999c43fef40bd969 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 3 Jan 2024 10:32:43 -0700 Subject: [PATCH 08/16] use -e istead of signing file in garasign script --- packages/signing-utils/src/garasign.sh | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/signing-utils/src/garasign.sh b/packages/signing-utils/src/garasign.sh index 515000ac..abbbba60 100644 --- a/packages/signing-utils/src/garasign.sh +++ b/packages/signing-utils/src/garasign.sh @@ -25,11 +25,6 @@ echo "${artifactory_password}" | docker login --password-stdin --username ${arti # If the docker login failed, exit [ $? -ne 0 ] && exit $? -cat < signing-envfile -GRS_CONFIG_USER1_USERNAME=${garasign_username} -GRS_CONFIG_USER1_PASSWORD=${garasign_password} -EOL - directory=$(pwd) file=$1 @@ -38,24 +33,24 @@ echo "Working directory: $directory" gpg_sign() { docker run \ - --env-file=signing-envfile \ + -e GRS_CONFIG_USER1_USERNAME="${garasign_username}" \ + -e GRS_CONFIG_USER1_PASSWORD="${garasign_password}" \ --rm \ -v $directory:$directory \ -w $directory \ ${ARTIFACTORY_HOST}/release-tools-container-registry-local/garasign-gpg \ /bin/bash -c "gpgloader && gpg --yes -v --armor -o $file.sig --detach-sign $file" - - rm signing-envfile } jsign_sign() { docker run \ - --env-file=signing-envfile \ - --rm \ - -v $directory:$directory \ - -w $directory \ - artifactory.corp.mongodb.com/release-tools-container-registry-local/garasign-jsign \ - /bin/bash -c "jsign --tsaurl "timestamp.url" -a mongo-authenticode-2021 $file" + -e GRS_CONFIG_USER1_USERNAME="${garasign_username}" \ + -e GRS_CONFIG_USER1_PASSWORD="${garasign_password}" \ + --rm \ + -v $directory:$directory \ + -w $directory \ + artifactory.corp.mongodb.com/release-tools-container-registry-local/garasign-jsign \ + /bin/bash -c "jsign --tsaurl "timestamp.url" -a mongo-authenticode-2021 $file" } if [[ "$method" -eq "gpg" ]]; then From 46c441c91e4a5691782b37750e94c47efaced850 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 3 Jan 2024 10:39:19 -0700 Subject: [PATCH 09/16] add doc comments for public API --- packages/signing-utils/src/index.ts | 13 ++++++---- .../src/signing-clients/index.ts | 24 ++++++++++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/signing-utils/src/index.ts b/packages/signing-utils/src/index.ts index 666077b0..81ab86e6 100644 --- a/packages/signing-utils/src/index.ts +++ b/packages/signing-utils/src/index.ts @@ -1,13 +1,18 @@ -import type { ClientOptions as SigningOptions } from './signing-clients'; +import type { ClientOptions } from './signing-clients'; import { getSigningClient } from './signing-clients'; -import { getEnv, debug } from './utils'; +import { debug } from './utils'; +/** + * Signs a file using Garasign. + * + * @param file the name of the file to sign + * @param options options to sign with - see the docs for `SigningOptions` + */ export async function sign( file: string, - options: SigningOptions + options: ClientOptions ): Promise { - getEnv(); debug( `Signing file: ${file} with client ${options.client} and options:`, options diff --git a/packages/signing-utils/src/signing-clients/index.ts b/packages/signing-utils/src/signing-clients/index.ts index 71d99b68..e7ed4a13 100644 --- a/packages/signing-utils/src/signing-clients/index.ts +++ b/packages/signing-utils/src/signing-clients/index.ts @@ -16,12 +16,24 @@ export type SigningClientOptions = { signingMethod: SigningMethod; }; -export type ClientOptions = - | (Pick & { - signingMethod: SigningMethod; - client: 'remote'; - }) - | { signingMethod: SigningMethod; client: 'local' }; +/** Options for signing a file remotely over an SSH connection. */ +export type RemoteSigningOptions = Pick< + ConnectConfig, + 'username' | 'host' | 'privateKey' | 'port' +> & { + /** The method to sign with. Use gpg on linux and jsign on windows. */ + signingMethod: SigningMethod; + client: 'remote'; +}; + +/** Options for signing a file locally. */ +export type LocalSigningOptions = { + /** The method to sign with. Use gpg on linux and jsign on windows. */ + signingMethod: SigningMethod; + client: 'local'; +}; + +export type ClientOptions = RemoteSigningOptions | LocalSigningOptions; export interface SigningClient { sign(file: string): Promise; From 42486309cd682909526d5a2df70813f63ee0f6dd Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 3 Jan 2024 12:18:36 -0700 Subject: [PATCH 10/16] fix failing tests --- package-lock.json | 14 ++-- package.json | 2 +- packages/signing-utils/package.json | 3 +- packages/signing-utils/src/index.spec.ts | 0 packages/signing-utils/src/mocha-hooks.ts | 10 --- .../src/signing-clients/index.ts | 25 ++++--- .../local-signing-client.spec.ts | 55 +++++++--------- .../signing-clients/local-signing-client.ts | 66 +++++++++---------- .../remote-signing-client.spec.ts | 44 ++++++------- .../signing-clients/remote-signing-client.ts | 14 ++-- packages/signing-utils/src/utils.ts | 9 +++ 11 files changed, 119 insertions(+), 123 deletions(-) delete mode 100644 packages/signing-utils/src/index.spec.ts delete mode 100644 packages/signing-utils/src/mocha-hooks.ts diff --git a/package-lock.json b/package-lock.json index 19feec57..e2320344 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4127,9 +4127,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "version": "18.19.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.4.tgz", + "integrity": "sha512-xNzlUhzoHotIsnFoXmJB+yWmBvFZgKCI9TtPIEdYIMM1KWfwuY8zh7wvc1u1OAXlC7dlf6mZVx/s+Y5KfFz19A==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -22596,7 +22596,7 @@ "@types/mocha": "^9.0.0", "@types/node": "^17.0.35", "@types/sinon-chai": "^3.2.5", - "@types/ssh2": "^1.11.18", + "@types/ssh2": "*", "chai": "^4.3.6", "commander": "^10.0.1", "debug": "^4.3.4", @@ -23515,9 +23515,9 @@ }, "dependencies": { "@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "version": "18.19.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.4.tgz", + "integrity": "sha512-xNzlUhzoHotIsnFoXmJB+yWmBvFZgKCI9TtPIEdYIMM1KWfwuY8zh7wvc1u1OAXlC7dlf6mZVx/s+Y5KfFz19A==", "dev": true, "requires": { "undici-types": "~5.26.4" diff --git a/package.json b/package.json index d41a8a4c..336d3428 100644 --- a/package.json +++ b/package.json @@ -51,4 +51,4 @@ "husky": "^8.0.3", "lerna": "^7.1.1" } -} +} \ No newline at end of file diff --git a/packages/signing-utils/package.json b/packages/signing-utils/package.json index dcb4b4ff..818a96b0 100644 --- a/packages/signing-utils/package.json +++ b/packages/signing-utils/package.json @@ -19,7 +19,8 @@ "url": "https://github.com/mongodb-js/devtools-shared.git" }, "files": [ - "dist" + "dist", + "src" ], "license": "SSPL", "main": "dist/index.js", diff --git a/packages/signing-utils/src/index.spec.ts b/packages/signing-utils/src/index.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/signing-utils/src/mocha-hooks.ts b/packages/signing-utils/src/mocha-hooks.ts deleted file mode 100644 index f52c2619..00000000 --- a/packages/signing-utils/src/mocha-hooks.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as os from 'os'; - -export const mochaHooks: { beforeEach: Mocha.HookFunction } = { - beforeEach() { - if (os.platform().toLowerCase().includes('win32')) { - // @ts-expect-error TS does not know mocha will properly set `this` when it invokes beforeEach - this.skip(); - } - }, -}; diff --git a/packages/signing-utils/src/signing-clients/index.ts b/packages/signing-utils/src/signing-clients/index.ts index e7ed4a13..c77b13c9 100644 --- a/packages/signing-utils/src/signing-clients/index.ts +++ b/packages/signing-utils/src/signing-clients/index.ts @@ -11,16 +11,21 @@ export { RemoteSigningClient } from './remote-signing-client'; export type SigningMethod = 'gpg' | 'jsign'; export type SigningClientOptions = { - rootDir: string; + workingDirectory: string; signingScript: string; signingMethod: SigningMethod; }; /** Options for signing a file remotely over an SSH connection. */ -export type RemoteSigningOptions = Pick< - ConnectConfig, - 'username' | 'host' | 'privateKey' | 'port' -> & { +export type RemoteSigningOptions = { + /** Username for authentication. */ + username?: string; + /** Password for password-based user authentication. */ + password?: string; + /** Port number of the ssh server. */ + port?: number; + /** Buffer or string that contains a private key for either key-based or hostbased user authentication (OpenSSH format). */ + privateKey?: Buffer | string; /** The method to sign with. Use gpg on linux and jsign on windows. */ signingMethod: SigningMethod; client: 'remote'; @@ -30,6 +35,10 @@ export type RemoteSigningOptions = Pick< export type LocalSigningOptions = { /** The method to sign with. Use gpg on linux and jsign on windows. */ signingMethod: SigningMethod; + + /** Full path to the directory in which to produce the signed file. */ + directory?: string; + client: 'local'; }; @@ -48,13 +57,13 @@ export async function getSigningClient( return sshClient; } - const signingScript = path.join(__dirname, '..', 'src', './garasign.sh'); + const signingScript = path.join(__dirname, '../..', 'src', './garasign.sh'); if (options.client === 'remote') { const sshClient = await getSshClient(options); // Currently only linux remote is supported to sign the artifacts return new RemoteSigningClient(sshClient, { - rootDir: '~/garasign', + workingDirectory: '~/garasign', signingScript, signingMethod: options.signingMethod, }); @@ -63,7 +72,7 @@ export async function getSigningClient( // For local client, we put everything in a tmp directory to avoid // polluting the user's working directory. return new LocalSigningClient({ - rootDir: path.resolve(__dirname, '..', 'tmp'), + workingDirectory: path.resolve(__dirname, '..', 'tmp'), signingScript, signingMethod: options.signingMethod, }); diff --git a/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts b/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts index 20c25176..04a880f6 100644 --- a/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts +++ b/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts @@ -1,53 +1,46 @@ import fs from 'fs/promises'; -import path from 'path'; -import os from 'os'; import { LocalSigningClient } from './local-signing-client'; import { expect } from 'chai'; +import { writeFileSync } from 'fs'; +import { cwd } from 'process'; describe('LocalSigningClient', function () { - let tmpDir: string; + const signingScript = './garasign-temp.sh'; + const fileToSign = 'file-to-sign.txt'; + const fileNameAfterGpgSigning = 'file-to-sign.txt.sig'; beforeEach(async function () { - tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'local-signing-client')); + writeFileSync( + signingScript, + ` + #!/bin/bash + echo "Signing script called with arguments: $@" + echo "signed content" > ${fileNameAfterGpgSigning} + ` + ); + await fs.writeFile(fileToSign, 'original content'); }); afterEach(async function () { - await fs.rm(tmpDir, { recursive: true, force: true }); + await Promise.allSettled( + [signingScript, fileToSign, fileNameAfterGpgSigning].map((file) => + fs.rm(file) + ) + ); }); - it('signs the file correctly', async function () { - // In order to sign a file locally, we setup the following: - // 1. Create a tmp directory, tmp file to sign and a tmp signing script - // 2. Instantiate a LocalSigningClient with the tmp directory and the tmp signing script - // 3. Sign the file and assert that the file was modified correctly - // 5. Assert that the signing script was called with the correct arguments - // 6. Assert that the signed file was copied back to the original file - - const fileToSign = path.join(tmpDir, 'originals', 'file-to-sign.txt'); - const signingScript = path.join(tmpDir, 'originals', 'script.sh'); - - { - await fs.mkdir(path.dirname(fileToSign), { recursive: true }); - await fs.writeFile(fileToSign, 'original content'); - await fs.writeFile( - signingScript, - ` - #!/bin/bash - echo "Signing script called with arguments: $@" - echo "signed content" > $1 - ` - ); - } - + it('executes the signing script correctly', async function () { const localSigningClient = new LocalSigningClient({ - rootDir: tmpDir, signingScript: signingScript, signingMethod: 'gpg', + workingDirectory: cwd(), }); await localSigningClient.sign(fileToSign); - const signedFile = (await fs.readFile(fileToSign, 'utf-8')).trim(); + const signedFile = ( + await fs.readFile(fileNameAfterGpgSigning, 'utf-8') + ).trim(); expect(signedFile).to.equal('signed content'); }); }); diff --git a/packages/signing-utils/src/signing-clients/local-signing-client.ts b/packages/signing-utils/src/signing-clients/local-signing-client.ts index 72907c36..af61c0c0 100644 --- a/packages/signing-utils/src/signing-clients/local-signing-client.ts +++ b/packages/signing-utils/src/signing-clients/local-signing-client.ts @@ -1,52 +1,46 @@ import path from 'path'; -import { exec, execFileSync } from 'child_process'; -import { promisify } from 'util'; -import { debug, getEnv } from '../utils'; +import { spawnSync } from 'child_process'; +import { debug, getEnv, signedFileName } from '../utils'; import type { SigningClient, SigningClientOptions } from '.'; -const execAsync = promisify(exec); +const localClientDebug = debug.extend('LocalSigningClient'); +/** + * The local signing client signs a file locally (as opposed to over an SSH connection). + * + * The LocalSigningClient takes the directory of the file to sign and uses this as a + * working directory. No temp directory / copying of files is necessary. + */ export class LocalSigningClient implements SigningClient { constructor(private options: SigningClientOptions) {} - private async init() { - const remoteScript = `${this.options.rootDir}/garasign.sh`; - await execAsync(`mkdir -p ${this.options.rootDir}`); - await this.copyFile(this.options.signingScript, remoteScript); - await execAsync(`chmod +x ${remoteScript}`); - } - - private async copyFile(from: string, to: string): Promise { - await execAsync(`cp ${from} ${to}`); - } - - async sign(file: string): Promise { - debug('Signing file', file); + sign(file: string): Promise { + localClientDebug(`Signing ${file}`); - const remotePath = path.join(this.options.rootDir, path.basename(file)); + const directoryOfFileToSign = path.dirname(file); try { - await this.init(); - - await this.copyFile(file, remotePath); - debug(`LocalSigningClient: Copied file ${file} to ${remotePath}`); - - execFileSync('./garasign.sh', [path.basename(file)], { - cwd: this.options.rootDir, - env: { - ...getEnv(), - method: this.options.signingMethod, - }, + const env = { + ...getEnv(), + method: this.options.signingMethod, + }; + + spawnSync('bash', [this.options.signingScript, path.basename(file)], { + cwd: directoryOfFileToSign, + env, + encoding: 'utf-8', }); - debug(`LocalSigningClient: Signed file ${remotePath}`); - await this.copyFile(remotePath, file); - debug( - `LocalSigningClient: Copied signed file back from ${remotePath} to ${file}` + localClientDebug( + `Signed file ${file} - output file ${signedFileName(file, { + ...this.options, + })}` ); - } finally { - // Clean up - void execAsync(`rm -f ${remotePath}`); + + return Promise.resolve(); + } catch (error) { + localClientDebug({ error }); + throw error; } } } diff --git a/packages/signing-utils/src/signing-clients/remote-signing-client.spec.ts b/packages/signing-utils/src/signing-clients/remote-signing-client.spec.ts index 5031c02a..b8fd39f8 100644 --- a/packages/signing-utils/src/signing-clients/remote-signing-client.spec.ts +++ b/packages/signing-utils/src/signing-clients/remote-signing-client.spec.ts @@ -1,6 +1,4 @@ import fs from 'fs/promises'; -import path from 'path'; -import os from 'os'; import { exec } from 'child_process'; import { RemoteSigningClient } from './remote-signing-client'; import { expect } from 'chai'; @@ -46,7 +44,7 @@ const getMockedSSHClient = () => { }, exec: (command: string) => { return new Promise((resolve, reject) => { - exec(command, (err) => { + exec(command, { shell: 'bash' }, (err) => { if (err) { return reject(err); } @@ -59,35 +57,33 @@ const getMockedSSHClient = () => { }; describe('RemoteSigningClient', function () { - let tmpDir: string; + const workingDirectoryPath = 'working-directory'; + const fileToSign = 'file-to-sign.txt'; + const signingScript = 'script.sh'; - beforeEach(async function () { - tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-signing-client')); + beforeEach(async function name() { + await fs.writeFile(fileToSign, 'RemoteSigningClient: original content'); + await fs.writeFile( + signingScript, + ` + #!/bin/bash + echo "Signing script called with arguments: $@" + echo "RemoteSigningClient: signed content" > $1 + ` + ); }); afterEach(async function () { - await fs.rm(tmpDir, { recursive: true, force: true }); + await Promise.allSettled([ + fs.rm(workingDirectoryPath, { recursive: true, force: true }), + fs.rm(signingScript), + fs.rm(fileToSign), + ]); }); it('signs the file correctly', async function () { - const fileToSign = path.join(tmpDir, 'originals', 'file-to-sign.txt'); - const signingScript = path.join(tmpDir, 'originals', 'script.sh'); - - { - await fs.mkdir(path.dirname(fileToSign), { recursive: true }); - await fs.writeFile(fileToSign, 'RemoteSigningClient: original content'); - await fs.writeFile( - signingScript, - ` - #!/bin/bash - echo "Signing script called with arguments: $@" - echo "RemoteSigningClient: signed content" > $1 - ` - ); - } - const remoteSigningClient = new RemoteSigningClient(getMockedSSHClient(), { - rootDir: tmpDir, + workingDirectory: workingDirectoryPath, signingScript: signingScript, signingMethod: 'gpg', }); diff --git a/packages/signing-utils/src/signing-clients/remote-signing-client.ts b/packages/signing-utils/src/signing-clients/remote-signing-client.ts index adeb7fcf..9e327619 100644 --- a/packages/signing-utils/src/signing-clients/remote-signing-client.ts +++ b/packages/signing-utils/src/signing-clients/remote-signing-client.ts @@ -20,18 +20,20 @@ export class RemoteSigningClient implements SigningClient { */ private async init() { this.sftpConnection = await this.sshClient.getSftpConnection(); - await this.sshClient.exec(`mkdir -p ${this.options.rootDir}`); + await this.sshClient.exec(`mkdir -p ${this.options.workingDirectory}`); // Copy the signing script to the remote machine { - const remoteScript = `${this.options.rootDir}/garasign.sh`; + const remoteScript = `${this.options.workingDirectory}/garasign.sh`; await this.copyFile(this.options.signingScript, remoteScript); await this.sshClient.exec(`chmod +x ${remoteScript}`); } } private getRemoteFilePath(file: string) { - return `${this.options.rootDir}/temp-${Date.now()}-${path.basename(file)}`; + return `${this.options.workingDirectory}/temp-${Date.now()}-${path.basename( + file + )}`; } private async copyFile(file: string, remotePath: string): Promise { @@ -75,7 +77,7 @@ export class RemoteSigningClient implements SigningClient { * So, here we are passing the env variables as part of the command. */ const cmds = [ - `cd ${this.options.rootDir}`, + `cd ${this.options.workingDirectory}`, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `export garasign_username=${env.garasign_username}`, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -101,11 +103,13 @@ export class RemoteSigningClient implements SigningClient { await this.copyFile(file, remotePath); debug(`SFTP: Copied file ${file} to ${remotePath}`); - await this.signRemoteFile(remotePath); + await this.signRemoteFile(path.basename(remotePath)); debug(`SFTP: Signed file ${file}`); await this.downloadFile(remotePath, file); debug(`SFTP: Downloaded signed file to ${file}`); + } catch (error) { + debug({ error }); } finally { await this.removeFile(remotePath); debug(`SFTP: Removed remote file ${remotePath}`); diff --git a/packages/signing-utils/src/utils.ts b/packages/signing-utils/src/utils.ts index f4919932..b640230a 100644 --- a/packages/signing-utils/src/utils.ts +++ b/packages/signing-utils/src/utils.ts @@ -1,4 +1,5 @@ import { debug as debugFn } from 'debug'; +import type { SigningClientOptions } from './signing-clients'; export const debug = debugFn('signing-utils'); @@ -19,3 +20,11 @@ export function getEnv() { artifactory_password, }; } + +/** Returns the name of the signed file. gpg signing signs files to a .sig file, and jsign signs files in-place. */ +export function signedFileName( + fileName: string, + { signingMethod }: SigningClientOptions +) { + return signingMethod === 'gpg' ? `${fileName}.sig` : fileName; +} From 7a5ad6f54f3c3b68152a1875468da9f2d02866f7 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 4 Jan 2024 11:35:10 -0700 Subject: [PATCH 11/16] remove custom hooks --- packages/signing-utils/.mocharc.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/signing-utils/.mocharc.js b/packages/signing-utils/.mocharc.js index 43001b2e..a484a9b8 100644 --- a/packages/signing-utils/.mocharc.js +++ b/packages/signing-utils/.mocharc.js @@ -1,5 +1,3 @@ const config = require('@mongodb-js/mocha-config-devtools'); -config.require.push(`./src/mocha-hooks.ts`); - module.exports = config; From 263a17a1b3adab3906c9a8cb650fcb08cc224fd0 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 4 Jan 2024 11:48:56 -0700 Subject: [PATCH 12/16] Apply suggestions from code review Co-authored-by: Anna Henningsen --- packages/signing-utils/src/cli.ts | 1 + packages/signing-utils/src/garasign.sh | 4 ++-- .../src/signing-clients/remote-signing-client.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/signing-utils/src/cli.ts b/packages/signing-utils/src/cli.ts index fe072fb9..fe25991c 100755 --- a/packages/signing-utils/src/cli.ts +++ b/packages/signing-utils/src/cli.ts @@ -44,5 +44,6 @@ sign(file, program.opts()).then( (error) => { // eslint-disable-next-line no-console console.error({ error }); + process.exitCode = 1; } ); diff --git a/packages/signing-utils/src/garasign.sh b/packages/signing-utils/src/garasign.sh index abbbba60..431a182a 100644 --- a/packages/signing-utils/src/garasign.sh +++ b/packages/signing-utils/src/garasign.sh @@ -39,7 +39,7 @@ gpg_sign() { -v $directory:$directory \ -w $directory \ ${ARTIFACTORY_HOST}/release-tools-container-registry-local/garasign-gpg \ - /bin/bash -c "gpgloader && gpg --yes -v --armor -o $file.sig --detach-sign $file" + /bin/bash -c "gpgloader && gpg --yes -v --armor -o '$file.sig' --detach-sign '$file'" } jsign_sign() { @@ -50,7 +50,7 @@ jsign_sign() { -v $directory:$directory \ -w $directory \ artifactory.corp.mongodb.com/release-tools-container-registry-local/garasign-jsign \ - /bin/bash -c "jsign --tsaurl "timestamp.url" -a mongo-authenticode-2021 $file" + /bin/bash -c "jsign --tsaurl "timestamp.url" -a mongo-authenticode-2021 '$file'" } if [[ "$method" -eq "gpg" ]]; then diff --git a/packages/signing-utils/src/signing-clients/remote-signing-client.ts b/packages/signing-utils/src/signing-clients/remote-signing-client.ts index 9e327619..02832434 100644 --- a/packages/signing-utils/src/signing-clients/remote-signing-client.ts +++ b/packages/signing-utils/src/signing-clients/remote-signing-client.ts @@ -77,7 +77,7 @@ export class RemoteSigningClient implements SigningClient { * So, here we are passing the env variables as part of the command. */ const cmds = [ - `cd ${this.options.workingDirectory}`, + `cd '${this.options.workingDirectory}'`, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `export garasign_username=${env.garasign_username}`, // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -87,7 +87,7 @@ export class RemoteSigningClient implements SigningClient { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `export artifactory_password=${env.artifactory_password}`, `export method=${this.options.signingMethod}`, - `./garasign.sh ${file}`, + `./garasign.sh '${file}'`, ]; const command = cmds.join(' && '); const res = await this.sshClient.exec(command); From 9630d8cbe38a3001fc145f26e20c85478003e3c5 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 5 Jan 2024 07:44:37 -0700 Subject: [PATCH 13/16] remove CLI interface --- packages/signing-utils/bin/sign.js | 3 -- packages/signing-utils/src/cli.ts | 49 ------------------------------ 2 files changed, 52 deletions(-) delete mode 100755 packages/signing-utils/bin/sign.js delete mode 100755 packages/signing-utils/src/cli.ts diff --git a/packages/signing-utils/bin/sign.js b/packages/signing-utils/bin/sign.js deleted file mode 100755 index 96180310..00000000 --- a/packages/signing-utils/bin/sign.js +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -require('../dist/cli.js'); diff --git a/packages/signing-utils/src/cli.ts b/packages/signing-utils/src/cli.ts deleted file mode 100755 index fe25991c..00000000 --- a/packages/signing-utils/src/cli.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { InvalidArgumentError, program } from 'commander'; -import { sign } from '.'; - -function parseEnumValue(name: string, values: string[]) { - return (value: unknown) => { - if (typeof value !== 'string' || !values.includes(value)) { - throw new InvalidArgumentError( - `${name} must be one of ${values.join('|')}` - ); - } - }; -} - -program - .arguments('file') - .requiredOption( - '-c, --client ', - 'The client to sign with. Can be `local` or `remote`.', - parseEnumValue('client', ['local', 'remote']) - ) - .requiredOption( - '--signing-method ', - 'The signing method to use. Can be `gpg` or `jsign`.', - parseEnumValue('signing method', ['gpg', 'jsign']) - ) - .option( - '-h, --host ', - 'The SSH host to use when signing with remote client.' - ) - .option('-u, --username ', 'The SSH host username.') - .option('-p, --port ', 'The SSH host port.') - .option( - '-k, --private-key ', - 'The SSH private key to use when signing with remote client.' - ) - .parse(process.argv); - -const file = program.args[0]; - -sign(file, program.opts()).then( - () => { - // do nothing - }, - (error) => { - // eslint-disable-next-line no-console - console.error({ error }); - process.exitCode = 1; - } -); From c0fb87e5fc89f12f6d317ac211e634d841c274a5 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 5 Jan 2024 07:46:29 -0700 Subject: [PATCH 14/16] remove cli parsing dependdency --- package-lock.json | 17 +---------------- packages/signing-utils/package.json | 1 - 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2320344..51bcd17d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19886,7 +19886,6 @@ "version": "0.1.0", "license": "SSPL", "dependencies": { - "commander": "^10.0.1", "debug": "^4.3.4", "ssh2": "^1.15.0" }, @@ -19911,14 +19910,6 @@ "typescript": "^5.0.4" } }, - "packages/signing-utils/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "engines": { - "node": ">=14" - } - }, "packages/signing-utils/node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -22596,9 +22587,8 @@ "@types/mocha": "^9.0.0", "@types/node": "^17.0.35", "@types/sinon-chai": "^3.2.5", - "@types/ssh2": "*", + "@types/ssh2": "^1.11.18", "chai": "^4.3.6", - "commander": "^10.0.1", "debug": "^4.3.4", "depcheck": "^1.4.1", "eslint": "^7.25.0", @@ -22611,11 +22601,6 @@ "typescript": "^5.0.4" }, "dependencies": { - "commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" - }, "typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", diff --git a/packages/signing-utils/package.json b/packages/signing-utils/package.json index 818a96b0..1249895c 100644 --- a/packages/signing-utils/package.json +++ b/packages/signing-utils/package.json @@ -67,7 +67,6 @@ "typescript": "^5.0.4" }, "dependencies": { - "commander": "^10.0.1", "debug": "^4.3.4", "ssh2": "^1.15.0" } From a75622cc6e8327e26d201a287df50ba710566755 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 5 Jan 2024 08:05:37 -0700 Subject: [PATCH 15/16] remove CLI --- package-lock.json | 4 ++-- packages/signing-utils/package.json | 2 +- packages/signing-utils/src/signing-clients/index.ts | 13 ++++++------- .../signing-clients/local-signing-client.spec.ts | 2 -- .../src/signing-clients/local-signing-client.ts | 12 +++++------- packages/signing-utils/src/utils.ts | 9 --------- 6 files changed, 14 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51bcd17d..c60f1232 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19895,7 +19895,7 @@ "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-devtools": "^1.0.1", "@types/chai": "^4.2.21", - "@types/mocha": "^9.0.0", + "@types/mocha": "^9.1.1", "@types/node": "^17.0.35", "@types/sinon-chai": "^3.2.5", "@types/ssh2": "^1.11.18", @@ -22584,7 +22584,7 @@ "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-devtools": "^1.0.1", "@types/chai": "^4.2.21", - "@types/mocha": "^9.0.0", + "@types/mocha": "^9.1.1", "@types/node": "^17.0.35", "@types/sinon-chai": "^3.2.5", "@types/ssh2": "^1.11.18", diff --git a/packages/signing-utils/package.json b/packages/signing-utils/package.json index 1249895c..56d93746 100644 --- a/packages/signing-utils/package.json +++ b/packages/signing-utils/package.json @@ -52,7 +52,7 @@ "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-devtools": "^1.0.1", "@types/chai": "^4.2.21", - "@types/mocha": "^9.0.0", + "@types/mocha": "^9.1.1", "@types/node": "^17.0.35", "@types/sinon-chai": "^3.2.5", "@types/ssh2": "^1.11.18", diff --git a/packages/signing-utils/src/signing-clients/index.ts b/packages/signing-utils/src/signing-clients/index.ts index c77b13c9..a1600fc6 100644 --- a/packages/signing-utils/src/signing-clients/index.ts +++ b/packages/signing-utils/src/signing-clients/index.ts @@ -28,6 +28,11 @@ export type RemoteSigningOptions = { privateKey?: Buffer | string; /** The method to sign with. Use gpg on linux and jsign on windows. */ signingMethod: SigningMethod; + + /** + * The path of the working directory in which to sign files **on the remote ssh server**. Defaults to `/home/ubuntu/garasign`. + */ + workingDirectory?: string; client: 'remote'; }; @@ -36,9 +41,6 @@ export type LocalSigningOptions = { /** The method to sign with. Use gpg on linux and jsign on windows. */ signingMethod: SigningMethod; - /** Full path to the directory in which to produce the signed file. */ - directory?: string; - client: 'local'; }; @@ -63,16 +65,13 @@ export async function getSigningClient( const sshClient = await getSshClient(options); // Currently only linux remote is supported to sign the artifacts return new RemoteSigningClient(sshClient, { - workingDirectory: '~/garasign', + workingDirectory: options.workingDirectory ?? '/home/ubuntu/garasign', signingScript, signingMethod: options.signingMethod, }); } if (options.client === 'local') { - // For local client, we put everything in a tmp directory to avoid - // polluting the user's working directory. return new LocalSigningClient({ - workingDirectory: path.resolve(__dirname, '..', 'tmp'), signingScript, signingMethod: options.signingMethod, }); diff --git a/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts b/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts index 04a880f6..0f9c7b97 100644 --- a/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts +++ b/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts @@ -2,7 +2,6 @@ import fs from 'fs/promises'; import { LocalSigningClient } from './local-signing-client'; import { expect } from 'chai'; import { writeFileSync } from 'fs'; -import { cwd } from 'process'; describe('LocalSigningClient', function () { const signingScript = './garasign-temp.sh'; @@ -33,7 +32,6 @@ describe('LocalSigningClient', function () { const localSigningClient = new LocalSigningClient({ signingScript: signingScript, signingMethod: 'gpg', - workingDirectory: cwd(), }); await localSigningClient.sign(fileToSign); diff --git a/packages/signing-utils/src/signing-clients/local-signing-client.ts b/packages/signing-utils/src/signing-clients/local-signing-client.ts index af61c0c0..d007cf76 100644 --- a/packages/signing-utils/src/signing-clients/local-signing-client.ts +++ b/packages/signing-utils/src/signing-clients/local-signing-client.ts @@ -1,6 +1,6 @@ import path from 'path'; import { spawnSync } from 'child_process'; -import { debug, getEnv, signedFileName } from '../utils'; +import { debug, getEnv } from '../utils'; import type { SigningClient, SigningClientOptions } from '.'; const localClientDebug = debug.extend('LocalSigningClient'); @@ -12,7 +12,9 @@ const localClientDebug = debug.extend('LocalSigningClient'); * working directory. No temp directory / copying of files is necessary. */ export class LocalSigningClient implements SigningClient { - constructor(private options: SigningClientOptions) {} + constructor( + private options: Omit + ) {} sign(file: string): Promise { localClientDebug(`Signing ${file}`); @@ -31,11 +33,7 @@ export class LocalSigningClient implements SigningClient { encoding: 'utf-8', }); - localClientDebug( - `Signed file ${file} - output file ${signedFileName(file, { - ...this.options, - })}` - ); + localClientDebug(`Signed file ${file}`); return Promise.resolve(); } catch (error) { diff --git a/packages/signing-utils/src/utils.ts b/packages/signing-utils/src/utils.ts index b640230a..f4919932 100644 --- a/packages/signing-utils/src/utils.ts +++ b/packages/signing-utils/src/utils.ts @@ -1,5 +1,4 @@ import { debug as debugFn } from 'debug'; -import type { SigningClientOptions } from './signing-clients'; export const debug = debugFn('signing-utils'); @@ -20,11 +19,3 @@ export function getEnv() { artifactory_password, }; } - -/** Returns the name of the signed file. gpg signing signs files to a .sig file, and jsign signs files in-place. */ -export function signedFileName( - fileName: string, - { signingMethod }: SigningClientOptions -) { - return signingMethod === 'gpg' ? `${fileName}.sig` : fileName; -} From d93b722c0d327b4b489ef8e8214f5b9621a96e46 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 5 Jan 2024 13:43:54 -0700 Subject: [PATCH 16/16] rework tests in ssh client && address Anna's feedback --- packages/signing-utils/src/ssh-client.spec.ts | 250 +++++++++--------- packages/signing-utils/src/ssh-client.ts | 80 +++--- 2 files changed, 160 insertions(+), 170 deletions(-) diff --git a/packages/signing-utils/src/ssh-client.spec.ts b/packages/signing-utils/src/ssh-client.spec.ts index 7289c563..805b09b1 100644 --- a/packages/signing-utils/src/ssh-client.spec.ts +++ b/packages/signing-utils/src/ssh-client.spec.ts @@ -1,6 +1,8 @@ import sinon from 'sinon'; import { SSHClient } from './ssh-client'; import { expect } from 'chai'; +import { PassThrough } from 'stream'; +import { promisify } from 'util'; describe('SSHClient', function () { let sshClient: SSHClient; @@ -20,51 +22,47 @@ describe('SSHClient', function () { sandbox.restore(); }); - it('connects successfully on ready', async function () { - const connectStub = sandbox - .stub(sshClient['sshConnection'], 'connect') - .resolves(sshClient['sshConnection']); - await Promise.all([ - sshClient.connect(), - sshClient['sshConnection'].emit('ready'), - ]); - expect(connectStub.calledOnce).to.be.true; - expect(connectStub.firstCall.firstArg).to.deep.equal({ - host: 'example.com', - port: 22, - username: 'admin', - privateKey: undefined, + describe('connect()', function () { + let connectStub: sinon.SinonStub; + + beforeEach(function () { + connectStub = sandbox + .stub(sshClient['sshConnection'], 'connect') + .returns(sshClient['sshConnection']); }); - expect(sshClient).to.have.property('connected', true); - }); + it('connects successfully on ready', async function () { + const connectPromise = sshClient.connect(); + sshClient['sshConnection'].emit('ready'); + await connectPromise; - it('does not called client.connect when connected', async function () { - const connectStub = sandbox - .stub(sshClient['sshConnection'], 'connect') - .resolves(sshClient['sshConnection']); - // Its connected here - sshClient['sshConnection'].emit('ready'); - await Promise.all([ - sshClient.connect(), - sshClient['sshConnection'].emit('ready'), - ]); - - expect(connectStub.calledOnce).to.be.false; - expect(sshClient).to.have.property('connected', true); - }); + expect(connectStub.calledOnce).to.be.true; + expect(connectStub.firstCall.firstArg).to.deep.equal({ + host: 'example.com', + port: 22, + username: 'admin', + privateKey: undefined, + }); + expect(sshClient).to.have.property('connected', true); + }); + + it('does not called client.connect when connected', async function () { + // connect the internal ssh client + sshClient['sshConnection'].emit('ready'); - it('throws when connecting on error', async function () { - const connectStub = sandbox - .stub(sshClient['sshConnection'], 'connect') - .resolves(sshClient['sshConnection']); - try { - await Promise.all([ - sshClient.connect(), - sshClient['sshConnection'].emit('error', new Error('Connection error')), - ]); - expect.fail('Expected SSH Client to throw'); - } catch (err) { - expect(err).to.have.property('message', 'Connection error'); + const connectPromise = sshClient.connect(); + sshClient['sshConnection'].emit('ready'); + await connectPromise; + + expect(connectStub.calledOnce).to.be.false; + expect(sshClient).to.have.property('connected', true); + }); + + it('throws when connecting on error', async function () { + const connectPromise = sshClient.connect(); + sshClient['sshConnection'].emit('error', new Error('Connection error')); + + const error = await connectPromise.catch((e) => e); + expect(error).to.have.property('message', 'Connection error'); expect(connectStub.calledOnce).to.be.true; expect(connectStub.firstCall.firstArg).to.deep.equal({ host: 'example.com', @@ -73,110 +71,116 @@ describe('SSHClient', function () { privateKey: undefined, }); expect(sshClient).to.have.property('connected', false); - } + }); }); - it('should disconnect from SSH server', function () { - const endStub = sandbox.stub(sshClient['sshConnection'], 'end').resolves(); - sshClient['sshConnection'].emit('ready'); + describe('disconnect()', function () { + it('disconnects from SSH server', function () { + const endStub = sandbox.stub(sshClient['sshConnection'], 'end'); + sshClient['sshConnection'].emit('ready'); - sshClient.disconnect(); - expect(endStub.calledOnce).to.be.true; + sshClient.disconnect(); + expect(endStub.calledOnce).to.be.true; + }); }); - context('exec', function () { + describe('exec()', function () { const COMMAND = 'echo "Hello World"'; - type ExecCallback = (err: Error | undefined, channel: any) => any; - it('should throw when exec fails', async function () { - const execStub = sandbox + function makeMockClientChannel() { + const stream: PassThrough & { stderr: PassThrough } = + new PassThrough() as any; + stream.stderr = new PassThrough(); + return stream; + } + let clientStream: ReturnType; + let execStub; + + beforeEach(function () { + clientStream = makeMockClientChannel(); + execStub = sandbox .stub(sshClient['sshConnection'], 'exec') - .callsFake((_command: string, cb: ExecCallback) => { - return cb(new Error('Callback Error'), null); - }); + .yieldsRight(undefined, clientStream); sshClient['sshConnection'].emit('ready'); - - try { - await sshClient.exec(COMMAND); - expect.fail('Expected SSH Client to throw'); - } catch (err) { - expect(err).to.have.property('message', 'Callback Error'); - expect(execStub.calledOnce).to.be.true; - expect(execStub.firstCall.firstArg).to.equal(COMMAND); - } }); - it('should throw when exec returns an error - code > 0', async function () { - const execStub = sandbox - .stub(sshClient['sshConnection'], 'exec') - .callsFake((_command: string, cb: ExecCallback) => { - return cb(undefined, { - stderr: { - on: (event: string, cb: (data: string) => void) => { - if (event === 'data') { - cb('Some Error'); - } - }, - }, - on: (event: string, cb: (code: number | string) => void) => { - if (event === 'close') { - cb(10); - } - }, - }); - }); + it('should throw when exec fails', async function () { + execStub.yieldsRight(new Error('Callback Error')); + sshClient['sshConnection'].emit('ready'); - try { - await sshClient.exec(COMMAND); - expect.fail('Expected SSH Client to throw'); - } catch (err) { - expect(err).to.have.a.property( - 'message', - 'Command failed with code 10. Error: Some Error' - ); - expect(execStub.calledOnce).to.be.true; - expect(execStub.firstCall.firstArg).to.equal(COMMAND); - } + const err = await sshClient.exec(COMMAND).catch((e) => e); + + expect(err).to.have.property('message', 'Callback Error'); + expect(execStub.calledOnce).to.be.true; + expect(execStub.firstCall.firstArg).to.equal(COMMAND); }); - it('should return stdout when exec succeeds', async function () { - const execStub = sandbox - .stub(sshClient['sshConnection'], 'exec') - .callsFake((_command: string, cb: ExecCallback) => { - return cb(undefined, { - stderr: { - on: () => {}, - }, - on: (event: string, cb: (code: number | string) => void) => { - if (event === 'data') { - cb('Hello World'); - } - if (event === 'close') { - cb(0); - } - }, - }); - }); - sshClient['sshConnection'].emit('ready'); + it('should throw when exec returns an error - code > 0', async function () { + const resultPromise = sshClient.exec(COMMAND); + // internally, exec() attaches event listeners to the `stream` after an `await` statement + // so we must queue a microtask to let the `exec` function resume before emitting events + // on the stream. otherwise, the events are emitted from the stream when there are no + // listeners on the stream. + await promisify(queueMicrotask)(); + clientStream.stderr.push('Some Error'); + clientStream.emit('close', 10); + + const error = await resultPromise.catch((e) => e); + expect(error).to.have.a.property( + 'message', + 'Command failed with code 10. Error: Some Error' + ); + expect(execStub.calledOnce).to.be.true; + expect(execStub.firstCall.firstArg).to.equal(COMMAND); + }); - const result = await sshClient.exec(COMMAND); + it('should return stdout when exec succeeds', async function () { + const resultPromise = sshClient.exec(COMMAND); + // internally, exec() attaches event listeners to the `stream` after an `await` statement + // so we must queue a microtask to let the `exec` function resume before emitting events + // on the stream. otherwise, the events are emitted from the stream when there are no + // listeners on the stream. + await promisify(queueMicrotask)(); + clientStream.push('Hello World'); + clientStream.emit('close', 0); + const result = await resultPromise; expect(result).to.equal('Hello World'); expect(execStub.calledOnce).to.be.true; expect(execStub.firstCall.firstArg).to.equal(COMMAND); }); }); - it('should get SFTP connection', async function () { - const sftpStub = sandbox - .stub(sshClient['sshConnection'], 'sftp') - .callsFake((cb: (err: Error | undefined, sftp: any) => any) => { - return cb(undefined, 'mockedSFTP'); + describe('getSftpConnection()', function () { + describe('when the ssh client is not connected', function () { + it('returns the sftp connection', async function () { + const error = await sshClient.getSftpConnection().catch((e) => e); + expect(error) + .to.be.instanceof(Error) + .to.match(/Not connected to ssh server/); }); + }); - sshClient['sshConnection'].emit('ready'); + describe('when the ssh client is connected', function () { + let connectionStub: sinon.SinonStub; + beforeEach(function () { + connectionStub = sandbox + .stub(sshClient['sshConnection'], 'sftp') + .yieldsRight(undefined, 'mockedSFTP'); - const result = await sshClient.getSftpConnection(); - expect(result).to.equal('mockedSFTP'); - expect(sftpStub.calledOnce).to.be.true; + sshClient['sshConnection'].emit('ready'); + }); + it('returns the sftp connection', async function () { + const connection = await sshClient.getSftpConnection(); + expect(connection).to.equal('mockedSFTP'); + }); + + it('caches the sftp connection', async function () { + await sshClient.getSftpConnection(); + connectionStub.yieldsRight(undefined, 'new value'); + + const connection = await sshClient.getSftpConnection(); + expect(connection).to.equal('mockedSFTP'); + }); + }); }); }); diff --git a/packages/signing-utils/src/ssh-client.ts b/packages/signing-utils/src/ssh-client.ts index 64d1e7a3..1b3aeb5c 100644 --- a/packages/signing-utils/src/ssh-client.ts +++ b/packages/signing-utils/src/ssh-client.ts @@ -1,7 +1,9 @@ -import type { ConnectConfig, SFTPWrapper } from 'ssh2'; +import type { ClientChannel, ConnectConfig, SFTPWrapper } from 'ssh2'; import { Client } from 'ssh2'; import { readFile } from 'fs/promises'; import { debug } from './utils'; +import { promisify } from 'util'; +import { once } from 'events'; export class SSHClient { private sshConnection: Client; @@ -32,20 +34,18 @@ export class SSHClient { async connect() { if (this.connected) { - return Promise.resolve(); + return; } const privateKey = this.sshClientOptions.privateKey ? await readFile(this.sshClientOptions.privateKey) : undefined; - return new Promise((resolve, reject) => { - this.sshConnection.connect({ - ...this.sshClientOptions, - privateKey, - }); - this.sshConnection.on('error', reject); - // @ts-expect-error We expect an error here - why? - this.sshConnection.on('ready', resolve); + + const ready = once(this.sshConnection, 'ready'); + this.sshConnection.connect({ + ...this.sshClientOptions, + privateKey, }); + await ready; } disconnect() { @@ -57,29 +57,26 @@ export class SSHClient { if (!this.connected) { throw new Error('Not connected to ssh server'); } - return new Promise((resolve, reject) => { - this.sshConnection.exec(command, (err, stream) => { - if (err) { - return reject(err); - } - let data = ''; - stream.on('data', (chunk: string) => { - data += chunk; - }); - stream.stderr.on('data', (chunk) => { - data += chunk; - }); - stream.on('close', (code: number) => { - if (code === 0) { - return resolve(data); - } else { - return reject( - new Error(`Command failed with code ${code}. Error: ${data}`) - ); - } - }); - }); + const stream: ClientChannel = await promisify( + this.sshConnection.exec.bind(this.sshConnection) + )(command); + let data = ''; + stream.setEncoding('utf-8'); + stream.stderr.setEncoding('utf-8'); + stream.on('data', (chunk: string) => { + data += chunk; + }); + stream.stderr.on('data', (chunk: string) => { + data += chunk; }); + const [code] = await once(stream, 'close'); + if (code !== 0) { + throw new Error( + `Command failed with code ${code as number}. Error: ${data}` + ); + } + + return data; } async getSftpConnection(): Promise { @@ -87,20 +84,9 @@ export class SSHClient { throw new Error('Not connected to ssh server'); } - if (this.sftpConnection) { - return Promise.resolve(this.sftpConnection); - } - - return new Promise((resolve, reject) => { - this.sshConnection.sftp((err, sftp) => { - if (err) { - debug('SFTP: Failed to setup connection', err); - return reject(err); - } - debug('SFTP: Connection established'); - this.sftpConnection = sftp; - return resolve(sftp); - }); - }); + this.sftpConnection = + this.sftpConnection ?? + (await promisify(this.sshConnection.sftp.bind(this.sshConnection))()); + return this.sftpConnection; } }