summaryrefslogtreecommitdiff
path: root/src/main/java/com/android/apkzlib/sign/SignatureExtension.java
blob: 9f2196f0f762d89c1e2cef3c7b68c4647b423bf6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.apkzlib.sign;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.apkzlib.utils.IOExceptionRunnable;
import com.android.apkzlib.zip.StoredEntry;
import com.android.apkzlib.zip.ZFile;
import com.android.apkzlib.zip.ZFileExtension;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Locale;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;

/**
 * {@link ZFile} extension that signs all files in the APK and generates a signature file and a
 * digital signature of the signature file. The extension registers itself automatically with the
 * {@link ZFile} upon creation.
 * <p>
 * The signature extension will recompute signatures of files already in the zip file but won't
 * update the manifest if these signatures match the ones in the manifest.
 * <p>
 * This extension does 4 main tasks: maintaining the digests of all files in the zip in the manifest
 * file, maintaining the digests of all files in the zip in the signature file, maintaining the
 * digest of the manifest in the signature file and maintaining the digital signature file. For
 * performance, the digests and signatures are only computed when needed.
 * <p>
 * These tasks are done at three different moments: when the extension
 * is created, when files are added to the zip and before the zip is updated.
 * When the extension is created: (Note that the manifest's digest is <em>not</em> checked when
 * the extension is created.)
 * <ul>
 *     <li>The signature file is read, if one exists.
 *     <li>The signature "administrative" info is read and updated if not up-to-date.
 *     <li>The digests for entries in the manifest and signature file that do not correspond to
 *     any file in the zip are removed.
 *     <li>The digests for all entries in the zip are recomputed and updated in the signature file
 *     and in the manifest, if needed.
 * </ul>
 * <p>
 * When files are added or removed:
 * <ul>
 *     <li>The signature file and manifest are updated to reflect the changes.
 *     <li>If the file was added, its digest is computed.
 * </ul>
 * <p>
 * Before updating the zip file:
 * <ul>
 *     <li>If a signature file already exists, checks the digest of the manifest and updates the
 *     signature file if needed.
 *     <li>Creates the signature file if it did not already exist.
 *     <li>Recreates the digital signature of the signature file if the signature file was created
 *     or updated.
 * </ul>
 */
public class SignatureExtension {

    /**
     * Base of signature files.
     */
    private static final String SIGNATURE_BASE = ManifestGenerationExtension.META_INF_DIR + "/CERT";

    /**
     * Path of the signature file.
     */
    private static final String SIGNATURE_FILE = SIGNATURE_BASE + ".SF";

    /**
     * Name of attribute with the signature version.
     */
    private static final String SIGNATURE_VERSION_NAME = "Signature-Version";

    /**
     * Version of the signature version.
     */
    private static final String SIGNATURE_VERSION_VALUE = "1.0";

    /**
     * Name of attribute with the "created by" attribute.
     */
    private static final String SIGNATURE_CREATED_BY_NAME = "Created-By";

    /**
     * Value of the "created by" attribute.
     */
    private static final String SIGNATURE_CREATED_BY_VALUE = "1.0 (Android)";

    /**
     * Name of the {@code X-Android-APK-Signer} attribute.
     */
    private static final String SIGNATURE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";

    /**
     * Value of the {@code X-Android-APK-Signer} attribute when the APK is signed with the v2
     * scheme.
     */
    public static final String SIGNATURE_ANDROID_APK_SIGNER_VALUE_WHEN_V2_SIGNED = "2";

    /**
     * Files to ignore when signing. See
     * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
     */
    private static final Set<String> IGNORED_FILES = Sets.newHashSet(
            ManifestGenerationExtension.MANIFEST_NAME, SIGNATURE_FILE);

    /**
     * Same as {@link #IGNORED_FILES} but with all names in lower case.
     */
    private static final Set<String> IGNORED_FILES_LC = Sets.newHashSet(
            IGNORED_FILES.stream()
                    .map(i -> i.toLowerCase(Locale.US))
                    .collect(Collectors.toSet()));


