Add signature reporting for mods, using new signature capture from ModLauncher. Need to figure out

how to reflect Minecraft's JAR signatures into here.

Signed-off-by: cpw <cpw+github@weeksfamily.ca>
This commit is contained in:
cpw 2020-10-25 22:26:16 -04:00
parent 6ff6277efa
commit ae160cad12
No known key found for this signature in database
GPG key ID: 8EB3DF749553B1B7
12 changed files with 132 additions and 67 deletions

View file

@ -435,11 +435,11 @@ project(':forge') {
installer 'org.ow2.asm:asm-tree:7.2' installer 'org.ow2.asm:asm-tree:7.2'
installer 'org.ow2.asm:asm-util:7.2' installer 'org.ow2.asm:asm-util:7.2'
installer 'org.ow2.asm:asm-analysis:7.2' installer 'org.ow2.asm:asm-analysis:7.2'
installer 'cpw.mods:modlauncher:7.0.+' installer 'cpw.mods:modlauncher:8.0.+'
installer 'cpw.mods:grossjava9hacks:1.3.+' installer 'cpw.mods:grossjava9hacks:1.3.+'
installer 'net.minecraftforge:accesstransformers:2.2.+:shadowed' installer 'net.minecraftforge:accesstransformers:2.2.+:shadowed'
installer 'net.minecraftforge:eventbus:3.0.+:service' installer 'net.minecraftforge:eventbus:3.0.+:service'
installer 'net.minecraftforge:forgespi:3.1.+' installer 'net.minecraftforge:forgespi:3.2.+'
installer 'net.minecraftforge:coremods:3.0.+' installer 'net.minecraftforge:coremods:3.0.+'
installer 'net.minecraftforge:unsafe:0.2.+' installer 'net.minecraftforge:unsafe:0.2.+'
installer 'com.electronwill.night-config:core:3.6.2' installer 'com.electronwill.night-config:core:3.6.2'

View file

@ -50,4 +50,8 @@ public class Java9BackportUtils
emptyAction.run(); emptyAction.run();
} }
} }
public static <T> Stream<T> toStream(final Optional<T> optional) {
return optional.map(Stream::of).orElseGet(Stream::empty);
}
} }

View file

@ -19,6 +19,7 @@
package net.minecraftforge.fml.loading; package net.minecraftforge.fml.loading;
import cpw.mods.modlauncher.api.LamdbaExceptionUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@ -83,6 +84,12 @@ public class ModJarURLHandler extends URLStreamHandler
} }
} }
// Used to cache protectiondomains by "top level object" aka the modid
@Override
public URL getURL() {
return LamdbaExceptionUtils.uncheck(()->new URL("modjar://"+modid));
}
public Optional<Manifest> getManifest() { public Optional<Manifest> getManifest() {
return manifest; return manifest;
} }

View file

@ -55,4 +55,13 @@ public class StringUtils
public static String parseStringFormat(final String input, final Map<String, String> properties) { public static String parseStringFormat(final String input, final Map<String, String> properties) {
return StrSubstitutor.replace(input, properties); return StrSubstitutor.replace(input, properties);
} }
public static String binToHex(final byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
sb.append(Integer.toHexString((bytes[i]&0xf0) >>4));
sb.append(Integer.toHexString(bytes[i]&0x0f));
}
return sb.toString();
}
} }

View file

