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

ebourg / jsign / #382

02 Oct 2025 02:50PM UTC coverage: 83.308% (+0.08%) from 83.226%
#382

push

ebourg
Apply the proxy settings before accessing the cloud signing services (Fixes #324)

2 of 4 new or added lines in 1 file covered. (50.0%)

36 existing lines in 2 files now uncovered.

4951 of 5943 relevant lines covered (83.31%)

0.83 hits per line

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

89.57
/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.net.Authenticator;
23
import java.net.InetSocketAddress;
24
import java.net.MalformedURLException;
25
import java.net.PasswordAuthentication;
26
import java.net.Proxy;
27
import java.net.ProxySelector;
28
import java.net.SocketAddress;
29
import java.net.URI;
30
import java.net.URL;
31
import java.nio.charset.Charset;
32
import java.nio.file.Files;
33
import java.security.KeyStore;
34
import java.security.KeyStoreException;
35
import java.security.PrivateKey;
36
import java.security.Provider;
37
import java.security.cert.Certificate;
38
import java.util.ArrayList;
39
import java.util.Base64;
40
import java.util.Collection;
41
import java.util.Collections;
42
import java.util.Date;
43
import java.util.LinkedHashSet;
44
import java.util.List;
45
import java.util.Set;
46
import java.util.logging.Logger;
47

48
import org.bouncycastle.asn1.ASN1Encodable;
49
import org.bouncycastle.asn1.ASN1InputStream;
50
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
51
import org.bouncycastle.asn1.DEROctetString;
52
import org.bouncycastle.asn1.DERSet;
53
import org.bouncycastle.asn1.DERUTF8String;
54
import org.bouncycastle.asn1.cms.AttributeTable;
55
import org.bouncycastle.asn1.cms.ContentInfo;
56
import org.bouncycastle.cert.X509CertificateHolder;
57
import org.bouncycastle.cms.CMSException;
58
import org.bouncycastle.cms.CMSProcessable;
59
import org.bouncycastle.cms.CMSSignedData;
60
import org.bouncycastle.cms.SignerId;
61
import org.bouncycastle.cms.SignerInformation;
62
import org.bouncycastle.cms.SignerInformationStore;
63
import org.bouncycastle.operator.DefaultAlgorithmNameFinder;
64
import org.bouncycastle.util.encoders.Hex;
65

66
import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers;
67
import net.jsign.timestamp.Timestamper;
68
import net.jsign.timestamp.TimestampingMode;
69

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

102
    private final Logger log = Logger.getLogger(getClass().getName());
1✔
103

104
    /** The name used to refer to a configuration parameter */
105
    private final String parameterName;
106

107
    /** The command to execute */
108
    private String command = "sign";
1✔
109

110
    private final KeyStoreBuilder ksparams;
111
    private String alias;
112
    private String tsaurl;
113
    private String tsmode;
114
    private int tsretries = -1;
1✔
115
    private int tsretrywait = -1;
1✔
116
    private String alg;
117
    private String name;
118
    private String url;
119
    private String proxyUrl;
120
    private String proxyUser;
121
    private String proxyPass;
122
    private boolean replace;
123
    private Charset encoding;
124
    private boolean detached;
125
    private String format;
126
    private String value;
127

128
    private AuthenticodeSigner signer;
129

130
    public SignerHelper(String parameterName) {
1✔
131
        this.parameterName = parameterName;
1✔
132
        this.ksparams = new KeyStoreBuilder(parameterName);
1✔
133
    }
1✔
134

135
    public SignerHelper command(String command) {
136
        this.command = command;
1✔
137
        return this;
1✔
138
    }
139

140
    public SignerHelper keystore(String keystore) {
141
        ksparams.keystore(keystore);
1✔
142
        signer = null;
1✔
143
        return this;
1✔
144
    }
145

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

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

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

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

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

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

182
    public SignerHelper certfile(String certfile) {
183
        ksparams.certfile(certfile);
1✔
184
        signer = null;
1✔
185
        return this;
1✔
186
    }
187

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

194
    public SignerHelper alg(String alg) {
195
        this.alg = alg;
1✔
196
        signer = null;
1✔
197
        return this;
1✔
198
    }
199

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

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

212
    public SignerHelper tsretries(int tsretries) {
213
        this.tsretries = tsretries;
1✔
214
        signer = null;
1✔
215
        return this;
1✔
216
    }
217

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

224
    public SignerHelper name(String name) {
225
        this.name = name;
1✔
226
        signer = null;
1✔
227
        return this;
1✔
228
    }
229

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

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

242
    public SignerHelper proxyUser(String proxyUser) {
243
        this.proxyUser = proxyUser;
1✔
244
        signer = null;
1✔
245
        return this;
1✔
246
    }
247

248
    public SignerHelper proxyPass(String proxyPass) {
249
        this.proxyPass = proxyPass;
1✔
250
        signer = null;
1✔
251
        return this;
1✔
252
    }
253

254
    public SignerHelper replace(boolean replace) {
255
        this.replace = replace;
1✔
256
        signer = null;
1✔
257
        return this;
1✔
258
    }
259

260
    public SignerHelper encoding(String encoding) {
261
        this.encoding = Charset.forName(encoding);
1✔
262
        return this;
1✔
263
    }
264

265
    public SignerHelper detached(boolean detached) {
266
        this.detached = detached;
1✔
267
        return this;
1✔
268
    }
269

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

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

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

314
    void setBaseDir(File basedir) {
315
        ksparams.setBaseDir(basedir);
1✔
316
    }
1✔
317

318
    public void execute(String file) throws SignerException {
UNCOV
319
        execute(ksparams.createFile(file));
×
UNCOV
320
    }
×
321

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

344
    private AuthenticodeSigner build() throws SignerException {
345
        try {
346
            initializeProxy(proxyUrl, proxyUser, proxyPass);
1✔
NEW
347
        } catch (Exception e) {
×
NEW
348
            throw new SignerException("Couldn't initialize proxy", e);
×
349
        }
1✔
350

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

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

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

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

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

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

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

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

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

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

468
            } else {
469
                if (signer == null) {
1✔
470
                    signer = build();
1✔
471
                }
472

473
                log.info("Adding Authenticode signature to " + file);
1✔
474
                signer.sign(signable);
1✔
475

476
                if (detached) {
1✔
477
                    detach(signable, detachedSignature);
1✔
478
                }
479
            }
480

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

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

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

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

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

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

527
    private File getDetachedSignature(File file) {
528
        return new File(file.getParentFile(), file.getName() + ".sig");
1✔
529
    }
530

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

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

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

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

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

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

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

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

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

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

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

612
        signers.remove(signer);
1✔
613
        signers.add(SignerInformation.replaceUnsignedAttributes(signer, attributes));
1✔
614
        return CMSSignedData.replaceSigners(signature, new SignerInformationStore(signers));
1✔
615
    }
