Send signatures and verify signature of patches file before loading it

This commit is contained in:
oSumAtrIX 2024-06-29 03:44:55 +02:00
parent f9cae1ea56
commit 4a685a2b53
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
17 changed files with 302 additions and 73 deletions

View File

@ -81,3 +81,6 @@ jobs:
push: true push: true
tags: ${{ steps.metadata.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }} labels: ${{ steps.metadata.outputs.labels }}
build-args: |
GITHUB_ACTOR=${{ github.actor }}
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}

View File

@ -65,9 +65,10 @@ dependencies {
implementation(libs.ktor.server.rate.limit) implementation(libs.ktor.server.rate.limit)
implementation(libs.ktor.server.host.common) implementation(libs.ktor.server.host.common)
implementation(libs.ktor.server.jetty) implementation(libs.ktor.server.jetty)
implementation(libs.ktor.server.call.logging)
implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.koin.ktor) implementation(libs.koin.ktor)
implementation("io.bkbn:kompendium-core:latest.release") implementation(libs.kompendium.core)
implementation(libs.h2) implementation(libs.h2)
implementation(libs.logback.classic) implementation(libs.logback.classic)
implementation(libs.exposed.core) implementation(libs.exposed.core)
@ -82,6 +83,8 @@ dependencies {
implementation(libs.revanced.patcher) implementation(libs.revanced.patcher)
implementation(libs.revanced.library) implementation(libs.revanced.library)
implementation(libs.caffeine) implementation(libs.caffeine)
implementation(libs.bouncy.castle.provider)
implementation(libs.bouncy.castle.pgp)
} }
// The maven-publish plugin is necessary to make signing work. // The maven-publish plugin is necessary to make signing work.

View File

@ -1,8 +1,6 @@
organization = "revanced" organization = "revanced"
patches-repository = "revanced-patches" patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc" }
integrations-repositories = [ integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations-public-key.asc" }
"revanced-integrations"
]
contributors-repositories = [ contributors-repositories = [
"revanced-patcher", "revanced-patcher",
"revanced-patches", "revanced-patches",

View File

@ -1,8 +1,6 @@
organization = "revanced" organization = "revanced"
patches-repository = "revanced-patches" patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
integrations-repositories = [ integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
"revanced-integrations"
]
contributors-repositories = [ contributors-repositories = [
"revanced-patcher", "revanced-patcher",
"revanced-patches", "revanced-patches",

View File

@ -6,6 +6,8 @@ services:
- /data/revanced-api/persistence:/app/persistence - /data/revanced-api/persistence:/app/persistence
- /data/revanced-api/.env:/app/.env - /data/revanced-api/.env:/app/.env
- /data/revanced-api/configuration.toml:/app/configuration.toml - /data/revanced-api/configuration.toml:/app/configuration.toml
- /data/revanced-api/patches-public-key.asc:/app/patches-public-key.asc
- /data/revanced-api/integrations-public-key.asc:/app/integrations-public-key.asc
environment: environment:
- COMMAND=start - COMMAND=start
ports: ports:

View File

@ -1,4 +1,5 @@
[versions] [versions]
kompendium-core = "latest.release"
kotlin = "2.0.0" kotlin = "2.0.0"
logback = "1.4.14" logback = "1.4.14"
exposed = "0.41.1" exposed = "0.41.1"
@ -7,13 +8,15 @@ koin = "3.5.3"
dotenv = "6.4.1" dotenv = "6.4.1"
ktor = "2.3.7" ktor = "2.3.7"
ktoml = "0.5.1" ktoml = "0.5.1"
picocli = "4.7.5" picocli = "4.7.6"
datetime = "0.5.0" datetime = "0.5.0"
revanced-patcher = "19.3.1" revanced-patcher = "19.3.1"
revanced-library = "2.3.0" revanced-library = "2.3.0"
caffeine = "3.1.8" caffeine = "3.1.8"
bouncy-castle = "1.78.1"
[libraries] [libraries]
kompendium-core = { module = "io.bkbn:kompendium-core", version.ref = "kompendium-core" }
ktor-client-core = { module = "io.ktor:ktor-client-core" } ktor-client-core = { module = "io.ktor:ktor-client-core" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio" } ktor-client-cio = { module = "io.ktor:ktor-client-cio" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" }
@ -29,6 +32,7 @@ ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" }
ktor-server-rate-limit = { module = "io.ktor:ktor-server-rate-limit" } ktor-server-rate-limit = { module = "io.ktor:ktor-server-rate-limit" }
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" } ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" }
ktor-server-jetty = { module = "io.ktor:ktor-server-jetty" } ktor-server-jetty = { module = "io.ktor:ktor-server-jetty" }
ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" } koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
h2 = { module = "com.h2database:h2", version.ref = "h2" } h2 = { module = "com.h2database:h2", version.ref = "h2" }
@ -45,6 +49,8 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" } revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" }
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" }
bouncy-castle-provider = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncy-castle" }
bouncy-castle-pgp = { module = "org.bouncycastle:bcpg-jdk18on", version.ref = "bouncy-castle" }
[plugins] [plugins]
serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

View File

@ -39,6 +39,7 @@ internal object StartAPICommand : Runnable {
configureSerialization() configureSerialization()
configureSecurity() configureSecurity()
configureOpenAPI() configureOpenAPI()
configureLogging()
configureRouting() configureRouting()
}.start(wait = true) }.start(wait = true)
} }

