Install: verify prebuilt binaries with Subresource Integrity check

This commit is contained in:
Lovell Fuller 2021-12-12 18:49:17 +00:00
parent 3da258f6fb
commit 3b492ea423
6 changed files with 85 additions and 5 deletions

View File

@ -10,6 +10,8 @@ Requires libvips v8.12.1
* Reduce minimum Linux ARM64v8 glibc requirement to 2.17. * Reduce minimum Linux ARM64v8 glibc requirement to 2.17.
* Verify prebuilt binaries with a Subresource Integrity check.
* Standardise WebP `effort` option name, deprecate `reductionEffort`. * Standardise WebP `effort` option name, deprecate `reductionEffort`.
* Standardise HEIF `effort` option name, deprecate `speed`. * Standardise HEIF `effort` option name, deprecate `speed`.

View File

@ -23,8 +23,9 @@ Ready-compiled sharp and libvips binaries are provided for use on the most commo
* Windows x64 * Windows x64
* Windows x86 * Windows x86
An ~7MB tarball containing libvips and its most commonly used dependencies A ~7MB tarball containing libvips and its most commonly used dependencies
is downloaded via HTTPS and stored within `node_modules/sharp/vendor` during `npm install`. is downloaded via HTTPS, verified via Subresource Integrity
and decompressed into `node_modules/sharp/vendor` during `npm install`.
This provides support for the This provides support for the
JPEG, PNG, WebP, AVIF, TIFF, GIF and SVG (input) image formats. JPEG, PNG, WebP, AVIF, TIFF, GIF and SVG (input) image formats.
@ -78,7 +79,7 @@ npm install --platform=... --arch=... --arm-version=... sharp
* `--platform`: one of `linux`, `linuxmusl`, `darwin` or `win32`. * `--platform`: one of `linux`, `linuxmusl`, `darwin` or `win32`.
* `--arch`: one of `x64`, `ia32`, `arm` or `arm64`. * `--arch`: one of `x64`, `ia32`, `arm` or `arm64`.
* `--arm-version`: one of `6`, `7` or `8` (`arm` defaults to `6`, `arm64` defaults to `8`). * `--arm-version`: one of `6`, `7` or `8` (`arm` defaults to `6`, `arm64` defaults to `8`).
* `--sharp-install-force`: skip version compatibility checks. * `--sharp-install-force`: skip version compatibility and Subresource Integrity checks.
These values can also be set via environment variables, These values can also be set via environment variables,
`npm_config_platform`, `npm_config_arch`, `npm_config_arm_version` `npm_config_platform`, `npm_config_arch`, `npm_config_arm_version`

View File

