• 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

89.62
/jsign-core/src/main/java/net/jsign/SignatureUtils.java
1
/*
2
 * Copyright 2024 Emmanuel Bourg
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package net.jsign;
18

19
import java.io.IOException;
20
import java.util.ArrayList;
21
import java.util.Collection;
22
import java.util.Collections;
23
import java.util.Date;
24
import java.util.List;
25
import java.util.NoSuchElementException;
26

27
import org.bouncycastle.asn1.ASN1Encodable;
28
import org.bouncycastle.asn1.ASN1EncodableVector;
29
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
30
import org.bouncycastle.asn1.ASN1Sequence;
31
import org.bouncycastle.asn1.DERSet;
32
import org.bouncycastle.asn1.cms.Attribute;
33
import org.bouncycastle.asn1.cms.AttributeTable;
34
import org.bouncycastle.asn1.cms.CMSAttributes;
35
import org.bouncycastle.asn1.cms.ContentInfo;
36
import org.bouncycastle.asn1.cms.Time;
37
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
38
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
39
import org.bouncycastle.asn1.x509.DigestInfo;
40
import org.bouncycastle.cert.X509CertificateHolder;
41
import org.bouncycastle.cert.selector.X509CertificateHolderSelector;
42
import org.bouncycastle.cms.CMSException;
43
import org.bouncycastle.cms.CMSSignedData;
44
import org.bouncycastle.cms.SignerInformation;
45
import org.bouncycastle.cms.SignerInformationStore;
46
import org.bouncycastle.util.Store;
47

48
import static net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers.*;
49

50
/**
51
 * Helper class for working with signatures.
52
 *
53
 * @since 7.0
54
 */
55
public class SignatureUtils {
×
56

57
    /**
58
     * Parse the specified signature.
59
     *
60
     * @param signature the signature to analyze
61
     * @since 7.5
62
     */
63
    public static CMSSignedData getSignature(byte[] signature) throws IOException {
64
        try {
65
            return new CMSSignedData(signature);
1✔
UNCOV
66
        } catch (CMSException | IllegalArgumentException | IllegalStateException | NoSuchElementException | ClassCastException | StackOverflowError e) {
×
67
            // Bouncy Castle can throw a wide range of exceptions when parsing a signature, so we wrap them all in an IOException
UNCOV
68
            throw new IOException("Malformed signature", e);
×
69
        }
70
    }
71

72
    /**
73
     * Parse the specified signature and return the nested Authenticode signatures.
74
     *
75
     * @param signature the signature to analyze
76
     */
77
    public static List<CMSSignedData> getSignatures(byte[] signature) throws IOException {
78
        return getSignatures(getSignature(signature));
1✔
79
    }
80

81
    /**
82
     * Extract the nested Authenticode signatures from the specified signature.
83
     *
84
     * @param signature the signature to analyze
85
     * @return the list of signatures (the first one is the parent signature without nested signatures)
86
     */
87
    public static List<CMSSignedData> getSignatures(CMSSignedData signature) throws IOException {
88
        List<CMSSignedData> signatures = new ArrayList<>();
1✔
89

90
        try {
91
            if (signature != null) {
1✔
92
                signatures.add(signature);
1✔
93
                signatures.set(0, SignatureUtils.removeNestedSignatures(signature));
1✔
94

95
                // look for nested signatures
96
                Attribute nestedSignatures = getUnsignedAttribute(signature, SPC_NESTED_SIGNATURE_OBJID);
1✔
97
                if (nestedSignatures != null) {
1✔
98
                    for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) {
1✔
99
                        signatures.add(new CMSSignedData(nestedSignature.toASN1Primitive().getEncoded()));
1✔
100
                    }
1✔
101
                }
102
            }
UNCOV
103
        } catch (CMSException e) {
×
UNCOV
104
            throw new IOException(e);
×
105
        }
1✔
106

107
        return signatures;
1✔
108
    }
109

