Add authorization endpoint (#284)

This commit is contained in:
KirillPamPam
2023-08-23 15:57:08 +04:00
committed by GitHub
parent 31b301952a
commit 75e137161b
39 changed files with 949 additions and 9 deletions

View File

@@ -98,6 +98,8 @@ dependencies {
implementation libs.caffeine implementation libs.caffeine
implementation libs.javax.annotations implementation libs.javax.annotations
implementation libs.auth0.jwt
testImplementation libs.cglib.nodep testImplementation libs.cglib.nodep
testImplementation libs.spockframework.core testImplementation libs.spockframework.core
testImplementation libs.grpc.testing testImplementation libs.grpc.testing

View File

@@ -124,6 +124,8 @@ spring-cloud-starter-sleuth = "org.springframework.cloud:spring-cloud-starter-sl
spring-cloud-sleuth-zipkin = "org.springframework.cloud:spring-cloud-sleuth-zipkin:3.1.6" spring-cloud-sleuth-zipkin = "org.springframework.cloud:spring-cloud-sleuth-zipkin:3.1.6"
brave-instrumentation-grpc = "io.zipkin.brave:brave-instrumentation-grpc:5.15.0" brave-instrumentation-grpc = "io.zipkin.brave:brave-instrumentation-grpc:5.15.0"
auth0-jwt = "com.auth0:java-jwt:4.4.0"
[bundles] [bundles]
apache-commons = ["commons-io", "apache-commons-lang3", "apache-commons-collections4"] apache-commons = ["commons-io", "apache-commons-lang3", "apache-commons-collections4"]
etherjar = ["etherjar-domain", "etherjar-hex", "etherjar-rpc-api", "etherjar-rpc-http", "etherjar-rpc-ws", "etherjar-tx", "etherjar-contract", "etherjar-erc20"] etherjar = ["etherjar-domain", "etherjar-hex", "etherjar-rpc-api", "etherjar-rpc-http", "etherjar-rpc-ws", "etherjar-tx", "etherjar-contract", "etherjar-erc20"]

View File

@@ -16,6 +16,7 @@
*/ */
package io.emeraldpay.dshackle package io.emeraldpay.dshackle
import io.emeraldpay.dshackle.config.AuthorizationConfig
import io.emeraldpay.dshackle.config.CacheConfig import io.emeraldpay.dshackle.config.CacheConfig
import io.emeraldpay.dshackle.config.ChainsConfig import io.emeraldpay.dshackle.config.ChainsConfig
import io.emeraldpay.dshackle.config.CompressionConfig import io.emeraldpay.dshackle.config.CompressionConfig
@@ -153,4 +154,9 @@ open class Config(
open fun chainsConfig(mainConfig: MainConfig): ChainsConfig { open fun chainsConfig(mainConfig: MainConfig): ChainsConfig {
return mainConfig.chains return mainConfig.chains
} }
@Bean
open fun authorizationConfig(mainConfig: MainConfig): AuthorizationConfig {
return mainConfig.authorization
}
} }

View File

@@ -16,6 +16,7 @@
*/ */
package io.emeraldpay.dshackle package io.emeraldpay.dshackle
import io.emeraldpay.dshackle.auth.AuthInterceptor
import io.emeraldpay.dshackle.config.MainConfig import io.emeraldpay.dshackle.config.MainConfig
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerGrpc import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerGrpc
import io.grpc.Codec import io.grpc.Codec
@@ -29,8 +30,6 @@ import io.micrometer.core.instrument.Metrics
import io.micrometer.core.instrument.Tag import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.scheduling.concurrent.CustomizableThreadFactory import org.springframework.scheduling.concurrent.CustomizableThreadFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -46,9 +45,7 @@ open class GrpcServer(
private val tlsSetup: TlsSetup, private val tlsSetup: TlsSetup,
private val accessHandler: AccessHandlerGrpc, private val accessHandler: AccessHandlerGrpc,
private val grpcServerBraveInterceptor: ServerInterceptor, private val grpcServerBraveInterceptor: ServerInterceptor,
@Autowired(required = false) private val authInterceptor: AuthInterceptor
@Qualifier("serverSpansInterceptor")
private val serverSpansInterceptor: ServerInterceptor?
) { ) {
@Value("\${spring.application.max-metadata-size}") @Value("\${spring.application.max-metadata-size}")
private var maxMetadataSize: Int = Defaults.maxMetadataSize private var maxMetadataSize: Int = Defaults.maxMetadataSize
@@ -87,9 +84,9 @@ open class GrpcServer(
} }
serverBuilder.intercept(grpcServerBraveInterceptor) serverBuilder.intercept(grpcServerBraveInterceptor)
serverSpansInterceptor?.let { if (mainConfig.authorization.enabled) {
serverBuilder.intercept(it) serverBuilder.intercept(authInterceptor)
log.info("Collect spans from provider is enabled") log.info("Token authorization is turned on")
} }
tlsSetup.setupServer("Native gRPC", mainConfig.tls, true)?.let { tlsSetup.setupServer("Native gRPC", mainConfig.tls, true)?.let {

View File

@@ -0,0 +1,25 @@
package io.emeraldpay.dshackle.auth
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
class AuthContext {
companion object {
val sessions = ConcurrentHashMap<String, TokenWrapper>()
fun putTokenInContext(tokenWrapper: TokenWrapper) {
sessions[tokenWrapper.sessionId] = tokenWrapper
}
fun removeToken(sessionId: String) {
sessions.remove(sessionId)
}
}
data class TokenWrapper(
val token: String,
val issuedAt: Instant,
val sessionId: String
)
}

View File

@@ -0,0 +1,38 @@
package io.emeraldpay.dshackle.auth
import io.emeraldpay.dshackle.auth.processor.SESSION_ID
import io.grpc.Metadata
import io.grpc.Metadata.ASCII_STRING_MARSHALLER
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import io.grpc.Status
import org.springframework.stereotype.Component
const val AUTH_METHOD_NAME = "emerald.Auth/Authenticate"
const val REFLECT_METHOD_NAME = "grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo"
@Component
class AuthInterceptor : ServerInterceptor {
private val specialMethods = setOf(AUTH_METHOD_NAME, REFLECT_METHOD_NAME)
override fun <ReqT : Any, RespT : Any> interceptCall(
call: ServerCall<ReqT, RespT>,
headers: Metadata,
next: ServerCallHandler<ReqT, RespT>
): ServerCall.Listener<ReqT> {
val sessionId = headers.get(
Metadata.Key.of(SESSION_ID, ASCII_STRING_MARSHALLER)
)
val isOrdinaryMethod = !specialMethods.contains(call.methodDescriptor.fullMethodName)
if (isOrdinaryMethod && (sessionId == null || !AuthContext.sessions.containsKey(sessionId))) {
val cause = if (sessionId == null) "sessionId is not passed" else "Session $sessionId does not exist"
throw Status.UNAUTHENTICATED
.withDescription(cause)
.asException()
}
return next.startCall(call, headers)
}
}

View File

@@ -0,0 +1,47 @@
package io.emeraldpay.dshackle.auth
import io.emeraldpay.api.proto.AuthOuterClass
import io.emeraldpay.api.proto.ReactorAuthGrpc
import io.emeraldpay.dshackle.auth.service.AuthService
import io.grpc.Status
import io.grpc.StatusException
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
@Service
class AuthRpc(
private val authService: AuthService,
private val authScheduler: Scheduler
) : ReactorAuthGrpc.AuthImplBase() {
companion object {
private val log = LoggerFactory.getLogger(AuthRpc::class.java)
}
override fun authenticate(request: Mono<AuthOuterClass.AuthRequest>): Mono<AuthOuterClass.AuthResponse> {
log.info("Start auth process...")
return request
.subscribeOn(authScheduler)
.map {
val token = authService.authenticate(it.token)
AuthOuterClass.AuthResponse.newBuilder()
.setProviderToken(token)
.build()
}.onErrorResume {
if (it is StatusException) {
log.error(it.message)
Mono.error(it)
} else {
val message = "Internal error: ${it.message}"
log.error(message, it)
Mono.error(
Status.INTERNAL
.withDescription(message)
.asException()
)
}
}
}
}

View File

@@ -0,0 +1,75 @@
package io.emeraldpay.dshackle.auth.processor
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import io.emeraldpay.dshackle.auth.AuthContext
import io.emeraldpay.dshackle.auth.service.KeyReader
import io.emeraldpay.dshackle.config.AuthorizationConfig
import io.grpc.Status
import org.springframework.stereotype.Component
import java.security.PrivateKey
import java.security.PublicKey
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.time.Instant
import java.util.UUID
const val SESSION_ID = "sessionId"
const val VERSION = "version"
enum class AuthVersion {
V1;
companion object {
fun getVersion(version: String) = values().find { it.name == version }
?: throw Status.INVALID_ARGUMENT
.withDescription("Unsupported auth version $version")
.asException()
}
}
abstract class AuthProcessor(
private val authorizationConfig: AuthorizationConfig
) {
open fun process(keys: KeyReader.Keys, token: String): AuthContext.TokenWrapper {
try {
val verifier: JWTVerifier = JWT.require(verifyingAlgorithm(keys.externalPublicKey))
.withIssuer(authorizationConfig.publicKeyOwner)
.build()
verifier.verify(token)
} catch (e: Exception) {
throw Status.INVALID_ARGUMENT
.withDescription("Invalid token: ${e.message}")
.asException()
}
return processInternal(keys.providerPrivateKey)
}
protected abstract fun processInternal(privateKey: PrivateKey): AuthContext.TokenWrapper
protected abstract fun verifyingAlgorithm(publicKey: PublicKey): Algorithm
}
@Component
open class AuthProcessorV1(
authorizationConfig: AuthorizationConfig
) : AuthProcessor(authorizationConfig) {
override fun processInternal(privateKey: PrivateKey): AuthContext.TokenWrapper {
val issAt = Instant.now()
val sessionId = UUID.randomUUID().toString()
val token = JWT.create()
.withIssuedAt(issAt)
.withClaim(SESSION_ID, sessionId)
.withClaim(VERSION, AuthVersion.V1.toString())
.sign(Algorithm.RSA256(privateKey as RSAPrivateKey))
return AuthContext.TokenWrapper(token, issAt, sessionId)
}
override fun verifyingAlgorithm(publicKey: PublicKey): Algorithm =
Algorithm.RSA256(publicKey as RSAPublicKey, null)
}

View File

@@ -0,0 +1,36 @@
package io.emeraldpay.dshackle.auth.processor
import com.auth0.jwt.interfaces.DecodedJWT
import io.grpc.Status
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class AuthProcessorResolver(
private val authProcessorV1: AuthProcessor
) {
companion object {
private val log = LoggerFactory.getLogger(AuthProcessorResolver::class.java)
}
fun getAuthProcessor(token: DecodedJWT): AuthProcessor {
val claimVersion = token.getClaim(VERSION)
if (claimVersion.isMissing) {
throw Status.INVALID_ARGUMENT
.withDescription("Version is not specified in the token")
.asException()
}
val version = AuthVersion.getVersion(claimVersion.asString())
log.info("Using $version of authentication")
if (version == AuthVersion.V1) {
return authProcessorV1
}
throw Status.INVALID_ARGUMENT
.withDescription("Unsupported auth version $version")
.asException()
}
}

View File

@@ -0,0 +1,26 @@
package io.emeraldpay.dshackle.auth.processor
import io.emeraldpay.dshackle.auth.AuthContext
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.time.Instant
import java.time.temporal.ChronoUnit
@Component
open class TokenProcessor {
companion object {
private val log = LoggerFactory.getLogger(TokenProcessor::class.java)
}
@Scheduled(fixedRate = 30000)
fun invalidateTokens() {
AuthContext.sessions
.filter { Instant.now().isAfter(it.value.issuedAt.plus(1, ChronoUnit.HOURS)) }
.forEach {
log.info("Invalidate token with sessionId ${it.key}")
AuthContext.removeToken(it.key)
}
}
}

View File

@@ -0,0 +1,37 @@
package io.emeraldpay.dshackle.auth.service
import com.auth0.jwt.JWT
import io.emeraldpay.dshackle.auth.AuthContext
import io.emeraldpay.dshackle.auth.processor.AuthProcessorResolver
import io.emeraldpay.dshackle.config.AuthorizationConfig
import io.grpc.Status
import org.springframework.stereotype.Service
@Service
class AuthService(
private val authorizationConfig: AuthorizationConfig,
private val rsaKeyReader: KeyReader,
private val authProcessorResolver: AuthProcessorResolver
) {
fun authenticate(token: String): String {
if (!authorizationConfig.enabled) {
throw Status.UNIMPLEMENTED
.withDescription("Authentication process is not enabled")
.asException()
}
val keys = rsaKeyReader.getKeyPair(
authorizationConfig.providerPrivateKeyPath, authorizationConfig.externalPublicKeyPath
)
val decodedJwt = JWT.decode(token)
return authProcessorResolver
.getAuthProcessor(decodedJwt)
.process(keys, token)
.run {
AuthContext.putTokenInContext(this)
this.token
}
}
}

View File

@@ -0,0 +1,14 @@
package io.emeraldpay.dshackle.auth.service
import java.security.PrivateKey
import java.security.PublicKey
interface KeyReader {
fun getKeyPair(providerPrivateKeyPath: String, externalPublicKeyPath: String): Keys
data class Keys(
val providerPrivateKey: PrivateKey,
val externalPublicKey: PublicKey
)
}

View File

@@ -0,0 +1,34 @@
package io.emeraldpay.dshackle.auth.service
import org.bouncycastle.openssl.PEMParser
import org.springframework.stereotype.Component
import java.io.StringReader
import java.nio.file.Files
import java.nio.file.Paths
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
@Component
class RsaKeyReader : KeyReader {
private val factory = KeyFactory.getInstance("RSA")
override fun getKeyPair(providerPrivateKeyPath: String, externalPublicKeyPath: String): KeyReader.Keys {
val privateKeyReader = StringReader(Files.readString(Paths.get(providerPrivateKeyPath)))
val publicKeyReader = StringReader(Files.readString(Paths.get(externalPublicKeyPath)))
val privatePem = PEMParser(privateKeyReader).readPemObject()
val publicPem = PEMParser(publicKeyReader).readPemObject()
val privateKeySpec = PKCS8EncodedKeySpec(privatePem.content)
val publicKeySpec = X509EncodedKeySpec(publicPem.content)
val pubKey = factory.generatePublic(publicKeySpec)
val privateKey = factory.generatePrivate(privateKeySpec)
return KeyReader.Keys(
privateKey,
pubKey
)
}
}

View File

@@ -0,0 +1,14 @@
package io.emeraldpay.dshackle.config
data class AuthorizationConfig(
val enabled: Boolean,
val publicKeyOwner: String,
val providerPrivateKeyPath: String,
val externalPublicKeyPath: String
) {
companion object {
@JvmStatic
fun default() = AuthorizationConfig(false, "", "", "")
}
}

View File

@@ -0,0 +1,53 @@
package io.emeraldpay.dshackle.config
import org.slf4j.LoggerFactory
import org.springframework.util.ResourceUtils
import org.yaml.snakeyaml.nodes.MappingNode
import java.io.FileNotFoundException
class AuthorizationConfigReader : YamlConfigReader<AuthorizationConfig>() {
companion object {
private val log = LoggerFactory.getLogger(AuthorizationConfigReader::class.java)
}
override fun read(input: MappingNode?): AuthorizationConfig {
val auth = getMapping(input, "auth")
if (auth == null) {
log.warn("Authorization is not using")
return AuthorizationConfig.default()
}
val enabled = getValueAsBool(auth, "enabled")
if (enabled == null || !enabled) {
log.warn("Authorization is not enabled")
return AuthorizationConfig.default()
}
val publicKeyOwner = getValueAsString(auth, "publicKeyOwner")
?: throw IllegalStateException("Public key owner in not specified")
val keyPair = getMapping(auth, "keys") ?: throw IllegalStateException("Auth keys is not specified")
val privateKey = getValueAsString(keyPair, "provider-private-key")
?: throw IllegalStateException("Private key in not specified")
val publicKey = getValueAsString(keyPair, "external-public-key")
?: throw IllegalStateException("External key in not specified")
if (fileNotExists(privateKey)) {
throw IllegalStateException("There is no such file: $privateKey")
}
if (fileNotExists(publicKey)) {
throw IllegalStateException("There is no such file: $publicKey")
}
return AuthorizationConfig(enabled, publicKeyOwner, privateKey, publicKey)
}
private fun fileNotExists(path: String): Boolean {
return try {
!ResourceUtils.getFile(path).exists()
} catch (e: FileNotFoundException) {
true
}
}
}

View File

@@ -30,4 +30,5 @@ class MainConfig {
var signature: SignatureConfig? = null var signature: SignatureConfig? = null
var compression: CompressionConfig = CompressionConfig.default() var compression: CompressionConfig = CompressionConfig.default()
var chains: ChainsConfig = ChainsConfig.default() var chains: ChainsConfig = ChainsConfig.default()
var authorization: AuthorizationConfig = AuthorizationConfig.default()
} }

View File

@@ -33,6 +33,7 @@ class MainConfigReader(
private val signatureConfigReader = SignatureConfigReader(fileResolver) private val signatureConfigReader = SignatureConfigReader(fileResolver)
private val compressionConfigReader = CompressionConfigReader() private val compressionConfigReader = CompressionConfigReader()
private val chainsConfigReader = ChainsConfigReader(upstreamsConfigReader) private val chainsConfigReader = ChainsConfigReader(upstreamsConfigReader)
private val authorizationConfigReader = AuthorizationConfigReader()
override fun read(input: MappingNode?): MainConfig { override fun read(input: MappingNode?): MainConfig {
val config = MainConfig() val config = MainConfig()
@@ -80,6 +81,9 @@ class MainConfigReader(
chainsConfigReader.read(input).let { chainsConfigReader.read(input).let {
config.chains = it config.chains = it
} }
authorizationConfigReader.read(input).let {
config.authorization = it
}
return config return config
} }
} }

View File

@@ -40,6 +40,11 @@ open class SchedulersConfig {
return makePool("grpc-client-channel", 10, monitoringConfig) return makePool("grpc-client-channel", 10, monitoringConfig)
} }
@Bean
open fun authScheduler(monitoringConfig: MonitoringConfig): Scheduler {
return makeScheduler("auth-scheduler", 4, monitoringConfig)
}
private fun makeScheduler(name: String, size: Int, monitoringConfig: MonitoringConfig): Scheduler { private fun makeScheduler(name: String, size: Int, monitoringConfig: MonitoringConfig): Scheduler {
return Schedulers.fromExecutorService(makePool(name, size, monitoringConfig)) return Schedulers.fromExecutorService(makePool(name, size, monitoringConfig))
} }

View File

@@ -0,0 +1,52 @@
package io.emeraldpay.dshackle.config
import spock.lang.Specification
class AuthorizationConfigReaderSpec extends Specification {
def reader = new AuthorizationConfigReader()
def "if no auth option then default settings"() {
setup:
def yamlIs = this.class.getClassLoader().getResourceAsStream("configs/upstreams-basic.yaml")
when:
def act = reader.read(yamlIs)
then:
act == AuthorizationConfig.default()
}
def "if auth is disabled then defaults settings"() {
setup:
def yamlIs = this.class.getClassLoader().getResourceAsStream("configs/auth-disabled.yaml")
when:
def act = reader.read(yamlIs)
then:
act == AuthorizationConfig.default()
}
def "check default settings"() {
when:
def act = AuthorizationConfig.default()
then:
!act.enabled
act.externalPublicKeyPath == ""
act.providerPrivateKeyPath == ""
}
def "exceptions if no settings"() {
setup:
def yamlIs = this.class.getClassLoader().getResourceAsStream(filePath)
when:
reader.read(yamlIs)
then:
def t = thrown(IllegalStateException)
t.message == message
where:
filePath | message
"configs/auth-without-public-key.yaml" | "External key in not specified"
"configs/auth-without-private-key.yaml" | "Private key in not specified"
"configs/auth-without-key-pair.yaml" | "Auth keys is not specified"
"configs/auth-without-key-owner.yaml" | "Public key owner in not specified"
"configs/auth-with-wrong-priv-key.yaml" | "There is no such file: classpath:keys/priv-wrong.p8.key"
"configs/auth-with-wrong-pub-key.yaml" | "There is no such file: classpath:keys/pub-wrong.key"
}
}

View File

@@ -102,5 +102,11 @@ class MainConfigReaderSpec extends Specification {
id == "infura" id == "infura"
} }
} }
act.authorization != null
with(act.authorization) {
enabled
providerPrivateKeyPath == "classpath:keys/priv.p8.key"
externalPublicKeyPath == "classpath:keys/public.pem"
}
} }
} }

