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

ebourg / jsign / #418

23 Jul 2026 12:50PM UTC coverage: 81.28% (+0.5%) from 80.829%
#418

push

ebourg
CodeQL workflow

5397 of 6640 relevant lines covered (81.28%)

0.81 hits per line

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

98.04
/jsign-core/src/main/java/net/jsign/pe/PEFile.java
1
/*
2
 * Copyright 2012 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.pe;
18

19
import java.io.File;
20
import java.io.IOException;
21
import java.nio.ByteBuffer;
22
import java.nio.ByteOrder;
23
import java.nio.channels.SeekableByteChannel;
24
import java.security.MessageDigest;
25
import java.util.ArrayList;
26
import java.util.List;
27

28
import org.bouncycastle.asn1.ASN1Object;
29
import org.bouncycastle.asn1.DERNull;
30
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
31
import org.bouncycastle.asn1.x509.DigestInfo;
32
import org.bouncycastle.cms.CMSSignedData;
33

34
import net.jsign.ChannelUtils;
35
import net.jsign.DigestAlgorithm;
36
import net.jsign.Signable;
37
import net.jsign.SignatureUtils;
38
import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers;
39
import net.jsign.asn1.authenticode.SpcAttributeTypeAndOptionalValue;
40
import net.jsign.asn1.authenticode.SpcIndirectDataContent;
41
import net.jsign.asn1.authenticode.SpcPeImageData;
42

43
import static net.jsign.ChannelUtils.*;
44

45
/**
46
 * Portable Executable File.
47
 * 
48
 * This class is thread safe.
49
 * 
50
 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/debug/pe-format">Microsoft PE and COFF Specification </a>
51
 * 
52
 * @author Emmanuel Bourg
53
 * @since 1.0
54
 */
