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

ebourg / jsign / #386

24 Oct 2025 09:55AM UTC coverage: 80.64% (-2.4%) from 83.057%
#386

push

ebourg
CI build with Java 25

4965 of 6157 relevant lines covered (80.64%)

0.81 hits per line

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

96.27
/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
                        + "- DIGICERTONE: DigiCert ONE Secure Software Manager\n"
97
                        + "- ESIGNER: SSL.com eSigner\n"
98
                        + "- GARASIGN: Garantir Remote Signing\n"
99
                        + "- GOOGLECLOUD: Google Cloud KMS\n"
100
                        + "- HASHICORPVAULT: HashiCorp Vault\n"
101
                        + "- ORACLECLOUD: Oracle Cloud Key Management Service\n"
102
                        + "- SIGNPATH: SignPath\n"
103
                        + "- SIGNSERVER: Keyfactor SignServer\n"
104
                        + "- TRUSTEDSIGNING: Azure Trusted Signing\n").build());
1✔
105
        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✔
106
        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✔
107
        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✔
108
        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✔
109
        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✔
110
        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✔
111
        options.addOption(Option.builder("t").hasArg().longOpt(PARAM_TSAURL).argName("URL").desc("The URL of the timestamping authority").build());
1✔
112
        options.addOption(Option.builder("m").hasArg().longOpt(PARAM_TSMODE).argName("MODE").desc("The timestamping mode (RFC3161 or Authenticode)").build());
1✔
113
        options.addOption(Option.builder("r").hasArg().longOpt(PARAM_TSRETRIES).argName("NUMBER").desc("The number of retries for timestamping").build());
1✔
114
        options.addOption(Option.builder("w").hasArg().longOpt(PARAM_TSRETRY_WAIT).argName("SECONDS").desc("The number of seconds to wait between timestamping retries").build());
1✔
115
        options.addOption(Option.builder("n").hasArg().longOpt(PARAM_NAME).argName("NAME").desc("The name of the application").build());
1✔
116
        options.addOption(Option.builder("u").hasArg().longOpt(PARAM_URL).argName("URL").desc("The URL of the application").build());
1✔
117
        options.addOption(Option.builder().hasArg().longOpt(PARAM_PROXY_URL).argName("URL").desc("The URL of the HTTP proxy").build());
1✔
118
        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✔
119
        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✔
120
        options.addOption(Option.builder().longOpt(PARAM_REPLACE).desc("Tells if the previous signatures should be replaced").build());
1✔
121
        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✔
122
        options.addOption(Option.builder().longOpt(PARAM_DETACHED).desc("Tells if a detached signature should be generated or reused").build());
1✔
123
        options.addOption(Option.builder().longOpt("quiet").desc("Print only error messages").build());
1✔
124
        options.addOption(Option.builder().longOpt("verbose").desc("Print more information").build());
1✔
125
        options.addOption(Option.builder().longOpt("debug").desc("Print debugging information").build());
1✔
126
        options.addOption(Option.builder("h").longOpt("help").desc("Print the help").build());
1✔
127
        options.addOption(Option.builder().longOpt("version").desc("Display version information").build());
1✔
128

129
        map.put("sign", options);
1✔
130

131
        options = new Options();
1✔
132
        options.addOption(Option.builder("t").hasArg().longOpt(PARAM_TSAURL).argName("URL").desc("The URL of the timestamping authority").build());
1✔
133
        options.addOption(Option.builder("m").hasArg().longOpt(PARAM_TSMODE).argName("MODE").desc("The timestamping mode (RFC3161 or Authenticode)").build());
1✔
134
        options.addOption(Option.builder("r").hasArg().longOpt(PARAM_TSRETRIES).argName("NUMBER").desc("The number of retries for timestamping").build());
1✔
135
        options.addOption(Option.builder("w").hasArg().longOpt(PARAM_TSRETRY_WAIT).argName("SECONDS").desc("The number of seconds to wait between timestamping retries").build());
1✔
136
        options.addOption(Option.builder().hasArg().longOpt(PARAM_PROXY_URL).argName("URL").desc("The URL of the HTTP proxy").build());
1✔
137
        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✔
138
        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✔
139
        options.addOption(Option.builder().longOpt(PARAM_REPLACE).desc("Tells if the previous timestamps should be replaced").build());
