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

ebourg / jsign / #400

06 Jul 2026 05:44PM UTC coverage: 80.842% (-0.03%) from 80.872%
#400

push

ebourg
Fixed opensc-pkcs11.so lookup on LD_LIBRARY_PATH

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

50 existing lines in 4 files now uncovered.

5068 of 6269 relevant lines covered (80.84%)

0.81 hits per line

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

89.59
/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.File;
20
import java.io.FileWriter;
21
import java.io.IOException;
22
import java.nio.charset.Charset;
23
import java.nio.file.Files;
24
import java.security.KeyStore;
25
import java.security.KeyStoreException;
26
import java.security.PrivateKey;
27
import java.security.Provider;
28
import java.security.cert.Certificate;
29
import java.util.ArrayList;
30
import java.util.Base64;
31
import java.util.Collection;
32
import java.util.Collections;
33
import java.util.Date;
34
import java.util.LinkedHashSet;
35
import java.util.List;
36
import java.util.Set;
37
import java.util.logging.Logger;
38

39
import org.bouncycastle.asn1.ASN1Encodable;
40
import org.bouncycastle.asn1.ASN1InputStream;
41
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
42
import org.bouncycastle.asn1.DEROctetString;
43
import org.bouncycastle.asn1.DERSet;
44
import org.bouncycastle.asn1.DERUTF8String;
45
import org.bouncycastle.asn1.cms.AttributeTable;
46
import org.bouncycastle.asn1.cms.ContentInfo;
47
import org.bouncycastle.cert.X509CertificateHolder;
48
import org.bouncycastle.cms.CMSException;
49
import org.bouncycastle.cms.CMSProcessable;
50
import org.bouncycastle.cms.CMSSignedData;
51
import org.bouncycastle.cms.SignerId;
52
import org.bouncycastle.cms.SignerInformation;
53
import org.bouncycastle.cms.SignerInformationStore;
54
import org.bouncycastle.operator.DefaultAlgorithmNameFinder;
55
import org.bouncycastle.util.encoders.Hex;
56

57
import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers;
58
import net.jsign.timestamp.Timestamper;
59
import net.jsign.timestamp.TimestampingMode;
60

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

94
    private final Logger log = Logger.getLogger(getClass().getName());
1✔
95

96
    /** The name used to refer to a configuration parameter */
97
    private final String parameterName;
98

99
    /** The command to execute */
100
    private String command = "sign";
1✔
101

102
    private final KeyStoreBuilder ksparams;
103
    private String alias;
104
    private String tsaurl;
105
    private String tsmode;
106
    private int tsretries = -1;
1✔
107
    private int tsretrywait = -1;
1✔
108
    private String alg;
109
    private String name;
110
    private String url;
111
    private final ProxySettings proxySettings = new ProxySettings();
1✔
112
    private boolean replace;
113
    private boolean lazy;
114
    private Charset encoding;
115
    private boolean detached;
116
    private String format;
117
    private String value;
118

119
    private AuthenticodeSigner signer;
120

121
    public SignerHelper(String parameterName) {
1✔
122
        this.parameterName = parameterName;
1✔
123
        this.ksparams = new KeyStoreBuilder(parameterName);
1✔
124
    }
1✔
125

126
    public SignerHelper command(String command) {
127
        this.command = command;
1✔
128
        return this;
1✔
129
    }
130

131
    public SignerHelper keystore(String keystore) {
132
        ksparams.keystore(keystore);
1✔
133
        signer = null;
1✔
134
        return this;
1✔
135
    }
136

137
    public SignerHelper storepass(String storepass) {
138
        ksparams.storepass(storepass);
1✔
139
        signer = null;
1✔
140
        return this;
1✔
141
    }
142

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

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

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

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

167
    public SignerHelper keyfile(File keyfile) {
168
        ksparams.keyfile(keyfile);
1✔
169
        signer = null;
1✔
170
        return this;
1✔
171
    }
172

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

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

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

191
    public SignerHelper tsaurl(String tsaurl) {
192
        this.tsaurl = tsaurl;
1✔
193
        signer = null;
1✔
194
        return this;
1✔
195
    }
196

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

203
    public SignerHelper tsretries(int tsretries) {
204
        this.tsretries = tsretries;
1✔
205
        signer = null;
1✔
206
        return this;
1✔
207
    }
208

209
    public SignerHelper tsretrywait(int tsretrywait) {
210
        this.tsretrywait = tsretrywait;
1✔
211
        signer = null;
1✔
212
        return this;
1✔
213
    }
