Consumer-side, install-time artifact-hash verification for CPAN.
CPAN workflows can pin versions without pinning bytes. Carton's
cpanfile.snapshot records the resolved dependency tree and exact versions but
no artifact digests; cpm consumes a snapshot but does not appear to provide a
lockfile-integrity mode equivalent to pip's hash-checking or npm's lockfile
integrity field;
cpanm --verify is off by default and does not verify the integrity of the
CHECKSUMS file itself. cpan-integ fills the consumer side: you already
resolved your dependency graph — this records the SHA-256 of each distribution's
bytes in a committed lockfile and fails the build if a fetched artifact
differs from what was pinned.
Status: independent experimental prototype, not a CPANSec-endorsed tool.
cpan-integ is designed to catch drift and tampering of artifact bytes between pin time and install time:
- a mirror or CDN serving different bytes than were originally resolved
- a re-uploaded / mutated distribution at the same version
- accidental corruption in transit or in a local cache
- an unexpected distribution being introduced into the install set (with
verify --snapshot)
It is not a registry-compromise defense and not an identity/provenance system. If an attacker controls the source at pin time, they control the hash you trust on first pin. That is the same trust boundary as every lockfile.
Trust-on-first-pin, cryptographically verified thereafter — the same class of
control as pip's hash mode and npm's lockfile integrity field.
pindownloads each artifact, computes its SHA-256 from the actual bytes, and cross-checks that against MetaCPAN's publishedchecksum_sha256when available. It aborts the pin on disagreement. The locally computed hash is recorded as authoritative. MetaCPAN is raw material and a cross-check, not an independent trusted third party — it provides both the artifact location and the checksum.- The committed lockfile becomes the trust source after first pin. Every
verifyafterward is a pure cryptographic comparison.
# Record hashes for everything in a Carton snapshot:
cpan-integ pin --snapshot cpanfile.snapshot --out cpanfile.integrity
# Commit cpanfile.integrity, then in CI / before deploy:
cpan-integ verify --integrity cpanfile.integrity
# Stronger: also assert the lock describes exactly the snapshot's artifacts:
cpan-integ verify --integrity cpanfile.integrity --snapshot cpanfile.snapshot
# Build a complete verified local mirror (artifacts + 02packages index):
cpan-integ fetch --integrity cpanfile.integrity --snapshot cpanfile.snapshot --cache cpan-integ-cache
# Install from the verified mirror only (cpanm resolves names via the generated index):
cpanm --mirror "$PWD/cpan-integ-cache" --mirror-only --installdeps .verify exits non-zero on any mismatch or inconsistency, so it drops straight
into a CI step.
Line-based and diff-friendly; each distribution is identified by its Package URL (ECMA-427) CPAN form, with the distfile URL recorded as artifact metadata:
# cpanfile.integrity - generated by cpan-integ 0.002
# format: <purl> sha256:<hex of locally-hashed bytes> <download_url>
pkg:cpan/ETHER/Try-Tiny@0.32 sha256:ef2d6cab...fa7fc0 https://cpan.metacpan.org/authors/id/E/ET/ETHER/Try-Tiny-0.32.tar.gz
$ cpan-integ verify --integrity cpanfile.tampered.integrity
FAIL pkg:cpan/ETHER/Try-Tiny@0.32
expected 000000000bad18e3ab1c4e6125cc5f695c7e459899f512451c8fa3ef83fa7fc0
got ef2d6cab0bad18e3ab1c4e6125cc5f695c7e459899f512451c8fa3ef83fa7fc0
ok pkg:cpan/DAGOLDEN/Capture-Tiny@0.50
ok pkg:cpan/DAGOLDEN/Path-Tiny@0.150
cpan-integ: 2 verified, 1 failed, 3 total
$ echo $?
1
- Not a replacement for
cpm,Carton,cpanm, orCPAN.pm. - Not a dependency resolver — it constrains only artifacts already present in your resolved snapshot, and never introduces new dependencies.
- Not a signing/provenance system. Sigstore/transparency-log verification is a possible future cross-check, not the core.
- Install path: verified mirror validated in CI.
fetch --snapshotmaterializes a complete local mirror with verifiedauthors/id/...artifacts and a PAUSE-formatmodules/02packages.details.txt.gzindex. The CI e2e job installs from that mirror withcpanm --mirror-onlyand asserts the installed module loads. This proves the core path: install the exact bytes that were pinned and verified. - CPAN-only. Non-CPAN snapshot sources (git/url/darkpan) are rejected unless
--allow-nonstandardis given, in which case they are skipped, not verified. - Network required for
pinandverify.
- Phase 0 (done): sidecar lockfile, local-bytes hashing + MetaCPAN cross-check, snapshot/lock reconciliation, tests.
- Phase 1 (done): richer
verifyconstraints (missing/extra/nonstandard). - Phase 2 (done): preflight→install gap closed.
fetch --snapshotmaterializes a verifiedauthors/id/...mirror +02packagesindex; the CI e2e job installs from it withcpanm --mirror-onlyand asserts the module loads. - Phase 3 (optional, deferred): verify Sigstore bundles as a stronger provenance signal, subject to an explicit identity/issuer policy.
Minimal dependency footprint: parsing, hashing, and lockfile handling use core
modules where possible. Live HTTPS fetching requires IO::Socket::SSL and
Net::SSLeay, matching HTTP::Tiny's TLS requirements — the goal is to avoid
nonessential dependencies, not to pretend TLS transport is dependency-free. The
PURL identity is format-compatible with URI::PackageURL (not a dependency).
# offline unit tests (parse / lockfile / reconcile / index):
prove -lv t/01-snapshot.t t/02-lockfile.t t/03-reconcile.t t/04-index.t
# live network test (pin / verify / fetch / tamper against real CPAN):
CPAN_INTEG_LIVE=1 perl -Ilib t/99-live.tEnd-to-end — build a verified mirror, install from it, and confirm the module loads (the shape the CI e2e job runs):
perl -Ilib bin/cpan-integ pin \
--snapshot examples/cpanfile.snapshot \
--out cpanfile.integrity
perl -Ilib bin/cpan-integ fetch \
--integrity cpanfile.integrity \
--snapshot examples/cpanfile.snapshot \
--cache mirror
cpanm --mirror "$PWD/mirror" --mirror-only --notest \
--local-lib-contained "$PWD/local-lib" Try::Tiny
perl -I"$PWD/local-lib/lib/perl5" -MTry::Tiny -e 'print Try::Tiny->VERSION, "\n"'Same terms as Perl itself (Artistic 1.0 / GPL 1.0+); see LICENSE.