1✔
140

141
        map.put("timestamp", options);
1✔
142

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

146
        map.put("extract", options);
1✔
147

148
        options = new Options();
1✔
149

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

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

155
        map.put("tag", options);
1✔
156

157
        return map;
1✔
158
    }
159

160
    void execute(String... args) throws SignerException, ParseException {
161
        DefaultParser parser = new DefaultParser();
1✔
162
        
163
        String command = "sign";
1✔
164
        if (args.length >= 1 && !args[0].startsWith("-")) {
1✔
165
            command = args[0];
1✔
166
            args = Arrays.copyOfRange(args, 1, args.length);
1✔
167
        }
168

169
        Options options = getOptions().get(command);
1✔
170
        if (options == null) {
1✔
171
            throw new ParseException("Unknown command '" + command + "'");
1✔
172
        }
173
        options.addOption(Option.builder().longOpt("quiet").build());
1✔
174
        options.addOption(Option.builder().longOpt("verbose").build());
1✔
175
        options.addOption(Option.builder().longOpt("debug").build());
1✔
176

177
        CommandLine cmd = parser.parse(options, args);
1✔
178

179
        if (cmd.hasOption("help") || args.length == 0) {
1✔
180
            printHelp();
1✔
181
            return;
1✔
182
        }
183

184
        if (cmd.hasOption("version")) {
1✔
185
            printVersion();
1✔
186
            return;
1✔
187
        }
188

189
        // configure the logging
190
        Logger log = Logger.getLogger("net.jsign");
1✔
191
        log.setLevel(cmd.hasOption("debug") ? Level.FINEST : cmd.hasOption("verbose") ? Level.FINE : cmd.hasOption("quiet") ? Level.WARNING : Level.INFO);
1✔
192
        log.setUseParentHandlers(false);
1✔
193
        Stream.of(log.getHandlers()).forEach(log::removeHandler);
1✔
194
        log.addHandler(new StdOutLogHandler());
1✔
195

196
        SignerHelper helper = new SignerHelper("option");
1✔
197
        helper.command(command);
1✔
198
        
199
        setOption(PARAM_KEYSTORE, helper, cmd);
1✔
200
        setOption(PARAM_STOREPASS, helper, cmd);
1✔
201
        setOption(PARAM_STORETYPE, helper, cmd);
1✔
202
        setOption(PARAM_ALIAS, helper, cmd);
1✔
203
        setOption(PARAM_KEYPASS, helper, cmd);
1✔
204
        setOption(PARAM_KEYFILE, helper, cmd);
1✔
205
        setOption(PARAM_CERTFILE, helper, cmd);
1✔
206
        setOption(PARAM_ALG, helper, cmd);
1✔
207
        setOption(PARAM_TSAURL, helper, cmd);
1✔
208
        setOption(PARAM_TSMODE, helper, cmd);
1✔
209
        setOption(PARAM_TSRETRIES, helper, cmd);
1✔
210
        setOption(PARAM_TSRETRY_WAIT, helper, cmd);
1✔
211
        setOption(PARAM_NAME, helper, cmd);
1✔
212
        setOption(PARAM_URL, helper, cmd);
1✔
213
        setOption(PARAM_PROXY_URL, helper, cmd);
1✔
214
        setOption(PARAM_PROXY_USER, helper, cmd);
1✔
215
        setOption(PARAM_PROXY_PASS, helper, cmd);
1✔
216
        helper.replace(cmd.hasOption(PARAM_REPLACE));
1✔
217
        setOption(PARAM_ENCODING, helper, cmd);
1✔
218
        helper.detached(cmd.hasOption(PARAM_DETACHED));
1✔
219
        setOption(PARAM_FORMAT, helper, cmd);
1✔
220
        setOption(PARAM_VALUE, helper, cmd);
1✔
221

222
        if (cmd.getArgList().isEmpty()) {
1✔
223
            throw new SignerException("No file specified");
1✔
224
        }
225

226
        for (String arg : cmd.getArgList()) {
1✔
227
            for (String filename : expand(arg)) {
1✔
228
                if (!filename.trim().isEmpty() && !filename.startsWith("#")) {
1✔
229
                    helper.execute(new File(unquote(filename)));
1✔
230
                }
231
            }
1✔
232
        }
1✔
233
    }