214

215
    public SignerHelper name(String name) {
216
        this.name = name;
1✔
217
        signer = null;
1✔
218
        return this;
1✔
219
    }
220

221
    public SignerHelper url(String url) {
222
        this.url = url;
1✔
223
        signer = null;
1✔
224
        return this;
1✔
225
    }
226

227
    public SignerHelper proxyUrl(String proxyUrl) {
228
        this.proxySettings.url = proxyUrl;
1✔
229
        signer = null;
1✔
230
        return this;
1✔
231
    }
232

233
    public SignerHelper proxyUser(String proxyUser) {
234
        this.proxySettings.username = proxyUser;
1✔
235
        signer = null;
1✔
236
        return this;
1✔
237
    }
238

239
    public SignerHelper proxyPass(String proxyPass) {
240
        this.proxySettings.password = proxyPass;
1✔
241
        signer = null;
1✔
242
        return this;
1✔
243
    }
244

245
    public SignerHelper replace(boolean replace) {
246
        this.replace = replace;
1✔
247
        signer = null;
1✔
248
        return this;
1✔
249
    }
250

251
    public SignerHelper lazy(boolean lazy) {
252
        this.lazy = lazy;
1✔
253
        return this;
1✔
254
    }
255

256
    public SignerHelper encoding(String encoding) {
257
        this.encoding = Charset.forName(encoding);
1✔
258
        return this;
1✔
259
    }
260

261
    public SignerHelper detached(boolean detached) {
262
        this.detached = detached;
1✔
263
        return this;
1✔
264
    }
265

266
    public SignerHelper format(String format) {
267
        this.format = format;
1✔
268
        return this;
1✔
269
    }
270

271
    public SignerHelper value(String value) {
272
        this.value = value;
1✔
273
        return this;
1✔
274
    }
275

276
    public SignerHelper param(String key, String value) {
277
        if (value == null) {
1✔
278
            return this;
1✔
279
        }
280
        
281
        switch (key) {
1✔
UNCOV
282
            case PARAM_COMMAND:    return command(value);
×
283
            case PARAM_KEYSTORE:   return keystore(value);
1✔
284
            case PARAM_STOREPASS:  return storepass(value);
1✔
285
            case PARAM_STORETYPE:  return storetype(value);
1✔
286
            case PARAM_ALIAS:      return alias(value);
1✔
287
            case PARAM_KEYPASS:    return keypass(value);
1✔
288
            case PARAM_KEYFILE:    return keyfile(value);
1✔
289
            case PARAM_CERTFILE:   return certfile(value);
1✔
290
            case PARAM_ALG:        return alg(value);
1✔
291
            case PARAM_TSAURL:     return tsaurl(value);
1✔
292
            case PARAM_TSMODE:     return tsmode(value);
1✔
293
            case PARAM_TSRETRIES:  return tsretries(Integer.parseInt(value));
1✔
294
            case PARAM_TSRETRY_WAIT: return tsretrywait(Integer.parseInt(value));
1✔
295
            case PARAM_NAME:       return name(value);
1✔
296
            case PARAM_URL:        return url(value);
1✔
297
            case PARAM_PROXY_URL:  return proxyUrl(value);
1✔
298
            case PARAM_PROXY_USER: return proxyUser(value);
1✔
299
            case PARAM_PROXY_PASS: return proxyPass(value);
1✔
UNCOV
300
            case PARAM_REPLACE:    return replace("true".equalsIgnoreCase(value));
×
UNCOV
301
            case PARAM_LAZY:       return lazy("true".equalsIgnoreCase(value));
×
302
            case PARAM_ENCODING:   return encoding(value);
1✔
UNCOV
303
            case PARAM_DETACHED:   return detached("true".equalsIgnoreCase(value));
×
UNCOV
304
            case PARAM_FORMAT:     return format(value);
×
305
            case PARAM_VALUE:      return value(value);
1✔
306
            default:
UNCOV
307
                throw new IllegalArgumentException("Unknown " + parameterName + ": " + key);
×
308
        }
309
    }
310

311
    void setBaseDir(File basedir) {
312
        ksparams.setBaseDir(basedir);
1✔
313
    }
1✔
314

315
    public void execute(String file) throws SignerException {
UNCOV
316
        execute(ksparams.createFile(file));
×
UNCOV
317
    }
×
318

