From a045a117122826a4e771e1404bd0c4b997bdae84 Mon Sep 17 00:00:00 2001 From: schedin Date: Sat, 23 Dec 2023 17:45:55 +0100 Subject: [PATCH] [MJARSIGNER-74] Allow usage of multiple Time Stamping Authority (TSA) servers Using multiple TSA URLs to try if first fail Adding support for tsapolicyid and tsadigestalg --- .../jarsigner/AbstractJarsignerMojo.java | 8 +- .../plugins/jarsigner/JarsignerSignMojo.java | 130 ++++++++++- .../maven/plugins/jarsigner/TsaSelector.java | 140 ++++++++++++ src/main/resources/jarsigner.properties | 4 + .../jarsigner/JarsignerSignMojoTsaTest.java | 208 ++++++++++++++++++ .../plugins/jarsigner/MojoTestCreator.java | 9 +- .../plugins/jarsigner/PluginXmlParser.java | 8 + .../plugins/jarsigner/RequestMatchers.java | 10 + .../plugins/jarsigner/TsaSelectorTest.java | 177 +++++++++++++++ 9 files changed, 680 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/apache/maven/plugins/jarsigner/TsaSelector.java create mode 100644 src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoTsaTest.java create mode 100644 src/test/java/org/apache/maven/plugins/jarsigner/TsaSelectorTest.java diff --git a/src/main/java/org/apache/maven/plugins/jarsigner/AbstractJarsignerMojo.java b/src/main/java/org/apache/maven/plugins/jarsigner/AbstractJarsignerMojo.java index 30b9b7a..db583e9 100644 --- a/src/main/java/org/apache/maven/plugins/jarsigner/AbstractJarsignerMojo.java +++ b/src/main/java/org/apache/maven/plugins/jarsigner/AbstractJarsignerMojo.java @@ -155,10 +155,10 @@ public abstract class AbstractJarsignerMojo extends AbstractMojo { *
      * {@code
      * 
-     *     
-     *         -signedjar
-     *         my-project_signed.jar
-     *     
+     *   
+     *     -signedjar
+     *     my-project_signed.jar
+     *   
      * 
      * }
*/ diff --git a/src/main/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojo.java b/src/main/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojo.java index aa35596..6434c6f 100644 --- a/src/main/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojo.java +++ b/src/main/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojo.java @@ -33,6 +33,7 @@ import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.jarsigner.TsaSelector.TsaServer; import org.apache.maven.shared.jarsigner.JarSigner; import org.apache.maven.shared.jarsigner.JarSignerRequest; import org.apache.maven.shared.jarsigner.JarSignerSignRequest; @@ -73,20 +74,107 @@ public class JarsignerSignMojo extends AbstractJarsignerMojo { private boolean removeExistingSignatures; /** + *

URL(s) to Time Stamping Authority (TSA) server(s) to use to timestamp the signing. * See options. + * Separate multiple TSA URLs with comma (without space) or a nested XML tag.

+ * + *
{@code
+     * 
+     *   http://timestamp.digicert.com,http://timestamp.globalsign.com/tsa/r6advanced1
+     * 
+     * }
+ * + *
{@code
+     * 
+     *   
+     *     http://timestamp.digicert.com
+     *     http://timestamp.globalsign.com/tsa/r6advanced1
+     *   
+     * 
+     * }
+ * + *

Usage of multiple TSA servers only makes sense when {@link #maxTries} is more than 1. A different TSA server + * will only be used at retries.

+ * + *

Changed to a list since 3.1.0. Single XML element (without comma) is still supported.

* * @since 1.3 */ @Parameter(property = "jarsigner.tsa") - private String tsa; + private String[] tsa; /** - * See options. + *

Alias(es) for certificate(s) in the active keystore used to find a TSA URL. From the certificate the X509v3 + * extension "Subject Information Access" field is examined to find the TSA server URL. See + * options. + * Separate multiple aliases with comma (without space) or a nested XML tag.

+ * + *
{@code
+     * 
+     *   alias1,alias2
+     * 
+     * }
+ * + *
{@code
+     * 
+     *   
+     *     alias1
+     *     alias2
+     *   
+     * 
+     * }
+ * + *

Should not be used at the same time as the {@link #tsa} parameter (because jarsigner will typically ignore + * tsacert, if tsa is set).

+ * + *

Usage of multiple aliases only makes sense when {@link #maxTries} is more than 1. A different TSA server + * will only be used at retries.

+ * + *

Changed to a list since 3.1.0. Single XML element (without comma) is still supported.

* * @since 1.3 */ @Parameter(property = "jarsigner.tsacert") - private String tsacert; + private String[] tsacert; + + /** + *

OID(s) to send to the TSA server to identify the policy ID the server should use. If not specified TSA server + * will choose a default policy ID. Each TSA server vendor will typically define their own policy OIDs. See + * options. + * Separate multiple OIDs with comma (without space) or a nested XML tag.

+ * + *
{@code
+     * 
+     *   1.3.6.1.4.1.4146.2.3.1.2,2.16.840.1.114412.7.1
+     * 
+     * }
+ * + *
{@code
+     * 
+     *   
+     *     1.3.6.1.4.1.4146.2.3.1.2
+     *     2.16.840.1.114412.7.1
+     *   
+     * 
+     * }
+ * + *

If used, the number of OIDs should be the same as the number of elements in {@link #tsa} or {@link #tsacert}. + * The first OID will be used for the first TSA server, the second OID for the second TSA server and so on.

+ * + * @since 3.1.0 + */ + @Parameter(property = "jarsigner.tsapolicyid") + private String[] tsapolicyid; + + /** + * The message digest algorithm to use in the messageImprint that the TSA server will timestamp. A default value + * (for example {@code SHA-384}) will be selected by jarsigner if this parameter is not set. Only available in + * Java 11 and later. See options. + * + * @since 3.1.0 + */ + @Parameter(property = "jarsigner.tsadigestalg") + private String tsadigestalg; /** * Location of the extra certificate chain file. See @@ -132,6 +220,8 @@ public class JarsignerSignMojo extends AbstractJarsignerMojo { /** Current WaitStrategy, to allow for sleeping after a signing failure. */ private WaitStrategy waitStrategy = this::defaultWaitStrategy; + private TsaSelector tsaSelector; + /** Exponent limit for exponential wait after failure function. 2^20 = 1048576 sec ~= 12 days. */ private static final int MAX_WAIT_EXPONENT_ATTEMPT = 20; @@ -175,6 +265,20 @@ protected void validateParameters() throws MojoExecutionException { getLog().warn(getMessage("invalidThreadCount", threadCount)); threadCount = 1; } + + if (tsa.length > 0 && tsacert.length > 0) { + getLog().warn(getMessage("warnUsageTsaAndTsacertSimultaneous")); + } + if (tsapolicyid.length > tsa.length || tsapolicyid.length > tsacert.length) { + getLog().warn(getMessage("warnUsageTsapolicyidTooMany", tsapolicyid.length, tsa.length, tsacert.length)); + } + if (tsa.length > 1 && maxTries == 1) { + getLog().warn(getMessage("warnUsageMultiTsaWithoutRetry", tsa.length)); + } + if (tsacert.length > 1 && maxTries == 1) { + getLog().warn(getMessage("warnUsageMultiTsacertWithoutRetry", tsacert.length)); + } + tsaSelector = new TsaSelector(tsa, tsacert, tsapolicyid, tsadigestalg); } /** @@ -184,8 +288,7 @@ protected void validateParameters() throws MojoExecutionException { protected JarSignerRequest createRequest(File archive) throws MojoExecutionException { JarSignerSignRequest request = new JarSignerSignRequest(); request.setSigfile(sigfile); - request.setTsaLocation(tsa); - request.setTsaAlias(tsacert); + updateJarSignerRequestWithTsa(request, tsaSelector.getServer()); request.setCertchain(certchain); // Special handling for passwords through the Maven Security Dispatcher @@ -193,6 +296,14 @@ protected JarSignerRequest createRequest(File archive) throws MojoExecutionExcep return request; } + /** Modifies JarSignerRequest with TSA parameters */ + private void updateJarSignerRequestWithTsa(JarSignerSignRequest request, TsaServer tsaServer) { + request.setTsaLocation(tsaServer.getTsaUrl()); + request.setTsaAlias(tsaServer.getTsaAlias()); + request.setTsapolicyid(tsaServer.getTsaPolicyId()); + request.setTsadigestalg(tsaServer.getTsaDigestAlt()); + } + /** * {@inheritDoc} Processing of files may be parallelized for increased performance. */ @@ -202,7 +313,7 @@ protected void processArchives(List archives) throws MojoExecutionExceptio List> futures = archives.stream() .map(file -> executor.submit((Callable) () -> { processArchive(file); - return null; + return null; // Return dummy value to conform with Void type })) .collect(Collectors.toList()); try { @@ -236,15 +347,18 @@ protected void executeJarSigner(JarSigner jarSigner, JarSignerRequest request) for (int attempt = 0; attempt < maxTries; attempt++) { JavaToolResult result = jarSigner.execute(request); int resultCode = result.getExitCode(); - Commandline commandLine = result.getCommandline(); if (resultCode == 0) { return; } + tsaSelector.registerFailure(); // Could be TSA server problem or something unrelated to TSA + if (attempt < maxTries - 1) { // If not last attempt waitStrategy.waitAfterFailure(attempt, Duration.ofSeconds(maxRetryDelaySeconds)); + updateJarSignerRequestWithTsa((JarSignerSignRequest) request, tsaSelector.getServer()); } else { // Last attempt failed, use this failure as resulting failure - throw new MojoExecutionException(getMessage("failure", getCommandlineInfo(commandLine), resultCode)); + throw new MojoExecutionException( + getMessage("failure", getCommandlineInfo(result.getCommandline()), resultCode)); } } } diff --git a/src/main/java/org/apache/maven/plugins/jarsigner/TsaSelector.java b/src/main/java/org/apache/maven/plugins/jarsigner/TsaSelector.java new file mode 100644 index 0000000..d330fab --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/jarsigner/TsaSelector.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.jarsigner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Helper class to select a Time Stamping Authority (TSA) server along with parameters to send. The protocol is defined + * in RFC 3161: Internet X.509 Public Key Infrastructure Time-Stamp Protocol (TSP). + * + * From a jarsigner perspective there are two things that are important: + * 1. Finding a TSA server URL + * 2. What parameters to use for TSA server communication. + * + * Finding a URL can be done in two ways: + * a) The end-user has specified an explicit URL (the most common way) + * b) The end-user has specified a keystore alias that points to a certificate in the active keystore. From the + * certificate the X509v3 extension "Subject Information Access" field is examined to find the TSA server URL. + * Example: + *
+ *    [vagrant@podmanhost ~]$ openssl x509 -noout -ext subjectInfoAccess -in tsa-server.crt
+ *    Subject Information Access:
+ *        AD Time Stamping - URI:http://timestamp.globalsign.com/tsa/r6advanced1
+ *    
+ * + * Each TSA server vendor typically has defined its own OID for what "policy" to use in the timestamping process. For + * example GlobalSign might use 1.3.6.1.4.1.4146.2.3.1.2. A DigiCert TSA server would not accept this OID. In most cases + * there is no need for the end-user to specify this because the TSA server will choose a default. + * + * jarsigner will send a message digest to the TSA server along with the message digest algorithm. For example + * {@code SHA-384}. A TSA server might reject the chosen algorithm, but typically most TSA servers supports the "common" + * ones (like SHA-256, SHA-384 and SHA-512). In most cases there is no need for the end-user to specify this because the + * jarsigner tool choose a good default. + */ +class TsaSelector { + + /** The current TsaServer in use (if any). One per thread */ + private final ThreadLocal currentTsaServer = new ThreadLocal<>(); + + /** List of TSA servers. Will at minimum contain a dummy/empty value */ + private final List tsaServers; + + TsaSelector(String[] tsa, String[] tsacert, String[] tsapolicyid, String tsadigestalg) { + List tsaServersTmp = new ArrayList<>(); + + for (int i = 0; i < Math.max(tsa.length, tsacert.length); i++) { + String tsaUrl = i < tsa.length ? tsa[i] : null; + String tsaAlias = i < tsacert.length ? tsacert[i] : null; + String tsaPolicyId = i < tsapolicyid.length ? tsapolicyid[i] : null; + tsaServersTmp.add(new TsaServer(tsaUrl, tsaAlias, tsaPolicyId, tsadigestalg)); + } + + if (tsaServersTmp.isEmpty()) { + tsaServersTmp.add(TsaServer.EMPTY); + } + this.tsaServers = Collections.unmodifiableList(tsaServersTmp); + } + + /** + * Gets the next "best" TSA server to use. + * + * Uses a "best effort" approach without any synchronization. It may not select the "snapshot-consistent" best TSA + * server, but good enough. + */ + TsaServer getServer() { + TsaServer best = tsaServers.get(0); + for (int i = 1; i < tsaServers.size(); i++) { + if (best.failureCount.get() > tsaServers.get(i).failureCount.get()) { + best = tsaServers.get(i); + } + } + currentTsaServer.set(best); + return best; + } + + /** + * Register that the current used TsaServer was involved in a jarsigner execution that failed. This could be a + * problem with the TsaServer, but it could also be other factors unrelated to the TsaServer. Regardless of the + * cause of the failure it is registered as a failure for the current used TsaServer to be used when determining the + * next TsaServer to try. + */ + void registerFailure() { + if (currentTsaServer.get() != null) { + currentTsaServer.get().failureCount.incrementAndGet(); + } + } + + /** Representation of a single TSA server and the parameters to use for it */ + static class TsaServer { + private static final TsaServer EMPTY = new TsaServer(null, null, null, null); + + private final AtomicInteger failureCount = new AtomicInteger(0); + private final String tsaUrl; + private final String tsaAlias; + private final String tsaPolicyId; + private final String tsaDigestAlt; + + private TsaServer(String tsaUrl, String tsaAlias, String tsaPolicyId, String tsaDigestAlt) { + this.tsaUrl = tsaUrl; + this.tsaAlias = tsaAlias; + this.tsaPolicyId = tsaPolicyId; + this.tsaDigestAlt = tsaDigestAlt; + } + + String getTsaUrl() { + return tsaUrl; + } + + String getTsaAlias() { + return tsaAlias; + } + + String getTsaPolicyId() { + return tsaPolicyId; + } + + String getTsaDigestAlt() { + return tsaDigestAlt; + } + } +} diff --git a/src/main/resources/jarsigner.properties b/src/main/resources/jarsigner.properties index 7146b16..2b9e00d 100644 --- a/src/main/resources/jarsigner.properties +++ b/src/main/resources/jarsigner.properties @@ -27,3 +27,7 @@ archiveNotSigned = Archive ''{0}'' is not signed invalidMaxTries = Invalid maxTries value. Was ''{0}'' but should be >= 1 invalidMaxRetryDelaySeconds = Invalid maxRetryDelaySeconds value. Was ''{0}'' but should be >= 0 invalidThreadCount = Invalid threadCount value. Was ''{0}'' but should be >= 1 +warnUsageTsaAndTsacertSimultaneous = Usage of both -tsa and -tsacert is undefined +warnUsageTsapolicyidTooMany = Too many ({0}) number of OIDs given, but only {1} and {2} TSA URL and TSA certificate alias, respectively +warnUsageMultiTsaWithoutRetry = {0} TSA URLs specified. Only first will be used because maxTries is set to 1 +warnUsageMultiTsacertWithoutRetry = {0} TSA certificate aliases specified. Only first will be used because maxTries is set to 1 diff --git a/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoTsaTest.java b/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoTsaTest.java new file mode 100644 index 0000000..80a0ced --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/jarsigner/JarsignerSignMojoTsaTest.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.jarsigner; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; +import org.apache.maven.shared.jarsigner.JarSigner; +import org.apache.maven.shared.jarsigner.JarSignerSignRequest; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.ArgumentCaptor; + +import static org.apache.maven.plugins.jarsigner.TestJavaToolResults.RESULT_ERROR; +import static org.apache.maven.plugins.jarsigner.TestJavaToolResults.RESULT_OK; +import static org.hamcrest.CoreMatchers.everyItem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class JarsignerSignMojoTsaTest { + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + private Locale originalLocale; + private MavenProject project = mock(MavenProject.class); + private JarSigner jarSigner = mock(JarSigner.class); + + private File projectDir; + private Map configuration = new LinkedHashMap<>(); + private Log log; + private MojoTestCreator mojoTestCreator; + + @Before + public void setUp() throws Exception { + originalLocale = Locale.getDefault(); + Locale.setDefault(Locale.ENGLISH); // For English ResourceBundle to test log messages + projectDir = folder.newFolder("dummy-project"); + mojoTestCreator = + new MojoTestCreator(JarsignerSignMojo.class, project, projectDir, jarSigner); + log = mock(Log.class); + mojoTestCreator.setLog(log); + Artifact mainArtifact = TestArtifacts.createJarArtifact(projectDir, "my-project.jar"); + when(project.getArtifact()).thenReturn(mainArtifact); + } + + @After + public void tearDown() { + Locale.setDefault(originalLocale); + } + + @Test + public void testAllTsaParameters() throws Exception { + when(jarSigner.execute(any(JarSignerSignRequest.class))).thenReturn(RESULT_OK); + configuration.put("archiveDirectory", createArchives(2).getPath()); + configuration.put("tsa", "http://my-timestamp.server.com"); + configuration.put("tsacert", "mytsacertalias"); // Normally you would not set both "tsacert alias" and "tsa url" + configuration.put("tsapolicyid", "0.1.2.3.4"); + configuration.put("tsadigestalg", "SHA-384"); + + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + mojo.execute(); + + ArgumentCaptor requestArgument = ArgumentCaptor.forClass(JarSignerSignRequest.class); + verify(jarSigner, times(3)).execute(requestArgument.capture()); + List requests = requestArgument.getAllValues(); + assertThat(requests, everyItem(RequestMatchers.hasTsa("http://my-timestamp.server.com"))); + assertThat(requests, everyItem(RequestMatchers.hasTsacert("mytsacertalias"))); + assertThat(requests, everyItem(RequestMatchers.hasTsaPolicyid("0.1.2.3.4"))); + assertThat(requests, everyItem(RequestMatchers.hasTsaDigestalg("SHA-384"))); + } + + @Test + public void testMutipleTsa() throws Exception { + // Special setup (non-normal) Mockito, because the same JarSignerSignRequest instance is used with modified URL + List tsaUrls = new ArrayList<>(); + when(jarSigner.execute(any(JarSignerSignRequest.class))) + .thenAnswer(invocation -> { + JarSignerSignRequest request = + (JarSignerSignRequest) invocation.getArguments()[0]; + tsaUrls.add(request.getTsaLocation()); + return RESULT_ERROR; + }) + .thenAnswer(invocation -> { + JarSignerSignRequest request = + (JarSignerSignRequest) invocation.getArguments()[0]; + tsaUrls.add(request.getTsaLocation()); + return RESULT_OK; + }); + + configuration.put("maxTries", "10"); + configuration.put("tsa", "http://my-timestamp.server.com,http://other-timestamp.example.com"); + + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + mojo.execute(); + + verify(jarSigner, times(2)).execute(any()); + assertEquals("http://my-timestamp.server.com", tsaUrls.get(0)); + assertEquals("http://other-timestamp.example.com", tsaUrls.get(1)); + } + + @Test + public void testVerifyUsageOfBothTsaAndTsacert() throws Exception { + when(jarSigner.execute(any(JarSignerSignRequest.class))).thenReturn(RESULT_OK); + configuration.put("maxTries", "2"); + configuration.put("tsa", "http://my-timestamp.server.com"); + configuration.put("tsacert", "mytsacertalias"); + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + mojo.execute(); + + verify(log).warn(contains("Usage of both -tsa and -tsacert is undefined")); + } + + @Test + public void testVerifyUsageOfDifferentNumberOfTsapolicyidAndTsa() throws Exception { + when(jarSigner.execute(any(JarSignerSignRequest.class))).thenReturn(RESULT_OK); + configuration.put("maxTries", "2"); + configuration.put("tsa", "http://my-timestamp1.server.com,http://my-timestamp2.server.com"); + configuration.put("tsapolicyid", "1.1,1.2,1.3"); // Too many OIDs specified + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + mojo.execute(); + + verify(log).warn(contains("Too many (3) number of OIDs given")); + } + + @Test + public void testVerifyUsageOfDifferentNumberOfTsapolicyidAndTsacert() throws Exception { + when(jarSigner.execute(any(JarSignerSignRequest.class))).thenReturn(RESULT_OK); + configuration.put("maxTries", "2"); + configuration.put("tsacert", "alias1,alias2"); + configuration.put("tsapolicyid", "1.1,1.2,1.3"); // Too many OIDs specified + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + mojo.execute(); + + // Alomst the same warning log as previous test case + verify(log).warn(contains("Too many (3) number of OIDs given")); + } + + @Test + public void testVerifyMultipleTsaButNoRetry() throws Exception { + when(jarSigner.execute(any(JarSignerSignRequest.class))).thenReturn(RESULT_OK); + configuration.put("tsa", "http://my-timestamp1.server.com,http://my-timestamp2.server.com"); + configuration.put("maxTries", "1"); + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + mojo.execute(); + + verify(log).warn(contains("2 TSA URLs specified. Only first will be used because maxTries is set to 1")); + } + + @Test + public void testVerifyMultipleTsacertButNoRetry() throws Exception { + when(jarSigner.execute(any(JarSignerSignRequest.class))).thenReturn(RESULT_OK); + configuration.put("tsacert", "alias1,alias2"); + configuration.put("maxTries", "1"); + JarsignerSignMojo mojo = mojoTestCreator.configure(configuration); + + mojo.execute(); + + verify(log).warn(contains("2 TSA certificate aliases specified. Only first")); + } + + private File createArchives(int numberOfArchives) throws IOException { + File archiveDirectory = new File(projectDir, "my_archive_dir"); + archiveDirectory.mkdir(); + for (int i = 0; i < numberOfArchives; i++) { + TestArtifacts.createDummyZipFile(new File(archiveDirectory, "archive" + i + ".jar")); + } + return archiveDirectory; + } +} diff --git a/src/test/java/org/apache/maven/plugins/jarsigner/MojoTestCreator.java b/src/test/java/org/apache/maven/plugins/jarsigner/MojoTestCreator.java index 9f5c597..ff5c0f9 100644 --- a/src/test/java/org/apache/maven/plugins/jarsigner/MojoTestCreator.java +++ b/src/test/java/org/apache/maven/plugins/jarsigner/MojoTestCreator.java @@ -124,8 +124,13 @@ private void setFieldByStringValue(Object instance, Field field, String stringVa } else if (fieldType.equals(File.class)) { field.set(instance, new File(stringValue)); } else if (fieldType.equals(String[].class)) { - String[] values = stringValue.split(","); - field.set(instance, values); + if (stringValue.isEmpty()) { + // Maven defaults to empty list if no default value exists + field.set(instance, new String[0]); + } else { + String[] values = stringValue.split(","); + field.set(instance, values); + } } else { if (!stringValue.startsWith("${")) { logger.warn( diff --git a/src/test/java/org/apache/maven/plugins/jarsigner/PluginXmlParser.java b/src/test/java/org/apache/maven/plugins/jarsigner/PluginXmlParser.java index a450a1d..468e8b5 100644 --- a/src/test/java/org/apache/maven/plugins/jarsigner/PluginXmlParser.java +++ b/src/test/java/org/apache/maven/plugins/jarsigner/PluginXmlParser.java @@ -68,6 +68,14 @@ public static Map getMojoDefaultConfiguration(Class hasTsacert(String tsacert) { "has tsacert ", tsacert, request -> request.getTsaAlias().equals(tsacert)); } + static TypeSafeMatcher hasTsaPolicyid(String tsapolicyid) { + return new JarSignerSignRequestMatcher("has tsapolicyid ", tsapolicyid, request -> request.getTsapolicyid() + .equals(tsapolicyid)); + } + + static TypeSafeMatcher hasTsaDigestalg(String tsadigestalg) { + return new JarSignerSignRequestMatcher("has tsadigestalg ", tsadigestalg, request -> request.getTsadigestalg() + .equals(tsadigestalg)); + } + static TypeSafeMatcher hasCertchain(String certchain) { return new JarSignerSignRequestMatcher("has certchain ", certchain, request -> request.getCertchain() .getPath() diff --git a/src/test/java/org/apache/maven/plugins/jarsigner/TsaSelectorTest.java b/src/test/java/org/apache/maven/plugins/jarsigner/TsaSelectorTest.java new file mode 100644 index 0000000..2703cde --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/jarsigner/TsaSelectorTest.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.jarsigner; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.maven.plugins.jarsigner.TsaSelector.TsaServer; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class TsaSelectorTest { + private static final String[] EMPTY = new String[0]; + private TsaSelector tsaSelector; + private TsaServer tsaServer; + private ExecutorService executor; + + @Test + public void testNullInit() { + tsaSelector = new TsaSelector(EMPTY, EMPTY, EMPTY, null); + tsaServer = tsaSelector.getServer(); + assertNull(tsaServer.getTsaUrl()); + assertNull(tsaServer.getTsaAlias()); + assertNull(tsaServer.getTsaPolicyId()); + assertNull(tsaServer.getTsaDigestAlt()); + + // Make sure "next" server also contains null values + tsaServer = tsaSelector.getServer(); + assertNull(tsaServer.getTsaUrl()); + assertNull(tsaServer.getTsaAlias()); + assertNull(tsaServer.getTsaPolicyId()); + assertNull(tsaServer.getTsaDigestAlt()); + } + + @Test + public void testFailureCount() { + tsaSelector = new TsaSelector( + new String[] {"http://url1.com", "http://url2.com", "http://url3.com"}, EMPTY, EMPTY, null); + + tsaServer = tsaSelector.getServer(); + assertEquals("http://url1.com", tsaServer.getTsaUrl()); + assertNull(tsaServer.getTsaAlias()); + assertNull(tsaServer.getTsaPolicyId()); + assertNull(tsaServer.getTsaDigestAlt()); + + tsaSelector.registerFailure(); + + tsaServer = tsaSelector.getServer(); + assertEquals("http://url2.com", tsaServer.getTsaUrl()); + assertNull(tsaServer.getTsaAlias()); + assertNull(tsaServer.getTsaPolicyId()); + assertNull(tsaServer.getTsaDigestAlt()); + + // Should get same server again + tsaServer = tsaSelector.getServer(); + assertEquals("http://url2.com", tsaServer.getTsaUrl()); + assertNull(tsaServer.getTsaAlias()); + assertNull(tsaServer.getTsaPolicyId()); + assertNull(tsaServer.getTsaDigestAlt()); + } + + @Test(timeout = 30000) + public void testMultiThreadedScenario() throws InterruptedException { + executor = Executors.newFixedThreadPool(2); + + tsaSelector = new TsaSelector( + new String[] {"http://url1.com", "http://url2.com", "http://url3.com"}, EMPTY, EMPTY, null); + + // Register a single failure on the first URL so that the threads will use URL 2 + TsaServer serverThreadMain = tsaSelector.getServer(); + tsaSelector.registerFailure(); + + CountDownLatch doneSignal = new CountDownLatch(2); // Indication that both threads has gotten a server + Semaphore semaphore = new Semaphore(0); // When the threads may continue executing after gotten a server + + AtomicReference serverThread1 = new AtomicReference<>(); + AtomicReference serverThread2 = new AtomicReference<>(); + executor.submit(() -> { + serverThread1.set(tsaSelector.getServer()); + doneSignal.countDown(); + semaphore.acquireUninterruptibly(); + tsaSelector.registerFailure(); + }); + executor.submit(() -> { + serverThread2.set(tsaSelector.getServer()); + doneSignal.countDown(); + semaphore.acquireUninterruptibly(); + tsaSelector.registerFailure(); + }); + + doneSignal.await(); // Wait until both threads has gotten an TsaServer + semaphore.release(2); // Release both threads waiting for the semaphore + + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + + assertEquals("http://url1.com", serverThreadMain.getTsaUrl()); + assertEquals("http://url2.com", serverThread1.get().getTsaUrl()); + assertEquals("http://url2.com", serverThread2.get().getTsaUrl()); + + // The best URL is now number 3 + assertEquals("http://url3.com", tsaSelector.getServer().getTsaUrl()); + + // Trigger a new failure, now URL 1 is best again. + tsaSelector.registerFailure(); + assertEquals("http://url1.com", tsaSelector.getServer().getTsaUrl()); + } + + @Test + public void testDigestAlgoritm() { + tsaSelector = new TsaSelector( + new String[] {"http://url1.com", "http://url2.com", "http://url3.com"}, EMPTY, EMPTY, "SHA-512"); + tsaServer = tsaSelector.getServer(); + assertEquals("http://url1.com", tsaServer.getTsaUrl()); + assertNull(tsaServer.getTsaAlias()); + assertNull(tsaServer.getTsaPolicyId()); + assertEquals("SHA-512", tsaServer.getTsaDigestAlt()); + + // Make sure that the next URL has the same digest algorithm + tsaSelector.registerFailure(); + tsaServer = tsaSelector.getServer(); + assertEquals("http://url2.com", tsaServer.getTsaUrl()); + assertNull(tsaServer.getTsaAlias()); + assertNull(tsaServer.getTsaPolicyId()); + assertEquals("SHA-512", tsaServer.getTsaDigestAlt()); + } + + @Test + public void testKeyStoreAliasAndOid() { + tsaSelector = new TsaSelector(EMPTY, new String[] {"alias1", "alias2"}, new String[] {"1.1", "1.2"}, null); + tsaServer = tsaSelector.getServer(); + assertNull(tsaServer.getTsaUrl()); + assertEquals("alias1", tsaServer.getTsaAlias()); + assertEquals("1.1", tsaServer.getTsaPolicyId()); + + tsaSelector.registerFailure(); + tsaServer = tsaSelector.getServer(); + assertNull(tsaServer.getTsaUrl()); + assertEquals("alias2", tsaServer.getTsaAlias()); + assertEquals("1.2", tsaServer.getTsaPolicyId()); + } + + @Test + public void testFailureRegistrationWithoutCurrent() { + tsaSelector = new TsaSelector( + new String[] {"http://url1.com"}, new String[] {"alias1"}, new String[] {"1.1"}, "SHA-384"); + tsaSelector.registerFailure(); // Should not throw any exception + + // Make sure further execution works + tsaServer = tsaSelector.getServer(); + assertEquals("http://url1.com", tsaServer.getTsaUrl()); + assertEquals("alias1", tsaServer.getTsaAlias()); + assertEquals("1.1", tsaServer.getTsaPolicyId()); + assertEquals("SHA-384", tsaServer.getTsaDigestAlt()); + } +}