View File

@@ -0,0 +1,55 @@
package io.emeraldpay.dshackle.auth.processor
import com.auth0.jwt.JWT
import io.emeraldpay.dshackle.config.AuthorizationConfig
import io.grpc.StatusException
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class AuthProcessorResolverTest {
private val authProcessorV1 = AuthProcessorV1(AuthorizationConfig.default())
private val authProcessorResolver = AuthProcessorResolver(authProcessorV1)
@Test
fun `get processor of V1 version`() {
val token = JWT.decode(
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjIiwiaWF0IjoxNjkyMTg1OTMxLCJ2ZXJzaW9uI" +
"joiVjEifQ.BZILN0GQ7JzXGFz-GZIbFTT9E5L-miB4Nga0v4o_cQThk8gbDelBRzEfdsqxCq_ppPr3v_Own8M-vR9yQElx5nEdlI4xe5QAMdIvr3g" +
"12fMckydX9IsW4sVQ1kJJY8RrHb-WL-uI0WSWqoMSwf-Psb-UyiEHAjc3oK7fA72lBaGT4waPHOxRBPvezwg7N934vCZvZMAftFfVgmeEtbCeD7bF" +
"umEr0uEmkIKPTg4QwP-VMvqoLBYpMiJVzP_Ipg_wRHJ7fUN0BGEPjjMvhQ_6TWByiQUBz1kTMd0Ebf_kEuXFQeiwA-FXHJpWczzh66CbbmmWAWsi" +
"ehKw3KPZeBj0oQ"
)
val processor = authProcessorResolver.getAuthProcessor(token)
assertTrue(processor is AuthProcessorV1)
}
@Test
fun `failed if no version is in a token`() {
val token = JWT.decode(
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjIiwiaWF0IjoxNjkyMTg5NTUzfQ.sQ1Q3DFC7kOlHWdnWaxv2F" +
"vhGso6ajJQVWtrE1hYmL_AIH_Lz8LNpqFShlL2itEGfmyRz5-Vrbdf-yWjeyLyNDlf7pay2lNE5Pvm2-EtQd8GUYcCFFIz7Sxc2Iphe2" +
"YIx6kBwSlaR0RXBcmUlOtKrnON0bBNzSojBmtCT7-j4hTpoKhYr04Fr9EJWHfw7grVZjU8rEizAX_SR3ZNoufjK_pZaIyI9qUKVPYSepP" +
"lXtQzVjA80qSeYpkeFCOLwlQD_yTArDNWlwe7-CthtBOAtctoTMwyudfJezT2ilXrigzbauzU5BEi1cNxacHpjNuXhyY0TiacJGugWfRgaaGy6g"
)
val e = assertThrows(StatusException::class.java) { authProcessorResolver.getAuthProcessor(token) }
assertEquals("INVALID_ARGUMENT: Version is not specified in the token", e.message)
}
@Test
fun `failed if the wrong version is specified`() {
val token = JWT.decode(
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjIiwiaWF0IjoxNjkyMTg5OTI3LCJ2ZXJzaW9uIjoiVjIifQ.pT" +
"mG3Z-h9P1DlocTKZ3BpAKNdsvRGjXo71I0GUpUemAnauUaH-OgxUOHlNcZ2uB4f1poEWsbpExeuZ15YAiBf2WBCEV6J6xH1u0cPC1O-8" +
"hHNb46166ngzo-BtZ7Rn7FeVEayyICslc9iXM5GvoFJyJIdn9uLMzalyDJq2bUmIelym4edkQ3ybF-pqf8garuVVErnAsbOFXbYQlfO5ZJ" +
"4zq6PfJo7QSkreMzQ4tK_-JJIGG-EK1bQjsAD7JiXipY2cJY17VlHuavI0DfcJlOe-QggbTH63rxL6JvXCyFux7gI7zdqSBP1fNDJNzTLA" +
"cnR7jCN0kMIa8urQgdePZyRg"
)
val e = assertThrows(StatusException::class.java) { authProcessorResolver.getAuthProcessor(token) }
assertEquals("INVALID_ARGUMENT: Unsupported auth version V2", e.message)
}
}

View File

@@ -0,0 +1,85 @@
package io.emeraldpay.dshackle.auth.processor
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.RegisteredClaims
import com.auth0.jwt.algorithms.Algorithm
import io.emeraldpay.dshackle.auth.service.RsaKeyReader
import io.emeraldpay.dshackle.config.AuthorizationConfig
import io.grpc.StatusException
import org.bouncycastle.openssl.PEMParser
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.springframework.util.ResourceUtils
import java.io.StringReader
import java.nio.file.Files
import java.nio.file.Paths
import java.security.KeyFactory
import java.security.PublicKey
import java.security.interfaces.RSAPublicKey
import java.security.spec.X509EncodedKeySpec
class AuthProcessorV1Test {
private val processor = AuthProcessorV1(AuthorizationConfig(true, "drpc", "", ""))
private val rsaKeyReader = RsaKeyReader()
private val privProviderPath = ResourceUtils.getFile("classpath:keys/priv.p8.key").path
private val publicDrpcPath = ResourceUtils.getFile("classpath:keys/public-drpc.pem").path
private val token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjIiwiaWF0IjoxNjkyMTg1OTMxLCJ2ZXJzaW9uI" +
"joiVjEifQ.BZILN0GQ7JzXGFz-GZIbFTT9E5L-miB4Nga0v4o_cQThk8gbDelBRzEfdsqxCq_ppPr3v_Own8M-vR9yQElx5nEdlI4xe5QAMdIvr3g" +
"12fMckydX9IsW4sVQ1kJJY8RrHb-WL-uI0WSWqoMSwf-Psb-UyiEHAjc3oK7fA72lBaGT4waPHOxRBPvezwg7N934vCZvZMAftFfVgmeEtbCeD7bF" +
"umEr0uEmkIKPTg4QwP-VMvqoLBYpMiJVzP_Ipg_wRHJ7fUN0BGEPjjMvhQ_6TWByiQUBz1kTMd0Ebf_kEuXFQeiwA-FXHJpWczzh66CbbmmWAWsi" +
"ehKw3KPZeBj0oQ"
private val keyPair = rsaKeyReader.getKeyPair(privProviderPath, publicDrpcPath)
@Test
fun `verify tokens is successful`() {
val publicProviderPath = ResourceUtils.getFile("classpath:keys/public.pem").path
val providerToken = processor.process(keyPair, token).token
val verifier: JWTVerifier = JWT.require(Algorithm.RSA256(generatePublicKey(publicProviderPath) as RSAPublicKey, null))
.withClaim(VERSION, "V1")
.build()
val decodedToken = verifier.verify(providerToken)
assertTrue(!decodedToken.getClaim(SESSION_ID).isMissing)
assertTrue(!decodedToken.getClaim(RegisteredClaims.ISSUED_AT).isMissing)
}
@Test
fun `verify token is failed by wrong key`() {
val publicProviderPath = ResourceUtils.getFile("classpath:keys/wrong-public.pem").path
val keyPair = rsaKeyReader.getKeyPair(privProviderPath, publicProviderPath)
val e = assertThrows(StatusException::class.java) { processor.process(keyPair, token) }
assertEquals(
"INVALID_ARGUMENT: Invalid token: The Token's Signature resulted invalid when verified using the Algorithm: SHA256withRSA",
e.message
)
}
@Test
fun `verify token is failed if no issuer`() {
val invalidToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjY2NjIiwiaWF0IjoxNjkyMTg3NDMwLCJ2ZXJzaW" +
"9uIjoiVjEifQ.J1WJ1GvjNJ9JQiMK0bwvtGX1o9P93F5-921myIx3TMa2X48qIG0GVEcoMgv01ca-_aisW-Amk27ygI09dPKE__Ijr6JhZ" +
"fDVNkw_ZArtQTcjhhJiCl3pqsouOlojc8EolpYUmyOefqemqycG0B84ibKAWTdXOtjibt1P5szWjIIV9yOYV7lTJkC0B5swcjjaMvTEPU7y" +
"ZJhg_wvCvT67yFM1K_Wnhys3-j-Xv1Y2wOkxNt4i5LKFDtMZml5eTIEscDpjp5ARjaSTW_Rs1Eixqltx_wz1ALiS0QXOJpX7pVMJjRcth4Nu" +
"R87ej434XoHZWqDmvOEM6M855WeHaO761A"
val e = assertThrows(StatusException::class.java) { processor.process(keyPair, invalidToken) }
assertEquals(
"INVALID_ARGUMENT: Invalid token: The Claim 'iss' value doesn't match the required issuer.",
e.message
)
}
private fun generatePublicKey(path: String): PublicKey {
val publicKeyReader = StringReader(Files.readString(Paths.get(path)))
val publicPem = PEMParser(publicKeyReader).readPemObject()
val publicKeySpec = X509EncodedKeySpec(publicPem.content)
return KeyFactory.getInstance("RSA").generatePublic(publicKeySpec)
}
}

