• 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

87.16
/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.bouncycastle.asn1.ASN1Encodable;
52
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
53
import org.bouncycastle.asn1.ASN1UTF8String;
54
import org.bouncycastle.asn1.DEROctetString;
55
import org.bouncycastle.asn1.DERUTF8String;
56
import org.bouncycastle.asn1.x500.RDN;
57
import org.bouncycastle.asn1.x500.X500Name;
58
import org.bouncycastle.asn1.x500.style.BCStyle;
59
import org.bouncycastle.asn1.x500.style.IETFUtils;
60
import org.bouncycastle.asn1.x509.DigestInfo;
61
import org.bouncycastle.cert.X509CertificateHolder;
62
import org.bouncycastle.cms.CMSException;
63
import org.bouncycastle.cms.CMSSignedData;
64
import org.bouncycastle.cms.SignerId;
65
import org.bouncycastle.cms.SignerInformation;
66
import org.bouncycastle.operator.DefaultAlgorithmNameFinder;
67
import org.bouncycastle.util.encoders.Hex;
68

69
import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers;
70
import net.jsign.timestamp.Timestamper;
71
import net.jsign.timestamp.TimestampingMode;
72

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

107
    private final Logger log = Logger.getLogger(getClass().getName());
1✔
108

109
    /** The name used to refer to a configuration parameter */
110
    private final String parameterName;
111

112
    /** The command to execute */
113
    private String command = "sign";
1✔
114

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

132
    private AuthenticodeSigner signer;
133

134
    public SignerHelper(String parameterName) {
1✔
135
        this.parameterName = parameterName;
1✔
136
        this.ksparams = new KeyStoreBuilder(parameterName);
1✔
137
    }
1✔
138

139
    public SignerHelper command(String command) {
140
        this.command = command;
1✔
141
        return this;
1✔
142
    }
143

144
    public SignerHelper keystore(String keystore) {
145
        ksparams.keystore(keystore);
1✔
146
        signer = null;
1✔
147
        return this;
1✔
148
    }
149

150
    public SignerHelper storepass(String storepass) {
151
        ksparams.storepass(storepass);
1✔
152
        signer = null;
1✔
153
        return this;
1✔
154
    }
155

156
    public SignerHelper storetype(String storetype) {
157
        ksparams.storetype(storetype);
1✔
158
        signer = null;
1✔
159
        return this;
1✔
160
    }
161

162
    public SignerHelper alias(String alias) {
163
        this.alias = alias;
1✔
164
        signer = null;
1✔
165
        return this;
1✔
166
    }
167

168
    public SignerHelper keypass(String keypass) {
169
        ksparams.keypass(keypass);
1✔
170
        signer = null;
1✔
171
        return this;
1✔
172
    }
173

174
    public SignerHelper keyfile(String keyfile) {
175
        ksparams.keyfile(keyfile);
1✔
176
        signer = null;
1✔
177
        return this;
1✔
178
    }
179

180
    public SignerHelper keyfile(File keyfile) {
181
        ksparams.keyfile(keyfile);
1✔
182
        signer = null;
1✔
183
        return this;
1✔
184
    }
185

186
    public SignerHelper certfile(String certfile) {
187
        ksparams.certfile(certfile);
1✔
188
        signer = null;
1✔
189
        return this;
1✔
190
    }
191

192
    public SignerHelper certfile(File certfile) {
193
        ksparams.certfile(certfile);
1✔
194
        signer = null;
1✔
195
        return this;
1✔
196
    }
197

198
    public SignerHelper alg(String alg) {
199
        this.alg = alg;
1✔
200
        signer = null;
1✔
201
        return this;
1✔
202
    }
203

204
    public SignerHelper tsaurl(String tsaurl) {
205
        this.tsaurl = tsaurl;
1✔
206
        signer = null;
1✔
207
        return this;
1✔
208
    }
209

210
    public SignerHelper tsmode(String tsmode) {
211
        this.tsmode = tsmode;
1✔
212
        signer = null;
1✔
213
        return this;
1✔
214
    }
215

216
    public SignerHelper tsretries(int tsretries) {
217
        this.tsretries = tsretries;
1✔
218
        signer = null;
1✔
219
        return this;
1✔
220
    }
