diff --git a/.gitignore b/.gitignore index 134e956..dc513fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -idea/ +.idea/ temp/ oie/ engine/ \ No newline at end of file diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 0000000..89779a9 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,3 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +groovy=4.0.27 diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1513f8 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Open Integration Engine release verification toolkit + +This repo houses the scripts to verify Open integration Engine release validity. + +The script downloads a packaged release tarball, and clones the [`engine`](https://github.com/OpenIntegrationEngine/engine) repository +at the specified commit. + +Open Integration Engine is then built locally and a verification script then verifies all jar files either by comparing the full file SHA-256 hash +or by comparing the hashes of each file in a `.jar`. + + + +## Requirements +1. [`sdkman`](https://sdkman.io/) +2. `bash` +3. Some Java already installed + +## Usage +Run `./verify.sh ` + +`./verify.sh "788a150f36a6bcd1db672e00d2e7ee609e2842d9" "https://github.com/OpenIntegrationEngine/engine/releases/download/v4.5.2/oie_unix_4_5_2.tar.gz"` \ No newline at end of file diff --git a/backend.groovy b/backend.groovy new file mode 100644 index 0000000..f74aba4 --- /dev/null +++ b/backend.groovy @@ -0,0 +1,190 @@ +import groovy.io.FileType + +import java.security.MessageDigest +import java.time.Duration +import java.time.Instant +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.zip.ZipEntry + +enum ValidationStrategy { + CLASS_DIGEST, + JAR_HASH, + COMPILATION, + SKIP, +} + +final DIGEST_PREFIXES = ['oie/client-lib','oie/extensions'] + +def ensureFileIsDirectory(File dir) { + if (!dir.exists() || !dir.isDirectory()) { + println "Error: “${args[0]}” is not a directory" + System.exit(1) + } +} + +static def tarPathToRepoPath(String path) { + return path + .replaceFirst('oie/', 'engine/') + .replaceFirst('server-lib/donkey', 'donkey/lib') + .replaceFirst('extensions', 'server/lib/extensions') + .replaceFirst('server-lib', 'server/lib') + .replaceFirst('client-lib', 'client/lib') + .replaceFirst('cli-lib', 'command/lib') + .replaceFirst('manager-lib', 'manager/lib') +} + +static def calculateFileHash(String path) { + def md = MessageDigest.getInstance("SHA-256") + + new File(path).withInputStream { is -> + byte[] buffer = new byte[8192] + int read + while((read = is.read(buffer)) != -1) { + md.update(buffer, 0, read) + } + } + return md.digest() + .collect { byte b -> String.format('%02x', b & 0xff) } + .join() +} + +def handleJarHashStrategy(String tarPath, String repoPath) { + def tarJarHash = calculateFileHash(tarPath) + def repoJarHash = calculateFileHash(repoPath) + + if (tarJarHash == repoJarHash) { + println("Hash verified for $tarPath - $tarJarHash") + } else { + println("""Hash verification failed for $tarPath +Expected: $tarJarHash +Calculated: $repoJarHash +""") + throw new RuntimeException("Hash verification failed for $tarPath") + } +} + +static def getJarClassDigests(String tarPath) { + def result = new HashSet>() + + new JarFile(tarPath).withCloseable { jar -> + jar.entries().findAll { e -> + !e.isDirectory() && e.name.endsWith('.class') + }.each { entry -> + def md = MessageDigest.getInstance('SHA-256') + jar.getInputStream(entry as ZipEntry).withCloseable { is -> + byte[] buffer = new byte[8192] + int read + while((read = is.read(buffer)) != -1) { + md.update(buffer, 0, read) + } + } + // convert bytes to hex + def hex = md.digest() + .collect { byte b -> String.format('%02x', b & 0xff) } + .join() + + def basename = (entry as JarEntry).name + result.add(new AbstractMap.SimpleImmutableEntry(basename, hex)) + } + } + return result +} + +def handleCompilationStrategy(String tarPath) { + def repoPath = tarPath.replace("oie/", "engine/server/setup/") + def tarJar = new JarFile(tarPath) + + def hasClasses = tarJar.entries().iterator().any { + !it.name.startsWith("META-INF") + } + + if (!hasClasses) { + // I don't know what to do here... skip? + println("$tarPath has no classes in it") + return + } + + def tarDigests = getJarClassDigests(tarPath) + def repoDigests = getJarClassDigests(repoPath) + + if (tarDigests == repoDigests) { + println("Class digests are equal for $tarPath") + } else { + throw new RuntimeException("Class digest verification failed for $tarPath") + } +} + +def clonedRepoDir = new File("engine") +def tarballDir = new File("oie") +def tempDir = new File("temp") + +ensureFileIsDirectory(clonedRepoDir) +ensureFileIsDirectory(tarballDir) +ensureFileIsDirectory(tempDir) + +def startInstant = Instant.now() + +def jarInTarToStrategyMap = new HashMap() + +// Get all Jar files from the cloned git repository +tarballDir.traverse( + type: FileType.FILES, + nameFilter: ~/.*\.jar/ +) { file -> + def strategy + + // Ignore Install4j files + if (file.path.contains("install4j")) { + strategy = ValidationStrategy.SKIP + + // Naive assumption that jars with no numbers in the name are compiled locally + } else if (file.name ==~ /.+[^0-9]\.jar/) { + strategy = ValidationStrategy.COMPILATION + + // client-lib and extensions jars are signed + } else if (DIGEST_PREFIXES.any { file.path.startsWith(it) }) { + strategy = ValidationStrategy.CLASS_DIGEST + + // Hash checks for everything else + } else { + strategy = ValidationStrategy.JAR_HASH + } + + jarInTarToStrategyMap[file as String] = strategy +} + +def initialFilesSet = jarInTarToStrategyMap.keySet() +def processedFilesSet = new HashSet() + +jarInTarToStrategyMap.each { + file = it.key + strategy = it.value + + repoPath = tarPathToRepoPath(file) + println("-- Checking file $file with strategy $strategy") + + if (strategy == ValidationStrategy.SKIP) { + return + } else if (strategy == ValidationStrategy.JAR_HASH) { + handleJarHashStrategy(file, repoPath) + processedFilesSet.add(file) + } else if (strategy == ValidationStrategy.CLASS_DIGEST || strategy == ValidationStrategy.COMPILATION) { + handleCompilationStrategy(file) + processedFilesSet.add(file) + } + + return +} + +def duration = Duration.between(startInstant, Instant.now()) + +println() +println("Initial files: ${initialFilesSet.size()}, processed files ${processedFilesSet.size()}") + +println("Unprocessed files: ${initialFilesSet - processedFilesSet}") +println("If the unprocessed files list contains install4j jars only then you're most likely good already") +println() + + +println("Time elapsed: ${duration.toSeconds()} seconds") diff --git a/verify.sh b/verify.sh new file mode 100755 index 0000000..0448458 --- /dev/null +++ b/verify.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +export SDKMAN_DIR="${HOME}/.sdkman" +[[ -s "${SDKMAN_DIR}/bin/sdkman-init.sh" ]] && source "${SDKMAN_DIR}/bin/sdkman-init.sh" + +############################################ +# +# Configure the release and commit hash here +# +############################################ + +# example "788a150f36a6bcd1db672e00d2e7ee609e2842d9" +COMMIT_HASH=$1 + +# example "https://github.com/OpenIntegrationEngine/engine/releases/download/v4.5.2/oie_unix_4_5_2.tar.gz +DOWNLOAD_URL=$2 + +# cleanup +rm -rf engine oie temp +mkdir oie engine temp + +curl -L -o temp/oie.tar.gz "$DOWNLOAD_URL" +tar xzf temp/oie.tar.gz -C . + +# Clone engine repository at +git clone --depth=1 --revision="$COMMIT_HASH" git@github.com:OpenIntegrationEngine/engine.git engine + +# Get correct groovy +sdk env install + +pushd engine + +# Get correct java and ant +sdk env install + +pushd server +# Build without signing - is faster +ant clean +ant -f mirth-build.xml -DdisableSigning=true + +# Drop out of dir stack +popd +popd + +groovy backend.groovy