View File

@@ -0,0 +1,53 @@
package io.emeraldpay.dshackle.auth.processor
import io.emeraldpay.dshackle.auth.AuthContext
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Instant
import java.time.temporal.ChronoUnit
class TokenProcessorTest {
private val tokenProcessor = TokenProcessor()
@BeforeEach
fun removeSessions() {
AuthContext.sessions.clear()
}
@Test
fun `invalidate all tokens`() {
AuthContext.putTokenInContext(
AuthContext.TokenWrapper("token", Instant.now().minus(1, ChronoUnit.HOURS), "session1")
)
AuthContext.putTokenInContext(
AuthContext.TokenWrapper("token", Instant.now().minus(1, ChronoUnit.HOURS), "session2")
)
AuthContext.putTokenInContext(
AuthContext.TokenWrapper("token", Instant.now().minus(1, ChronoUnit.HOURS), "session3")
)
tokenProcessor.invalidateTokens()
assertTrue(AuthContext.sessions.isEmpty())
}
@Test
fun `tokens are still in the context after invalidation`() {
val token1 = AuthContext.TokenWrapper("token", Instant.now().minus(30, ChronoUnit.MINUTES), "session1")
val token2 = AuthContext.TokenWrapper("token", Instant.now().minus(30, ChronoUnit.MINUTES), "session2")
val token3 = AuthContext.TokenWrapper("token", Instant.now().minus(30, ChronoUnit.MINUTES), "session3")
AuthContext.putTokenInContext(token1)
AuthContext.putTokenInContext(token2)
AuthContext.putTokenInContext(token3)
tokenProcessor.invalidateTokens()
assertEquals(3, AuthContext.sessions.size)
assertEquals(
mapOf(token1.sessionId to token1, token2.sessionId to token2, token3.sessionId to token3),
AuthContext.sessions
)
}
}