319
    public void execute(File file) throws SignerException {
320
        switch (command) {
1✔
321
            case "sign":
322
                sign(file);
1✔
323
                break;
1✔
324
            case "timestamp":
325
                timestamp(file);
1✔
326
                break;
1✔
327
            case "extract":
328
                extract(file);
1✔
329
                break;
1✔
330
            case "remove":
331
                remove(file);
1✔
332
                break;
1✔
333
            case "tag":
334
                tag(file);
1✔
335
                break;
1✔
336
            default:
337
                throw new SignerException("Unknown command '" + command + "'");
1✔
338
        }
339
    }
1✔
340

341
    private AuthenticodeSigner build() throws SignerException {
342
        try {
343
            proxySettings.initializeProxy();
1✔
UNCOV
344
        } catch (Exception e) {
×
UNCOV
345
            throw new SignerException("Couldn't initialize proxy", e);
×
346
        }
1✔
347

348
        KeyStore ks;
349
        try {
350
            ks = ksparams.build();
1✔
351
        } catch (KeyStoreException e) {
1✔
352
            throw new SignerException("Failed to load the keystore " + (ksparams.keystore() != null ? ksparams.keystore() : ""), e);
1✔
353
        }
1✔
354
        KeyStoreType storetype = ksparams.storetype();
1✔
355
        Provider provider = ksparams.provider();
1✔
356

357
        Set<String> aliases = null;
1✔
358
        if (alias == null) {
1✔
359
            // guess the alias if there is only one in the keystore
360
            try {
361
                aliases = storetype.getAliases(ks);
1✔
UNCOV
362
            } catch (KeyStoreException e) {
×
UNCOV
363
                throw new SignerException(e.getMessage(), e);
×
364
            }
1✔
365

366
            if (aliases.isEmpty()) {
1✔
UNCOV
367
                throw new SignerException("No certificate found in the keystore " + (provider != null ? provider.getName() : ksparams.keystore()));
×
368
            } else if (aliases.size() == 1) {
1✔
369
                alias = aliases.iterator().next();
1✔
370
            } else {
371
                throw new SignerException("alias " + parameterName + " must be set to select a certificate (available aliases: " + String.join(", ", aliases) + ")");
1✔
372
            }
373
        }
374

375
        Certificate[] chain;
376
        if (ksparams.certfile() != null) {
1✔
377
            // replace the certificate chain from the keystore with the complete chain from file
378
            try {
379
                chain = CertificateUtils.loadCertificateChain(ksparams.certfile());
1✔
UNCOV
380
            } catch (Exception e) {
×
UNCOV
381
                throw new SignerException("Failed to load the certificate from " + ksparams.certfile(), e);
×
382
            }
1✔
383
        } else {
384
            try {
385
                chain = ks.getCertificateChain(alias);
1✔
UNCOV
386
            } catch (KeyStoreException e) {
×
UNCOV
387
                throw new SignerException(e.getMessage(), e);
×
388
            }
1✔
389
            if (chain == null) {
1✔
390
                String message = "No certificate found under the alias '" + alias + "' in the keystore " + (provider != null ? provider.getName() : ksparams.keystore());
1✔
391
                if (aliases == null) {
1✔
392
                    try {
393
                        aliases = new LinkedHashSet<>(Collections.list(ks.aliases()));
1✔
394
                        if (aliases.isEmpty()) {
1✔
395
                            message = "No certificate found in the keystore " + (provider != null ? provider.getName() : ksparams.keystore());
1✔
396
                        } else if (aliases.contains(alias)) {
1✔
397
                            message = "The keystore password must be specified";
1✔
398
                        } else {
399
                            message += " (available aliases: " + String.join(", ", aliases) + ")";
1✔
400
                        }
UNCOV
401
                    } catch (KeyStoreException e) {
×
UNCOV
402
                        message += " (couldn't load the list of available aliases: " + e.getMessage() + ")";
×
403
                    }
1✔
404
                }
405
                throw new SignerException(message);
1✔
406
            }
407
        }
408

409
        String keypass = ksparams.keypass();
1✔
410
        char[] password = keypass != null ? keypass.toCharArray() : new char[0];
1✔
411

412
        PrivateKey privateKey;
413
        try {
414
            privateKey = (PrivateKey) ks.getKey(alias, password);
1✔
415
        } catch (Exception e) {
1✔
416
            throw new SignerException("Failed to retrieve the private key from the keystore", e);
1✔
417
        }
1✔
418

419
        if (alg != null && DigestAlgorithm.of(alg) == null) {
1✔
420
            throw new SignerException("The digest algorithm " + alg + " is not supported");
1✔
421
        }
422

423
        // enable timestamping with Azure Artifact Signing
424
        if (tsaurl == null && storetype == KeyStoreType.TRUSTEDSIGNING) {
1✔
UNCOV
425
            tsaurl = "http://timestamp.acs.microsoft.com/";
×
UNCOV
426
            tsmode = TimestampingMode.RFC3161.name();
×
UNCOV
427
            tsretries = 3;
×
428
        }
429
        
430
        // configure the signer
431
        return new AuthenticodeSigner(chain, privateKey)
1✔
432
                .withProgramName(name)
1✔
433
                .withProgramURL(url)
1✔
434
                .withDigestAlgorithm(DigestAlgorithm.of(alg))
1✔
435
                .withSignatureProvider(provider)
1✔
436
                .withSignaturesReplaced(replace)
1✔
437
                .withTimestamping(tsaurl != null || tsmode != null)
1✔
438
                .withTimestampingMode(tsmode != null ? TimestampingMode.of(tsmode) : TimestampingMode.AUTHENTICODE)
1✔
439
                .withTimestampingRetries(tsretries)
1✔
440
                .withTimestampingRetryWait(tsretrywait)
1✔
441
                .withTimestampingAuthority(tsaurl != null ? tsaurl.split(",") : null);
1✔
442
    }