221

222
    public SignerHelper tsretrywait(int tsretrywait) {
223
        this.tsretrywait = tsretrywait;
1✔
224
        signer = null;
1✔
225
        return this;
1✔
226
    }
227

228
    public SignerHelper name(String name) {
229
        this.name = name;
1✔
230
        signer = null;
1✔
231
        return this;
1✔
232
    }
233

234
    public SignerHelper url(String url) {
235
        this.url = url;
1✔
236
        signer = null;
1✔
237
        return this;
1✔
238
    }
239

240
    public SignerHelper proxyUrl(String proxyUrl) {
241
        this.proxySettings.url = proxyUrl;
1✔
242
        signer = null;
1✔
243
        return this;
1✔
244
    }
245

246
    public SignerHelper proxyUser(String proxyUser) {
247
        this.proxySettings.username = proxyUser;
1✔
248
        signer = null;
1✔
249
        return this;
1✔
250
    }
251

252
    public SignerHelper proxyPass(String proxyPass) {
253
        this.proxySettings.password = proxyPass;
1✔
254
        signer = null;
1✔
255
        return this;
1✔
256
    }
257

258
    public SignerHelper nonProxyHosts(String nonProxyHosts) {
259
        this.proxySettings.nonProxyHosts = nonProxyHosts;
1✔
260
        signer = null;
1✔
261
        return this;
1✔
262
    }
263

264
    public SignerHelper replace(boolean replace) {
265
        this.replace = replace;
1✔
266
        signer = null;
1✔
267
        return this;
1✔
268
    }
269

270
    public SignerHelper lazy(boolean lazy) {
271
        this.lazy = lazy;
1✔
272
        return this;
1✔
273
    }
274

275
    public SignerHelper encoding(String encoding) {
276
        this.encoding = Charset.forName(encoding);
1✔
277
        return this;
1✔
278
    }
279

280
    public SignerHelper detached(boolean detached) {
281
        this.detached = detached;
1✔
282
        return this;
1✔
283
    }
284

285
    public SignerHelper format(String format) {
286
        this.format = format;
1✔
287
        return this;
1✔
288
    }
289

290
    public SignerHelper value(String value) {
291
        this.value = value;
1✔
292
        return this;
1✔
293
    }
294

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

331
    void setBaseDir(File basedir) {
332
        ksparams.setBaseDir(basedir);
1✔
333
    }
1✔
334

335
    public void execute(String file) throws SignerException {
UNCOV
336
        execute(ksparams.createFile(file));
×
UNCOV
337
    }
×
338

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

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

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

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

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

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

432
        String keypass = ksparams.keypass();
1✔
433
        char[] password = keypass != null ? keypass.toCharArray() : new char[0];
1✔
434

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

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

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

467
    public void sign(String file) throws SignerException {
468
        sign(ksparams.createFile(file));
×
UNCOV
469
    }
×
470

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

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

495
                if (signer == null) {
1✔
496
                    signer = build();
1✔
497
                }
498

499
                log.info("Adding Authenticode signature to " + file);
1✔
500
                signer.sign(signable);
1✔
501

502
                if (detached) {
1✔
503
                    detach(signable, detachedSignature);
1✔
504
                }
505
            }
506

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

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

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

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

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

551
    private File getDetachedSignature(File file) {
552
        return new File(file.getParentFile(), file.getName() + ".sig");
1✔
553
    }
554

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

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

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

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

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

593
            log.info("Removing " + signatures.size() + " signature" + (signatures.size() > 1 ? "s" : "") + " from " + file);
1✔
594
            signable.setSignatures(null);
1✔
595
            signable.save();
1✔
596
        } catch (UnsupportedOperationException | IllegalArgumentException e) {
1✔
UNCOV
597
            throw new SignerException(e.getMessage(), e);
×
UNCOV
598
        } catch (Exception e) {
×
UNCOV
599
            throw new SignerException("Couldn't remove the signature from " + file, e);
×
600
        }
1✔
601
    }
1✔
602

