elharo commented on issue #91:
URL:
https://github.com/apache/maven-artifact-plugin/issues/91#issuecomment-5847080275
## Implementation plan for `artifact:verify-checksum`
Plan for implementing the goal described in the previous comment. This
supersedes the `-D"sha512[classifier:extension]=..."` interface proposed there:
the two comments on the issue were right that the properties form does not
scale, and the reasons turn out to be structural rather than stylistic, so the
plan is built around a checksum file as the primary input. Details and the
measurements behind each decision are below.
### What the feedback changed
| Draft | Now | Why |
| --- | --- | --- |
| primary input: `-D"sha512[classifier:extension]=..."` | primary input: a
checksum file | measured: the property name cannot be declared in a POM, cannot
be enumerated, and a mojo can only bind statically declared names, so "verify
these 7 files" is not expressible |
| `<checksums>` list in POM `<configuration>` | removed | the list would
have to be keyed by the same unsupportable names |
| attached artifacts identified by `classifier:extension` | identified by
published file name | that is the name that appears in every checksum file the
user can actually obtain, so no translation is needed |
| output shown as `sha512 matches <file>` | same, plus absolute path and the
offset of the first differing digit on mismatch | "precise diff ... alongside
the absolute file path" |
| reactor behaviour undecided | non-aggregator, per project, artifacts of
the current project | one invocation covers the whole reactor, and per-module
manifests work |
### Measurements that settle the interface
Run against Maven 3.9.9 with `-Dsha512[source-release:zip]=abc123`:
| Probe | Result |
| --- | --- |
| `<sha512[source-release:zip]>abc</sha512[source-release:zip]>` in POM
`<properties>` | `Non-parseable POM ... start tag unexpected character [` |
| same name as a plugin `<configuration>` element | parses, but is
unreachable: a parameter's property is only ever resolved from POM
`<properties>`, `-D`, system properties and `${session...}`, never from plugin
configuration, and a parameter name containing `[` cannot be declared |
| `${sha512[source-release:zip]}` expression lookup | resolves to `abc123` —
so expression resolution does un-escape |
| `project.getProperties()` | `<properties/>` — CLI `-D` values are *user*
properties and never reach `project.getProperties()` |
| `session.getUserProperties()` key | `sha512_.005bsource-release:zip_.005d`
— Maven escapes `[` to `_.005b` and `]` to `_.005d` |
So the name only survives if a mojo declares it as a `@Parameter` in
advance. That caps the properties interface at a fixed set of artifact
coordinates chosen when the plugin is compiled, gives users no way to add their
own, and prevents the goal from ever reporting what it did and did not cover.
Combined with the bracket-glob quoting hazard already noted, that is enough to
drop it rather than keep it as a secondary path. The only piece worth keeping
is the single simplest case, and a checksum file expresses that too.
### Interface
```
$ mvn artifact:verify-checksum
-Dverify.checksumFiles=target/maven-artifact-plugin-3.7.0-source-release.zip.sha512
```
or, when the checksum sidecars have been downloaded next to the artifacts
into one directory:
```
$ mvn artifact:verify-checksum -Dverify.checksumDirectory=/home/rm/downloads
```
No input at all is not an error: the goal reports that there is nothing to
verify and returns, so it can be left bound to a phase before the values are
known.
Not bound to a lifecycle phase by default, like `artifact:compare` and
`artifact:describe-build-output`; bindable to `verify` or `deploy` through
`<executions>` when a repeatable check is wanted.
### Checksum file formats accepted
All three are already in circulation, so all three are parsed. Blank lines
and `#` comments are ignored, files are read as UTF-8, and a trailing newline
is never significant.
1. **Bare sidecar, the ASF and Central form.** The file contains only the
digest. The artifact is taken from the file name, the algorithm from the
suffix. This is what `maven-artifact-plugin-3.7.0-source-release.zip.sha512` in
the dist area contains, and what `maven-artifact-plugin-3.7.0.jar.md5` on
Central contains:
```
aa97e7a9b11043e49c3a2f85e2e9e97fcc9732da0fdc2f9c6293be6e3c93c32d316872d6bed11e5fe3877cfcd019535adae7469c6bd29e8515c63d2663a23f1
```
2. **GNU coreutils output**, the format of `sha512sum` / `sha1sum` /
`md5sum` with either the text or binary marker, so a user can generate a
manifest with the tools they already have:
```
aa97e7a9...a23f1 maven-artifact-plugin-3.7.0-source-release.zip
f8c9a275...d9f39 *maven-artifact-plugin-3.7.0.jar
```
3. **POM-style `checksum:` prefix**, for anyone who wants to keep the list
in the POM, `<type>` mapping to the extension:
```
checksum:sha512 maven-artifact-plugin-3.7.0-source-release.zip
```
Algorithm resolution, in order: explicit `checksum:<algorithm>` prefix, else
the checksum file name suffix, else the digest length. The four lengths are
unambiguous — 32 `md5`, 40 `sha1`, 64 `sha256`, 128 `sha512` — so a `sha512sum`
manifest needs no extra information. When a suffix and a length are both
present and disagree, that is a hard error rather than a silent choice.
This format choice is also what makes the goal interoperate rather than
overlap: GNU coreutils format is what Resolver's own trusted-checksum sources
consume (see the scope note below), so a user can produce one file and use it
for both purposes.
### Deciding which file to verify
For every entry, the artifact file name from the manifest is looked up in a
map built in this order:
1. the artifacts of the **current project**: the main artifact, the project
file itself as `<artifactId>-<version>.pom`, the build/consumer pom where Maven
4 produces one, then `project.getAttachedArtifacts()`, keyed with the existing
`BuildInfoWriter.getArtifactFilename` helper so the names are identical to the
names `artifact:describe-build-output` prints;
2. the plain files inside `verify.checksumDirectory`, matched by file name.
Step 2 is what lets a release manager verify the copies downloaded from the
dist area, not only the local build output, without the goal needing to know
anything about remote repositories.
### Outcomes
| Situation | Log | Counted | Fails the build |
| --- | --- | --- | --- |
| digest matches | INFO `sha512 matches <file>` | `verified` | no |
| digest differs | ERROR with algorithm, file name, absolute path, expected,
actual, and the zero-based offset of the first differing character |
`mismatched` | yes, unless `verify.failOnMismatch=false` |
| manifest entry matches no artifact | WARN naming the entry and the
expected value | `unresolved` | yes, unless `verify.failOnUnresolved=false` |
| project artifact that no entry covers | WARN listing the uncovered file
names | `uncovered` | only if `verify.failOnUncovered=true` |
| malformed manifest line, or unusable input | ERROR | — | yes |
| no manifest given | INFO `no checksums to verify, skipping` | — | no |
Reporting the offset of the first differing character is a small addition
aimed straight at the original complaint: a single transposed digit out of 128
is otherwise effectively invisible, and the offset turns "these two strings
differ somewhere" into a pointer.
One summary line at the end, in the style `artifact:compare` already uses
for its `N files match, M differ` line:
```
[INFO] Checksum verification result: 7 verified, 0 mismatched, 0 unresolved,
3 uncovered
```
The three outcome counts are kept separate rather than collapsed, because
"unresolved" means the user made a mistake in the manifest while "uncovered"
means the manifest is simply incomplete, and the two deserve different
reactions.
### Parameters
| Parameter | Type | Default | Notes |
| --- | --- | --- | --- |
| `verify.checksumFiles` | `List<String>` | — | manifest files; each may
also name a directory, in which case it is treated as `checksumDirectory` |
| `verify.checksumDirectory` | `File` | — | searched for sidecars and for
the files to verify |
| `verify.failOnMismatch` | `boolean` | `true` | mirrors `compare.fail` |
| `verify.failOnUnresolved` | `boolean` | `true` | fail-fast on manifest
entries that match nothing |
| `verify.failOnUncovered` | `boolean` | `false` | opt in to requiring
complete coverage |
Property names follow the existing convention of the plugin's first goal
word (`buildinfo.*`, `check.*` from `check-buildplan`, `compare.*`).
`checksums.*` was the other candidate; `verify.*` was chosen for consistency,
and the names are specific enough not to collide with the generic `verify.*` of
the enforcer or failsafe plugins.
No `requiresDependencyResolution`: the goal only reads the current project's
own artifacts, so resolving the dependency graph would be wasted work and would
make the goal fail on an unresolvable graph for no reason. `threadSafe = true`,
`requiresProject = true`, not an aggregator, `@since 3.7.1`.
No `skip` parameter: `compare` and `check-buildplan` have none, and "no
manifest given" already covers the need to neutralise the goal.
### Scope relative to Maven Resolver trusted checksums
Worth recording explicitly, since the second comment raised it. The two are
complementary, not alternatives:
- `aether.artifactResolver.postProcessor.trustedChecksums` is an
`ArtifactResolverPostProcessor`. It only sees artifacts that go **through the
resolver**, that is things downloaded or read from a repository. The artifacts
the current build just produced are never resolved, so trusted checksums
structurally cannot check that a build output matches its published digest.
- This goal reads the **current build output** and compares it against a
digest published elsewhere. That is the direction of trust the issue is about,
and the plugin already draws this line: `artifact:compare` fetches the
*previous* release to compare against the *current* build.
- Resolver's own documentation is also careful that checksums "only provide
integrity verification. They do not provide security or trust", and points at
signatures for that. The goal is documented the same way; it answers "is this
the file that was published", not "is this file trustworthy".
Overlap is limited to the file format, and that is deliberate, as noted
above.
### Files added
| File | Purpose |
| --- | --- |
| `src/main/java/.../buildinfo/VerifyChecksumMojo.java` | the goal |
| `src/main/java/.../buildinfo/ChecksumManifest.java` | parses one checksum
file into `(fileName, algorithm, expected)` entries; format detection,
comments, the suffix/length conflict check |
| `src/test/java/.../buildinfo/ChecksumManifestTest.java` | the parser,
which is where all the fiddly logic lives |
| `src/test/java/.../buildinfo/VerifyChecksumMojoTest.java` | outcome matrix
against temp files and a hand-built `MavenProject` |
| `src/it/verify-checksum-manifest/` | end-to-end IT |
| `src/it/verify-checksum-uncovered/` | IT for the coverage warning and
`failOnUncovered` |
The parser is separated from the mojo so that the format handling is
unit-testable without a `MavenProject`, and so the format support can grow
without touching goal logic.
### Files changed
| File | Change |
| --- | --- |
| `src/site/site.xml` | one `<item name="artifact:verify-checksum"
href="verify-checksum-mojo.html"/>`, alphabetically last under Plugin
Documentation |
| `src/site/markdown/index.md` | new bullet; also fix the existing "has 4
goals currently", which lists five |
| `src/site/markdown/usage.md.vm` | a "Verifying published checksums"
section with a console transcript, matching the style of the existing
`artifact:compare` example |
`pom.xml` needs no change: `commons-codec` 1.22.1 and `commons-io` 2.22.0
are already compile-scoped, and `commons-codec` is what supplies the digests,
as it already does for `buildinfo` and `describe-build-output`.
### Tests
Unit, on `ChecksumManifest`: bare sidecar with each of the four suffixes;
`sha512sum` text mode and `*` binary mode; blank lines and comments; CRLF
input; digest and algorithm mismatch between suffix and length; a bare digest
with no file name inside a multi-line manifest; empty file; non-hex digest;
digest of an unsupported length; file that does not exist; directory passed
where a file was expected.
Unit, on `VerifyChecksumMojo`: one match; one mismatch; main artifact, POM,
build POM, an attached artifact, and a file found in `checksumDirectory`; an
entry that matches nothing; an artifact nothing covers; each of the three
`failOn*` toggles; no input at all; and that a wrong digest reports the correct
first-differing offset.
IT, `verify-checksum-manifest`: a project that attaches a
`source-release:zip` via `build-helper:attach-artifact`, and whose main jar and
POM are covered too. Step 1 runs the goal against a correct manifest and
expects success. Step 2 runs it against a manifest with one digit altered and
expects failure, with `invoker.buildResult.2=failure`. `verify.groovy` asserts
on the `build.log` lines: the summary line, the per-file match lines, and for
the failing run the absolute path and the expected/actual pair. Generating the
manifest from the build itself, via `checksum-plugin3` as the `apache-release`
profile does, keeps the "correct" step honest without hardcoding a digest in
the IT. The attached zip is created in-project rather than by depending on a
released version of this plugin, so the IT stays self-contained and does not
need a specific past release in the IT repository.
IT, `verify-checksum-uncovered`: a manifest covering only the main jar while
sources and javadoc are also produced, asserting the uncovered warning, success
under the default, and failure under `-Dverify.failOnUncovered=true`.
### Out of scope for this change
- **Downloading the published sidecar from a remote repository.** Tempting,
and `artifact:compare` already has the resolution machinery for it, but it
needs an artifact resolver and turns a pure local check into a networked one.
`verify.checksumDirectory` covers the same need for a release manager, who has
the file already. A natural follow-up.
- **Signature verification** (`.asc`). Out of scope by design, for the
reason quoted from Resolver's own documentation above: integrity is not
authenticity, and `maven-gpg-plugin` is the right tool for the latter.
- **A report file.** `buildinfo` and `compare` write `.buildinfo` and
`.buildcompare` because those are formats others consume. There is no consumer
for a checksum verification report, so the build log is enough; revisit if one
appears.
- **Recording checksums.** Resolver's `trustedChecksums.record` already does
this well for the incoming direction, and `checksum-plugin3` already does it
for the ASF release artifacts in the `apache-release` profile.
### Remaining questions
1. Should the default when no input is given be "scan for sidecars next to
the artifacts in `target/`"? Measured: there are none to find in a normal Maven
3 build, because Resolver does not download Central's `.sha1`/`.md5` sidecars
into the local repository. It would be a no-op default, so the plan is to
report and return instead. Worth confirming that a no-op is preferable to
looking like it did something.
2. Multi-module: the plan is a plain non-aggregator goal, so `mvn
artifact:verify-checksum -Dverify.checksumFiles=...` from the root verifies
every module, and each module picks up its own `<checksums>` configuration if
given. The alternative is an aggregator goal reading one manifest for the whole
reactor, which centralises the file but moves per-module configuration out of
the module POMs. The non-aggregator form follows `artifact:check-buildplan`;
the aggregator form follows `artifact:describe-build-output`. Preference is for
the non-aggregator form, since the release artifacts of a multi-module project
are mostly per-module and belong next to the module that produced them.
3. Anything here that should go to `src/site/markdown/faq.md` rather than
`usage.md.vm`?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]