View File

@ -4,6 +4,7 @@ import app.revanced.api.configuration.repository.AnnouncementRepository
import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.BackendRepository
import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.repository.GitHubBackendRepository import app.revanced.api.configuration.repository.GitHubBackendRepository
import app.revanced.api.configuration.services.*
import app.revanced.api.configuration.services.AnnouncementService import app.revanced.api.configuration.services.AnnouncementService
import app.revanced.api.configuration.services.ApiService import app.revanced.api.configuration.services.ApiService
import app.revanced.api.configuration.services.AuthService import app.revanced.api.configuration.services.AuthService
@ -130,6 +131,7 @@ fun Application.configureDependencies(
) )
} }
singleOf(::AnnouncementService) singleOf(::AnnouncementService)
singleOf(::SignatureService)
singleOf(::PatchesService) singleOf(::PatchesService)
singleOf(::ApiService) singleOf(::ApiService)
} }

View File

@ -0,0 +1,16 @@
package app.revanced.api.configuration
import io.ktor.server.application.*
import io.ktor.server.plugins.callloging.*
import io.ktor.server.request.*
internal fun Application.configureLogging() {
install(CallLogging) {
format { call ->
val status = call.response.status()
val httpMethod = call.request.httpMethod.value
val uri = call.request.uri
"$status $httpMethod $uri"
}
}
}

View File

@ -4,7 +4,7 @@ import io.ktor.client.*
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
/** /**
* The backend of the application used to get data for the API. * The backend of the API used to get data.
* *
* @param client The HTTP client to use for requests. * @param client The HTTP client to use for requests.
*/ */
@ -97,12 +97,18 @@ abstract class BackendRepository internal constructor(
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val assets: Set<BackendAsset>, val assets: Set<BackendAsset>,
) { ) {
companion object {
fun Set<BackendAsset>.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) }
}
/** /**
* An asset of a release. * An asset of a release.
* *
* @property name The name of the asset.
* @property downloadUrl The URL to download the asset. * @property downloadUrl The URL to download the asset.
*/ */
class BackendAsset( class BackendAsset(
val name: String,
val downloadUrl: String, val downloadUrl: String,
) )
} }

View File

