diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/binary/JARLauncherBinary.java b/LaunchServer/src/main/java/pro/gravit/launchserver/binary/JARLauncherBinary.java
index e28d0f04..b8082682 100644
--- a/LaunchServer/src/main/java/pro/gravit/launchserver/binary/JARLauncherBinary.java
+++ b/LaunchServer/src/main/java/pro/gravit/launchserver/binary/JARLauncherBinary.java
@@ -47,6 +47,7 @@ public void init() {
tasks.add(new AdditionalFixesApplyTask(server));
if (!server.config.launcher.attachLibraryBeforeProGuard) tasks.add(new AttachJarsTask(server));
if (server.config.launcher.compress) tasks.add(new CompressBuildTask(server));
+ if(server.config.sign.enabled) tasks.add(new SignJarTask(server.config.sign, server));
}
@Override
diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/binary/SignerJar.java b/LaunchServer/src/main/java/pro/gravit/launchserver/binary/SignerJar.java
new file mode 100644
index 00000000..b0d525cb
--- /dev/null
+++ b/LaunchServer/src/main/java/pro/gravit/launchserver/binary/SignerJar.java
@@ -0,0 +1,287 @@
+package pro.gravit.launchserver.binary;
+
+import org.bouncycastle.cms.CMSProcessableByteArray;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.CMSSignedDataGenerator;
+import org.bouncycastle.cms.CMSTypedData;
+import pro.gravit.launchserver.helper.SignHelper;
+import pro.gravit.utils.helper.IOHelper;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+
+/**
+ * Generator of signed Jars. It stores some data in memory therefore it is not suited for creation of large files. The
+ * usage:
+ *
+ * KeyStore keystore = KeyStore.getInstance("JKS");
+ * keyStore.load(keystoreStream, "keystorePassword");
+ * SignerJar jar = new SignerJar(out, keyStore, "keyAlias", "keyPassword");
+ * signedJar.addManifestAttribute("Main-Class", "com.example.MainClass");
+ * signedJar.addManifestAttribute("Application-Name", "Example");
+ * signedJar.addManifestAttribute("Permissions", "all-permissions");
+ * signedJar.addManifestAttribute("Codebase", "*");
+ * signedJar.addFileContents("com/example/MainClass.class", clsData);
+ * signedJar.addFileContents("JNLP-INF/APPLICATION.JNLP", generateJnlpContents());
+ * signedJar.close();
+ *
+ */
+public class SignerJar implements AutoCloseable {
+
+ private static final String MANIFEST_FN = "META-INF/MANIFEST.MF";
+ private static final String SIG_FN = "META-INF/SIGNUMO.SF";
+ private static final String SIG_RSA_FN = "META-INF/SIGNUMO.RSA";
+ private static final String DIGEST_HASH = SignHelper.hashFunctionName + "-Digest";
+
+ private final ZipOutputStream zos;
+
+ private final Map manifestAttributes;
+ private String manifestHash;
+ private String manifestMainHash;
+
+ private final Map fileDigests;
+
+ private final Map sectionDigests;
+ private final Supplier gen;
+
+ public SignerJar(ZipOutputStream out, Supplier gen) {
+ zos = out;
+ this.gen = gen;
+ manifestAttributes = new LinkedHashMap<>();
+ fileDigests = new LinkedHashMap<>();
+ sectionDigests = new LinkedHashMap<>();
+ }
+
+ /**
+ * Adds a file to the JAR. The file is immediately added to the zipped output stream. This method cannot be called once
+ * the stream is closed.
+ *
+ * @param filename name of the file to add (use forward slash as a path separator)
+ * @param contents contents of the file
+ * @throws IOException
+ * @throws NullPointerException if any of the arguments is {@code null}
+ */
+ public void addFileContents(String filename, byte[] contents) throws IOException {
+ addFileContents(filename, new ByteArrayInputStream(contents));
+ }
+
+ /**
+ * Adds a file to the JAR. The file is immediately added to the zipped output stream. This method cannot be called once
+ * the stream is closed.
+ *
+ * @param filename name of the file to add (use forward slash as a path separator)
+ * @param contents contents of the file
+ * @throws IOException
+ * @throws NullPointerException if any of the arguments is {@code null}
+ */
+ public void addFileContents(String filename, InputStream contents) throws IOException {
+ addFileContents(IOHelper.newZipEntry(filename), contents);
+ }
+
+ /**
+ * Adds a file to the JAR. The file is immediately added to the zipped output stream. This method cannot be called once
+ * the stream is closed.
+ *
+ * @param entry name of the file to add (use forward slash as a path separator)
+ * @param contents contents of the file
+ * @throws IOException
+ * @throws NullPointerException if any of the arguments is {@code null}
+ */
+ public void addFileContents(ZipEntry entry, byte[] contents) throws IOException {
+ addFileContents(entry, new ByteArrayInputStream(contents));
+ }
+
+ /**
+ * Adds a file to the JAR. The file is immediately added to the zipped output stream. This method cannot be called once
+ * the stream is closed.
+ *
+ * @param entry name of the file to add (use forward slash as a path separator)
+ * @param contents contents of the file
+ * @throws IOException
+ * @throws NullPointerException if any of the arguments is {@code null}
+ */
+ public void addFileContents(ZipEntry entry, InputStream contents) throws IOException {
+ zos.putNextEntry(entry);
+ SignHelper.HashingOutputStream out = new SignHelper.HashingNonClosingOutputStream(zos, SignHelper.hasher());
+ IOHelper.transfer(contents, out);
+ zos.closeEntry();
+ fileDigests.put(entry.getName(), Base64.getEncoder().encodeToString(out.digest()));
+ }
+
+ /**
+ * Adds a header to the manifest of the JAR.
+ *
+ * @param name name of the attribute, it is placed into the main section of the manifest file
+ * @param value value of the attribute
+ */
+ public void addManifestAttribute(String name, String value) {
+ manifestAttributes.put(name, value);
+ }
+
+
+ /**
+ * Closes the JAR file by writing the manifest and signature data to it and finishing the ZIP entries. It closes the
+ * underlying stream.
+ *
+ * @throws IOException
+ * @throws RuntimeException if the signing goes wrong
+ */
+ @Override
+ public void close() throws IOException {
+ finish();
+ zos.close();
+ }
+
+
+ /**
+ * Finishes the JAR file by writing the manifest and signature data to it and finishing the ZIP entries. It leaves the
+ * underlying stream open.
+ *
+ * @throws IOException
+ * @throws RuntimeException if the signing goes wrong
+ */
+ public void finish() throws IOException {
+ writeManifest();
+ byte[] sig = writeSigFile();
+ writeSignature(sig);
+ zos.finish();
+ }
+
+ public ZipOutputStream getZos() {
+ return zos;
+ }
+
+ /**
+ * Helper for {@link #writeManifest()} that creates the digest of one entry.
+ */
+ private String hashEntrySection(String name, Attributes attributes) throws IOException {
+ Manifest manifest = new Manifest();
+ manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ ByteArrayOutputStream o = new ByteArrayOutputStream();
+ manifest.write(o);
+ int emptyLen = o.toByteArray().length;
+
+ manifest.getEntries().put(name, attributes);
+
+ manifest.write(o);
+ byte[] ob = o.toByteArray();
+ ob = Arrays.copyOfRange(ob, emptyLen, ob.length);
+ return Base64.getEncoder().encodeToString(SignHelper.hasher().digest(ob));
+ }
+
+ /**
+ * Helper for {@link #writeManifest()} that creates the digest of the main section.
+ */
+ private String hashMainSection(Attributes attributes) throws IOException {
+ Manifest manifest = new Manifest();
+ manifest.getMainAttributes().putAll(attributes);
+ SignHelper.HashingOutputStream o = new SignHelper.HashingNonClosingOutputStream(SignHelper.NULL, SignHelper.hasher());
+ manifest.write(o);
+ return Base64.getEncoder().encodeToString(o.digest());
+ }
+
+ /**
+ * Returns the CMS signed data.
+ */
+ private byte[] signSigFile(byte[] sigContents) throws Exception {
+ CMSSignedDataGenerator gen = this.gen.get();
+ CMSTypedData cmsData = new CMSProcessableByteArray(sigContents);
+ CMSSignedData signedData = gen.generate(cmsData, true);
+ return signedData.getEncoded();
+ }
+
+ /**
+ * Writes the manifest to the JAR. It also calculates the digests that are required to be placed in the the signature
+ * file.
+ *
+ * @throws IOException
+ */
+ private void writeManifest() throws IOException {
+ zos.putNextEntry(IOHelper.newZipEntry(MANIFEST_FN));
+ Manifest man = new Manifest();
+
+ // main section
+ Attributes mainAttributes = man.getMainAttributes();
+ mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+
+ for (Map.Entry entry : manifestAttributes.entrySet())
+ mainAttributes.put(new Attributes.Name(entry.getKey()), entry.getValue());
+
+ // individual files sections
+ Attributes.Name digestAttr = new Attributes.Name(DIGEST_HASH);
+ for (Map.Entry entry : fileDigests.entrySet()) {
+ Attributes attributes = new Attributes();
+ man.getEntries().put(entry.getKey(), attributes);
+ attributes.put(digestAttr, entry.getValue());
+ sectionDigests.put(entry.getKey(), hashEntrySection(entry.getKey(), attributes));
+ }
+
+ SignHelper.HashingOutputStream out = new SignHelper.HashingNonClosingOutputStream(zos, SignHelper.hasher());
+ man.write(out);
+ zos.closeEntry();
+
+ manifestHash = Base64.getEncoder().encodeToString(out.digest());
+ manifestMainHash = hashMainSection(man.getMainAttributes());
+ }
+
+ /**
+ * Writes the .SIG file to the JAR.
+ *
+ * @return the contents of the file as bytes
+ */
+ private byte[] writeSigFile() throws IOException {
+ zos.putNextEntry(IOHelper.newZipEntry(SIG_FN));
+ Manifest man = new Manifest();
+ // main section
+ Attributes mainAttributes = man.getMainAttributes();
+ mainAttributes.put(Attributes.Name.SIGNATURE_VERSION, "1.0");
+ mainAttributes.put(new Attributes.Name(DIGEST_HASH + "-Manifest"), manifestHash);
+ mainAttributes.put(new Attributes.Name(DIGEST_HASH + "-Manifest-Main-Attributes"), manifestMainHash);
+
+ // individual files sections
+ Attributes.Name digestAttr = new Attributes.Name(DIGEST_HASH);
+ for (Map.Entry entry : sectionDigests.entrySet()) {
+ Attributes attributes = new Attributes();
+ man.getEntries().put(entry.getKey(), attributes);
+ attributes.put(digestAttr, entry.getValue());
+ }
+
+ man.write(zos);
+ zos.closeEntry();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ man.write(baos);
+ return baos.toByteArray();
+ }
+
+ /**
+ * Signs the .SIG file and writes the signature (.RSA file) to the JAR.
+ *
+ * @throws IOException
+ * @throws RuntimeException if the signing failed
+ */
+ private void writeSignature(byte[] sigFile) throws IOException {
+ zos.putNextEntry(IOHelper.newZipEntry(SIG_RSA_FN));
+ try {
+ byte[] signature = signSigFile(sigFile);
+ zos.write(signature);
+ } catch (IOException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException("Signing failed.", e);
+ }
+ zos.closeEntry();
+ }
+}
\ No newline at end of file
diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/binary/tasks/SignJarTask.java b/LaunchServer/src/main/java/pro/gravit/launchserver/binary/tasks/SignJarTask.java
new file mode 100644
index 00000000..8746b342
--- /dev/null
+++ b/LaunchServer/src/main/java/pro/gravit/launchserver/binary/tasks/SignJarTask.java
@@ -0,0 +1,78 @@
+package pro.gravit.launchserver.binary.tasks;
+
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSSignedDataGenerator;
+import org.bouncycastle.operator.OperatorCreationException;
+import pro.gravit.launchserver.LaunchServer;
+import pro.gravit.launchserver.binary.SignerJar;
+import pro.gravit.launchserver.config.LaunchServerConfig;
+import pro.gravit.launchserver.helper.SignHelper;
+import pro.gravit.utils.helper.IOHelper;
+import pro.gravit.utils.helper.LogHelper;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateEncodingException;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+public class SignJarTask implements LauncherBuildTask {
+
+ private final LaunchServerConfig.JarSignerConf config;
+ private final LaunchServer srv;
+
+ public SignJarTask(LaunchServerConfig.JarSignerConf config, LaunchServer srv) {
+ this.config = config;
+ this.srv = srv;
+ }
+
+ @Override
+ public String getName() {
+ return "SignJar";
+ }
+
+ @Override
+ public Path process(Path inputFile) throws IOException {
+ Path toRet = srv.launcherBinary.nextPath("signed");
+ KeyStore c = SignHelper.getStore(new File(config.keyStore).toPath(), config.keyStorePass, config.keyStoreType);
+ try (SignerJar output = new SignerJar(new ZipOutputStream(IOHelper.newOutput(toRet)), () -> this.gen(c));
+ ZipInputStream input = new ZipInputStream(IOHelper.newInput(inputFile))) {
+ //input.getManifest().getMainAttributes().forEach((a, b) -> output.addManifestAttribute(a.toString(), b.toString())); // may not work such as after Radon.
+ ZipEntry e = input.getNextEntry();
+ while (e != null) {
+ if ("META-INF/MANIFEST.MF".equals(e.getName()) || "/META-INF/MANIFEST.MF".equals(e.getName())) {
+ Manifest m = new Manifest(input);
+ m.getMainAttributes().forEach((a, b) -> output.addManifestAttribute(a.toString(), b.toString()));
+ e = input.getNextEntry();
+ continue;
+ }
+ output.addFileContents(IOHelper.newZipEntry(e), input);
+ e = input.getNextEntry();
+ }
+ }
+ return toRet;
+ }
+
+ @Override
+ public boolean allowDelete() {
+ return true;
+ }
+
+ public CMSSignedDataGenerator gen(KeyStore c) {
+ try {
+ return SignHelper.createSignedDataGenerator(c,
+ config.keyAlias, config.signAlgo, config.keyStorePass);
+ } catch (CertificateEncodingException | UnrecoverableKeyException | KeyStoreException
+ | OperatorCreationException | NoSuchAlgorithmException | CMSException e) {
+ LogHelper.error(e);
+ return null;
+ }
+ }
+}
diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerConfig.java b/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerConfig.java
index fcc9cec0..27b010dd 100644
--- a/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerConfig.java
+++ b/LaunchServer/src/main/java/pro/gravit/launchserver/config/LaunchServerConfig.java
@@ -88,6 +88,7 @@ public AuthProviderPair getAuthProviderPair() {
public String whitelistRejectString;
public LauncherConf launcher;
public CertificateConf certificate;
+ public JarSignerConf sign;
public String startScript;
@@ -225,6 +226,16 @@ public static class CertificateConf {
public boolean enabled;
}
+ public static class JarSignerConf {
+ public boolean enabled = true;
+ public String keyStore = "pathToKey";
+ public String keyStoreType = "JKS";
+ public String keyStorePass = "mypass";
+ public String keyAlias = "myname";
+ public String keyPass = "mypass";
+ public String signAlgo = "SHA256WITHRSA";
+ }
+
public static class NettyUpdatesBind {
public String url;
public boolean zip;
@@ -324,6 +335,7 @@ public static LaunchServerConfig getDefault(LaunchServer.LaunchServerEnv env) {
newConfig.certificate = new LaunchServerConfig.CertificateConf();
newConfig.certificate.enabled = false;
+ newConfig.sign = new JarSignerConf();
newConfig.components = new HashMap<>();
AuthLimiterComponent authLimiterComponent = new AuthLimiterComponent();
diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/helper/SignHelper.java b/LaunchServer/src/main/java/pro/gravit/launchserver/helper/SignHelper.java
new file mode 100644
index 00000000..86e3a165
--- /dev/null
+++ b/LaunchServer/src/main/java/pro/gravit/launchserver/helper/SignHelper.java
@@ -0,0 +1,158 @@
+package pro.gravit.launchserver.helper;
+
+import org.bouncycastle.cert.jcajce.JcaCertStore;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSSignedDataGenerator;
+import org.bouncycastle.cms.SignerInfoGenerator;
+import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.DigestCalculatorProvider;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
+import org.bouncycastle.util.Store;
+import pro.gravit.utils.helper.IOHelper;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class SignHelper {
+
+ /**
+ * Helper output stream that also sends the data to the given.
+ */
+ public static class HashingOutputStream extends OutputStream {
+ public final OutputStream out;
+ public final MessageDigest hasher;
+
+ public HashingOutputStream(OutputStream out, MessageDigest hasher) {
+ this.out = out;
+ this.hasher = hasher;
+ }
+
+ @Override
+ public void close() throws IOException {
+ out.close();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ out.flush();
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ out.write(b);
+ hasher.update(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ out.write(b, off, len);
+ hasher.update(b, off, len);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ out.write(b);
+ hasher.update((byte) b);
+ }
+
+ public byte[] digest() {
+ return hasher.digest();
+ }
+ }
+ /**
+ * Helper output stream that also sends the data to the given.
+ */
+ public static class HashingNonClosingOutputStream extends HashingOutputStream {
+ public HashingNonClosingOutputStream(OutputStream out, MessageDigest hasher) {
+ super(out, hasher);
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Do nothing
+ }
+ }
+
+ public static final OutputStream NULL = new OutputStream() {
+ @Override
+ public String toString() {
+ return "NullOutputStream";
+ }
+
+ /** Discards the specified byte array. */
+ @Override
+ public void write(byte[] b) {
+ }
+
+ /** Discards the specified byte array. */
+ @Override
+ public void write(byte[] b, int off, int len) {
+ }
+
+ /** Discards the specified byte. */
+ @Override
+ public void write(int b) {
+ }
+
+ /** Never closes */
+ @Override
+ public void close() {
+ }
+ };
+ private SignHelper() {
+ }
+
+ /**
+ * Creates the KeyStore with given algo.
+ */
+ public static KeyStore getStore(Path file, String storepass, String algo) throws IOException {
+ try {
+ KeyStore st = KeyStore.getInstance(algo);
+ st.load(IOHelper.newInput(file), storepass != null ? storepass.toCharArray() : null);
+ return st;
+ } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Creates the beast that can actually sign the data (for JKS, for other make it).
+ */
+ public static CMSSignedDataGenerator createSignedDataGenerator(KeyStore keyStore, String keyAlias, String signAlgo, String keyPassword) throws KeyStoreException, OperatorCreationException, CertificateEncodingException, UnrecoverableKeyException, NoSuchAlgorithmException, CMSException {
+ List certChain = new ArrayList<>(Arrays.asList(keyStore.getCertificateChain(keyAlias)));
+ @SuppressWarnings("rawtypes")
+ Store certStore = new JcaCertStore(certChain);
+ Certificate cert = keyStore.getCertificate(keyAlias);
+ PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword != null ? keyPassword.toCharArray() : null);
+ ContentSigner signer = new JcaContentSignerBuilder(signAlgo).setProvider("BC").build(privateKey);
+ CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
+ DigestCalculatorProvider dcp = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
+ SignerInfoGenerator sig = new JcaSignerInfoGeneratorBuilder(dcp).build(signer, (X509Certificate) cert);
+ generator.addSignerInfoGenerator(sig);
+ generator.addCertificates(certStore);
+ return generator;
+ }
+
+ public static final String hashFunctionName = "SHA-256";
+
+ public static MessageDigest hasher() {
+ try {
+ return MessageDigest.getInstance(hashFunctionName);
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ }
+}
diff --git a/modules b/modules
index bcaa597a..6f008f59 160000
--- a/modules
+++ b/modules
@@ -1 +1 @@
-Subproject commit bcaa597a85e3ff749144fabc182e47b26f4f14ef
+Subproject commit 6f008f595975887b6a1366cb444b82b339e3afad