Skip to content

Commit ec8f576

Browse files
authored
GH-699: hash/digest verification of URL dependencies (#701)
This change introduces a new set of options that can be specified when referencing protoc plugins by URL that allow users to specify the expected digest of the resource to be downloaded. If the downloaded resource does not match the digest, then the plugins are not executed, and the build will fail with an error. The aim is to allow users to verify that their dependencies have not been tampered with prior to running anything. This is already performed internally by Maven on Maven-based dependencies. Digests will be able to be specified in the format `md5:09f7e02f1290be211da707a266f153b3`, `sha256:66a045b452102c59d840ec097d59d9467e13a3f34f6494e539ffd32c1bb35f18`, etc for any supported JVM `MessageDigest` (this is usually a small set including MD5, SHA-1, SHA-256, and SHA-512). Users should consult the documentation for their Java version to see which MessageDigest format are supported for their platform. Users may _in theory_ be able to extend this by adding bouncy castle to the classpath, although this will not be tested nor verified in this PR. --- ## TODO list - [x] Implement Digest class for parsing, holding, comparing digests - [x] Unit test Digest class - [x] Implement Plexus DigestConverter to allow parsing digests to Digest objects in pom.xml configuration blocks - [x] Unit test DigestConverter class - [x] Remove all references of `*.utils.Digests`, and replace with this new `Digest` class - [x] Add optional digest attribute to URL plugins - [x] Validate digest of URL resource if the digest is provided in the configuration - [x] Add optional digest for protoc executable if specified as a URL. - [x] Report user-friendly errors if digests do not match - [x] Implement new integration tests - [x] Minor version bump - [x] Update documentation for MOJO goals - [x] Update user guide markdown documentation Closes GH-699.
1 parent 48dabf4 commit ec8f576

28 files changed

Lines changed: 1145 additions & 229 deletions

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
<groupId>io.github.ascopes</groupId>
2323
<artifactId>protobuf-maven-plugin-parent</artifactId>
24-
<version>3.4.3-SNAPSHOT</version>
24+
<version>3.5.0-SNAPSHOT</version>
2525

2626
<name>Protobuf Maven Plugin Parent</name>
2727
<description>Parent POM for the Protobuf Maven Plugin.</description>

protobuf-maven-plugin/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<parent>
2323
<groupId>io.github.ascopes</groupId>
2424
<artifactId>protobuf-maven-plugin-parent</artifactId>
25-
<version>3.4.3-SNAPSHOT</version>
25+
<version>3.5.0-SNAPSHOT</version>
2626
</parent>
2727

2828
<artifactId>protobuf-maven-plugin</artifactId>

protobuf-maven-plugin/src/it/http-url-grpc-plugin/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
<binaryUrlPlugins>
9090
<binaryUrlPlugin>
9191
<url>https://repo1.maven.org/maven2/io/grpc/protoc-gen-grpc-java/${grpc.version}/protoc-gen-grpc-java-${grpc.version}-linux-x86_64.exe</url>
92+
<digest>sha256:a9f9a7987be4a37c69a85e5e8885394356fd2f1a47f843c6461da4fc99f407b3</digest>
9293
<!-- See https://github.com/grpc/grpc-java/issues/9179 -->
9394
<options>@generated=omit</options>
9495
</binaryUrlPlugin>

protobuf-maven-plugin/src/it/http-url-protoc/pom.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
<artifactId>@project.artifactId@</artifactId>
6868

6969
<configuration>
70-
<protocVersion>https://repo1.maven.org/maven2/com/google/protobuf/protoc/${protobuf.version}/protoc-${protobuf.version}-linux-x86_64.exe</protocVersion>
70+
<protoc>https://repo1.maven.org/maven2/com/google/protobuf/protoc/${protobuf.version}/protoc-${protobuf.version}-linux-x86_64.exe</protoc>
71+
<protocDigest>md5:6c224d84618c71e2ebb46dd9c4459aa6</protocDigest>
7172
</configuration>
7273

7374
<executions>

protobuf-maven-plugin/src/it/scalapb-plugin/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
<binaryUrlPlugins>
7676
<binaryUrlPlugin>
7777
<url>zip:https://github.com/scalapb/ScalaPB/releases/download/v${scalapb.version}/protoc-gen-scala-${scalapb.version}-linux-x86_64.zip!/protoc-gen-scala</url>
78+
<digest>sha1:bca1d071820bab1ebb0ddd058e6fb621aca321c5</digest>
7879
<options>flat_package,grpc,scala3_sources</options>
7980
</binaryUrlPlugin>
8081
</binaryUrlPlugins>

protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/fs/UriResourceFetcher.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package io.github.ascopes.protobufmavenplugin.fs;
1717

18-
import io.github.ascopes.protobufmavenplugin.utils.Digests;
18+
import io.github.ascopes.protobufmavenplugin.utils.Digest;
1919
import io.github.ascopes.protobufmavenplugin.utils.ResolutionException;
2020
import java.io.BufferedInputStream;
2121
import java.io.BufferedOutputStream;
@@ -175,7 +175,7 @@ private Optional<Path> handleOtherUri(URI uri, String extension) throws Resoluti
175175
}
176176

