Skip to content

[BUG] npm audit signatures returns ETARGET on lockfile-pinned versions younger than min-release-age #9277

@Bafff

Description

@Bafff

Is there an existing issue for this?

  • I have searched the existing issues

This issue exists in the latest npm version

  • I am using the latest npm

Current Behavior

With min-release-age (or its flattened equivalent before) set in .npmrc or via CLI, npm audit signatures fails with ETARGET for any package whose lockfile-pinned version was published after the cutoff — even though that exact version has already been installed by npm ci in the same pipeline run, moments earlier.

Observed in a CI pipeline with min-release-age=7:

> npm audit signatures

npm warn Fetching verification keys using TUF failed.  Fetching directly from https://<private-registry>/.
npm error code ETARGET
npm error notarget No matching version found for caniuse-lite@1.0.30001790 with a date before 4/16/2026, 7:54:30 PM.
npm error notarget In most cases you or one of your dependencies are requesting a package version that doesn't exist.
##[error]Bash exited with code '1'.

Timeline of that same pipeline run:

  1. npm cisucceeds, installs caniuse-lite@1.0.30001790 (pinned in package-lock.json) into node_modules.
  2. npm audit signaturesfails with ETARGET on the exact same version that was just installed.

The version exists, it's in the lockfile, it's on disk. It's rejected only because audit signatures re-resolves through the registry with the before filter applied, and the packument's time[version] is newer than the cutoff.

Expected Behavior

npm audit signatures should verify the signatures of the packages that are actually installed, using the exact versions recorded in node_modules / package-lock.json. min-release-age is an install-time policy; by the time we reach audit signatures, the policy has already been evaluated (or bypassed) during npm ci. Re-applying it during signature verification can only produce false negatives on already-admitted code — it cannot protect anything that isn't already protected, because the code is already on disk.

Concretely: the pacote.manifest() call inside audit signatures is asked for an exact name@version spec (not a range, not a tag), so the --before filter contributes nothing beyond turning valid lookups into ETARGETs.

Steps To Reproduce

  1. Create a project with a single dep:
    // package.json
    { "name": "repro", "version": "1.0.0", "dependencies": { "kms-demo": "1.0.0" } }
  2. npm install to produce node_modules and package-lock.json.
  3. Run:
    npm audit signatures --before=2020-01-01
    (or --min-release-age=99999, or set min-release-age=99999 in .npmrc)
  4. Observe:
    npm error code ETARGET
    npm error notarget No matching version found for kms-demo@1.0.0 with a date before 1/1/2020, 12:00:00 AM.
    

The package is installed. The version is pinned. audit signatures still refuses to verify it.

Environment

Root cause

workspaces/config/lib/definitions/definitions.js:1377-1383 flattens min-release-age into flatOptions.before = Date.now() - days * 86400000. Every consumer of flatOptions inherits that cutoff.

lib/utils/verify-signatures.js:294-306:

async verifySignatures (name, version, registry) {
  const {
    _integrity: integrity,
    _signatures,
    _attestations,
    _attestationBundles,
    _resolved: resolved,
  } = await pacote.manifest(`${name}@${version}`, {
    verifySignatures: true,
    verifyAttestations: true,
    ...this.buildRegistryConfig(registry),
    ...this.npm.flatOptions,   // ← leaks `before` into manifest lookup
  })
  ...
}

name and version come from arb.loadActual() a few frames up — they are the exact versions present in node_modules. The pacote spec is exact. The before filter has no legitimate work to do here — its only possible effect is to reject a version that is already installed.

Proposed fix

One line, in lib/utils/verify-signatures.js:

     } = await pacote.manifest(`${name}@${version}`, {
       verifySignatures: true,
       verifyAttestations: true,
       ...this.buildRegistryConfig(registry),
       ...this.npm.flatOptions,
+      before: null, // audit signatures verifies exact installed versions; install-time policy does not apply here
     })

Why safe:

  • audit signatures only operates on loadActual() trees — the code is already installed. Stripping before cannot bypass the policy because the policy decision happened during install and can't be un-done at audit time.
  • The spec is exact (name@version), not a range/tag — before has no legitimate filtering work to do.
  • No breaking change: the fix only removes false-negative ETARGETs; verification results themselves are unchanged.

Relation to prior issues / RFCs

  • Distinguishes from #9212 (closed, not-planned): that issue was about npm audit fix declining to install a fix younger than the cutoff. That is policy working as intended — audit fix must respect install-time policy because it installs new code. This issue is about npm audit signatures erroring on code that was already installed by npm ci seconds earlier in the same pipeline. That is not policy working — it is a pipeline-internal self-contradiction: npm ci admitted the version, audit signatures pretends it does not exist.
  • Aligned with RRFC #856 (open): "Ability to override --min-release-age on CLI or in the lockfile" proposes that lockfile-pinned versions be implicit exceptions to the policy. This fix is a narrower, immediately-actionable subset: only audit signatures (which cannot install anything) is exempted, and only for exact-version lookups. No new config surface.
  • Related: #9005 (closed): min-release-age leaking into git dep preparation — same family of "the flattened before filter is reaching internal invocations where it shouldn't".

Additional context — what the fix does not change

  • npm install / npm ci still apply min-release-age exactly as today.
  • npm audit fix still applies min-release-age exactly as today.
  • npm audit signatures on a package that is not installed (e.g., after a partial install) behaves as it does today — it returns what pacote returns; the exact-version lookup still fails if the tarball is genuinely gone.
  • No change to TUF/keys fetch; no change to signature/attestation logic.

Happy to send a PR with the 1-line fix + regression test if this looks good.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions