Fix updater and add signature verification
This commit is contained in:
parent
956622a345
commit
a9d316a901
12 changed files with 276 additions and 34 deletions
7
.idea/encodings.xml
Normal file
7
.idea/encodings.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
26
README.md
26
README.md
|
@ -2,10 +2,22 @@
|
|||
|
||||
This plugin adds naturally spawning Giants with AI to your Minecraft server.
|
||||
|
||||
### Requested features
|
||||
- Texture variation \
|
||||
I think this can be done, but would require a texture pack
|
||||
- Boss bar \
|
||||
I don't want to get buried in that, so this would require an API, which, for performance reasons, should not be mandatory
|
||||
- Hitboxes \
|
||||
I don't know if this is still a major issue, but a person reported that the hitbox doesn't cover the whole body
|
||||
### Signing
|
||||
Public key goes into `resources/verifies_downloaded_jars.pem`
|
||||
|
||||
A test (and default) keystore is provided:
|
||||
- keystore: `testkeystore`
|
||||
- storepass: `123456`
|
||||
- alias: `testkey`
|
||||
|
||||
When using `mvn`, override with `-Djarsigner.`
|
||||
```
|
||||
mvn clean package -Djarsigner.keystore=/home/user/mykeystore.jks -Djarsigner.alias=mykey
|
||||
```
|
||||
|
||||
To create a keystore and export public key:
|
||||
```
|
||||
keytool -keystore testkeystore2.jks -genkeypair -keyalg RSA -alias testkey -validity 999999
|
||||
keytool -exportcert -alias testkey -keystore testkeystore2.jks -file cert.cer -rfc
|
||||
openssl x509 -inform pem -in cert.cer -pubkey -noout > public_key.pem
|
||||
```
|
44
pom.xml
44
pom.xml
|
@ -5,8 +5,12 @@
|
|||
<version>2.0.8-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<maven.compiler.release>11</maven.compiler.release>
|
||||
<jarsigner.keystore>${project.basedir}/testkeystore.jks</jarsigner.keystore>
|
||||
<jarsigner.alias>testkey</jarsigner.alias>
|
||||
<jarsigner.storepass>123456</jarsigner.storepass>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
|
@ -53,7 +57,7 @@
|
|||
<dependency>
|
||||
<groupId>eu.m724</groupId>
|
||||
<artifactId>jarupdater</artifactId>
|
||||
<version>0.1.3</version>
|
||||
<version>0.1.5</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
@ -79,6 +83,7 @@
|
|||
<version>3.6.0</version>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<minimizeJar>true</minimizeJar>
|
||||
<!-- <shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<shadedClassifierName>full</shadedClassifierName> -->
|
||||
<relocations>
|
||||
|
@ -93,6 +98,15 @@
|
|||
<include>eu.m724:jarupdater</include>
|
||||
</includes>
|
||||
</artifactSet>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||
<exclude>META-INF/maven/**</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
|
@ -103,6 +117,30 @@
|
|||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jarsigner-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign</id>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>verify</id>
|
||||
<goals>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<keystore>${jarsigner.keystore}</keystore>
|
||||
<alias>${jarsigner.alias}</alias>
|
||||
<storepass>${jarsigner.storepass}</storepass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
package eu.m724.giants;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.*;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDeathEvent;
|
||||
import org.bukkit.event.entity.EntitySpawnEvent;
|
||||
import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
|
||||
import org.bukkit.event.world.ChunkLoadEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package eu.m724.giants;
|
||||
|
||||
import eu.m724.giants.updater.JarVerifier;
|
||||
import eu.m724.giants.updater.PluginUpdater;
|
||||
import eu.m724.giants.updater.UpdateCommand;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
|
@ -10,7 +11,8 @@ import org.bukkit.entity.LivingEntity;
|
|||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class GiantsPlugin extends JavaPlugin implements CommandExecutor {
|
||||
private final File configFile = new File(getDataFolder(), "config.yml");
|
||||
|
@ -28,7 +30,7 @@ public class GiantsPlugin extends JavaPlugin implements CommandExecutor {
|
|||
|
||||
UpdateCommand updateCommand = null;
|
||||
if (configuration.updater != null) {
|
||||
PluginUpdater updater = PluginUpdater.build(this, configuration.updater);
|
||||
PluginUpdater updater = PluginUpdater.build(this, getFile(), configuration.updater);
|
||||
updater.initNotifier();
|
||||
updateCommand = new UpdateCommand(updater);
|
||||
}
|
||||
|
@ -37,7 +39,22 @@ public class GiantsPlugin extends JavaPlugin implements CommandExecutor {
|
|||
|
||||
giantProcessor.start();
|
||||
|
||||
// bStats is optional
|
||||
new Metrics(this, 14131);
|
||||
|
||||
try (InputStream keyInputStream = getResource("verifies_downloaded_jars.pem")) {
|
||||
JarVerifier.verifyWithRsaKey(
|
||||
getFile().getPath().replace(".paper-remapped/", ""), // paper remapping removes data from manifest
|
||||
keyInputStream
|
||||
);
|
||||
} catch (IOException e) {
|
||||
getLogger().warning(e.getMessage());
|
||||
getLogger().warning("Failed checking JAR signature. This is not important right now, but it usually forecasts future problems.");
|
||||
} catch (JarVerifier.VerificationException e) {
|
||||
getLogger().warning(e.getMessage());
|
||||
getLogger().warning("Plugin JAR is of invalid signature. It's possible that the signature has changed, in which case it's normal. But I can't verify that, you must see the version changelog yourself.");
|
||||
}
|
||||
|
||||
/* bStats is optional. not anymore
|
||||
try {
|
||||
Class<?> clazz = Class.forName("eu.m724.giants.bukkit.Metrics");
|
||||
Constructor<?> constructor = clazz.getDeclaredConstructor(JavaPlugin.class, int.class);
|
||||
|
@ -45,10 +62,12 @@ public class GiantsPlugin extends JavaPlugin implements CommandExecutor {
|
|||
getLogger().info("Enabled bStats");
|
||||
} catch (Exception e) {
|
||||
getLogger().info("Not using bStats (" + e.getClass().getName() + ")");
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TODO api, untested
|
||||
|
||||
/**
|
||||
|
|
134
src/main/java/eu/m724/giants/updater/JarVerifier.java
Normal file
134
src/main/java/eu/m724/giants/updater/JarVerifier.java
Normal file
|
@ -0,0 +1,134 @@
|
|||
package eu.m724.giants.updater;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.CodeSigner;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
public class JarVerifier {
|
||||
/**
|
||||
* Loads an RSA public key from a PEM file
|
||||
*
|
||||
* @param keyInputStream inputStream of the public key file
|
||||
* @return {@link RSAPublicKey} instance of the public key
|
||||
* @throws IOException if reading the key input stream failed
|
||||
*/
|
||||
private static RSAPublicKey loadPublicKey(InputStream keyInputStream) throws IOException {
|
||||
// Read the key file
|
||||
String keyContent = new String(keyInputStream.readAllBytes());
|
||||
|
||||
// Remove PEM headers and newlines
|
||||
keyContent = keyContent.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
|
||||
// Decode the key
|
||||
byte[] keyBytes = Base64.getDecoder().decode(keyContent);
|
||||
|
||||
// Create public key specification
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
|
||||
|
||||
// Generate public key
|
||||
try {
|
||||
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||
return (RSAPublicKey) kf.generatePublic(spec);
|
||||
} catch (GeneralSecurityException e) {
|
||||
// because this shouldn't happen
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if a JAR file's signature matches an RSA public key
|
||||
*
|
||||
* @param jarPath the path of the JAR file
|
||||
* @param keyInputStream inputStream of the public key file
|
||||
* @throws VerificationException if verification failed
|
||||
*/
|
||||
public static void verifyWithRsaKey(String jarPath, InputStream keyInputStream) throws VerificationException {
|
||||
try {
|
||||
// Load the RSA public key
|
||||
RSAPublicKey publicKey = loadPublicKey(keyInputStream);
|
||||
|
||||
// Open the JAR file
|
||||
try (JarFile jarFile = new JarFile(jarPath, true)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
|
||||
// Get manifest to check signature files
|
||||
Manifest manifest = jarFile.getManifest();
|
||||
if (manifest == null) {
|
||||
throw new VerificationException("JAR has no manifest");
|
||||
}
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
|
||||
if (entry.isDirectory() || entry.getName().startsWith("META-INF/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int bytesRead = 0;
|
||||
// Read entry to trigger signature verification
|
||||
try (InputStream is = jarFile.getInputStream(entry)) {
|
||||
while ((bytesRead += is.read(buffer)) != -1) {
|
||||
if (bytesRead > 1024 * 1024 * 100) { // unusual for a file to have >100 MiB
|
||||
throw new IOException("File too large: " + entry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get signers for this entry
|
||||
CodeSigner[] signers = entry.getCodeSigners();
|
||||
if (signers == null || signers.length == 0) {
|
||||
throw new VerificationException("Unsigned entry: " + entry.getName());
|
||||
}
|
||||
|
||||
// Check if any signer's public key matches our RSA key
|
||||
boolean keyMatch = false;
|
||||
for (CodeSigner signer : signers) {
|
||||
for (Certificate cert : signer.getSignerCertPath().getCertificates()) {
|
||||
PublicKey certPublicKey = cert.getPublicKey();
|
||||
if (certPublicKey instanceof RSAPublicKey) {
|
||||
RSAPublicKey rsaKey = (RSAPublicKey) certPublicKey;
|
||||
if (rsaKey.getModulus().equals(publicKey.getModulus()) &&
|
||||
rsaKey.getPublicExponent().equals(publicKey.getPublicExponent())) {
|
||||
keyMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (keyMatch) break;
|
||||
}
|
||||
|
||||
if (!keyMatch) {
|
||||
throw new VerificationException("Entry not signed with matching RSA key: " + entry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new VerificationException("Verification error: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class VerificationException extends Exception {
|
||||
public VerificationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public VerificationException(String message, Exception exception) {
|
||||
super(message, exception);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,9 +6,7 @@ import org.bukkit.plugin.Plugin;
|
|||
import java.nio.file.Path;
|
||||
|
||||
public class PluginEnvironment extends ConstantEnvironment {
|
||||
public PluginEnvironment(Plugin plugin, String channel) {
|
||||
super(plugin.getDescription().getVersion(),
|
||||
channel,
|
||||
Path.of(plugin.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()));
|
||||
public PluginEnvironment(Plugin plugin, String channel, Path path) {
|
||||
super(plugin.getDescription().getVersion(), channel, path);
|
||||
}
|
||||
}
|
|
@ -1,24 +1,30 @@
|
|||
package eu.m724.giants.updater;
|
||||
|
||||
import eu.m724.jarupdater.Updater;
|
||||
import eu.m724.jarupdater.download.Downloader;
|
||||
import eu.m724.jarupdater.download.SimpleDownloader;
|
||||
import eu.m724.jarupdater.environment.Environment;
|
||||
import eu.m724.jarupdater.live.GiteaMetadataDAO;
|
||||
import eu.m724.jarupdater.live.MetadataDAO;
|
||||
import eu.m724.jarupdater.live.MetadataFacade;
|
||||
import eu.m724.jarupdater.updater.Updater;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
public class PluginUpdater extends Updater {
|
||||
private final Plugin plugin;
|
||||
boolean updatePending = false;
|
||||
|
||||
private PluginUpdater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Plugin plugin) {
|
||||
super(environment, metadataProvider, downloader);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public static PluginUpdater build(Plugin plugin, String channel) {
|
||||
Environment environment = new PluginEnvironment(plugin, channel);
|
||||
public static PluginUpdater build(Plugin plugin, File file, String channel) {
|
||||
Environment environment = new PluginEnvironment(plugin, channel, file.toPath());
|
||||
MetadataDAO metadataDAO = new GiteaMetadataDAO("https://git.m724.eu/Minecon724/giants-metadata", "master");
|
||||
MetadataFacade metadataFacade = new MetadataFacade(environment, metadataDAO);
|
||||
Downloader downloader = new SimpleDownloader("giants");
|
||||
|
@ -30,4 +36,26 @@ public class PluginUpdater extends Updater {
|
|||
UpdateNotifier updateNotifier = new UpdateNotifier(plugin, this, (version) -> {});
|
||||
updateNotifier.register();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> installLatestVersion() throws NoSuchFileException {
|
||||
return installLatestVersion(true);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> installLatestVersion(boolean verify) throws NoSuchFileException {
|
||||
if (this.downloaded == null) {
|
||||
throw new NoSuchFileException("Download it first");
|
||||
} else {
|
||||
return this.downloaded.thenCompose((file) -> {
|
||||
if (verify) {
|
||||
try {
|
||||
JarVerifier.verifyWithRsaKey(file.getPath(), plugin.getResource("verifies_downloaded_jars.pem"));
|
||||
} catch (JarVerifier.VerificationException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}
|
||||
return this.downloader.install(file, this.environment.getRunningJarFilePath().toFile());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,32 @@
|
|||
package eu.m724.giants.updater;
|
||||
|
||||
import eu.m724.jarupdater.Updater;
|
||||
import eu.m724.jarupdater.object.Version;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* not actually a command but deserves a separate file
|
||||
*/
|
||||
public class UpdateCommand {
|
||||
private final Updater updater;
|
||||
private final PluginUpdater updater;
|
||||
|
||||
public UpdateCommand(Updater updater) {
|
||||
public UpdateCommand(PluginUpdater updater) {
|
||||
this.updater = updater;
|
||||
}
|
||||
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
sender.sendMessage("Please wait...");
|
||||
sender.sendMessage("Channel: " + updater.getEnvironment().getChannel());
|
||||
CompletableFuture<Version> latestFuture = updater.getLatestVersion();
|
||||
|
||||
if (updater.updatePending) {
|
||||
sender.sendMessage("Server restart required");
|
||||
}
|
||||
|
||||
if (args.length == 1) { // remember this function is proxied
|
||||
latestFuture.thenAccept(metadata -> {
|
||||
updater.getLatestVersion().thenAccept(metadata -> {
|
||||
if (metadata != null) {
|
||||
sender.sendMessage("An update is available!");
|
||||
sender.sendMessage("Giants " + metadata.getLabel() + " released " + formatDate(metadata.getTimestamp()));
|
||||
|
@ -70,6 +68,7 @@ public class UpdateCommand {
|
|||
try {
|
||||
updater.installLatestVersion().thenAccept(v -> {
|
||||
sender.sendMessage("Installation completed, restart server to apply.");
|
||||
updater.updatePending = true;
|
||||
}).exceptionally(e -> {
|
||||
sender.sendMessage("Install failed, see console for details. " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
|
@ -86,7 +85,7 @@ public class UpdateCommand {
|
|||
return true;
|
||||
}
|
||||
|
||||
private String formatDate(long timestamp) { // TODO move this
|
||||
private String formatDate(long timestamp) {
|
||||
return LocalDate.ofEpochDay(timestamp / 86400).format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package eu.m724.giants.updater;
|
||||
|
||||
import eu.m724.jarupdater.Updater;
|
||||
import eu.m724.jarupdater.object.Version;
|
||||
import eu.m724.jarupdater.updater.Updater;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
|
|
9
src/main/resources/verifies_downloaded_jars.pem
Normal file
9
src/main/resources/verifies_downloaded_jars.pem
Normal file
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjjjayrwlo3cnv+rX1EX
|
||||
lJN9vHS9MNfvE7zFOHr2JEAx2fRosb2oRzNK0ssoHJFOgrwLWIqrLVS8bTHRujsF
|
||||
asck2Z1RY5UGe34vNQ5u5MZvm4G25LggC6+ei2kEptoAfgp9kjmeKVPiSnruLn7N
|
||||
YQc9U4nmr/vJg+SNmy00EkXFU5z3ZsLf8aCjx9rtogZzyZmVPXEDGY3ZjzZxOpv9
|
||||
TAvSQlmrc6qmLlY7XZmJMtbzCTq+qqemZBKp6WpNmEogpPgXamOrET434+oE7OCz
|
||||
+WCFKsVN8qbrQdFLf1HSjghvDoIjHcGfz6cP4nBonSKIfMcr+NziAVmimfqOiDxa
|
||||
nwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
BIN
testkeystore.jks
Normal file
BIN
testkeystore.jks
Normal file
Binary file not shown.
Loading…
Reference in a new issue