@ -5,6 +5,7 @@ const os = require('os');
const path = require('path'); const path = require('path');
const stream = require('stream'); const stream = require('stream');
const zlib = require('zlib'); const zlib = require('zlib');
const { createHash } = require('crypto');
const detectLibc = require('detect-libc'); const detectLibc = require('detect-libc');
const semverLessThan = require('semver/functions/lt'); const semverLessThan = require('semver/functions/lt');
@ -55,6 +56,33 @@ const handleError = function (err) {
} }
}; };
const verifyIntegrity = function (platformAndArch) {
const expected = libvips.integrity(platformAndArch);
if (installationForced || !expected) {
libvips.log(`Integrity check skipped for ${platformAndArch}`);
return new stream.PassThrough();
}
const hash = createHash('sha512');
return new stream.Transform({
transform: function (chunk, _encoding, done) {
hash.update(chunk);
done(null, chunk);
},
flush: function (done) {
const digest = `sha512-${hash.digest('base64')}`;
if (expected !== digest) {
libvips.removeVendoredLibvips();
libvips.log(`Integrity expected: ${expected}`);
libvips.log(`Integrity received: ${digest}`);
done(new Error(`Integrity check failed for ${platformAndArch}`));
} else {
libvips.log(`Integrity check passed for ${platformAndArch}`);
done();
}
}
});
};
const extractTarball = function (tarPath, platformAndArch) { const extractTarball = function (tarPath, platformAndArch) {
const versionedVendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platformAndArch); const versionedVendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platformAndArch);
libvips.mkdirSync(versionedVendorPath); libvips.mkdirSync(versionedVendorPath);
@ -66,6 +94,7 @@ const extractTarball = function (tarPath, platformAndArch) {
stream.pipeline( stream.pipeline(
fs.createReadStream(tarPath), fs.createReadStream(tarPath),
verifyIntegrity(platformAndArch),
new zlib.BrotliDecompress(), new zlib.BrotliDecompress(),
tarFs.extract(versionedVendorPath, { ignore }), tarFs.extract(versionedVendorPath, { ignore }),
function (err) { function (err) {

View File

@ -8,10 +8,11 @@ const semverCoerce = require('semver/functions/coerce');
const semverGreaterThanOrEqualTo = require('semver/functions/gte'); const semverGreaterThanOrEqualTo = require('semver/functions/gte');
const platform = require('./platform'); const platform = require('./platform');
const { config } = require('../package.json');
const env = process.env; const env = process.env;
const minimumLibvipsVersionLabelled = env.npm_package_config_libvips || /* istanbul ignore next */ const minimumLibvipsVersionLabelled = env.npm_package_config_libvips || /* istanbul ignore next */
require('../package.json').config.libvips; config.libvips;
const minimumLibvipsVersion = semverCoerce(minimumLibvipsVersionLabelled).version; const minimumLibvipsVersion = semverCoerce(minimumLibvipsVersionLabelled).version;
const spawnSyncOptions = { const spawnSyncOptions = {
@ -19,6 +20,8 @@ const spawnSyncOptions = {
shell: true shell: true
}; };
const vendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platform());
const mkdirSync = function (dirPath) { const mkdirSync = function (dirPath) {
try { try {
fs.mkdirSync(dirPath, { recursive: true }); fs.mkdirSync(dirPath, { recursive: true });
@ -39,6 +42,10 @@ const cachePath = function () {
return libvipsCachePath; return libvipsCachePath;
}; };
const integrity = function (platformAndArch) {
return env[`npm_package_config_integrity_${platformAndArch.replace('-', '_')}`] || config.integrity[platformAndArch];
};
const log = function (item) { const log = function (item) {
if (item instanceof Error) { if (item instanceof Error) {
console.error(`sharp: Installation error: ${item.message}`); console.error(`sharp: Installation error: ${item.message}`);
@ -67,10 +74,15 @@ const globalLibvipsVersion = function () {
}; };
const hasVendoredLibvips = function () { const hasVendoredLibvips = function () {
const vendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platform());
return fs.existsSync(vendorPath); return fs.existsSync(vendorPath);
}; };
/* istanbul ignore next */
const removeVendoredLibvips = function () {
const rm = fs.rmSync ? fs.rmSync : fs.rmdirSync;
rm(vendorPath, { recursive: true, maxRetries: 3, force: true });
};
const pkgConfigPath = function () { const pkgConfigPath = function () {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
const brewPkgConfigPath = spawnSync('which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR', spawnSyncOptions).stdout || ''; const brewPkgConfigPath = spawnSync('which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR', spawnSyncOptions).stdout || '';
@ -99,9 +111,11 @@ module.exports = {
minimumLibvipsVersion, minimumLibvipsVersion,
minimumLibvipsVersionLabelled, minimumLibvipsVersionLabelled,
cachePath, cachePath,
integrity,
log, log,
globalLibvipsVersion, globalLibvipsVersion,
hasVendoredLibvips, hasVendoredLibvips,
removeVendoredLibvips,
pkgConfigPath, pkgConfigPath,
useGlobalLibvips, useGlobalLibvips,
mkdirSync mkdirSync

View File

@ -151,6 +151,19 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"config": { "config": {
"libvips": "8.12.1", "libvips": "8.12.1",
"integrity": {
"darwin-arm64v8": "sha512-Keb3wpyDLdYad3NFvh0qpXXpUDTK/QvUukMuQkUNoIdkMlVL73HI7TOW29UKxCS4C1xFn284miOh3UczvXwToQ==",
"darwin-x64": "sha512-Q0laQN+afWnXjlTLebPLnqLmDBKj8nN6g/VW2jfU3XfzUNqxFM39gCmddKdN5ekIfGka6tZAf8tTJuCqXotVUg==",
"linux-arm64v8": "sha512-gaUQsJReRd2GfUpQWRJnQtRAoLr+CABsGxyMOXk3m4ajp53rGr34l4Pcyet4r/XFOtF9jDvcAOKpyydZMv3qsg==",
"linux-armv6": "sha512-s+nQWZNsclbQopLxbuSAuHfluX31vEpsU020mNAS5VUE2Pemhn4CCGX79thyvXF0hlXxBsxAb18dzU7hXOz/PQ==",
"linux-armv7": "sha512-ra8Yp0FvO3b5dJQ2nqBxc1sBz3R8LQOno8MzNlubg6jWM5FIfGlfFe1yxXLKEXVgqOLf4ABjNPhX6kDQx5v+Kw==",
"linux-x64": "sha512-q2JPFq9FXCEIYKlfdhF70hEtpQquqGMnecYnTlB0Vcjyy1CY6aN+91difiEnvCSCHYzpoXDGv6C/ZNCMmdcD3Q==",
"linuxmusl-arm64v8": "sha512-JwG5rTa2UneL0uZCjbEmw0pRoQY5gnqtT2g9OkSuuGrlbHsrJuBGFVKEnqzox7C3g/yXgk0ePn4JaxefJXFaOQ==",
"linuxmusl-x64": "sha512-Quj7YHPNWZu+HvdQbreSxaWUc9gg1nYTrN/NQLkf7DmME5XrVxA2tkIy7+IIuFz2Tk7LfG8jLJ8c6ZKuYEMBSg==",
"win32-arm64v8": "sha512-QWchuNLKgZSpdh+Ixr9tve1XJFYRir9wFdPfwncJpOAAa5cbmJRMFReYa0iF4ncqO2MxTw6aHcDvfbg6bwaDtw==",
"win32-ia32": "sha512-/uyO2tP0okJOD1UjXIiGurwLlF7M+7MYaldQ9p2in+rUMZxdSuglEhgHS8tjMXHc7QeXMfGaGSW0bxk2Yu1L0A==",
"win32-x64": "sha512-PF+ZcWnDYbsQXWaaJ3yngWD+E0bPq1I7S+xeqms8frrA8trPdQxC8Jjvw4Aaylynn9YMXzPYY7RAeIzopOqFxA=="
},
"runtime": "napi", "runtime": "napi",
"target": 5 "target": 5
}, },

View File

@ -76,6 +76,27 @@ describe('libvips binaries', function () {
}); });
}); });
describe('integrity', function () {
it('reads value from environment variable', function () {
const prev = process.env.npm_package_config_integrity_platform_arch;
process.env.npm_package_config_integrity_platform_arch = 'sha512-test';
const integrity = libvips.integrity('platform-arch');
assert.strictEqual('sha512-test', integrity);
process.env.npm_package_config_integrity_platform_arch = prev;
});
it('reads value from package.json', function () {
const prev = process.env.npm_package_config_integrity_linux_x64;
delete process.env.npm_package_config_integrity_linux_x64;
const integrity = libvips.integrity('linux-x64');
assert.strictEqual('sha512-', integrity.substr(0, 7));
process.env.npm_package_config_integrity_linux_x64 = prev;
});
});
describe('safe directory creation', function () { describe('safe directory creation', function () {
before(function () { before(function () {
mockFS({ mockFS({