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

ebourg / jsign / #418

23 Jul 2026 12:50PM UTC coverage: 81.28% (+0.5%) from 80.829%
#418

push

ebourg
CodeQL workflow

5397 of 6640 relevant lines covered (81.28%)

0.81 hits per line

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

88.29
/jsign-core/src/main/java/net/jsign/SignerHelper.java
1
/*
2
 * Copyright 2017 Emmanuel Bourg and contributors
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.ByteArrayInputStream;
20
import java.io.File;
21
import java.io.FileWriter;
22
import java.io.IOException;
23
import java.nio.charset.Charset;
24
import java.nio.file.Files;
25
import java.security.KeyStore;
26
import java.security.KeyStoreException;
27
import java.security.PrivateKey;
28
import java.security.Provider;
29
import java.security.PublicKey;
30
import java.security.cert.Certificate;
31
import java.security.cert.CertificateException;
32
import java.security.cert.CertificateFactory;
33
import java.security.cert.X509Certificate;
34
import java.security.interfaces.ECPublicKey;
35
import java.security.interfaces.RSAPublicKey;
36
import java.text.DateFormat;
37
import java.text.SimpleDateFormat;
38
import java.time.Duration;
39
import java.time.Instant;
40
import java.util.ArrayList;
41
import java.util.Arrays;
42
import java.util.Base64;
43
import java.util.Collections;
44
import java.util.Date;
45
import java.util.LinkedHashSet;
46
import java.util.List;
47
import java.util.Set;
48
import java.util.logging.Level;
49
import java.util.logging.Logger;
50

51
import org.apache.commons.collections4.functors.AndPredicate;
52
import org.bouncycastle.asn1.ASN1Encodable;
53
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
54
import org.bouncycastle.asn1.ASN1UTF8String;
55
import org.bouncycastle.asn1.DEROctetString;
56
import org.bouncycastle.asn1.DERUTF8String;
57
import org.bouncycastle.asn1.x500.RDN;
58
import org.bouncycastle.asn1.x500.X500Name;
59
import org.bouncycastle.asn1.x500.style.BCStyle;
60
import org.bouncycastle.asn1.x500.style.IETFUtils;
61
import org.bouncycastle.asn1.x509.DigestInfo;
62
import org.bouncycastle.cert.X509CertificateHolder;
63
import org.bouncycastle.cms.CMSException;
64
import org.bouncycastle.cms.CMSSignedData;
65
import org.bouncycastle.cms.SignerId;
66
import org.bouncycastle.cms.SignerInformation;
67
import org.bouncycastle.operator.DefaultAlgorithmNameFinder;
68
import org.bouncycastle.util.encoders.Hex;
69

70
import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers;
71
import net.jsign.asn1.authenticode.SpcLink;
72
import net.jsign.asn1.authenticode.SpcSpOpusInfo;
73
import net.jsign.timestamp.Timestamper;
74
import net.jsign.timestamp.TimestampingMode;
75

76
/**
77
 * Helper class to create AuthenticodeSigner instances with untyped parameters.
78
 * This is used internally to share the parameter validation logic
79
 * between the Ant task, the Maven/Gradle plugins and the CLI tool.
80
 *
81
 * @since 2.0
82
 */
