Fix updater and add signature verification

This commit is contained in:
Minecon724 2024-10-27 14:42:50 +01:00
parent 956622a345
commit a9d316a901
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
12 changed files with 276 additions and 34 deletions

7
.idea/encodings.xml Normal file
View 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>

View file

@ -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
View file

@ -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>

View file

@ -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;

View file

@ -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
/**

View 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);
}
}
}

View file

@ -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);
}
}

View file

@ -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());
});
}
}
}

View file

@ -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"));
}

View file

@ -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;

View 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

Binary file not shown.