177177
private Path targetFile(URL url, String extension) {
178-
var digest = Digests.sha1(url.toExternalForm());
178+
var digest = Digest.compute("SHA-1", url.toExternalForm()).toHexString();
179179
var path = url.getPath();
180180
var lastSlash = path.lastIndexOf('/');
181181
var fileName = lastSlash < 0

protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/generation/GenerationRequest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.github.ascopes.protobufmavenplugin.plugins.MavenProtocPlugin;
2121
import io.github.ascopes.protobufmavenplugin.plugins.PathProtocPlugin;
2222
import io.github.ascopes.protobufmavenplugin.plugins.UriProtocPlugin;
23+
import io.github.ascopes.protobufmavenplugin.utils.Digest;
2324
import java.nio.file.Path;
2425
import java.util.Collection;
2526
import java.util.List;
@@ -158,6 +159,17 @@ public interface GenerationRequest {
158159
*/
159160
@Nullable String getOutputDescriptorAttachmentClassifier();
160161

162+
/**
163+
* The digest of the {@code protoc} binary to verify, or {@code null} if
164+
* no verification should take place.
165+
*
166+
* <p>This does not affect any verification performed by Aether.
167+
*
168+
* @return the digest.
169+
* @since 3.5.0
170+
*/
171+
@Nullable Digest getProtocDigest();
172+
161173
/**
162174
* The version of {@code protoc} to use.
163175
*

protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/generation/ProtobufBuildOrchestrator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ private GenerationResult handleMissingTargets(GenerationRequest request) {
203203
}
204204

205205
private Path discoverProtocPath(GenerationRequest request) throws ResolutionException {
206-
return protocResolver.resolve(request.getProtocVersion())
206+
return protocResolver.resolve(request.getProtocVersion(), request.getProtocDigest())
207207
.orElseThrow(() -> new ResolutionException("Protoc binary was not found"));
208208
}
209209

protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojo.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.github.ascopes.protobufmavenplugin.plugins.MavenProtocPluginBean;
3131
import io.github.ascopes.protobufmavenplugin.plugins.PathProtocPluginBean;
3232
import io.github.ascopes.protobufmavenplugin.plugins.UriProtocPluginBean;
33+
import io.github.ascopes.protobufmavenplugin.utils.Digest;
3334
import java.nio.file.Files;
3435
import java.nio.file.Path;
3536
import java.util.Collection;
@@ -60,11 +61,13 @@ public abstract class AbstractGenerateMojo extends AbstractMojo {
6061
private static final String DEFAULT_TRUE = "true";
6162
private static final String DEFAULT_TRANSITIVE = "TRANSITIVE";
6263

64+
private static final String PROTOBUF_COMPILER_DIGEST = "protobuf.compiler.digest";
6365
private static final String PROTOBUF_COMPILER_EXCLUDES = "protobuf.compiler.excludes";
6466
private static final String PROTOBUF_COMPILER_INCLUDES = "protobuf.compiler.includes";
6567
private static final String PROTOBUF_COMPILER_INCREMENTAL = "protobuf.compiler.incremental";
6668
private static final String PROTOBUF_COMPILER_VERSION = "protobuf.compiler.version";
6769
private static final String PROTOBUF_SKIP = "protobuf.skip";
70+
private static final String PROTOC_ALIAS = "protoc";
6871

6972
private static final Logger log = LoggerFactory.getLogger(AbstractGenerateMojo.class);
7073

@@ -264,6 +267,10 @@ public AbstractGenerateMojo() {
264267
* no effect, and the project-wide setting is used. If explicitly
265268
* specified, then the project setting is ignored in favour of this
266269
* value instead.</li>
270+
* <li>{@code digest} - an optional digest to verify the binary against.
271+
* If specified, this is a string in the format {@code sha512:1a2b3c4d...},
272+
* using any supported message digest provided by your JDK (e.g. {@code md5},
273+
* {@code sha1}, {@code sha256}, {@code sha512}, etc).</li>
267274
* </ul>
268275
*
269276
* @since 2.0.0
@@ -731,6 +738,22 @@ public AbstractGenerateMojo() {
731738
@Parameter(defaultValue = DEFAULT_FALSE)
732739
boolean phpEnabled;
733740

741+
/**
742+
* Optional digest to verify {@code protoc} against.
743+
*
744+
* <p>Generally, you will not need to provide this, as the Maven Central
745+
* {@code protoc} binaries will already be digest-verified as part of distribution.
746+
* You may wish to specify this if you are using a {@code PATH}-based binary, or
747+
* using a URL for {@code protoc}.
748+
*
749+
* <p>This is a string in the format {@code sha512:1a2b3c...}, using any
750+
* message digest algorithm supported by your JDK.
751+
*
752+
* @since 3.5.0
753+
*/
754+
@Parameter(property = PROTOBUF_COMPILER_DIGEST)
755+
@Nullable Digest protocDigest;
756+
734757
/**
735758
* Specifies where to find {@code protoc} or which version to download.
736759
*
@@ -768,7 +791,11 @@ public AbstractGenerateMojo() {
768791
*
769792
* @since 0.0.1
770793
*/
771-
@Parameter(required = true, property = PROTOBUF_COMPILER_VERSION)
794+
@Parameter(
795+
alias = PROTOC_ALIAS,
796+
required = true,
797+
property = PROTOBUF_COMPILER_VERSION
798+
)
772799
String protocVersion;
773800

774801
/**
@@ -912,7 +939,7 @@ public AbstractGenerateMojo() {
912939
* Override the source directories to compile from.
913940
*
914941
* <p>Leave unspecified or explicitly null/empty to use the defaults.
915-
*
942+
*
916943
* <p><strong>Note that specifying custom directories will override the default
917944
* directories rather than adding to them.</strong>
918945
*
@@ -1037,6 +1064,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
10371064
.outputDescriptorIncludeSourceInfo(outputDescriptorIncludeSourceInfo)
10381065
.outputDescriptorRetainOptions(outputDescriptorRetainOptions)
10391066
.outputDirectory(outputDirectory())
1067+
.protocDigest(protocDigest)
10401068
.protocVersion(protocVersion())
10411069
.registerAsCompilationRoot(registerAsCompilationRoot)
10421070
.sourceDependencies(nonNullList(sourceDependencies))
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (C) 2023 - 2025, Ashley Scopes.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.ascopes.protobufmavenplugin.mojo.plexus;
17+
18+
import io.github.ascopes.protobufmavenplugin.utils.Digest;
19+
import java.util.Collections;
20+
import java.util.Locale;
21+
import java.util.Map;
22+
import java.util.TreeMap;
23+
import java.util.regex.Pattern;
24+
import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
25+
import org.codehaus.plexus.component.configurator.converters.basic.AbstractBasicConverter;
26+
27+
/**
28+
* Converter for {@link Digest}s.
29+
*
30+
* @author Ashley Scopes
31+
* @since 3.5.0
32+
*/
33+
final class DigestConverter extends AbstractBasicConverter {
34+
35+
private static final Pattern PATTERN = Pattern.compile(
36+
"^(?<algorithm>[-a-z0-9]+):(?<digest>[0-9a-f]+)$",
37+
Pattern.CASE_INSENSITIVE
38+
);
39+
40+
private static final Map<String, String> DIGEST_ALIASES;
41+
42+
static {
43+
var digestAliases = new TreeMap<String, String>(String::compareToIgnoreCase);
44+
digestAliases.put("sha1", "SHA-1");
45+
digestAliases.put("sha224", "SHA-224");
46+
digestAliases.put("sha256", "SHA-256");
47+
digestAliases.put("sha384", "SHA-384");
48+
digestAliases.put("sha512", "SHA-512");
49+
DIGEST_ALIASES = Collections.unmodifiableMap(digestAliases);
50+
}
51+
52+
@Override
53+
public boolean canConvert(Class<?> type) {
54+
return Digest.class.equals(type);
55+
}
56+
57+
@Override
58+
protected Object fromString(String str) throws ComponentConfigurationException {
59+
// Users may wish to split digests into more than one line
60+
// so they can satisfy tools like spotless. Support this by
61+
// yanking any whitespace prior to matching the string.
62+
str = removeWhitespace(str);
63+
var matcher = PATTERN.matcher(str);
64+
65+
if (!matcher.matches()) {
66+
throw new ComponentConfigurationException(
67+
"Failed to parse digest '" + str + "'. "
68+
+ "Ensure that the digest is in a format such as "
69+
+ "'sha512:1a2b3c4d', where the digest is a hexadecimal-encoded "
70+
+ "string."
71+
);
72+
}
73+
74+
try {
75+
var algorithm = matcher.group("algorithm");
76+
algorithm = DIGEST_ALIASES.getOrDefault(algorithm, algorithm)
77+
.toUpperCase(Locale.ROOT);
78+
79+
var digest = matcher.group("digest")
80+
.toLowerCase(Locale.ROOT);
81+
82+
return Digest.from(algorithm, digest);
83+
} catch (Exception ex) {
84+
throw new ComponentConfigurationException(
85+
"Failed to parse digest '" + str + "': " + ex,
86+
ex
87+
);
88+
}
89+
}
90+
91+
private static String removeWhitespace(String string) {
92+
var sb = new StringBuilder();
93+
for (var i = 0; i < string.length(); ++i) {
94+
var c = string.charAt(i);
95+
if (!Character.isWhitespace(c)) {
96+
sb.append(c);
97+
}
98+
}
99+
return sb.toString();
100+
}
101+
}

0 commit comments

Comments
 (0)