TsaSelector.java

/*
 * 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;
        }
    }
}