@ -19,20 +19,25 @@
package net.minecraftforge.fml.loading.moddiscovery; package net.minecraftforge.fml.loading.moddiscovery;
import cpw.mods.modlauncher.api.LamdbaExceptionUtils;
import net.minecraftforge.forgespi.locating.IModFile; import net.minecraftforge.forgespi.locating.IModFile;
import net.minecraftforge.forgespi.locating.IModLocator; import net.minecraftforge.forgespi.locating.IModLocator;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.CodeSigner;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -79,20 +84,31 @@ public abstract class AbstractJarFileLocator implements IModLocator {
LOGGER.debug(SCAN,"Scan finished: {}", file); LOGGER.debug(SCAN,"Scan finished: {}", file);
} }
static final Method ENSURE_INIT = LamdbaExceptionUtils.uncheck(()->JarFile.class.getDeclaredMethod("ensureInitialization"));
static {
ENSURE_INIT.setAccessible(true);
}
@Override @Override
public Optional<Manifest> findManifest(final Path file) public Optional<Manifest> findManifest(final Path file)
{ {
return findManifestAndSigners(file).getKey();
}
@Override
public Pair<Optional<Manifest>, Optional<CodeSigner[]>> findManifestAndSigners(final Path file) {
try (JarFile jf = new JarFile(file.toFile())) try (JarFile jf = new JarFile(file.toFile()))
{ {
final Manifest manifest = jf.getManifest(); final Manifest manifest = jf.getManifest();
if (manifest != null) { if (manifest!=null) {
// injection of signing stuff here final JarEntry jarEntry = jf.getJarEntry(JarFile.MANIFEST_NAME);
LamdbaExceptionUtils.uncheck(() -> ENSURE_INIT.invoke(jf));
return Pair.of(Optional.of(manifest), Optional.ofNullable(jarEntry.getCodeSigners()));
} }
return Optional.ofNullable(manifest); return Pair.of(Optional.empty(), Optional.empty());
} }
catch (IOException e) catch (IOException e)
{ {
return Optional.empty(); return Pair.of(Optional.empty(), Optional.empty());
} }
} }

View file

@ -32,6 +32,7 @@ import net.minecraftforge.fml.loading.progress.StartupMessageManager;
import net.minecraftforge.forgespi.Environment; import net.minecraftforge.forgespi.Environment;
import net.minecraftforge.forgespi.locating.IModFile; import net.minecraftforge.forgespi.locating.IModFile;
import net.minecraftforge.forgespi.locating.IModLocator; import net.minecraftforge.forgespi.locating.IModLocator;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@ -45,6 +46,7 @@ import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.security.CodeSigner;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -57,6 +59,8 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -215,6 +219,24 @@ public class ModDiscoverer {
LOGGER.debug(SCAN,"Scan finished: {}", modFile); LOGGER.debug(SCAN,"Scan finished: {}", modFile);
} }
@Override
public Pair<Optional<Manifest>, Optional<CodeSigner[]>> findManifestAndSigners(final Path file) {
if (Files.isDirectory(mcJar)) {
return Pair.of(Optional.empty(), Optional.empty());
}
try (JarFile jf = new JarFile(mcJar.toFile())) {
final Manifest manifest = jf.getManifest();
if (manifest!=null) {
final JarEntry jarEntry = jf.getJarEntry(JarFile.MANIFEST_NAME);
LamdbaExceptionUtils.uncheck(() -> AbstractJarFileLocator.ENSURE_INIT.invoke(jf));
return Pair.of(Optional.of(manifest), Optional.ofNullable(jarEntry.getCodeSigners()));
}
} catch (IOException ioe) {
return Pair.of(Optional.empty(), Optional.empty());
}
return Pair.of(Optional.empty(), Optional.empty());
}
@Override @Override
public Optional<Manifest> findManifest(final Path file) { public Optional<Manifest> findManifest(final Path file) {
return Optional.empty(); return Optional.empty();

View file

@ -20,16 +20,29 @@
package net.minecraftforge.fml.loading.moddiscovery; package net.minecraftforge.fml.loading.moddiscovery;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import cpw.mods.modlauncher.api.LamdbaExceptionUtils;
import net.minecraftforge.fml.loading.Java9BackportUtils;
import net.minecraftforge.fml.loading.StringUtils; import net.minecraftforge.fml.loading.StringUtils;
import net.minecraftforge.forgespi.language.IConfigurable; import net.minecraftforge.forgespi.language.IConfigurable;
import net.minecraftforge.forgespi.language.IModFileInfo; import net.minecraftforge.forgespi.language.IModFileInfo;
import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.forgespi.language.MavenVersionAdapter; import net.minecraftforge.forgespi.language.MavenVersionAdapter;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.artifact.versioning.VersionRange;
import javax.security.auth.x500.X500Principal;
import java.net.URL; import java.net.URL;
import java.security.CodeSigner;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -55,6 +68,7 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
// Caches the manifest of the mod jar as parsing the manifest can be expensive for // Caches the manifest of the mod jar as parsing the manifest can be expensive for
// signed jars. // signed jars.
private final Optional<Manifest> manifest; private final Optional<Manifest> manifest;
private final Optional<CodeSigner[]> signers;
ModFileInfo(final ModFile modFile, final IConfigurable config) ModFileInfo(final ModFile modFile, final IConfigurable config)
{ {
@ -83,7 +97,9 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
this.modFile::getFileName, this.modFile::getFileName,
() -> this.mods.stream().map(IModInfo::getModId).collect(Collectors.joining(",", "{", "}")), () -> this.mods.stream().map(IModInfo::getModId).collect(Collectors.joining(",", "{", "}")),
() -> this.mods.stream().map(IModInfo::getVersion).map(Objects::toString).collect(Collectors.joining(",", "{", "}"))); () -> this.mods.stream().map(IModInfo::getVersion).map(Objects::toString).collect(Collectors.joining(",", "{", "}")));
this.manifest = modFile.getLocator().findManifest(modFile.getFilePath()); final Pair<Optional<Manifest>, Optional<CodeSigner[]>> manifestAndSigners = modFile.getLocator().findManifestAndSigners(modFile.getFilePath());
this.manifest = manifestAndSigners.getKey();
this.signers = manifestAndSigners.getValue();
} }
@Override @Override
@ -153,4 +169,42 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
{ {
return Strings.isNullOrEmpty(license); return Strings.isNullOrEmpty(license);
} }
public Optional<CodeSigner[]> getCodeSigners() {
return this.signers;
}
public Optional<String> getCodeSigningFingerprint() {
return Java9BackportUtils.toStream(this.signers)
.flatMap(csa->csa[0].getSignerCertPath().getCertificates().stream())
.findFirst()
.map(LamdbaExceptionUtils.rethrowFunction(Certificate::getEncoded))
.map(bytes->LamdbaExceptionUtils.uncheck(()->MessageDigest.getInstance("SHA-256")).digest(bytes))
.map(StringUtils::binToHex)
.map(str-> String.join(":", str.split("(?<=\\G.{2})")));
}
public Optional<String> getTrustData() {
return Java9BackportUtils.toStream(this.signers)
.flatMap(csa->csa[0].getSignerCertPath().getCertificates().stream())
.findFirst()
.map(X509Certificate.class::cast)
.map(c->{
StringBuffer sb = new StringBuffer();
sb.append(c.getSubjectX500Principal().getName(X500Principal.RFC2253).split(",")[0]);
boolean selfSigned = false;
try {
c.verify(c.getPublicKey());
selfSigned = true;
} catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) {
// not self signed
}
if (selfSigned) {
sb.append(" self-signed");
} else {
sb.append(" signed by ").append(c.getIssuerX500Principal().getName(X500Principal.RFC2253).split(",")[0]);
};
return sb.toString();
});
}
} }