55
public class PEFile implements Signable {
56

57
    /** The position of the PE header in the file */
58
    private final long peHeaderOffset;
59

60
    final SeekableByteChannel channel;
61

62
    /** Reusable buffer for reading bytes, words, dwords and qwords from the file */
63
    private final ByteBuffer valueBuffer = ByteBuffer.allocate(8);
1✔
64
    {
65
        valueBuffer.order(ByteOrder.LITTLE_ENDIAN);
1✔
66
    }
67

68
    /**
69
     * Tells if the specified file is a Portable Executable file.
70
     *
71
     * @param file the file to check
72
     * @return <code>true</code> if the file is a Portable Executable, <code>false</code> otherwise
73
     * @throws IOException if an I/O error occurs
74
     * @since 3.0
75
     */
76
    public static boolean isPEFile(File file) throws IOException {
77
        if (!file.exists() || !file.isFile()) {
1✔
78
            return false;
1✔
79
        }
80
        
81
        try {
82
            PEFile peFile = new PEFile(file);
1✔
83
            peFile.close();
1✔
84
            return true;
1✔
85
        } catch (IOException e) {
1✔
86
            if (e.getMessage().contains("DOS header signature not found") || e.getMessage().contains("PE signature not found")) {
1✔
87
                return false;
1✔
88
            } else {
89
                throw e;
×
90
            }
91
        }
92
    }
93

94
    /**
95
     * Create a PEFile from the specified file.
96
     *
97
     * @param file the file to open
98
     * @throws IOException if an I/O error occurs
99
     */
100
    public PEFile(File file) throws IOException {
101
        this(ChannelUtils.openReadWriteOrReadOnly(file));
1✔
102
    }
1✔
103

104
    /**
105
     * Create a PEFile from the specified channel.
106
     *
107
     * @param channel the channel to read the file from
108
     * @throws IOException if an I/O error occurs
109
     * @since 2.0
110
     */
111
    public PEFile(SeekableByteChannel channel) throws IOException {
1✔
112
        this.channel = channel;
1✔
113
        
114
        try {
115
            // check if the PE file is too big
116
            if (channel.size() >= 1L << 32) {
1✔
117
                throw new IOException("Invalid PE file: the size exceeds 4GB");
×
118
            }
119

120
            // DOS Header
121
            read(0, 0, 2);
1✔
122
            if (valueBuffer.get() != 'M' || valueBuffer.get() != 'Z') {
1✔
123
                throw new IOException("DOS header signature not found");
1✔
124
            }
125

126
            // PE Header
127
            read(0x3C, 0, 4);
1✔
128
            peHeaderOffset = valueBuffer.getInt() & 0xFFFFFFFFL;
1✔
129
            read(peHeaderOffset, 0, 4);
1✔
130
            if (valueBuffer.get() != 'P' || valueBuffer.get() != 'E' || valueBuffer.get() != 0 || valueBuffer.get() != 0) {
1✔
131
                throw new IOException("PE signature not found as expected at offset 0x" + Long.toHexString(peHeaderOffset));
1✔
132
            }
133

134
        } catch (IOException e) {
1✔
135
            channel.close();
1✔
136
            throw e;
1✔
137
        }
1✔
138
    }
1✔
139

140
    public void save() {
141
    }
1✔
142

143
    /**
144
     * Closes the file
145
     *
146
     * @throws IOException if an I/O error occurs
147
     */
148
    public synchronized void close() throws IOException {
149
        channel.close();
1✔
150
    }
1✔
151

152
    synchronized int read(byte[] buffer, long base, int offset) throws IOException {
153
        channel.position(base + offset);
1✔
154
        return channel.read(ByteBuffer.wrap(buffer));
1✔
155
    }
156

157
    private void read(long base, int offset, int length) throws IOException {
158
        valueBuffer.limit(length);
1✔
159
        valueBuffer.clear();
1✔
160
        channel.position(base + offset);
1✔
161
        channel.read(valueBuffer);
1✔
162
        valueBuffer.rewind();
1✔
163
    }
1✔
164

165
    synchronized int readWord(long base, int offset) throws IOException {
166
        read(base, offset, 2);
1✔
167
        return valueBuffer.getShort() & 0xFFFF;
1✔
168
    }
169

170
    synchronized long readDWord(long base, int offset) throws IOException {
171
        read(base, offset, 4);
1✔
172
        return valueBuffer.getInt() & 0xFFFFFFFFL;
1✔
173
    }
174

175
    synchronized void write(long base, byte[] data) throws IOException {
176
        write(base, ByteBuffer.wrap(data));
1✔
177
    }
1✔
178

179
    synchronized void write(long base, ByteBuffer data) throws IOException {
180
        channel.position(base);
1✔
181
        while (data.hasRemaining()) {
1✔
182
            channel.write(data);
1✔
183
        }
184
    }
1✔
185

186
    PEFormat getFormat() throws IOException {
187
        return PEFormat.valueOf(readWord(peHeaderOffset, 24));
1✔
188
    }
189

190
    /**
191
     * The image file checksum.
192
     * 
193
     * @return the checksum of the image
194
     */
195
    long getCheckSum() throws IOException {
196
        return readDWord(peHeaderOffset, 88);
1✔
197
    }
198

199
    /**
200
     * Compute the checksum of the image file. The algorithm for computing
201
     * the checksum is incorporated into IMAGHELP.DLL.
202
     * 
203
     * @return the checksum of the image
204
     */
205
    synchronized long computeChecksum() throws IOException {
206
        PEImageChecksum checksum = new PEImageChecksum(peHeaderOffset + 88);
1✔
207
        
208
        ByteBuffer b = ByteBuffer.allocate(64 * 1024);
1✔
209

210
        channel.position(0);
1✔
211

212
        int len;
213
        while ((len = channel.read(b)) > 0) {
1✔
214
            b.flip();
1✔
215
            checksum.update(b.array(), 0, len);
1✔
216
        }
217
        
218
        return checksum.getValue();
1✔
219
    }
220

221
    synchronized void updateChecksum() throws IOException {
222
        ByteBuffer buffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
1✔
223
        buffer.putInt((int) computeChecksum());
1✔
224
        buffer.flip();
1✔
225

226
        write(peHeaderOffset + 88, buffer);
1✔
227
    }
1✔
228

229
    /**
230
     * The subsystem that is required to run this image.
231
     * 
232
     * @return the required subsystem
233
     */
234
    Subsystem getSubsystem() throws IOException {
235
        return Subsystem.valueOf(readWord(peHeaderOffset, 92));
1✔
236
    }
237

238
    boolean isEFI() throws IOException {
239
        Subsystem subsystem = getSubsystem();
1✔
240
        return subsystem == Subsystem.EFI_APPLICATION
1✔
241
                || subsystem == Subsystem.EFI_BOOT_SERVICE_DRIVER
242
                || subsystem == Subsystem.EFI_ROM
243
                || subsystem == Subsystem.EFI_RUNTIME_DRIVER;
244
    }
245

246
    /**
247
     * The number of data-directory entries in the remainder of the optional
248
     * header. Each describes a location and size.
249
     * 
250
     * @return the number of data-directory entries
251
     */
252
    int getNumberOfRvaAndSizes() throws IOException {
253
        return (int) readDWord(peHeaderOffset, PEFormat.PE32.equals(getFormat()) ? 116 : 132);
1✔
254
    }
255

256
    int getDataDirectoryOffset() throws IOException {
257
        return (int) peHeaderOffset + (PEFormat.PE32.equals(getFormat()) ? 120 : 136);
1✔
258
    }
259

260
    /**
261
     * Returns the data directory of the specified type.
262
     * 
263
     * @param type the type of data directory
264
     * @return the data directory of the specified type
265
     */
266
    DataDirectory getDataDirectory(DataDirectoryType type) throws IOException {
267
        if (type.ordinal() >= getNumberOfRvaAndSizes()) {
1✔
268
            return null;
1✔
269
        } else {
270
            return new DataDirectory(this, type);
1✔
271
        }
272
    }
273

274
    /**
275
     * Writes the certificate table. The data is either appended at the end of the file
276
     * or written over the previous certificate table.
277
     * 
278
     * @param entries the entries of the certificate table
279
     * @throws IOException if an I/O error occurs
280
     */
281
    private synchronized void writeCertificateTable(CertificateTableEntry[] entries) throws IOException {
282
        int totalSize = 0;
1✔
283
        for (CertificateTableEntry entry : entries) {
1✔
284
            totalSize += entry.getSize();
1✔
285
        }
286

287
        byte[] data = new byte[totalSize];
1✔
288
        int pos = 0;
1✔
289
        for (CertificateTableEntry entry : entries) {
1✔
290
            System.arraycopy(entry.toBytes(), 0, data, pos, entry.getSize());
1✔
291
            pos += entry.getSize();
1✔
292
        }
293

294
        DataDirectory directory = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
1✔
295
        if (directory == null) {
1✔
296
            throw new IOException("No space allocated in the data directories index for the certificate table");
1✔
297
        }
298
        
299
        if (!directory.exists()) {
1✔
300
            // append the data directory at the end of the file on a 8-byte boundary
301
            long offset = channel.size() + (8 - channel.size() % 8) % 8;
1✔
302
            
303
            write(offset, data);
1✔
304
            
305
            // update the entry in the data directory table
306
            directory.write(offset, data.length);
1✔
307
            
308
        } else if (directory.isTrailing()) {
1✔
309
            // the data is at the end of the file, overwrite it
310
            write(directory.getVirtualAddress(), data);
1✔
311
            channel.truncate(directory.getVirtualAddress() + data.length); // trim the file if the data shrunk
1✔
312

313
            // update the size in the data directory table
314
            directory.write(directory.getVirtualAddress(), data.length);
1✔
315

316
        } else {
317
            throw new IOException("The certificate table isn't at the end of the file");
×
318
        }
319
        
320
        updateChecksum();
1✔
321
    }
1✔
322

323
    @Override
324
    public synchronized List<CMSSignedData> getSignatures() throws IOException {
325
        List<CMSSignedData> signatures = new ArrayList<>();
1✔
326
        
327
        for (CertificateTableEntry certificate : getCertificateTable()) {
1✔
328
            if (certificate.isSupported()) {
1✔
329
                signatures.addAll(SignatureUtils.getSignatures(certificate.getContent()));
1✔
330
            }
331
        }
1✔
332
        
333
        return signatures;
1✔
334
    }
335

336
    @Override
337
    public void setSignature(CMSSignedData signature) throws IOException {
338
        if (signature != null) {
1✔
339
            CertificateTableEntry[] entries;
340
            if (isEFI()) {
1✔
341
                List<CMSSignedData> signatures = SignatureUtils.getSignatures(signature);
1✔
342
                entries = new CertificateTableEntry[signatures.size()];
1✔
343
                for (int i = 0; i < signatures.size(); i++) {
1✔
344
                    entries[i] = new CertificateTableEntry(signatures.get(i));
1✔
345
                }
346
            } else {
1✔
347
                CertificateTableEntry entry = new CertificateTableEntry(signature);
1✔
348
                entries = new CertificateTableEntry[] { entry };
1✔
349
            }
350
            writeCertificateTable(entries);
1✔
351

352
        } else if (getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE).exists()) {
1✔
353
            // erase the previous signature
354
            DataDirectory certificateTable = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
1✔
355
            channel.truncate(certificateTable.getVirtualAddress());
1✔
356
            certificateTable.write(0, 0);
1✔
357
        }
358
    }