@ -1,18 +1,78 @@
package app.revanced.api.configuration.repository package app.revanced.api.configuration.repository
import app.revanced.api.configuration.services.PatchesService
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.io.File
/**
* The repository storing the configuration for the API.
*
* @property organization The API backends organization name where the repositories for the patches and integrations are.
* @property patches The source of the patches.
* @property integrations The source of the integrations.
* @property contributorsRepositoryNames The names of the repositories to get contributors from.
* @property apiVersion The version to use for the API.
* @property host The host of the API to configure CORS.
*/
@Serializable @Serializable
internal class ConfigurationRepository( internal class ConfigurationRepository(
val organization: String, val organization: String,
@SerialName("patches-repository") val patches: AssetConfiguration,
val patchesRepository: String, val integrations: AssetConfiguration,
@SerialName("integrations-repositories")
val integrationsRepositoryNames: Set<String>,
@SerialName("contributors-repositories") @SerialName("contributors-repositories")
val contributorsRepositoryNames: Set<String>, val contributorsRepositoryNames: Set<String>,
@SerialName("api-version") @SerialName("api-version")
val apiVersion: Int = 1, val apiVersion: Int = 1,
val host: String, val host: String,
) {
/**
* An asset configuration.
*
* [PatchesService] uses [BackendRepository] to get assets from its releases.
* A release contains multiple assets.
*
* This configuration is used in [ConfigurationRepository]
* to determine which release assets from repositories to get and to verify them.
*
* @property repository The repository in which releases are made to get an asset.
* @property assetRegex The regex matching the asset name.
* @property signatureAssetRegex The regex matching the signature asset name to verify the asset.
* @property publicKeyFile The public key file to verify the signature of the asset.
*/
@Serializable
internal class AssetConfiguration(
val repository: String,
@Serializable(with = RegexSerializer::class)
@SerialName("asset-regex")
val assetRegex: Regex,
@Serializable(with = RegexSerializer::class)
@SerialName("signature-asset-regex")
val signatureAssetRegex: Regex,
@Serializable(with = FileSerializer::class)
@SerialName("public-key-file")
val publicKeyFile: File,
) )
}
private object RegexSerializer : KSerializer<Regex> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Regex", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Regex) = encoder.encodeString(value.pattern)
override fun deserialize(decoder: Decoder) = Regex(decoder.decodeString())
}
private object FileSerializer : KSerializer<File> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("File", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: File) = encoder.encodeString(value.path)
override fun deserialize(decoder: Decoder) = File(decoder.decodeString())
}

View File

@ -35,7 +35,10 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
releaseNote = release.body, releaseNote = release.body,
createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC), createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC),
assets = release.assets.map { assets = release.assets.map {
BackendAsset(downloadUrl = it.browserDownloadUrl) BackendAsset(
name = it.name,
downloadUrl = it.browserDownloadUrl,
)
}.toSet(), }.toSet(),
) )
} }
@ -156,6 +159,7 @@ class GitHubOrganization {
) { ) {
@Serializable @Serializable
class GitHubAsset( class GitHubAsset(
val name: String,
val browserDownloadUrl: String, val browserDownloadUrl: String,
) )
} }

View File

@ -1,6 +1,8 @@
package app.revanced.api.configuration.routes package app.revanced.api.configuration.routes
import app.revanced.api.configuration.installCache
import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.installNotarizedRoute
import app.revanced.api.configuration.schema.APIAssetPublicKeys
import app.revanced.api.configuration.schema.APIRelease import app.revanced.api.configuration.schema.APIRelease
import app.revanced.api.configuration.schema.APIReleaseVersion import app.revanced.api.configuration.schema.APIReleaseVersion
import app.revanced.api.configuration.services.PatchesService import app.revanced.api.configuration.services.PatchesService
@ -10,6 +12,7 @@ import io.ktor.server.application.*
import io.ktor.server.plugins.ratelimit.* import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.days
import org.koin.ktor.ext.get as koinGet import org.koin.ktor.ext.get as koinGet
internal fun Route.patchesRoute() = route("patches") { internal fun Route.patchesRoute() = route("patches") {
@ -42,6 +45,18 @@ internal fun Route.patchesRoute() = route("patches") {
} }
} }
} }
rateLimit(RateLimitName("strong")) {
route("keys") {
installCache(356.days)
installPatchesPublicKeyRouteDocumentation()
get {
call.respond(patchesService.publicKeys())
}
}
}
} }
fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute { fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute {
@ -88,3 +103,18 @@ fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute {
} }
} }
} }
fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute {
tags = setOf("Patches")
get = GetInfo.builder {
description("Get the public keys for verifying patches and integrations assets")
summary("Get patches and integrations public keys")
response {
description("The public keys")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<APIAssetPublicKeys>()
}
}
}

View File

