diff --git a/build.gradle b/build.gradle index 6a1fd781..777dd96c 100644 --- a/build.gradle +++ b/build.gradle @@ -155,6 +155,9 @@ dependencies { } testImplementation testLibs.mockito testImplementation testLibs.java.debug + testImplementation testLibs.bcprov + testImplementation testLibs.bcutil + testImplementation testLibs.bcpkix compileOnly runtimeLibs.jetbrains.annotations testCompileOnly runtimeLibs.jetbrains.annotations diff --git a/gradle/test.libs.versions.toml b/gradle/test.libs.versions.toml index df35b023..651dfb70 100644 --- a/gradle/test.libs.versions.toml +++ b/gradle/test.libs.versions.toml @@ -5,6 +5,7 @@ javalin = "6.3.0" mockito = "5.14.2" java-debug = "0.52.0" mixin = "0.15.3+mixin.0.8.7" +bouncycastle = "1.80" gradle-nightly = "8.14-20250225001625+0000" fabric-loader = "0.16.9" @@ -18,4 +19,7 @@ mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" } java-debug = { module = "com.microsoft.java:com.microsoft.java.debug.core", version.ref = "java-debug" } mixin = { module = "net.fabricmc:sponge-mixin", version.ref = "mixin" } gradle-nightly = { module = "org.gradle:dummy", version.ref = "gradle-nightly" } -fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } \ No newline at end of file +fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } +bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" } +bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" } +bcutil = { module = "org.bouncycastle:bcutil-jdk18on", version.ref = "bouncycastle" } \ No newline at end of file diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java index 3f2d4437..59c11584 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java @@ -93,7 +93,6 @@ public final class MergedMinecraftProvider extends MinecraftProvider { File minecraftServerJar = getMinecraftServerJar(); if (getServerBundleMetadata() != null) { - extractBundledServerJar(); minecraftServerJar = getMinecraftExtractedServerJar(); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java index b789a3b0..2aed7d1e 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import com.google.common.base.Preconditions; import org.gradle.api.JavaVersion; @@ -41,9 +42,12 @@ import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.providers.BundleMetadata; +import net.fabricmc.loom.configuration.providers.minecraft.verify.MinecraftJarVerification; +import net.fabricmc.loom.configuration.providers.minecraft.verify.SignatureVerificationFailure; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.download.DownloadExecutor; import net.fabricmc.loom.util.download.GradleDownloadProgressListener; +import net.fabricmc.loom.util.gradle.GradleUtils; import net.fabricmc.loom.util.gradle.ProgressGroup; public abstract class MinecraftProvider { @@ -88,10 +92,18 @@ public abstract class MinecraftProvider { } } - downloadJars(); + boolean didDownload = downloadJars(); if (provideServer()) { serverBundleMetadata = BundleMetadata.fromJar(minecraftServerJar.toPath()); + + if (serverBundleMetadata != null) { + extractBundledServerJar(); + } + } + + if (didDownload) { + verifyJars(); } final MinecraftLibraryProvider libraryProvider = new MinecraftLibraryProvider(this, configContext.project()); @@ -109,7 +121,34 @@ public abstract class MinecraftProvider { } } - private void downloadJars() throws IOException { + private void verifyJars() throws IOException, SignatureVerificationFailure { + if (GradleUtils.getBooleanProperty(getProject(), Constants.Properties.DISABLE_MINECRAFT_VERIFICATION)) { + LOGGER.info("Skipping Minecraft jar verification!"); + } + + LOGGER.info("Verifying Minecraft jars"); + + MinecraftJarVerification verification = getProject().getObjects().newInstance(MinecraftJarVerification.class, minecraftVersion()); + + if (provideClient()) { + verification.verifyClientJar(minecraftClientJar.toPath()); + } + + if (provideServer()) { + if (serverBundleMetadata == null) { + verification.verifyServerJar(minecraftServerJar.toPath()); + } else { + verification.verifyServerJar(getMinecraftExtractedServerJar().toPath()); + } + } + + LOGGER.info("Jar verification complete"); + } + + // Returns true when a file was downloaded + private boolean downloadJars() throws IOException { + AtomicBoolean didDownload = new AtomicBoolean(false); + try (ProgressGroup progressGroup = new ProgressGroup(getProject(), "Download Minecraft jars"); DownloadExecutor executor = new DownloadExecutor(2)) { if (provideClient()) { @@ -117,7 +156,12 @@ public abstract class MinecraftProvider { getExtension().download(client.url()) .sha1(client.sha1()) .progress(new GradleDownloadProgressListener("Minecraft client", progressGroup::createProgressLogger)) - .downloadPathAsync(minecraftClientJar.toPath(), executor); + .downloadPathAsync(minecraftClientJar.toPath(), executor) + .thenAccept(downloadResult -> { + if (downloadResult.didDownload()) { + didDownload.set(true); + } + }); } if (provideServer()) { @@ -125,12 +169,25 @@ public abstract class MinecraftProvider { getExtension().download(server.url()) .sha1(server.sha1()) .progress(new GradleDownloadProgressListener("Minecraft server", progressGroup::createProgressLogger)) - .downloadPathAsync(minecraftServerJar.toPath(), executor); + .downloadPathAsync(minecraftServerJar.toPath(), executor) + .thenAccept(downloadResult -> { + if (downloadResult.didDownload()) { + didDownload.set(true); + } + }); } } + + if (didDownload.get()) { + LOGGER.info("Downloaded new Minecraft jars"); + return true; + } + + LOGGER.info("Using cached Minecraft jars"); + return false; } - protected final void extractBundledServerJar() throws IOException { + private void extractBundledServerJar() throws IOException { Preconditions.checkArgument(provideServer(), "Not configured to provide server jar"); Objects.requireNonNull(getServerBundleMetadata(), "Cannot bundled mc jar from none bundled server jar"); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java index c96564fa..ee682109 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java @@ -139,14 +139,13 @@ public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvide } @Override - public Path getInputJar(SingleJarMinecraftProvider provider) throws Exception { + public Path getInputJar(SingleJarMinecraftProvider provider) { BundleMetadata serverBundleMetadata = provider.getServerBundleMetadata(); if (serverBundleMetadata == null) { return provider.getMinecraftServerJar().toPath(); } - provider.extractBundledServerJar(); return provider.getMinecraftExtractedServerJar().toPath(); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java index d2190142..8b56df43 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java @@ -74,8 +74,6 @@ public final class SplitMinecraftProvider extends MinecraftProvider { throw new UnsupportedOperationException("Only Minecraft versions using a bundled server jar can be split, please use a merged jar setup for this version of minecraft"); } - extractBundledServerJar(); - final Path clientJar = getMinecraftClientJar().toPath(); final Path serverJar = getMinecraftExtractedServerJar().toPath(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/CertificateChain.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/CertificateChain.java new file mode 100644 index 00000000..e0c16f45 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/CertificateChain.java @@ -0,0 +1,194 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.providers.minecraft.verify; + +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +/** + * A node in the certificate chain. + */ +public interface CertificateChain { + /** + * The certificate itself. + */ + X509Certificate certificate(); + + /** + * The issuer of this certificate, or null if this is a root certificate. + */ + @Nullable CertificateChain issuer(); + + /** + * The children of this certificate, or an empty list if this is a leaf certificate. + */ + List children(); + + /** + * Verify that this certificate chain matches exactly with another one. + * @param other the other certificate chain + */ + void verifyChainMatches(CertificateChain other) throws SignatureVerificationFailure; + + /** + * Recursively visit all certificates in the chain, including this one. + */ + static void visitAll(CertificateChain chain, CertificateConsumer consumer) throws SignatureVerificationFailure { + consumer.accept(chain.certificate()); + + for (CertificateChain child : chain.children()) { + visitAll(child, consumer); + } + } + + /** + * Load certificate chain from the classpath, returning the root certificate. + */ + static CertificateChain getRoot(String name) throws IOException { + try (InputStream is = JarVerifier.class.getClassLoader().getResourceAsStream("certs/" + name + ".cer")) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Collection certificates = cf.generateCertificates(is).stream() + .map(c -> (X509Certificate) c) + .toList(); + return getRoot(certificates); + } catch (CertificateException e) { + throw new RuntimeException("Failed to load certificate: " + name, e); + } + } + + /** + * Takes an unordered collection of certificates and builds a tree structure. + */ + static CertificateChain getRoot(Collection certificates) { + Map certificateNodes = new HashMap<>(); + + for (X509Certificate certificate : certificates) { + Impl node = new Impl(); + node.certificate = certificate; + certificateNodes.put(certificate.getSubjectX500Principal().getName(), node); + } + + for (X509Certificate certificate : certificates) { + String subject = certificate.getSubjectX500Principal().getName(); + String issuer = certificate.getIssuerX500Principal().getName(); + + if (subject.equals(issuer)) { + continue; // self-signed + } + + Impl parent = certificateNodes.get(issuer); + Impl self = certificateNodes.get(subject); + + if (parent == self) { + throw new IllegalStateException("Certificate " + subject + " is its own issuer"); + } + + if (parent == null) { + throw new IllegalStateException("Certificate " + subject + " defines issuer " + issuer + " which is not in the chain"); + } + + parent.children.add(self); + self.issuer = parent; + } + + List roots = certificateNodes.values() + .stream() + .filter(node -> node.issuer == null) + .toList(); + + if (roots.size() != 1) { + throw new IllegalStateException("Expected exactly one root certificate, but found " + roots.size()); + } + + return roots.get(0); + } + + @FunctionalInterface + interface CertificateConsumer { + void accept(X509Certificate certificate) throws SignatureVerificationFailure; + } + + class Impl implements CertificateChain { + X509Certificate certificate; + @Nullable CertificateChain.Impl issuer; + List children = new ArrayList<>(); + + private Impl() { + } + + @Override + public X509Certificate certificate() { + return certificate; + } + + @Override + public @Nullable CertificateChain issuer() { + return issuer; + } + + @Override + public List children() { + return children; + } + + @Override + public void verifyChainMatches(CertificateChain other) throws SignatureVerificationFailure { + if (!this.certificate().equals(other.certificate())) { + throw new SignatureVerificationFailure("Certificate mismatch: " + this + " != " + other); + } + + if (this.children().size() != other.children().size()) { + throw new SignatureVerificationFailure("Certificate mismatch: " + this + " has " + this.children().size() + " children, but " + other + " has " + other.children().size()); + } + + if (this.children.isEmpty()) { + // Fine, leaf certificate + return; + } + + if (this.children.size() != 1) { + // TODO support this, not needed currently + throw new UnsupportedOperationException("Validating Certificate chain with multiple children is not supported"); + } + + this.children.get(0).verifyChainMatches(other.children().get(0)); + } + + @Override + public String toString() { + return certificate.getSubjectX500Principal().getName(); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/CertificateRevocationList.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/CertificateRevocationList.java new file mode 100644 index 00000000..24c3ec08 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/CertificateRevocationList.java @@ -0,0 +1,122 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.providers.minecraft.verify; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.cert.CRLException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.gradle.api.Project; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.util.download.DownloadException; + +public record CertificateRevocationList(Collection crls, boolean downloadFailure) { + /** + * Hardcoded CRLs for Mojang's certificate, we don't want to add a large dependency just to parse this each time. + */ + public static final List CSC3_2010 = List.of( + "http://crl.verisign.com/pca3-g5.crl", + "http://crl.verisign.com/pca3.crl", + "http://csc3-2010-crl.verisign.com/CSC3-2010.crl" + ); + + private static final Logger LOGGER = LoggerFactory.getLogger(CertificateRevocationList.class); + + /** + * Attempt to download the CRL from the given URL, if we fail to get it its not the end of the world. + */ + public static CertificateRevocationList create(Project project, List urls) throws IOException { + List crls = new ArrayList<>(); + + boolean downloadFailure = false; + + for (String url : urls) { + try { + crls.add(download(project, url)); + } catch (DownloadException e) { + LOGGER.warn("Failed to download CRL from {}: {}", url, e.getMessage()); + LOGGER.warn("Loom will not be able to verify the integrity of the minecraft jar"); + downloadFailure = true; + } + } + + return new CertificateRevocationList(crls, downloadFailure); + } + + static X509CRL download(Project project, String url) throws IOException { + final LoomGradleExtension extension = LoomGradleExtension.get(project); + final String name = url.substring(url.lastIndexOf('/') + 1); + final Path path = extension.getFiles().getUserCache().toPath() + .resolve("crl") + .resolve(name); + + LOGGER.info("Downloading CRL from {} to {}", url, path); + + extension.download(url) + .allowInsecureProtocol() + .maxAge(Duration.ofDays(7)) // Cache the CRL for a week + .downloadPath(path); + + return parse(path); + } + + static X509CRL parse(Path path) throws IOException { + try (InputStream inStream = Files.newInputStream(path)) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509CRL) cf.generateCRL(inStream); + } catch (CRLException | CertificateException e) { + throw new RuntimeException(e); + } + } + + /** + * Verify that none of the certs in the chain are revoked. + * @throws SignatureVerificationFailure if the certificate is revoked + */ + public void verify(CertificateChain certificateChain) throws SignatureVerificationFailure { + CertificateChain.visitAll(certificateChain, this::verify); + } + + private void verify(X509Certificate certificate) throws SignatureVerificationFailure { + for (X509CRL crl : crls) { + if (crl.isRevoked(certificate)) { + throw new SignatureVerificationFailure("Certificate " + certificate.getSubjectX500Principal().getName() + " is revoked"); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/JarVerifier.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/JarVerifier.java new file mode 100644 index 00000000..19779031 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/JarVerifier.java @@ -0,0 +1,92 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.providers.minecraft.verify; + +import java.io.IOException; +import java.nio.file.Path; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.util.ZipReprocessorUtil; + +public final class JarVerifier { + private static final Logger LOGGER = LoggerFactory.getLogger(JarVerifier.class); + + private JarVerifier() { + } + + public static void verify(Path jarPath, CertificateChain certificateChain) throws IOException, SignatureVerificationFailure { + Objects.requireNonNull(jarPath, "jarPath"); + Objects.requireNonNull(certificateChain, "certificateChain"); + + if (certificateChain.issuer() != null) { + throw new IllegalStateException("Can only verify jars from a root certificate"); + } + + Set jarCertificates = new HashSet<>(); + + try (JarFile jarFile = new JarFile(jarPath.toFile(), true)) { + for (JarEntry jarEntry : Collections.list(jarFile.entries())) { + if (ZipReprocessorUtil.isSpecialFile(jarEntry.getName()) + || jarEntry.getName().equals("META-INF/MANIFEST.MF") + || jarEntry.isDirectory()) { + continue; + } + + try { + // Must read the entire entry to trigger the signature verification + byte[] bytes = jarFile.getInputStream(jarEntry).readAllBytes(); + } catch (SecurityException e) { + throw new SignatureVerificationFailure("Jar entry " + jarEntry.getName() + " failed signature verification", e); + } + + Certificate[] entryCertificates = jarEntry.getCertificates(); + + if (entryCertificates == null) { + throw new SignatureVerificationFailure("Jar entry " + jarEntry.getName() + " does not have a signature"); + } + + Arrays.stream(entryCertificates) + .map(c -> (X509Certificate) c) + .forEach(jarCertificates::add); + } + } + + CertificateChain jarCertificateChain = CertificateChain.getRoot(jarCertificates); + + jarCertificateChain.verifyChainMatches(certificateChain); + LOGGER.debug("Jar {} is signed by the expected certificate", jarPath); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/KnownVersions.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/KnownVersions.java new file mode 100644 index 00000000..f585a956 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/KnownVersions.java @@ -0,0 +1,57 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.providers.minecraft.verify; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import com.google.common.base.Suppliers; + +import net.fabricmc.loom.LoomGradlePlugin; + +/** + * The know versions keep track of the versions that are signed using SHA1 or not signature at all. + * The maps are the Minecraft version to sha256 hash of the jar file. + */ +public record KnownVersions( + Map client, + Map server) { + public static final Supplier INSTANCE = Suppliers.memoize(KnownVersions::load); + + private static KnownVersions load() { + try (InputStream is = KnownVersions.class.getClassLoader().getResourceAsStream("certs/known_versions.json"); + Reader reader = new InputStreamReader(Objects.requireNonNull(is))) { + return LoomGradlePlugin.GSON.fromJson(reader, KnownVersions.class); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load known versions", e); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/MinecraftJarVerification.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/MinecraftJarVerification.java new file mode 100644 index 00000000..1a864838 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/MinecraftJarVerification.java @@ -0,0 +1,113 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.providers.minecraft.verify; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import javax.inject.Inject; + +import com.google.common.base.Function; +import org.gradle.api.Project; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.util.Checksum; + +public abstract class MinecraftJarVerification { + private static final Logger LOGGER = LoggerFactory.getLogger(MinecraftJarVerification.class); + + private final String minecraftVersion; + + @Inject + protected abstract Project getProject(); + + @Inject + public MinecraftJarVerification(String minecraftVersion) { + this.minecraftVersion = minecraftVersion; + } + + public void verifyClientJar(Path path) throws IOException, SignatureVerificationFailure { + verifyJarSignature(path, KnownJarType.CLIENT); + } + + public void verifyServerJar(Path path) throws IOException, SignatureVerificationFailure { + verifyJarSignature(path, KnownJarType.SERVER); + } + + private void verifyJarSignature(Path path, KnownJarType type) throws IOException, SignatureVerificationFailure { + CertificateChain chain = CertificateChain.getRoot("mojangcs"); + CertificateRevocationList revocationList = CertificateRevocationList.create(getProject(), CertificateRevocationList.CSC3_2010); + + try { + revocationList.verify(chain); + JarVerifier.verify(path, chain); + } catch (SignatureVerificationFailure e) { + if (isValidKnownVersion(path, minecraftVersion, type)) { + LOGGER.info("Minecraft {} signature verification failed, but is a known version", path.getFileName()); + return; + } + + LOGGER.error("Verification of Minecraft {} signature failed: {}", path.getFileName(), e.getMessage()); + throw e; + } + } + + private boolean isValidKnownVersion(Path path, String version, KnownJarType type) throws IOException, SignatureVerificationFailure { + Map knownVersions = type.getKnownVersions(); + String expectedHash = knownVersions.get(version); + + if (expectedHash == null) { + return false; + } + + LOGGER.info("Found executed hash ({}) for known version: {}", expectedHash, version); + String hash = Checksum.sha256Hex(Files.readAllBytes(path)); + + if (hash.equalsIgnoreCase(expectedHash)) { + LOGGER.info("Minecraft {} hash matches known version", path.getFileName()); + return true; + } + + throw new SignatureVerificationFailure("Hash mismatch for known Minecraft version " + version + ": expected " + expectedHash + ", got " + hash); + } + + private enum KnownJarType { + CLIENT(KnownVersions::client), + SERVER(KnownVersions::server),; + + private final Function> knownVersions; + + KnownJarType(Function> knownVersions) { + this.knownVersions = knownVersions; + } + + private Map getKnownVersions() { + return knownVersions.apply(KnownVersions.INSTANCE.get()); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/SignatureVerificationFailure.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/SignatureVerificationFailure.java new file mode 100644 index 00000000..03b368c0 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/SignatureVerificationFailure.java @@ -0,0 +1,35 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.providers.minecraft.verify; + +public final class SignatureVerificationFailure extends Exception { + public SignatureVerificationFailure(String message) { + super(message); + } + + public SignatureVerificationFailure(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index 8a7a9c08..4ca0a1f6 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -149,6 +149,10 @@ public class Constants { public static final String RUNTIME_JAVA_COMPATIBILITY_VERSION = "fabric.loom.runtimeJavaCompatibilityVersion"; public static final String DECOMPILE_CACHE_MAX_FILES = "fabric.loom.decompileCacheMaxFiles"; public static final String DECOMPILE_CACHE_MAX_AGE = "fabric.loom.decompileCacheMaxAge"; + /** + * Skip the signature verification of the Minecraft jar after downloading it. + */ + public static final String DISABLE_MINECRAFT_VERIFICATION = "fabric.loom.disableMinecraftVerification"; } public static final class Manifest { diff --git a/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java b/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java index 39f0f282..e59b8396 100644 --- a/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java +++ b/src/main/java/net/fabricmc/loom/util/ZipReprocessorUtil.java @@ -46,7 +46,7 @@ public class ZipReprocessorUtil { private static final String META_INF = "META-INF/"; // See https://docs.oracle.com/en/java/javase/20/docs/specs/jar/jar.html#signed-jar-file - private static boolean isSpecialFile(String zipEntryName) { + public static boolean isSpecialFile(String zipEntryName) { if (!zipEntryName.startsWith(META_INF)) { return false; } diff --git a/src/main/java/net/fabricmc/loom/util/download/Download.java b/src/main/java/net/fabricmc/loom/util/download/Download.java index df903148..158651ac 100644 --- a/src/main/java/net/fabricmc/loom/util/download/Download.java +++ b/src/main/java/net/fabricmc/loom/util/download/Download.java @@ -145,13 +145,13 @@ public final class Download { } } - void downloadPath(Path output) throws DownloadException { + DownloadResult downloadPath(Path output) throws DownloadException { boolean downloadRequired = requiresDownload(output); if (!downloadRequired) { // Does not require download, we are done here. progressListener.onEnd(); - return; + return new DownloadResultImpl(false); } try { @@ -162,6 +162,8 @@ public final class Download { } finally { progressListener.onEnd(); } + + return new DownloadResultImpl(true); } private void doDownload(Path output) throws DownloadException { @@ -483,4 +485,6 @@ public final class Download { private DownloadException error(Throwable throwable, String message, Object... args) { return new DownloadException(message.formatted(args), throwable); } + + private record DownloadResultImpl(boolean didDownload) implements DownloadResult { } } diff --git a/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java b/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java index ab787bfb..8da80ce0 100644 --- a/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java +++ b/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java @@ -33,6 +33,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.Locale; +import java.util.concurrent.CompletableFuture; @SuppressWarnings("UnusedReturnValue") public class DownloadBuilder { @@ -115,15 +116,12 @@ public class DownloadBuilder { return new Download(this.url, this.expectedHash, this.useEtag, this.forceDownload, this.offline, maxAge, progressListener, httpVersion, downloadAttempt); } - public void downloadPathAsync(Path path, DownloadExecutor executor) { - executor.runAsync(() -> downloadPath(path)); + public CompletableFuture downloadPathAsync(Path path, DownloadExecutor executor) { + return executor.runAsync(() -> downloadPath(path)); } - public void downloadPath(Path path) throws DownloadException { - withRetries((download) -> { - download.downloadPath(path); - return null; - }); + public DownloadResult downloadPath(Path path) throws DownloadException { + return withRetries((download) -> download.downloadPath(path)); } public String downloadString() throws DownloadException { diff --git a/src/main/java/net/fabricmc/loom/util/download/DownloadExecutor.java b/src/main/java/net/fabricmc/loom/util/download/DownloadExecutor.java index f1a4606e..a4c9eba0 100644 --- a/src/main/java/net/fabricmc/loom/util/download/DownloadExecutor.java +++ b/src/main/java/net/fabricmc/loom/util/download/DownloadExecutor.java @@ -24,10 +24,11 @@ package net.fabricmc.loom.util.download; -import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -40,20 +41,20 @@ public class DownloadExecutor implements AutoCloseable { executorService = Executors.newFixedThreadPool(threads); } - void runAsync(DownloadRunner downloadRunner) { + CompletableFuture runAsync(DownloadRunner downloadRunner) { if (!downloadExceptions.isEmpty()) { - return; + return CompletableFuture.failedFuture(new DownloadException("Download blocked due to previous errors")); } - executorService.execute(() -> { + return CompletableFuture.supplyAsync(() -> { try { - downloadRunner.run(); + return downloadRunner.run(); } catch (DownloadException e) { executorService.shutdownNow(); downloadExceptions.add(e); - throw new UncheckedIOException(e); + throw new CompletionException(e); } - }); + }, executorService); } @Override @@ -79,6 +80,6 @@ public class DownloadExecutor implements AutoCloseable { @FunctionalInterface public interface DownloadRunner { - void run() throws DownloadException; + DownloadResult run() throws DownloadException; } } diff --git a/src/main/java/net/fabricmc/loom/util/download/DownloadResult.java b/src/main/java/net/fabricmc/loom/util/download/DownloadResult.java new file mode 100644 index 00000000..4ac1deb1 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/download/DownloadResult.java @@ -0,0 +1,29 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.download; + +public interface DownloadResult { + boolean didDownload(); +} diff --git a/src/main/resources/certs/known_versions.json b/src/main/resources/certs/known_versions.json new file mode 100644 index 00000000..f8653c49 --- /dev/null +++ b/src/main/resources/certs/known_versions.json @@ -0,0 +1,682 @@ +{ + "client": { + "14w18b": "8403c9fb03b1e9c60cb6fb2f97e25dd60041aba4f0a62596f4e63eed49cfa9c0", + "14w18a": "8b18e1c6bcc01d9a96e44171e8ed05bd8193a2bb6a3c9c9f5fecce7492b04cac", + "14w17a": "db07fed9bed91d5de8f48c3906b87b7c05aa2b7242a16000c24009eabcf3516e", + "1.7.9": "38f8d799a9b42fb539ca7250e317dd6546910c8ac7718a720c11aad79780e8d8", + "1.7.8": "09d06a078aaedc075682440a5d87d473ee5ebfb35270aa8f085575f133029a31", + "1.7.7": "3e41fd81092a1e9bb7d3d268ebd8c8700b5906065974dc6a962ba8417d57ba73", + "14w10c": "542a846d2ca4975069230c1b8e3da143731966190ff307cc202fd75117fa7c83", + "14w10b": "02367e9efcbd9feff8c41afb9e1e2801076190821fd259de61d0d49137c9c078", + "14w10a": "267dcc0da9deff6a5e8ad4a80b3ecf1da0f17c9184b7c236c3b11e2350f92d53", + "14w08a": "5fefa25e18becd6d4a522b21488f835100dde75d1e20612ff1864586453f8ab9", + "1.7.5": "5f83b944b59c48ea7fa8f92fefd491ecb6d1e8d6c9b412fc849f6457c8cee27b", + "14w07a": "298f9b762fdd56fab0aa250385629f8971cebc7054c37d48c8f3f65454f62116", + "14w06b": "f1e87881adf7e02af3dc69a2f57631c32aed31d709b7820037771af28faf55cc", + "14w06a": "047db6da1fb6a5d6d9f8500c4f37fc274d351dc3aeec5b4b5a5879565617265c", + "14w05b": "7c9d9fbdcdca7160bef96cdb1ada2e8cc4edad73d0f996e36305a79d3a1790bc", + "14w05a": "893f6b8129a1fdee79c0aec3f912a11a6f3af3240debdf75a685aad2a00b118b", + "14w04b": "0278bdc4c5ba4e7035555fc5e5b90b8d005478ad12be32613ecc6fe03aee4da7", + "14w04a": "a09bae6167d85fc4cabef338a5f9f4b710b029079354eb9c36097fa9dc137244", + "14w03b": "f027f1aaded0445f769aad31770cc93abdeb6bb7b66cf23b1b32d68feaf638b6", + "14w03a": "d640a5a69510251fe8417cb96acd9f2ee2e302e671adb33581abcbe6e27eb1cd", + "14w02c": "1189a21527efd3e2bc9936787d6697b865b84f917e271fa29edd8b1e6ddfb66d", + "14w02b": "02080a3cea941c7fdedebada32315c62a121a2d372310f80ba119c4ba2fd8d2d", + "14w02a": "4b08784409fbf168a8d27a74f8a4c992fe50ace58021434a1fd2d22d6df2bc01", + "1.7.4": "c51494c52a612648cbcb7ebcdaedbcb61f6cd287f6215f90113a43136cb62526", + "1.7.3": "8b1d90ae662b235072d05dbc27d38ae5fef8b322a21d10302703fd798feb05c5", + "13w49a": "63e598023cbdbf77f4ec08b7fae4c25ab276b6dd17f3f59ff1086d1334ebdb00", + "13w48b": "d606b7eb41bdcbedb678bb5aa87d05e9e0fb13b31e16dac845ea658b3e970d51", + "13w48a": "92edd6cfa4ec6dd46f361bfbc2a537803f92177c80a643def663d2c7be376385", + "13w47e": "0cf206856ebaf09f08fa27faa5cedce3230fe837b5486806ea8354b952aae7c9", + "13w47d": "19d5f72e2aa1779a02335a3a4692c1a1a2a04be8d0329c15bf675bcd74464fdf", + "13w47c": "0a69ec62aca3a4b5c6f2956df4cabe465d4a7f6f2fc80a7ac1444936f07a5dd3", + "13w47b": "11216038d4c85cbf1cb2d3bf61808b0d26ca287b064b5fd8d511e11323414bf2", + "13w47a": "e8b0204b3d35b2a6c381417d8a32d3163c9e98caa4c733b4e9914989fecc7282", + "1.7.2": "507fb3660e77ab1a3000424d9b08c61606ae1e5a9be41caa3728bddd9597365f", + "1.7.1": "0ae4bb959d5d916ce7a6a83882aa154bb4233ea8a3624d3fb0970a5fd3720fa8", + "1.7": "af3ce46c2b91d5ae61bc5c590fb8c72b00b538875c8de225e9f2f2dd465afb50", + "13w43a": "ae0fe25f37d971787e73116fd091c1e1edbce197552bf15be70862ac7b4e5c40", + "13w42b": "e5ec51369856028631bb973cffdb13e5b83ba3fdd466314469ddedaa5c1725ba", + "13w42a": "1c1d7ee75225aedba2c04f871f7f1de6261f42debbc1219024bb2ec4af0e47ae", + "13w41b": "3e9eaef7162673b5fb062017ee02e847cc7e53395bd9366ce1d7db95d808dd6e", + "13w41a": "ef428b96e3550b7c733d43544950553971503bf99dc68942971d35c5269d73a3", + "13w39b": "cf804e8ed8c0589df08baa76f2f77eb89046b1df51b5e7e7a6ec198012a06bb1", + "13w39a": "a4cbdea94098d07c30c2ed4941344ee0fa2019edfc6e483826cc7e075e9934e1", + "13w38c": "d28cb5d42a8bd4bd9e870f13ce6f6cb2cf5427b43efb631a71751cff56c32288", + "13w38b": "0ea32cb82f5f49db167987907e4ff14e5358a42cc202c794a8cd4525cc286009", + "13w38a": "941e72596782c260e2cc8687757c141060b3f3d1b77c1a75e7c4e4e2b04504e4", + "1.6.4": "f4513e51c766cdd1d32ca8acd0835d7e16e2851ebea3f2a3b4ce5ae696baf3ae", + "1.6.3": "e6dc707af0a0cd050d5f66da38476c81f2ad6437074e46c852bae410ee2a30d3", + "13w37b": "998240601ae985884f7899933410188075320e432ca2257748b56f17e8d09770", + "13w37a": "61252567fe990c23689a1bbd3514d82460442b3c6822dafc2fdd06a2e5746809", + "13w36b": "00e81ac87bfe31a47f2b7f5aeb532649e0b75b847a237b3bcd2f6b101a161667", + "13w36a": "796e2540899dac97bcdc873a84f08d74a41212ec5648e5f70ae1e87f75dcf692", + "1.6.2": "08406549a47412294923705bf023b1080ae14da22ec2770d16d920ce420c3092", + "1.6.1": "1d8a2945e196db1226930ca51f20f8dbe2d9578884949c569c5cc8dc5fbdcc53", + "1.6": "1362475725c495e34fba6cd8bd10af1e621d9425180c8428b9cd8efca168e611", + "13w26a": "71e49cfd67aade77449ee898bdbdd719149ff142be501d29312d702db107d530", + "13w25c": "26c2259621d151b29ffcda4b4f2f39d044b835a4d5152cc3f87138b81a6ab080", + "13w25b": "18d9e3ff2ae3184c5d2be0ea6ad5ebac496e2ef8454a8911b036b078087c8c35", + "13w25a": "8ebc73a02b8b3c952d678b9cadd3250bdf47218513baabe6b7529e8871e19b9e", + "13w24b": "bfa6fc3154afad3ce6f3e416f7bf8b7dcc93f21b8d67fc1cb8872346462e73c9", + "13w24a": "391edb763fa015d2893ddf9345ffa676322e939719b16ff77fd442fcac873282", + "13w23b": "53ba4cb9f0e2ad5ddddb6cebddca18cfd85db3968ae8b3ac4f36ba93169faeac", + "13w23a": "f99593e38e106713d022c43cca369ec08187a062b8112b34cfb6f484534d342a", + "13w22a": "cd3b4e88804f7e6ba6ca604f3e53c49770d8b5a967c9f069c78b637f9a79d407", + "13w21b": "6790b306bdc554d5485edbee9cbb434707f8f45c85d36041079d80588dd2d96b", + "13w21a": "7a267523ed4f42e654bd80817368dead64b3e6c3a4afb8f4b2070d87441a004a", + "13w19a": "0cc59566ee89deb80090c8752c35cc532667921332ed4b4d5b2b136e6131388c", + "13w18c": "45d3b2ff10dfbb55f5f4d719b26cdd3c5e232261d0a3d9d66c2536f7a1c9695a", + "13w18b": "bc54c2f86339d489a0f20376fbb23f19dcf8464645a675cf0eeb623b249c2934", + "13w18a": "1750c66d6c4aaeb75061ef1cb8f55821553ad469ea3a8dbdf1f27c70af2ac53d", + "13w17a": "d9b3725b86e69b04eb55f24aacd92cb339d77e1083f6c986acfd5ae8ae1511d3", + "1.5.2": "dc0fa48951f61c12eafede5e46e248aa86ab86d1e4c28cd880c1d9c348ec44d6", + "13w16b": "9a7108fbefc3e4a85b82100eaa57f725668540d98a5b0160a55ada148d5e80a7", + "13w16a": "54eeeb77ef7813415bf31551ea14005d612d64fee0d04e5b543bff4b5c2bc787", + "1.5.1": "e3164ae3a18954b0e09aefa20afd9c6713d9ca3b63d603fc8ae2502026149922", + "1.5": "4ad020a9c3fd95850370a45ea3511a8fd98728f7dffda68007ca2e99276b67c4", + "1.4.7": "c5f4972bd775e03b1d3a1255d962ffc00f8c1f6ad98a30a149d5f107153c0da6", + "1.4.5": "b7f06f2019ccfe5d8aaf7e4fb5f5e8e2bdc3ce6509c4ab4f559a5ce4ffe011a8", + "1.4.6": "c4bb5fd8f98ac7160b34fff7a65f809e0930ee39c5d94f463f0884a6477b25ca", + "1.4.4": "9e155ec17c574488c0bf361aedb1c485d1f08b31fa9d21ddcbcdd441a0d41f53", + "1.4.3": "06d60d55d1c16f60884f0def32fa249145bf7e0b8f5058ad885e3aeaecf798fb", + "1.4.2": "199f44e06dfbc20567f9b6db0a965776b38208359a62956b43906bdc51118cf2", + "1.4.1": "773e2bbe53db57ff8ba9dd530ad62db546f6d9f4c37b0dcb7631e337d26641e3", + "1.4": "3a70045649d016e8026e98ed0488b1578910b9bef0dd48a744690501b369f43c", + "1.3.2": "24ec25082cf2bafad90518b5e24d23fdfec55f1bff7f2ead391ab40501eb8354", + "1.3.1": "26a5586a1dd5555e918813284ab6cf05af1fb997f66b99c93035b03ca066cc52", + "1.3": "a71f49a4a9fae65db94da535e5b0318606151927dcacb68908ec937c71ac7b71", + "1.2.5": "c1c3740a912ef523a8bd46605ab5708643498330140cba175c7ce6f177e468e1", + "1.2.4": "ece050b625d1836a035197ed312dc59caf26429f4b386c81bdd84cdff3ca80cb", + "1.2.3": "ea15a60614f96a6bb9aecf865de213635c62d664124092d84bce240622c4a9d3", + "1.2.2": "5cc415005abc4931238c1af056f7f7aa650990951a58142d7ee855532b2b0fd8", + "1.2.1": "3c519ce303504c2f112f6d2f94f81cf1570c83ce81348df7db93ada317e33054", + "1.1": "e065e9681462aa1b78f3cc589ba880d9aae51994bcd4af8c67bcab5c47391ca0", + "1.0": "136e3dd54454e96175badf50bee2cdebdab9e7d66fee4fd6d135f39ead99eb58", + "b1.8.1": "3a61a9cb5b8b6cade30a8bfe21f6793122c7349d0c3bbe1cd0fc2a93add36f4a", + "b1.8": "5d861a5ae1ada4f659905dfde3ff76859a8fd687b2b040b2cc97b0e4e43d9358", + "b1.7.3": "af1fa04b8006d3ef78c7e24f8de4aa56f439a74d7f314827529062d5bab6db4c", + "b1.7.2": "16c8f63be9e5039f2ce974974a5688d772e93c9dba53731deac4b2434efb8d12", + "b1.7": "43efdd70ac7c7b1b7d5c2ccb6c1aad8d9fb7af110187fb4d1070b21d04648b7c", + "b1.6.6": "7764cb6cd9832d3b270d749a9ebf44e5ec8297faf5f14753146ab70cbd316b66", + "b1.6.5": "b203d6d62f5b97671d262d88c7bf8891b8501b401fe9ababc9e4ee2d960cd0a8", + "b1.6.4": "02d02cd94905c4a2c771b6c1696d236cb38150600a3dfb9946393d3e1431df0d", + "b1.6.3": "a88794d52654321527669ac38cb39764ab41cef804ad98eae8e7e00861bf845f", + "b1.6.2": "b3a6cff53774e1c0f7e9faee014f49728b72a7b1d1301fc9d39eb12cc20fc310", + "b1.6.1": "5108cdc9f7f9c5514ad2d16ee1b2a7bbfaaa13f1816aafdcd24e9ee975a4707a", + "b1.6": "a084d22ea2c872a564ebef4e5b99be9ec0727e3f7be6cfb4f6cb2f9dea4522f8", + "b1.5_01": "088e96109679889c62cf8a91c499bc44ca292252872c242ffefcad95be077618", + "b1.5": "d99cba867acc5aabf383f8252f047d5d1e9afb4f531c9b7205d1b845cd4fa7f0", + "b1.4_01": "36170e61a2ce9dcb6dda5af63440070d1bad82c0846d779f25c2fb0bdc14992e", + "b1.4": "c48c430cb60d3197ca4b9c0aa33821ea56fa10edba5a10a46b77aa46883a6908", + "b1.3_01": "e993b789dd450e1538667def644e1376bd702fe26eebec3197598d5157042684", + "b1.3b": "335ed6e306324ba4b7eb8ae16479839ae5678f0c1da62757e515d640801e1159", + "b1.2_02": "c803cd9c4a0815b98317efaee3dcc84863049a5a3b5d250c56776669c8996131", + "b1.2_01": "55df77ca089baa030c5a4e62905ea78f755485ea3a8e968a2e0ff549bfe13bc0", + "b1.2": "a0996c12b0bfd5a9c85a03542cecf02f573f4cd4dd60f16b05323e897750cc9c", + "b1.1_02": "3439c894641b07bb1b31b89209e9dc5818755353d1f646ee48e583610d6670f1", + "b1.1_01": "f5a4cf4b631c67cafc490dcb43c17dbbab0383d3ec1ffb946f0eb217374a8368", + "b1.0.2": "82e28d25b493ade968cc0015e5b1c2e9145bbbe247d7f1ea7d603e7ddfe056bd", + "b1.0_01": "61829173ce6d311246db4f68bf649bab5ef9f8e29c436cd7c9f2ae494db7f1bb", + "b1.0": "ad62df9cc678bfa15ff981f56aa8aa7ad0d60a1e584c39b981f9ec089ed450f0", + "a1.2.6": "63276bf2617068ffbaf2a1992d1f06f9339c96c21991eafc9583d5f3e7074b9c", + "a1.2.5": "fdde933363df3a1d95a76d180dbdb14464f8390f954d272613ed25ce42155787", + "a1.2.4_01": "047a05550de80c186a3f23d7c4c8b25056dcc0a775e73f4e4d919f39d8bd1c37", + "a1.2.3_04": "e80e49d2ea895198fcd2d3866e4541a4d464aa5263f3923d540cbaf1053f9eb9", + "a1.2.3_02": "bc23d23764761484472060a49353598bbaa33d574e6953015e75e192e03bb9f9", + "a1.2.3_01": "17c0463e56800aa257de3e76011d7f3db015b44fe2f3e846275fcee7f6129571", + "a1.2.3": "76b99d9a0d884bb0de99257740fd391c319abebc2ec30174a39bb63e5ebe69b1", + "a1.2.2b": "fe33a245d0c1a995ffc82f2673436029fd0c4f04b9597fa094a1fd6157cd65d7", + "a1.2.2a": "054c311b6ac2181f8d362d579ab8d3992ebff1867f19c480191e417c030e531a", + "a1.2.1_01": "7eab51320f26cf68ff9b06fcf34b64ee60aa7aa0ee5c0421e8fb0a265e811c18", + "a1.2.1": "7eab51320f26cf68ff9b06fcf34b64ee60aa7aa0ee5c0421e8fb0a265e811c18", + "a1.2.0_02": "cb4d712cbbe51a6a8b375ca771fdc4ff3fc70bc7bdd640e53a020ae7e687af56", + "a1.2.0_01": "e658a3a6eceac9eed89bd1e2a00768f72dd5aa8ad085fde59a9e1cff2f3c43b9", + "a1.2.0": "2139af187a74e4c60af45110665f38777fe6df07da0b72930fa08245c732e149", + "a1.1.2_01": "167a9cef74eb60417ab9670df10953634c0dba8371ebae1e44588e0ba0a7b07d", + "a1.1.2": "6beaee2c909ed33591a985ab6bd03d6962958c0b3d0aeb7a7a3cc144adf7f50d", + "a1.1.0": "0723b15a4b56b202a46574f2bf039da2760f838e8ea3ce5aac683b26dbf38e8e", + "a1.0.17_04": "34105933da180843e575909978246fb373f2db53b00905b92c8110cd19fad6f2", + "a1.0.17_02": "9505e19f30b79ce2964504de29594e18afeed7fb5dcb3f0960177705880dde0e", + "a1.0.16": "fd14110691b14ea51f9320a949e2b3c1855e38af2668bf4e743c19b08de00234", + "a1.0.15": "c9018315807056c610a30ff08d6d9515712a50997d6c06ff7d923676d61e87ce", + "a1.0.14": "0859315d26dab43e004454f19f51301351e826acf3ffaed636d7d2a5f6d5a584", + "a1.0.11": "a11120202e2ae0b474a60c2944b03cc5dac5c6d659a0926f8c881619de70c17c", + "a1.0.5_01": "5ef06e9c7e0421505e3cecc732f2d192a9e04b5a55840d91418a301bae375c70", + "a1.0.4": "e464928fdfc445de13b91b635d31c1aadf212c656f9d7a9f3ae54da5c2783f5f", + "inf-20100618": "26c18bbdb55c0c7f5858a8094ce082a76f28d76e7f6f3a035383129f75a365c0", + "c0.30_01c": "3bfaa9ceccfd49f62d4c1863d5cd565c24de1f83bab982a38e4811ac369993a5", + "c0.0.13a": "b3b5a88834c2351c948950c6d60c822d0d1d60f88c5adad4794f8ad6fe5b3a33", + "c0.0.13a_03": "40595f0c37adb1c581d0d4d836733806ff75af977836f815f1a5600fef0a43be", + "c0.0.11a": "2ca13b43ea3efc0388c5c1d4613854f6412f97d4c02f4b18729f648e46f02d1f", + "rd-161348": "cb8bda0074ac44d8d26bfe0c101f08edeb2291915f32ad1d2909831cd84934e0", + "rd-160052": "bbd6e24a276c5082f8aafae152bed450c161d56ff776472821de56c0daa7fe1e", + "rd-20090515": "cb8bda0074ac44d8d26bfe0c101f08edeb2291915f32ad1d2909831cd84934e0", + "rd-132328": "0627a893265fd697fff165626e000a2640c6a6ece6b64b75c3ab0194f960ed8b", + "rd-132211": "407460840eaeab01260b9e7951bd518f7c31e2e3f12a352e436502fa7050a6e7" + }, + "server": { + "21w38a": "56ebc9c2a2997a983df5777d7fa1e2d25ba38fa3b740b3cd1db8308819449e5a", + "21w37a": "051cf95cc9aa199f4872bcfaf873ab39841c0a266ac09093d2cad6c93e14d7b0", + "1.17.1": "e8c211b41317a9f5a780c98a89592ecb72eb39a6e475d4ac9657e5bc9ffaf55f", + "1.17.1-rc2": "b54452f67071054983935a02ec344a5d9e0c7ade5a7a4f7966c5f8cceb62335f", + "1.17.1-rc1": "643c414bfa6493f2e644b64bda0e558fb209a12bde27bcaaa508e2976006660a", + "1.17.1-pre3": "b4a23854646c7631aa0e39520184e2a36eaeb4674139264b480cc46d60ca88de", + "1.17.1-pre2": "dbe08754799fc8661f0140d34391885f367a64048437080cc2b3048b11032e19", + "1.17.1-pre1": "c710f543d60df1d0218848c34ba240044dc2eaaa7f210d825b83eb9a5c6af9fa", + "1.17": "7b390d8d9f6b5649b226d82686aec7f11bd9aa4430bb5cac9072ffd32f3c1f4b", + "1.17-rc2": "038782cd1017e75513c6954086b423e227691e91d76567ddeea749119af3756f", + "1.17-rc1": "64a0f55142ff835deac34b7d949f4173e01791e13bfc3825561b1f72156603e8", + "1.17-pre5": "b6ec94e542dc0783bfd76f822f44f0a3434a4dbc7205c545e3c6f42a4bb9b323", + "1.17-pre4": "885a42fb4243b6dc4d4f6fb4f32797f27b88bc74e402b1ae19b69a77b789a257", + "1.17-pre3": "3db83b98c5afb4a49ad2d5e8b1dde30db8edd35d470602b878e5c4b01bfd30a7", + "1.17-pre2": "58bc609735b32aade48a857b95ec92c16202eace828e2d6c6b5384e68083c5bc", + "1.17-pre1": "f2e66bc5d818f1afbf80d46cbcd178cd14fba10ad1ce21330fcf2426e5295b60", + "21w20a": "ea47315ba2b9abfe3ddc08a0e0da4713434b2a3cc8a404a907c1e12267ff6120", + "21w19a": "4242ddb3304917206ee57d4f8733ac6f271bfcd789c9b93cbe5ebb684bba366c", + "21w18a": "991df5a799aad616244022e52ce058c1b52c48eb67c2388f0e9bf514aaff8073", + "21w17a": "79c5fe68415b0198e6716706cdbb7f900b146b678af2883b5ca6edc42e783207", + "21w16a": "6d5fab818c5928c9bb9060f7b2d9968110c620a62def1ce92aaefc39ddc1a4f5", + "21w15a": "afb63475c6e2ba324fcdc4630ae9fefdc422ff86bd6a962c9ca3094ba6a6bff1", + "21w14a": "03467ee277f483cdadfa2f603e6473105f277b0f3fd5f9d5c77ffb2c34159a5c", + "21w13a": "fcd97c853dc0edc5c07cd4753b73bfb9a1ae72c616ba811c044f6075272eeb68", + "21w11a": "b5d0f819e6361327ca96661eed042f92751ff282a137b03acfca763707d6c7e5", + "21w10a": "cd8f9846c42332d4859a35f7645e325dc07b59d4ebdce76a3ff50d73a81b3ab5", + "21w08b": "d8ec1e31705409c641f3ed293b26207f62091b524e3364dbea344dec0a63f386", + "21w08a": "59ea0491b83285061bcfd8ecda579d2dc204f9182a093c4f1f539311a046f3bd", + "21w07a": "292e9f641eaf137b51cc32e68000ad96cd2bb6b6e89b3f31a82de2e2501f0f9a", + "21w06a": "18dac09c4014641ddd6bd17c84425a614a27290dc9cf499671750f7746a4f9f5", + "21w05b": "91583bcc0639113cec87b536de9d94da88cc8381e769d97c319b98a8a14ea17e", + "21w05a": "2c833407945fb4a21f845fe1e2611bdf940573ab6d2df9a307b1a6d3ffdb7079", + "21w03a": "ca3c83d4747ef8e9f458ecc5814561c6042095842aa4a7b604101dc1dca03e05", + "1.16.5": "58f329c7d2696526f948470aa6fd0b45545039b64cb75015e64c12194b373da6", + "1.16.5-rc1": "77417618470294df7df7e10ee1969441287c32f9a6807ced290c2b048fb3808b", + "20w51a": "311493da9f55ee4ba3d17f6615e44167abd69749f917758994952a17a15c665e", + "20w49a": "374f5a9fcc4d72b3a5b094fe04e7a075fdce2d27558d4115ad323d74fa6e577c", + "20w48a": "cd5167235cee40ee8b5487015b0e172964472c1753b9034de19058c31daaa332", + "20w46a": "d66328d61a0442b37ad7e125f2bf69ec4f38295e525e4d42c83bf5d990ffb764", + "20w45a": "0647b8dce42f02c807c7bd83a06c6d68845f917bb9ac1260529a83636b403675", + "1.16.4": "444d30d903a1ef489b6737bb9d021494faf23434ca8568fd72ce2e3d40b32506", + "1.16.4-rc1": "78e7cf12a0e07ae1f5c314720b36cc44ac936fde89263d57927dadd9889f18ba", + "1.16.4-pre2": "5131ae739be591e23e779beca823bcff1df8dc764b6eda2abec3c7f2a9c9df27", + "1.16.4-pre1": "0ae53adc1154fda746dc373dfb5af7b4d1c253f418c84118880d1840f362c468", + "1.16.3": "32e450e74c081aec06dcfbadfa5ba9aa1c7f370bd869e658caec0c3004f7ad5b", + "1.16.3-rc1": "e0b7fb222eb8c4225bf66441045f491da06de851382ece144bcaff38044ef88a", + "1.16.2": "2902ed3ff84e4f810a2c0620c6b6df9c3ef8488b272c61274d5eac2433876f39", + "1.16.2-rc2": "fe0cbac64b7d62ccc50519aa4b8c58a780d8aafca4629d964bbab9903774a37f", + "1.16.2-rc1": "bed63f508d1aa1dd7293ee7d965c62237ffe9e11a753164ede274759758cadfd", + "1.16.2-pre3": "fd81170c5abc101e41a79a218cca9ef59d44821a7cea540b3704d1a79159f098", + "1.16.2-pre2": "a35a434a4bce236c95828e95a4814c2c2938c39feffbcb707bedc430c08eb39d", + "1.16.2-pre1": "3f0eb69be02920a4ba5006047fba2534d3d2142e90df25de4fdee105f7e4894e", + "20w30a": "127b624cbd83fac5931f753ecc16ade2c6324d163ed6370c75b3561ed2381799", + "20w29a": "b8a34501e01b1f2e5cc7e34d90ec8fd54cd2f99557c7fb464ca5bbb6ab4d4237", + "20w28a": "c00f686fa78732a1446fc17d7c3a76695df64b8085005e6c816aea19622b3019", + "20w27a": "a01d3784a033cfee18eb43069f73f450f8a57ef64615ccff8d819af4d2394d85", + "1.16.1": "2782d547724bc3ffc0ef6e97b2790e75c1df89241f9d4645b58c706f5e6c935b", + "1.16": "7d2d2d127b90baf2bd8fc61092cbff42bea1bdfae30a2838f45edb31294979b9", + "1.16-rc1": "372963701a2a7ae47eccc8bf9879a11f2e283c2ef5f33a0275c44aa26daf9883", + "1.16-pre8": "ac4a92fad98af7b65e09e6d4a833860242b9f4938c94c74b8e7661ed5139ab5e", + "1.16-pre7": "d99c608ab3d8aa84fe9851684518fa97967a50d8d2c7ad45b449c4ed72e72a73", + "1.16-pre6": "b33afa7c5ff0586334e2961cc5d81700a1c5d8e7bfacbc155534d41fe4d20e4c", + "1.16-pre5": "cf7999b76c7659bf4a277943b61c0571b8bd7c3e8764f7bbce7a16d461ab7b2c", + "1.16-pre4": "70da4865676df62aae67a897b47684efcfbebef491f30bdfde0962e821b03727", + "1.16-pre3": "9c9b0163aaea79e9c437e7c27befa27482ec21284a77e0f07b2e7765d44e1b00", + "1.16-pre2": "cdba37b0fcb9ed958cf999d3f763d146738e2ec339ee7c9052db053b47e09f8c", + "1.16-pre1": "c9c62c1410601fdd9b14cf0e2545251577512fab586bc75c10c748ee1d9a8ed9", + "20w22a": "c155d45a4bbf6150d7b187ecce11eed199040299f9ccc789cf3e6d89e667a786", + "20w21a": "68fecc31f9ee1952d099269a946e7be8742b04ee90a447e9cb42dbc41ec46883", + "20w20b": "933a424ad1e82d33b0d782b54158e877969dd0893329f190495ca3ba287e8358", + "20w20a": "67c6a4ef7a0cfdf1212b6d9ef28de92a855a78a9b053e3ef5e6bca5e5bf01a41", + "20w19a": "bcc4c321cbe9c1d95ca4d46677f93488a10549337d224495ca2cc1b2fe01bf17", + "20w18a": "9deb9a207c9f4ecf43bd16fbd7d948a3f85642981d65e62715312a4f9e36114e", + "20w17a": "fc40e4759636430547f22df66d6431a99abe16406d29035283d8fb60b7286a55", + "20w16a": "3026f0a3e750b16b5a08c1e7a5172ebdb908ec662af52759228f00d1275a7c62", + "20w15a": "eecca6cee337657ece906dd0055b275283e0b9bf82e9703bebc2ad88b45328a4", + "20w14a": "0dfd9a8d5b09f0f5abf9b297855c8332e490b266c21e77ccfc3c60560dd1e5ed", + "20w14infinite": "1b31cb0c36471632c97b3ea30962c4b8f210f73094a81d9dab8cf9c6f15871fb", + "20w13b": "66fca31e1d3979161e2ac5114647c44faf3347e2eba2a1793c6769e71d2a50da", + "20w13a": "f216eded69f2f22cfbe3c3ed8baafea4a33b2c0f94a04778f09d2dc20236cf86", + "20w12a": "461a870208ff3ff51a5327d6067c8734f9b8cfef9633a70d3dc550cad31b3e17", + "20w11a": "bc2de25797bce59b753507cc9f3cda660c1065999a46c7ef16c3e1f51abc9413", + "20w10a": "fd52aa742e806c6fcfb7afab7f7c94528ff369995df47b474c9a6e5596527887", + "20w09a": "b5417d9821ecddb112eff4dfe5b5b456c2f000896518b10f7ecc0f767600f531", + "20w08a": "fec35ad3e793348ad344afb493cb22d17938a98caa27913da4c37a9782a98e74", + "20w07a": "16def0cf2d6db7f251496772ee44dd68c5af0c71512facd29765ff2ca16852f5", + "20w06a": "cbdee7195fb76e87d39d57fe07e6e10a82b5e9ce376129edb113e2b9cb6ce7e1", + "1.15.2": "80cf86dc2004ec6a2dc0183d1c75a9af3ba0669f7c332e4247afb1d76fb67e8a", + "1.15.2-pre2": "7271730b514ae2520bdefa4fc58614c40511c43ac0d9079e664ff68811226c30", + "1.15.2-pre1": "0452ef9adc2a75394474d7fbd84a043f24659e1a8ae885c4b1d61301e53ca858", + "1.15.1": "a0c062686bee5a92d60802ca74d198548481802193a70dda6d5fe7ecb7207993", + "1.15.1-pre1": "6a2a5c42bc79b24f13659fe90a37b30d470c4011f3477bdaedcdbc429ecdc548", + "1.15": "e0fe1749263b5ec211b358b598b46e787645bffa8411414f0c812a92bdc70c84", + "1.15-pre7": "4bdc556ac0deec5fee4d1ad13abbf5ba5798992f60381747767f33f6c3262653", + "1.15-pre6": "d574de39dd9897e3d6c616a04313fa54ac394c027e25f212defb99e4210f222a", + "1.15-pre5": "1a78bed6ad7c6c4e2f78ef5552b1085a98ace4190968f0d78a1663510b0116d6", + "1.15-pre4": "2313cc6f9e2b6d5111c513c68fcd7162fce255f836e67ac21473dc3a5c812924", + "1.15-pre3": "549bc3993c77ca5cfafbcc224b66dda0d8f01e194cd3df80daf7cdae66632934", + "1.15-pre2": "3a4bd1e1635e3cd56abe7903036b1fb70514204ca4f3d9e4195703e1a9cc324e", + "1.15-pre1": "7c9c58700f70bd8be4e07ff9ba10e6b0eae64735e7b7c784f277dbd7d26e9f61", + "19w46b": "e108ffa6fab53c29d159a3d38ea24fea0f5d2f7783dbf707a4059bc364748604", + "19w46a": "9823fa660bc1ec137f1b8f166267a36ce355dcd20a64f48e1ad36a6b915e3551", + "19w45b": "6d4583a47adbc27463e173880cbcaf55a35537806857105c7eafa7f808c73cae", + "19w45a": "76caafcd6f41017f2c53409b22927a720313ed2b6e6600dd2fcfc3a3569669d5", + "19w44a": "36f8714c89b3f2be48a36bd8e57a751abe3ad6b32bc7e5431cc8923d1f1d774a", + "19w42a": "a729692962823fbede0c86f03aa9e15517f55e7b99ca970415433263be08a88f", + "19w41a": "dcd0755feffdccb93ffabef6a70c85992c3549a0f3868a38187d6b05f7137820", + "19w40a": "6043b833358370fbb7455ab66f5384d37ce8efd5df1578f1426aa389bcece864", + "19w39a": "0ffd910953ab50a736dcc0ab214a0659966713857422e9b7211440b7b181a427", + "19w38b": "61966431ad3d6d70f01582d0e44a6fb88d397da45038b14e08739107fed27906", + "19w38a": "fd23eb1dde29585ba9fd483b13efbf5cbf0a363178fc7207d2754efb8fa68dcf", + "19w37a": "c5d58c9ee416dfa2ffb5523062eeec002048f3a3cd1d3512f3a1f7938a43278c", + "19w36a": "392ec1912c4ff94db122584a32110e2ac4b2a7bc0ac7ce01fc7fb956748890ae", + "19w35a": "5a32b44741e23f43aad504cf83be944b5c5bb75cae1d640f7ecfb7f7e339e744", + "19w34a": "e90576934e580b87b8467cbec0c2a8614e8ed4b829aa4a409d89b7442ea610cc", + "1.14.4": "5ecdedab3a6e129321a444490d0a467c25ea702a24a99cebe3b6aed41f8f5729", + "1.14.4-pre7": "23aba0515ddc24f0936461275651f25ab2b94c927302fdd58cbddd40b2114260", + "1.14.4-pre6": "f18763257d0f23e17906c1a45c1f0ccb123ea68b6b916d2725a8516409cbc110", + "1.14.4-pre5": "b9acac7a5963bbce7ca45d2dc70a4444b69965d007795d32ded9f5beb680aa13", + "1.14.4-pre4": "4b280aa3bba0bf8213c7347daf043bb2943d9a692da359beee841a9149bd3e80", + "1.14.4-pre3": "d86b1ca6eedd93c3b6c5768ab9901176c95fe07976ce949e4d01354698ad0b53", + "1.14.4-pre2": "1f894e747df7106ef604d9000462e5b8a0fb8347fcbda4ac72e534fec3f704f2", + "1.14.4-pre1": "f326a95cf33d4744258da423659dd4feb06d4c5bcfc1e7d5a3282063b8084102", + "1.14.3": "942256f0bfec40f2331b1b0c55d7a683b86ee40e51fa500a2aa76cf1f1041b38", + "1.14.3-pre4": "0f14bd6bcfb987bb98e1e445f1eaf0a330fe8c3f0dce577707bb6611e600af53", + "1.14.3-pre3": "4230cf4f2ed3940dce187cbc6d6f5ed5286b3cc0ec51cdb190bfb0f696b6a563", + "1.14.3-pre2": "6285e75d6d63e82ca07308369465640b5570eda46cfa28aa63dc47a83105e86a", + "1.14.3-pre1": "ca7ecc41eb29dfda1fec8a4d0a2122b1550ae8129aa7c6cb3e831140dcf90438", + "1.14.2": "b47fd85155ae77c2bc59e62a215310c4dce87c7dfdf7588385973fa20ff4655b", + "1.14.2 Pre-Release 4": "8be46e24663acb8a7905029d352af180b39fb98ef7123f609ce2ad208981340a", + "1.14.2 Pre-Release 3": "efb78b0d2f20ac0ec4ec2a27a1eeed7c39d6324060950f8f17eda81a32cf5329", + "1.14.2 Pre-Release 2": "33589482861ad3423c36625f76205498d4e55796956d44f20025ab9e972d34f7", + "1.14.2 Pre-Release 1": "47ff9fff64aea5460c1efbd4dd62c7477e2f28ecbb357edadedf7da558cc1cc5", + "1.14.1": "f822f0b730b7e1f05fca84248a6873400bac4ca449ff6762a55cab3d68b1f03e", + "1.14.1 Pre-Release 2": "0b8271aa0f4c0cdefd311d4d878747405efe60c95710e0a93518f1bc996ced9c", + "1.14.1 Pre-Release 1": "6aa6978e91f9b89c9ce4e878e4cf93ec3d4adffa1fdd276924ea544ba282cd28", + "1.14": "671e3d334dd601c520bf1aeb96e49038145172bef16bc6c418e969fd8bf8ff6c", + "1.14 Pre-Release 5": "dcd1365d9031c17c64a1aca227e0ea1b339f92f162b83aed4c30d784fe3dd690", + "1.14 Pre-Release 4": "008f97dd87b89afc8eaf672e72bf3c37ea4d7961cadda8fe502b51130c418136", + "1.14 Pre-Release 3": "b689a062f9958121da1f656862a5239c3ab70169f6e1851f824621da78636db2", + "1.14 Pre-Release 2": "c2868040da54c422bcd037aa139526558b0abd0aa9d25b995384b0a9322b60c5", + "1.14 Pre-Release 1": "89b5b5fcf3602657b65cacd7358923fb0a921dd9e3aef0a57fee89f3acfea6ef", + "19w14b": "dffe87aee391720976d3d9badff28f56886c50fc6bd9cc80e4248a6a6cb683eb", + "19w14a": "16f04769de4092d9fec1285fddc9d41432f09c93acc88eed1b817204d5109624", + "3D Shareware v1.34": "90d4fc6471ff11ddaff3f981327f851e26891c184716af74c84c6e2fbfd22e67", + "19w13b": "14e08085fc7b42936a9d7567c821cf94b4fe8b4e631ae71132a71e8e8cc189b3", + "19w13a": "60c5eb086681dc5b0f82b42ae10f940c6215c69e1dde558356cf04c8c63a03ce", + "19w12b": "db02070f79fd42d659bc2de8e373836a77504bd4624a62acf1f6b61a6a967614", + "19w12a": "7241d81af90bc685ef1b4b500119cfdbce97f60363a5286b863b00102ad33a2b", + "19w11b": "a95568c5f2255eb3ec1b6125574db35f4f0eca1c1ee1276748d2019d277d17be", + "19w11a": "c9326a467b8d503812035c9000fe527632dcfbc0feab3d3012f4e8c8def0755d", + "19w09a": "10623b38e5343c90aa5b56afa85b6a9a5508c509586afcba3ce75ff3de04b781", + "19w08b": "7674fb600ebcaaeec29ce4ae0975b85b8cfe768eb2b2dafaab1d2840c6d976d8", + "19w08a": "cee4a8ce0645e04ba95a5a7c3f7d98679979c1f781881aeb870a36d303993d24", + "19w07a": "ca225e2ef6b5f39c4c7558fe8d58931d6eb3c9cff8b69b24583b70443e83baeb", + "19w06a": "f3913592807eedd540faa160b914785ce39eaecfc908cdb5170f68e0b442973c", + "19w05a": "efa7ec1b4ab2102146969b9055fdf055f0ae651fca785fd83d32c419647d6a7e", + "19w04b": "1088d3c424dc084f1d10acf4da07315b61617ae7545dad2e0a0d99b5e4afda8d", + "19w04a": "4a70a201366bf22639b9e535ba7fe418a082bde36d4882acef6373589b9c793a", + "19w03c": "420a09a8011532b5f14aff99c4ffc3eaa63ac55732f7701998d9eea62ed8f16f", + "19w03b": "3b5f4bb672b9881bbf29edc935fab991124d5dc3d533cad5b4789f94d6678919", + "19w03a": "39af3a62ac674bab60af54559cad3dd1f37e8b3d097908173849551f0050b8eb", + "19w02a": "60a8947079785b4dfd6398b5fe5213205a18704953dfa9779ba1a76cb68cdf6d", + "18w50a": "a3a6e4519823971b04b8ec719d78443a5b2482161d0dd5d00711cb49cb342ffe", + "18w49a": "926ab61e8064b90d35f8f48d85703b1862056eb781769934551c8a20f4b37e13", + "18w48b": "1b03939ba45778497638df11582525ab9c436c3f53fc9b1d35f229240ec14fe1", + "18w48a": "86db5f64d2bba30a29d3c89dfc5eb1c3fa69db15ee864bbb159980105020cb17", + "18w47b": "e88d8a372a02c1c917f90e330c0b84425c60ebec79c323ebba93b5da2e1faa2a", + "18w47a": "c43648005b63737e2d62a9978a8ac3a6fa218952d64660deb9aade56d1ae20ee", + "18w46a": "0d1688a0aa0e5ccd5689949138fde0585e2039b775a1665e762f9a98d8d3860f", + "18w45a": "d9652f4958f840a34746e2dd349d8e91fb92b1d5193cfed0b799267a447d5c0f", + "18w44a": "f623d5a5f2c0360eef8397320f8784b4d25e7d513e05d27745e23696212196c0", + "18w43c": "91d8eeaef10c01de935fbd8018e9360b4606b4dfd000cfa25d99ceaf7faa1310", + "18w43b": "d64207c164f66248b168e6b8db1daad034f0e796ffdaec8dc3e9bf671eeac2b9", + "18w43a": "0d400f0ad9f31d789d779abaeaa455f120dfede7fc9aa4b429e4f63c7a20cc0f", + "1.13.2": "ffd3aa2c25c5ba68a706b59f2abdc69ac1748e115ca9d3b47941e197736f088e", + "1.13.2-pre2": "5d7e47d3dbe2f464dd4118ffedd37d5aafa09f601bab9806aeb61dc9f42a41f7", + "1.13.2-pre1": "029fab08d8ebd36d77c79e9649d88f5fde1d6d71ca50beb40ec754f95ab8ed43", + "1.13.1": "2ea6047e7651c429228340acd7d1e35f4f6c7af42f59f92b0b1cd476561253d1", + "1.13.1-pre2": "358feb0457aec2940f0a53252cd9ed66f5837710e97871f0d4d25dcfdf00b5f7", + "1.13.1-pre1": "7a6cb5c06dc582e2b5799fec44e8cc2d105ed1229c0576273749e2d067aafe44", + "18w33a": "b116e0785bd7e4853468a9e9f300c07d872f2e2339d682e7237ec62076174dd1", + "18w32a": "99de375c834c939347f025b1afd93d6a464fdf8c3d431cb70701cb535a11c90f", + "18w31a": "d27eb76f6143fe2cd6e641c388b588299e9417c156c498cd39be97c91639608b", + "18w30b": "6127da70d52b47d51adccb0c1627f692f56678ea68de404e9dd5e6a15e6a3ae8", + "18w30a": "6e83809a5a308e16ce266909424b8a6f539af47c82f2b964a57e355a30c65325", + "1.13": "e76f3927904d331c969a2c437d5661ac02f24be86062dd1c607bfd4ebdc550b9", + "1.13-pre10": "a8ff609f7891618b63f1f163d2416650779d2c4fcd6300e296fd994983736b5f", + "1.13-pre9": "33a1812ea0f128551cb2cad28f28808b11434e16c55f4af7b42ba9f9eee787d2", + "1.13-pre8": "a658c769b8f29ac4fc103a4c7b24cada1f0c48400658bd2a7def60b1152711b9", + "1.13-pre7": "21086e3ee7ab37f91dca33a57e682276f8f28d3dff37a5029d756d3dc36da0d3", + "1.13-pre6": "be788e95a3b60545a506e7841c091651f1f32d5a078ad12f9b4c74e57121bec9", + "1.13-pre5": "96acdd0a6389fdc049ba597818a1d27a4b6747385c43b7db2d1e9acd2ea3529a", + "1.13-pre4": "45a8d618b2b8771d91f8f689871cb2163228461eb7e637acf95dcf419f830b94", + "1.13-pre3": "3c9da719f5501ed27fb54cfa19e4fd96cbb69e07b92c0def37fdcb45782f4780", + "1.13-pre2": "c3bc9ef540ecb5819252c4ab85cd7bf492a5e077e250d3b4958c8ea0774cc8ee", + "1.13-pre1": "3fad9696de535038e90d2b4338d817256cde2d9b23cdead190ee8900aa9a2a83", + "18w22c": "4a176e787d8635afc5ddce5477a8fa992c8da07df7284062dd1e6139511be83e", + "18w22b": "d95f737959b708b7af63fcf71eed9d8266e668ca522a7eeb67a38460692bf6b4", + "18w22a": "c534aa4cd9251a005c701a22b60e69e35bb39d7479962dae3f3aff9b4ade0eb9", + "18w21b": "0bc312dbe2c1ee8fc411210708dea05ad51da13784d94c6445922e6541f29184", + "18w21a": "b38caf0c824db0a4fc3d3056a2f47d93ffa9f9aaca1bd4bca8d8ba0a35e0118c", + "18w20c": "24ace4e82c4b907bb3caa6953cefcdd33f60d4cc00f8f474365a9f1402ceceb3", + "18w20b": "37561559343a5fa0354d1d4408bbb5da190e2b5cf31d79cc939cdd494c27c34b", + "18w20a": "e9cd263e1608c8d40841a9254214cd3c93479711056cea004265a4dd322f2996", + "18w19b": "2c048d0accef680ffef7cd2722682f65765d2d085633bc8b5b7e5baf1d378207", + "18w19a": "548def192939baddf8717619ffb157d95cb22f069b6627e4f26008298cee4e10", + "18w16a": "16696c021eecb5150656f6041874a5ef7f40f1a01f1c53c68885b3f17ca249b1", + "18w15a": "b9b11bab9b5d1fef199fc277bd0b68533581d3150f49a5ecc7f8e39dcf627af1", + "18w14b": "b38223a2452ce41aa7e2266a7b05d2015c99faae2426b39713c9f1648665dfa2", + "18w14a": "58cfdfb099ac2bcc7fc67536b0a8b94488ac0acd099dba10bd280f82285c71ab", + "18w11a": "4b11887faa3725becf4385f8b0c014cbfb24d6745216a69177deb91c168250a1", + "18w10d": "c1b13e09fed92138b04151b65b6144be42fdf28f71d8a5035980422114302e0f", + "18w10c": "949ace0cc5a1193f4c1d5a54ea16b7db4ea5cf36643f6bc521df4b82ff12a990", + "18w10b": "d88febaf435c7858ef4faca46550f8cb8f06df2448332a1afaccb54a1385cf64", + "18w10a": "11ef7c590536ba0496e6d9aa089df5ad2b47d2ea1531bf62a30cfb6ea841c225", + "18w09a": "17b0dd2f4f75d30868b2bbe5251cef7d9a82e3c06a843aef54934f93f27ac139", + "18w08b": "1dc97992a9ec2558e348f5e4f67d55415cb906d494b2e8f5e44d7613cd2567b1", + "18w08a": "116d6ec30088b552a2c8f38da270ac11e49d4e0f6226716df81e023ab4dbb749", + "18w07c": "b25ec60a1cd18c878c99865db5767ab86c54d7e18e9aa0b6e5d379feffcd6f64", + "18w07b": "990777b050e015d99483a902990247c55ef402fe6a18aed4b53bfea01a92ef35", + "18w07a": "e40a0b64b3e8372cf8673953abd32b625c64115b0a482d4f16989bf340c15127", + "18w06a": "5f4d89418606c8c14be0d428e991b5cd62b58fd2762a6040284877f29eafc60b", + "18w05a": "399767cd2cbe45b4dfb2a3746b885a989151ca4fa4c2a22cede14333be28f8ea", + "18w03b": "a8d380b9a28a07e544308ab55e0c86f01dd509c890409a6a4a7d0a88f7b51fe8", + "18w03a": "5cf4d2680901e4483f3d7c09b7aecff612a07954eca35e12b4645640334e0b92", + "18w02a": "58bb24f857ecb851c0b34ec711225642fd75252ce62b55ac39a7650f89462631", + "18w01a": "d017bfc7ee82317140cac4c0f722da3f7e4f6bbe73f45f10ae8bd2cf84787f55", + "17w50a": "46e7c69769cf1da826258d9d6b17592273c2728bf91e21fd26b3508dca0bebb9", + "17w49b": "a68135f67ca398bbb9d74942f94db015458c0fba7f94968753decabf56dd9518", + "17w49a": "b1f37d282f5100b1ff46a40c1f25465983f9e1fa24fd511247c18f69bb2cc80a", + "17w48a": "af1cd881b372f8460467b4d55120f89fe39a2410c6d15f5ae34ba8d0fe65e12e", + "17w47b": "6d88ff769dade25bcfeb56c6292061487ba2e3613c29a313b8e97e624aafe02c", + "17w47a": "376f208c9f43356697f5e28d0a3f50c112b78da35358c3b0f89c99e171b6cd30", + "17w46a": "417b0b0b39f5c8f1775ec3dbb8cfdda116d3175f4b68fafa87092644ac8a22fb", + "17w45b": "79ca13a3ae15b69a665762dc7859c50769b6fcd2f4d3f55797a242851155d253", + "17w45a": "a230593306d30e11d2e3d33bacf33873461d060270e138dc083871ceaee96032", + "17w43b": "2f4d74b3fa01d970289e9c4a8e5e00b2930d8edb621b2d693ddd6bfbf7df8b99", + "17w43a": "0362895039ad160810c7a96886938cf01d79aef040da65feefa3e9ed8e60cc82", + "1.12.2": "fe1f9274e6dad9191bf6e6e8e36ee6ebc737f373603df0946aafcded0d53167e", + "1.12.2-pre2": "724f1b2560afceda2f0bae37b4b9d3cfada7203edab2f4e1ad5fba8d9085c67c", + "1.12.2-pre1": "c4413733ed6d43f77706c7c8888b9dd4301d613a96f9e1d91d11d3e0c3cb6380", + "1.12.1": "848912640bccfa7ea34a2cc1c76cb2b35f8467c4216d9603917c991660f91a8b", + "1.12.1-pre1": "95a52cd874e32fbc4e4e490c64a1ccb47628f693a910c49a132dab9907694c9e", + "17w31a": "cdee889556c01336f33040c24adb6d59e474f8008f93aeeb7cb09a38d08f487e", + "1.12": "feebff3834e41cc096522525707d2dd27adc2431b1f3145b9d0ccfc4c8a3dc09", + "1.12-pre7": "670fe62bc16e28dfac35816e72ea639cc6cee2c11113d5c777f7916bbf0a3e68", + "1.12-pre6": "0eaa41ac44f2821fadb8f795e0b1081588418972a4c00e700dfc943ce19497a4", + "1.12-pre5": "53860ea9b4c0635da1246af27349b2e76912b1fa74790a2ecd8b60b99b9a15fa", + "1.12-pre4": "ba153a70a4775e03149abf4d93ff4f5272fdba005f5a3b24f9c781fa31c8c965", + "1.12-pre3": "9441df4d269ac3f7adc4229c00bb16c1f6292badaf89b425a0ba936ac98de051", + "1.12-pre2": "7c049cca26ee35fb3758487019c9c6d4784400089283ea85c6874e7d4d1db3cb", + "1.12-pre1": "1ff015429a46b8d6433fbb775c5b3d93863bacd30e085b1ac52e32e3fbbe6c62", + "17w18b": "acd52df80ad8c90463184e2b0e2355695aa3857ebd65902f8b82fcb029a23310", + "17w18a": "4db1fb4e55b4d87c85e2b7580606c9104065ad4a5ebe09f3f9c9269099967540", + "17w17b": "a0cc570906ac5cda9995edd7648189f3df1c42b11e6ff48ee304fa6f0406b366", + "17w17a": "9a6f5985c4612a903b875c09a2ad058440e4c14ecd03b929d1cd83822c536988", + "17w16b": "1d0c3f8028495275fa58c765f55081ea07462c33bdd0860b3d7d726367b739dd", + "17w16a": "8eeead591b5cb4660a0a800af02800816c34e6d221d989e21d20d0b2e1cf87f2", + "17w15a": "1f5125fa4829159297073245aa26fdfeef19f75df958877a0ad980f85d051276", + "17w14a": "1bf64744a93be9efd6eb866bd66875e81dc0af93b0ef0d190c4ea2ff3b8d8dc4", + "17w13b": "24d86a3d9fde9ef9ac748732f641a5a3f600c4c3331c3e1994a7403c5f917926", + "17w13a": "407832c1add81e2f1d1c5f4349df90ab4d0206060399ca5fc5e27cf15029ac2a", + "17w06a": "2f9b96bcfc7b19f51c26e623e22c76d7acbd009ca00ae1483123baaeea97258d", + "1.11.2": "dec47d36b429fd05076b90b1f42c2a25138bc39204aa51b9674ef2a98d64d88a", + "1.11.1": "8002bc32fdba21bf73fa30d94524b0c823bf7256e9c71598d2f18fb319e72c98", + "16w50a": "1fcbc53dfbfcf653c7e27621168a2cc4b204f0c7b07f371ec19fd90a8ac4f4b1", + "1.11": "3277965fd83d26944dfbd1b9740d95cf206985da330a1b3733868e4de7dc6f83", + "1.11-pre1": "41a5a5d9ecbdb9876adb834fa44d17a617b263c2a909dcdd806182b1be47e053", + "16w44a": "5414d2c025591ab3b30e70054582ee21a8a4d1ef1607fec39f09ca2ab3a69820", + "16w43a": "4a9d0c7f6a1c3c78b864039078d5c5c0aa6e4dcf0571bebb32b93dc9e8d18649", + "16w42a": "ad5d72d4b40cde7ece1196d93518419822576bb9fb94ab914c82a1db22aa3606", + "16w41a": "44b8a5de5cca66562e6da8afbe195760292e65cae1ccdde2a0311adc9426695b", + "16w40a": "803679e3afb2ad21366b7b95f19d7e882a2009ff9d8e09220b75c3c220bf2b8f", + "16w39c": "400f18ed5ca26d870e8ff1cb143b02dc1fbcf262647ef2ef9fb3478b90f140d3", + "16w39b": "f3e2d0a4b1215b21eea13b11321c520493ff68d95a867334c16d25e188fa87d1", + "16w39a": "5d7fbc59215e694d8f6814426c47b79b28040081d30ad1d3c10814d6966fb85f", + "16w38a": "34dc55b0c358e68d22831f9da261939f407bc995e93b95d5c3c26c6bf443392e", + "16w36a": "69aec3df1fe9b087be60573111382c3c7fbbfc5e54f8a70e839bcf17ab9b1345", + "16w35a": "24f92327cdf6e836c2e36884a20f5d6b9eb62b2c326e73ec8c816b8ac8f91cd2", + "16w33a": "7ee55ace0109b7f31eab8496838e8b001da154c8c848b2d2cbc26952ef44107b", + "16w32b": "7c59216c96eb750e9e5a18dc255139645b90553942ec9569ea2618e5a88a9849", + "16w32a": "ff7cfbbe3d93e0ddd7d79b9185051d0a607c2d98bf3240c3663e74f9d219d6d8", + "1.10.2": "195f468227c5f9218f3919538b9b16ba34adced67fc7d7b652c508a5e8d07a21", + "1.10.1": "623c8d67e7357c7078f30cee450562c52a05a42c394f064474c7f2d45b4a7d3f", + "1.10": "dd44a72e920a01dedf57507b73642f4a9dd8c6052e1f42ff6cc0635008014201", + "1.10-pre2": "80970a1ad42fc434d306205dad3e71d74d2af057eca8a49756d742628b3725cd", + "1.10-pre1": "920f899b57892424a9c8117f8c21563a76f5fe91ca70f94da467efc7772dbba6", + "16w21b": "52d3b46dac4f555930879f5eb7d326e55dd79a90eef381bd178fcfca42a263da", + "16w21a": "4d9ecda93f23af7f9efef333a4b9ba4d0af2484b4664940c1b6f18f613002c4d", + "16w20a": "9b0b5b31b198e67bdd316b1de23c2df587692d064df1b20746d40d4394891105", + "1.9.4": "13fea7aa10d804dd14ed7ebde2493dc64c7d3c8173369309bd7f6ea4c0ea40ad", + "1.9.3": "0eb669eeca23bad6d4ee5237aa24cd51274dfbda15813477f8414f4bfc2f1f27", + "1.9.3-pre3": "fd57a52c156192c06490a9775d694433065fc8bf93f41d6286ebe68db3fb213f", + "1.9.3-pre2": "7a21a84b472a50ef865872c7eef5ed8c2d0a94aed8a459a11562ed6b1086a5d8", + "1.9.3-pre1": "367f81f9a5c3d8983db29e6531c048d8abb5e62eec666fe1c2725b2ea455b3c3", + "16w15b": "0141cc7bf999021b14be167e087806135878410aa283a1f904a86937593aedee", + "16w15a": "748877dfaedaf531cb0173b39e54d9f005628a19e89e30204cad5ce3823bd6ef", + "16w14a": "37047a269614bf730a6bd942cd1180f7cffff1c33aa3e962ec895d7b5a34b7b4", + "1.RV-Pre1": "b85a55a0fc78771d2c3571dc7c4d9c8f1a2aab9edcfb3e9fe3825dff5befc817", + "1.9.2": "a972d127be3b9d5fafe5bd610a173563cb24331b6664a3dc5f73b3cc76d77081", + "1.9.1": "fcc5393c191afbcc0b706bd94616fb171e2fadaa107a3e5f36298ed2abf76c41", + "1.9.1-pre3": "b62800a3e74c3be7ee99503a848dd523e56c762f69c23d7ebab99d22538a4fa5", + "1.9.1-pre2": "eb162b8f05c638bc5ab296ddc56dfb598849ea29ba349e0dff38a09398904e1a", + "1.9.1-pre1": "20dca2343649d91ef2e017ca61d71c5cace0d73159ac020ec44e156c583509da", + "1.9": "38a797f50c71f55202e2135a30302cf3a5c8cb494c6d225b88599542957d3a7d", + "1.9-pre4": "e55154e2b238be686385dd3a29f98a8aef4b2175cacbc68432037def340fb6fb", + "1.9-pre3": "d652eb5de18eddd95de594d9c45b0aca76c80b72b30ce7b929ce97a512f662b2", + "1.9-pre2": "f5037617e8b0235544f546b178799305e3f32829996e0827c9f1b496fc231047", + "1.9-pre1": "84644f7ea596b7f730cc98018660ba6629e6392323ccdf5c71fb85332bc088f9", + "16w07b": "342bab138d633ddd2470b9b88e65dcc39da5fbb8b65b7fc917600587f084ac54", + "16w07a": "6ecdb84ba29e8478464997f2a0f8774776e209052e9e124e5c015a02b6caa149", + "16w06a": "88d71f7a3eb597b06c0519de221746ecdaab2cd82707ce443828c3c0becdc0f6", + "16w05b": "344ee31868913e095301a9153d118ec1b87bc624251231ca5032978d726e6515", + "16w05a": "e070b7a3a46ec6bd03d950f745e0d298e68a88b360d242797f50c9295e0f7e41", + "16w04a": "dbe527c539d7a3fc2bd6a50b11988bc3e484cd941f93bf8e6c852f4ae194aae5", + "16w03a": "992f0ac1b195a538f4b84ebcbe137935a714658d8acaae197b11508f7744e23d", + "16w02a": "aba2d1579db0afef8064747402622741bd771195d6377bd3629f7dd73a8a5cf8", + "15w51b": "8c8e8a435c8bd832a2c85358124f86ed9ccc327c5c630c28a10b29bd01526c0b", + "15w51a": "2b046d0728a74c0b016c9448cffc6bd49c05d1642156b3133ffcd937b98298a6", + "15w50a": "e8b92400015342eabadcfba5e6f31372a1c71d07dd868e0139277c0de5400d85", + "15w49b": "e948e5aa52ea52e323fbfd25091c7bfc687ff75a529e12efe791ee2b7b6342aa", + "1.8.9": "c18e4245073aaff580eb7359902f0251436568b1647a9e443a924cdb73fa8312", + "15w49a": "8f281199d271ea365542523e486d9b623d061867709833046442d3512c4b4720", + "15w47c": "0e70fb9243c774f12cded32a7c50b812cbf341cafa637fb1be46f788a117b9d4", + "15w47b": "f38c0d3ab4f7a9d7ba75e02cbdaa0ca2761a7406249ee03d51dac130f52c8234", + "15w47a": "ca07d91f61fd9661c52be2fd56270900e2d2d6659ccc8166dc4dfc89bab5f6e7", + "15w46a": "ccdc14e21259818d824c1498f035347bbcb74e9c4d6dc20169d61f3896eeb963", + "15w45a": "863eebd07e30d5d4d9f18f419d6cf123f75ac2b6f2c12dfcd61decd4202d269c", + "15w44b": "bb56195201a68a4966efe30c3c317d69d74ceff756276e159340d17b2b9c44aa", + "15w44a": "0a13803768cb8097648131d681c17f99f75c2948d6e5c6567c220f33c9e579fc", + "15w43c": "dd85afaca19fe0d335819dfd59e6162ad2675b0c59300a27c644b535bf359a8a", + "15w43b": "5efe6b619b1ca31a63657b6b58a1b9d4c27e4f40ae7d50d1970c1d3e692b4ce8", + "15w43a": "074212c0b20aba62419277b7ebcefc64c68d3a80813e95db7966a72ad67f6679", + "15w42a": "a16bbe888735e68872ce6863d93cc9d89df39ed2cc9e227b425e2048c6bc9343", + "15w41b": "8926af4de6224a9fef9f010fe7eb114da54fd4af86e21b055faca20c6f61c2db", + "15w41a": "4ab8a919feb53f5f8cb7325c001ebad6f3d721f0387983459da883c8563af87a", + "15w40b": "e2569a83a80ba0fcdf5c1c495b677c3652d28a3bbc3b14f91a3cbde5a8da0e7a", + "15w40a": "2a4b746ea50cb855710dd0dc3e04bdafc7ffe02ec9501e45b7782a9eaf0c0b55", + "15w39c": "41ed25f97e6e767d47185c5e73e16b288f71398c895f8874fdab620de951a305", + "15w39b": "a840c2fa3f11a3d4591aeeb9235d46cff269959e5c03f40207e00bbbb997abee", + "15w39a": "ed755605c41790de977c07c3bdd283e1eb79d6ee2b4ca33e476a3ff87f8154dd", + "15w38b": "8f7729e2c9a433e4786a04074d78f78d9a8496cab4a28d993558b31d1b8442d4", + "15w38a": "b46119ef3d5c5c2509108f86466cbfc15b5bf87d21358f5d787241f9168b61cb", + "15w37a": "e3e16fdb2149a1cd1fe3a286ae822a84581db90cf79f8f3646d3a3d394a55f34", + "15w36d": "3bcca5c4b0bc4281fbaf9bb24d64c75f90e003fa742f5de996aed0b21a19afd2", + "15w36c": "7ee42fe9f4b8c4df8918c8cdaa806d07b67ec0ecc6a0ea3ebd45cf084a88cee3", + "15w36b": "0641d3294b319a1116633f8b6658433ff6a5d46106f0027529bbaaf73fd0d8fa", + "15w36a": "88b420ae14500ece0164b70470ae114732175ba242c792379d006c9a8b09e92b", + "15w35e": "74bf64ef314fee221d5e330be910aa4a6bf55a8c1ec65d2077fb89be41bd64fd", + "15w35d": "55e871006e45e156de6a0dd4e3516ff7ab41c6e6803ecc9a648117f8fb512b1e", + "15w35c": "3c9afb5bcf6fef5933314ad09dda469be9e6cfbd07b8ab5789e468e4f3f93833", + "15w35b": "c31ee139dcfb4cecfb08d12313cee5bf60dd0f05194d4ddf804df471a0e76446", + "15w35a": "f17463ced603699eeb075916a97ec3de3171c145a619a498fcc8a7f045f4da7e", + "15w34d": "a1b9a9a1fa47bc8ff7e1efc8f3603921ec150abef71f8fefa5f209bbbe079859", + "15w34c": "62a1ae63cbf38ff9f94288b5d3dcdd462918cd4c2d465a809fce3c47ac05baf5", + "15w34b": "9e3c2f29682f2be1a4c628eb94040ac4d67ba4270e98ba8330aa3ce33423ce12", + "15w34a": "46875a087599b89618ee5502dcfebaa33307babb03b221aa9d924d83c5ce3f48", + "15w33c": "f36b658c781660cf4218e3ccc45aacaa2800afd3bc81f136cbe9c13364b6963c", + "15w33b": "c2a8bed5a76a95afb35950c2ee3605bdd3fa774a905103edafe915283e9fb1c6", + "15w33a": "4422eaf4702b96b40a58e7e6d440ca29289669f562575ea4016971971eca8be3", + "15w32c": "e8f3c78a60b659297f4ebda8351779a183385eaa2edab954920eaee184f0ab88", + "15w32b": "5aed03f99346ed70cb2f63279382822abe754f047db3f4820f1581754a0bb843", + "15w32a": "dcdcebf422252abc2fb9f2ff8835c39e3bbd0a0197b293de93eda5818ab3beac", + "15w31c": "3c502fad30a1bcd1b1ffa077825885754bf524b9412e2cbf65628b8609efaccf", + "15w31b": "e0de11fe8a2c9f1020e35d0b178b0eba37476dc38e7f7a0a15bda52cdadd60b2", + "15w31a": "e4f9e7b1ebbef58f1ae99c35c6cd3d0fb13d7f4aa6b68995501f512561c6a63f", + "1.8.8": "39aef720dc5309476f56f2e96a516f3dd3041bbbf442cbfd47d63acbd06af31e", + "1.8.7": "5cf4a49762c996c94f6b8b119f1c80b4de3c12b2f5c53268801905bb7daa0644", + "1.8.6": "7fc66b2b54f0f4d65fdd6d6484a50f432c144ef02072d3435d5660f120f58e0d", + "1.8.5": "6a412e89009acfcd5c56084ddab4f9676c5561bb58a3f22d5ea4ba4ac5d3503e", + "1.8.4": "394a9d0d5bcd03272a58f036b8736a47d26d63b45a4e7c820629114876e72107", + "15w14a": "9920b744ac1f7de76ebf4f33fae5d0b53baef35bb175529a3633f53ae17f2e99", + "1.8.3": "a26751b18ccc80ceef488da645c3b785aa528e2ae20a6a6dbd46f6dc754e62bd", + "1.8.2": "d99f3b3478018cb454fbee36fd60e3c4acbe1132f1cb26b3657f9ad291e7035c", + "1.8.2-pre7": "d19c50683a17c43fb64563a5fe75b6417626c57e2ebd76d04f71316794499b72", + "1.8.2-pre6": "01a28ada45d313a1ef59757496ae0c50b79f9152d5ab9fd83b90e9628345a114", + "1.8.2-pre5": "b9694f042fc6028e3d36160383169fe6d9a5455a05d002733f547466e5bce69f", + "1.8.2-pre4": "0511885adad9255c4d8e1d0b193f8b0fe3cf5fb323629235571d337c46f6a342", + "1.8.2-pre3": "9e5d0000101e61f3c5de520743850954a62d4b0b85df0d75a58f9f9384941e8a", + "1.8.2-pre2": "c4acd0660a05bc76c24c6bd7dec0c8c05fafd03a6218071cda989dfc2ec9d6b8", + "1.8.2-pre1": "00514ad81d46b19a8b8e066f0f4716c2cb0a3275183cbff9db119b01ea5e48fd", + "1.8.1": "ef5f5a1a1a78087859b18153acf97efc6ecb12540ac08d82b9c95024249b9845", + "1.8.1-pre5": "f492628c8a192e9ec8f9f5a4283c3c36748fe703d6733d4304c0cbc725d20ee0", + "1.8.1-pre4": "970cdaccb4ff3acdf2bbc5b4cab07e2ea9a1b20271f09829ffbecaba7bcaf93a", + "1.8.1-pre3": "dac84ad1dbd16365e48d59b6772dde5758256c2c9a315d2bb61dd29854595e70", + "1.8.1-pre2": "0e19fd1db175aa8a957c9cf7ce60dfc430090ed71d4f981af502856f69227630", + "1.8.1-pre1": "c581dc7475b45f35f573ca6ba90e9cf7df9b95ebac007594c7d10b24c781550c", + "1.8": "40e23f3823d6f0e3cbadc491cedb55b8ba53f8ab516b68182ddd1536babeb291", + "1.8-pre3": "da969b62ffde078c8890c40b05c47082bc691ad53b026534c87057a2f4cd0118", + "1.8-pre2": "5863a5256894335787f9a048c96650b1d28ce7e049641268bec89fe6e083a310", + "1.8-pre1": "0927b56f23c480d94889c0a837932a9d44ed848059d04835764f78365ef27660", + "14w34d": "05d4a426e75245475d3c88cbbe913658964661df2d8605a0598200ece76b81de", + "14w34c": "069dd43e6c3235d488f945b675798f5c522a0e9cf811a7487a6fcf48db52a3ca", + "14w34b": "602e9e03cc853f9291aed021f3cbcdc87377d2afbfe19a7de314ba0b759676b4", + "14w34a": "19bdb3b1366ccfac7fab918b9f02f75f420dcdfc210129bf909c720f2cf7a51b", + "14w33c": "594aaf7505dbc447b2cf3b67cc026c4885581e45841107d065544824e6e34667", + "14w33b": "b935e611d9a55f464fe29d6b40ea628d7d5d3e87e03c2ee84a055aed40757b06", + "14w33a": "9f6dc04ea65c2404e3c910efbba33ba057428c873b55ea21b98ade7ebaf8b025", + "14w32d": "a59c224c33d0a5b24d41d13e21509fa79053e413e22b05471118e221cbf65864", + "14w32c": "d673d52b400459ed6b5003af36897149f9917c5146452b87bf806a851661378b", + "14w32b": "c2c4ff5940197e7ab6ef922cccce04f9cee0ef118052c4d607d9351b930f3ae5", + "14w32a": "6969688ad556ee91ae078d31e7d5d84b24dd907ea0ee55976b90a1b16981ddbb", + "14w31a": "a58dd363035c79925a5ce03ef3da2ba5a25d514122e842d0188d4e1055259375", + "14w30c": "55b92a96054b8f50c7b0d4a67fffa4ca9004fc87d88402699d66afe230b42773", + "14w30b": "ebf69ac57cfa9ed3146bcc5b4757cf3faf2b6729b61e016c12e25f8ce4bf01d0", + "14w30a": "a9fa0fe45a96412e1e3dcc065de6b0802f659836e366115c6f9557c572560ad8", + "14w29b": "445d21e51f68b5bd30fd905c5739662f2f42fd6bcff8bf9a8fedc2926d94b407", + "14w29a": "d2f52276df97dc039ffbb6dd2b9a2adfc6fe8227b394e98efdf125646a4c0e8d", + "14w28b": "30974fb655414499f186018c4c68dc970f0ae21cae868ce1b7ee1debaf58b6d3", + "14w28a": "b894cd9051fe26d0619b9a717237ce770f8c622b16b39e514ee9e804fd9afe90", + "14w27b": "05511f5e092e94ac7342ddf9685ec3ca820cda5dcc072ed9a9deadcc6c2856d8", + "14w27a": "a8efb5015780dd070b8fb92a11dd876a20b653031f4314b717c972608003bede", + "14w26c": "e6793c31489f25a76cbb64edb9b675e13db5e9d7e7d48ec12aa2c3f16812c25d", + "14w26b": "f65db21b7e44208ccdfdeec9b57d6e4ea640cbbd8481503ede4a27f3613522c4", + "14w26a": "e998161bd9dbb3ce60c3fc757d227dcabb3a95836ddebc1d8740dd2888c06638", + "14w25b": "96bb81e322c8ad08e6b085cc6f2eca068a069db44fde78afb4760e060bc4cb70", + "14w25a": "decd0e814c06860fc66a8642fccfb1deeaf60bebd7980cbf975a0797051f6509", + "14w21b": "2c9e15b5d47eed678da1172edd0e05f07fca9b2b9d21a91c6d3dc92558c3aac0", + "14w21a": "387c81b1aaffb6982cbf1ba1926620235dccddb27cadc14449cdb5b3befadf1b", + "14w20b": "66543e6776a72fa407cae0e804e9fca642ea41f22b54fcfefbef76f584763a1b", + "14w20a": "0bbb5bd0038c09401033fe5d28f4c207c44bec6920afe5d6cc3eaff1a7b49ac1", + "1.7.10": "c70870f00c4024d829e154f7e5f4e885b02dd87991726a3308d81f513972f3fc", + "1.7.10-pre4": "882648310f8e370b7fcb71a2e4f0dc578d59d3c23f6d24e0509810fc8ae3edf4", + "1.7.10-pre3": "376cf7df05a1d1f265a0aec0129040e9ca75ba43c279e929b9e43ae5adb781b8", + "1.7.10-pre2": "21a9c212e91c0af5b4d56bf0dd411227732554804b009a4fbd36d7b15a4fcbd8", + "1.7.10-pre1": "c0e908addbceaf60d42a424baa1b413264b9618584948794ec5ae151a81f8068", + "14w19a": "632fd260c009bcd29e5d0412657911cfffd8b7664b5f51802bbfa169684a7cfd", + "14w18b": "24d6d49d9f3b332bb3c5a530561279077f986c6e19e8a4b6f7be3842080afaf2", + "14w18a": "86ebb97a1b18657f8468c45c64d7de6ce3ef19e9897564a4adb17abd5dc88fb6", + "14w17a": "c7f77b08a3df25c30386daaf75ec36b3c8968e6621c558c995c427adbd327863", + "14w11b": "131a37f8f960c06bf119d435682bc40ceead6de48b73aef2bda25a418c1c239a", + "1.7.9": "f7b9150d05c2cf8c48541527de310557e6bd9bde73e8ac9479e8ffe722c60a21", + "1.7.8": "5907ef1103acf15952d6d50cde3db01e4fc5a95b9f5fec0be25fa56a5ef0d121", + "1.7.7": "74646f88ed76d878eaaf2b28c4ebd4043bb11255999e389345dc55d2f11c19a6", + "1.7.6": "5cdc6ab6168ae496ce1d1ae96c0b165360184518d030417429ec7687c0b9d527", + "14w11a": "508e7a5c272b0428414ea6a84ca4b22c11d48ff4cecca26ae083390de8983655", + "1.7.6-pre2": "c4e8751d2b38e4ddb9d72189d5d3f0bfb82d5b76ce2d4792878cf37f4162bda8", + "1.7.6-pre1": "bf0407bd78a9583c0957cc6e1657dec4d19813b8006c6ae1e8f1c49f64807981", + "14w10c": "9dae5fe55d939a5837887a1f69844d9154888331fd362b1c3f722516d4f4f5cc", + "14w10b": "83d4a315981a07e1f1951a95c02f4d0d7a0b7d5f7fe476c60af78b6d6410ae2c", + "14w10a": "4b2d17daf2a41a336abdf1e098825a5cbf3b163bfb8d992b9d8c4fc99fd418ff", + "14w08a": "a84139d1887b20fa3363f6b94dd93de41b26c8a1ba1697967ebf26d65de6879d", + "1.7.5": "caa9e13aee32112b3b5305c02ecc01c05502fe244cdb83faa7a01832937542e4", + "14w07a": "44a814b0e306a0368e569e0596719ff07897b355df131b21acbe85199e6f9ff0", + "14w06b": "7d0d40ec07935c79c46a2f1689b0e983dc1214b7f57b9a91c5f4c47b1aed1353", + "14w06a": "5d2f23dc66faf6ab6d1b7196e30c0016d324e3c3a4f2b1facffee2677f8415b7", + "14w05b": "08dd19438e280481c2b83bc912b162250709ff523b127f365cc35c696c452c87", + "14w05a": "0c61c3c1d4069c29cd7db31593061a4b4abbe2b74623b54fa86ef73eac9daf05", + "14w04b": "5492e4ec31762bcd059d1d7238adf5a33e2bb5b5816a5d72a9eee6dbbec8800e", + "14w04a": "d24129f6e93e69706b465134177186d22e404067d58099ccc07f3bec81941085", + "14w03b": "0342ee40cb4bc3e2f2142bc4efd27033093c68c856f8d08f10afef3d0f5f31af", + "14w03a": "7a2a49d7985a9d82bc2026ae0465705cfa12f32e65f9bc1bc4d05de7e3de4f69", + "14w02c": "13095b3c5871fbbe1ee3dee8908b53ba2ab05b50f51dde0681b58c3f24777742", + "14w02b": "d39db2835ff2b2b2f6d6cbaad7dd626a2ffa8097343bf82c72a6203306208ae2", + "14w02a": "aada0c408d7776a08617748cbf0a3fd4abea36474d87fca1639b453512b48980", + "1.7.4": "796d6ca283861a3185f2e87ec321b1233540cdf2638da6e00f1d96d47791031f", + "1.7.3": "027b0bf027910d9ed3c49ff643ee81fa875e95052cd66ab7648a9e602973f266", + "13w49a": "94218edaaed13d8ddffb83cdf2f2f9e4d6b1e43d2094fe0a9e8c231f610a867f", + "13w48b": "9d66f65d85ffecf0c90fdc751f3ae1151f90dcbe175811f198c7320f1b37ee0a", + "13w48a": "8dd6c1fde2cc68f314bb50d7a0eb63a33d2d8ddcf1e5e0d92475529d2006b656", + "13w47e": "8123fad577fae5a92744847f203c3dfc8937de1f42ee21154c9e4d656d783c5f", + "13w47d": "ed8a11184ad77069a92500d0e90ff2f05e0d3ac4f0f40b7e51dd3b6198f869bc", + "13w47c": "aa0ef4490c66e4eb9d013d15a6368c38a658a3706bd9d6ca74ad1e5939879317", + "13w47b": "e4ea5ea684c7cd3c2a435fa35d351e5eb33d5a74b8d6c091001ea3bf3aac6d98", + "13w47a": "e2e288bc12fc7823dcc69d6e8dfbcf9c5e41c7f7032af5f2bb23d1f684977485", + "1.7.2": "b4139899700c1bdbf72880eec4bdb9e46c2cf22d1724a48a018ee0330035462e", + "1.7.1": "78ddbd2dee1c68b5f1d92ff4752cfc6dec3cc15320b41df142e2049c6e3c0ed6", + "1.7": "5a4e9b5a4cdfd7a681195c6016c94d8e5f49e18d91ba0ececa1d81ba5fb15aa7", + "13w43a": "7542353677246babc76d0ab2de4f2c9770c3685ce31f7f08143583b3c7f4f2d7", + "13w42b": "bbbff097c83f1dbb9120eb9f3af0457e339ca58f54b3be612ed4eec09be0b03d", + "13w42a": "0f62c56b10d7b52df893377b60ee9417c956513b8e1f1606b8cb7bee59404642", + "13w41b": "e9ef9fc8304460c9b0efc73e7a07b6b183d2a90d21c4ed13f4087e41742b0aab", + "13w41a": "494126c0143fa85a1b28ac03ee36d1d404931acad8364ee5f5e2acdebb53ed73", + "13w39b": "99229c20fc69fce7994e6db1f0b22224ebc75d5489fbddb4ba85a503ba6ad0ef", + "13w39a": "b738f75209bc5acb3ce9baebd707ceb98306ca59395c7a60e69370d61b48a0b4", + "13w38c": "d42dc65f9a173815a67933b12ef0abed77428142c6559336155a7f9b231b3498", + "13w38b": "571b16ffc98af57ef0f197318b6b3674a07bac40c541acbb90b5e5cca64deebc", + "13w38a": "dd58e97a800b63d88f3a9599120fec9bcae0643dbc70f3aa602e2f620f99fa56", + "1.6.4": "81841a2fedfe0ce19983156a06fa5294335284beeb95c8ca872d3c1a5fcf5774", + "1.6.3": "5a7b3f4ec258b55c8e4feb956d7861b48501a61a618f5c6495fc86ae4985f0e0", + "13w37b": "75090b7f800c690df76b8ada5d31ff96f54e33a24f6337e1c13e71dff625a938", + "13w37a": "3531513b752a1eb65a3f500e14023c04ff9711c61f4769b48e75d31d92a57bad", + "13w36b": "d868871077dba9094990274b8791882ee60374c2696a3b19d04fab5aea6da399", + "13w36a": "1db816ff10df3f0d5a8e9728360ddc8090f6ba7bb5e8c17066ea7ed614303a88", + "1.6.2": "99a7f4088226f5574ec47fa69fda4779376499e5c9c5b8c2342563c7ac35368e", + "1.6.1": "d58a6cc07305bc3bc3915fd8a81ceb07ba4bede3111c971115815789e5674611", + "1.6": "7e6fd851b7646aa32964b0d3370ec33ddc64695074e5c26634d3ff0951617be1", + "13w26a": "3dd2c0ea0ce08c1569acd40e1f09145c620325eafa65ef522551139e1e039fa0", + "13w25c": "949f32be91f32f85f17511abd47b79cacc7bd5d8d8a5d4eb99f282b80bcfd156", + "13w25b": "0dbe9d6cd69551c8f17bcbaf2adecffad6a9c5bc1c0f0e189c2005fb5fa73fc5", + "13w25a": "a9143baab9a1f0d693ebab2420d9e876ff1d62274781a2b2d3112bc0f3da2540", + "13w24b": "b7cd0305793f8a61363781b8dd9800afe0c5bb65e9542407a0c1d15a573b14c2", + "13w24a": "99e504f1ee5dc1b11bf9a084a8d8afaa03f413471f987a39d99cdd561af7b5f2", + "13w23b": "ac28f91f4d9dbe4a2c2098e1231345cb9d15d52df4f0405e4d2beeab30943886", + "13w23a": "005b3ba8d7c26c370be32503868b83984fc67ad88dc5e46b9208bf980335919b", + "13w22a": "a5e21402916da0fb6663a0d0d5584de2b3bf58c2620d4aae6235efb74747fb33", + "13w21b": "3ed24ca37e193d647737ceee6e5f3acb1b40d2209b46bdec9d87df688ecceb77", + "13w21a": "7f4d1dd9cf844fa590cc30051040e1fd8c3c115cb4b48a4b9a200cbe10a5bc0d", + "13w19a": "3d0083272afe405518daa345de20824272669bb91b0568b14e28cad06f4de4d6", + "13w18c": "f0b5bb768e87d88334da5d30c8cbd1503448e248091aaacd8cb8b56bb30c3a76", + "13w18b": "a251665c17145c9f5b0a7aa9f6ed2b7bbb007c48fc7ff4e5dc4cd3b2efb8c4fa", + "13w18a": "46e96fff19bada9e994e20fbe044ec4bc1b226a31f855b4a1760825425676076", + "13w17a": "f2fa011daed1647006df3859147c08d5935f4a9700c0fbd300524c3d800d9fde", + "1.5.2": "4f0c7b79ca2b10716703436550f75fa14e784b999707ffad0ea4e9c38cc256a0", + "13w16b": "ee358d4f84f91f1623f746dcdf157bd526bb2897d5c5293a099ec7ecf72dab51", + "13w16a": "3d3d2f3d9480ef2cb1abd31a1c5879603af0260bd95fd298fed6f026bf42f7ef", + "1.5.1": "e8dc60c93992e495c3a9e6dac7517e6811af7a803a3419eea50dc78e97f51297", + "1.5": "71239880440f8f22a96aac5c00d954401859ddc8847b4f973f563de0b6a5d781", + "1.4.7": "96b7512aead2fb20ddf780d7dd74208d77f209e16058ea8944150179e65b4dd3", + "1.4.5": "b8af871d6b0a03dd2fe65ee9238bb52c60dd5e30d3ded0f37a9eb860e5df206d", + "1.4.6": "90b3b9cd466abcd6ed9e932e1b81f8e34c5771f536670ed9ac493188b021000b", + "1.4.4": "2ea46e24c3c2931dbce11e4d79a83668cd6f002b5bfe131645a98ece099430a3", + "1.4.3": "283c15e256ad4776906e6832de90cecc9a5fd2c28651c6800024c3bc90f5f9fe", + "1.4.2": "16bc7305231d5ceba8b81e43cca8bdcd19cc6d92a488ed270baa5ab827b0fa40", + "1.4.1": "a6ff759d3161ceb3dd9997daaa53c3916f2c8b8b61e38f850a4d9577ea0678ab", + "1.4": "49c50a2c9ad4ab78c1ff9048c1a7f000a4f4d0628000902190ddc1a64d293b71", + "1.3.2": "0795e098d970b459832750d4c6c2c4f58ef55a333a67281418318275e6026eba", + "1.3.1": "62b8c8a3691fb5f51af3bd7efc34d1bc5a227e6162072e9827f439744df994f2", + "1.3": "64ba1cc32240cf12c76b6a235b299c16110e90a535f9c83fc08d9e2e766da0a9", + "1.2.5": "19285d7d16aee740f5a0584f0d80a4940f273a97f5a3eaf251fc1c6c3f2982d1" + } +} \ No newline at end of file diff --git a/src/main/resources/certs/mojangcs.cer b/src/main/resources/certs/mojangcs.cer new file mode 100644 index 00000000..795f7306 --- /dev/null +++ b/src/main/resources/certs/mojangcs.cer @@ -0,0 +1,120 @@ +subject=C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification Authority +issuer=C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification Authority +-----BEGIN CERTIFICATE----- +MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz +cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 +MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV +BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE +BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is +I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G +CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i +2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ +2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ +-----END CERTIFICATE----- + +subject=C=US, O=VeriSign, Inc., OU=VeriSign Trust Network, OU=(c) 2006 VeriSign, Inc. - For authorized use only, CN=VeriSign Class 3 Public Primary Certification Authority - G5 +issuer=C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification Authority +-----BEGIN CERTIFICATE----- +MIIE0DCCBDmgAwIBAgIQJQzo4DBhLp8rifcFTXz4/TANBgkqhkiG9w0BAQUFADBf +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xNzA1BgNVBAsT +LkNsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw +HhcNMDYxMTA4MDAwMDAwWhcNMjExMTA3MjM1OTU5WjCByjELMAkGA1UEBhMCVVMx +FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz +dCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZv +ciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAz +IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1nmAMqudLO07cfLw8 +RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbext0uz/o9+B1fs70Pb +ZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIzSdhDY2pSS9KP6HBR +TdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQGBO+QueQA5N06tRn/ +Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+rCpSx4/VBEnkjWNH +iDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/NIeWiu5T6CUVAgMB +AAGjggGbMIIBlzAPBgNVHRMBAf8EBTADAQH/MDEGA1UdHwQqMCgwJqAkoCKGIGh0 +dHA6Ly9jcmwudmVyaXNpZ24uY29tL3BjYTMuY3JsMA4GA1UdDwEB/wQEAwIBBjA9 +BgNVHSAENjA0MDIGBFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cudmVy +aXNpZ24uY29tL2NwczAdBgNVHQ4EFgQUf9Nlp8Ld7LvwMAnzQzn6Aq8zMTMwbQYI +KwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQU +j+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVyaXNpZ24uY29t +L3ZzbG9nby5naWYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8v +b2NzcC52ZXJpc2lnbi5jb20wPgYDVR0lBDcwNQYIKwYBBQUHAwEGCCsGAQUFBwMC +BggrBgEFBQcDAwYJYIZIAYb4QgQBBgpghkgBhvhFAQgBMA0GCSqGSIb3DQEBBQUA +A4GBABMC3fjohgDyWvj4IAxZiGIHzs73Tvm7WaGY5eE43U68ZhjTresY8g3JbT5K +lCDDPLq9ZVTGr0SzEK0saz6r1we2uIFjxfleLuUqZ87NMwwq14lWAyMfs77oOghZ +tOxFNfeKW/9mz1Cvxm1XjRl4t7mi0VfqH5pLr7rJjhJ+xr3/ +-----END CERTIFICATE----- + +subject=C=SE, ST=Stockholm, L=Stockholm, O=Mojang, OU=Digital ID Class 3 - Java Object Signing, CN=Mojang +issuer=C=US, O=VeriSign, Inc., OU=VeriSign Trust Network, OU=Terms of use at https://www.verisign.com/rpa (c)10, CN=VeriSign Class 3 Code Signing 2010 CA +-----BEGIN CERTIFICATE----- +MIIFRzCCBC+gAwIBAgIQWAyDGhMqlzv+buZKWtQ52DANBgkqhkiG9w0BAQUFADCB +tDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2Ug +YXQgaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykxMDEuMCwGA1UEAxMl +VmVyaVNpZ24gQ2xhc3MgMyBDb2RlIFNpZ25pbmcgMjAxMCBDQTAeFw0xMjA0MDUw +MDAwMDBaFw0xNTA0MDUyMzU5NTlaMIGKMQswCQYDVQQGEwJTRTESMBAGA1UECBMJ +U3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xDzANBgNVBAoUBk1vamFuZzEx +MC8GA1UECxMoRGlnaXRhbCBJRCBDbGFzcyAzIC0gSmF2YSBPYmplY3QgU2lnbmlu +ZzEPMA0GA1UEAxQGTW9qYW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEApK2feVemdO2gfn5ewbbUZGeRvSygG+bHwIj4fK8c/epUh169x+hDUCrrSI97 +7mrulhegTTI+zqsF36SRaWOxlDu75LUGSqp/WKCRQyBqiUB+ZIJXaemWIZipBcWv +rPRYm0bZLMJERT1W+KlshCQSkXDcof8zlFnV4HQ9X9zxlk+9uKhYlCuM1c09sjlK +7xegZUIDiu92g/sRIpVHrtyXLbnSpHRxDzFYZkJDPFhVDjK2x8NIuK4yNOf1nWoM +QYu5V/8tD7uG+HVFTiIg1SkOLNW4XCn1+0vUt2TANga+/NZxLSrlR0Zwtm8KPTNj +o7aOZ40dCfbZqQlqk9wS+2X6awIDAQABo4IBezCCAXcwCQYDVR0TBAIwADAOBgNV +HQ8BAf8EBAMCB4AwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cDovL2NzYzMtMjAxMC1j +cmwudmVyaXNpZ24uY29tL0NTQzMtMjAxMC5jcmwwRAYDVR0gBD0wOzA5BgtghkgB +hvhFAQcXAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy52ZXJpc2lnbi5jb20v +cnBhMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHEGCCsGAQUFBwEBBGUwYzAkBggrBgEF +BQcwAYYYaHR0cDovL29jc3AudmVyaXNpZ24uY29tMDsGCCsGAQUFBzAChi9odHRw +Oi8vY3NjMy0yMDEwLWFpYS52ZXJpc2lnbi5jb20vQ1NDMy0yMDEwLmNlcjAfBgNV +HSMEGDAWgBTPmanqeyb0S8mOj9fwBSbv49KnnTARBglghkgBhvhCAQEEBAMCBBAw +FgYKKwYBBAGCNwIBGwQIMAYBAQABAf8wDQYJKoZIhvcNAQEFBQADggEBAHT+RhnF +LoqUlSvo0bxl3eUj81FZg0neyCnpGZV1bFqmDwcwHAWRqOSkrOYxTed6v9cJl0q1 +FPXU/6ic0lfUWNqcn0uaS5vfVRpAhRnliLrGlfE5fQfE4lguOUQ4cILK6AxJpeKU +JVDUoeObG2ven83yIy0guevE/1so2VXnV1bFLTtdS5r6iqqMGCshDZMFVleYMo0S +uhfubZDtKIhd9pRLkpg3MzchYLmri5NB67vYZizW11W86QZIWoDIJG8NAWyz6HOJ +rS+ecFa1TBo4gkcKDrd6DT8dMBNUUTTECo6bTGpSfkxjaRK82ZZwS7ui2DMb7K7e +K2GyWhR2PnMg09o= +-----END CERTIFICATE----- + +subject=C=US, O=VeriSign, Inc., OU=VeriSign Trust Network, OU=Terms of use at https://www.verisign.com/rpa (c)10, CN=VeriSign Class 3 Code Signing 2010 CA +issuer=C=US, O=VeriSign, Inc., OU=VeriSign Trust Network, OU=(c) 2006 VeriSign, Inc. - For authorized use only, CN=VeriSign Class 3 Public Primary Certification Authority - G5 +-----BEGIN CERTIFICATE----- +MIIGCjCCBPKgAwIBAgIQUgDlqiVW/BqG7ZbJ1EszxzANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMTAwMjA4MDAwMDAwWhcNMjAwMjA3MjM1OTU5WjCBtDEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2UgYXQg +aHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykxMDEuMCwGA1UEAxMlVmVy +aVNpZ24gQ2xhc3MgMyBDb2RlIFNpZ25pbmcgMjAxMCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAPUjS16l14q7MunUV/fv5Mcmfq0ZmP6onX2U9jZr +ENd1gTB/BGh/yyt1Hs0dCIzfaZSnN6Oce4DgmeHuN01fzjsU7obU0PUnNbwlCzin +jGOdF6MIpauw+81qYoJM1SHaG9nx44Q7iipPhVuQAU/Jp3YQfycDfL6ufn3B3fkF +vBtInGnnwKQ8PEEAPt+W5cXklHHWVQHHACZKQDy1oSapDKdtgI6QJXvPvz8c6y+W ++uWHd8a1VrJ6O1QwUxvfYjT/HtH0WpMoheVMF05+W/2kk5l/383vpHXv7xX2R+f4 +GXLYLjQaprSnTH69u08MPVfxMNamNo7WgHbXGS6lzX40LYkCAwEAAaOCAf4wggH6 +MBIGA1UdEwEB/wQIMAYBAf8CAQAwcAYDVR0gBGkwZzBlBgtghkgBhvhFAQcXAzBW +MCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy52ZXJpc2lnbi5jb20vY3BzMCoGCCsG +AQUFBwICMB4aHGh0dHBzOi8vd3d3LnZlcmlzaWduLmNvbS9ycGEwDgYDVR0PAQH/ +BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFswWTBXMFUWCWltYWdlL2dpZjAhMB8w +BwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgsexkuMCUWI2h0dHA6Ly9sb2dvLnZl +cmlzaWduLmNvbS92c2xvZ28uZ2lmMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9j +cmwudmVyaXNpZ24uY29tL3BjYTMtZzUuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr +BgEFBQcwAYYYaHR0cDovL29jc3AudmVyaXNpZ24uY29tMB0GA1UdJQQWMBQGCCsG +AQUFBwMCBggrBgEFBQcDAzAoBgNVHREEITAfpB0wGzEZMBcGA1UEAxMQVmVyaVNp +Z25NUEtJLTItODAdBgNVHQ4EFgQUz5mp6nsm9EvJjo/X8AUm7+PSp50wHwYDVR0j +BBgwFoAUf9Nlp8Ld7LvwMAnzQzn6Aq8zMTMwDQYJKoZIhvcNAQEFBQADggEBAFYi +5jSkxGHLSLkBrVaoZA/ZjJHEu8wM5a16oCJ/30c4Si1s0X9xGnzscKmx8E/kDwxT ++hVe/nSYSSSFgSYckRRHsExjjLuhNNTGRegNhSZzA9CpjGRt3HGS5kUFYBVZUTn8 +WBRr/tSk7XlrCAxBcuc3IgYJviPpP0SaHulhncyxkFz8PdKNrEI9ZTbUtD1AKI+b +EM8jJsxLIMuQH12MTDTKPNjlN9ZvpSC9NOsm2a4N58Wa96G0IZEzb4boWLslfHQO +WP51G2M/zjF8m48blp7FU3aEW5ytkfqs7ZO6XcghU8KCU2OvEg1QhxEbPVRSloos +nD2SGgiaBS7Hk6VIkdM= +-----END CERTIFICATE----- + diff --git a/src/main/resources/certs/readme.md b/src/main/resources/certs/readme.md new file mode 100644 index 00000000..1da9fafb --- /dev/null +++ b/src/main/resources/certs/readme.md @@ -0,0 +1,5 @@ +# Minecraft certificate chain + +Exported from the vanilla jar by extracting MOJANGCS.RSA from the jar and then running: + +`openssl pkcs7 -inform DER -in MOJANGCS.RSA -print_certs -out cert.pem` \ No newline at end of file diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy index cb3edfc3..42afc919 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy @@ -38,6 +38,7 @@ import net.fabricmc.loom.util.download.Download import net.fabricmc.loom.util.download.DownloadException import net.fabricmc.loom.util.download.DownloadExecutor import net.fabricmc.loom.util.download.DownloadProgressListener +import net.fabricmc.loom.util.download.DownloadResult class DownloadFileTest extends DownloadTest { @IgnoreIf({ os.windows }) // Requires admin on windows. @@ -115,16 +116,20 @@ class DownloadFileTest extends DownloadTest { } def output = new File(File.createTempDir(), "file.txt").toPath() + def results = [] as List when: for (i in 0..<2) { - Download.create("$PATH/sha1.txt") + def result = Download.create("$PATH/sha1.txt") .sha1("0a4d55a8d778e5022fab701977c5d840bbc486d0") .downloadPath(output) + results << result } then: requestCount == 1 + results[0].didDownload() + !results[1].didDownload() } def "Invalid Sha1"() { @@ -365,6 +370,28 @@ class DownloadFileTest extends DownloadTest { Files.readString(dir.resolve("4.txt")) == "Hello World" } + def "File: Async result"() { + setup: + server.get("/async1") { + it.result("Hello World") + } + + def dir = File.createTempDir().toPath() + + when: + boolean didDownload = false + new DownloadExecutor(2).withCloseable { + Download.create("$PATH/async1") + .downloadPathAsync(dir.resolve("1.txt"), it) + .thenAccept { + didDownload = it.didDownload() + } + } + + then: + didDownload + } + def "File: Async Error"() { setup: server.get("/async2") { diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/providers/CertificateChainTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/providers/CertificateChainTest.groovy new file mode 100644 index 00000000..d3796baf --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/providers/CertificateChainTest.groovy @@ -0,0 +1,110 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.providers + +import spock.lang.Specification + +import net.fabricmc.loom.configuration.providers.minecraft.verify.CertificateChain +import net.fabricmc.loom.configuration.providers.minecraft.verify.SignatureVerificationFailure +import net.fabricmc.loom.test.util.CertificateUtils + +class CertificateChainTest extends Specification { + def "load mojang's cert chain"() { + when: + def chain = CertificateChain.getRoot("mojangcs") + + then: + chain.certificate().issuerX500Principal.name == "OU=Class 3 Public Primary Certification Authority,O=VeriSign\\, Inc.,C=US" + } + + def "load certificate chain"() { + given: + def keyPair = CertificateUtils.generateKeyPair() + def root = CertificateUtils.createCert(keyPair, "CN=Test Root Certificate") + def intermediate = CertificateUtils.createCert(keyPair, "CN=Test Intermediate Certificate", root) + def leaf = CertificateUtils.createCert(keyPair, "CN=Test Leaf Certificate", intermediate) + + when: + def chain = CertificateChain.getRoot([root, intermediate, leaf]) + + then: + chain.issuer() == null + chain.certificate() == root + chain.children().size() == 1 + + chain.children()[0].issuer().certificate() == root + chain.children()[0].certificate() == intermediate + chain.children()[0].children().size() == 1 + } + + def "matching cert chain"() { + given: + def keyPair = CertificateUtils.generateKeyPair() + def root = CertificateUtils.createCert(keyPair, "CN=Test Root Certificate") + def intermediate = CertificateUtils.createCert(keyPair, "CN=Test Intermediate Certificate", root) + def leaf = CertificateUtils.createCert(keyPair, "CN=Test Leaf Certificate", intermediate) + + when: + def chain1 = CertificateChain.getRoot([root, intermediate, leaf]) + def chain2 = CertificateChain.getRoot([root, intermediate, leaf]) + + then: + chain1.verifyChainMatches(chain2) + } + + def "different leaf cert"() { + given: + def keyPair = CertificateUtils.generateKeyPair() + def root = CertificateUtils.createCert(keyPair, "CN=Test Root Certificate") + def intermediate = CertificateUtils.createCert(keyPair, "CN=Test Intermediate Certificate", root) + def leaf1 = CertificateUtils.createCert(keyPair, "CN=Test Leaf 1 Certificate", intermediate) + def leaf2 = CertificateUtils.createCert(keyPair, "CN=Test Leaf 2 Certificate", intermediate) + + when: + def chain1 = CertificateChain.getRoot([root, intermediate, leaf1]) + def chain2 = CertificateChain.getRoot([root, intermediate, leaf2]) + + chain1.verifyChainMatches(chain2) + + then: + thrown SignatureVerificationFailure + } + + def "different cert"() { + given: + def keyPair = CertificateUtils.generateKeyPair() + def root = CertificateUtils.createCert(keyPair, "CN=Test Root Certificate") + def root2 = CertificateUtils.createCert(keyPair, "CN=Test Root 2 Certificate") + + when: + def chain1 = CertificateChain.getRoot([root]) + def chain2 = CertificateChain.getRoot([root2]) + + chain1.verifyChainMatches(chain2) + + then: + thrown SignatureVerificationFailure + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/providers/CertificateRevocationListTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/providers/CertificateRevocationListTest.groovy new file mode 100644 index 00000000..52259044 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/providers/CertificateRevocationListTest.groovy @@ -0,0 +1,110 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.providers + +import spock.lang.Specification + +import net.fabricmc.loom.configuration.providers.minecraft.verify.CertificateChain +import net.fabricmc.loom.configuration.providers.minecraft.verify.CertificateRevocationList +import net.fabricmc.loom.configuration.providers.minecraft.verify.SignatureVerificationFailure +import net.fabricmc.loom.test.util.CertificateUtils +import net.fabricmc.loom.test.util.GradleTestUtil + +class CertificateRevocationListTest extends Specification { + // Test to make sure that the CRL URL is correct for the mojang cert chain + // As we don't want to depend on bouncycastle in the main project just to extract the same crl url each time + def "crl url matches"() { + given: + def cert = CertificateChain.getRoot("mojangcs") + when: + def crls = CertificateUtils.getCrls(cert) + then: + crls.sort() == CertificateRevocationList.CSC3_2010 + } + + def "valid cert"() { + given: + def keyPair = CertificateUtils.generateKeyPair() + def root = CertificateUtils.createCert(keyPair, "CN=Test Root Certificate") + def intermediate = CertificateUtils.createCert(keyPair, "CN=Test Intermediate Certificate", root) + def validLeaf = CertificateUtils.createCert(keyPair, "CN=Test Valid Leaf Certificate", intermediate) + def revokedLeaf = CertificateUtils.createCert(keyPair, "CN=Test Revoked Leaf Certificate", intermediate) + + def x509crl = CertificateUtils.createCrl(keyPair, intermediate, [revokedLeaf]) + + def chain = CertificateChain.getRoot([root, intermediate, validLeaf]) + + when: + def crl = new CertificateRevocationList([x509crl], false) + + then: + crl.verify(chain) + } + + def "revoked cert"() { + given: + def keyPair = CertificateUtils.generateKeyPair() + def root = CertificateUtils.createCert(keyPair, "CN=Test Root Certificate") + def intermediate = CertificateUtils.createCert(keyPair, "CN=Test Intermediate Certificate", root) + def revokedLeaf = CertificateUtils.createCert(keyPair, "CN=Test Revoked Leaf Certificate", intermediate) + + def x509crl = CertificateUtils.createCrl(keyPair, intermediate, [revokedLeaf]) + + def chain = CertificateChain.getRoot([ + root, + intermediate, + revokedLeaf + ]) + + when: + def crl = new CertificateRevocationList([x509crl], false) + crl.verify(chain) + + then: + thrown SignatureVerificationFailure + } + + def "Verify Mojang cert"() { + given: + def project = GradleTestUtil.mockProject() + def cert = CertificateChain.getRoot("mojangcs") + when: + def crl = CertificateRevocationList.create(project, CertificateRevocationList.CSC3_2010) + then: + !crl.downloadFailure() + crl.verify(cert) + } + + def "Invalid URL"() { + given: + def project = GradleTestUtil.mockProject() + def cert = CertificateChain.getRoot("mojangcs") + when: + def crl = CertificateRevocationList.create(project, ["http://invalid.url"]) + then: + crl.downloadFailure() + crl.verify(cert) + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/providers/JarVerifierTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/providers/JarVerifierTest.groovy new file mode 100644 index 00000000..fd869091 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/providers/JarVerifierTest.groovy @@ -0,0 +1,127 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.providers + +import java.nio.file.Files +import java.nio.file.Path + +import spock.lang.Specification + +import net.fabricmc.loom.configuration.providers.minecraft.verify.CertificateChain +import net.fabricmc.loom.configuration.providers.minecraft.verify.JarVerifier +import net.fabricmc.loom.configuration.providers.minecraft.verify.SignatureVerificationFailure +import net.fabricmc.loom.test.LoomTestConstants +import net.fabricmc.loom.util.ZipUtils +import net.fabricmc.loom.util.download.Download + +class JarVerifierTest extends Specification { + public static final String CLIENT_JAR_URL = "https://launcher.mojang.com/v1/objects/7e46fb47609401970e2818989fa584fd467cd036/client.jar" + public static final String INSTALLER_JAR_URL = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/1.0.3/fabric-installer-1.0.3.jar" + public static final File mcJarDir = new File(LoomTestConstants.TEST_DIR, "jar-verifier") + + def "verify Minecraft Jar"() { + setup: + def clientJar = downloadJarIfNotExists(CLIENT_JAR_URL, "client.jar") + def cert = CertificateChain.getRoot("mojangcs") + when: + JarVerifier.verify(clientJar, cert) + then: + true == true + } + + def "invalid Minecraft Jar, extra entry"() { + setup: + def clientJar = downloadJarIfNotExists(CLIENT_JAR_URL, "client.jar") + Path tempDir = Files.createTempDirectory("test") + def tempJar = tempDir.resolve("client.jar") + Files.copy(clientJar, tempJar) + + ZipUtils.add(tempJar, "extra.txt", "Hello World".bytes) + + def cert = CertificateChain.getRoot("mojangcs") + when: + JarVerifier.verify(tempJar, cert) + then: + def e = thrown SignatureVerificationFailure + e.message == "Jar entry extra.txt does not have a signature" + } + + def "invalid Minecraft Jar, modified entry"() { + setup: + def clientJar = downloadJarIfNotExists(CLIENT_JAR_URL, "client.jar") + Path tempDir = Files.createTempDirectory("test") + def tempJar = tempDir.resolve("client.jar") + Files.copy(clientJar, tempJar) + + ZipUtils.replace(tempJar, "version.json", "Hello World".bytes) + + def cert = CertificateChain.getRoot("mojangcs") + when: + JarVerifier.verify(tempJar, cert) + then: + def e = thrown SignatureVerificationFailure + e.message == "Jar entry version.json failed signature verification" + } + + def "invalid Minecraft Jar, not signed"() { + setup: + Path tempDir = Files.createTempDirectory("test") + def tempJar = tempDir.resolve("client.jar") + + ZipUtils.add(tempJar, "hello.txt", "Hello World".bytes) + + def cert = CertificateChain.getRoot("mojangcs") + when: + JarVerifier.verify(tempJar, cert) + then: + def e = thrown SignatureVerificationFailure + e.message == "Jar entry hello.txt does not have a signature" + } + + def "not minecraft"() { + setup: + def installerJar = downloadJarIfNotExists(INSTALLER_JAR_URL, "installer.jar") + + def cert = CertificateChain.getRoot("mojangcs") + when: + JarVerifier.verify(installerJar, cert) + then: + def e = thrown SignatureVerificationFailure + e.message == "Certificate mismatch: CN=Fabric,OU=CI,O=Fabric,L=Unknown,ST=Unknown,C=Unknown != OU=Class 3 Public Primary Certification Authority,O=VeriSign\\, Inc.,C=US" + } + + static Path downloadJarIfNotExists(String url, String name) { + File dst = new File(mcJarDir, name) + + if (!dst.exists()) { + dst.parentFile.mkdirs() + Download.create(url) + .defaultCache() + .downloadPath(dst.toPath()) + } + + return dst.toPath() + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/providers/KnownVersionsTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/providers/KnownVersionsTest.groovy new file mode 100644 index 00000000..8c71654e --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/providers/KnownVersionsTest.groovy @@ -0,0 +1,40 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.providers + +import spock.lang.Specification + +import net.fabricmc.loom.configuration.providers.minecraft.verify.KnownVersions + +class KnownVersionsTest extends Specification { + // Just a simple test to make sure we can load the known versions + def "check known versions"() { + when: + def versions = KnownVersions.INSTANCE.get() + then: + versions.client().get("1.2.5") == "c1c3740a912ef523a8bd46605ab5708643498330140cba175c7ce6f177e468e1" + versions.server().get("1.16.5") == "58f329c7d2696526f948470aa6fd0b45545039b64cb75015e64c12194b373da6" + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftJarVerificationTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftJarVerificationTest.groovy new file mode 100644 index 00000000..acab9c58 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftJarVerificationTest.groovy @@ -0,0 +1,157 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.providers + +import java.nio.file.Files +import java.nio.file.Path + +import spock.lang.Specification + +import net.fabricmc.loom.LoomGradlePlugin +import net.fabricmc.loom.configuration.providers.BundleMetadata +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta +import net.fabricmc.loom.configuration.providers.minecraft.VersionsManifest +import net.fabricmc.loom.configuration.providers.minecraft.verify.MinecraftJarVerification +import net.fabricmc.loom.configuration.providers.minecraft.verify.SignatureVerificationFailure +import net.fabricmc.loom.test.util.GradleTestUtil +import net.fabricmc.loom.util.Constants +import net.fabricmc.loom.util.ZipUtils +import net.fabricmc.loom.util.download.Download + +class MinecraftJarVerificationTest extends Specification { + static Path dir = Path.of(".gradle", "test-files", "jar-verification") + + def "check client verified"() { + setup: + def jar = getJar(version, "client") + def project = GradleTestUtil.mockProject() + def verification = project.objects.newInstance(MinecraftJarVerification.class, version) + + when: + verification.verifyClientJar(jar) + + then: + true == true + + where: + version | _ + "1.21.5" | _ + "1.16.5" | _ + "1.14.4" | _ + "1.7.10" | _ + "1.7.9" | _ // Sha1 signed + "b1.5" | _ // Not signed + } + + def "check bundled server verified"() { + setup: + def jar = getJar(version, "server") + def unpackedJar = jar.resolveSibling(jar.fileName.toString() + ".unpacked") + def project = GradleTestUtil.mockProject() + def verification = project.objects.newInstance(MinecraftJarVerification.class, version) + def bundle = BundleMetadata.fromJar(jar) + bundle.versions().get(0).unpackEntry(jar, unpackedJar, project) + + when: + verification.verifyServerJar(unpackedJar) + + then: + true == true + + where: + version | _ + "1.21.5" | _ + } + + def "check standalone server verified"() { + setup: + def jar = getJar(version, "server") + def project = GradleTestUtil.mockProject() + def verification = project.objects.newInstance(MinecraftJarVerification.class, version) + + when: + verification.verifyServerJar(jar) + + then: + true == true + + where: + version | _ + "1.16.5" | _ + "1.14.4" | _ + "1.7.10" | _ + "1.7.9" | _ // Sha1 signed + "1.2.5" | _ + } + + def "hash mismatch"() { + setup: + def jar = getJar("1.2.5", "client") + def project = GradleTestUtil.mockProject() + def verification = project.objects.newInstance(MinecraftJarVerification.class, "1.2.4") + + when: + verification.verifyClientJar(jar) + + then: + thrown SignatureVerificationFailure + } + + def "unverified jar"() { + setup: + Path tempDir = Files.createTempDirectory("test") + def jar = tempDir.resolve("client.jar") + ZipUtils.add(jar, "hello.txt", "Hello World".bytes) + + def project = GradleTestUtil.mockProject() + def verification = project.objects.newInstance(MinecraftJarVerification.class, "blah") + + when: + verification.verifyClientJar(jar) + + then: + thrown SignatureVerificationFailure + } + + private static Path getJar(String id, String type) { + def versionManifest = Download.create(Constants.VERSION_MANIFESTS) + .downloadString(dir.resolve("manifest.json")) + final VersionsManifest versions = LoomGradlePlugin.GSON.fromJson(versionManifest, VersionsManifest.class) + + def version = versions.getVersion(id) + def manifest = Download.create(version.url) + .sha1(version.sha1) + .downloadString(dir.resolve(version.id + ".json")) + def meta = LoomGradlePlugin.GSON.fromJson(manifest, MinecraftVersionMeta.class) + + def download = meta.download(type) + Path jarPath = dir.resolve(download.sha1() + ".jar") + Download.create(download.url()) + .sha1(download.sha1()) + .downloadPath(jarPath) + + return jarPath + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/util/CertificateUtils.groovy b/src/test/groovy/net/fabricmc/loom/test/util/CertificateUtils.groovy new file mode 100644 index 00000000..1c00e004 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/util/CertificateUtils.groovy @@ -0,0 +1,131 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.util + +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.cert.X509CRL +import java.security.cert.X509Certificate + +import org.bouncycastle.asn1.DERIA5String +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.CRLDistPoint +import org.bouncycastle.asn1.x509.CRLNumber +import org.bouncycastle.asn1.x509.CRLReason +import org.bouncycastle.asn1.x509.DistributionPointName +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNames +import org.bouncycastle.cert.jcajce.JcaX509CRLConverter +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils +import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder + +import net.fabricmc.loom.configuration.providers.minecraft.verify.CertificateChain + +/** + * Test code, not for production use. + */ +class CertificateUtils { + private static final JcaContentSignerBuilder SIGNER_BUILDER = new JcaContentSignerBuilder("SHA384withECDSA") + private static final JcaX509CertificateConverter CERT_CONVERTER = new JcaX509CertificateConverter() + private static final JcaX509CRLConverter CRL_CONVERTER = new JcaX509CRLConverter() + + static KeyPair generateKeyPair() { + def keyPairGenerator = KeyPairGenerator.getInstance("EC") + keyPairGenerator.initialize(384) + return keyPairGenerator.generateKeyPair() + } + + static X509Certificate createCert(KeyPair keyPair, String name, X509Certificate parent = null) { + def issuerName = new X500Name(parent ? parent.subjectX500Principal.name : name) + def subjectName = new X500Name(name) + def notBefore = new Date() + def notAfter = new Date(notBefore.getTime() + 365 * 24 * 60 * 60 * 1000) // 1 year + + def serialNumber = BigInteger.valueOf(System.currentTimeMillis()) + def builder = new JcaX509v3CertificateBuilder( + issuerName, + serialNumber, + notBefore, + notAfter, + subjectName, + keyPair.getPublic() + ) + + def contentSigner = SIGNER_BUILDER.build(keyPair.getPrivate()) + return CERT_CONVERTER.getCertificate(builder.build(contentSigner)) + } + + static X509CRL createCrl(KeyPair keyPair, X509Certificate issuerCert, List revokedCerts) { + def builder = new JcaX509v2CRLBuilder(issuerCert, new Date()) + + for (final def revoked in revokedCerts) { + assert revoked.getIssuerX500Principal() == issuerCert.getSubjectX500Principal() + builder.addCRLEntry(revoked.getSerialNumber(), new Date(), CRLReason.keyCompromise) + } + + builder.addExtension(Extension.authorityKeyIdentifier, false, + new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic())) + builder.addExtension(Extension.cRLNumber, false, new CRLNumber(BigInteger.ONE)) + + def crlSigner = SIGNER_BUILDER.build(keyPair.getPrivate()) + return CRL_CONVERTER.getCRL(builder.build(crlSigner)) + } + + static List getCrls(CertificateChain certificateChain) { + def crls = [] as Set + getCrls(certificateChain, crls) + return crls.toList() + } + + static void getCrls(CertificateChain certificateChain, Set crls) { + crls.addAll(getCrls(certificateChain.certificate())) + + certificateChain.children().each { child -> + getCrls(child, crls) + } + } + + static ArrayList getCrls(X509Certificate certificate) { + byte[] crlDistributionPointsValue = certificate.getExtensionValue(Extension.cRLDistributionPoints.getId()) + + if (crlDistributionPointsValue == null) { + return [] + } + + return CRLDistPoint + .getInstance(JcaX509ExtensionUtils.parseExtensionValue(crlDistributionPointsValue)) + .getDistributionPoints() + .findAll { it.getDistributionPoint().type == DistributionPointName.FULL_NAME } + .collectMany { distPoint -> + GeneralNames.getInstance(distPoint.getDistributionPoint().getName()).getNames() + .findAll { it.tagNo == GeneralName.uniformResourceIdentifier } + .collect { DERIA5String.getInstance(it.name).getString() } + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy b/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy index 2700c2cd..264d3cd8 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/GradleTestUtil.groovy @@ -33,6 +33,7 @@ import org.gradle.api.internal.tasks.DefaultSourceSet import org.gradle.api.model.ObjectFactory import org.gradle.api.plugins.ExtensionContainer import org.gradle.api.provider.Property +import org.gradle.api.provider.ProviderFactory import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.util.PatternFilterable import org.jetbrains.annotations.Nullable @@ -63,9 +64,10 @@ class GradleTestUtil { } static Project mockProject() { - def objectFactory = TestServiceFactory.objectFactory - def providerFactory = TestServiceFactory.providerFactory def mock = mock(Project.class) + def serviceRegistry = TestServiceFactory.createServiceRegistry(mock) + def objectFactory = serviceRegistry.get(ObjectFactory) + def providerFactory = serviceRegistry.get(ProviderFactory) def extensions = mockExtensionContainer() when(mock.getExtensions()).thenReturn(extensions) when(mock.getObjects()).thenReturn(objectFactory) diff --git a/src/test/groovy/net/fabricmc/loom/test/util/KnownVersionsGenerator.groovy b/src/test/groovy/net/fabricmc/loom/test/util/KnownVersionsGenerator.groovy new file mode 100644 index 00000000..33cff626 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/util/KnownVersionsGenerator.groovy @@ -0,0 +1,143 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.util + +import java.nio.file.Path +import java.security.MessageDigest + +import net.fabricmc.loom.LoomGradlePlugin +import net.fabricmc.loom.configuration.providers.BundleMetadata +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta +import net.fabricmc.loom.configuration.providers.minecraft.VersionsManifest +import net.fabricmc.loom.configuration.providers.minecraft.verify.CertificateChain +import net.fabricmc.loom.configuration.providers.minecraft.verify.JarVerifier +import net.fabricmc.loom.configuration.providers.minecraft.verify.KnownVersions +import net.fabricmc.loom.configuration.providers.minecraft.verify.SignatureVerificationFailure +import net.fabricmc.loom.util.Constants +import net.fabricmc.loom.util.download.Download +import net.fabricmc.loom.util.download.DownloadExecutor + +/** + * A quick and dirty script to generate the known_versions.json file + * This file contains a list of all the unsigned versions of Minecraft and their sha1 hashes + * Note: Running this will take a while as it downloads all the versions of Minecraft + */ +class KnownVersionsGenerator { + static Path dir = Path.of(".gradle", "test-files", "unsigned") + static CertificateChain chain = CertificateChain.getRoot("mojangcs") + + static void main(String[] args) { + def versionManifest = Download.create(Constants.VERSION_MANIFESTS) + .downloadString() + final VersionsManifest manifest = LoomGradlePlugin.GSON.fromJson(versionManifest, VersionsManifest.class) + + // Download all the minecraft jars + new DownloadExecutor(10).withCloseable { + for (def version in manifest.versions()) { + downloadVersion(version, it) + } + } + + println("Downloaded all versions") + + def unsignedClientVersions = [:] as Map + def unsignedServerVersions = [:] as Map + + for (def version in manifest.versions()) { + println("Checking version " + version.id) + checkVersion(version, unsignedClientVersions, unsignedServerVersions) + } + + def json = LoomGradlePlugin.GSON.toJson(new KnownVersions(unsignedClientVersions, unsignedServerVersions)) + println(json) + } + + static void downloadVersion(VersionsManifest.Version version, DownloadExecutor downloadExecutor) { + def manifest = Download.create(version.url) + .sha1(version.sha1) + .downloadString(dir.resolve(version.id + ".json")) + def meta = LoomGradlePlugin.GSON.fromJson(manifest, MinecraftVersionMeta.class) + + def client = meta.download("client") + def server = meta.download("server") + + download(client, downloadExecutor) + + if (server != null) { + download(server, downloadExecutor) + } + } + + static void download(MinecraftVersionMeta.Download download, DownloadExecutor executor) { + Path jarPath = dir.resolve(download.sha1() + ".jar") + Download.create(download.url()) + .sha1(download.sha1()) + .downloadPathAsync(jarPath, executor) + } + + static void checkVersion(VersionsManifest.Version version, Map unsignedClientVersions, Map unsignedServerVersions) { + def manifest = Download.create(version.url) + .sha1(version.sha1) + .downloadString(dir.resolve(version.id + ".json")) + def meta = LoomGradlePlugin.GSON.fromJson(manifest, MinecraftVersionMeta.class) + + def client = meta.download("client") + def server = meta.download("server") + + def clientJar = dir.resolve(client.sha1() + ".jar") + + if (!isSigned(clientJar)) { + unsignedClientVersions.put(version.id, sha256(clientJar)) + } + + if (server != null) { + def serverJar = dir.resolve(server.sha1() + ".jar") + if (BundleMetadata.fromJar(serverJar) == null) { + unsignedServerVersions.put(version.id, sha256(serverJar)) + } + } + } + + static boolean isSigned(Path jarPath) { + try { + JarVerifier.verify(jarPath, chain) + return true + } catch (SignatureVerificationFailure ignored) { + return false + } + } + + static String sha256(Path path) { + MessageDigest md = MessageDigest.getInstance("SHA-256") + path.withInputStream { inputStream -> + byte[] buffer = new byte[8192] + int bytesRead + while ((bytesRead = inputStream.read(buffer)) != -1) { + md.update(buffer, 0, bytesRead) + } + } + return md.digest().encodeHex().toString() + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/util/TestServiceFactory.groovy b/src/test/groovy/net/fabricmc/loom/test/util/TestServiceFactory.groovy index 31eb7cdc..52518feb 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/TestServiceFactory.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/TestServiceFactory.groovy @@ -25,6 +25,7 @@ package net.fabricmc.loom.test.util import groovy.transform.CompileStatic +import org.gradle.api.Project import org.gradle.api.internal.CollectionCallbackActionDecorator import org.gradle.api.internal.MutationGuard import org.gradle.api.internal.MutationGuards @@ -71,7 +72,7 @@ class TestServiceFactory { public static final ObjectFactory objectFactory = serviceRegistry.get(ObjectFactory) public static final ProviderFactory providerFactory = serviceRegistry.get(ProviderFactory) - private static ServiceRegistry createServiceRegistry() { + static ServiceRegistry createServiceRegistry(Project project = null) { def services = new DefaultServiceRegistry() services.register { it.add(DefaultPropertyFactory) @@ -83,6 +84,11 @@ class TestServiceFactory { it.add(FileCollectionFactory, fileCollectionFactory()) it.add(DefaultDomainObjectCollectionFactory) it.add(CrossBuildInMemoryCacheFactory, new DefaultCrossBuildInMemoryCacheFactory(mock(ListenerManager))) + + if (project != null) { + it.add(Project, project) + } + //noinspection unused it.addProvider(new ServiceRegistrationProvider() { @Provides