Skip to content

Commit

Permalink
[MJARSIGNER-74] Allow usage of multiple Time Stamping Authority (TSA)…
Browse files Browse the repository at this point in the history
… servers

Using multiple TSA URLs to try if first fail
Adding support for tsapolicyid and tsadigestalg
  • Loading branch information
schedin authored and slawekjaranowski committed Sep 3, 2024
1 parent 5a9a484 commit 5535caa
Show file tree
Hide file tree
Showing 9 changed files with 680 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -184,15 +288,22 @@ 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
request.setKeypass(decrypt(keypass));
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.
*/
Expand All @@ -202,7 +313,7 @@ protected void processArchives(List<File> archives) throws MojoExecutionExceptio
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 {
Expand Down Expand Up @@ -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));
}
}
}
Expand Down
140 changes: 140 additions & 0 deletions src/main/java/org/apache/maven/plugins/jarsigner/TsaSelector.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
4 changes: 4 additions & 0 deletions src/main/resources/jarsigner.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 5535caa

Please sign in to comment.