1✔
359

360
    synchronized List<CertificateTableEntry> getCertificateTable() throws IOException {
361
        List<CertificateTableEntry> entries = new ArrayList<>();
1✔
362
        DataDirectory certificateTable = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
1✔
363
        
364
        if (certificateTable != null && certificateTable.exists()) {
1✔
365
            long position = certificateTable.getVirtualAddress();
1✔
366
            long size = certificateTable.getSize();
1✔
367
            
368
            try {
369
                while (position < certificateTable.getVirtualAddress() + size) {
1✔
370
                    CertificateTableEntry entry = new CertificateTableEntry(this, position);
1✔
371
                    entries.add(entry);
1✔
372
                    if (!isEFI()) {
1✔
373
                        // only one entry for non-EFI files
374
                        break;
1✔
375
                    }
376

377
                    position += entry.getSize();
1✔
378
                }
1✔
379
            } catch (Exception e) {
1✔
380
                e.printStackTrace();
1✔
381
            }
1✔
382
        }
383
        
384
        return entries;
1✔
385
    }
386

387
    /**
388
     * Compute the digest of the file. The checksum field, the certificate
389
     * directory table entry and the certificate table are excluded from
390
     * the digest.
391
     * 
392
     * @param digestAlgorithm the digest algorithm to use
393
     * @return the digest of the file
394
     * @throws IOException if an I/O error occurs
395
     */
