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" } } diff --git a/package-lock.json b/package-lock.json index 1119e3b3..c60f1232 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": { @@ -3014,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 @@ -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.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" + } + }, "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", @@ -19807,6 +19881,48 @@ "typescript": "^4.3.5" } }, + "packages/signing-utils": { + "name": "@mongodb-js/signing-utils", + "version": "0.1.0", + "license": "SSPL", + "dependencies": { + "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.1.1", + "@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/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", @@ -21937,7 +22053,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": { @@ -22460,6 +22576,39 @@ } } }, + "@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.1.1", + "@types/node": "^17.0.35", + "@types/sinon-chai": "^3.2.5", + "@types/ssh2": "^1.11.18", + "chai": "^4.3.6", + "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": { + "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": { @@ -23341,6 +23490,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.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" + } + } + } + }, "@types/ssri": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@types/ssri/-/ssri-7.1.1.tgz", @@ -24455,6 +24624,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 +25344,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 +30630,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 +33444,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 +33947,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 +34145,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/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/.depcheckrc b/packages/signing-utils/.depcheckrc new file mode 100644 index 00000000..48bf9af6 --- /dev/null +++ b/packages/signing-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/signing-utils/.eslintignore b/packages/signing-utils/.eslintignore new file mode 100644 index 00000000..85a8a75e --- /dev/null +++ b/packages/signing-utils/.eslintignore @@ -0,0 +1,2 @@ +.nyc-output +dist diff --git a/packages/signing-utils/.eslintrc.js b/packages/signing-utils/.eslintrc.js new file mode 100644 index 00000000..83296d73 --- /dev/null +++ b/packages/signing-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/signing-utils/.mocharc.js b/packages/signing-utils/.mocharc.js new file mode 100644 index 00000000..a484a9b8 --- /dev/null +++ b/packages/signing-utils/.mocharc.js @@ -0,0 +1,3 @@ +const config = require('@mongodb-js/mocha-config-devtools'); + +module.exports = config; diff --git a/packages/signing-utils/.prettierignore b/packages/signing-utils/.prettierignore new file mode 100644 index 00000000..4d28df66 --- /dev/null +++ b/packages/signing-utils/.prettierignore @@ -0,0 +1,3 @@ +.nyc_output +dist +coverage diff --git a/packages/signing-utils/.prettierrc.json b/packages/signing-utils/.prettierrc.json new file mode 100644 index 00000000..dfae21d0 --- /dev/null +++ b/packages/signing-utils/.prettierrc.json @@ -0,0 +1 @@ +"@mongodb-js/prettier-config-devtools" diff --git a/packages/signing-utils/LICENSE b/packages/signing-utils/LICENSE new file mode 100644 index 00000000..63f6b6c1 --- /dev/null +++ b/packages/signing-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/signing-utils/package.json b/packages/signing-utils/package.json new file mode 100644 index 00000000..56d93746 --- /dev/null +++ b/packages/signing-utils/package.json @@ -0,0 +1,73 @@ +{ + "name": "@mongodb-js/signing-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", + "src" + ], + "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.1.1", + "@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": { + "debug": "^4.3.4", + "ssh2": "^1.15.0" + } +} diff --git a/packages/signing-utils/src/garasign.sh b/packages/signing-utils/src/garasign.sh new file mode 100644 index 00000000..431a182a --- /dev/null +++ b/packages/signing-utils/src/garasign.sh @@ -0,0 +1,62 @@ +#! /usr/bin/env bash + +if [ -z "$1" ]; then + echo "Usage: garasign.sh " + exit 1 +fi + +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 +if [ -z ${method+omitted} ]; then echo "method must either be gpg or jsign" && 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 $? + +directory=$(pwd) +file=$1 + +echo "File to be signed: $file" +echo "Working directory: $directory" + +gpg_sign() { + docker run \ + -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'" +} + +jsign_sign() { + docker run \ + -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 + gpg_sign +elif [[ "$method" -eq "jsign" ]]; then + jsign_sign +fi + +echo "Finished signing $file" \ No newline at end of file diff --git a/packages/signing-utils/src/index.ts b/packages/signing-utils/src/index.ts new file mode 100644 index 00000000..81ab86e6 --- /dev/null +++ b/packages/signing-utils/src/index.ts @@ -0,0 +1,27 @@ +import type { ClientOptions } from './signing-clients'; + +import { getSigningClient } from './signing-clients'; +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: ClientOptions +): Promise { + debug( + `Signing file: ${file} with client ${options.client} and options:`, + options + ); + try { + const signingClient = await getSigningClient(options); + await signingClient.sign(file); + } catch (err) { + debug(`Error signing file: ${file}`, err); + throw err; + } +} diff --git a/packages/signing-utils/src/signing-clients/index.ts b/packages/signing-utils/src/signing-clients/index.ts new file mode 100644 index 00000000..a1600fc6 --- /dev/null +++ b/packages/signing-utils/src/signing-clients/index.ts @@ -0,0 +1,82 @@ +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'; + +export type SigningMethod = 'gpg' | 'jsign'; + +export type SigningClientOptions = { + workingDirectory: string; + signingScript: string; + signingMethod: SigningMethod; +}; + +/** Options for signing a file remotely over an SSH connection. */ +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; + + /** + * 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'; +}; + +/** 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; +} + +export async function getSigningClient( + options: ClientOptions +): Promise { + async function getSshClient(sshOptions: ConnectConfig) { + const sshClient = new SSHClient(sshOptions); + await sshClient.connect(); + return sshClient; + } + + 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, { + workingDirectory: options.workingDirectory ?? '/home/ubuntu/garasign', + signingScript, + signingMethod: options.signingMethod, + }); + } + if (options.client === 'local') { + return new LocalSigningClient({ + signingScript, + signingMethod: options.signingMethod, + }); + } + // @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/signing-utils/src/signing-clients/local-signing-client.spec.ts b/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts new file mode 100644 index 00000000..0f9c7b97 --- /dev/null +++ b/packages/signing-utils/src/signing-clients/local-signing-client.spec.ts @@ -0,0 +1,44 @@ +import fs from 'fs/promises'; +import { LocalSigningClient } from './local-signing-client'; +import { expect } from 'chai'; +import { writeFileSync } from 'fs'; + +describe('LocalSigningClient', function () { + const signingScript = './garasign-temp.sh'; + const fileToSign = 'file-to-sign.txt'; + const fileNameAfterGpgSigning = 'file-to-sign.txt.sig'; + + beforeEach(async function () { + writeFileSync( + signingScript, + ` + #!/bin/bash + echo "Signing script called with arguments: $@" + echo "signed content" > ${fileNameAfterGpgSigning} + ` + ); + await fs.writeFile(fileToSign, 'original content'); + }); + + afterEach(async function () { + await Promise.allSettled( + [signingScript, fileToSign, fileNameAfterGpgSigning].map((file) => + fs.rm(file) + ) + ); + }); + + it('executes the signing script correctly', async function () { + const localSigningClient = new LocalSigningClient({ + signingScript: signingScript, + signingMethod: 'gpg', + }); + + await localSigningClient.sign(fileToSign); + + 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 new file mode 100644 index 00000000..d007cf76 --- /dev/null +++ b/packages/signing-utils/src/signing-clients/local-signing-client.ts @@ -0,0 +1,44 @@ +import path from 'path'; +import { spawnSync } from 'child_process'; +import { debug, getEnv } from '../utils'; +import type { SigningClient, SigningClientOptions } from '.'; + +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: Omit + ) {} + + sign(file: string): Promise { + localClientDebug(`Signing ${file}`); + + const directoryOfFileToSign = path.dirname(file); + + try { + const env = { + ...getEnv(), + method: this.options.signingMethod, + }; + + spawnSync('bash', [this.options.signingScript, path.basename(file)], { + cwd: directoryOfFileToSign, + env, + encoding: 'utf-8', + }); + + localClientDebug(`Signed file ${file}`); + + 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 new file mode 100644 index 00000000..b8fd39f8 --- /dev/null +++ b/packages/signing-utils/src/signing-clients/remote-signing-client.spec.ts @@ -0,0 +1,96 @@ +import fs from 'fs/promises'; +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, { shell: 'bash' }, (err) => { + if (err) { + return reject(err); + } + return resolve('Ok'); + }); + }); + }, + disconnect: () => {}, + } as unknown as SSHClient; +}; + +describe('RemoteSigningClient', function () { + const workingDirectoryPath = 'working-directory'; + const fileToSign = 'file-to-sign.txt'; + const signingScript = 'script.sh'; + + 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 Promise.allSettled([ + fs.rm(workingDirectoryPath, { recursive: true, force: true }), + fs.rm(signingScript), + fs.rm(fileToSign), + ]); + }); + + it('signs the file correctly', async function () { + const remoteSigningClient = new RemoteSigningClient(getMockedSSHClient(), { + workingDirectory: workingDirectoryPath, + signingScript: signingScript, + signingMethod: 'gpg', + }); + + await remoteSigningClient.sign(fileToSign); + + const signedFile = (await fs.readFile(fileToSign, 'utf-8')).trim(); + expect(signedFile).to.equal('RemoteSigningClient: signed content'); + }); +}); diff --git a/packages/signing-utils/src/signing-clients/remote-signing-client.ts b/packages/signing-utils/src/signing-clients/remote-signing-client.ts new file mode 100644 index 00000000..02832434 --- /dev/null +++ b/packages/signing-utils/src/signing-clients/remote-signing-client.ts @@ -0,0 +1,119 @@ +import path from 'path'; +import type { SFTPWrapper } from 'ssh2'; +import type { SSHClient } from '../ssh-client'; +import { debug, getEnv } 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.workingDirectory}`); + + // Copy the signing script to the remote machine + { + 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.workingDirectory}/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) { + 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. + * So, here we are passing the env variables as part of the command. + */ + const cmds = [ + `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 + `export garasign_password=${env.garasign_password}`, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `export artifactory_username=${env.artifactory_username}`, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `export artifactory_password=${env.artifactory_password}`, + `export method=${this.options.signingMethod}`, + `./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(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}`); + this.sshClient.disconnect(); + } + } +} diff --git a/packages/signing-utils/src/ssh-client.spec.ts b/packages/signing-utils/src/ssh-client.spec.ts new file mode 100644 index 00000000..805b09b1 --- /dev/null +++ b/packages/signing-utils/src/ssh-client.spec.ts @@ -0,0 +1,186 @@ +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; + let sandbox: sinon.SinonSandbox; + + beforeEach(function () { + const sshClientOptions = { + host: 'example.com', + port: 22, + username: 'admin', + }; + sshClient = new SSHClient(sshClientOptions); + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('connect()', function () { + let connectStub: sinon.SinonStub; + + beforeEach(function () { + connectStub = sandbox + .stub(sshClient['sshConnection'], 'connect') + .returns(sshClient['sshConnection']); + }); + it('connects successfully on ready', async function () { + const connectPromise = sshClient.connect(); + sshClient['sshConnection'].emit('ready'); + await connectPromise; + + 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'); + + 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', + port: 22, + username: 'admin', + privateKey: undefined, + }); + expect(sshClient).to.have.property('connected', false); + }); + }); + + 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; + }); + }); + + describe('exec()', function () { + const COMMAND = 'echo "Hello World"'; + 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') + .yieldsRight(undefined, clientStream); + sshClient['sshConnection'].emit('ready'); + }); + + it('should throw when exec fails', async function () { + execStub.yieldsRight(new Error('Callback Error')); + + sshClient['sshConnection'].emit('ready'); + + 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 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); + }); + + 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); + }); + }); + + 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/); + }); + }); + + describe('when the ssh client is connected', function () { + let connectionStub: sinon.SinonStub; + beforeEach(function () { + connectionStub = sandbox + .stub(sshClient['sshConnection'], 'sftp') + .yieldsRight(undefined, 'mockedSFTP'); + + 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 new file mode 100644 index 00000000..1b3aeb5c --- /dev/null +++ b/packages/signing-utils/src/ssh-client.ts @@ -0,0 +1,92 @@ +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; + private sftpConnection?: SFTPWrapper; + + private connected = false; + + constructor(private sshClientOptions: ConnectConfig) { + 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; + } + const privateKey = this.sshClientOptions.privateKey + ? await readFile(this.sshClientOptions.privateKey) + : undefined; + + const ready = once(this.sshConnection, 'ready'); + this.sshConnection.connect({ + ...this.sshClientOptions, + privateKey, + }); + await ready; + } + + disconnect() { + this.sshConnection.end(); + this.connected = false; + } + + async exec(command: string): Promise { + if (!this.connected) { + throw new Error('Not connected to ssh server'); + } + 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 { + if (!this.connected) { + throw new Error('Not connected to ssh server'); + } + + this.sftpConnection = + this.sftpConnection ?? + (await promisify(this.sshConnection.sftp.bind(this.sshConnection))()); + return this.sftpConnection; + } +} diff --git a/packages/signing-utils/src/utils.ts b/packages/signing-utils/src/utils.ts new file mode 100644 index 00000000..f4919932 --- /dev/null +++ b/packages/signing-utils/src/utils.ts @@ -0,0 +1,21 @@ +import { debug as debugFn } from 'debug'; + +export const debug = debugFn('signing-utils'); + +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, + }; +} diff --git a/packages/signing-utils/tsconfig-lint.json b/packages/signing-utils/tsconfig-lint.json new file mode 100644 index 00000000..6bdef84f --- /dev/null +++ b/packages/signing-utils/tsconfig-lint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/signing-utils/tsconfig.json b/packages/signing-utils/tsconfig.json new file mode 100644 index 00000000..21899b27 --- /dev/null +++ b/packages/signing-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.*"] +}