• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ebourg / jsign / #414

15 Jul 2026 08:39PM UTC coverage: 80.805% (-0.07%) from 80.877%
#414

push

Tathorack
Fix duplicate slashes in API endpoint paths for DigiCertOneSigningService

DigiCert has pushed a change to their public API's which are causing requests to endpoints with duplicate slashes to fail and return HTML instead of the expected JSON response. This causes JSIGN to fail when using the DigiCert signing service

1 of 1 new or added line in 1 file covered. (100.0%)

63 existing lines in 4 files now uncovered.

5262 of 6512 relevant lines covered (80.8%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.43
/jsign-cli/src/main/java/net/jsign/JsignCLI.java
1
/*
2
 * Copyright 2012 Emmanuel Bourg
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package net.jsign;
18

19
import java.io.BufferedInputStream;
20
import java.io.File;
21
import java.io.FileInputStream;
22
import java.io.IOException;
23
import java.io.PrintWriter;
24
import java.nio.file.Path;
25
import java.util.Arrays;
26
import java.util.Collections;
27
import java.util.HashMap;
28
import java.util.LinkedHashMap;
29
import java.util.List;
30
import java.util.Map;
31
import java.util.logging.Level;
32
import java.util.logging.Logger;
33
import java.util.stream.Collectors;
34
import java.util.stream.Stream;
35

36
import org.apache.commons.cli.CommandLine;
37
import org.apache.commons.cli.DefaultParser;
38
import org.apache.commons.cli.HelpFormatter;
39
import org.apache.commons.cli.Option;
40
import org.apache.commons.cli.Options;
41
import org.apache.commons.cli.ParseException;
42
import org.apache.commons.io.IOUtils;
43
import org.apache.commons.io.input.BOMInputStream;
44

45
import static net.jsign.SignerHelper.*;
46
import static org.apache.commons.io.ByteOrderMark.*;
47

48
/**
49
 * Command line interface for signing files.
50
 *
51
 * @author Emmanuel Bourg
52
 * @since 1.1
53
 */
54
public class JsignCLI {
1✔
55

56
    public static void main(String... args) {
57
        try {
58
            new JsignCLI().execute(args);
1✔
59
        } catch (SignerException | IllegalArgumentException | ParseException e) {
1✔
60
            System.err.println("jsign: " + e.getMessage());
1✔
61
            if (e.getCause() != null) {
1✔
62
                e.getCause().printStackTrace(System.err);
×
63
            }
64
            System.err.println("Try `" + getProgramName() + " --help' for more information.");
1✔
65
            System.exit(1);
×
66
        }
1✔
67
    }
1✔
68

69
    /**
70
     * Returns the options for each operation.
71
     */
72
    private Map<String, Options> getOptions() {
73
        Map<String, Options> map = new LinkedHashMap<>();
1✔
74

75
        Options options = new Options();
1✔
76
        options.addOption(Option.builder("s").hasArg().longOpt(PARAM_KEYSTORE).argName("FILE").desc("The keystore file, the SunPKCS11 configuration file, the cloud keystore name, or the card/token name").type(File.class).build());
1✔
77
        options.addOption(Option.builder().hasArg().longOpt(PARAM_STOREPASS).argName("PASSWORD").desc("The password to open the keystore").build());
1✔
78
        options.addOption(Option.builder().hasArg().longOpt(PARAM_STORETYPE).argName("TYPE")
1✔
79
                .desc("The type of the keystore\n"
1✔
80
                        + "File based:\n"
81
                        + "- PKCS12: Standard PKCS#12 keystore (.p12 or .pfx files)\n"
82
                        + "- JKS: Java keystore (.jks files)\n"
83
                        + "- JCEKS: SunJCE keystore (.jceks files)\n"
84
                        + "Hardware tokens\n"
85
                        + "- PKCS11: PKCS#11 hardware token\n"
86
                        + "- CRYPTOCERTUM: CryptoCertum card\n"
87
                        + "- ETOKEN: SafeNet eToken\n"
88
                        + "- NITROKEY: Nitrokey HSM\n"
89
                        + "- OPENPGP: OpenPGP card\n"
90
                        + "- OPENSC: Smart card\n"
91
                        + "- PIV: PIV card\n"
92
                        + "- YUBIKEY: YubiKey security key\n"
93
                        + "Cloud key management systems:\n"
94
                        + "- AWS: AWS Key Management Service\n"
95
                        + "- AZUREKEYVAULT: Azure Key Vault key management system\n"
96
                        + "- CODESIGNSECURE: Encryption Consulting CodeSign Secure\n"
97
                        + "- DIGICERTONE: DigiCert ONE Secure Software Manager\n"
98
                        + "- ESIGNER: SSL.com eSigner\n"
99
                        + "- GARASIGN: Garantir Remote Signing\n"
100
                        + "- GOOGLECLOUD: Google Cloud KMS\n"
101
                        + "- HASHICORPVAULT: HashiCorp Vault\n"
102
                        + "- ORACLECLOUD: Oracle Cloud Key Management Service\n"
103
                        + "- SIGNPATH: SignPath\n"
104
                        + "- SIGNSERVER: Keyfactor SignServer\n"
105
                        + "- TRUSTEDSIGNING: Azure Artifact Signing\n").build());
1✔
106
        options.addOption(Option.builder("a").hasArg().longOpt(PARAM_ALIAS).argName("NAME").desc("The alias of the certificate used for signing in the keystore").build());
1✔
107
        options.addOption(Option.builder().hasArg().longOpt(PARAM_KEYPASS).argName("PASSWORD").desc("The password of the private key. When using a keystore, this parameter can be omitted if the keystore shares the same password").build());
1✔
108
        options.addOption(Option.builder().hasArg().longOpt(PARAM_KEYFILE).argName("FILE").desc("The file containing the private key. PEM and PVK files are supported").type(File.class).build());
1✔
109
        options.addOption(Option.builder("c").hasArg().longOpt(PARAM_CERTFILE).argName("FILE").desc("The file containing the PKCS#7 certificate chain\n(.p7b or .spc files)").type(File.class).build());
1✔
110
        options.addOption(Option.builder("d").hasArg().longOpt(PARAM_ALG).argName("ALGORITHM").desc("The digest algorithm (SHA-1, SHA-256, SHA-384 or SHA-512)").build());
1✔
111
        options.addOption(Option.builder("t").hasArg().longOpt(PARAM_TSAURL).argName("URL").desc("The URL of the timestamping authority. Several URLs separated by a comma can be specified to fallback on alternative servers").build());
1✔
112
        options.addOption(Option.builder("t").hasArg().longOpt(PARAM_TSAURL).argName("URL").desc("The URL of the timestamping authority").build());
1✔
113
        options.addOption(Option.builder("m").hasArg().longOpt(PARAM_TSMODE).argName("MODE").desc("The timestamping mode (RFC3161 or Authenticode)").build());
1✔
114
        options.addOption(Option.builder("r").hasArg().longOpt(PARAM_TSRETRIES).argName("NUMBER").desc("The number of retries for timestamping").build());
1✔
115
        options.addOption(Option.builder("w").hasArg().longOpt(PARAM_TSRETRY_WAIT).argName("SECONDS").desc("The number of seconds to wait between timestamping retries").build());
1✔
116
        options.addOption(Option.builder("n").hasArg().longOpt(PARAM_NAME).argName("NAME").desc("The name of the application").build());
1✔
117
        options.addOption(Option.builder("u").hasArg().longOpt(PARAM_URL).argName("URL").desc("The URL of the application").build());
1✔
118
        options.addOption(Option.builder().hasArg().longOpt(PARAM_PROXY_URL).argName("URL").desc("The URL of the HTTP proxy").build());
1✔
119
        options.addOption(Option.builder().hasArg().longOpt(PARAM_PROXY_USER).argName("NAME").desc("The user for the HTTP proxy. If a user is needed").build());
1✔
120
        options.addOption(Option.builder().hasArg().longOpt(PARAM_PROXY_PASS).argName("PASSWORD").desc("The password for the HTTP proxy user. If a user is needed").build());
1✔
121
        options.addOption(Option.builder().hasArg().longOpt(PARAM_NON_PROXY_HOSTS).argName("HOSTS").desc("The hosts that bypass the HTTP proxy, as a pipe-separated list of patterns").build());
1✔
122
        options.addOption(Option.builder().longOpt(PARAM_REPLACE).desc("Tells if the previous signatures should be replaced").build());
1✔
123
        options.addOption(Option.builder().longOpt(PARAM_LAZY).desc("Skip files that are already signed").build());
1✔
124
        options.addOption(Option.builder("e").hasArg().longOpt(PARAM_ENCODING).argName("ENCODING").desc("The encoding of the script to be signed (UTF-8 by default, or the encoding specified by the byte order mark if there is one)").build());
1✔
125
        options.addOption(Option.builder().longOpt(PARAM_DETACHED).desc("Tells if a detached signature should be generated or reused").build());
1✔
126
        options.addOption(Option.builder().longOpt("quiet").desc("Print only error messages").build());
1✔
127
        options.addOption(Option.builder().longOpt("verbose").desc("Print more information").build());
1✔
128
        options.addOption(Option.builder().longOpt("debug").desc("Print debugging information").build());
1✔
129
        options.addOption(Option.builder("h").longOpt("help").desc("Print the help").build());
1✔
130
        options.addOption(Option.builder().longOpt("version").desc("Display version information").build());
1✔
131

132
        map.put("sign", options);
1✔
133

134
        options = new Options();
1✔
135
        options.addOption(Option.builder("t").hasArg().longOpt(PARAM_TSAURL).argName("URL").desc("The URL of the timestamping authority").build());
1✔
136
        options.addOption(Option.builder("m").hasArg().longOpt(PARAM_TSMODE).argName("MODE").desc("The timestamping mode (RFC3161 or Authenticode)").build());
1✔
137
        options.addOption(Option.builder("r").hasArg().longOpt(PARAM_TSRETRIES).argName("NUMBER").desc("The number of retries for timestamping").build());
1✔
138
        options.addOption(Option.builder("w").hasArg().longOpt(PARAM_TSRETRY_WAIT).argName("SECONDS").desc("The number of seconds to wait between timestamping retries").build());
1✔
139
        options.addOption(Option.builder().hasArg().longOpt(PARAM_PROXY_URL).argName("URL").desc("The URL of the HTTP proxy").build());
1✔
140
        options.addOption(Option.builder().hasArg().longOpt(PARAM_PROXY_USER).argName("NAME").desc("The user for the HTTP proxy. If a user is needed").build());
1✔
141
        options.addOption(Option.builder().hasArg().longOpt(PARAM_PROXY_PASS).argName("PASSWORD").desc("The password for the HTTP proxy user. If a user is needed").build());
1✔
142
        options.addOption(Option.builder().hasArg().longOpt(PARAM_NON_PROXY_HOSTS).argName("HOSTS").desc("The hosts that bypass the HTTP proxy, as a pipe-separated list of patterns").build());
1✔
143
        options.addOption(Option.builder().longOpt(PARAM_REPLACE).desc("Tells if the previous timestamps should be replaced").build());
1✔
144

145
        map.put("timestamp", options);
1✔
146

147
        options = new Options();
1✔
148
        options.addOption(Option.builder().hasArg().longOpt(PARAM_FORMAT).argName("FORMAT").desc("The output format of the signature (DER or PEM)").build());
1✔
149

150
        map.put("extract", options);
1✔
151

152
        options = new Options();
1✔
153

154
        map.put("remove", options);
1✔
155

156
        options = new Options();
1✔
157

158
        map.put("show", options);
1✔
159

160
        options = new Options();
1✔
161
        options.addOption(Option.builder().hasArg().longOpt(PARAM_VALUE).argName("VALUE").desc("The value of the unsigned attribute").build());
1✔
162

163
        map.put("tag", options);
1✔
164

165
        return map;
1✔
166
    }
167

168
    void execute(String... args) throws SignerException, ParseException {
169
        DefaultParser parser = new DefaultParser();
1✔
170
        
171
        String command = "sign";
1✔
172
        if (args.length >= 1 && !args[0].startsWith("-")) {
1✔
173
            command = args[0];
1✔
174
            args = Arrays.copyOfRange(args, 1, args.length);
1✔
175
        }
176

177
        Options options = getOptions().get(command);
1✔
178
        if (options == null) {
1✔
179
            throw new ParseException("Unknown command '" + command + "'");
1✔
180
        }
181
        options.addOption(Option.builder().longOpt("quiet").build());
1✔
182
        options.addOption(Option.builder().longOpt("verbose").build());
1✔
183
        options.addOption(Option.builder().longOpt("debug").build());
1✔
184

185
        CommandLine cmd = parser.parse(options, args);
1✔
186

187
        if (cmd.hasOption("help") || args.length == 0) {
1✔
188
            printHelp();
1✔
189
            return;
1✔
190
        }
191

192
        if (cmd.hasOption("version")) {
1✔
193
            printVersion();
1✔
194
            return;
1✔
195
        }
196

197
        // configure the logging
198
        Logger log = Logger.getLogger("net.jsign");
1✔
199
        log.setLevel(cmd.hasOption("debug") ? Level.FINEST : cmd.hasOption("verbose") ? Level.FINE : cmd.hasOption("quiet") ? Level.WARNING : Level.INFO);
1✔
200
        log.setUseParentHandlers(false);
1✔
201
        Stream.of(log.getHandlers()).forEach(log::removeHandler);
1✔
202
        log.addHandler(new StdOutLogHandler());
1✔
203

204
        SignerHelper helper = new SignerHelper("option");
1✔
205
        helper.command(command);
1✔
206
        
207
        setOption(PARAM_KEYSTORE, helper, cmd);
1✔
208
        setOption(PARAM_STOREPASS, helper, cmd);
1✔
209
        setOption(PARAM_STORETYPE, helper, cmd);
1✔
210
        setOption(PARAM_ALIAS, helper, cmd);
1✔
211
        setOption(PARAM_KEYPASS, helper, cmd);
1✔
212
        setOption(PARAM_KEYFILE, helper, cmd);
1✔
213
        setOption(PARAM_CERTFILE, helper, cmd);
1✔
214
        setOption(PARAM_ALG, helper, cmd);
1✔
215
        setOption(PARAM_TSAURL, helper, cmd);
1✔
216
        setOption(PARAM_TSMODE, helper, cmd);
1✔
217
        setOption(PARAM_TSRETRIES, helper, cmd);
1✔
218
        setOption(PARAM_TSRETRY_WAIT, helper, cmd);
1✔
219
        setOption(PARAM_NAME, helper, cmd);
1✔
220
        setOption(PARAM_URL, helper, cmd);
1✔
221
        setOption(PARAM_PROXY_URL, helper, cmd);
1✔
222
        setOption(PARAM_PROXY_USER, helper, cmd);
1✔
223
        setOption(PARAM_PROXY_PASS, helper, cmd);
1✔
224
        setOption(PARAM_NON_PROXY_HOSTS, helper, cmd);
1✔
225
        helper.replace(cmd.hasOption(PARAM_REPLACE));
1✔
226
        helper.lazy(cmd.hasOption(PARAM_LAZY));
1✔
227
        setOption(PARAM_ENCODING, helper, cmd);
1✔
228
        helper.detached(cmd.hasOption(PARAM_DETACHED));
1✔
229
        setOption(PARAM_FORMAT, helper, cmd);
1✔
230
        setOption(PARAM_VALUE, helper, cmd);
1✔
231

232
        if (cmd.getArgList().isEmpty()) {
1✔
233
            throw new SignerException("No file specified");
1✔
234
        }
235

236
        for (String arg : cmd.getArgList()) {
1✔
237
            for (String filename : expand(arg)) {
1✔
238
                if (!filename.trim().isEmpty() && !filename.startsWith("#")) {
1✔
239
                    helper.execute(new File(unquote(filename)));
1✔
240
                }
241
            }
1✔
242
        }
1✔
243
    }
1✔
244

245
    /**
246
     * Expands filenames starting with @ to a list of filenames.
247
     */
248
    private List<String> expand(String filename) {
249
        if (filename.startsWith("@")) {
1✔
250
            try {
251
                return readFile(new File(filename.substring(1)));
1✔
UNCOV
252
            } catch (IOException e) {
×
UNCOV
253
                throw new IllegalArgumentException("Failed to read the file list: " + filename.substring(1), e);
×
254
            }
255
        } else if (filename.contains("*")) {
1✔
256
            try {
257
                return new DirectoryScanner().scan(filename).stream().map(Path::toString).collect(Collectors.toList());
1✔
UNCOV
258
            } catch (IOException e) {
×
UNCOV
259
                throw new IllegalArgumentException("Failed to scan the directory: " + filename, e);
×
260
            }
261
        } else {
262
            return Collections.singletonList(filename);
1✔
263
        }
264
    }
265

266
    /**
267
     * Reads the content of the text file specified. Byte order marks are supported to detect the encoding,
268
     * otherwise UTF-8 is used.
269
     */
270
    private List<String> readFile(File file) throws IOException {
271
        try (BOMInputStream in = new BOMInputStream(new BufferedInputStream(new FileInputStream(file)), false, UTF_8, UTF_16BE, UTF_16LE)) {
1✔
272
            return IOUtils.readLines(in, in.hasBOM() ? in.getBOMCharsetName() : "UTF-8");
1✔
273
        }
274
    }
275

276
    /**
277
     * Removes the quotes around the specified file name.
278
     */
279
    private String unquote(String value) {
280
        value = value.trim();
1✔
281
        if (value.startsWith("\"") && value.endsWith("\"")) {
1✔
282
            value = value.substring(1, value.length() - 1);
1✔
283
        }
284
        return value;
1✔
285
    }
286

287
    private void setOption(String key, SignerHelper helper, CommandLine cmd) {
288
        String value = cmd.getOptionValue(key);
1✔
289
        helper.param(key, value);
1✔
290
    }
1✔
291

292
    private void printHelp() {
293
        String header = "Sign and timestamp Windows executable files, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX), Microsoft Dynamics 365 extension packages, NuGet packages and scripts (PowerShell, VBScript, JScript, WSF)\n\n";
1✔
294
        String footer ="\n" +
1✔
295
                "Examples:\n\n" +
296
                "   Signing with a PKCS#12 keystore and timestamping:\n\n" +
297
                "     jsign --keystore keystore.p12 --alias test --storepass pwd \\\n" +
298
                "           --tsaurl http://timestamp.sectigo.com application.exe\n\n" +
299
                "   Signing with a SPC certificate and a PVK key:\n\n" +
300
                "     jsign --certfile certificate.spc --keyfile key.pvk --keypass pwd installer.msi\n\n" +
301
                "Please report suggestions and issues on the GitHub project at https://github.com/ebourg/jsign/issues";
302

303
        HelpFormatter formatter = new HelpFormatter();
1✔
304
        formatter.setOptionComparator(null);
1✔
305
        formatter.setWidth(85);
1✔
306

307
        PrintWriter out = new PrintWriter(System.out);
1✔
308
        formatter.printUsage(out, formatter.getWidth(), getProgramName() + " [COMMAND] [OPTIONS] [FILE] [PATTERN] [@FILELIST]...");
1✔
309
        out.println();
1✔
310
        formatter.printWrapped(out, formatter.getWidth(), header);
1✔
311

312
        Map<String, Options> options = getOptions();
1✔
313
        out.println("commands: " + options.keySet().stream().map(s -> "sign".equals(s) ? s + " (default)" : s).collect(Collectors.joining(", ")));
1✔
314

315
        Map<String, Integer> paddings = new HashMap<>();
1✔
316
        paddings.put("extract", 6);
1✔
317
        paddings.put("tag", 8);
1✔
318

319
        for (String command : options.keySet()) {
1✔
320
            if (!options.get(command).getOptions().isEmpty()) {
1✔
321
                out.println();
1✔
322
                out.println(command + ":");
1✔
323
                formatter.setDescPadding(paddings.getOrDefault(command, 1));
1✔
324
                formatter.printOptions(out, formatter.getWidth(), options.get(command), formatter.getLeftPadding(), formatter.getDescPadding());
1✔
325
            }
326
        }
1✔
327
        formatter.printWrapped(out, formatter.getWidth(), footer);
1✔
328
        out.flush();
1✔
329
    }
1✔
330

331
    private void printVersion() {
332
        System.out.println("Jsign " + getClass().getPackage().getImplementationVersion());
1✔
333
    }
1✔
334

335
    private static String getProgramName() {
336
        return System.getProperty("basename", "java -jar jsign.jar");
1✔
337
    }
338
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc