Add bisq2 gradle-tasks module

This commit is contained in:
Alva Swanson 2023-05-05 09:53:21 +02:00
parent 2e892bf90a
commit 4bd8d421be
No known key found for this signature in database
GPG key ID: 004760E77F753090
11 changed files with 402 additions and 0 deletions

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
}
}

View file

@ -0,0 +1,7 @@
package bisq.gradle.tasks
object PgpFingerprint {
fun normalize(fingerprint: String): String =
fingerprint.filterNot { it.isWhitespace() } // Remove all spaces
.toLowerCase()
}

View file

@ -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<URL>
@get:OutputFile
abstract val outputFile: Property<Provider<RegularFile>>
@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)
}
}
}
}
}

View file

@ -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<URL>): TaskProvider<DownloadTask> {
val outputFileProvider: Provider<Provider<RegularFile>> = 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<DownloadTask>(taskName) {
downloadUrl.set(url)
outputFile.set(outputFileProvider)
}
}
}

View file

@ -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<String>,
private val perOsUrlProvider: (String) -> PerOsUrlProvider,
private val downloadDirectory: String,
private val pgpFingerprintToKeyUrlMap: Map<String, URL>
) {
lateinit var verifySignatureTask: TaskProvider<SignatureVerificationTask>
private val downloadTaskFactory = DownloadTaskFactory(project, downloadDirectory)
fun registerTasks() {
val binaryDownloadUrl: Provider<URL> = version.map { perOsUrlProvider(it).url }
val binaryDownloadTask =
downloadTaskFactory.registerDownloadTask("download${binaryName}Binary", binaryDownloadUrl)
val signatureDownloadUrl: Provider<URL> = binaryDownloadUrl.map { URL("$it.asc") }
val signatureDownloadTask =
downloadTaskFactory.registerDownloadTask("download${binaryName}Signature", signatureDownloadUrl)
verifySignatureTask = project.tasks.register<SignatureVerificationTask>("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"))
}
}
}

View file

@ -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<PGPPublicKey> = ArrayList()
val keyById: Map<Long, PGPPublicKey>
get() {
val keyByIdMap = HashMap<Long, PGPPublicKey>()
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<PGPPublicKeyRing> = 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`")
}
}
}
}

View file

@ -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<Provider<RegularFile>>
@get:InputFile
abstract val detachedSignatureFile: Property<Provider<RegularFile>>
@get:Input
abstract val pgpFingerprintToKeyUrl: MapProperty<String, URL>
@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}."
)
}
}
}

View file

@ -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<String, URL>
) {
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<Long, PGPPublicKey>,
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()
}
}

View file

@ -7,3 +7,4 @@ dependencyResolutionManagement {
}
include 'commons'
include 'gradle-tasks'