83
class SignerHelper {
84
    public static final String PARAM_COMMAND = "command";
85
    public static final String PARAM_KEYSTORE = "keystore";
86
    public static final String PARAM_STOREPASS = "storepass";
87
    public static final String PARAM_STORETYPE = "storetype";
88
    public static final String PARAM_ALIAS = "alias";
89
    public static final String PARAM_KEYPASS = "keypass";
90
    public static final String PARAM_KEYFILE = "keyfile";
91
    public static final String PARAM_CERTFILE = "certfile";
92
    public static final String PARAM_ALG = "alg";
93
    public static final String PARAM_TSAURL = "tsaurl";
94
    public static final String PARAM_TSMODE = "tsmode";
95
    public static final String PARAM_TSRETRIES = "tsretries";
96
    public static final String PARAM_TSRETRY_WAIT = "tsretrywait";
97
    public static final String PARAM_NAME = "name";
98
    public static final String PARAM_URL = "url";
99
    public static final String PARAM_PROXY_URL = "proxyUrl";
100
    public static final String PARAM_PROXY_USER = "proxyUser";
101
    public static final String PARAM_PROXY_PASS = "proxyPass";
102
    public static final String PARAM_NON_PROXY_HOSTS = "nonProxyHosts";
103
    public static final String PARAM_REPLACE = "replace";
104
    public static final String PARAM_LAZY = "lazy";
105
    public static final String PARAM_ENCODING = "encoding";
106
    public static final String PARAM_DETACHED = "detached";
107
    public static final String PARAM_FORMAT = "format";
108
    public static final String PARAM_VALUE = "value";
109

110
    private final Logger log = Logger.getLogger(getClass().getName());
1✔
111

112
    /** The name used to refer to a configuration parameter */
113
    private final String parameterName;
114

115
    /** The command to execute */
116
    private String command = "sign";
1✔
117

118
    private final KeyStoreBuilder ksparams;
119
    private String alias;
120
    private String tsaurl;
121
    private String tsmode;
122
    private int tsretries = -1;
1✔
123
    private int tsretrywait = -1;
1✔
124
    private String alg;
125
    private String name;
126
    private String url;
127
    private final ProxySettings proxySettings = new ProxySettings();
1✔
128
    private boolean replace;
129
    private boolean lazy;
130
    private Charset encoding;
131
    private boolean detached;
132
    private String format;
133
    private String value;
134

135
    private AuthenticodeSigner signer;
136

137
    public SignerHelper(String parameterName) {
1✔
138
        this.parameterName = parameterName;
1✔
139
        this.ksparams = new KeyStoreBuilder(parameterName);
1✔
140
    }
1✔
141

142
    public SignerHelper command(String command) {
143
        this.command = command;
1✔
144
        return this;
1✔
145
    }
146

147
    public SignerHelper keystore(String keystore) {
148
        ksparams.keystore(keystore);
1✔
149
        signer = null;
1✔
150
        return this;
1✔
151
    }
152

153
    public SignerHelper storepass(String storepass) {
154
        ksparams.storepass(storepass);
1✔
155
        signer = null;
1✔
156
        return this;
1✔
157
    }
158

159
    public SignerHelper storetype(String storetype) {
160
        ksparams.storetype(storetype);
1✔
161
        signer = null;
1✔
162
        return this;
1✔
163
    }
164

165
    public SignerHelper alias(String alias) {
166
        this.alias = alias;
1✔
167
        signer = null;
1✔
168
        return this;
1✔
169
    }
170

171
    public SignerHelper keypass(String keypass) {
172
        ksparams.keypass(keypass);
1✔
173
        signer = null;
1✔
174
        return this;
1✔
175
    }
176

177
    public SignerHelper keyfile(String keyfile) {
178
        ksparams.keyfile(keyfile);
1✔
179
        signer = null;
1✔
180
        return this;
1✔
181
    }
182

183
    public SignerHelper keyfile(File keyfile) {
184
        ksparams.keyfile(keyfile);
1✔
185
        signer = null;
1✔
186
        return this;
1✔
187
    }
188

189
    public SignerHelper certfile(String certfile) {
190
        ksparams.certfile(certfile);
1✔
191
        signer = null;
1✔
192
        return this;
1✔
193
    }
194

195
    public SignerHelper certfile(File certfile) {
196
        ksparams.certfile(certfile);
1✔
197
        signer = null;
1✔
198
        return this;
1✔
199
    }
200

201
    public SignerHelper alg(String alg) {
202
        this.alg = alg;
1✔
203
        signer = null;
1✔
204
        return this;
1✔
205
    }
206

207
    public SignerHelper tsaurl(String tsaurl) {
208
        this.tsaurl = tsaurl;
1✔
209
        signer = null;
1✔
210
        return this;
1✔
211
    }
212

213
    public SignerHelper tsmode(String tsmode) {
214
        this.tsmode = tsmode;
1✔
215
        signer = null;
1✔
216
        return this;
1✔
217
    }
218

219
    public SignerHelper tsretries(int tsretries) {
220
        this.tsretries = tsretries;
1✔
221
        signer = null;
1✔
222
        return this;
1✔
223
    }
224

225
    public SignerHelper tsretrywait(int tsretrywait) {
226
        this.tsretrywait = tsretrywait;
1✔
227
        signer = null;
1✔
228
        return this;
1✔
229
    }
230

231
    public SignerHelper name(String name) {
232
        this.name = name;
1✔
233
        signer = null;
1✔
234
        return this;
1✔
235
    }
236

237
    public SignerHelper url(String url) {
238
        this.url = url;
1✔
239
        signer = null;
1✔
240
        return this;
1✔
241
    }
242

243
    public SignerHelper proxyUrl(String proxyUrl) {
244
        this.proxySettings.url = proxyUrl;
1✔
245
        signer = null;
1✔
246
        return this;
1✔
247
    }
248

249
    public SignerHelper proxyUser(String proxyUser) {
250
        this.proxySettings.username = proxyUser;
1✔
251
        signer = null;
1✔
252
        return this;
1✔
253
    }
254

255
    public SignerHelper proxyPass(String proxyPass) {
256
        this.proxySettings.password = proxyPass;
1✔
257
        signer = null;
1✔
258
        return this;
1✔
259
    }
260

261
    public SignerHelper nonProxyHosts(String nonProxyHosts) {
262
        this.proxySettings.nonProxyHosts = nonProxyHosts;
1✔
263
        signer = null;
1✔
264
        return this;
1✔
265
    }
266

267
    public SignerHelper replace(boolean replace) {
268
        this.replace = replace;
1✔
269
        signer = null;
1✔
270
        return this;
1✔
271
    }
272

273
    public SignerHelper lazy(boolean lazy) {
274
        this.lazy = lazy;
1✔
275
        return this;
1✔
276
    }
277

278
    public SignerHelper encoding(String encoding) {
279
        this.encoding = Charset.forName(encoding);
1✔
280
        return this;
1✔
281
    }
282

283
    public SignerHelper detached(boolean detached) {
284
        this.detached = detached;
1✔
285
        return this;
1✔
286
    }
287

288
    public SignerHelper format(String format) {
289
        this.format = format;
1✔
290
        return this;
1✔
291
    }
292

293
    public SignerHelper value(String value) {
294
        this.value = value;
1✔
295
        return this;
1✔
296
    }
297

298
    public SignerHelper param(String key, String value) {
299
        if (value == null) {
1✔
300
            return this;
1✔
301
        }
302
        
303
        switch (key) {
1✔
304
            case PARAM_COMMAND:    return command(value);
×
305
            case PARAM_KEYSTORE:   return keystore(value);
1✔
306
            case PARAM_STOREPASS:  return storepass(value);
1✔
307
            case PARAM_STORETYPE:  return storetype(value);
1✔
308
            case PARAM_ALIAS:      return alias(value);
1✔
309
            case PARAM_KEYPASS:    return keypass(value);
1✔
310
            case PARAM_KEYFILE:    return keyfile(value);
1✔
311
            case PARAM_CERTFILE:   return certfile(value);
1✔
312
            case PARAM_ALG:        return alg(value);
1✔
313
            case PARAM_TSAURL:     return tsaurl(value);
1✔
314
            case PARAM_TSMODE:     return tsmode(value);
1✔
315
            case PARAM_TSRETRIES:  return tsretries(Integer.parseInt(value));
1✔
316
            case PARAM_TSRETRY_WAIT: return tsretrywait(Integer.parseInt(value));
1✔
317
            case PARAM_NAME:       return name(value);
1✔
318
            case PARAM_URL:        return url(value);
1✔
319
            case PARAM_PROXY_URL:  return proxyUrl(value);
1✔
320
            case PARAM_PROXY_USER: return proxyUser(value);
1✔
321
            case PARAM_PROXY_PASS: return proxyPass(value);
1✔
322
            case PARAM_NON_PROXY_HOSTS: return nonProxyHosts(value);
1✔
323
            case PARAM_REPLACE:    return replace("true".equalsIgnoreCase(value));
×
324
            case PARAM_LAZY:       return lazy("true".equalsIgnoreCase(value));
×
325
            case PARAM_ENCODING:   return encoding(value);
1✔
326
            case PARAM_DETACHED:   return detached("true".equalsIgnoreCase(value));
×
327
            case PARAM_FORMAT:     return format(value);
×
328
            case PARAM_VALUE:      return value(value);
1✔
329
            default:
330
                throw new IllegalArgumentException("Unknown " + parameterName + ": " + key);
×
331
        }
332
    }
333

334
    void setBaseDir(File basedir) {
335
        ksparams.setBaseDir(basedir);
1✔
336
    }
1✔
337

338
    public void execute(String file) throws SignerException {
339
        execute(ksparams.createFile(file));
×
340
    }
×
341

342
    public void execute(File file) throws SignerException {
343
        switch (command) {
1✔
344
            case "sign":
345
                sign(file);
1✔
346
                break;
1✔
347
            case "timestamp":
348
                timestamp(file);
1✔
349
                break;
1✔
350
            case "extract":
351
                extract(file);
1✔
352
                break;
1✔
353
            case "remove":
354
                remove(file);
1✔
355
                break;
1✔
356
            case "show":
357
                show(file);
1✔
358
                break;
1✔
359
            case "tag":
360
                tag(file);
1✔
361
                break;
1✔
362
            default:
363
                throw new SignerException("Unknown command '" + command + "'");
1✔
364
        }
365
    }
1✔
366

367
    private AuthenticodeSigner build() throws SignerException {
368
        try {
369
            proxySettings.initializeProxy();
1✔
370
        } catch (Exception e) {
×
371
            throw new SignerException("Couldn't initialize proxy", e);
×
372
        }
1✔
373

374
        KeyStore ks;
375
        try {
376
            ks = ksparams.build();
1✔
377
        } catch (KeyStoreException e) {
1✔
378
            throw new SignerException("Failed to load the keystore " + (ksparams.keystore() != null ? ksparams.keystore() : ""), e);
1✔
379
        }
1✔
380
        KeyStoreType storetype = ksparams.storetype();
1✔
381
        Provider provider = ksparams.provider();
1✔
382

383
        Set<String> aliases = null;
1✔
384
        if (alias == null) {
1✔
385
            // guess the alias if there is only one in the keystore
386
            try {
387
                aliases = storetype.getAliases(ks);
1✔
388
            } catch (KeyStoreException e) {
×
389
                throw new SignerException(e.getMessage(), e);
×
390
            }
1✔
391

392
            if (aliases.isEmpty()) {
1✔
393
                throw new SignerException("No certificate found in the keystore " + (provider != null ? provider.getName() : ksparams.keystore()));
×
394
            } else if (aliases.size() == 1) {
1✔
395
                alias = aliases.iterator().next();
1✔
396
            } else {
397
                throw new SignerException("alias " + parameterName + " must be set to select a certificate (available aliases: " + String.join(", ", aliases) + ")");
1✔
398
            }
399
        }
400

401
        Certificate[] chain;
402
        if (ksparams.certfile() != null) {
1✔
403
            // replace the certificate chain from the keystore with the complete chain from file
404
            try {
405
                chain = CertificateUtils.loadCertificateChain(ksparams.certfile());
1✔
406
            } catch (Exception e) {
×
407
                throw new SignerException("Failed to load the certificate from " + ksparams.certfile(), e);
×
408
            }
1✔
409
        } else {
410
            try {
411
                chain = ks.getCertificateChain(alias);
1✔
412
            } catch (KeyStoreException e) {
×
413
                throw new SignerException(e.getMessage(), e);
×
414
            }
1✔
415
            if (chain == null) {
1✔
416
                String message = "No certificate found under the alias '" + alias + "' in the keystore " + (provider != null ? provider.getName() : ksparams.keystore());
1✔
417
                if (aliases == null) {
1✔
418
                    try {
419
                        aliases = new LinkedHashSet<>(Collections.list(ks.aliases()));
1✔
420
                        if (aliases.isEmpty()) {
1✔
421
                            message = "No certificate found in the keystore " + (provider != null ? provider.getName() : ksparams.keystore());
1✔
422
                        } else if (aliases.contains(alias)) {
1✔
423
                            message = "The keystore password must be specified";
1✔
424
                        } else {
425
                            message += " (available aliases: " + String.join(", ", aliases) + ")";
1✔
426
                        }
427
                    } catch (KeyStoreException e) {
×
428
                        message += " (couldn't load the list of available aliases: " + e.getMessage() + ")";
×
429
                    }
1✔
430
                }
431
                throw new SignerException(message);
1✔
432
            }
433
        }
434

435
        String keypass = ksparams.keypass();
1✔
436
        char[] password = keypass != null ? keypass.toCharArray() : new char[0];
1✔
437

438
        PrivateKey privateKey;
439
        try {
440
            privateKey = (PrivateKey) ks.getKey(alias, password);
1✔
441
        } catch (Exception e) {
1✔
442
            throw new SignerException("Failed to retrieve the private key from the keystore", e);
1✔
443
        }
1✔
444

445
        if (alg != null && DigestAlgorithm.of(alg) == null) {
1✔
446
            throw new SignerException("The digest algorithm " + alg + " is not supported");
1✔
447
        }
448

449
        // enable timestamping with Azure Artifact Signing
450
        if (tsaurl == null && storetype == KeyStoreType.TRUSTEDSIGNING) {
1✔
451
            tsaurl = "http://timestamp.acs.microsoft.com/";
×
452
            tsmode = TimestampingMode.RFC3161.name();
×
453
            tsretries = 3;
×
454
        }
455
        
456
        // configure the signer
457
        return new AuthenticodeSigner(chain, privateKey)
1✔
458
                .withProgramName(name)
1✔
459
                .withProgramURL(url)
1✔
460
                .withDigestAlgorithm(DigestAlgorithm.of(alg))
1✔
461
                .withSignatureProvider(provider)
1✔
462
                .withSignaturesReplaced(replace)
1✔
463
                .withTimestamping(tsaurl != null || tsmode != null)
1✔
464
                .withTimestampingMode(tsmode != null ? TimestampingMode.of(tsmode) : TimestampingMode.AUTHENTICODE)
1✔
465
                .withTimestampingRetries(tsretries)
1✔
466
                .withTimestampingRetryWait(tsretrywait)
1✔
467
                .withTimestampingAuthority(tsaurl != null ? tsaurl.split(",") : null);
1✔
468
    }
469

470
    public void sign(String file) throws SignerException {
471
        sign(ksparams.createFile(file));
×
472
    }
×
473

474
    public void sign(File file) throws SignerException {
475
        if (file == null) {
1✔
476
            throw new SignerException("No file specified");
1✔
477
        }
478
        if (!file.exists()) {
1✔
479
            throw new SignerException("The file " + file + " couldn't be found");
1✔
480
        }
481
        
482
        try (Signable signable = Signable.of(file, encoding)) {
1✔
483
            File detachedSignature = getDetachedSignature(file);
1✔
484
            if (detached && detachedSignature.exists()) {
1✔
485
                try {
486
                    log.info("Attaching Authenticode signature to " + file);
1✔
487
                    attach(signable, detachedSignature);
1✔
488
                } catch (Exception e) {
×
489
                    throw new SignerException("Couldn't attach the signature to " + file, e);
×
490
                }
1✔
491

492
            } else {
493
                if (lazy && !signable.getSignatures().isEmpty()) {
1✔
494
                    log.info("Skipping already signed file " + file);
1✔
495
                    return;
1✔
496
                }
497

498
                if (signer == null) {
1✔
499
                    signer = build();
1✔
500
                }
501

502
                log.info("Adding Authenticode signature to " + file);
1✔
503
                signer.sign(signable);
1✔
504

505
                if (detached) {
1✔
506
                    detach(signable, detachedSignature);
1✔
507
                }
508
            }
509

510
        } catch (UnsupportedOperationException | IllegalArgumentException e) {
1✔
511
            throw new SignerException(e.getMessage(), e);
1✔
512
        } catch (SignerException e) {
1✔
513
            throw e;
1✔
514
        } catch (Exception e) {
1✔
515
            throw new SignerException("Couldn't sign " + file, e);
1✔
516
        }
1✔
517
    }
1✔
518

519
    private void attach(Signable signable, File detachedSignature) throws IOException {
520
        byte[] signatureBytes = Files.readAllBytes(detachedSignature.toPath());
1✔
521
        signable.setSignatures(SignatureUtils.getSignatures(signatureBytes));
1✔
522
        signable.save();
1✔
523
        // todo warn if the hashes don't match
524
    }
1✔
525

526
    private void detach(Signable signable, File detachedSignature) throws IOException {
527
        List<CMSSignedData> signatures = signable.getSignatures();
1✔
528

529
        // ensure the secondary signatures are nested in the first one (for EFI files)
530
        CMSSignedData signedData = signatures.get(0);
1✔
531
        if (signatures.size() > 1) {
1✔
532
            List<CMSSignedData> nestedSignatures = signatures.subList(1, signatures.size());
1✔
533
            signedData = SignatureUtils.addNestedSignature(signedData, true, nestedSignatures.toArray(new CMSSignedData[0]));
1✔
534
        }
535

536
        byte[] content = signedData.toASN1Structure().getEncoded("DER");
1✔
537
        if (format == null || "DER".equalsIgnoreCase(format)) {
1✔
538
            Files.write(detachedSignature.toPath(), content);
1✔
539
        } else if ("PEM".equalsIgnoreCase(format)) {
1✔
540
            try (FileWriter out = new FileWriter(detachedSignature)) {
1✔
541
                String encoded = Base64.getEncoder().encodeToString(content);
1✔
542
                out.write("-----BEGIN PKCS7-----\n");
1✔
543
                for (int i = 0; i < encoded.length(); i += 64) {
1✔
544
                    out.write(encoded.substring(i, Math.min(i + 64, encoded.length())));
1✔
545
                    out.write('\n');
1✔
546
                }
547
                out.write("-----END PKCS7-----\n");
1✔
548
            }
549
        } else {
550
            throw new IllegalArgumentException("Unknown output format '" + format + "'");
1✔
551
        }
552
    }
1✔
553

554
    private File getDetachedSignature(File file) {
555
        return new File(file.getParentFile(), file.getName() + ".sig");
1✔
556
    }
557

558
    private void extract(File file) throws SignerException {
559
        if (!file.exists()) {
1✔
560
            throw new SignerException("Couldn't find " + file);
1✔
561
        }
562

563
        try (Signable signable = Signable.of(file)) {
1✔
564
            List<CMSSignedData> signatures = signable.getSignatures();
1✔
565
            if (signatures.isEmpty()) {
1✔
566
                throw new SignerException("No signature found in " + file);
1✔
567
            }
568

569
            File detachedSignature = getDetachedSignature(file);
1✔
570
            if ("PEM".equalsIgnoreCase(format)) {
1✔
571
                detachedSignature = new File(detachedSignature.getParentFile(), detachedSignature.getName() + ".pem");
1✔
572
            }
573
            log.info("Extracting signature to " + detachedSignature);
1✔
574
            detach(signable, detachedSignature);
1✔
575
        } catch (UnsupportedOperationException | IllegalArgumentException e) {
1✔
576
            throw new SignerException(e.getMessage(), e);
1✔
577
        } catch (SignerException e) {
1✔
578
            throw e;
1✔
579
        } catch (Exception e) {
×
580
            throw new SignerException("Couldn't extract the signature from " + file, e);
×
581
        }
1✔
582
    }
1✔
583

584
    private void remove(File file) throws SignerException {
585
        if (!file.exists()) {
1✔
586
            throw new SignerException("Couldn't find " + file);
1✔
587
        }
588

589
        try (Signable signable = Signable.of(file)) {
1✔
590
            List<CMSSignedData> signatures = signable.getSignatures();
1✔
591
            if (signatures.isEmpty()) {
1✔
592
                log.severe("No signature found in " + file);
1✔
593
                return;
1✔
594
            }
595

596
            DigestAlgorithm removedAlgorithm = alg != null ? DigestAlgorithm.of(alg) : null;
1✔
597
            if (alg != null && removedAlgorithm == null) {
1✔
598
                throw new SignerException("The digest algorithm " + alg + " is not supported");
1✔
599
            }
600

601
            int signatureCount = signatures.size();
1✔
602
            
603
            signatures.removeIf(new AndPredicate<>(
1✔
604
                    signature -> alg == null || signature.getSignerInfos().iterator().next().getDigestAlgOID().equals(removedAlgorithm.oid.getId()),
1✔
605
                    signature -> {
606
                        SignerInformation signer = signature.getSignerInfos().iterator().next();
1✔
607
                        X509CertificateHolder cert = (X509CertificateHolder) signature.getCertificates().getMatches(signer.getSID()).iterator().next();
1✔
608
                        return name == null || formatName(cert.getSubject(), false).toLowerCase().contains(name.toLowerCase());
1✔
609
                    }));
610

611
            if (signatures.size() == signatureCount) {
1✔
612
                List<String> criteria = new ArrayList<>();
1✔
613
                if (name != null) {
1✔
614
                    criteria.add("the name '" + name + "'");
1✔
615
                }
616
                if (alg != null) {
1✔
617
                    criteria.add("the digest algorithm " + alg);
1✔
618
                }
619

620
                log.info("No signature matching " + String.join(" and ", criteria) + " found in " + file);
1✔
621
                return;
1✔
622
            }
623

624
            int removedCount = signatureCount - signatures.size();
1✔
625
            log.info("Removing " + removedCount + " signature" + (removedCount > 1 ? "s" : "") + " from " + file);
1✔
626

627
            signable.setSignatures(signatures);
1✔
628
            signable.save();
1✔
629
        } catch (UnsupportedOperationException | IllegalArgumentException e) {
1✔
630
            throw new SignerException(e.getMessage(), e);
×
631
        } catch (Exception e) {
1✔
632
            throw new SignerException("Couldn't remove the signature from " + file, e);
1✔
633
        }
1✔
634
    }
1✔
635

636
    private void show(File file) throws SignerException {
637
        if (!file.exists()) {
1✔
638
            throw new SignerException("Couldn't find " + file);
×
639
        }
640

641
        AnsiFormatter ansiFormatter = new AnsiFormatter();
1✔
642
        log.setFilter(record -> {
1✔
643
            record.setMessage(ansiFormatter.format(record.getMessage()));
1✔
644
            return true;
1✔
645
        });
646

647
        try (Signable signable = Signable.of(file)) {
1✔
648
            boolean verbose = log.isLoggable(Level.FINE);
1✔
649

650
            List<CMSSignedData> signatures = signable.getSignatures();
1✔
651
            if (signatures.isEmpty()) {
1✔
652
                log.info("No signature found in " + (verbose ? file.getAbsolutePath() : file.getName()));
×
653
                return;
×
654
            }
655

656
            log.info("Signature" + (signatures.size() > 1 ? "s" : "") + " of " + (verbose ? file.getAbsolutePath() : file.getName()) + ":");
1✔
657
            log.info("");
1✔
658

659
            DateFormat datetimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
1✔
660
            DateFormat dateFormat = verbose ? datetimeFormat : new SimpleDateFormat("yyyy-MM-dd");
1✔
661

662
            for (int i = 0; i < signatures.size(); i++) {
1✔
663
                CMSSignedData signature = signatures.get(i);
1✔
664
                SignerInformation signer = signature.getSignerInfos().iterator().next();
1✔
665
                DigestInfo digestInfo = SignatureUtils.getDigestInfo(signature);
1✔
666
                X509CertificateHolder cert = (X509CertificateHolder) signature.getCertificates().getMatches(signer.getSID()).iterator().next();
1✔
667

668
                boolean expired = cert.getNotAfter().before(new Date());
1✔
669
                long daysLeft = Duration.between(Instant.now(), cert.getNotAfter().toInstant()).toDays();
1✔
670

671
                if (signatures.size() > 1) {
1✔
672
                    log.info("Signature #" + (i + 1));
×
673
                }
674
                if (digestInfo != null) {
1✔
675
                    DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(signer.getDigestAlgorithmID().getAlgorithm());
1✔
676
                    byte[] computedDigest = signable.computeDigest(digestAlgorithm);
1✔
677
                    boolean matches = Arrays.equals(computedDigest, digestInfo.getDigest());
1✔
678
                    log.info("  <b>Digest:</b>          (" + digestAlgorithm.id + ") " + Hex.toHexString(digestInfo.getDigest()) + (matches ? " (<green>matches</green>)" : " (<red>mismatches</red>)"));
1✔
679
                    if (!matches) {
1✔
680
                        log.info("  <b>Expected Digest:</b> (" + digestAlgorithm.id + ") " + Hex.toHexString(computedDigest));
×
681
                    }
682
                }
683

684
                Date timestamp = SignatureUtils.getTimestampDate(signature);
1✔
685
                if (timestamp != null) {
1✔
686
                    X509CertificateHolder timestampCertificate = SignatureUtils.getTimestampCertificate(signature);
×
687
                    log.info("  <b>Timestamp:</b>       " + datetimeFormat.format(timestamp) + " (" + formatName(timestampCertificate.getSubject(), verbose) + ")");
×
688
                }
689

690
                SpcSpOpusInfo spOpusInfo = SignatureUtils.getSpcSpOpusInfo(signature);
1✔
691
                if (spOpusInfo != null) {
1✔
692
                    if (spOpusInfo.getProgramName() != null && !spOpusInfo.getProgramName().trim().isEmpty()) {
1✔
693
                        log.info("  <b>Program Name:</b>    " + spOpusInfo.getProgramName());
1✔
694
                    }
695
                    SpcLink moreInfo = spOpusInfo.getMoreInfo();
1✔
696
                    if (moreInfo != null && moreInfo.getUrl() != null && !moreInfo.getUrl().trim().isEmpty()) {
1✔
697
                        log.info("  <b>URL:</b>             " + moreInfo.getUrl());
1✔
698
                    }
699
                }
700

701
                String tag = formatTag(SignatureUtils.getTag(signature));
1✔
702
                if (tag != null) {
1✔
703
                    log.info("  <b>Tag:</b>             " + tag.trim());
1✔
704
                }
705

706
                log.info("  <b>Certificate</b>");
1✔
707
                log.info("    <b>Subject:</b>       " + formatName(cert.getSubject(), verbose));
1✔
708
                log.info("    <b>Issuer:</b>        " + formatName(cert.getIssuer(), verbose));
1✔
709
                log.info("    <b>Key:</b>           " + getKeyAlgorithm(cert));
1✔
710
                log.info("    <b>Validity:</b>      " + dateFormat.format(cert.getNotBefore()) + " - " + dateFormat.format(cert.getNotAfter()) + " (" + (expired ? "expired" : daysLeft + " days left") + ")");
1✔
711
                log.info("    <b>Serial:</b>        " + String.format("%032x", cert.getSerialNumber()));
1✔
712
                log.info("");
1✔
713
            }
714
        } catch (Exception e) {
×
715
            throw new SignerException("Couldn't show the signatures of " + file, e);
×
716
        }
1✔
717
    }
1✔
718

719
    /**
720
     * Formats the X500 name:
721
     * <ul>
722
     *   <li>in normal mode, returns only the common name (CN)</li>
723
     *   <li>in verbose mode, returns the full name in LDAP order (starting with the common name)</li>
724
     * </ul>
725
     */
726
    private String formatName(X500Name name, boolean verbose) {
727
        if (verbose) {
1✔
728
            return new X500Name(new BCStyle() {
1✔
729
                public String toString(X500Name name) {
730
                    StringBuilder buf = new StringBuilder();
1✔
731
                    RDN[] rdns = name.getRDNs();
1✔
732
                    for (int i = rdns.length - 1; i >= 0; i--) {
1✔
733
                        if (i != rdns.length - 1) {
1✔
734
                            buf.append(", ");
×
735
                        }
736
                        IETFUtils.appendRDN(buf, rdns[i], defaultSymbols);
1✔
737
                    }
738
                    return buf.toString();
1✔
739
                }
740
            }, name.getRDNs()).toString().replaceAll("\\\\,", ",");
1✔
741
        } else {
742
            return name.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString();
1✔
743
        }
744
    }
745

746
    /**
747
     * Returns the algorithm of the public key of the certificate (for example "RSA 2048" or "EC 384").
748
     */
749
    private String getKeyAlgorithm(X509CertificateHolder certificate) throws IOException, CertificateException {
750
        X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(certificate.getEncoded()));
1✔
751
        PublicKey publicKey = cert.getPublicKey();
1✔
752

753
        if (publicKey instanceof RSAPublicKey) {
1✔
754
            return "RSA " + ((RSAPublicKey) publicKey).getModulus().bitLength();
1✔
755
        } else if (publicKey instanceof ECPublicKey) {
×
756
            return "EC " + (((ECPublicKey) publicKey).getParams()).getCurve().getField().getFieldSize();
×
757
        } else {
758
            return publicKey.getAlgorithm();
×
759
        }
760
    }