    /**
     * Prefix of files in META-INF to ignore when signing. See
     * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
     */
    private static final Set<String> IGNORED_PREFIXES = Sets.newHashSet(
            "SIG-");

    /**
     * Same as {@link #IGNORED_PREFIXES} but with all names in lower case.
     */
    private static final Set<String> IGNORED_PREFIXES_LC = Sets.newHashSet(
            IGNORED_PREFIXES.stream()
                    .map(i -> i.toLowerCase(Locale.US))
                    .collect(Collectors.toSet()));

    /**
     * Suffixes of files in META-INF to ignore when signing. See
     * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
     */
    private static final Set<String> IGNORED_SUFFIXES = Sets.newHashSet(
            ".SF", ".DSA", ".RSA", ".EC");

    /**
     * Same as {@link #IGNORED_SUFFIXES} but with all names in lower case.
     */
    private static final Set<String> IGNORED_SUFFIXES_LC = Sets.newHashSet(
            IGNORED_SUFFIXES.stream()
                    .map(i -> i.toLowerCase(Locale.US))
                    .collect(Collectors.toSet()));

    /**
     * Extension maintaining the manifest.
     */
    @NonNull
    private final ManifestGenerationExtension mManifestExtension;

    /**
     * Message digest to use.
     */
    @NonNull
    private final MessageDigest mMessageDigest;

    /**
     * Signature file. Note that the signature file is itself a manifest file but it is
     * a different one from the "standard" MANIFEST.MF.
     */
    @NonNull
    private final Manifest mSignatureFile;

    /**
     * Has the signature manifest been changed?
     */
    private boolean mDirty;

    /**
     * Signer certificate.
     */
    @NonNull
    private final X509Certificate mCertificate;

    /**
     * The private key used to sign the jar.
     */
    @NonNull
    private final PrivateKey mPrivateKey;

    /**
     * Algorithm with which .SF file is signed.
     */
    @NonNull
    private final SignatureAlgorithm mSignatureAlgorithm;

    /**
     * Digest algorithm to use for MANIFEST.MF and contents of APK entries.
     */
    @NonNull
    private final DigestAlgorithm mDigestAlgorithm;

    /**
     * Value to output for the {@code X-Android-APK-Signed} header or {@code null} if the header
     * should not be output.
     */
    @Nullable
    private final String mApkSignedHeaderValue;

    /**
     * The extension registered with the {@link ZFile}. {@code null} if not registered.
     */
    @Nullable
    private ZFileExtension mExtension;

    /**
     * Creates a new signature extension.
     *
     * @param manifestExtension the extension maintaining the manifest
     * @param minSdkVersion minSdkVersion of the package
     * @param certificate sign certificate
     * @param privateKey the private key to sign the jar
     * @param apkSignedHeaderValue value of the {@code X-Android-APK-Signed} header to output into
     * the {@code .SF} file or {@code null} if the header should not be output.
     *
     * @throws NoSuchAlgorithmException failed to obtain the digest algorithm.
     */
    public SignatureExtension(@NonNull ManifestGenerationExtension manifestExtension,
            int minSdkVersion, @NonNull X509Certificate certificate, @NonNull PrivateKey privateKey,
            @Nullable String apkSignedHeaderValue)
            throws NoSuchAlgorithmException {
        mManifestExtension = manifestExtension;
        mSignatureFile = new Manifest();
        mDirty = false;
        mCertificate = certificate;
        mPrivateKey = privateKey;
        mApkSignedHeaderValue = apkSignedHeaderValue;

        mSignatureAlgorithm =
                SignatureAlgorithm.fromKeyAlgorithm(privateKey.getAlgorithm(), minSdkVersion);
        mDigestAlgorithm = DigestAlgorithm.findBest(minSdkVersion, mSignatureAlgorithm);
        mMessageDigest = MessageDigest.getInstance(mDigestAlgorithm.messageDigestName);
    }