1✔
234

235
    /**
236
     * Expands filenames starting with @ to a list of filenames.
237
     */
238
    private List<String> expand(String filename) {
239
        if (filename.startsWith("@")) {
1✔
240
            try {
241
                return readFile(new File(filename.substring(1)));
1✔
242
            } catch (IOException e) {
×
243
                throw new IllegalArgumentException("Failed to read the file list: " + filename.substring(1), e);
×
244
            }
245
        } else if (filename.contains("*")) {
1✔
246
            try {
247
                return new DirectoryScanner().scan(filename).stream().map(Path::toString).collect(Collectors.toList());
1✔
248
            } catch (IOException e) {
×
249
                throw new IllegalArgumentException("Failed to scan the directory: " + filename, e);
×
250
            }
251
        } else {
252
            return Collections.singletonList(filename);
1✔
253
        }
254
    }
255

256
    /**
257
     * Reads the content of the text file specified. Byte order marks are supported to detect the encoding,
258
     * otherwise UTF-8 is used.
259
     */
260
    private List<String> readFile(File file) throws IOException {
261
        try (BOMInputStream in = new BOMInputStream(new BufferedInputStream(new FileInputStream(file)), false, UTF_8, UTF_16BE, UTF_16LE)) {
1✔
262
            return IOUtils.readLines(in, in.hasBOM() ? in.getBOMCharsetName() : "UTF-8");
1✔
263
        }
264
    }
265

266
    /**
267
     * Removes the quotes around the specified file name.
268
     */
269
    private String unquote(String value) {
270
        value = value.trim();
1✔
271
        if (value.startsWith("\"") && value.endsWith("\"")) {
1✔
272
            value = value.substring(1, value.length() - 1);
1✔
273
        }
274
        return value;
1✔
275
    }
276

277
    private void setOption(String key, SignerHelper helper, CommandLine cmd) {
278
        String value = cmd.getOptionValue(key);
1✔
279
        helper.param(key, value);
1✔
280
    }
1✔
281

282
    private void printHelp() {
283
        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✔
284
        String footer ="\n" +
1✔
285
                "Examples:\n\n" +
286
                "   Signing with a PKCS#12 keystore and timestamping:\n\n" +
287
                "     jsign --keystore keystore.p12 --alias test --storepass pwd \\\n" +
288
                "           --tsaurl http://timestamp.sectigo.com application.exe\n\n" +
289
                "   Signing with a SPC certificate and a PVK key:\n\n" +
290
                "     jsign --certfile certificate.spc --keyfile key.pvk --keypass pwd installer.msi\n\n" +
291
                "Please report suggestions and issues on the GitHub project at https://github.com/ebourg/jsign/issues";
292

293
        HelpFormatter formatter = new HelpFormatter();
1✔
294
        formatter.setOptionComparator(null);
1✔
295
        formatter.setWidth(85);
1✔
296

297
        PrintWriter out = new PrintWriter(System.out);
1✔
298
        formatter.printUsage(out, formatter.getWidth(), getProgramName() + " [COMMAND] [OPTIONS] [FILE] [PATTERN] [@FILELIST]...");
1✔
299
        out.println();
1✔
300
        formatter.printWrapped(out, formatter.getWidth(), header);
1✔
301

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

305
        Map<String, Integer> paddings = new HashMap<>();
1✔
306
        paddings.put("extract", 6);
1✔
307
        paddings.put("tag", 8);
1✔
308

309
        for (String command : options.keySet()) {
1✔
310
            if (!options.get(command).getOptions().isEmpty()) {
1✔
311
                out.println();
1✔
312
                out.println(command + ":");
1✔
313
                formatter.setDescPadding(paddings.getOrDefault(command, 1));
1✔
314
                formatter.printOptions(out, formatter.getWidth(), options.get(command), formatter.getLeftPadding(), formatter.getDescPadding());
1✔
315
            }
316
        }
1✔
317
        formatter.printWrapped(out, formatter.getWidth(), footer);
1✔
318
        out.flush();
1✔
319
    }
1✔
320

321
    private void printVersion() {
322
        System.out.println("Jsign " + getClass().getPackage().getImplementationVersion());
1✔
323
    }
1✔
324

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

© 2026 Coveralls, Inc