View file

@ -19,7 +19,6 @@
package net.minecraftforge.fml; package net.minecraftforge.fml;
import cpw.mods.modlauncher.api.LamdbaExceptionUtils;
import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.lifecycle.IModBusEvent; import net.minecraftforge.fml.event.lifecycle.IModBusEvent;

View file

@ -82,9 +82,12 @@ public class ModList
} }
private String fileToLine(ModFile mf) { private String fileToLine(ModFile mf) {
return mf.getFileName() + " " + mf.getModInfos().get(0).getDisplayName() + " " + return String.format("%-50.50s|%-30.30s|%-30.30s|%-20.20s|%-10.10s|%s", mf.getFileName(),
mf.getModInfos().stream().map(mi -> mi.getModId() + "@" + mi.getVersion() + " " + mf.getModInfos().get(0).getDisplayName(),
getModContainerState(mi.getModId())).collect(Collectors.joining(", ", "{", "}")); mf.getModInfos().get(0).getModId(),
mf.getModInfos().get(0).getVersion(),
getModContainerState(mf.getModInfos().get(0).getModId()),
((ModFileInfo)mf.getModFileInfo()).getCodeSigningFingerprint().orElse("NOSIGNATURE"));
} }
private String crashReport() { private String crashReport() {
return "\n"+applyForEachModFile(this::fileToLine).collect(Collectors.joining("\n\t\t", "\t\t", "")); return "\n"+applyForEachModFile(this::fileToLine).collect(Collectors.joining("\n\t\t", "\t\t", ""));

View file

@ -41,7 +41,6 @@ import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.RenderComponentsUtil;
import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.client.gui.widget.button.Button; import net.minecraft.client.gui.widget.button.Button;
@ -50,7 +49,6 @@ import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.NativeImage; import net.minecraft.client.renderer.texture.NativeImage;
import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.IReorderingProcessor; import net.minecraft.util.IReorderingProcessor;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocation;
import net.minecraft.util.Util; import net.minecraft.util.Util;
@ -462,6 +460,9 @@ public class ModListScreen extends Screen
lines.add(ForgeI18n.parseMessage("fml.menu.mods.info.license", selectedMod.getOwningFile().getLicense())); lines.add(ForgeI18n.parseMessage("fml.menu.mods.info.license", selectedMod.getOwningFile().getLicense()));
lines.add(null); lines.add(null);
lines.add(selectedMod.getDescription()); lines.add(selectedMod.getDescription());
lines.add(null);
lines.add(ForgeI18n.parseMessage("fml.menu.mods.info.signature", selectedMod.getOwningFile().getCodeSigningFingerprint().orElse(ForgeI18n.parseMessage("fml.menu.mods.info.signature.unsigned"))));
lines.add(ForgeI18n.parseMessage("fml.menu.mods.info.trust", selectedMod.getOwningFile().getTrustData().orElse(ForgeI18n.parseMessage("fml.menu.mods.info.trust.noauthority"))));
if ((vercheck.status == VersionChecker.Status.OUTDATED || vercheck.status == VersionChecker.Status.BETA_OUTDATED) && vercheck.changes.size() > 0) if ((vercheck.status == VersionChecker.Status.OUTDATED || vercheck.status == VersionChecker.Status.BETA_OUTDATED) && vercheck.changes.size() > 0)
{ {

View file

@ -1,54 +0,0 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2020.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.event.lifecycle;
import java.io.File;
import java.util.Set;
import com.google.common.collect.ImmutableSet;
import net.minecraftforge.fml.common.Mod;
/**
* A special event used when the {@link Mod#certificateFingerprint()} doesn't match the certificate loaded from the JAR
* file. You could use this to log a warning that the code that is running might not be yours, for example.
*/
public class FMLFingerprintViolationEvent extends ModLifecycleEvent
{
private final boolean isDirectory;
private final Set<String> fingerprints;
private final File source;
private final String expectedFingerprint;
public FMLFingerprintViolationEvent(boolean isDirectory, File source, ImmutableSet<String> fingerprints, String expectedFingerprint)
{
super(null);
this.isDirectory = isDirectory;
this.source = source;
this.fingerprints = fingerprints;
this.expectedFingerprint = expectedFingerprint;
}
public boolean isDirectory() { return isDirectory; }
public Set<String> getFingerprints() { return fingerprints; }
public File getSource() { return source; }
public String getExpectedFingerprint() { return expectedFingerprint; }
}

View file

@ -14,6 +14,10 @@
"fml.menu.mods.info.authors":"Authors: {0}", "fml.menu.mods.info.authors":"Authors: {0}",
"fml.menu.mods.info.displayurl":"Homepage: {0}", "fml.menu.mods.info.displayurl":"Homepage: {0}",
"fml.menu.mods.info.license":"License: {0}", "fml.menu.mods.info.license":"License: {0}",
"fml.menu.mods.info.signature":"Signature: {0}",
"fml.menu.mods.info.signature.unsigned":"UNSIGNED",
"fml.menu.mods.info.trust": "Trust: {0}",
"fml.menu.mods.info.trust.noauthority": "None",
"fml.menu.mods.info.nochildmods":"No child mods found", "fml.menu.mods.info.nochildmods":"No child mods found",
"fml.menu.mods.info.childmods":"Child mods: {0}", "fml.menu.mods.info.childmods":"Child mods: {0}",
"fml.menu.mods.info.updateavailable":"Update available: {0}", "fml.menu.mods.info.updateavailable":"Update available: {0}",