From 4bd8d421be724c8e6858487f4bd2ebaadc5a005a Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Fri, 5 May 2023 09:53:21 +0200 Subject: [PATCH] Add bisq2 gradle-tasks module --- build-logic/gradle-tasks/build.gradle | 12 +++ .../src/main/kotlin/bisq/gradle/tasks/OS.kt | 32 +++++++ .../bisq/gradle/tasks/PerOsUrlProvider.kt | 22 +++++ .../bisq/gradle/tasks/PgpFingerprint.kt | 7 ++ .../gradle/tasks/download/DownloadTask.kt | 40 ++++++++ .../tasks/download/DownloadTaskFactory.kt | 25 +++++ .../tasks/download/SignedBinaryDownloader.kt | 44 +++++++++ .../tasks/signature/PpgPublicKeyParser.kt | 95 +++++++++++++++++++ .../signature/SignatureVerificationTask.kt | 46 +++++++++ .../tasks/signature/SignatureVerifier.kt | 78 +++++++++++++++ build-logic/settings.gradle | 1 + 11 files changed, 402 insertions(+) create mode 100644 build-logic/gradle-tasks/build.gradle create mode 100644 build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/OS.kt create mode 100644 build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/PerOsUrlProvider.kt create mode 100644 build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/PgpFingerprint.kt create mode 100644 build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/DownloadTask.kt create mode 100644 build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/DownloadTaskFactory.kt create mode 100644 build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/SignedBinaryDownloader.kt create mode 100644 build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/PpgPublicKeyParser.kt create mode 100644 build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/SignatureVerificationTask.kt create mode 100644 build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/SignatureVerifier.kt diff --git a/build-logic/gradle-tasks/build.gradle b/build-logic/gradle-tasks/build.gradle new file mode 100644 index 0000000000..7c84d60149 --- /dev/null +++ b/build-logic/gradle-tasks/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.7.10' + id 'org.gradle.kotlin.kotlin-dsl' version '2.4.1' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation libs.bouncycastle.bcpg.jdk15on +} diff --git a/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/OS.kt b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/OS.kt new file mode 100644 index 0000000000..aa8fdc8e76 --- /dev/null +++ b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/OS.kt @@ -0,0 +1,32 @@ +package bisq.gradle.tasks + +import java.util.* + +enum class OS { + LINUX, MAC_OS, WINDOWS +} + +fun getOS(): OS { + val osName = System.getProperty("os.name").toLowerCase(Locale.US) + if (isLinux(osName)) { + return OS.LINUX + } else if (isMacOs(osName)) { + return OS.MAC_OS + } else if (isWindows(osName)) { + return OS.WINDOWS + } + + throw IllegalStateException("Running on unsupported OS: $osName") +} + +private fun isLinux(osName: String): Boolean { + return osName.contains("linux") +} + +private fun isMacOs(osName: String): Boolean { + return osName.contains("mac") || osName.contains("darwin") +} + +private fun isWindows(osName: String): Boolean { + return osName.contains("win") +} \ No newline at end of file diff --git a/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/PerOsUrlProvider.kt b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/PerOsUrlProvider.kt new file mode 100644 index 0000000000..dd8a068cd6 --- /dev/null +++ b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/PerOsUrlProvider.kt @@ -0,0 +1,22 @@ +package bisq.gradle.tasks + +import java.net.URL + +interface PerOsUrlProvider { + val urlPrefix: String + + val linuxUrl: String + val macOsUrl: String + val windowsUrl: String + + val url: URL + get() = URL(urlPrefix + getUrlSuffix()) + + private fun getUrlSuffix() = + when (getOS()) { + OS.LINUX -> linuxUrl + OS.MAC_OS -> macOsUrl + OS.WINDOWS -> windowsUrl + } + +} \ No newline at end of file diff --git a/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/PgpFingerprint.kt b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/PgpFingerprint.kt new file mode 100644 index 0000000000..4728beb7d4 --- /dev/null +++ b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/PgpFingerprint.kt @@ -0,0 +1,7 @@ +package bisq.gradle.tasks + +object PgpFingerprint { + fun normalize(fingerprint: String): String = + fingerprint.filterNot { it.isWhitespace() } // Remove all spaces + .toLowerCase() +} \ No newline at end of file diff --git a/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/DownloadTask.kt b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/DownloadTask.kt new file mode 100644 index 0000000000..d9f28ddf79 --- /dev/null +++ b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/DownloadTask.kt @@ -0,0 +1,40 @@ +package bisq.gradle.tasks.download + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.FileOutputStream +import java.net.URL +import java.nio.channels.Channels + +abstract class DownloadTask : DefaultTask() { + + @get:Input + abstract val downloadUrl: Property + + @get:OutputFile + abstract val outputFile: Property> + + @TaskAction + fun download() { + downloadFile() + } + + private fun downloadFile() { + val url = downloadUrl.get() + url.openStream().use { inputStream -> + Channels.newChannel(inputStream).use { readableByteChannel -> + println("Downloading: $url") + + FileOutputStream(outputFile.get().get().asFile).use { fileOutputStream -> + fileOutputStream.channel + .transferFrom(readableByteChannel, 0, Long.MAX_VALUE) + } + } + } + } +} \ No newline at end of file diff --git a/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/DownloadTaskFactory.kt b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/DownloadTaskFactory.kt new file mode 100644 index 0000000000..0319721cb9 --- /dev/null +++ b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/DownloadTaskFactory.kt @@ -0,0 +1,25 @@ +package bisq.gradle.tasks.download + +import org.gradle.api.Project +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.register +import java.net.URL + +class DownloadTaskFactory( + private val project: Project, private val downloadDirectoryPath: String +) { + fun registerDownloadTask(taskName: String, url: Provider): TaskProvider { + val outputFileProvider: Provider> = url.map { + // url.file: + // https://example.org/1.2.3/binary.exe -> 1.2.3/binary.exe + val fileName = it.file.split("/").last() + project.layout.buildDirectory.file("$downloadDirectoryPath/$fileName") + } + return project.tasks.register(taskName) { + downloadUrl.set(url) + outputFile.set(outputFileProvider) + } + } +} \ No newline at end of file diff --git a/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/SignedBinaryDownloader.kt b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/SignedBinaryDownloader.kt new file mode 100644 index 0000000000..c4aa50b16b --- /dev/null +++ b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/download/SignedBinaryDownloader.kt @@ -0,0 +1,44 @@ +package bisq.gradle.tasks.download + +import bisq.gradle.tasks.PerOsUrlProvider +import bisq.gradle.tasks.signature.SignatureVerificationTask +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.register +import java.net.URL + +class SignedBinaryDownloader( + private val project: Project, + private val binaryName: String, + private val version: Property, + + private val perOsUrlProvider: (String) -> PerOsUrlProvider, + private val downloadDirectory: String, + + private val pgpFingerprintToKeyUrlMap: Map +) { + lateinit var verifySignatureTask: TaskProvider + private val downloadTaskFactory = DownloadTaskFactory(project, downloadDirectory) + + fun registerTasks() { + val binaryDownloadUrl: Provider = version.map { perOsUrlProvider(it).url } + val binaryDownloadTask = + downloadTaskFactory.registerDownloadTask("download${binaryName}Binary", binaryDownloadUrl) + + val signatureDownloadUrl: Provider = binaryDownloadUrl.map { URL("$it.asc") } + val signatureDownloadTask = + downloadTaskFactory.registerDownloadTask("download${binaryName}Signature", signatureDownloadUrl) + + verifySignatureTask = project.tasks.register("verify${binaryName}Binary") { + dependsOn(binaryDownloadTask) + + fileToVerify.set(binaryDownloadTask.flatMap { it.outputFile }) + detachedSignatureFile.set(signatureDownloadTask.flatMap { it.outputFile }) + pgpFingerprintToKeyUrl.set(pgpFingerprintToKeyUrlMap) + + resultFile.set(project.layout.buildDirectory.file("${downloadDirectory}/sha256.result")) + } + } +} \ No newline at end of file diff --git a/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/PpgPublicKeyParser.kt b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/PpgPublicKeyParser.kt new file mode 100644 index 0000000000..508adedc01 --- /dev/null +++ b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/PpgPublicKeyParser.kt @@ -0,0 +1,95 @@ +package bisq.gradle.tasks.signature + +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.openpgp.jcajce.JcaPGPPublicKeyRingCollection +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider +import org.bouncycastle.util.encoders.Hex +import org.gradle.api.GradleException +import java.io.ByteArrayInputStream +import java.net.URL + +class PpgPublicKeyParser( + private val primaryKeyFingerprint: String, + private val keyUrl: URL +) { + + private var masterKey: PGPPublicKey? = null + private val subKeys: MutableList = ArrayList() + + val keyById: Map + get() { + val keyByIdMap = HashMap() + keyByIdMap[masterKey!!.keyID] = masterKey!! + subKeys.forEach { key -> keyByIdMap[key.keyID] = key } + return keyByIdMap + } + + fun parse() { + val publicKey: ByteArray = keyUrl.readBytes() + val byteArrayInputStream = ByteArrayInputStream(publicKey) + PGPUtil.getDecoderStream(byteArrayInputStream) + .use { decoderInputStream -> + val publicKeyRingCollection = JcaPGPPublicKeyRingCollection(decoderInputStream) + parseMasterAndSubKeys(publicKeyRingCollection) + + checkNotNull(masterKey) { "Couldn't find master key." } + verifyMasterKeyFingerprint() + + if (subKeys.isNotEmpty()) { + verifySubKeySignatures() + } + } + } + + private fun parseMasterAndSubKeys(publicKeyRingCollection: JcaPGPPublicKeyRingCollection) { + val rIt: Iterator = publicKeyRingCollection.keyRings + while (rIt.hasNext()) { + val kRing = rIt.next() + val kIt = kRing.publicKeys + while (kIt.hasNext()) { + val k = kIt.next() + + if (k.isMasterKey) { + if (masterKey == null) { + masterKey = k + } else { + throw GradleException("Found multiple find master keys.") + } + + } else { + subKeys.add(k) + } + } + } + } + + private fun verifyMasterKeyFingerprint() { + val fingerprint = Hex.toHexString(masterKey!!.fingerprint) + if (fingerprint != primaryKeyFingerprint) { + throw GradleException("$keyUrl has invalid fingerprint.") + } + } + + private fun verifySubKeySignatures() { + subKeys.forEach { subKey -> + var hasValidSignature = false + subKey.keySignatures.forEach { signature -> + signature.init( + JcaPGPContentVerifierBuilderProvider().setProvider("BC"), + masterKey!! + ) + val isSubKeySignedByMasterKey = signature.verifyCertification(masterKey!!, subKey) + + if (isSubKeySignedByMasterKey) { + hasValidSignature = true + } + } + + if (!hasValidSignature) { + throw GradleException("Subkey `$subKey` is not signed by masterkey `$masterKey`") + } + } + } +} \ No newline at end of file diff --git a/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/SignatureVerificationTask.kt b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/SignatureVerificationTask.kt new file mode 100644 index 0000000000..6dd1732712 --- /dev/null +++ b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/SignatureVerificationTask.kt @@ -0,0 +1,46 @@ +package bisq.gradle.tasks.signature + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.net.URL + +abstract class SignatureVerificationTask : DefaultTask() { + + @get:InputFile + abstract val fileToVerify: Property> + + @get:InputFile + abstract val detachedSignatureFile: Property> + + @get:Input + abstract val pgpFingerprintToKeyUrl: MapProperty + + @get:OutputFile + abstract val resultFile: RegularFileProperty + + @TaskAction + fun verify() { + val signatureVerifier = SignatureVerifier(pgpFingerprintToKeyUrl.get()) + val isSignatureValid = signatureVerifier.verifySignature( + signatureFile = detachedSignatureFile.get().get().asFile, + fileToVerify = fileToVerify.get().get().asFile + ) + + resultFile.get().asFile.writeText("$isSignatureValid") + + if (!isSignatureValid) { + throw GradleException( + "Signature verification failed for ${fileToVerify.get().get().asFile.absolutePath}." + ) + } + } +} \ No newline at end of file diff --git a/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/SignatureVerifier.kt b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/SignatureVerifier.kt new file mode 100644 index 0000000000..6c0a31a59a --- /dev/null +++ b/build-logic/gradle-tasks/src/main/kotlin/bisq/gradle/tasks/signature/SignatureVerifier.kt @@ -0,0 +1,78 @@ +package bisq.gradle.tasks.signature + +import org.bouncycastle.bcpg.ArmoredInputStream +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider +import java.io.ByteArrayInputStream +import java.io.File +import java.net.URL +import java.security.Security + +class SignatureVerifier( + private val pgpFingerprintToKeyUrl: Map +) { + + fun verifySignature( + fileToVerify: File, + signatureFile: File, + ): Boolean { + Security.addProvider(BouncyCastleProvider()) + + var isSuccess = true + pgpFingerprintToKeyUrl.forEach { (fingerprint, keyUrl) -> + val ppgPublicKeyParser = PpgPublicKeyParser(fingerprint, keyUrl) + ppgPublicKeyParser.parse() + + val isSignedByAnyKey = verifyDetachedSignature( + potentialSigningKeys = ppgPublicKeyParser.keyById, + pgpSignatureByteArray = readSignatureFromFile(signatureFile), + data = fileToVerify.readBytes() + ) + + isSuccess = isSuccess && isSignedByAnyKey + } + + return isSuccess + } + + private fun readSignatureFromFile(signatureFile: File): ByteArray { + val signatureByteArray = signatureFile.readBytes() + val signatureInputStream = ByteArrayInputStream(signatureByteArray) + val armoredSignatureInputStream = ArmoredInputStream(signatureInputStream) + return armoredSignatureInputStream.readBytes() + } + + private fun verifyDetachedSignature( + potentialSigningKeys: Map, + pgpSignatureByteArray: ByteArray, + data: ByteArray + ): Boolean { + val pgpObjectFactory = JcaPGPObjectFactory(pgpSignatureByteArray) + val signatureList: PGPSignatureList = pgpObjectFactory.nextObject() as PGPSignatureList + + var pgpSignature: PGPSignature? = null + var signingKey: PGPPublicKey? = null + + for (s in signatureList) { + signingKey = potentialSigningKeys[s.keyID] + if (signingKey != null) { + pgpSignature = s + break + } + } + + checkNotNull(signingKey) { "signingKey not found" } + checkNotNull(pgpSignature) { "signature for key not found" } + + pgpSignature.init( + JcaPGPContentVerifierBuilderProvider().setProvider("BC"), + signingKey + ) + pgpSignature.update(data) + return pgpSignature.verify() + } +} \ No newline at end of file diff --git a/build-logic/settings.gradle b/build-logic/settings.gradle index f5ee55df57..f8f63b2425 100644 --- a/build-logic/settings.gradle +++ b/build-logic/settings.gradle @@ -7,3 +7,4 @@ dependencyResolutionManagement { } include 'commons' +include 'gradle-tasks'