603
    private void show(File file) throws SignerException {
604
        if (!file.exists()) {
1✔
UNCOV
605
            throw new SignerException("Couldn't find " + file);
×
606
        }
607

608
        AnsiFormatter ansiFormatter = new AnsiFormatter();
1✔
609
        log.setFilter(record -> {
1✔
610
            record.setMessage(ansiFormatter.format(record.getMessage()));
1✔
611
            return true;
1✔
612
        });
613

614
        try (Signable signable = Signable.of(file)) {
1✔
615
            boolean verbose = log.isLoggable(Level.FINE);
1✔
616

617
            List<CMSSignedData> signatures = signable.getSignatures();
1✔
618
            if (signatures.isEmpty()) {
1✔
UNCOV
619
                log.info("No signature found in " + (verbose ? file.getAbsolutePath() : file.getName()));
×
UNCOV
620
                return;
×
621
            }
622

623
            log.info("Signature" + (signatures.size() > 1 ? "s" : "") + " of " + (verbose ? file.getAbsolutePath() : file.getName()) + ":");
1✔
624
            log.info("");
1✔
625

626
            DateFormat datetimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
1✔
627
            DateFormat dateFormat = verbose ? datetimeFormat : new SimpleDateFormat("yyyy-MM-dd");
1✔
628

629
            for (int i = 0; i < signatures.size(); i++) {
1✔
630
                CMSSignedData signature = signatures.get(i);
1✔
631
                SignerInformation signer = signature.getSignerInfos().iterator().next();
1✔
632
                DigestInfo digestInfo = SignatureUtils.getDigestInfo(signature);
1✔
633
                X509CertificateHolder cert = (X509CertificateHolder) signature.getCertificates().getMatches(signer.getSID()).iterator().next();
1✔
634

635
                boolean expired = cert.getNotAfter().before(new Date());
1✔
636
                long daysLeft = Duration.between(Instant.now(), cert.getNotAfter().toInstant()).toDays();
1✔
637

638
                if (signatures.size() > 1) {
1✔
UNCOV
639
                    log.info("Signature #" + (i + 1));
×
640
                }
641
                if (digestInfo != null) {
1✔
642
                    DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(signer.getDigestAlgorithmID().getAlgorithm());
1✔
643
                    byte[] computedDigest = signable.computeDigest(digestAlgorithm);
1✔
644
                    boolean matches = Arrays.equals(computedDigest, digestInfo.getDigest());
1✔
645
                    log.info("  <b>Digest:</b>          (" + digestAlgorithm.id + ") " + Hex.toHexString(digestInfo.getDigest()) + (matches ? " (<green>matches</green>)" : " (<red>mismatches</red>)"));
1✔
646
                    if (!matches) {
1✔
UNCOV
647
                        log.info("  <b>Expected Digest:</b> (" + digestAlgorithm.id + ") " + Hex.toHexString(computedDigest));
×
648
                    }
649
                }
650

651
                Date timestamp = SignatureUtils.getTimestampDate(signature);
1✔
652
                if (timestamp != null) {
1✔
653
                    X509CertificateHolder timestampCertificate = SignatureUtils.getTimestampCertificate(signature);
×
UNCOV
654
                    log.info("  <b>Timestamp:</b>       " + datetimeFormat.format(timestamp) + " (" + formatName(timestampCertificate.getSubject(), verbose) + ")");
×
655
                }
656

657
                String tag = formatTag(SignatureUtils.getTag(signature));
1✔
658
                if (tag != null) {
1✔
659
                    log.info("  <b>Tag:</b>             " + tag.trim());
1✔
660
                }
661

662
                log.info("  <b>Certificate</b>");
1✔
663
                log.info("    <b>Subject:</b>       " + formatName(cert.getSubject(), verbose));
1✔
664
                log.info("    <b>Issuer:</b>        " + formatName(cert.getIssuer(), verbose));
1✔
665
                log.info("    <b>Key:</b>           " + getKeyAlgorithm(cert));
1✔
666
                log.info("    <b>Validity:</b>      " + dateFormat.format(cert.getNotBefore()) + " - " + dateFormat.format(cert.getNotAfter()) + " (" + (expired ? "expired" : daysLeft + " days left") + ")");
1✔
667
                log.info("    <b>Serial:</b>        " + String.format("%032x", cert.getSerialNumber()));
1✔
668
                log.info("");
1✔
669
            }
UNCOV
670
        } catch (Exception e) {
×
UNCOV
671
            throw new SignerException("Couldn't show the signatures of" + file, e);
×
672
        }
1✔
673
    }
