Skip to content

Commit

Permalink
Send signatures and verify signature of patches file before loading it
Browse files Browse the repository at this point in the history
  • Loading branch information
oSumAtrIX committed Jun 29, 2024
1 parent 83b232e commit e3477da
Show file tree
Hide file tree
Showing 15 changed files with 298 additions and 74 deletions.
5 changes: 4 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ dependencies {
implementation(libs.ktor.server.rate.limit)
implementation(libs.ktor.server.host.common)
implementation(libs.ktor.server.jetty)
implementation(libs.ktor.server.call.logging)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.koin.ktor)
implementation("io.bkbn:kompendium-core:latest.release")
implementation(libs.kompendium.core)
implementation(libs.h2)
implementation(libs.logback.classic)
implementation(libs.exposed.core)
Expand All @@ -82,6 +83,8 @@ dependencies {
implementation(libs.revanced.patcher)
implementation(libs.revanced.library)
implementation(libs.caffeine)
implementation(libs.bouncy.castle.provider)
implementation(libs.bouncy.castle.pgp)
}

// The maven-publish plugin is necessary to make signing work.
Expand Down
6 changes: 2 additions & 4 deletions configuration.example.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
organization = "revanced"
patches-repository = "revanced-patches"
integrations-repositories = [
"revanced-integrations"
]
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches.asc" }
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations.asc" }
contributors-repositories = [
"revanced-patcher",
"revanced-patches",
Expand Down
6 changes: 2 additions & 4 deletions configuration.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
organization = "revanced"
patches-repository = "revanced-patches"
integrations-repositories = [
"revanced-integrations"
]
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
contributors-repositories = [
"revanced-patcher",
"revanced-patches",
Expand Down
8 changes: 7 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[versions]
kompendium-core = "latest.release"
kotlin = "2.0.0"
logback = "1.4.14"
exposed = "0.41.1"
Expand All @@ -7,13 +8,15 @@ koin = "3.5.3"
dotenv = "6.4.1"
ktor = "2.3.7"
ktoml = "0.5.1"
picocli = "4.7.5"
picocli = "4.7.6"
datetime = "0.5.0"
revanced-patcher = "19.3.1"
revanced-library = "2.3.0"
caffeine = "3.1.8"
bouncy-castle = "1.78.1"

[libraries]
kompendium-core = { module = "io.bkbn:kompendium-core", version.ref = "kompendium-core" }
ktor-client-core = { module = "io.ktor:ktor-client-core" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" }
Expand All @@ -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-host-common = { module = "io.ktor:ktor-server-host-common" }
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" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
h2 = { module = "com.h2database:h2", version.ref = "h2" }
Expand All @@ -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-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" }
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]
serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal object StartAPICommand : Runnable {
configureSerialization()
configureSecurity()
configureOpenAPI()
configureLogging()
configureRouting()
}.start(wait = true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import app.revanced.api.configuration.repository.AnnouncementRepository
import app.revanced.api.configuration.repository.BackendRepository
import app.revanced.api.configuration.repository.ConfigurationRepository
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.ApiService
import app.revanced.api.configuration.services.AuthService
Expand Down Expand Up @@ -130,6 +131,7 @@ fun Application.configureDependencies(
)
}
singleOf(::AnnouncementService)
singleOf(::SignatureService)
singleOf(::PatchesService)
singleOf(::ApiService)
}
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/app/revanced/api/configuration/Logging.kt
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.ktor.client.*
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.
*/
Expand Down Expand Up @@ -97,12 +97,18 @@ abstract class BackendRepository internal constructor(
val createdAt: LocalDateTime,
val assets: Set<BackendAsset>,
) {
companion object {
fun Set<BackendAsset>.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) }
}

/**
* An asset of a release.
*
* @property name The name of the asset.
* @property downloadUrl The URL to download the asset.
*/
class BackendAsset(
val name: String,
val downloadUrl: String,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,78 @@
package app.revanced.api.configuration.repository

import app.revanced.api.configuration.services.PatchesService
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
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
internal class ConfigurationRepository(
val organization: String,
@SerialName("patches-repository")
val patchesRepository: String,
@SerialName("integrations-repositories")
val integrationsRepositoryNames: Set<String>,
val patches: AssetConfiguration,
val integrations: AssetConfiguration,
@SerialName("contributors-repositories")
val contributorsRepositoryNames: Set<String>,
@SerialName("api-version")
val apiVersion: Int = 1,
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())
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
releaseNote = release.body,
createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC),
assets = release.assets.map {
BackendAsset(downloadUrl = it.browserDownloadUrl)
BackendAsset(
name = it.name,
downloadUrl = it.browserDownloadUrl,
)
}.toSet(),
)
}
Expand Down Expand Up @@ -156,6 +159,7 @@ class GitHubOrganization {
) {
@Serializable
class GitHubAsset(
val name: String,
val browserDownloadUrl: String,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package app.revanced.api.configuration.routes

import app.revanced.api.configuration.installCache
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.APIReleaseVersion
import app.revanced.api.configuration.services.PatchesService
Expand All @@ -10,6 +12,7 @@ import io.ktor.server.application.*
import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.days
import org.koin.ktor.ext.get as koinGet

internal fun Route.patchesRoute() = route("patches") {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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>()
}
}
}
35 changes: 16 additions & 19 deletions src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package app.revanced.api.configuration.schema

import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
class APIRelease(
val version: String,
val createdAt: LocalDateTime,
val changelog: String,
val description: String,
val assets: Set<APIAsset>,
)

Expand Down Expand Up @@ -49,23 +48,15 @@ class APIContributable(
@Serializable
class APIAsset(
val downloadUrl: String,
) {
val type = when {
downloadUrl.endsWith(".jar") -> Type.PATCHES
downloadUrl.endsWith(".apk") -> Type.INTEGRATIONS
else -> Type.UNKNOWN
}

enum class Type {
@SerialName("patches")
PATCHES,

@SerialName("integrations")
INTEGRATIONS,

@SerialName("unknown")
UNKNOWN,
}
val signatureDownloadUrl: String,
// TODO: Remove this eventually when integrations are merged into patches.
val type: APIAssetType,
)

@Serializable
enum class APIAssetType {
PATCHES,
INTEGRATION,
}

@Serializable
Expand Down Expand Up @@ -113,3 +104,9 @@ class APIRateLimit(
val remaining: Int,
val reset: LocalDateTime,
)

@Serializable
class APIAssetPublicKeys(
val patchesPublicKey: String,
val integrationsPublicKey: String,
)
Loading

0 comments on commit e3477da

Please sign in to comment.