761

762
    /**
763
     * Formats the value of the unsigned tag.
764
     */
765
    static String formatTag(ASN1Encodable value) {
766
        if (value != null) {
1✔
767
            if (value instanceof ASN1UTF8String) {
1✔
768
                return ((ASN1UTF8String) value).getString();
1✔
769
            }
770

771
            if (value instanceof DEROctetString) {
×
772
                int limit = 100;
×
773
                byte[] bytes = ((DEROctetString) value).getOctets();
×
774
                return "(" + bytes.length + " bytes) " + new String(bytes).substring(0, limit) + (bytes.length > limit ? " ... (truncated)" : "");
×
775
            }
776
        }
777
        return null;
×
778
    }
779

780
    private void tag(File file) throws SignerException {
781
        if (!file.exists()) {
1✔
782
            throw new SignerException("Couldn't find " + file);
1✔
783
        }
784

785
        try (Signable signable = Signable.of(file)) {
1✔
786
            List<CMSSignedData> signatures = signable.getSignatures();
1✔
787
            if (signatures.isEmpty()) {
1✔
788
                throw new SignerException("No signature found in " + file);
1✔
789
            }
790

791
            log.info("Adding tag to " + file);
1✔
792
            signatures.set(0, SignatureUtils.addUnsignedAttribute(signatures.get(0), AuthenticodeObjectIdentifiers.JSIGN_UNSIGNED_DATA_OBJID, getTagValue()));
1✔
793
            signable.setSignatures(signatures);
1✔
794
            signable.save();
1✔
795
        } catch (SignerException e) {
1✔
796
            throw e;
1✔
797
        } catch (Exception e) {
1✔
798
            throw new SignerException("Couldn't modify the signature of " + file, e);
1✔
799
        }
1✔
800
    }
