Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MJARSIGNER-74] Allow usage of multiple Time Stamping Authority (TSA) servers #19

Merged
merged 1 commit into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be tsaDigestAlg?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, yes it should be tsaDigestAlg!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schedin @pzygielo what's now ... should I cancel release vote

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have:

        request.setTsadigestalg(tsaServer.getTsaDigestAlt());

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's very internal usage of package-visible only class. No need to cancel vote, as this is not exposed anywhere.

This might be updated anytime later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made pull request #38 for a spelling error fix. It can be merged at a later time.


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