    /**
     * Registers the extension with the {@link ZFile} provided in the
     * {@link ManifestGenerationExtension}. Note that the {@code ManifestGenerationExtension}
     * needs to be registered as a precondition for this method.
     *
     * @throws IOException failed to analyze the zip
     */
    public void register() throws IOException {
        Preconditions.checkState(mExtension == null, "register() already invoked");

        mExtension = new ZFileExtension() {
            @Nullable
            @Override
            public IOExceptionRunnable beforeUpdate() {
                return SignatureExtension.this::updateSignatureIfNeeded;
            }

            @Nullable
            @Override
            public IOExceptionRunnable added(@NonNull final StoredEntry entry,
                    @Nullable final StoredEntry replaced) {
                if (replaced != null) {
                    Preconditions.checkArgument(entry.getCentralDirectoryHeader().getName().equals(
                            replaced.getCentralDirectoryHeader().getName()));
                }

                if (isIgnoredFile(entry.getCentralDirectoryHeader().getName())) {
                    return null;
                }

                return () -> {
                    if (replaced != null) {
                        SignatureExtension.this.removed(replaced);
                    }

                    SignatureExtension.this.added(entry);
                };
            }

            @Nullable
            @Override
            public IOExceptionRunnable removed(@NonNull final StoredEntry entry) {
                if (isIgnoredFile(entry.getCentralDirectoryHeader().getName())) {
                    return null;
                }

                return () -> SignatureExtension.this.removed(entry);
            }
        };

        mManifestExtension.zFile().addZFileExtension(mExtension);
        readSignatureFile();
    }

    /**
     * Reads the signature file (if any) on the zip file.
     * <p>
     * When this method terminates, we have the following guarantees:
     * <ul>
     *      <li>An internal signature manifest exists.</li>
     *      <li>All entries in the in-memory signature file exist in the zip file.</li>
     *      <li>All entries in the zip file (with the exception of the signature-related files,
     *      as specified by https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html)
     *      exist in the in-memory signature file.</li>
     *      <li>All entries in the in-memory signature file have digests that match their
     *      contents in the zip.</li>
     *      <li>All entries in the in-memory signature manifest exist also in the manifest file
     *      and the digests are the same.</li>
     *      <li>The main attributes of the in-memory signature manifest are valid. The manifest's
     *      digest has not been verified and may not even exist.</li>
     *      <li>If the internal in-memory signature manifest differs in any way from the one
     *      written in the file, {@link #mDirty} will be set to {@code true}. Otherwise,
     *      {@link #mDirty} will be set to {@code false}.</li>
     * </ul>
     *
     * @throws IOException failed to read the signature file
     */
    private void readSignatureFile() throws IOException {
        boolean needsNewSignature = false;

        StoredEntry signatureEntry = mManifestExtension.zFile().get(SIGNATURE_FILE);
        if (signatureEntry != null) {
            byte[] signatureData = signatureEntry.read();
            mSignatureFile.read(new ByteArrayInputStream(signatureData));

            Attributes mainAttrs = mSignatureFile.getMainAttributes();
            String versionName = mainAttrs.getValue(SIGNATURE_VERSION_NAME);
            String createdBy = mainAttrs.getValue(SIGNATURE_CREATED_BY_NAME);
            String apkSigned = mainAttrs.getValue(SIGNATURE_ANDROID_APK_SIGNED_NAME);

            if (!SIGNATURE_VERSION_VALUE.equals(versionName)
                    || !SIGNATURE_CREATED_BY_VALUE.equals(createdBy)
                    || mainAttrs.getValue(mDigestAlgorithm.manifestAttributeName) == null
                    || !Objects.equal(mApkSignedHeaderValue, apkSigned)) {
                needsNewSignature = true;
            }
        } else {
            needsNewSignature = true;
        }

        if (needsNewSignature) {
            Attributes mainAttrs = mSignatureFile.getMainAttributes();

            mainAttrs.putValue(SIGNATURE_CREATED_BY_NAME, SIGNATURE_CREATED_BY_VALUE);
            mainAttrs.putValue(SIGNATURE_VERSION_NAME, SIGNATURE_VERSION_VALUE);
            if (mApkSignedHeaderValue != null) {
                mainAttrs.putValue(SIGNATURE_ANDROID_APK_SIGNED_NAME, mApkSignedHeaderValue);
            } else {
                mainAttrs.remove(SIGNATURE_ANDROID_APK_SIGNED_NAME);
            }

            mDirty = true;
        }

        /*
         * At this point we have a valid in-memory signature file with a valid header. mDirty
         * states whether this is the same as the file-based signature file.
         *
         * Now, check we have the same files in the zip as in the signature file and that all
         * digests match. While we do this, make sure the manifest is also up-do-date.
         *
         * We ignore all signature-related files that exist in the zip that are signature-related.
         * This are defined in the jar format specification.
         */
        Set<StoredEntry> allEntries =
                mManifestExtension.zFile().entries().stream()
                        .filter(se -> !isIgnoredFile(se.getCentralDirectoryHeader().getName()))
                        .collect(Collectors.toSet());

        Set<String> sigEntriesToRemove = Sets.newHashSet(mSignatureFile.getEntries().keySet());
        Set<String> manEntriesToRemove = Sets.newHashSet(mManifestExtension.allEntries().keySet());
        for (StoredEntry se : allEntries) {
            /*
             * Update the entry's digest, if needed.
             */
            setDigestForEntry(se);

            /*
             * This entry exists in the file, so remove it from the list of entries to remove
             * from the manifest and signature file.
             */
            sigEntriesToRemove.remove(se.getCentralDirectoryHeader().getName());
            manEntriesToRemove.remove(se.getCentralDirectoryHeader().getName());
        }

        for (String toRemoveInSignature : sigEntriesToRemove) {
            mSignatureFile.getEntries().remove(toRemoveInSignature);
            mDirty = true;
        }

        for (String toRemoveInManifest : manEntriesToRemove) {
            mManifestExtension.removeEntry(toRemoveInManifest);
        }
    }