110
    /**
111
     * Embed a signature as an unsigned attribute of an existing signature.
112
     *
113
     * @param parent    the root signature hosting the nested secondary signature
114
     * @param children  the additional signature to nest inside the root signature
115
     * @return the signature combining the specified signatures
116
     */
117
    static CMSSignedData addNestedSignature(CMSSignedData parent, boolean replace, CMSSignedData... children) {
118
        SignerInformation signerInformation = parent.getSignerInfos().iterator().next();
1✔
119

120
        AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
1✔
121
        if (unsignedAttributes == null) {
1✔
122
            unsignedAttributes = new AttributeTable(new DERSet());
1✔
123
        }
124

125
        Attribute nestedSignaturesAttribute = unsignedAttributes.get(SPC_NESTED_SIGNATURE_OBJID);
1✔
126
        ASN1EncodableVector nestedSignatures = new ASN1EncodableVector();
1✔
127
        if (nestedSignaturesAttribute != null && !replace) {
1✔
128
            // keep the previous nested signatures
UNCOV
129
            for (ASN1Encodable nestedSignature : nestedSignaturesAttribute.getAttrValues()) {
×
UNCOV
130
                nestedSignatures.add(nestedSignature);
×
UNCOV
131
            }
×
132
        }
133

134
        // append the new signatures
135
        for (CMSSignedData nestedSignature : children) {
1✔
136
            nestedSignatures.add(nestedSignature.toASN1Structure());
1✔
137
        }
138

139
        // replace the nested signatures attribute
140
        ASN1EncodableVector attributes = unsignedAttributes.remove(SPC_NESTED_SIGNATURE_OBJID).toASN1EncodableVector();
1✔
141
        attributes.add(new Attribute(SPC_NESTED_SIGNATURE_OBJID, new DERSet(nestedSignatures)));
1✔
142

143
        unsignedAttributes = new AttributeTable(attributes);
1✔
144

145
        signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes);
1✔
146
        return CMSSignedData.replaceSigners(parent, new SignerInformationStore(signerInformation));
1✔
147
    }
148

149
    /**
150
     * Remove the nested signatures from the specified signature.
151
     *
152
     * @param signature the signature to modify
153
     * @return the signature without nested signatures
154
     */
155
    static CMSSignedData removeNestedSignatures(CMSSignedData signature) {
156
        return removeUnsignedAttributes(signature, SPC_NESTED_SIGNATURE_OBJID);
1✔
157
    }
158

159
    /**
160
     * Tells if the specified signature is timestamped.
161
     *
162
     * @param signature the signature to check
163
     */
164
    static boolean isTimestamped(CMSSignedData signature) {
165
        boolean authenticode = isAuthenticode(signature.getSignedContentTypeOID());
1✔
166
        Attribute authenticodeTimestampAttribute = getUnsignedAttribute(signature, CMSAttributes.counterSignature);
1✔
167
        Attribute rfc3161TimestampAttribute = getUnsignedAttribute(signature, authenticode ? SPC_RFC3161_OBJID : PKCSObjectIdentifiers.id_aa_signatureTimeStampToken);
1✔
168
        return authenticodeTimestampAttribute != null || rfc3161TimestampAttribute != null;
1✔
169
    }
170

171
    /**
172
     * Removes the timestamp from the specified signature.
173
     *
174
     * @param signature the signature to modify
175
     */
176
    static CMSSignedData removeTimestamp(CMSSignedData signature) {
177
        // todo remove the TSA certificates from the certificate store
178

179
        return removeUnsignedAttributes(signature,
1✔
180
                CMSAttributes.counterSignature,
181
                PKCSObjectIdentifiers.id_aa_signatureTimeStampToken,
182
                SPC_RFC3161_OBJID);
183
    }
184

185
    /**
186
     * Remove the specified unsigned attributes from the signature.
187
     *
188
     * @param signature the signature to modify
189
     * @param oids the OIDs of the attributes to remove
190
     * @return the modified signature
191
     */
