Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,19 @@ same major line. Should you need to upgrade to a new major, use an explicit
environment variables are required and as plain text. If you want to send an
empty password, explicitly set `COREPACK_NPM_PASSWORD` to an empty string.

- `COREPACK_ON_UNVERIFIED_DOWNLOAD` can be set to:
- `warn` (case insensitive): attempting to download an unsigned version without
providing a hash will emit a warning to stderr.
- `error` (case insensitive): attempting to download an unsigned version without
providing a hash will fail with an error, and nothing gets downloaded.
- `strict-warn` (case insensitive): same as `warn`, and additionally emits a
warning when downloading a version that is not pinned by a hash, even when
its signature can be verified.
- `strict-error` (case insensitive): same as `error`, and additionally fails
when downloading a version that is not pinned by a hash, even when its
signature can be verified.
- `ignore` (or any other unsupported value): disables that security feature.

- `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are supported through
[`NODE_USE_ENV_PROXY=1`](https://nodejs.org/api/cli.html#node_use_env_proxy1).

Expand Down
47 changes: 35 additions & 12 deletions sources/corepackUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,38 @@ export async function installVersion(installTarget: string, locator: Locator, {s
}
}

const registry = getRegistryFromPackageManagerSpec(spec);
const canVerifySignature = registry.type === `npm` && !registry.bin && !shouldSkipIntegrityCheck();
if (!build[1]) {
const {COREPACK_ON_UNVERIFIED_DOWNLOAD} = process.env;
debugUtils.log(`No hash provided${canVerifySignature ? `` : `, and signature cannot be verified`}; checking COREPACK_ON_UNVERIFIED_DOWNLOAD, set to: ${COREPACK_ON_UNVERIFIED_DOWNLOAD}`);
const mode = COREPACK_ON_UNVERIFIED_DOWNLOAD?.toUpperCase();
// In strict mode, a hash is required even when the signature can be verified.
const isStrict = mode === `STRICT-ERROR` || mode === `STRICT-WARN`;
if (isStrict || !canVerifySignature) {
const reason = canVerifySignature
? `is not pinned by a hash`
: `could not be verified`;

switch (mode) {
case `ERROR`:
case `STRICT-ERROR`:
throw new Error(`Integrity of ${locator.name}@${version} ${reason}. Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable. Please provide a hash.`);

case `WARN`:
case `STRICT-WARN`:
console.warn(`Integrity of ${locator.name}@${version} ${reason}. Consider providing a hash. Set COREPACK_ON_UNVERIFIED_DOWNLOAD to 'ignore' to remove this warning.`);
}
}
}

let url: string;
let signatures: Array<{keyid: string, sig: string}>;
let integrity: string;
let binPath: string | null = null;
if (locatorIsASupportedPackageManager) {
url = spec.url.replace(`{}`, version);
if (process.env.COREPACK_NPM_REGISTRY) {
const registry = getRegistryFromPackageManagerSpec(spec);
if (registry.type === `npm`) {
({tarball: url, signatures, integrity} = await npmRegistryUtils.fetchTarballURLAndSignature(registry.package, version));
if (registry.bin) {
Expand Down Expand Up @@ -294,19 +318,18 @@ export async function installVersion(installTarget: string, locator: Locator, {s
}
}

if (!build[1]) {
const registry = getRegistryFromPackageManagerSpec(spec);
if (registry.type === `npm` && !registry.bin && !shouldSkipIntegrityCheck()) {
if (signatures! == null || integrity! == null)
({signatures, integrity} = (await npmRegistryUtils.fetchTarballURLAndSignature(registry.package, version)));

await npmRegistryUtils.verifySignature({signatures, integrity, packageName: registry.package, version});
// @ts-expect-error ignore readonly
build[1] = Buffer.from(integrity.slice(`sha512-`.length), `base64`).toString(`hex`);
}
if (!build[1] && canVerifySignature && registry.type === `npm`) {
if (signatures! == null || integrity! == null)
({signatures, integrity} = (await npmRegistryUtils.fetchTarballURLAndSignature(registry.package, version)));

npmRegistryUtils.verifySignature({signatures, integrity, packageName: registry.package, version});
// @ts-expect-error ignore readonly
build[1] = Buffer.from(integrity.slice(`sha512-`.length), `base64`).toString(`hex`);
}
if (build[1] && actualHash !== build[1])
if (build[1] && actualHash !== build[1]) {
await fs.promises.rm(tmpFolder, {recursive: true, force: true});
throw new Error(`Mismatch hashes. Expected ${build[1]}, got ${actualHash}`);
}

const serializedHash = `${algo}.${actualHash}`;

Expand Down
4 changes: 3 additions & 1 deletion tests/Up.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ describe(`UpCommand`, () => {
packageManager: `yarn@2.1.0`,
});

process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`;