@ -1,14 +1,13 @@
package app.revanced.api.configuration.schema package app.revanced.api.configuration.schema
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
class APIRelease( class APIRelease(
val version: String, val version: String,
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val changelog: String, val description: String,
val assets: Set<APIAsset>, val assets: Set<APIAsset>,
) )
@ -49,23 +48,15 @@ class APIContributable(
@Serializable @Serializable
class APIAsset( class APIAsset(
val downloadUrl: String, val downloadUrl: String,
) { val signatureDownloadUrl: String,
val type = when { // TODO: Remove this eventually when integrations are merged into patches.
downloadUrl.endsWith(".jar") -> Type.PATCHES val type: APIAssetType,
downloadUrl.endsWith(".apk") -> Type.INTEGRATIONS )
else -> Type.UNKNOWN
}
enum class Type { @Serializable
@SerialName("patches") enum class APIAssetType {
PATCHES, PATCHES,
INTEGRATION,
@SerialName("integrations")
INTEGRATIONS,
@SerialName("unknown")
UNKNOWN,
}
} }
@Serializable @Serializable
@ -113,3 +104,9 @@ class APIRateLimit(
val remaining: Int, val remaining: Int,
val reset: LocalDateTime, val reset: LocalDateTime,
) )
@Serializable
class APIAssetPublicKeys(
val patchesPublicKey: String,
val integrationsPublicKey: String,
)

View File

@ -1,87 +1,119 @@
package app.revanced.api.configuration.services package app.revanced.api.configuration.services
import app.revanced.api.configuration.repository.BackendRepository import app.revanced.api.configuration.repository.BackendRepository
import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first
import app.revanced.api.configuration.repository.ConfigurationRepository import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.schema.APIAsset import app.revanced.api.configuration.schema.*
import app.revanced.api.configuration.schema.APIRelease
import app.revanced.api.configuration.schema.APIReleaseVersion
import app.revanced.library.PatchUtils import app.revanced.library.PatchUtils
import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.PatchBundleLoader
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import kotlinx.coroutines.Dispatchers import io.ktor.util.*
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.net.URL import java.net.URL
internal class PatchesService( internal class PatchesService(
private val signatureService: SignatureService,
private val backendRepository: BackendRepository, private val backendRepository: BackendRepository,
private val configurationRepository: ConfigurationRepository, private val configurationRepository: ConfigurationRepository,
) { ) {
private val patchesListCache = Caffeine
.newBuilder()
.maximumSize(1)
.build<String, ByteArray>()
suspend fun latestRelease(): APIRelease { suspend fun latestRelease(): APIRelease {
val patchesRelease = backendRepository.release( val patchesRelease = backendRepository.release(
configurationRepository.organization, configurationRepository.organization,
configurationRepository.patchesRepository, configurationRepository.patches.repository,
) )
val integrationsReleases = withContext(Dispatchers.Default) {
configurationRepository.integrationsRepositoryNames.map {
async { backendRepository.release(configurationRepository.organization, it) }
}
}.awaitAll()
val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets }) val integrationsRelease = backendRepository.release(
.map { APIAsset(it.downloadUrl) } configurationRepository.organization,
.filter { it.type != APIAsset.Type.UNKNOWN } configurationRepository.integrations.repository,
.toSet() )
fun ConfigurationRepository.AssetConfiguration.asset(
release: BackendRepository.BackendOrganization.BackendRepository.BackendRelease,
assetType: APIAssetType,
) = APIAsset(
release.assets.first(assetRegex).downloadUrl,
release.assets.first(signatureAssetRegex).downloadUrl,
assetType,
)
val patchesAsset = configurationRepository.patches.asset(
patchesRelease,
APIAssetType.PATCHES,
)
val integrationsAsset = configurationRepository.integrations.asset(
integrationsRelease,
APIAssetType.INTEGRATION,
)
return APIRelease( return APIRelease(
patchesRelease.tag, patchesRelease.tag,
patchesRelease.createdAt, patchesRelease.createdAt,
patchesRelease.releaseNote, patchesRelease.releaseNote,
assets, setOf(patchesAsset, integrationsAsset),
) )
} }
suspend fun latestVersion(): APIReleaseVersion { suspend fun latestVersion(): APIReleaseVersion {
val patchesRelease = backendRepository.release( val patchesRelease = backendRepository.release(
configurationRepository.organization, configurationRepository.organization,
configurationRepository.patchesRepository, configurationRepository.patches.repository,
) )
return APIReleaseVersion(patchesRelease.tag) return APIReleaseVersion(patchesRelease.tag)
} }
private val patchesListCache = Caffeine
.newBuilder()
.maximumSize(1)
.build<String, ByteArray>()
suspend fun list(): ByteArray { suspend fun list(): ByteArray {
val patchesRelease = backendRepository.release( val patchesRelease = backendRepository.release(
configurationRepository.organization, configurationRepository.organization,
configurationRepository.patchesRepository, configurationRepository.patches.repository,
) )
return patchesListCache.getIfPresent(patchesRelease.tag) ?: run { return patchesListCache.get(patchesRelease.tag) {
val downloadUrl = patchesRelease.assets val patchesDownloadUrl = patchesRelease.assets
.map { APIAsset(it.downloadUrl) } .first(configurationRepository.patches.assetRegex).downloadUrl
.find { it.type == APIAsset.Type.PATCHES }
?.downloadUrl
val patches = kotlin.io.path.createTempFile().toFile().apply { val signatureDownloadUrl = patchesRelease.assets
outputStream().use { URL(downloadUrl).openStream().copyTo(it) } .first(configurationRepository.patches.signatureAssetRegex).downloadUrl
}.let { file ->
PatchBundleLoader.Jar(file).also { file.delete() } val patchesFile = kotlin.io.path.createTempFile().toFile().apply {
outputStream().use { URL(patchesDownloadUrl).openStream().copyTo(it) }
} }
val patches = if (
signatureService.verify(
patchesFile,
signatureDownloadUrl,
configurationRepository.patches.publicKeyFile,
)
) {
PatchBundleLoader.Jar(patchesFile)
} else {
// Use an empty set of patches if the signature is invalid.
emptySet()
}
patchesFile.delete()
ByteArrayOutputStream().use { stream -> ByteArrayOutputStream().use { stream ->
PatchUtils.Json.serialize(patches, outputStream = stream) PatchUtils.Json.serialize(patches, outputStream = stream)
stream.toByteArray() stream.toByteArray()
}.also {
patchesListCache.put(patchesRelease.tag, it)
} }
} }
} }
fun publicKeys(): APIAssetPublicKeys {
fun publicKeyBase64(getAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.AssetConfiguration) =
configurationRepository.getAssetConfiguration().publicKeyFile.readBytes().encodeBase64()
return APIAssetPublicKeys(
publicKeyBase64 { patches },
publicKeyBase64 { integrations },
)
}
} }