1✔
801

802
    private ASN1Encodable getTagValue() throws IOException {
803
        if (value == null) {
1✔
804
            byte[] array = new byte[1024];
1✔
805
            String begin = "-----BEGIN TAG-----";
1✔
806
            System.arraycopy(begin.getBytes(), 0, array, 0, begin.length());
1✔
807
            String end = "-----END TAG-----";
1✔
808
            System.arraycopy(end.getBytes(), 0, array, array.length - end.length(), end.length());
1✔
809
            return new DEROctetString(array);
1✔
810

811
        } else if (value.startsWith("0x")) {
1✔
812
            byte[] array = Hex.decode(value.substring(2));
1✔
813
            return new DEROctetString(array);
1✔
814

815
        } else if (value.startsWith("file:")) {
1✔
816
            byte[] array = Files.readAllBytes(new File(value.substring("file:".length())).toPath());
1✔
817
            return new DEROctetString(array);
1✔
818

819
        } else {
820
            return new DERUTF8String(value);
1✔
821
        }
822
    }
823

824
    private void timestamp(File file) throws SignerException {
825
        if (!file.exists()) {
1✔
826
            throw new SignerException("Couldn't find " + file);
1✔
827
        }
828

829
        try {
830
            proxySettings.initializeProxy();
1✔
831
        } catch (Exception e) {
×
832
            throw new SignerException("Couldn't initialize proxy", e);
×
833
        }
1✔
834

835
        try (Signable signable = Signable.of(file)) {
1✔
836
            if (signable.getSignatures().isEmpty()) {
1✔
837
                throw new SignerException("No signature found in " + file);
1✔
838
            }
839

840
            Timestamper timestamper = Timestamper.create(tsmode != null ? TimestampingMode.of(tsmode) : TimestampingMode.AUTHENTICODE);
1✔
841
            timestamper.setRetries(tsretries);
1✔
842
            timestamper.setRetryWait(tsretrywait);
1✔
843
            if (tsaurl != null) {
1✔
844
                timestamper.setURLs(tsaurl.split(","));
1✔
845
            }
846
            DigestAlgorithm digestAlgorithm = alg != null ? DigestAlgorithm.of(alg) : DigestAlgorithm.getDefault();
1✔
847

848
            List<CMSSignedData> signatures = new ArrayList<>();
1✔
849
            for (CMSSignedData signature : signable.getSignatures()) {
1✔
850
                SignerInformation signerInformation = signature.getSignerInfos().iterator().next();
1✔
851
                SignerId signerId = signerInformation.getSID();
1✔
852
                X509CertificateHolder certificate = (X509CertificateHolder) signature.getCertificates().getMatches(signerId).iterator().next();
1✔
853

854
                String digestAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(signerInformation.getDigestAlgorithmID()); 
1✔
855
                String keyAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(new ASN1ObjectIdentifier(signerInformation.getEncryptionAlgOID()));
1✔
856
                String name = digestAlgorithmName + "/" + keyAlgorithmName + " signature from '" + certificate.getSubject() + "'";
1✔
857

858
                if (SignatureUtils.isTimestamped(signature) && !replace) {
1✔
859
                    log.fine(name + " already timestamped");
1✔
860
                    signatures.add(signature);
1✔
861
                    continue;
1✔
862
                }
863

864
                boolean expired = certificate.getNotAfter().before(new Date());
1✔
865
                if (expired) {
1✔
866
                    log.fine(name + " is expired, skipping");
×
867
                    signatures.add(signature);
×
868
                    continue;
×
869
                }
870

871
                log.info("Adding timestamp to " + name);
1✔
872
                signature = SignatureUtils.removeTimestamp(signature);
1✔
873
                signature = timestamper.timestamp(digestAlgorithm, signature);
1✔
874

875
                signatures.add(signature);
1✔
876
            }
1✔
877

878
            signable.setSignatures(signatures);
1✔
879
            signable.save();
1✔
880
        } catch (IOException | CMSException e) {
×
881
            throw new SignerException("Couldn't timestamp " + file, e);
×
882
        }
1✔
883
    }
1✔
884
}
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