616

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

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

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

634
        } else {
635
            return new DERUTF8String(value);
1✔
636
        }
637
    }
638

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

644
        try {
645
            initializeProxy(proxyUrl, proxyUser, proxyPass);
1✔
UNCOV
646
        } catch (Exception e) {
×
UNCOV
647
            throw new SignerException("Couldn't initialize proxy", e);
×
648
        }
1✔
649

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

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

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

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

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

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

686
                log.info("Adding timestamp to " + name);
1✔
687
                signature = SignatureUtils.removeTimestamp(signature);
1✔
688
                signature = timestamper.timestamp(digestAlgorithm, signature);
1✔
689

690
                signatures.add(signature);
1✔
691
            }
1✔
692

693
            signable.setSignatures(signatures);
1✔
694
            signable.save();
1✔
UNCOV
695
        } catch (IOException | CMSException e) {
×
UNCOV
696
            throw new SignerException("Couldn't timestamp " + file, e);
×
697
        }
1✔
698
    }
1✔
699

700
    /**
701
     * Initializes the proxy.
702
     *
703
     * @param proxyUrl       the url of the proxy (either as hostname:port or http[s]://hostname:port)
704
     * @param proxyUser      the username for the proxy authentication
705
     * @param proxyPassword  the password for the proxy authentication
706
     */
707
    private void initializeProxy(String proxyUrl, final String proxyUser, final String proxyPassword) throws MalformedURLException {
708
        // Do nothing if there is no proxy url.
709
        if (proxyUrl != null && !proxyUrl.trim().isEmpty()) {
1✔
710
            if (!proxyUrl.trim().startsWith("http")) {
1✔
711
                proxyUrl = "http://" + proxyUrl.trim();
1✔
712
            }
713
            final URL url = new URL(proxyUrl);
1✔
714
            final int port = url.getPort() < 0 ? 80 : url.getPort();
1✔
715

716
            ProxySelector.setDefault(new ProxySelector() {
1✔
717
                public List<Proxy> select(URI uri) {
718
                    Proxy proxy;
719
                    if (uri.getScheme().equals("socket")) {
1✔
UNCOV
720
                        proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(url.getHost(), port));
×
721
                    } else {
722
                        proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(url.getHost(), port));
1✔
723
                    }
724
                    log.fine("Proxy selected for " + uri + " : " + proxy);
1✔
725
                    return Collections.singletonList(proxy);
1✔
726
                }
727

728
                public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
UNCOV
729
                }
×
730
            });
731

732
            if (proxyUser != null && !proxyUser.isEmpty() && proxyPassword != null) {
1✔
733
                Authenticator.setDefault(new Authenticator() {
1✔
734
                    protected PasswordAuthentication getPasswordAuthentication() {
735
                        return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray());
1✔
736
                    }
737
                });
738
            }
739
        }
740
    }
1✔
741
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc