Add signature verification

Closes #1
This commit is contained in:
Minecon724 2024-10-31 16:53:03 +01:00
parent 4be5185cd6
commit 247bdfddf7
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
5 changed files with 203 additions and 7 deletions

View file

@ -1,5 +1,12 @@
package eu.m724.jarupdater.updater;
import eu.m724.jarupdater.download.Downloader;
import eu.m724.jarupdater.environment.Environment;
import eu.m724.jarupdater.live.MetadataFacade;
import eu.m724.jarupdater.object.Version;
import eu.m724.jarupdater.verify.VerificationException;
import eu.m724.jarupdater.verify.Verifier;
import java.io.File;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
@ -7,22 +14,19 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import eu.m724.jarupdater.download.Downloader;
import eu.m724.jarupdater.environment.Environment;
import eu.m724.jarupdater.live.MetadataFacade;
import eu.m724.jarupdater.object.Version;
public class Updater {
protected Environment environment;
protected MetadataFacade metadataProvider;
protected Downloader downloader;
protected Verifier verifier;
protected CompletableFuture<File> downloaded;
public Updater(Environment environment, MetadataFacade metadataProvider, Downloader downloader) {
public Updater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier) {
this.environment = environment;
this.metadataProvider = metadataProvider;
this.downloader = downloader;
this.verifier = verifier;
}
public Environment getEnvironment() {
@ -91,7 +95,14 @@ public class Updater {
if (downloaded == null)
throw new NoSuchFileException("Download it first");
return downloaded.thenCompose(file -> downloader.install(file, environment.getRunningJarFilePath().toFile()));
return downloaded.thenCompose(file -> {
try {
verifier.verify(file.getAbsolutePath());
} catch (VerificationException e) {
throw new CompletionException(e);
}
return downloader.install(file, environment.getRunningJarFilePath().toFile());
});
}
}

View file

@ -0,0 +1,41 @@
package eu.m724.jarupdater.verify;
import java.io.IOException;
import java.io.InputStream;
import java.security.interfaces.RSAPublicKey;
import java.util.HashSet;
import java.util.Set;
/**
* Utility for verifying downloaded JARs with RSA signatures
*/
public class SignatureVerifier implements Verifier {
private final Set<RSAPublicKey> publicKeys = new HashSet<>();
/**
* Load an RSA public key
* @param inputStream the input stream of the public key file
*/
public void loadPublicKey(InputStream inputStream) throws IOException {
RSAPublicKey publicKey = SignatureVerifierImpl.loadPublicKey(inputStream);
publicKeys.add(publicKey);
}
/**
* How much public keys loaded
* @return amount of public keys loaded
*/
public int publicKeysCount() {
return publicKeys.size();
}
/**
* Verify a JAR with loaded public keys
*
* @param jarPath the path of the JAR file
* @throws VerificationException if something went wrong or not signed with known keys
*/
public void verify(String jarPath) throws VerificationException {
SignatureVerifierImpl.verifyWithRsaKey(jarPath, publicKeys.toArray(RSAPublicKey[]::new));
}
}

View file

@ -0,0 +1,128 @@
package eu.m724.jarupdater.verify;
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.ArrayList;
import java.util.Base64;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
public class SignatureVerifierImpl {
/**
* 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
*/
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 publicKeys the {@link RSAPublicKey}s
* @throws VerificationException if verification failed
*/
static void verifyWithRsaKey(String jarPath, RSAPublicKey... publicKeys) throws VerificationException {
try {
// 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;
List<String> signerPublicKeys = new ArrayList<>();
for (CodeSigner signer : signers) {
for (Certificate cert : signer.getSignerCertPath().getCertificates()) {
PublicKey certPublicKey = cert.getPublicKey();
if (certPublicKey instanceof RSAPublicKey rsaKey) {
signerPublicKeys.add(Base64.getEncoder().encodeToString(rsaKey.getEncoded()));
for (RSAPublicKey publicKey : publicKeys) {
if (rsaKey.getModulus().equals(publicKey.getModulus()) &&
rsaKey.getPublicExponent().equals(publicKey.getPublicExponent())) {
keyMatch = true;
break;
}
}
}
}
if (keyMatch) break;
}
if (!keyMatch) {
throw new VerificationException("Entry " + entry.getName() + " signed with unknown key");
}
}
}
} catch (IOException e) {
throw new VerificationException("Verification error: " + e.getMessage(), e);
}
}
}

View file

@ -0,0 +1,11 @@
package eu.m724.jarupdater.verify;
public class VerificationException extends Exception {
public VerificationException(String message) {
super(message);
}
public VerificationException(String message, Exception exception) {
super(message, exception);
}
}

View file

@ -0,0 +1,5 @@
package eu.m724.jarupdater.verify;
public interface Verifier {
void verify(String jarPath) throws VerificationException;
}