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

@@ -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"
}
}
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
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:
redis:
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-----