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

ebourg / jsign / #381

22 Sep 2025 04:50PM UTC coverage: 83.226% (+0.1%) from 83.103%
#381

push

ebourg
Reorganized the documentation (command line first)

4927 of 5920 relevant lines covered (83.23%)

0.83 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.nio.file.Files;
25
import java.nio.file.StandardOpenOption;
26
import java.security.MessageDigest;
27
import java.util.ArrayList;
28
import java.util.List;
29

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

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

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

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

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

61
    final SeekableByteChannel channel;
62

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

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

95
    /**
96
     * Create a PEFile from the specified file.
97
     *
98
     * @param file the file to open
99
     * @throws IOException if an I/O error occurs
100
     */
101
    public PEFile(File file) throws IOException {
102
        this(Files.newByteChannel(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE));
1✔
103
    }
1✔
104

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

211
        channel.position(0);
1✔
212

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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