await expect(runCli(cwd, [`up`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
stderr: `Integrity of yarn@2.4.3 could not be verified. Consider providing a hash. Set COREPACK_ON_UNVERIFIED_DOWNLOAD to 'ignore' to remove this warning.\n`,
stdout: expect.stringMatching(/^Installing yarn@2\.4\.3 in the project\.\.\.\n\n/),
});

Expand Down
4 changes: 3 additions & 1 deletion tests/Use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,11 @@ describe(`UseCommand`, () => {
const subfolder = ppath.join(cwd, `subfolder`);
await xfs.mkdirPromise(subfolder);

process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`;

await expect(runCli(subfolder, [`use`, `yarn@2.2.2`])).resolves.toMatchObject({
exitCode: 0,
stderr: ``,
stderr: `Integrity of yarn@2.2.2 could not be verified. Consider providing a hash. Set COREPACK_ON_UNVERIFIED_DOWNLOAD to 'ignore' to remove this warning.\n`,
});
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
Expand Down
204 changes: 197 additions & 7 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ it(`should support disabling the network accesses from the environment`, async (

await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
packageManager: `yarn@2.2.2`,
packageManager: `yarn@2.2.2+sha1.9aede2626b101719cbc1314d61def0742852ba11`,
});

await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
Expand All @@ -881,12 +881,12 @@ describe(`read-only and offline environment`, () => {

// Prepare fake project
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
packageManager: `yarn@2.2.2`,
packageManager: `yarn@2.2.2+sha1.9aede2626b101719cbc1314d61def0742852ba11`,
});

// $ corepack install
await expect(runCli(cwd, [`install`])).resolves.toMatchObject({
stdout: `Adding yarn@2.2.2 to the cache...\n`,
stdout: `Adding yarn@2.2.2+sha1.9aede2626b101719cbc1314d61def0742852ba11 to the cache...\n`,
stderr: ``,
exitCode: 0,
});
Expand Down Expand Up @@ -1549,6 +1549,196 @@ describe(`should pick up COREPACK_INTEGRITY_KEYS from env`, () => {
});
});

describe(`unverified downloads`, () => {
beforeEach(() => {
process.env.AUTH_TYPE = `COREPACK_NPM_TOKEN`; // See `_registryServer.mjs`
process.env.COREPACK_DEFAULT_TO_LATEST = `1`;
process.env.COREPACK_INTEGRITY_KEYS = `0`;
});

it(`from env variable`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {});

process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `error`;
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 1,
stdout: ``,
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
});
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 1,
stdout: ``,
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
});

process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `ignore`;
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `yarn: Hello from custom registry\n`,
stderr: ``, // No warning expected
});

process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`;
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `pnpm: Hello from custom registry\n`,
stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified.`),
});
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `yarn: Hello from custom registry\n`,
stderr: ``, // Already cached, no warning expected
});
});
});

it(`from .corepack.env file`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {});

await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=error\n`);
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 1,
stdout: ``,
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
});
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 1,
stdout: ``,
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
});

await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=ignore\n`);
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `yarn: Hello from custom registry\n`,
stderr: ``, // No warning expected
});

await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=warn\n`);
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `pnpm: Hello from custom registry\n`,
stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified.`),
});
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `yarn: Hello from custom registry\n`,
stderr: ``, // Already cached, no warning expected
});
});
});

it(`from env file defined by COREPACK_ENV_FILE`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
});

await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=error\n`);
await xfs.writeFilePromise(ppath.join(cwd, `.other.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=warn\n`);

// By default, Corepack should be using .corepack.env and fail.
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 1,
stdout: ``,
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
});

process.env.COREPACK_ENV_FILE = `.other.env`;
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `pnpm: Hello from custom registry\n`,
stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified.`),
});
});
});

it(`from env even if there's a .corepack.env file`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {});

await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=error\n`);

// By default, Corepack should be using .corepack.env (or the built-in ones on Node.js 18.x) and fail.
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 1,
stdout: ``,
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
});

process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`;
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `pnpm: Hello from custom registry\n`,
stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified`),
});
});
});
});

describe(`downloads not pinned by a hash`, () => {
it(`should not warn in non-strict mode when the signature can be verified`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
packageManager: `yarn@1.22.4`,
});

process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`;
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `1.22.4\n`,
stderr: ``,
});
});
});

it(`should warn when COREPACK_ON_UNVERIFIED_DOWNLOAD is set to strict-warn`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
packageManager: `yarn@1.22.4`,
});

process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `strict-warn`;
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `1.22.4\n`,
stderr: `Integrity of yarn@1.22.4 is not pinned by a hash. Consider providing a hash. Set COREPACK_ON_UNVERIFIED_DOWNLOAD to 'ignore' to remove this warning.\n`,
});
});
});

it(`should fail when COREPACK_ON_UNVERIFIED_DOWNLOAD is set to STRICT-ERROR`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
packageManager: `yarn@1.22.4`,
});

process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `STRICT-ERROR`;
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 1,
stdout: ``,
stderr: expect.stringContaining(`Integrity of yarn@1.22.4 is not pinned by a hash. Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable.`),
});
});
});

it(`should not interfere with versions pinned by a hash in strict mode`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
packageManager: `yarn@1.22.4+sha224.0d6eecaf4d82ec12566fdd97143794d0f0c317e0d652bd4d1b305430`,
});

process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `strict-error`;
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
exitCode: 0,
stdout: `1.22.4\n`,
stderr: ``,
});
});
});
});

for (const authType of [`COREPACK_NPM_REGISTRY`, `COREPACK_NPM_TOKEN`, `COREPACK_NPM_PASSWORD`, `PROXY`]) {
describe(`custom registry with auth ${authType}`, () => {
beforeEach(() => {
Expand Down Expand Up @@ -1741,8 +1931,8 @@ describe(`handle integrity checks`, () => {
});
await expect(runCli(cwd, [`use`, `pnpm`], true)).resolves.toMatchObject({
exitCode: 1,
stdout: expect.stringContaining(`Signature does not match`),
stderr: ``,
stderr: expect.stringContaining(`Signature does not match`),
stdout: `Installing pnpm@1.9998.9999 in the project...\n`,
});
});
});
Expand All @@ -1757,8 +1947,8 @@ describe(`handle integrity checks`, () => {
});
await expect(runCli(cwd, [`use`, `yarn@1.9998.9999`], true)).resolves.toMatchObject({
exitCode: 1,
stdout: expect.stringContaining(`Signature does not match`),
stderr: ``,
stderr: expect.stringContaining(`Signature does not match`),
stdout: `Installing yarn@1.9998.9999 in the project...\n`,
});
});
});
Expand Down