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.
* Verify prebuilt binaries with a Subresource Integrity check.
* Standardise WebP `effort` option name, deprecate `reductionEffort`.
* 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 x86
An ~7MB tarball containing libvips and its most commonly used dependencies
is downloaded via HTTPS and stored within `node_modules/sharp/vendor` during `npm install`.
A ~7MB tarball containing libvips and its most commonly used dependencies
is downloaded via HTTPS, verified via Subresource Integrity
and decompressed into `node_modules/sharp/vendor` during `npm install`.
This provides support for the
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`.
* `--arch`: one of `x64`, `ia32`, `arm` or `arm64`.
* `--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,
`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 stream = require('stream');
const zlib = require('zlib');
const { createHash } = require('crypto');
const detectLibc = require('detect-libc');
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 versionedVendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platformAndArch);
libvips.mkdirSync(versionedVendorPath);
@ -66,6 +94,7 @@ const extractTarball = function (tarPath, platformAndArch) {
stream.pipeline(
fs.createReadStream(tarPath),
verifyIntegrity(platformAndArch),
new zlib.BrotliDecompress(),
tarFs.extract(versionedVendorPath, { ignore }),
function (err) {

View File

@ -8,10 +8,11 @@ const semverCoerce = require('semver/functions/coerce');
const semverGreaterThanOrEqualTo = require('semver/functions/gte');
const platform = require('./platform');
const { config } = require('../package.json');
const env = process.env;
const minimumLibvipsVersionLabelled = env.npm_package_config_libvips || /* istanbul ignore next */
require('../package.json').config.libvips;
config.libvips;
const minimumLibvipsVersion = semverCoerce(minimumLibvipsVersionLabelled).version;
const spawnSyncOptions = {
@ -19,6 +20,8 @@ const spawnSyncOptions = {
shell: true
};
const vendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platform());
const mkdirSync = function (dirPath) {
try {
fs.mkdirSync(dirPath, { recursive: true });
@ -39,6 +42,10 @@ const cachePath = function () {
return libvipsCachePath;
};
const integrity = function (platformAndArch) {
return env[`npm_package_config_integrity_${platformAndArch.replace('-', '_')}`] || config.integrity[platformAndArch];
};
const log = function (item) {
if (item instanceof Error) {
console.error(`sharp: Installation error: ${item.message}`);
@ -67,10 +74,15 @@ const globalLibvipsVersion = function () {
};
const hasVendoredLibvips = function () {
const vendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platform());
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 () {
if (process.platform !== 'win32') {
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,
minimumLibvipsVersionLabelled,
cachePath,
integrity,
log,
globalLibvipsVersion,
hasVendoredLibvips,
removeVendoredLibvips,
pkgConfigPath,
useGlobalLibvips,
mkdirSync

View File

@ -151,6 +151,19 @@
"license": "Apache-2.0",
"config": {
"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",
"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 () {
before(function () {
mockFS({