1✔
674

675
    /**
676
     * Formats the X500 name:
677
     * <ul>
678
     *   <li>in normal mode, returns only the common name (CN)</li>
679
     *   <li>in verbose mode, returns the full name in LDAP order (starting with the common name)</li>
680
     * </ul>
681
     */
682
    private String formatName(X500Name name, boolean verbose) {
683
        if (verbose) {
1✔
684
            return new X500Name(new BCStyle() {
1✔
685
                public String toString(X500Name name) {
686
                    StringBuilder buf = new StringBuilder();
1✔
687
                    RDN[] rdns = name.getRDNs();
1✔
688
                    for (int i = rdns.length - 1; i >= 0; i--) {
1✔
689
                        if (i != rdns.length - 1) {
1✔
UNCOV
690
                            buf.append(", ");
×
691
                        }
692
                        IETFUtils.appendRDN(buf, rdns[i], defaultSymbols);
1✔
693
                    }
694
                    return buf.toString();
1✔
695
                }
696
            }, name.getRDNs()).toString().replaceAll("\\\\,", ",");
1✔
697
        } else {
698
            return name.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString();
1✔
699
        }
700
    }
701

702
    /**
703
     * Returns the algorithm of the public key of the certificate (for example "RSA 2048" or "EC 384").
704
     */
705
    private String getKeyAlgorithm(X509CertificateHolder certificate) throws IOException, CertificateException {
706
        X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(certificate.getEncoded()));
1✔
707
        PublicKey publicKey = cert.getPublicKey();
1✔
708

709
        if (publicKey instanceof RSAPublicKey) {
1✔
710
            return "RSA " + ((RSAPublicKey) publicKey).getModulus().bitLength();
1✔
UNCOV
711
        } else if (publicKey instanceof ECPublicKey) {
×
UNCOV
712
            return "EC " + (((ECPublicKey) publicKey).getParams()).getCurve().getField().getFieldSize();
×
713
        } else {
UNCOV
714
            return publicKey.getAlgorithm();
×
715
        }
716
    }
717

718
    /**
719
     * Formats the value of the unsigned tag.
720
     */
721
    static String formatTag(ASN1Encodable value) {
722
        if (value != null) {
1✔
723
            if (value instanceof ASN1UTF8String) {
1✔
724
                return ((ASN1UTF8String) value).getString();
1✔
725
            }
726

UNCOV
727
            if (value instanceof DEROctetString) {
×
UNCOV
728
                int limit = 100;
×
UNCOV
729
                byte[] bytes = ((DEROctetString) value).getOctets();
×
UNCOV
730
                return "(" + bytes.length + " bytes) " + new String(bytes).substring(0, limit) + (bytes.length > limit ? " ... (truncated)" : "");
×
731
            }
732
        }
UNCOV
733
        return null;
×
734
    }
735

736
    private void tag(File file) throws SignerException {
737
        if (!file.exists()) {
1✔
738
            throw new SignerException("Couldn't find " + file);
1✔
739
        }
740

741
        try (Signable signable = Signable.of(file)) {
1✔
742
            List<CMSSignedData> signatures = signable.getSignatures();
1✔
743
            if (signatures.isEmpty()) {
1✔
744
                throw new SignerException("No signature found in " + file);
1✔
745
            }
746

747
            log.info("Adding tag to " + file);
1✔
748
            signatures.set(0, SignatureUtils.addUnsignedAttribute(signatures.get(0), AuthenticodeObjectIdentifiers.JSIGN_UNSIGNED_DATA_OBJID, getTagValue()));
1✔
749
            signable.setSignatures(signatures);
1✔
750
            signable.save();
1✔
751
        } catch (SignerException e) {
1✔
752
            throw e;
1✔
753
        } catch (Exception e) {
1✔
754
            throw new SignerException("Couldn't modify the signature of " + file, e);
1✔
755
        }
1✔
756
    }
