mirror of
https://github.com/bisq-network/bisq.git
synced 2025-03-03 18:56:59 +01:00
Add bisq2 gradle-tasks module
This commit is contained in:
parent
2e892bf90a
commit
4bd8d421be
11 changed files with 402 additions and 0 deletions
12
build-logic/gradle-tasks/build.gradle
Normal file
12
build-logic/gradle-tasks/build.gradle
Normal 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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package bisq.gradle.tasks
|
||||
|
||||
object PgpFingerprint {
|
||||
fun normalize(fingerprint: String): String =
|
||||
fingerprint.filterNot { it.isWhitespace() } // Remove all spaces
|
||||
.toLowerCase()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -7,3 +7,4 @@ dependencyResolutionManagement {
|
|||
}
|
||||
|
||||
include 'commons'
|
||||
include 'gradle-tasks'
|
||||
|
|
Loading…
Add table
Reference in a new issue