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

ebourg / jsign / #370

25 Apr 2025 04:41PM UTC coverage: 83.31% (+0.008%) from 83.302%
#370

push

ebourg
Changelog update

4862 of 5836 relevant lines covered (83.31%)

0.83 hits per line

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

97.67
/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 number of data-directory entries in the remainder of the optional
232
     * header. Each describes a location and size.
233
     * 
234
     * @return the number of data-directory entries
235
     */
236
    int getNumberOfRvaAndSizes() throws IOException {
237
        return (int) readDWord(peHeaderOffset, PEFormat.PE32.equals(getFormat()) ? 116 : 132);
1✔
238
    }
239

240
    int getDataDirectoryOffset() throws IOException {
241
        return (int) peHeaderOffset + (PEFormat.PE32.equals(getFormat()) ? 120 : 136);
1✔
242
    }
243

244
    /**
245
     * Returns the data directory of the specified type.
246
     * 
247
     * @param type the type of data directory
248
     * @return the data directory of the specified type
249
     */
250
    DataDirectory getDataDirectory(DataDirectoryType type) throws IOException {
251
        if (type.ordinal() >= getNumberOfRvaAndSizes()) {
1✔
252
            return null;
1✔
253
        } else {
254
            return new DataDirectory(this, type.ordinal());
1✔
255
        }
256
    }
257

258
    /**
259
     * Writes the certificate table. The data is either appended at the end of the file
260
     * or written over the previous certificate table.
261
     * 
262
     * @param data the content of the certificate table
263
     * @throws IOException if an I/O error occurs
264
     */
265
    synchronized void writeCertificateTable(byte[] data) throws IOException {
266
        DataDirectory directory = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
1✔
267
        if (directory == null) {
1✔
268
            throw new IOException("No space allocated in the data directories index for the certificate table");
1✔
269
        }
270
        
271
        if (!directory.exists()) {
1✔
272
            // append the data directory at the end of the file on a 8-byte boundary
273
            long offset = channel.size() + (8 - channel.size() % 8) % 8;
1✔
274
            
275
            write(offset, data);
1✔
276
            
277
            // update the entry in the data directory table
278
            directory.write(offset, data.length);
1✔
279
            
280
        } else if (directory.isTrailing()) {
1✔
281
            // the data is at the end of the file, overwrite it
282
            write(directory.getVirtualAddress(), data);
1✔
283
            channel.truncate(directory.getVirtualAddress() + data.length); // trim the file if the data shrunk
1✔
284

285
            // update the size in the data directory table
286
            directory.write(directory.getVirtualAddress(), data.length);
1✔
287

288
        } else {
289
            throw new IOException("The certificate table isn't at the end of the file");
×
290
        }
291
        
292
        updateChecksum();
1✔
293
    }
1✔
294

295
    @Override
296
    public synchronized List<CMSSignedData> getSignatures() throws IOException {
297
        List<CMSSignedData> signatures = new ArrayList<>();
1✔
298
        
299
        for (CertificateTableEntry certificate : getCertificateTable()) {
1✔
300
            if (certificate.isSupported()) {
1✔
301
                signatures.addAll(SignatureUtils.getSignatures(certificate.getContent()));
1✔
302
            }
303
        }
1✔
304
        
305
        return signatures;
1✔
306
    }
307

308
    @Override
309
    public void setSignature(CMSSignedData signature) throws IOException {
310
        if (signature != null) {
1✔
311
            CertificateTableEntry entry = new CertificateTableEntry(signature);
1✔
312
            writeCertificateTable(entry.toBytes());
1✔
313

314
        } else if (getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE).exists()) {
1✔
315
            // erase the previous signature
316
            DataDirectory certificateTable = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
1✔
317
            channel.truncate(certificateTable.getVirtualAddress());
1✔
318
            certificateTable.write(0, 0);
1✔
319
        }
320
    }
1✔
321

322
    private synchronized List<CertificateTableEntry> getCertificateTable() throws IOException {
323
        List<CertificateTableEntry> entries = new ArrayList<>();
1✔
324
        DataDirectory certificateTable = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
1✔
325
        
326
        if (certificateTable != null && certificateTable.exists()) {
1✔
327
            long position = certificateTable.getVirtualAddress();
1✔
328
            
329
            try {
330
                if (position < channel.size()) {
1✔
331
                    entries.add(new CertificateTableEntry(this, position));
1✔
332
                }
333
                
334
                // todo read the remaining entries (but Authenticode use only one, extra signatures are appended as a SPC_NESTED_SIGNATURE unauthenticated attribute)
335
            } catch (Exception e) {
1✔
336
                e.printStackTrace();
1✔
337
            }
1✔
338
        }
339
        
340
        return entries;
1✔
341
    }
342

343
    /**
344
     * Compute the digest of the file. The checksum field, the certificate
345
     * directory table entry and the certificate table are excluded from
346
     * the digest.
347
     * 
348
     * @param digestAlgorithm the digest algorithm to use
349
     * @return the digest of the file
350
     * @throws IOException if an I/O error occurs
351
     */
352
    @Override
353
    public synchronized byte[] computeDigest(DigestAlgorithm digestAlgorithm) throws IOException {
354
        MessageDigest digest = digestAlgorithm.getMessageDigest();
1✔
355

356
        long checksumLocation = peHeaderOffset + 88;
1✔
357
        
358
        DataDirectory certificateTable = getDataDirectory(DataDirectoryType.CERTIFICATE_TABLE);
1✔
359
        
360
        // digest from the beginning to the checksum field (excluded)
361
        updateDigest(channel, digest, 0, checksumLocation);
1✔
362
        
363
        // skip the checksum field
364
        long position = checksumLocation + 4;
1✔
365
        
366
        // digest from the end of the checksum field to the beginning of the certificate table entry
367
        int certificateTableOffset = getDataDirectoryOffset() + 8 * DataDirectoryType.CERTIFICATE_TABLE.ordinal();
1✔
368
        updateDigest(channel, digest, position, certificateTableOffset);
1✔
369
        
370
        // skip the certificate entry
371
        position = certificateTableOffset + 8;
1✔
372
        
373
        // digest from the end of the certificate table entry to the beginning of the certificate table
374
        if (certificateTable != null && certificateTable.exists()) {
1✔
375
            certificateTable.check();
1✔
376
            updateDigest(channel, digest, position, certificateTable.getVirtualAddress());
1✔
377
            position = certificateTable.getVirtualAddress() + certificateTable.getSize();
1✔
378
        }
379
        
380
        // digest from the end of the certificate table to the end of the file
381
        updateDigest(channel, digest, position, channel.size());
1✔
382
        
383
        if (certificateTable == null || !certificateTable.exists()) {
1✔
384
            // if the file has never been signed before, update the digest as if the file was padded on a 8 byte boundary
385
            int paddingLength = (int) (8 - channel.size() % 8) % 8;
1✔
386
            digest.update(new byte[paddingLength]);
1✔
387
        }
388

389
        return digest.digest();
1✔
390
    }
391

392
    @Override
393
    public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException {
394
        AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE);
1✔
395
        DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, computeDigest(digestAlgorithm));
1✔
396
        SpcAttributeTypeAndOptionalValue data = new SpcAttributeTypeAndOptionalValue(AuthenticodeObjectIdentifiers.SPC_PE_IMAGE_DATA_OBJID, new SpcPeImageData());
1✔
397

398
        return new SpcIndirectDataContent(data, digestInfo);
1✔
399
    }
400
}
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