1✔
757

758
    private ASN1Encodable getTagValue() throws IOException {
759
        if (value == null) {
1✔
760
            byte[] array = new byte[1024];
1✔
761
            String begin = "-----BEGIN TAG-----";
1✔
762
            System.arraycopy(begin.getBytes(), 0, array, 0, begin.length());
1✔
763
            String end = "-----END TAG-----";
1✔
764
            System.arraycopy(end.getBytes(), 0, array, array.length - end.length(), end.length());
1✔
765
            return new DEROctetString(array);
1✔
766

767
        } else if (value.startsWith("0x")) {
1✔
768
            byte[] array = Hex.decode(value.substring(2));
1✔
769
            return new DEROctetString(array);
1✔
770

771
        } else if (value.startsWith("file:")) {
1✔
772
            byte[] array = Files.readAllBytes(new File(value.substring("file:".length())).toPath());
1✔
773
            return new DEROctetString(array);
1✔
774

775
        } else {
776
            return new DERUTF8String(value);
1✔
777
        }
778
    }
779

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

785
        try {
786
            proxySettings.initializeProxy();
1✔
UNCOV
787
        } catch (Exception e) {
×
UNCOV
788
            throw new SignerException("Couldn't initialize proxy", e);
×
789
        }
1✔
790

791
        try (Signable signable = Signable.of(file)) {
1✔
792
            if (signable.getSignatures().isEmpty()) {
1✔
793
                throw new SignerException("No signature found in " + file);
1✔
794
            }
795

796
            Timestamper timestamper = Timestamper.create(tsmode != null ? TimestampingMode.of(tsmode) : TimestampingMode.AUTHENTICODE);
1✔
797
            timestamper.setRetries(tsretries);
1✔
798
            timestamper.setRetryWait(tsretrywait);
1✔
799
            if (tsaurl != null) {
1✔
800
                timestamper.setURLs(tsaurl.split(","));
1✔
801
            }
802
            DigestAlgorithm digestAlgorithm = alg != null ? DigestAlgorithm.of(alg) : DigestAlgorithm.getDefault();
1✔
803

804
            List<CMSSignedData> signatures = new ArrayList<>();
1✔
805
            for (CMSSignedData signature : signable.getSignatures()) {
1✔
806
                SignerInformation signerInformation = signature.getSignerInfos().iterator().next();
1✔
807
                SignerId signerId = signerInformation.getSID();
1✔
808
                X509CertificateHolder certificate = (X509CertificateHolder) signature.getCertificates().getMatches(signerId).iterator().next();
1✔
809

810
                String digestAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(signerInformation.getDigestAlgorithmID()); 
1✔
811
                String keyAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(new ASN1ObjectIdentifier(signerInformation.getEncryptionAlgOID()));
1✔
812
                String name = digestAlgorithmName + "/" + keyAlgorithmName + " signature from '" + certificate.getSubject() + "'";
1✔
813

814
                if (SignatureUtils.isTimestamped(signature) && !replace) {
1✔
815
                    log.fine(name + " already timestamped");
1✔
816
                    signatures.add(signature);
1✔
817
                    continue;
1✔
818
                }
819

820
                boolean expired = certificate.getNotAfter().before(new Date());
1✔
821
                if (expired) {
1✔
UNCOV
822
                    log.fine(name + " is expired, skipping");
×
UNCOV
823
                    signatures.add(signature);
×
UNCOV
824
                    continue;
×
825
                }
826

827
                log.info("Adding timestamp to " + name);
1✔
828
                signature = SignatureUtils.removeTimestamp(signature);
1✔
829
                signature = timestamper.timestamp(digestAlgorithm, signature);
1✔
830

831
                signatures.add(signature);
1✔
832
            }
1✔
833

834
            signable.setSignatures(signatures);
1✔
835
            signable.save();
1✔
UNCOV
836
        } catch (IOException | CMSException e) {
×
UNCOV
837
            throw new SignerException("Couldn't timestamp " + file, e);
×
838
        }
1✔
839
    }
1✔
840
}
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