This is an automated email from the ASF dual-hosted git repository.

sjaranowski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-jarsigner-plugin.git


The following commit(s) were added to refs/heads/master by this push:
     new 5535caa  [MJARSIGNER-74] Allow usage of multiple Time Stamping 
Authority (TSA) servers
5535caa is described below

commit 5535caade70570e970e8e0561a0de9587c612b0b
Author: schedin <sche...@users.noreply.github.com>
AuthorDate: Sat Dec 23 17:45:55 2023 +0100

    [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
---
 .../plugins/jarsigner/AbstractJarsignerMojo.java   |   8 +-
 .../maven/plugins/jarsigner/JarsignerSignMojo.java | 130 ++++++++++++-
 .../maven/plugins/jarsigner/TsaSelector.java       | 140 ++++++++++++++
 src/main/resources/jarsigner.properties            |   4 +
 .../jarsigner/JarsignerSignMojoTsaTest.java        | 208 +++++++++++++++++++++
 .../maven/plugins/jarsigner/MojoTestCreator.java   |   9 +-
 .../maven/plugins/jarsigner/PluginXmlParser.java   |   8 +
 .../maven/plugins/jarsigner/RequestMatchers.java   |  10 +
 .../maven/plugins/jarsigner/TsaSelectorTest.java   | 177 ++++++++++++++++++
 9 files changed, 680 insertions(+), 14 deletions(-)

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 {
      * <pre>
      * {@code
      * <configuration>
-     *     <arguments>
-     *         <argument>-signedjar</argument>
-     *         <argument>my-project_signed.jar</argument>
-     *     </arguments>
+     *   <arguments>
+     *     <argument>-signedjar</argument>
+     *     <argument>my-project_signed.jar</argument>
+     *   </arguments>
      * </configuration>
      * }</pre>
      */
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.plugin.MojoExecutionException;
 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;
 
     /**
+     * <p>URL(s) to Time Stamping Authority (TSA) server(s) to use to 
timestamp the signing.
      * See <a 
href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options";>options</a>.
+     * Separate multiple TSA URLs with comma (without space) or a nested XML 
tag.</p>
+     *
+     * <pre>{@code
+     * <configuration>
+     *   
<tsa>http://timestamp.digicert.com,http://timestamp.globalsign.com/tsa/r6advanced1</tsa>
+     * </configuration>
+     * }</pre>
+     *
+     * <pre>{@code
+     * <configuration>
+     *   <tsa>
+     *     <url>http://timestamp.digicert.com</url>
+     *     <url>http://timestamp.globalsign.com/tsa/r6advanced1</url>
+     *   </tsa>
+     * </configuration>
+     * }</pre>
+     *
+     * <p>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.</p>
+     *
+     * <p>Changed to a list since 3.1.0. Single XML element (without comma) is 
still supported.</p>
      *
      * @since 1.3
      */
     @Parameter(property = "jarsigner.tsa")
-    private String tsa;
+    private String[] tsa;
 
     /**
-     * See <a 
href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options";>options</a>.
+     * <p>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
+     * <a 
href="https://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html#Options";>options</a>.
+     * Separate multiple aliases with comma (without space) or a nested XML 
tag.</p>
+     *
+     * <pre>{@code
+     * <configuration>
+     *   <tsacert>alias1,alias2</tsacert>
+     * </configuration>
+     * }</pre>
+     *
+     * <pre>{@code
+     * <configuration>
+     *   <tsacert>
+     *     <alias>alias1</alias>
+     *     <alias>alias2</alias>
+     *   </tsacert>
+     * </configuration>
+     * }</pre>
+     *
+     * <p>Should not be used at the same time as the {@link #tsa} parameter 
(because jarsigner will typically ignore
+     * tsacert, if tsa is set).</p>
+     *
+     * <p>Usage of multiple aliases only makes sense when {@link #maxTries} is 
more than 1. A different TSA server
+     * will only be used at retries.</p>
+     *
+     * <p>Changed to a list since 3.1.0. Single XML element (without comma) is 
still supported.</p>
      *
      * @since 1.3
      */
     @Parameter(property = "jarsigner.tsacert")
-    private String tsacert;
+    private String[] tsacert;
+
+    /**
+     * <p>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
+     * <a 
href="https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html#CCHIFIAD";>options</a>.
+     * Separate multiple OIDs with comma (without space) or a nested XML 
tag.</p>
+     *
+     * <pre>{@code
+     * <configuration>
+     *   
<tsapolicyid>1.3.6.1.4.1.4146.2.3.1.2,2.16.840.1.114412.7.1</tsapolicyid>
+     * </configuration>
+     * }</pre>
+     *
+     * <pre>{@code
+     * <configuration>
+     *   <tsapolicyid>
+     *     <oid>1.3.6.1.4.1.4146.2.3.1.2</oid>
+     *     <oid>2.16.840.1.114412.7.1</oid>
+     *   </tsapolicyid>
+     * </configuration>
+     * }</pre>
+     *
+     * <p>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.</p>
+     *
+     * @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 <a 
href="https://docs.oracle.com/en/java/javase/11/tools/jarsigner.html";>options</a>.
+     *
+     * @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 @@ public class JarsignerSignMojo extends 
AbstractJarsignerMojo {
             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 @@ public class JarsignerSignMojo extends 
AbstractJarsignerMojo {
     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 @@ public class JarsignerSignMojo extends 
AbstractJarsignerMojo {
         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 @@ public class JarsignerSignMojo extends 
AbstractJarsignerMojo {
         List<Future<Void>> futures = archives.stream()
                 .map(file -> executor.submit((Callable<Void>) () -> {
                     processArchive(file);
-                    return null;
+                    return null; // Return dummy value to conform with Void 
type
                 }))
                 .collect(Collectors.toList());
         try {
@@ -236,15 +347,18 @@ public class JarsignerSignMojo extends 
AbstractJarsignerMojo {
         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:
+ *    <pre>
+ *    [vagrant@podmanhost ~]$ openssl x509 -noout -ext subjectInfoAccess -in 
tsa-server.crt
+ *    Subject Information Access:
+ *        AD Time Stamping - 
URI:http://timestamp.globalsign.com/tsa/r6advanced1
+ *    </pre>
+ *
+ * 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<TsaServer> currentTsaServer = new 
ThreadLocal<>();
+
+    /** List of TSA servers. Will at minimum contain a dummy/empty value */
+    private final List<TsaServer> tsaServers;
+
+    TsaSelector(String[] tsa, String[] tsacert, String[] tsapolicyid, String 
tsadigestalg) {
+        List<TsaServer> 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<String, String> configuration = new LinkedHashMap<>();
+    private Log log;
+    private MojoTestCreator<JarsignerSignMojo> 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>(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<JarSignerSignRequest> requestArgument = 
ArgumentCaptor.forClass(JarSignerSignRequest.class);
+        verify(jarSigner, times(3)).execute(requestArgument.capture());
+        List<JarSignerSignRequest> 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<String> 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 @@ public class MojoTestCreator<T extends 
AbstractJarsignerMojo> {
         } 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 class PluginXmlParser {
             if (configurationElement.hasAttribute(CONF_DEFAULT_VALUE)) {
                 String defaultValue = 
configurationElement.getAttribute(CONF_DEFAULT_VALUE);
                 defaultConfiguration.put(configurationParameterName, 
defaultValue);
+            } else {
+                if (configurationElement.hasAttribute("implementation")) {
+                    String implementation = 
configurationElement.getAttribute("implementation");
+                    // String arrays are per default set to empty array if 
user does not configure them
+                    if ("java.lang.String[]".equals(implementation)) {
+                        defaultConfiguration.put(configurationParameterName, 
"");
+                    }
+                }
             }
         }
         return defaultConfiguration;
diff --git 
a/src/test/java/org/apache/maven/plugins/jarsigner/RequestMatchers.java 
b/src/test/java/org/apache/maven/plugins/jarsigner/RequestMatchers.java
index 518e651..f7993b1 100644
--- a/src/test/java/org/apache/maven/plugins/jarsigner/RequestMatchers.java
+++ b/src/test/java/org/apache/maven/plugins/jarsigner/RequestMatchers.java
@@ -185,6 +185,16 @@ class RequestMatchers {
                 "has tsacert ", tsacert, request -> 
request.getTsaAlias().equals(tsacert));
     }
 
+    static TypeSafeMatcher<JarSignerSignRequest> hasTsaPolicyid(String 
tsapolicyid) {
+        return new JarSignerSignRequestMatcher("has tsapolicyid ", 
tsapolicyid, request -> request.getTsapolicyid()
+                .equals(tsapolicyid));
+    }
+
+    static TypeSafeMatcher<JarSignerSignRequest> hasTsaDigestalg(String 
tsadigestalg) {
+        return new JarSignerSignRequestMatcher("has tsadigestalg ", 
tsadigestalg, request -> request.getTsadigestalg()
+                .equals(tsadigestalg));
+    }
+
     static TypeSafeMatcher<JarSignerSignRequest> 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<TsaServer> serverThread1 = new AtomicReference<>();
+        AtomicReference<TsaServer> 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());
+    }
+}


Reply via email to