192
    static CMSSignedData removeUnsignedAttributes(CMSSignedData signature, ASN1ObjectIdentifier... oids) {
193
        SignerInformation signerInformation = signature.getSignerInfos().iterator().next();
1✔
194

195
        AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
1✔
196
        if (unsignedAttributes == null) {
1✔
197
            return signature;
1✔
198
        }
199

200
        for (ASN1ObjectIdentifier oid : oids) {
1✔
201
            unsignedAttributes = unsignedAttributes.remove(oid);
1✔
202
        }
203

204
        signerInformation = SignerInformation.replaceUnsignedAttributes(signerInformation, unsignedAttributes);
1✔
205
        return CMSSignedData.replaceSigners(signature, new SignerInformationStore(signerInformation));
1✔
206
    }
207

208
    /**
209
     * Returns the digest info of the signature.
210
     *
211
     * @since 7.5
212
     */
213
    static DigestInfo getDigestInfo(CMSSignedData signature) {
214
        if (SPC_INDIRECT_DATA_OBJID.equals(signature.getSignedContent().getContentType())) {
1✔
215
            ASN1Sequence indirectData = (ASN1Sequence) signature.getSignedContent().getContent();
1✔
216
            return DigestInfo.getInstance(indirectData.getObjectAt(1));
1✔
217
        }
218

219
        if (PKCSObjectIdentifiers.data.equals(signature.getSignedContent().getContentType())) {
1✔
220
            // the data is assumed to be the digest of the file with the same algorithm as the signature
221
            SignerInformation signer = signature.getSignerInfos().iterator().next();
1✔
222
            AlgorithmIdentifier digestAlgorithm = signer.getDigestAlgorithmID();
1✔
223
            return new DigestInfo(digestAlgorithm, (byte[]) signature.getSignedContent().getContent());
1✔
224
        }
225

UNCOV
226
        return null;
×
227
    }
228

229
    /**
230
     * Returns the timestamp signer information.
231
     *
232
     * @since 7.5
233
     */
234
    static SignerInformation getCounterSigner(CMSSignedData signature) throws CMSException {
235
        SignerInformation signer = signature.getSignerInfos().iterator().next();
1✔
236

237
        Collection<SignerInformation> counterSigners = Collections.emptyList();
1✔
238

239
        Attribute timestampAttribute = getUnsignedAttribute(signature, CMSAttributes.counterSignature);
1✔
240
        if (timestampAttribute != null) {
1✔
241
            counterSigners = signer.getCounterSignatures().getSigners();
1✔
242
        }
243

244
        timestampAttribute  = getUnsignedAttribute(signature, SPC_RFC3161_OBJID);
1✔
245
        if (timestampAttribute == null) {
1✔
246
            timestampAttribute = getUnsignedAttribute(signature, PKCSObjectIdentifiers.id_aa_signatureTimeStampToken);
1✔
247
        }
248
        if (timestampAttribute != null) {
1✔
249
            CMSSignedData signedData = new CMSSignedData(ContentInfo.getInstance(timestampAttribute.getAttrValues().getObjectAt(0)));
1✔
250
            counterSigners = signedData.getSignerInfos().getSigners();
1✔
251
        }
252

253
        return !counterSigners.isEmpty() ? counterSigners.iterator().next() : null;
1✔
254
    }
255

256
    /**
257
     * Returns the signing time of the timestamp.
258
     *
259
     * @since 7.5
260
     */
261
    static Date getTimestampDate(CMSSignedData signature) throws CMSException {
262
        SignerInformation counterSigner = getCounterSigner(signature);
1✔
263
        if (counterSigner != null) {
1✔
264
            Attribute signingTime = counterSigner.getSignedAttributes().get(CMSAttributes.signingTime);
1✔
265
            if (signingTime != null) {
1✔
266
                return Time.getInstance(signingTime.getAttrValues().getObjectAt(0)).getDate();
1✔
267
            }
268
        }
269

270
        return null;
1✔
271
    }
272

273
    /**
274
     * Returns the certificate of the timestamp.
275
     *
276
     * @since 7.5
277
     */