443

444
    public void sign(String file) throws SignerException {
UNCOV
445
        sign(ksparams.createFile(file));
×
UNCOV
446
    }
×
447

448
    public void sign(File file) throws SignerException {
449
        if (file == null) {
1✔
450
            throw new SignerException("No file specified");
1✔
451
        }
452
        if (!file.exists()) {
1✔
453
            throw new SignerException("The file " + file + " couldn't be found");
1✔
454
        }
455
        
456
        try (Signable signable = Signable.of(file, encoding)) {
1✔
457
            File detachedSignature = getDetachedSignature(file);
1✔
458
            if (detached && detachedSignature.exists()) {
1✔
459
                try {
460
                    log.info("Attaching Authenticode signature to " + file);
1✔
461
                    attach(signable, detachedSignature);
1✔
UNCOV
462
                } catch (Exception e) {
×
UNCOV
463
                    throw new SignerException("Couldn't attach the signature to " + file, e);
×
464
                }
1✔
465

466
            } else {
467
                if (lazy && !signable.getSignatures().isEmpty()) {
1✔
468
                    log.info("Skipping already signed file " + file);
1✔
469
                    return;
1✔
470
                }
471

472
                if (signer == null) {
1✔
473
                    signer = build();
1✔
474
                }
475

476
                log.info("Adding Authenticode signature to " + file);
1✔
477
                signer.sign(signable);
1✔
478

479
                if (detached) {
1✔
480
                    detach(signable, detachedSignature);
1✔
481
                }
482
            }
483

484
        } catch (UnsupportedOperationException | IllegalArgumentException e) {
1✔
485
            throw new SignerException(e.getMessage(), e);
1✔
486
        } catch (SignerException e) {
1✔
487
            throw e;
1✔
488
        } catch (Exception e) {
1✔
489
            throw new SignerException("Couldn't sign " + file, e);
1✔
490
        }
1✔
491
    }
1✔
492

493
    private void attach(Signable signable, File detachedSignature) throws IOException, CMSException {
494
        byte[] signatureBytes = Files.readAllBytes(detachedSignature.toPath());
1✔
495
        CMSSignedData signedData = new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(new ASN1InputStream(signatureBytes).readObject()));
1✔
496

497
        signable.setSignatures(SignatureUtils.getSignatures(signedData));
1✔
498
        signable.save();
1✔
499
        // todo warn if the hashes don't match
500
    }
1✔
501

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

505
        // ensure the secondary signatures are nested in the first one (for EFI files)
506
        CMSSignedData signedData = signatures.get(0);
1✔
507
        if (signatures.size() > 1) {
1✔
508
            List<CMSSignedData> nestedSignatures = signatures.subList(1, signatures.size());
1✔
509
            signedData = SignatureUtils.addNestedSignature(signedData, true, nestedSignatures.toArray(new CMSSignedData[0]));
1✔
510
        }
511

512
        byte[] content = signedData.toASN1Structure().getEncoded("DER");