    /**
     * This method will recompute the manifest's digest and will update the signature file if the
     * manifest has changed. It then writes the signature file, if dirty for any reason (including
     * from recomputing the manifest's digest).
     *
     * @throws IOException failed to read / write zip data
     */
    private void updateSignatureIfNeeded() throws IOException {
        byte[] manifestData = mManifestExtension.getManifestBytes();
        byte[] manifestDataDigest = mMessageDigest.digest(manifestData);


        String manifestDataDigestTxt = Base64.getEncoder().encodeToString(manifestDataDigest);

        if (!manifestDataDigestTxt.equals(mSignatureFile.getMainAttributes().getValue(
                mDigestAlgorithm.manifestAttributeName))) {
            mSignatureFile
                    .getMainAttributes()
                    .putValue(mDigestAlgorithm.manifestAttributeName, manifestDataDigestTxt);
            mDirty = true;
        }

        if (!mDirty) {
            return;
        }

        ByteArrayOutputStream signatureBytes = new ByteArrayOutputStream();
        mSignatureFile.write(signatureBytes);

        mManifestExtension.zFile().add(
                SIGNATURE_FILE,
                new ByteArrayInputStream(signatureBytes.toByteArray()));

        String digitalSignatureFile = SIGNATURE_BASE + "." + mPrivateKey.getAlgorithm();
        try {
            mManifestExtension.zFile().add(
                    digitalSignatureFile,
                    new ByteArrayInputStream(computePkcs7Signature(signatureBytes.toByteArray())));
        } catch (CertificateEncodingException | OperatorCreationException | CMSException e) {
            throw new IOException("Failed to digitally sign signature file.", e);
        }

        mDirty = false;
    }

    /**
     * A new file has been added.
     *
     * @param entry the entry added
     * @throws IOException failed to add the entry to the signature file (or failed to compute the
     * entry's signature)
     */
    private void added(@NonNull StoredEntry entry) throws IOException {
        setDigestForEntry(entry);
    }