View File

@@ -0,0 +1,85 @@
package io.emeraldpay.dshackle.auth.service
import io.emeraldpay.dshackle.auth.AuthContext
import io.emeraldpay.dshackle.auth.processor.AuthProcessor
import io.emeraldpay.dshackle.auth.processor.AuthProcessorResolver
import io.emeraldpay.dshackle.config.AuthorizationConfig
import io.grpc.StatusException
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import java.security.PrivateKey
import java.security.PublicKey
import java.time.Instant
import java.util.concurrent.CompletableFuture
class AuthServiceTest {
private val rsaKeyReader = mock(KeyReader::class.java)
private val mockV1Processor = mock(AuthProcessor::class.java)
private val factory = AuthProcessorResolver(mockV1Processor)
private val token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkcnBjIiwiaWF0IjoxNjkyMTg1OTMxLCJ2ZXJzaW9uI" +
"joiVjEifQ.BZILN0GQ7JzXGFz-GZIbFTT9E5L-miB4Nga0v4o_cQThk8gbDelBRzEfdsqxCq_ppPr3v_Own8M-vR9yQElx5nEdlI4xe5QAMdIvr3g" +
"12fMckydX9IsW4sVQ1kJJY8RrHb-WL-uI0WSWqoMSwf-Psb-UyiEHAjc3oK7fA72lBaGT4waPHOxRBPvezwg7N934vCZvZMAftFfVgmeEtbCeD7bF" +
"umEr0uEmkIKPTg4QwP-VMvqoLBYpMiJVzP_Ipg_wRHJ7fUN0BGEPjjMvhQ_6TWByiQUBz1kTMd0Ebf_kEuXFQeiwA-FXHJpWczzh66CbbmmWAWsi" +
"ehKw3KPZeBj0oQ"
@Test
fun `unimplemented error if auth is disabled`() {
val authService = AuthService(AuthorizationConfig.default(), rsaKeyReader, factory)
val e = assertThrows(StatusException::class.java) { authService.authenticate("") }
assertEquals("UNIMPLEMENTED: Authentication process is not enabled", e.message)
}
@Test
fun `auth is successful`() {
val tokenWrapper = AuthContext.TokenWrapper(
"token", Instant.now(), "sessionId"
)
val authService = AuthService(AuthorizationConfig(true, "drpc", "privPath", "pubPath"), rsaKeyReader, factory)
val pair = KeyReader.Keys(mock(PrivateKey::class.java), mock(PublicKey::class.java))
`when`(rsaKeyReader.getKeyPair("privPath", "pubPath"))
.thenReturn(pair)
`when`(mockV1Processor.process(pair, token)).thenReturn(tokenWrapper)
authService.authenticate(token)
verify(rsaKeyReader).getKeyPair("privPath", "pubPath")
verify(mockV1Processor).process(pair, token)
assertTrue(AuthContext.sessions.containsKey(tokenWrapper.sessionId))
}
@Test
fun `parallel try to auth is successful`() {
val tokenWrapper = AuthContext.TokenWrapper(
"token", Instant.now(), "sessionId"
)
val tokenWrapper1 = AuthContext.TokenWrapper(
"token", Instant.now(), "sessionIdNext"
)
val pair = KeyReader.Keys(mock(PrivateKey::class.java), mock(PublicKey::class.java))
val authService = AuthService(AuthorizationConfig(true, "drpc", "privPath", "pubPath"), rsaKeyReader, factory)
`when`(rsaKeyReader.getKeyPair("privPath", "pubPath")).thenReturn(pair)
`when`(mockV1Processor.process(pair, token))
.thenReturn(tokenWrapper)
.thenReturn(tokenWrapper1)
val task = Runnable { authService.authenticate(token) }
CompletableFuture.allOf(
CompletableFuture.runAsync(task), CompletableFuture.runAsync(task)
).join()
verify(rsaKeyReader, times(2)).getKeyPair("privPath", "pubPath")
verify(mockV1Processor, times(2)).process(pair, token)
assertTrue(AuthContext.sessions.containsKey(tokenWrapper.sessionId))
assertTrue(AuthContext.sessions.containsKey(tokenWrapper1.sessionId))
}
}