1✔
513
        if (format == null || "DER".equalsIgnoreCase(format)) {
1✔
514
            Files.write(detachedSignature.toPath(), content);
1✔
515
        } else if ("PEM".equalsIgnoreCase(format)) {
1✔
516
            try (FileWriter out = new FileWriter(detachedSignature)) {
1✔
517
                String encoded = Base64.getEncoder().encodeToString(content);
1✔
518
                out.write("-----BEGIN PKCS7-----\n");
1✔
519
                for (int i = 0; i < encoded.length(); i += 64) {
1✔
520
                    out.write(encoded.substring(i, Math.min(i + 64, encoded.length())));
1✔
521
                    out.write('\n');
1✔
522
                }
523
                out.write("-----END PKCS7-----\n");
1✔
524
            }
525
        } else {
526
            throw new IllegalArgumentException("Unknown output format '" + format + "'");
1✔
527
        }
528
    }
1✔
529

530
    private File getDetachedSignature(File file) {
531
        return new File(file.getParentFile(), file.getName() + ".sig");
1✔
532
    }
533

534
    private void extract(File file) throws SignerException {
535
        if (!file.exists()) {
1✔
536
            throw new SignerException("Couldn't find " + file);
1✔
537
        }
538

539
        try (Signable signable = Signable.of(file)) {
1✔
540
            List<CMSSignedData> signatures = signable.getSignatures();
1✔
541
            if (signatures.isEmpty()) {
1✔
542
                throw new SignerException("No signature found in " + file);
1✔
543
            }
544

545
            File detachedSignature = getDetachedSignature(file);
1✔
546
            if ("PEM".equalsIgnoreCase(format)) {
1✔
547
                detachedSignature = new File(detachedSignature.getParentFile(), detachedSignature.getName() + ".pem");
1✔
548
            }
549
            log.info("Extracting signature to " + detachedSignature);
1✔
550
            detach(signable, detachedSignature);
1✔
551
        } catch (UnsupportedOperationException | IllegalArgumentException e) {
1✔
552
            throw new SignerException(e.getMessage(), e);
1✔
553
        } catch (SignerException e) {
1✔
554
            throw e;
1✔
UNCOV
555
        } catch (Exception e) {
×
UNCOV
556
            throw new SignerException("Couldn't extract the signature from " + file, e);
×
557
        }
1✔
558
    }
1✔
559

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

565
        try (Signable signable = Signable.of(file)) {
1✔
566
            List<CMSSignedData> signatures = signable.getSignatures();
1✔
567
            if (signatures.isEmpty()) {
1✔
568
                log.severe("No signature found in " + file);
1✔
569
                return;
1✔
570
            }
571

572
            log.info("Removing " + signatures.size() + " signature" + (signatures.size() > 1 ? "s" : "") + " from " + file);
1✔
573
            signable.setSignatures(null);
1✔
574
            signable.save();
1✔
575
        } catch (UnsupportedOperationException | IllegalArgumentException e) {
1✔
UNCOV
576
            throw new SignerException(e.getMessage(), e);
×
UNCOV
577
        } catch (Exception e) {
×
UNCOV
578
            throw new SignerException("Couldn't remove the signature from " + file, e);
×
579
        }
1✔
580
    }
1✔
581

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

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

593
            log.info("Adding tag to " + file);
1✔
594
            signatures.set(0, addUnsignedAttribute(signatures.get(0), AuthenticodeObjectIdentifiers.JSIGN_UNSIGNED_DATA_OBJID, getTagValue()));
1✔
595
            signable.setSignatures(signatures);
1✔
596
            signable.save();
1✔
597
        } catch (SignerException e) {
1✔
598
            throw e;
1✔
599
        } catch (Exception e) {
1✔
600
            throw new SignerException("Couldn't modify the signature of " + file, e);
1✔
601
        }
1✔
602
    }
1✔
603

604
    private CMSSignedData addUnsignedAttribute(CMSSignedData signature, ASN1ObjectIdentifier oid, ASN1Encodable value) {
605
        SignerInformationStore store = signature.getSignerInfos();
1✔
606
        Collection<SignerInformation> signers = store.getSigners();
1✔
607
        SignerInformation signer = signers.iterator().next();
1✔
608

609
        AttributeTable attributes = signer.getUnsignedAttributes();
1✔
610
        if (attributes == null) {
1✔
611
            attributes = new AttributeTable(new DERSet());
1✔
612
        }
613
        attributes = attributes.add(oid, value);
1✔
614

615
        signers.remove(signer);
1✔
616
        signers.add(SignerInformation.replaceUnsignedAttributes(signer, attributes));
1✔
617
        return CMSSignedData.replaceSigners(signature, new SignerInformationStore(signers));
1✔
618
    }