    /**
     * Adds / updates the signature for an entry. If this entry has no signature, or its digest
     * doesn't match the one in the signature file (or manifest), it will be updated.
     *
     * @param entry the entry
     * @throws IOException failed to compute the entry's digest
     */
    private void setDigestForEntry(@NonNull StoredEntry entry) throws IOException {
        String entryName = entry.getCentralDirectoryHeader().getName();
        byte[] entryDigestArray = mMessageDigest.digest(entry.read());
        String entryDigest = Base64.getEncoder().encodeToString(entryDigestArray);

        Attributes signatureAttributes = mSignatureFile.getEntries().get(entryName);
        if (signatureAttributes == null) {
            signatureAttributes = new Attributes();
            mSignatureFile.getEntries().put(entryName, signatureAttributes);
            mDirty = true;
        }

        if (!entryDigest.equals(signatureAttributes.getValue(
                mDigestAlgorithm.entryAttributeName))) {
            signatureAttributes.putValue(mDigestAlgorithm.entryAttributeName, entryDigest);
            mDirty = true;
        }

        /*
         * setAttribute will not mark the manifest as changed if the attribute is already there
         * and with the same value.
         */
        mManifestExtension.setAttribute(entryName, mDigestAlgorithm.entryAttributeName,
                entryDigest);
    }

    /**
     * File has been removed.
     *
     * @param entry the entry removed
     */
    private void removed(@NonNull StoredEntry entry) {
        mSignatureFile.getEntries().remove(entry.getCentralDirectoryHeader().getName());
        mManifestExtension.removeEntry(entry.getCentralDirectoryHeader().getName());
        mDirty = true;
    }

    /**
     * Checks if a file should be ignored when signing.
     *
     * @param name the file name
     * @return should it be ignored
     */
    public static boolean isIgnoredFile(@NonNull String name) {
        String metaInfPfx = ManifestGenerationExtension.META_INF_DIR + "/";
        boolean inMetaInf = name.startsWith(metaInfPfx)
                && !name.substring(metaInfPfx.length()).contains("/");

        /*
         * Only files in META-INF can be ignored. Files in sub-directories of META-INF are not
          * ignored.
         */
        if (!inMetaInf) {
            return false;
        }

        String nameLc = name.toLowerCase(Locale.US);

        /*
         * All files with names that match (case insensitive) the ignored list are ignored.
         */
        if (IGNORED_FILES_LC.contains(nameLc)) {
            return true;
        }

        for (String pfx : IGNORED_PREFIXES_LC) {
            if (nameLc.startsWith(pfx)) {
                return true;
            }
        }

        for (String sfx : IGNORED_SUFFIXES_LC) {
            if (nameLc.endsWith(sfx)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Computes the digital signature of an array of data.
     *
     * @param data the data
     * @return the digital signature
     * @throws IOException failed to read/write signature data
     * @throws CertificateEncodingException failed to sign the data
     * @throws OperatorCreationException failed to sign the data
     * @throws CMSException failed to sign the data
     */
    private byte[] computePkcs7Signature(@NonNull byte[] data) throws IOException,
            CertificateEncodingException, OperatorCreationException, CMSException {
        CMSProcessableByteArray cmsData = new CMSProcessableByteArray(data);

        ArrayList<X509Certificate> certList = new ArrayList<>();
        certList.add(mCertificate);
        JcaCertStore certs = new JcaCertStore(certList);

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        String signatureAlgName = mSignatureAlgorithm.signatureAlgorithmName(mDigestAlgorithm);
        ContentSigner shaSigner =
                new JcaContentSignerBuilder(signatureAlgName).build(mPrivateKey);
        gen.addSignerInfoGenerator(
                new JcaSignerInfoGeneratorBuilder(
                        new JcaDigestCalculatorProviderBuilder()
                                .build())
                                .setDirectSignature(true)
                                .build(shaSigner, mCertificate));
        gen.addCertificates(certs);
        CMSSignedData sigData = gen.generate(cmsData, false);

        ByteArrayOutputStream outputBytes = new ByteArrayOutputStream();

        /*
         * DEROutputStream is not closeable! OMG!
         */
        DEROutputStream dos = null;
        try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
            dos = new DEROutputStream(outputBytes);
            dos.writeObject(asn1.readObject());

            DEROutputStream toClose = dos;
            dos = null;
            toClose.close();
        } catch (IOException e) {
            if (dos != null) {
                try {
                    dos.close();
                } catch (IOException ee) {
                    e.addSuppressed(ee);
                }
            }
        }

        return outputBytes.toByteArray();
    }
}