diff --git a/build.gradle.kts b/build.gradle.kts index 3d9e1d9..d71accb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,12 +20,38 @@ repositories { mavenCentral() } +extra["springCloudVersion"] = "2023.0.1" + +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") + } +} + dependencies { implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-cache") + implementation("com.github.ben-manes.caffeine:caffeine:2.8.8") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign") implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.17.1") + + // Dependencies for generating JWT token + implementation("io.jsonwebtoken:jjwt-impl:0.11.2") + implementation("io.jsonwebtoken:jjwt-jackson:0.11.2") + testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("io.mockk:mockk:1.12.0") +} + +configurations.all { + resolutionStrategy { + eachDependency { + when (requested.module.toString()) { + "org.bouncycastle:bcprov-jdk18on" -> useVersion("1.78") + } + } + } } detekt { @@ -58,4 +84,4 @@ tasks.jacocoTestReport { xml.required.set(true) xml.outputLocation.set(File("${projectDir}/build/jacocoXml/jacocoTestReport.xml")) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt b/src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt new file mode 100644 index 0000000..2d6cf17 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt @@ -0,0 +1,15 @@ +package net.leanix.githubagent + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.runApplication +import org.springframework.cloud.openfeign.EnableFeignClients + +@SpringBootApplication +@EnableFeignClients +@EnableConfigurationProperties(value = [net.leanix.githubagent.config.GitHubEnterpriseProperties::class]) +class GitHubAgentApplication + +fun main() { + runApplication() +} diff --git a/src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt b/src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt deleted file mode 100644 index 58a5e51..0000000 --- a/src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.leanix.githubagent - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class GithubAgentApplication - -fun main() { - runApplication() -} diff --git a/src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt b/src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt new file mode 100644 index 0000000..36da759 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt @@ -0,0 +1,16 @@ +package net.leanix.githubagent.client + +import net.leanix.githubagent.dto.GitHubAppResponse +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestHeader + +@FeignClient(name = "githubClient", url = "\${github-enterprise.baseUrl}") +interface GitHubClient { + + @GetMapping("/api/v3/app") + fun getApp( + @RequestHeader("Authorization") jwt: String, + @RequestHeader("Accept") accept: String = "application/vnd.github.v3+json" + ): GitHubAppResponse +} diff --git a/src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt b/src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt new file mode 100644 index 0000000..ef85fc6 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt @@ -0,0 +1,30 @@ +package net.leanix.githubagent.config + +import jakarta.annotation.PostConstruct +import net.leanix.githubagent.exceptions.GitHubEnterpriseConfigurationMissingException +import org.springframework.stereotype.Component + +@Component +class AgentSetupValidation( + private val githubEnterpriseProperties: GitHubEnterpriseProperties +) { + + @PostConstruct + fun validateConfiguration() { + val missingProperties = mutableListOf() + + if (githubEnterpriseProperties.baseUrl.isBlank()) { + missingProperties.add("GITHUB_ENTERPRISE_BASE_URL") + } + if (githubEnterpriseProperties.githubAppId.isBlank()) { + missingProperties.add("GITHUB_ENTERPRISE_GITHUB_APP_ID") + } + if (githubEnterpriseProperties.pemFile.isBlank()) { + missingProperties.add("GITHUB_ENTERPRISE_PEM_FILE") + } + + if (missingProperties.isNotEmpty()) { + throw GitHubEnterpriseConfigurationMissingException(missingProperties.joinToString(", ")) + } + } +} diff --git a/src/main/kotlin/net/leanix/githubagent/config/GitHubEnterpriseProperties.kt b/src/main/kotlin/net/leanix/githubagent/config/GitHubEnterpriseProperties.kt new file mode 100644 index 0000000..7791c9e --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/config/GitHubEnterpriseProperties.kt @@ -0,0 +1,10 @@ +package net.leanix.githubagent.config + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "github-enterprise") +data class GitHubEnterpriseProperties( + val baseUrl: String, + val githubAppId: String, + val pemFile: String, +) diff --git a/src/main/kotlin/net/leanix/githubagent/dto/GitHubAppResponse.kt b/src/main/kotlin/net/leanix/githubagent/dto/GitHubAppResponse.kt new file mode 100644 index 0000000..167859f --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/dto/GitHubAppResponse.kt @@ -0,0 +1,11 @@ +package net.leanix.githubagent.dto + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +@JsonIgnoreProperties(ignoreUnknown = true) +data class GitHubAppResponse( + @JsonProperty("name") val name: String, + @JsonProperty("permissions") val permissions: Map, + @JsonProperty("events") val events: List +) diff --git a/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt b/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt new file mode 100644 index 0000000..a0b4691 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt @@ -0,0 +1,8 @@ +package net.leanix.githubagent.exceptions + +class GitHubEnterpriseConfigurationMissingException(properties: String) : RuntimeException( + "Github Enterprise properties '$properties' are not set" +) +class GitHubAppInsufficientPermissionsException(message: String) : RuntimeException(message) +class FailedToCreateJWTException(message: String) : RuntimeException(message) +class UnableToConnectToGitHubEnterpriseException(message: String) : RuntimeException(message) diff --git a/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt b/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt new file mode 100644 index 0000000..897b19b --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt @@ -0,0 +1,16 @@ +package net.leanix.githubagent.runners + +import net.leanix.githubagent.services.GitHubAuthenticationService +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component + +@Component +@Profile("!test") +class PostStartupRunner(private val githubAuthenticationService: GitHubAuthenticationService) : ApplicationRunner { + + override fun run(args: ApplicationArguments?) { + githubAuthenticationService.generateJwtToken() + } +} diff --git a/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt b/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt new file mode 100644 index 0000000..900934b --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt @@ -0,0 +1,61 @@ +package net.leanix.githubagent.services + +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.Expiry +import jakarta.annotation.PostConstruct +import net.leanix.githubagent.config.GitHubEnterpriseProperties +import org.springframework.stereotype.Service + +@Service +class CachingService( + private val githubEnterpriseProperties: GitHubEnterpriseProperties +) { + + data class CacheValue(val value: Any, val expiry: Long?) + + private val cache: Cache = Caffeine.newBuilder() + .maximumSize(100) + .expireAfter(object : Expiry { + override fun expireAfterCreate( + key: String, + value: CacheValue, + currentTime: Long + ): Long { + return value.expiry ?: Long.MAX_VALUE + } + + override fun expireAfterUpdate( + key: String, + value: CacheValue, + currentTime: Long, + currentDuration: Long + ): Long { + return value.expiry ?: Long.MAX_VALUE + } + + override fun expireAfterRead( + key: String, + value: CacheValue, + currentTime: Long, + currentDuration: Long + ): Long { + return currentDuration + } + }) + .build() + + fun set(key: String, value: Any, expiry: Long?) { + cache.put(key, CacheValue(value, expiry)) + } + + fun get(key: String): Any? { + return cache.getIfPresent(key)?.value + } + + @PostConstruct + private fun init() { + set("baseUrl", githubEnterpriseProperties.baseUrl, null) + set("githubAppId", githubEnterpriseProperties.githubAppId, null) + } +} diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt new file mode 100644 index 0000000..c76c573 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt @@ -0,0 +1,84 @@ +package net.leanix.githubagent.services + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import net.leanix.githubagent.config.GitHubEnterpriseProperties +import net.leanix.githubagent.exceptions.FailedToCreateJWTException +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.slf4j.LoggerFactory +import org.springframework.core.io.ResourceLoader +import org.springframework.stereotype.Service +import java.io.File +import java.io.IOException +import java.nio.charset.Charset +import java.nio.file.Files +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.Security +import java.security.spec.InvalidKeySpecException +import java.security.spec.PKCS8EncodedKeySpec +import java.util.* + +@Service +class GitHubAuthenticationService( + private val cachingService: CachingService, + private val githubEnterpriseProperties: GitHubEnterpriseProperties, + private val resourceLoader: ResourceLoader, + private val gitHubEnterpriseService: GitHubEnterpriseService +) { + + companion object { + private const val JWT_EXPIRATION_DURATION = 600000L + private const val pemPrefix = "-----BEGIN RSA PRIVATE KEY-----" + private const val pemSuffix = "-----END RSA PRIVATE KEY-----" + private val logger = LoggerFactory.getLogger(GitHubAuthenticationService::class.java) + } + + fun generateJwtToken() { + runCatching { + logger.info("Generating JWT token") + Security.addProvider(BouncyCastleProvider()) + val rsaPrivateKey: String = readPrivateKey() + val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(rsaPrivateKey)) + val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec) + val jwt = createJwtToken(privateKey) + cachingService.set("jwtToken", jwt.getOrThrow(), JWT_EXPIRATION_DURATION) + gitHubEnterpriseService.verifyJwt(jwt.getOrThrow()) + }.onFailure { + logger.error("Failed to generate/validate JWT token", it) + if (it is InvalidKeySpecException) { + throw IllegalArgumentException("The provided private key is not in a valid PKCS8 format.", it) + } else { + throw it + } + } + } + + private fun createJwtToken(privateKey: PrivateKey): Result { + return runCatching { + Jwts.builder() + .setIssuedAt(Date()) + .setExpiration(Date(System.currentTimeMillis() + JWT_EXPIRATION_DURATION)) + .setIssuer(cachingService.get("githubAppId").toString()) + .signWith(privateKey, SignatureAlgorithm.RS256) + .compact() + }.onFailure { + throw FailedToCreateJWTException("Failed to generate a valid JWT token") + } + } + + @Throws(IOException::class) + private fun readPrivateKey(): String { + val pemFile = File(resourceLoader.getResource("file:${githubEnterpriseProperties.pemFile}").uri) + val fileContent = String(Files.readAllBytes(pemFile.toPath()), Charset.defaultCharset()).trim() + + require(fileContent.startsWith(pemPrefix) && fileContent.endsWith(pemSuffix)) { + "The provided file is not a valid PEM file." + } + + return fileContent + .replace(pemPrefix, "") + .replace(System.lineSeparator().toRegex(), "") + .replace(pemSuffix, "") + } +} diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt new file mode 100644 index 0000000..d809db3 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt @@ -0,0 +1,51 @@ +package net.leanix.githubagent.services + +import net.leanix.githubagent.client.GitHubClient +import net.leanix.githubagent.dto.GitHubAppResponse +import net.leanix.githubagent.exceptions.GitHubAppInsufficientPermissionsException +import net.leanix.githubagent.exceptions.UnableToConnectToGitHubEnterpriseException +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class GitHubEnterpriseService(private val githubClient: GitHubClient) { + + companion object { + val expectedPermissions = listOf("administration", "contents", "metadata") + val expectedEvents = listOf("label", "public", "repository") + } + private val logger = LoggerFactory.getLogger(GitHubEnterpriseService::class.java) + + fun verifyJwt(jwt: String) { + runCatching { + val githubApp = githubClient.getApp("Bearer $jwt") + validateGithubAppResponse(githubApp) + logger.info("Authenticated as GitHub App: '${githubApp.name}'") + }.onFailure { + logger.error("Failed to verify JWT token", it) + when (it) { + is GitHubAppInsufficientPermissionsException -> throw it + else -> throw UnableToConnectToGitHubEnterpriseException("Failed to verify JWT token") + } + } + } + + fun validateGithubAppResponse(response: GitHubAppResponse) { + val missingPermissions = expectedPermissions.filterNot { response.permissions.containsKey(it) } + val missingEvents = expectedEvents.filterNot { response.events.contains(it) } + + if (missingPermissions.isNotEmpty() || missingEvents.isNotEmpty()) { + var message = "GitHub App is missing the following " + if (missingPermissions.isNotEmpty()) { + message = message.plus("permissions: $missingPermissions") + } + if (missingEvents.isNotEmpty()) { + if (missingPermissions.isNotEmpty()) { + message = message.plus(", and the following") + } + message = message.plus("events: $missingEvents") + } + throw GitHubAppInsufficientPermissionsException(message) + } + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..a6b5655 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,4 @@ +github-enterprise: + baseUrl: ${GITHUB_ENTERPRISE_BASE_URL:} + githubAppId: ${GITHUB_APP_ID:} + pemFile: ${PEM_FILE:} diff --git a/src/test/kotlin/net/leanix/githubagent/GithubAgentApplicationTests.kt b/src/test/kotlin/net/leanix/githubagent/GitHubAgentApplicationTests.kt similarity index 61% rename from src/test/kotlin/net/leanix/githubagent/GithubAgentApplicationTests.kt rename to src/test/kotlin/net/leanix/githubagent/GitHubAgentApplicationTests.kt index 0a55605..2405e95 100644 --- a/src/test/kotlin/net/leanix/githubagent/GithubAgentApplicationTests.kt +++ b/src/test/kotlin/net/leanix/githubagent/GitHubAgentApplicationTests.kt @@ -2,9 +2,11 @@ package net.leanix.githubagent import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles @SpringBootTest -class GithubAgentApplicationTests { +@ActiveProfiles("test") +class GitHubAgentApplicationTests { @Test fun contextLoads() { diff --git a/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt b/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt new file mode 100644 index 0000000..23b6882 --- /dev/null +++ b/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt @@ -0,0 +1,47 @@ +import io.mockk.every +import io.mockk.mockk +import net.leanix.githubagent.config.GitHubEnterpriseProperties +import net.leanix.githubagent.services.CachingService +import net.leanix.githubagent.services.GitHubAuthenticationService +import net.leanix.githubagent.services.GitHubEnterpriseService +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.springframework.core.io.ClassPathResource +import org.springframework.core.io.ResourceLoader + +class GitHubAuthenticationServiceTest { + + private val cachingService = mockk() + private val githubEnterpriseProperties = mockk() + private val resourceLoader = mockk() + private val gitHubEnterpriseService = mockk() + private val githubAuthenticationService = GitHubAuthenticationService( + cachingService, + githubEnterpriseProperties, + resourceLoader, + gitHubEnterpriseService + ) + + @Test + fun `generateJwtToken with valid data should not throw exception`() { + every { cachingService.get(any()) } returns "dummy-value" + every { cachingService.set(any(), any(), any()) } returns Unit + every { githubEnterpriseProperties.pemFile } returns "valid-private-key.pem" + every { resourceLoader.getResource(any()) } returns ClassPathResource("valid-private-key.pem") + every { gitHubEnterpriseService.verifyJwt(any()) } returns Unit + + assertDoesNotThrow { githubAuthenticationService.generateJwtToken() } + assertNotNull(cachingService.get("jwtToken")) + } + + @Test + fun `generateJwtToken with invalid data should throw exception`() { + every { cachingService.get(any()) } returns "dummy-value" + every { githubEnterpriseProperties.pemFile } returns "invalid-private-key.pem" + every { resourceLoader.getResource(any()) } returns ClassPathResource("invalid-private-key.pem") + + assertThrows(IllegalArgumentException::class.java) { githubAuthenticationService.generateJwtToken() } + } +} diff --git a/src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt b/src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt new file mode 100644 index 0000000..7e4fdf5 --- /dev/null +++ b/src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt @@ -0,0 +1,74 @@ +import io.mockk.every +import io.mockk.mockk +import net.leanix.githubagent.client.GitHubClient +import net.leanix.githubagent.dto.GitHubAppResponse +import net.leanix.githubagent.exceptions.GitHubAppInsufficientPermissionsException +import net.leanix.githubagent.exceptions.UnableToConnectToGitHubEnterpriseException +import net.leanix.githubagent.services.GitHubEnterpriseService +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow + +class GitHubEnterpriseServiceTest { + + private val githubClient = mockk() + private val service = GitHubEnterpriseService(githubClient) + + @Test + fun `verifyJwt with valid jwt should not throw exception`() { + val jwt = "validJwt" + val githubApp = GitHubAppResponse( + name = "validApp", + permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read"), + events = listOf("label", "public", "repository") + ) + every { githubClient.getApp(any()) } returns githubApp + + assertDoesNotThrow { service.verifyJwt(jwt) } + } + + @Test + fun `verifyJwt with invalid jwt should throw exception`() { + val jwt = "invalidJwt" + every { githubClient.getApp(any()) } throws Exception() + + assertThrows(UnableToConnectToGitHubEnterpriseException::class.java) { service.verifyJwt(jwt) } + } + + @Test + fun `validateGithubAppResponse with correct permissions should not throw exception`() { + val response = GitHubAppResponse( + name = "validApp", + permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read"), + events = listOf("label", "public", "repository") + ) + + assertDoesNotThrow { service.validateGithubAppResponse(response) } + } + + @Test + fun `validateGithubAppResponse with missing permissions should throw exception`() { + val response = GitHubAppResponse( + name = "validApp", + permissions = mapOf("administration" to "read", "contents" to "read"), + events = listOf("label", "public", "repository") + ) + + assertThrows( + GitHubAppInsufficientPermissionsException::class.java + ) { service.validateGithubAppResponse(response) } + } + + @Test + fun `validateGithubAppResponse with missing events should throw exception`() { + val response = GitHubAppResponse( + name = "validApp", + permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read"), + events = listOf("label", "public") + ) + + assertThrows( + GitHubAppInsufficientPermissionsException::class.java + ) { service.validateGithubAppResponse(response) } + } +} diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml new file mode 100644 index 0000000..aa42a24 --- /dev/null +++ b/src/test/resources/application.yaml @@ -0,0 +1,4 @@ +github-enterprise: + baseUrl: ${GITHUB_ENTERPRISE_BASE_URL:dummy} + githubAppId: ${GITHUB_APP_ID:dummy} + pemFile: ${PEM_FILE:dummy} diff --git a/src/test/resources/invalid-private-key.pem b/src/test/resources/invalid-private-key.pem new file mode 100644 index 0000000..c5ccba8 --- /dev/null +++ b/src/test/resources/invalid-private-key.pem @@ -0,0 +1,27 @@ + +MIIEpAIBAAKCAQEAwqjwjl1IJ2Mo4TMtNmAoZl+lnRP88D2ocMrj1QgKYzHTnsAW +UudOX909Mxbjj9ZKpuDpggL3/X+h/pCaD7yhS1OgYo1pl8TmWwmDq8ok0VJYlfxi +3oH76kexyQZ+SYT7YqZ7Xy67Q/kcjDyVK708vnKdhEaGFCVdIxbUfzsIynq6xbKx +PETEMlW1dBHQSrwIYgGTAwKTrvqmpNxZw7yIFA6qASddQmpbm0ycoMXYVrz+Nac6 +RrR93YVY3Jc+0c13bSenCqlRtMEHLfmGTuKboKiQWRgS16CEgfg2b29310OtLC6T +PSk9Dtv1knrVjpnWVaMq3w28ky3I1aeoKZCkPQIDAQABAoIBAQCWEJ0ac0k7rBMI +wWY6hBjBCz1mgdE995qSEadgRImVfQUSXi0Xjl/6QVl7uEqISYBVdBAv/U/m6m0d +DabnONjzdC2xrCjaKp4XUpdiaTzG7f+C6QXjWTu2mbyyJ8JVtSIDJCr57tHJDhN2 +/QFWrdVVUJCkN6YHg+JwOZpp1z3osSldnRCYUJ7NcPfNYCj/n0Gq5fQ3MUmk17ch +O5+XOxa8GBFj9hCqqFB97qnYSkRDTv0YoLdlIdnnVQeKYYMFCdKa++vgHX/7Pu8B +KFr34Fm1BFjkoIYjOtYbeUf2lWG+dzwEwLUu5DUcYS+YyUBCogUDLtROHScPSSFU +5hHin6S1AoGBAPHde46hvPmBNR6DGkds1twavbvEynlKiKdpgWn+ycBaLOPXO/hb +xdjAohZNIYwE72ggYWnMhHy1OnhytUMopMsT/xbDu+v5iwF+/9x9C7gdBj8drEzx +4E86O+lQ7ROh1PoAPwTqFUY0rEmsJRvfTY8oUp9LuiPWuO5Mc1tGIjJXAoGBAM4J +OYVKqc5Rzt4pSWzy3wzxekE1XVN7SRdcdYyjqOiYRLmc1jSx5nuTotluSd/trtZw +5Sf65e9YkO2zx5Ou4/TWdnGurWP8BgBAT2bDCDKjetiJTHSB68Hcz0zfH99C9h+E +8vn8Lpn57fFG+TOiADBPAYNEEkBxBJyGn4d+r8mLAoGBAISRIhT2f46+DDByKWg2 +trmjipUtctDyUl54TK+dMFXW1z32je891f5M70qL8jQ9zD7laJ9FsuRrrOWx8boi +v9hzWGDQ3eKkP1WNl43xmAfNGMxlZjgyZwDl6UqjyZ32GLcChYgbCZgWbMxgp2JU +jb1Gm6qmJhtYqLosexnvIfU3AoGAR5znNFAmQ0MmDwv0rHyiUIJiRuYAgTK5zffi +F7cOz4GVaZp8zaYEAXHoSYDPBpk7iueEjufjIdT70tMJDGjebMxaMNtRAw6nG1E/ +B+3EHK271iWqwFgkFKbmGsb28gf5Oi1gsskXfYdkT9emaG7nd+MOGI0BdwqRWsJk +EplTCk8CgYATcdreHFdXBCbRLszoiPPpvNTi0lBUdor+PzVrewAdByOY9dajBbap +2Fbuu2fkhBPEP8BL+3fJmbXsVVxOf9Nzy/IusekfuC5ZGnc41aCtaC6hplaXs131 +UvAdbhohImJi8D/p6uXPvrwrApBvoDpEu3Sq36VMCPeSv3YmTngLXw== +-----END RSA PRIVATE KEY----- diff --git a/src/test/resources/valid-private-key.pem b/src/test/resources/valid-private-key.pem new file mode 100644 index 0000000..dd61cc2 --- /dev/null +++ b/src/test/resources/valid-private-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwqjwjl1IJ2Mo4TMtNmAoZl+lnRP88D2ocMrj1QgKYzHTnsAW +UudOX909Mxbjj9ZKpuDpggL3/X+h/pCaD7yhS1OgYo1pl8TmWwmDq8ok0VJYlfxi +3oH76kexyQZ+SYT7YqZ7Xy67Q/kcjDyVK708vnKdhEaGFCVdIxbUfzsIynq6xbKx +PETEMlW1dBHQSrwIYgGTAwKTrvqmpNxZw7yIFA6qASddQmpbm0ycoMXYVrz+Nac6 +RrR93YVY3Jc+0c13bSenCqlRtMEHLfmGTuKboKiQWRgS16CEgfg2b29310OtLC6T +PSk9Dtv1knrVjpnWVaMq3w28ky3I1aeoKZCkPQIDAQABAoIBAQCWEJ0ac0k7rBMI +wWY6hBjBCz1mgdE995qSEadgRImVfQUSXi0Xjl/6QVl7uEqISYBVdBAv/U/m6m0d +DabnONjzdC2xrCjaKp4XUpdiaTzG7f+C6QXjWTu2mbyyJ8JVtSIDJCr57tHJDhN2 +/QFWrdVVUJCkN6YHg+JwOZpp1z3osSldnRCYUJ7NcPfNYCj/n0Gq5fQ3MUmk17ch +O5+XOxa8GBFj9hCqqFB97qnYSkRDTv0YoLdlIdnnVQeKYYMFCdKa++vgHX/7Pu8B +KFr34Fm1BFjkoIYjOtYbeUf2lWG+dzwEwLUu5DUcYS+YyUBCogUDLtROHScPSSFU +5hHin6S1AoGBAPHde46hvPmBNR6DGkds1twavbvEynlKiKdpgWn+ycBaLOPXO/hb +xdjAohZNIYwE72ggYWnMhHy1OnhytUMopMsT/xbDu+v5iwF+/9x9C7gdBj8drEzx +4E86O+lQ7ROh1PoAPwTqFUY0rEmsJRvfTY8oUp9LuiPWuO5Mc1tGIjJXAoGBAM4J +OYVKqc5Rzt4pSWzy3wzxekE1XVN7SRdcdYyjqOiYRLmc1jSx5nuTotluSd/trtZw +5Sf65e9YkO2zx5Ou4/TWdnGurWP8BgBAT2bDCDKjetiJTHSB68Hcz0zfH99C9h+E +8vn8Lpn57fFG+TOiADBPAYNEEkBxBJyGn4d+r8mLAoGBAISRIhT2f46+DDByKWg2 +trmjipUtctDyUl54TK+dMFXW1z32je891f5M70qL8jQ9zD7laJ9FsuRrrOWx8boi +v9hzWGDQ3eKkP1WNl43xmAfNGMxlZjgyZwDl6UqjyZ32GLcChYgbCZgWbMxgp2JU +jb1Gm6qmJhtYqLosexnvIfU3AoGAR5znNFAmQ0MmDwv0rHyiUIJiRuYAgTK5zffi +F7cOz4GVaZp8zaYEAXHoSYDPBpk7iueEjufjIdT70tMJDGjebMxaMNtRAw6nG1E/ +B+3EHK271iWqwFgkFKbmGsb28gf5Oi1gsskXfYdkT9emaG7nd+MOGI0BdwqRWsJk +EplTCk8CgYATcdreHFdXBCbRLszoiPPpvNTi0lBUdor+PzVrewAdByOY9dajBbap +2Fbuu2fkhBPEP8BL+3fJmbXsVVxOf9Nzy/IusekfuC5ZGnc41aCtaC6hplaXs131 +UvAdbhohImJi8D/p6uXPvrwrApBvoDpEu3Sq36VMCPeSv3YmTngLXw== +-----END RSA PRIVATE KEY-----