View File

@ -0,0 +1,72 @@
package app.revanced.api.configuration.services
import com.github.benmanes.caffeine.cache.Caffeine
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openpgp.*
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider
import java.io.File
import java.io.InputStream
import java.net.URL
import java.security.MessageDigest
import java.security.Security
internal class SignatureService {
private val signatureCache = Caffeine
.newBuilder()
.maximumSize(2) // Assuming this is enough for patches and integrations.
.build<ByteArray, Boolean>() // Hash -> Verified.
fun verify(
file: File,
signatureDownloadUrl: String,
publicKeyFile: File,
): Boolean {
val fileBytes = file.readBytes()
return signatureCache.get(MessageDigest.getInstance("SHA-256").digest(fileBytes)) {
verify(
fileBytes = fileBytes,
signatureInputStream = URL(signatureDownloadUrl).openStream(),
publicKeyInputStream = publicKeyFile.inputStream(),
)
}
}
private fun verify(
fileBytes: ByteArray,
signatureInputStream: InputStream,
publicKeyInputStream: InputStream,
) = getSignature(signatureInputStream).apply {
init(BcPGPContentVerifierBuilderProvider(), getPublicKey(publicKeyInputStream))
update(fileBytes)
}.verify()
private fun getPublicKey(publicKeyInputStream: InputStream): PGPPublicKey {
val decoderStream = PGPUtil.getDecoderStream(publicKeyInputStream)
PGPPublicKeyRingCollection(decoderStream, BcKeyFingerprintCalculator()).forEach { keyRing ->
keyRing.publicKeys.forEach { publicKey ->
if (publicKey.isEncryptionKey) {
return publicKey
}
}
}
throw IllegalArgumentException("Can't find encryption key in key ring.")
}
private fun getSignature(inputStream: InputStream): PGPSignature {
val decoderStream = PGPUtil.getDecoderStream(inputStream)
val pgpObjectFactory = PGPObjectFactory(decoderStream, BcKeyFingerprintCalculator())
val signatureList = pgpObjectFactory.nextObject() as PGPSignatureList
return signatureList.first()
}
private companion object {
init {
Security.addProvider(BouncyCastleProvider())
}
}
}

View File

@ -4,8 +4,7 @@
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="trace"> <root level="info">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="org.eclipse.jetty" level="INFO"/>
</configuration> </configuration>