parent
4be5185cd6
commit
247bdfddf7
5 changed files with 203 additions and 7 deletions
|
@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
5
src/main/java/eu/m724/jarupdater/verify/Verifier.java
Normal file
5
src/main/java/eu/m724/jarupdater/verify/Verifier.java
Normal file
|
@ -0,0 +1,5 @@
|
|||
package eu.m724.jarupdater.verify;
|
||||
|
||||
public interface Verifier {
|
||||
void verify(String jarPath) throws VerificationException;
|
||||
}
|
Loading…
Reference in a new issue