View File

@@ -0,0 +1,69 @@
package io.emeraldpay.dshackle.auth.service
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.springframework.util.ResourceUtils
import java.math.BigInteger
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
class RsaKeyReaderTest {
private val rsaKeyReader = RsaKeyReader()
@Test
fun `read rsa keys`() {
val privPath = ResourceUtils.getFile("classpath:keys/priv.p8.key").path
val publicPath = ResourceUtils.getFile("classpath:keys/public-drpc.pem").path
val pair = rsaKeyReader.getKeyPair(privPath, publicPath)
val privateKey = pair.providerPrivateKey
val publicKey = pair.externalPublicKey
assertEquals("RSA", publicKey.algorithm)
assertEquals("RSA", privateKey.algorithm)
assertTrue(privateKey is RSAPrivateKey)
assertTrue(publicKey is RSAPublicKey)
assertTrue {
(publicKey as RSAPublicKey)
.run {
val pubExponent = BigInteger("65537")
val pubModulus = BigInteger(
"240721495118071395463408682378448427874712549978328672120875120395" +
"572530650526403138656868181996853461531503006596703750232991335166031612653183175739344513002793" +
"13348833980409910131805866311837600488070189526246817317791758685747539443624701130779843529258916" +
"650627570641542946773123818063304222185205897480168513227047010620610697362185901253798594085716467" +
"92700927695491574061920135297311257486773262784524717857554194798974994838378321973611865011031457" +
"8859834631052697211398759550437148562864384448640344034211059348047816397935347753413946137935402" +
"562264627177210569215727831448291731093608051334025332988696191"
)
publicExponent == pubExponent && modulus == pubModulus
}
}
assertTrue {
(privateKey as RSAPrivateKey)
.run {
val privModulus = BigInteger(
"2832492054027911929186769713106999105750517776225145627897719982164048887089826085864021820" +
"93695282142899084731750598834879176944012613634073603363270499693159806868257221336886781437442" +
"7025894179697109350195776864078690751332857203663624730020413376539574511375327958483292435380" +
"2072047577218574632871804997852794886795318485439021501388394533101191305015940756335545719496" +
"04557847518214006893286271137213491059994279470633688916032797849945887259137876037521925382454" +
"762238089597912107394417801381736627011587603418078578130206830219321107525729614610300336784925" +
"6741484379041499791605663418628739419306248595872949"
)
val privExponent = BigInteger(
"2704777724417882789147602456408780048462378556719441898242139736695157047955437327090684858" +
"9022442508913912020206014886681429805316382175637569992035330289450733224635356250217508515675" +
"1746547003301653260530619509144105809847739245799589488158044325999968951997941276374874356761" +
"7744591423894125208163912446945277960899800043458394321204754817284255735206518583285355872405" +
"3966258052672146605639969648388744960700258048094135244952249403403074548577386549705289154261" +
"2346391566526533514196717633725503640437930121313225102536030460301806443109607461162231878852" +
"64869837629637060803692577216204745443259318662678133769"
)
privateExponent == privExponent && modulus == privModulus
}
}
}
}