619

620
    private ASN1Encodable getTagValue() throws IOException {
621
        if (value == null) {
1✔
622
            byte[] array = new byte[1024];
1✔
623
            String begin = "-----BEGIN TAG-----";
1✔
624
            System.arraycopy(begin.getBytes(), 0, array, 0, begin.length());
1✔
625
            String end = "-----END TAG-----";
1✔
626
            System.arraycopy(end.getBytes(), 0, array, array.length - end.length(), end.length());
1✔
627
            return new DEROctetString(array);
1✔
628

629
        } else if (value.startsWith("0x")) {
1✔
630
            byte[] array = Hex.decode(value.substring(2));
1✔
631
            return new DEROctetString(array);
1✔
632

633
        } else if (value.startsWith("file:")) {
1✔
634
            byte[] array = Files.readAllBytes(new File(value.substring("file:".length())).toPath());
1✔
635
            return new DEROctetString(array);
1✔
636

637
        } else {
638
            return new DERUTF8String(value);
1✔
639
        }
640
    }
641

642
    private void timestamp(File file) throws SignerException {
643
        if (!file.exists()) {
1✔
644
            throw new SignerException("Couldn't find " + file);
1✔
645
        }
646

647
        try {
648
            proxySettings.initializeProxy();
1✔
UNCOV
649
        } catch (Exception e) {
×
UNCOV
650
            throw new SignerException("Couldn't initialize proxy", e);
×
651
        }
1✔
652

653
        try (Signable signable = Signable.of(file)) {
1✔
654
            if (signable.getSignatures().isEmpty()) {
1✔
655
                throw new SignerException("No signature found in " + file);
1✔
656
            }
657

658
            Timestamper timestamper = Timestamper.create(tsmode != null ? TimestampingMode.of(tsmode) : TimestampingMode.AUTHENTICODE);
1✔
659
            timestamper.setRetries(tsretries);
1✔
660
            timestamper.setRetryWait(tsretrywait);
1✔
661
            if (tsaurl != null) {
1✔
662
                timestamper.setURLs(tsaurl.split(","));
1✔
663
            }
664
            DigestAlgorithm digestAlgorithm = alg != null ? DigestAlgorithm.of(alg) : DigestAlgorithm.getDefault();
1✔
665

666
            List<CMSSignedData> signatures = new ArrayList<>();
1✔
667
            for (CMSSignedData signature : signable.getSignatures()) {
1✔
668
                SignerInformation signerInformation = signature.getSignerInfos().iterator().next();
1✔
669
                SignerId signerId = signerInformation.getSID();
1✔
670
                X509CertificateHolder certificate = (X509CertificateHolder) signature.getCertificates().getMatches(signerId).iterator().next();
1✔
671

672
                String digestAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(signerInformation.getDigestAlgorithmID()); 
1✔
673
                String keyAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(new ASN1ObjectIdentifier(signerInformation.getEncryptionAlgOID()));
1✔
674
                String name = digestAlgorithmName + "/" + keyAlgorithmName + " signature from '" + certificate.getSubject() + "'";
1✔
675

676
                if (SignatureUtils.isTimestamped(signature) && !replace) {
1✔
677
                    log.fine(name + " already timestamped");
1✔
678
                    signatures.add(signature);
1✔
679
                    continue;
1✔
680
                }
681

682
                boolean expired = certificate.getNotAfter().before(new Date());
1✔
683
                if (expired) {
1✔
UNCOV
684
                    log.fine(name + " is expired, skipping");
×
685
                    signatures.add(signature);
×
686
                    continue;
×
687
                }
688

689
                log.info("Adding timestamp to " + name);
1✔
690
                signature = SignatureUtils.removeTimestamp(signature);
1✔
691
                signature = timestamper.timestamp(digestAlgorithm, signature);
1✔
692

693
                signatures.add(signature);
1✔
694
            }
1✔
695

696
            signable.setSignatures(signatures);
1✔
697
            signable.save();
1✔
UNCOV
698
        } catch (IOException | CMSException e) {
×
UNCOV
699
            throw new SignerException("Couldn't timestamp " + file, e);
×
700
        }
1✔
701
    }
1✔
702
}
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