PESignerTest.java
/*
* Copyright 2012 Emmanuel Bourg
*
* 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 net.jsign;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Collection;
import java.util.HashSet;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.JavaVersion;
import org.apache.commons.lang3.SystemUtils;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.cms.CMSAttributes;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerId;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Assume;
import org.junit.Test;
import net.jsign.pe.PEFile;
import net.jsign.timestamp.AuthenticodeTimestamper;
import net.jsign.timestamp.TimestampingException;
import net.jsign.timestamp.TimestampingMode;
import static net.jsign.DigestAlgorithm.*;
import static net.jsign.KeyStoreType.*;
import static org.junit.Assert.*;
public class PESignerTest {
private static final String PRIVATE_KEY_PASSWORD = "password";
private static final String ALIAS = "test";
private KeyStore getKeyStore() throws Exception {
return new KeyStoreBuilder().keystore("target/test-classes/keystores/keystore.jks").storepass("password").build();
}
@Test
public void testSign() throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed.exe");
FileUtils.copyFile(sourceFile, targetFile);
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD)
.withTimestamping(false)
.withProgramName("WinEyes")
.withProgramURL("http://www.steelblue.com/WinEyes");
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
CMSSignedData signature = peFile.getSignatures().get(0);
assertNotNull("signature", signature);
assertNull("signingTime attribute found", signature.getSignerInfos().iterator().next().getSignedAttributes().get(CMSAttributes.signingTime));
}
}
@Test
public void testSignWithUnknownKeyStoreEntry() {
Exception e = assertThrows(IllegalArgumentException.class, () -> new PESigner(getKeyStore(), "unknown", PRIVATE_KEY_PASSWORD));
assertEquals("message", "No certificate found in the keystore with the alias 'unknown'", e.getMessage());
}
@Test
public void testSigningWithKeyAndChain() throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed-key-chain.exe");
FileUtils.copyFile(sourceFile, targetFile);
Certificate[] chain;
try (FileInputStream in = new FileInputStream("target/test-classes/keystores/jsign-test-certificate-full-chain.spc")) {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
chain = certificates.toArray(new Certificate[0]);
}
PrivateKey key = PrivateKeyUtils.load(new File("target/test-classes/keystores/privatekey-encrypted.pvk"), "password");
PESigner signer = new PESigner(chain, key)
.withTimestamping(false)
.withProgramName("WinEyes")
.withProgramURL("http://www.steelblue.com/WinEyes");
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
CMSSignedData signature = peFile.getSignatures().get(0);
assertNotNull("signature", signature);
// check the signer id
SignerId signerId = signature.getSignerInfos().iterator().next().getSID();
X509CertificateHolder certificate = (X509CertificateHolder) signature.getCertificates().getMatches(signerId).iterator().next();
String commonName = certificate.getSubject().getRDNs(X509ObjectIdentifiers.commonName)[0].getFirst().getValue().toString();
assertEquals("signer", "Jsign Code Signing Test Certificate 2024 (RSA)", commonName);
}
}
@Test
public void testSigningWithYubikey() throws Exception {
Assume.assumeTrue("No Yubikey detected", YubiKey.isPresent());
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed-yubikey.exe");
FileUtils.copyFile(sourceFile, targetFile);
KeyStore keystore = new KeyStoreBuilder().storetype(YUBIKEY).storepass("123456")
.certfile("target/test-classes/keystores/jsign-test-certificate-full-chain.spc").build();
AuthenticodeSigner signer = new AuthenticodeSigner(keystore, "X.509 Certificate for Digital Signature", null);
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
}
}
@Test
public void testEmptyChain() throws Exception {
PrivateKey key = PrivateKeyUtils.load(new File("target/test-classes/keystores/privatekey-encrypted.pvk"), "password");
assertThrows(IllegalArgumentException.class, () -> new PESigner(new Certificate[0], key));
}
@Test
public void testNullChain() throws Exception {
PrivateKey key = PrivateKeyUtils.load(new File("target/test-classes/keystores/privatekey-encrypted.pvk"), "password");
assertThrows(IllegalArgumentException.class, () -> new PESigner(null, key));
}
@Test
public void testSigningWithMismatchingKeyAndCertificate() throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed-mismatching-key-certificate.exe");
FileUtils.copyFile(sourceFile, targetFile);
Certificate[] chain;
try (FileInputStream in = new FileInputStream("target/test-classes/keystores/jsign-root-ca.pem")) {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
chain = certificates.toArray(new Certificate[0]);
}
PrivateKey key = PrivateKeyUtils.load(new File("target/test-classes/keystores/privatekey-encrypted.pvk"), "password");
PESigner signer = new PESigner(chain, key)
.withTimestamping(false)
.withProgramName("WinEyes")
.withProgramURL("http://www.steelblue.com/WinEyes");
try (PEFile peFile = new PEFile(targetFile)) {
// todo investigate why no exception is thrown when the mismatched keys have the same length
assertThrows(Exception.class, () -> signer.sign(peFile));
}
}
@Test
public void testTimestampAuthenticode() throws Exception {
testTimestamp(TimestampingMode.AUTHENTICODE, SHA1);
}
@Test
public void testTimestampRFC3161() throws Exception {
testTimestamp(TimestampingMode.RFC3161, SHA256);
}
public void testTimestamp(TimestampingMode mode, DigestAlgorithm alg) throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-timestamped-" + mode.name().toLowerCase() + ".exe");
FileUtils.copyFile(sourceFile, targetFile);
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD);
signer.withDigestAlgorithm(alg);
signer.withTimestamping(true);
signer.withTimestampingMode(mode);
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, alg);
SignatureAssert.assertTimestamped("Invalid timestamp", peFile.getSignatures().get(0));
}
}
/**
* Tests that a custom Timestamper implementation can be provided.
*/
@Test
public void testWithTimestamper() throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-timestamped-custom.exe");
FileUtils.copyFile(sourceFile, targetFile);
final HashSet<Boolean> called = new HashSet<>();
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD);
signer.withDigestAlgorithm(SHA1);
signer.withTimestamping(true);
signer.withTimestamper(new AuthenticodeTimestamper() {
@Override
protected CMSSignedData timestamp(DigestAlgorithm algo, byte[] encryptedDigest) throws IOException, TimestampingException {
called.add(true);
return super.timestamp(algo, encryptedDigest);
}
});
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
assertTrue("expecting our Timestamper to be used", called.contains(true));
SignatureAssert.assertSigned(peFile, SHA1);
SignatureAssert.assertTimestamped("Invalid timestamp", peFile.getSignatures().get(0));
}
}
@Test
public void testSignTwice() throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed-twice.exe");
FileUtils.copyFile(sourceFile, targetFile);
try (PEFile peFile = new PEFile(targetFile)) {
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD)
.withDigestAlgorithm(SHA1)
.withTimestamping(true)
.withProgramName("WinEyes")
.withProgramURL("http://www.steelblue.com/WinEyes");
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA1);
SignatureAssert.assertTimestamped("Invalid timestamp", peFile.getSignatures().get(0));
// second signature
signer.withDigestAlgorithm(SHA256);
signer.withTimestamping(false);
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA1, SHA256);
SignatureAssert.assertTimestamped("Timestamp corrupted after adding the second signature", peFile.getSignatures().get(0));
}
}
@Test
public void testSignThreeTimes() throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed-three-times.exe");
FileUtils.copyFile(sourceFile, targetFile);
try (PEFile peFile = new PEFile(targetFile)) {
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD)
.withDigestAlgorithm(SHA1)
.withTimestamping(true)
.withProgramName("WinEyes")
.withProgramURL("http://www.steelblue.com/WinEyes");
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA1);
SignatureAssert.assertTimestamped("Invalid timestamp", peFile.getSignatures().get(0));
// second signature
signer.withDigestAlgorithm(SHA256);
signer.withTimestamping(false);
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA1, SHA256);
SignatureAssert.assertTimestamped("Timestamp corrupted after adding the second signature", peFile.getSignatures().get(0));
// third signature
signer.withDigestAlgorithm(SHA512);
signer.withTimestamping(false);
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA1, SHA256, SHA512);
SignatureAssert.assertTimestamped("Timestamp corrupted after adding the third signature", peFile.getSignatures().get(0));
}
}
@Test
public void testReplaceSignature() throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-re-signed.exe");
FileUtils.copyFile(sourceFile, targetFile);
try (PEFile peFile = new PEFile(targetFile)) {
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD)
.withDigestAlgorithm(SHA1)
.withProgramName("WinEyes")
.withProgramURL("http://www.steelblue.com/WinEyes");
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA1);
// second signature
signer.withDigestAlgorithm(SHA256);
signer.withTimestamping(false);
signer.withSignaturesReplaced(true);
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
}
}
@Test
public void testInvalidAuthenticodeTimestampingAuthority() throws Exception {
testInvalidTimestampingAuthority(TimestampingMode.AUTHENTICODE);
}
@Test
public void testInvalidRFC3161TimestampingAuthority() throws Exception {
testInvalidTimestampingAuthority(TimestampingMode.RFC3161);
}
public void testInvalidTimestampingAuthority(TimestampingMode mode) throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-timestamped-unavailable-" + mode.name().toLowerCase() + ".exe");
FileUtils.copyFile(sourceFile, targetFile);
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD);
signer.withDigestAlgorithm(SHA1);
signer.withTimestamping(true);
signer.withTimestampingMode(mode);
signer.withTimestampingAuthority("http://www.google.com/" + mode.name().toLowerCase());
signer.withTimestampingRetries(1);
try (PEFile peFile = new PEFile(targetFile)) {
Exception e = assertThrows(TimestampingException.class, () -> signer.sign(peFile));
assertTrue("Missing suppressed IOException", e.getSuppressed() != null && e.getSuppressed().length > 0 && e.getSuppressed()[0].getClass().equals(IOException.class));
}
SignatureAssert.assertNotSigned(new PEFile(targetFile));
}
@Test
public void testBrokenAuthenticodeTimestampingAuthority() throws Exception {
testBrokenTimestampingAuthority(TimestampingMode.AUTHENTICODE);
}
@Test
public void testBrokenRFC3161TimestampingAuthority() throws Exception {
testBrokenTimestampingAuthority(TimestampingMode.RFC3161);
}
public void testBrokenTimestampingAuthority(TimestampingMode mode) throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-timestamped-broken-" + mode.name().toLowerCase() + ".exe");
FileUtils.copyFile(sourceFile, targetFile);
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD);
signer.withDigestAlgorithm(SHA1);
signer.withTimestamping(true);
signer.withTimestampingMode(mode);
signer.withTimestampingAuthority("http://github.com");
signer.withTimestampingRetries(1);
try (PEFile peFile = new PEFile(targetFile)) {
assertThrows(TimestampingException.class, () -> signer.sign(peFile));
}
SignatureAssert.assertNotSigned(new PEFile(targetFile));
}
@Test
public void testInvalidTimestampingURL() throws Exception {
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD);
signer.withDigestAlgorithm(SHA1);
signer.withTimestamping(true);
signer.withTimestampingMode(TimestampingMode.RFC3161);
signer.withTimestampingAuthority("example://example.com");
signer.withTimestampingRetries(1);
try (PEFile peFile = new PEFile(new File("target/test-classes/wineyes.exe"))) {
assertThrows(IllegalArgumentException.class, () -> signer.sign(peFile));
}
}
@Test
public void testAuthenticodeTimestampingFailover() throws Exception {
testTimestampingFailover(TimestampingMode.AUTHENTICODE, "http://timestamp.sectigo.com");
}
@Test
public void testRFC3161TimestampingFailover() throws Exception {
testTimestampingFailover(TimestampingMode.RFC3161, "http://timestamp.sectigo.com");
}
public void testTimestampingFailover(TimestampingMode mode, String validURL) throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-timestamped-failover-" + mode.name().toLowerCase() + ".exe");
FileUtils.copyFile(sourceFile, targetFile);
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD);
signer.withDigestAlgorithm(SHA256);
signer.withTimestamping(true);
signer.withTimestampingMode(mode);
signer.withTimestampingRetryWait(1);
signer.withTimestampingAuthority("http://www.google.com/" + mode.name().toLowerCase(), "http://github.com", validURL);
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
SignatureAssert.assertTimestamped("Invalid timestamp", peFile.getSignatures().get(0));
}
}
/**
* Tests that it is possible to specify a signature algorithm.
*/
@Test
public void testWithSignatureAlgorithmSHA1withRSA() throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed.exe");
FileUtils.copyFile(sourceFile, targetFile);
try (PEFile peFile = new PEFile(targetFile)) {
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD)
.withTimestamping(false)
.withDigestAlgorithm(SHA256)
.withSignatureAlgorithm("SHA1withRSA");
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA1);
// Check the signature algorithm
CMSSignedData signedData = peFile.getSignatures().get(0);
SignerInformation si = signedData.getSignerInfos().getSigners().iterator().next();
assertEquals("Digest algorithm", SHA1.oid, si.getDigestAlgorithmID().getAlgorithm());
assertEquals("Encryption algorithm", PKCSObjectIdentifiers.rsaEncryption.getId(), si.getEncryptionAlgOID());
}
}
/**
* Tests that it is possible to specify a signature algorithm who's name is
* not simply a concatenation of a digest algorithm and the key algorithm.
*
* This test also sets the signature provider as a provider supporting
* the RSASSA-PSS algorithms might not be installed.
*/
@Test
public void testWithSignatureAlgorithmSHA256withRSAandMGF1() throws Exception {
Security.addProvider(new BouncyCastleProvider());
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed.exe");
FileUtils.copyFile(sourceFile, targetFile);
try (PEFile peFile = new PEFile(targetFile)) {
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD)
.withTimestamping(false)
.withDigestAlgorithm(SHA1)
.withSignatureAlgorithm("SHA256withRSAandMGF1", "BC");
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
CMSSignedData signedData = peFile.getSignatures().get(0);
assertNotNull("signature", signedData);
// Check the signature algorithm
SignerInformation si = signedData.getSignerInfos().getSigners().iterator().next();
assertEquals("Digest algorithm", NISTObjectIdentifiers.id_sha256, si.getDigestAlgorithmID().getAlgorithm());
assertEquals("Encryption algorithm", PKCSObjectIdentifiers.id_RSASSA_PSS.getId(), si.getEncryptionAlgOID());
}
}
@Test
public void testSignWithECKey() throws Exception {
KeyStore keystore = new KeyStoreBuilder().keystore("target/test-classes/keystores/keystore-ec.p12").storepass("password").build();
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed-ec.exe");
FileUtils.copyFile(sourceFile, targetFile);
AuthenticodeSigner signer = new AuthenticodeSigner(keystore, ALIAS, PRIVATE_KEY_PASSWORD)
.withTimestamping(false)
.withProgramName("WinEyes")
.withProgramURL("http://www.steelblue.com/WinEyes");
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
}
}
@Test
public void testSignWithEd25519Key() throws Exception {
Assume.assumeTrue("EdDSA requires Java 15 or higher", SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_15));
KeyStore keystore = new KeyStoreBuilder().keystore("target/test-classes/keystores/keystore-ed25519.p12").storepass("password").build();
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed-ed25519.exe");
FileUtils.copyFile(sourceFile, targetFile);
AuthenticodeSigner signer = new AuthenticodeSigner(keystore, ALIAS, PRIVATE_KEY_PASSWORD).withTimestamping(false);
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
}
}
@Test
public void testSignWithEd448Key() throws Exception {
Assume.assumeTrue("EdDSA requires Java 15 or higher", SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_15));
KeyStore keystore = new KeyStoreBuilder().keystore("target/test-classes/keystores/keystore-ed448.p12").storepass("password").build();
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed-ed448.exe");
FileUtils.copyFile(sourceFile, targetFile);
AuthenticodeSigner signer = new AuthenticodeSigner(keystore, ALIAS, PRIVATE_KEY_PASSWORD).withTimestamping(false);
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
}
}
@Test
public void testContentDigestAlgorithmIdentifier() throws Exception {
// ensure the algorithm identifier has a DER NULL optional parameters field to match the signtool output
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed.exe");
FileUtils.copyFile(sourceFile, targetFile);
AuthenticodeSigner signer = new AuthenticodeSigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD)
.withTimestamping(false)
.withDigestAlgorithm(SHA256);
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
CMSSignedData signature = peFile.getSignatures().get(0);
AlgorithmIdentifier ai = signature.getDigestAlgorithmIDs().iterator().next();
assertEquals("Algorithm identifier", signer.digestAlgorithm.oid, ai.getAlgorithm());
assertEquals("Algorithm parameters", DERNull.INSTANCE, ai.getParameters());
}
}
@Test
public void testSignWithIncompleteChain() throws Exception {
File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed-completed-chain.exe");
FileUtils.copyFile(sourceFile, targetFile);
PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD)
.withTimestamping(false);
try (PEFile peFile = new PEFile(targetFile)) {
signer.sign(peFile);
SignatureAssert.assertSigned(peFile, SHA256);
CMSSignedData signature = peFile.getSignatures().get(0);
Collection<X509CertificateHolder> certificates = signature.getCertificates().getMatches(null);
assertEquals("Number of certificates", 2, certificates.size());
for (X509CertificateHolder certificate : certificates) {
if (certificate.getSubject().toString().equals(certificate.getIssuer().toString())) {
fail("Root certificate found: " + certificate.getSubject());
}
}
}
}
}