View File

@@ -0,0 +1,2 @@
auth:
enabled: false

View File

@@ -0,0 +1,6 @@
auth:
enabled: true
publicKeyOwner: drpc
keys:
provider-private-key: "classpath:keys/priv-wrong.p8.key"
external-public-key: "classpath:keys/pub-wrong.key"

View File

@@ -0,0 +1,6 @@
auth:
enabled: true
publicKeyOwner: drpc
keys:
provider-private-key: "classpath:keys/priv.p8.key"
external-public-key: "classpath:keys/pub-wrong.key"

View File

@@ -0,0 +1,2 @@
auth:
enabled: true

View File

@@ -0,0 +1,3 @@
auth:
enabled: true
publicKeyOwner: drpc

View File

@@ -0,0 +1,5 @@
auth:
enabled: true
publicKeyOwner: drpc
keys:
external-public-key: /keys/pub.key

View File

@@ -0,0 +1,5 @@
auth:
enabled: true
publicKeyOwner: drpc
keys:
provider-private-key: /keys/priv.p8.key

View File

@@ -11,6 +11,13 @@ tls:
require: false require: false
ca: "/path/ca.dshackle.test.crt" ca: "/path/ca.dshackle.test.crt"
auth:
enabled: true
publicKeyOwner: drpc
keys:
provider-private-key: "classpath:keys/priv.p8.key"
external-public-key: "classpath:keys/public.pem"
cache: cache:
redis: redis:
enabled: true enabled: true

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC+sCrzY7pN4L3C
uPyV8Q5dFilQYuP8kAWF2X6nAz2WFzHAPQJQXkwUeFz0T1Py0TtwrX6RcYu1KOuC
Bj78KvtptThFxFk9T9I3mpi00ZoIGVGPniW/4lMdN2DYqsXF/2r7K6dmtA9JQKAl
RjQiIkclwIn2/JYIFWi5viaeBl1XLG7m06TdksGt5JJ/c6yk0cFn0jlFeVLZr087
EWkj+3NN5+7dNHc+LCGunKHXX+cl+GHGc3JO6a953t6POhHXZh+/lX+WzD83uKUN
bYdCn0eyz2++6/lOhYR7x6Nc2d+ikEGzqGDnalD3bJPLV0hm5fXTUyd2zipPDtXc
R3UQsdZ/AgMBAAECggEBAJmDyEBDxGQYD40pUigAdhjY04/k3TiiVd/pNj6MrFMd
/y6Nr0Q2djdwox0IlEQC0ECpXCEJcXj6JYd7Mc/fZqI2g8QfC90BWvLq4g+IQuJM
R85AepGdSl5WDADCkxAQzfbBsTZMtZW6knMPrr7gK7kn5apStRJy8em/POWxqAmZ
1WLZP/CC2EBNk8zI+vJkZpWh4bbAXYalPBr+e+QXMdBJMlm8sO/cexqQSG3A2Igj
4DO16wxQn7ZahUF/caM4Y+v4LyO8fF75h+CLW8dZ5f344hI8QoEQWzbRjeDlgmie
BMkOAiB2Q6ejLO+3iPptVyTdFGrz5PkVfiHbVaD4A3kCgYEA9JwKDSBA03EyeIuf
PFkbcdiAOr8mSrtnfaFjfICOGYmiC/FQjVVa4l984J0OLRAIr+aEHe4zKY2wcWhZ
EaJGBaBAD7yEUR5nAMP+8zVnUNgVjIvvQuDhiVzPSl7qFj4QZOpztv2zBKmneBsW
HE2M5fxBcNxewSuPVPGzEh/p9/sCgYEAx5FWnAPu6SbCpXyK9JNty/Y9d+Ls+g/R
U9zZPAeuIFDclDEteP/GfE8fRkQ50IRRoxP0DUUDnNgHkqnCk+L/yB7lg4N9LlaK
9IF3o/LLuj40LwWZa8/E+72+16XtjuBCVNcglv9Ph/maGL0sYLXOYJUYMvslJm/4
ITtZicoiwE0CgYAwN/5HPh9pTvwrBSL7q3kchRgp/HpY6v5opoLNDS513ErEXeqK
IdRLoZUlVfBwc6OywRc0KzuMqnCounAsaLey7jcSow/WSc72OKyuVs2qAx6kWQVu
QpRTFqeKOpGcltl2ez0aSoKanbL0mypNo//taj+gNuC65ZJYpViubvoZNwKBgQC7
Eti3Y1B/ql/oNPklD1ZrTaaNTWGLf2xgSrQwe5qUnHhJSlgwBsQPHzRX7/iqKH9G
eZvbHIiobDGdIBlP/Ah9lcjcIVQlLecQEJUQULb9HPZZ5cvNrnQe792H1Pj9Z+eG
FCtuoGHzOt1it2J0Pbj5/Ik0sQxAHdHhhPsIGT2+OQKBgQDWcgBLTd1VNVR0iX74
aJx5VhYCMHQ+bA8qI46tyDid/c5GibXuWf8kHUfqlfcsRzv+HGsGRvyyvYn5LimE
HB9U3xy0z5q5i+G/6iwftw+ObaVgVYr5D5ovl4DT56nlG8IOYVNA1T1Heewkk5jc
ZrLvcE6YXcAKE1y5RWaDprVmjw==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDgYGhG/i+93a7W
oLK7T4SQkPH1+q1UppWxlPvQ3FunlOx8iGkTMKYx+iJEUm3E36eX466gVJYJpGPp
n+oYQ0f2QDx4zcElcqTvJhu644BBp2b1qim4f+iaWdEWUmdgIEfoePHe9NkUyIxK
m/TOYwtLSa8Rb+Vs7XePDUi9F/t/aQrHe6AwII8vKxgdvHUsqL+8A9oHIAMFZ6gL
4tasbFJqCfGaMyphW2AOnVNnkDOvLAGi2C9q9cjWnHZRhaHO34pEFnP6Wpor7rau
xSqS3WYtM1/Uxff/l5JCMB9AyRofbZSNWpsZYUesubSghzRfxcZzzk76lXPeDKS+
mb8P1Jy1AgMBAAECggEBANZCeZFBbWmLZUfC0KoZdudt4gAYTqhFIzEeqZB6vHM8
vM72nBAdJrcWut4vMG8Ne8aJPtoPq+6tMlOHx01AAlneYWCiakxOO7260EQYtwL6
zlDNG2X5jq7GEemU6u5aJRCTXbSgbyNvlrgwUQc8gS51wF2QUq8/3CmYb9tSKXEx
nRSvIT4RzMHxPfHgkSppSIJt4AQcof25f7ImVFNdv3rSlKCKhPTjMf87gBzOqlxg
xyAfIuxqpaDeY9rjywVahhthDDfRk7zt7m52huSIxg1oQuRKHY8iExCYXZgqc5Il
oelZN9ZleA+y+2LjSdpiU6yp63IUXo52246knPo7PAkCgYEA/S1E0ZDrc51trjcj
3KbHbq+O0w81WCjUzSWwtykVh1cyhZbwasqFUq2ydeYvpyq1z0sxbkUCjB9TV3G7
LvQg+cUJmUgAvJEuP9BwCb2lwCfBnqVK33T1QnbG/AynPoey5SiyZ7/AnRcujGLo
OwsttnxPw7NM1Us27bn+bF9bSgMCgYEA4uDshYJbJa6xgqkvdlZvZZnmxxjq0HP8
htR3hdh2/cMsDRr/gjUknQPsTLZKVxTQw+qJaYVBZpskWd0y5Bx5e+mhitHGr7z9
5FLz2wrWuQBFw0Lh97zcPhAiupAlkrpW2VFzc3wQvVjiJv9oaxFX3zXnFXXFN668
Px2K423JnOcCgYBP3LTFqoIrITDU8L3WP6HPT3+tIjIAkji2UbpRvJr261GbhEZo
WWP+9Z3CxQ6pG7boId8A2rH4A3WlstGJ+SZhw37IpEbfNKizJowA9prPZ3sTES7y
GBG5moAgR9mFxwZudQz53yniI8riK9z3qwoLc2Ex6WBGNAiqqq4P/3BrgwKBgEX2
urmJM3ZxYsg6SCqkCQI57ZLkOjVCL1Oc3abm0/r5XvJDqxuKK9pHxWz6of4sqxxf
jTR/JWXw3crgjbsOlOADcg+PFUhIbbslYZHgy8qNLPZD/88X7IsvCqahoRSYZgPq
PEIWtkrNyB/ij17FIGXzB2n0wXakaeTExXnQ92VtAoGAcwA5881UZWvXEdz7hoOt
BPssHkyKEPKWVhyyuyhJ4M2c1//vCdQM53RiWuNo5aU3kztS5+o2bQYm9VkmRL0o
yjR+UP61ekyK8vd+c0hG5m++Rp6SqjLSj3b4Ln7jtarM9OdXD12l1qZetBrDXnFi
bd1Cq+3lNGpl9Ut/u5DbWgw=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvrAq82O6TeC9wrj8lfEO
XRYpUGLj/JAFhdl+pwM9lhcxwD0CUF5MFHhc9E9T8tE7cK1+kXGLtSjrggY+/Cr7
abU4RcRZPU/SN5qYtNGaCBlRj54lv+JTHTdg2KrFxf9q+yunZrQPSUCgJUY0IiJH
JcCJ9vyWCBVoub4mngZdVyxu5tOk3ZLBreSSf3OspNHBZ9I5RXlS2a9POxFpI/tz
Tefu3TR3Piwhrpyh11/nJfhhxnNyTumved7ejzoR12Yfv5V/lsw/N7ilDW2HQp9H
ss9vvuv5ToWEe8ejXNnfopBBs6hg52pQ92yTy1dIZuX101Mnds4qTw7V3Ed1ELHW
fwIDAQAB
-----END PUBLIC KEY-----

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4GBoRv4vvd2u1qCyu0+E
kJDx9fqtVKaVsZT70Nxbp5TsfIhpEzCmMfoiRFJtxN+nl+OuoFSWCaRj6Z/qGENH
9kA8eM3BJXKk7yYbuuOAQadm9aopuH/omlnRFlJnYCBH6Hjx3vTZFMiMSpv0zmML
S0mvEW/lbO13jw1IvRf7f2kKx3ugMCCPLysYHbx1LKi/vAPaByADBWeoC+LWrGxS
agnxmjMqYVtgDp1TZ5AzrywBotgvavXI1px2UYWhzt+KRBZz+lqaK+62rsUqkt1m
LTNf1MX3/5eSQjAfQMkaH22UjVqbGWFHrLm0oIc0X8XGc85O+pVz3gykvpm/D9Sc
tQIDAQAB
-----END PUBLIC KEY-----

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy93QvZ7evi8V9crEQzPf
NxTdIx0tvvwUcp85TsoNfHJLAD41yjJaVPwXk2mD6KuoV7hkZjoGu5ow5pPKZUKp
MzOXsfVIAvGf0xEowWaANZflrX87ZwsWLK+4mZHSCA92THQoGncBu3kUGFPjukVn
vlKUcX+4lizXGDztMD35KrPXBNXziOMSxxqnL7jlWeNYDxqmCmquwy+kqF/mCwr1
n+k6GxF1H7plSGHNZalLRB6jJUQiC6EYDYl1hPv3EdPRPwjKXQ12J6LZ+XVdOLYC
s8Q1I1ajPsT6/rg5MjXn7paWc9GrBVG57lC7R0n9zSfo64ol3DuVQognOcU3MQT9
rQIDAQAB
-----END PUBLIC KEY-----