278
    static X509CertificateHolder getTimestampCertificate(CMSSignedData signature) throws CMSException {
279
        SignerInformation counterSigner = getCounterSigner(signature);
1✔
280
        if (counterSigner == null) {
1✔
UNCOV
281
            return null;
×
282
        }
283

284
        Store<X509CertificateHolder> certificates = signature.getCertificates();
1✔
285
        AttributeTable unsignedAttributes = signature.getSignerInfos().iterator().next().getUnsignedAttributes();
1✔
286
        Attribute timestampAttribute  = unsignedAttributes.get(SPC_RFC3161_OBJID);
1✔
287
        if (timestampAttribute != null) {
1✔
288
            CMSSignedData signedData = new CMSSignedData(ContentInfo.getInstance(timestampAttribute.getAttrValues().getObjectAt(0)));
1✔
289
            certificates = signedData.getCertificates();
1✔
290
        }
291

292
        X509CertificateHolderSelector selector = new X509CertificateHolderSelector(counterSigner.getSID().getIssuer(), counterSigner.getSID().getSerialNumber());
1✔
293

294
        Collection<X509CertificateHolder> matches = certificates.getMatches(selector);
1✔
295
        return !matches.isEmpty() ? matches.iterator().next() : null;
1✔
296
    }
297

298

299
    /**
300
     * Returns the value of the unsigned tag
301
     *
302
     * @param signature the CMS signed data
303
     * @return the value of the unsigned tag (ASN1UTF8String or ASN1OctetString), or null if not found
304
     * @since 7.5
305
     */
306
    static ASN1Encodable getTag(CMSSignedData signature) throws IOException {
307
        Attribute attribute = getUnsignedAttribute(signature, JSIGN_UNSIGNED_DATA_OBJID);
1✔
308
        if (attribute == null) {
1✔
UNCOV
309
            attribute = getUnsignedAttribute(signature, new ASN1ObjectIdentifier("1.3.6.1.4.1.42921.1.2.1")); // Dropbox OID used by osslsigncode
×
310
        }
311

312
        return attribute != null ? attribute.getAttrValues().getObjectAt(0) : null;
1✔
313
    }
314

315
    /**
316
     * Returns the specified unsigned attribute from the signature.
317
     *
318
     * @param signature the signature
319
     * @param oid       the object identifier of the attribute
320
     * @return the unsigned attribute, or null if not found
321
     * @since 7.5
322
     */
323
    static Attribute getUnsignedAttribute(CMSSignedData signature, ASN1ObjectIdentifier oid) {
324
        SignerInformation signer = signature.getSignerInfos().iterator().next();
1✔
325

326
        AttributeTable unsignedAttributes = signer.getUnsignedAttributes();
1✔
327
        if (unsignedAttributes == null) {
1✔
328
            return null;
1✔
329
        }
330

331
        return unsignedAttributes.get(oid);
1✔
332
    }
333

334
    /**
335
     * Adds an unsigned attribute to the signature/
336
     *
337
     * @param signature the signature
338
     * @param oid       the object identifier of the attribute
339
     * @param value     the value of the attribute
340
     * @since 7.5
341
     */
342
    static CMSSignedData addUnsignedAttribute(CMSSignedData signature, ASN1ObjectIdentifier oid, ASN1Encodable value) {
343
        SignerInformationStore store = signature.getSignerInfos();
1✔
344
        Collection<SignerInformation> signers = store.getSigners();
1✔
345
        SignerInformation signer = signers.iterator().next();
1✔
346

347
        AttributeTable attributes = signer.getUnsignedAttributes();
1✔
348
        if (attributes == null) {
1✔
349
            attributes = new AttributeTable(new DERSet());
1✔
350
        }
351
        attributes = attributes.add(oid, value);
1✔
352

353
        signers.remove(signer);
1✔
354
        signers.add(SignerInformation.replaceUnsignedAttributes(signer, attributes));
1✔
355
        return CMSSignedData.replaceSigners(signature, new SignerInformationStore(signers));
1✔
356
    }
357
}
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