396
    @Override
397
    public synchronized byte[] computeDigest(DigestAlgorithm digestAlgorithm) throws IOException {
398
        MessageDigest digest = digestAlgorithm.getMessageDigest();
1✔
399

400
        long checksumLocation = peHeaderOffset + 88;
1✔
401
        
402
        DataDirectory certificateTable = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
1✔
403
        
404
        // digest from the beginning to the checksum field (excluded)
405
        updateDigest(channel, digest, 0, checksumLocation);
1✔
406
        
407
        // skip the checksum field
408
        long position = checksumLocation + 4;
1✔
409
        
410
        // digest from the end of the checksum field to the beginning of the certificate table entry
411
        int certificateTableOffset = getDataDirectoryOffset() + 8 * DataDirectoryType.CERTIFICATE_TABLE.ordinal();
1✔
412
        updateDigest(channel, digest, position, certificateTableOffset);
1✔
413
        
414
        // skip the certificate entry
415
        position = certificateTableOffset + 8;
1✔
416
        
417
        // digest from the end of the certificate table entry to the beginning of the certificate table
418
        if (certificateTable != null && certificateTable.exists()) {
1✔
419
            certificateTable.check();
1✔
420
            updateDigest(channel, digest, position, certificateTable.getVirtualAddress());
1✔
421
            position = certificateTable.getVirtualAddress() + certificateTable.getSize();
1✔
422
        }
423
        
424
        // digest from the end of the certificate table to the end of the file
425
        updateDigest(channel, digest, position, channel.size());
1✔
426
        
427
        if (certificateTable == null || !certificateTable.exists()) {
1✔
428
            // if the file has never been signed before, update the digest as if the file was padded on a 8 byte boundary
429
            int paddingLength = (int) (8 - channel.size() % 8) % 8;
1✔
430
            digest.update(new byte[paddingLength]);
1✔
431
        }
432

433
        return digest.digest();
1✔
434
    }
435

436
    @Override
437
    public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException {
438
        AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE);
1✔
439
        DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, computeDigest(digestAlgorithm));
1✔
440
        SpcAttributeTypeAndOptionalValue data = new SpcAttributeTypeAndOptionalValue(AuthenticodeObjectIdentifiers.SPC_PE_IMAGE_DATA_OBJID, new SpcPeImageData());
1✔
441

442
        return new SpcIndirectDataContent(data, digestInfo);
1✔
443
    }
444
}
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