Skip to content

Commit

Permalink
Extract command implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ebourg committed Apr 15, 2024
1 parent c31c942 commit 3edde08
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ See https://ebourg.github.io/jsign for more information.
* The Oracle Cloud signing service has been integrated
* Signing of NuGet packages has been implemented (contributed by Sebastian Stamm)
* Jsign now checks if the certificate subject matches the app manifest publisher before signing APPX/MSIX packages
* The `extract` command has been added to extract the signature from a signed file, in DER or PEM format
* The JCA provider now works with [apksigner](https://developer.android.com/tools/apksigner) for signing Android applications
* The APPX/MSIX bundles are now signed with the correct Authenticode UUID
* The error message displayed when the password of a PKCS#12 keystore is missing has been fixed
Expand Down
6 changes: 6 additions & 0 deletions jsign-cli/src/main/java/net/jsign/JsignCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ public static void main(String... args) {
options.addOption(Option.builder("h").longOpt("help").desc("Print the help").build());

this.options.put("sign", options);

options = new Options();
options.addOption(Option.builder().hasArg().longOpt(PARAM_FORMAT).argName("FORMAT").desc(" The output format of the signature (DER or PEM)").build());

this.options.put("extract", options);
}

void execute(String... args) throws SignerException, ParseException {
Expand Down Expand Up @@ -146,6 +151,7 @@ void execute(String... args) throws SignerException, ParseException {
helper.replace(cmd.hasOption(PARAM_REPLACE));
setOption(PARAM_ENCODING, helper, cmd);
helper.detached(cmd.hasOption(PARAM_DETACHED));
setOption(PARAM_FORMAT, helper, cmd);

if (cmd.getArgList().isEmpty()) {
throw new SignerException("No file specified");
Expand Down
10 changes: 10 additions & 0 deletions jsign-cli/src/test/java/net/jsign/JsignCLITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -543,4 +543,14 @@ public void testUnknownCommand() throws Exception {
assertEquals("exception message", "Unknown command 'unsign'", e.getMessage());
}
}

@Test
public void testExtract() throws Exception {
try {
cli.execute("extract", "" + targetFile);
fail("No exception thrown");
} catch (SignerException e) {
assertEquals("exception message", "No signature found in " + targetFile.getPath(), e.getMessage());
}
}
}
56 changes: 54 additions & 2 deletions jsign-core/src/main/java/net/jsign/SignerHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package net.jsign;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.Authenticator;
import java.net.InetSocketAddress;
Expand All @@ -33,6 +34,7 @@
import java.security.PrivateKey;
import java.security.Provider;
import java.security.cert.Certificate;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
Expand Down Expand Up @@ -76,6 +78,7 @@ class SignerHelper {
public static final String PARAM_REPLACE = "replace";
public static final String PARAM_ENCODING = "encoding";
public static final String PARAM_DETACHED = "detached";
public static final String PARAM_FORMAT = "format";

private final Console console;

Expand All @@ -98,6 +101,7 @@ class SignerHelper {
private boolean replace;
private Charset encoding;
private boolean detached;
private String format;

private AuthenticodeSigner signer;

Expand Down Expand Up @@ -222,6 +226,11 @@ public SignerHelper detached(boolean detached) {
return this;
}

public SignerHelper format(String format) {
this.format = format;
return this;
}

public SignerHelper param(String key, String value) {
if (value == null) {
return this;
Expand Down Expand Up @@ -249,6 +258,7 @@ public SignerHelper param(String key, String value) {
case PARAM_REPLACE: return replace("true".equalsIgnoreCase(value));
case PARAM_ENCODING: return encoding(value);
case PARAM_DETACHED: return detached("true".equalsIgnoreCase(value));
case PARAM_FORMAT: return format(value);
default:
throw new IllegalArgumentException("Unknown " + parameterName + ": " + key);
}
Expand All @@ -267,6 +277,9 @@ public void execute(File file) throws SignerException {
case "sign":
sign(file);
break;
case "extract":
extract(file);
break;
default:
throw new SignerException("Unknown command '" + command + "'");
}
Expand Down Expand Up @@ -443,14 +456,53 @@ private void attach(Signable signable, File detachedSignature) throws IOExceptio
private void detach(Signable signable, File detachedSignature) throws IOException {
CMSSignedData signedData = signable.getSignatures().get(0);
byte[] content = signedData.toASN1Structure().getEncoded("DER");

FileUtils.writeByteArrayToFile(detachedSignature, content);
if (format == null || "DER".equalsIgnoreCase(format)) {
FileUtils.writeByteArrayToFile(detachedSignature, content);
} else if ("PEM".equalsIgnoreCase(format)) {
try (FileWriter out = new FileWriter(detachedSignature)) {
String encoded = Base64.getEncoder().encodeToString(content);
out.write("-----BEGIN PKCS7-----\n");
for (int i = 0; i < encoded.length(); i += 64) {
out.write(encoded.substring(i, Math.min(i + 64, encoded.length())));
out.write('\n');
}
out.write("-----END PKCS7-----\n");
}
} else {
throw new IllegalArgumentException("Unknown output format '" + format + "'");
}
}

private File getDetachedSignature(File file) {
return new File(file.getParentFile(), file.getName() + ".sig");
}

private void extract(File file) throws SignerException {
if (!file.exists()) {
throw new SignerException("Couldn't find " + file);
}

try (Signable signable = Signable.of(file)) {
List<CMSSignedData> signatures = signable.getSignatures();
if (signatures.isEmpty()) {
throw new SignerException("No signature found in " + file);
}

File detachedSignature = getDetachedSignature(file);
if ("PEM".equalsIgnoreCase(format)) {
detachedSignature = new File(detachedSignature.getParentFile(), detachedSignature.getName() + ".pem");
}
console.info("Extracting signature to " + detachedSignature);
detach(signable, detachedSignature);
} catch (UnsupportedOperationException | IllegalArgumentException e) {
throw new SignerException(e.getMessage());
} catch (SignerException e) {
throw e;
} catch (Exception e) {
throw new SignerException("Couldn't extract the signature from " + file, e);
}
}

/**
* Initializes the proxy.
*
Expand Down
95 changes: 95 additions & 0 deletions jsign-core/src/test/java/net/jsign/SignerHelperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -421,4 +421,99 @@ public void testUnknownCommand() {
assertEquals("message", "Unknown command 'unsign'", e.getMessage());
}
}

@Test
public void testExtractDER() 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);

SignerHelper signer = new SignerHelper(new StdOutConsole(2), "parameter")
.keystore("target/test-classes/keystores/keystore.jks")
.keypass("password");

signer.execute(targetFile);

signer.command("extract");
signer.execute(targetFile);

File signatureFile = new File("target/test-classes/wineyes-signed.exe.sig");
assertTrue("Signature not extracted", signatureFile.exists());
}

@Test
public void testExtractPEM() 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);

SignerHelper signer = new SignerHelper(new StdOutConsole(2), "parameter")
.keystore("target/test-classes/keystores/keystore.jks")
.keypass("password");

signer.execute(targetFile);

signer.command("extract");
signer.format("PEM");
signer.execute(targetFile);

File signatureFile = new File("target/test-classes/wineyes-signed.exe.sig.pem");
assertTrue("Signature not extracted", signatureFile.exists());
}

@Test
public void testExtractWithInvalidFormat() 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);

SignerHelper signer = new SignerHelper(new StdOutConsole(2), "parameter")
.keystore("target/test-classes/keystores/keystore.jks")
.keypass("password");

signer.execute(targetFile);

signer.command("extract");
signer.format("TXT");

try {
signer.execute(targetFile);
fail("No exception thrown");
} catch (SignerException e) {
assertEquals("message", "Unknown output format 'TXT'", e.getMessage());
}
}

@Test
public void testExtractFromUnsignedFile() {
File file = new File("target/test-classes/wineyes.exe");

SignerHelper signer = new SignerHelper(new StdOutConsole(2), "parameter");
signer.command("extract");

try {
signer.execute(file);
fail("No exception thrown");
} catch (SignerException e) {
assertEquals("message", "No signature found in target/test-classes/wineyes.exe", e.getMessage().replace('\\', '/'));
}
}

@Test
public void testExtractFromMissingFile() {
File file = new File("target/test-classes/xeyes.exe");

SignerHelper signer = new SignerHelper(new StdOutConsole(2), "parameter");
signer.command("extract");

try {
signer.execute(file);
fail("No exception thrown");
} catch (SignerException e) {
assertEquals("message", "Couldn't find target/test-classes/xeyes.exe", e.getMessage().replace('\\', '/'));
}
}
}

0 comments on commit 3edde08

Please sign in to comment.