Skip to content

Commit

Permalink
Implement client SSL certificates (#2783)
Browse files Browse the repository at this point in the history
* implemented mutual ssl

* review comments, refactor config

* formatting
  • Loading branch information
WesselVS authored Apr 21, 2024
1 parent c2f754e commit 37e74bc
Show file tree
Hide file tree
Showing 13 changed files with 443 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,58 +16,76 @@

package zio.http.netty.client

import java.io.{FileInputStream, InputStream}
import java.io.{File, FileInputStream, InputStream}
import java.security.KeyStore
import javax.net.ssl.TrustManagerFactory

import scala.util.Using

import zio.Config.Secret
import zio.stacktracer.TracingImplicits.disableAutoTrace

import zio.http.ClientSSLCertConfig.{FromClientCertFile, FromClientCertResource}
import zio.http.ClientSSLConfig

import io.netty.handler.ssl.util.InsecureTrustManagerFactory
import io.netty.handler.ssl.{SslContext, SslContextBuilder}
object ClientSSLConverter {
private def trustStoreToSslContext(trustStoreStream: InputStream, trustStorePassword: Secret): SslContext = {
private def trustStoreToSslContext(
trustStoreStream: InputStream,
trustStorePassword: Secret,
sslContextBuilder: SslContextBuilder,
): SslContextBuilder = {
val trustStore = KeyStore.getInstance("JKS")
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)

trustStore.load(trustStoreStream, trustStorePassword.value.toArray)
trustManagerFactory.init(trustStore)
SslContextBuilder
.forClient()
.trustManager(trustManagerFactory)
.build()
}

private def certToSslContext(certStream: InputStream): SslContext =
SslContextBuilder
.forClient()
.trustManager(certStream)
.build()

def toNettySSLContext(sslConfig: ClientSSLConfig): SslContext = sslConfig match {
case ClientSSLConfig.Default =>
SslContextBuilder
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build()
sslContextBuilder.trustManager(trustManagerFactory)
}

case ClientSSLConfig.FromCertFile(certPath) =>
private def buildNettySslContextBuilder(
sslConfig: ClientSSLConfig,
sslContextBuilder: SslContextBuilder,
): SslContextBuilder = sslConfig match {
case ClientSSLConfig.Default =>
sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE)
case ClientSSLConfig.FromCertFile(certPath) =>
val certStream = new FileInputStream(certPath)
certToSslContext(certStream)

case ClientSSLConfig.FromCertResource(certPath) =>
sslContextBuilder.trustManager(certStream)
case ClientSSLConfig.FromCertResource(certPath) =>
val certStream = getClass.getClassLoader.getResourceAsStream(certPath)
certToSslContext(certStream)

case ClientSSLConfig.FromTrustStoreFile(trustStorePath, trustStorePassword) =>
sslContextBuilder.trustManager(certStream)
case ClientSSLConfig.FromTrustStoreResource(trustStorePath, trustStorePassword) =>
val trustStoreStream = getClass.getClassLoader.getResourceAsStream(trustStorePath)
trustStoreToSslContext(trustStoreStream, trustStorePassword, sslContextBuilder)
case ClientSSLConfig.FromClientAndServerCert(serverCertConfig, FromClientCertFile(certPath, keyPath)) =>
val newBuilder = buildNettySslContextBuilder(serverCertConfig, sslContextBuilder)
Using.Manager { use =>
val certInputStream = use(new FileInputStream(new File(certPath)))
val keyInputStream = use(new FileInputStream(new File(keyPath)))
newBuilder.keyManager(certInputStream, keyInputStream)
}.get
case ClientSSLConfig.FromClientAndServerCert(serverCertConfig, FromClientCertResource(certPath, keyPath)) =>
val newBuilder = buildNettySslContextBuilder(serverCertConfig, sslContextBuilder)
Using.Manager { use =>
val classLoader = getClass.getClassLoader
val certInputStream = use(classLoader.getResourceAsStream(certPath))
val keyInputStream = use(classLoader.getResourceAsStream(keyPath))
newBuilder.keyManager(certInputStream, keyInputStream)
}.get
case ClientSSLConfig.FromTrustStoreFile(trustStorePath, trustStorePassword) =>
val trustStoreStream = new FileInputStream(trustStorePath)
trustStoreToSslContext(trustStoreStream, trustStorePassword)
trustStoreToSslContext(trustStoreStream, trustStorePassword, sslContextBuilder)
}

case ClientSSLConfig.FromTrustStoreResource(trustStorePath, trustStorePassword) =>
val trustStoreStream = getClass.getClassLoader.getResourceAsStream(trustStorePath)
trustStoreToSslContext(trustStoreStream, trustStorePassword)
def toNettySSLContext(sslConfig: ClientSSLConfig): SslContext = {
buildNettySslContextBuilder(
sslConfig,
SslContextBuilder
.forClient(),
).build()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel._
import io.netty.handler.codec.http._
import io.netty.handler.codec.http.websocketx.{WebSocketFrame => JWebSocketFrame, WebSocketServerProtocolHandler}
import io.netty.handler.ssl.SslHandler
import io.netty.handler.timeout.ReadTimeoutException

@Sharable
Expand All @@ -52,6 +53,7 @@ private[zio] final case class ServerInboundHandler(
private var env: ZEnvironment[Any] = _

val inFlightRequests: LongAdder = new LongAdder()
val readClientCert = config.sslConfig.exists(_.includeClientCert)

def refreshApp(): Unit = {
val pair = appRef.get()
Expand Down Expand Up @@ -204,8 +206,13 @@ private[zio] final case class ServerInboundHandler(
case HttpVersion.HTTP_1_1 => Version.Http_1_1
case _ => throw new IllegalArgumentException(s"Unsupported HTTP version: $nettyHttpVersion")
}

val remoteAddress = ctx.channel().remoteAddress() match {
val clientCert = if (readClientCert) {
val sslHandler = ctx.pipeline().get(classOf[SslHandler])
sslHandler.engine().getSession().getPeerCertificates().headOption
} else {
None
}
val remoteAddress = ctx.channel().remoteAddress() match {
case m: InetSocketAddress => Option(m.getAddress)
case _ => None
}
Expand All @@ -222,6 +229,7 @@ private[zio] final case class ServerInboundHandler(
url = URL.decode(nettyReq.uri()).getOrElse(URL.empty),
version = protocolVersion,
remoteAddress = remoteAddress,
remoteCertificate = clientCert,
)
case nettyReq: HttpRequest =>
val knownContentLength = headers.get(Header.ContentLength).map(_.length)
Expand All @@ -235,6 +243,7 @@ private[zio] final case class ServerInboundHandler(
url = URL.decode(nettyReq.uri()).getOrElse(URL.empty),
version = protocolVersion,
remoteAddress = remoteAddress,
remoteCertificate = clientCert,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

package zio.http.netty.server

import java.io.FileInputStream
import java.io.{FileInputStream, InputStream}
import java.util

import scala.util.Using

import zio.http.SSLConfig.{HttpBehaviour, Provider}
import zio.http.netty.Names
import zio.http.{ClientAuth, SSLConfig, Server}
Expand Down Expand Up @@ -65,26 +67,58 @@ object SSLUtil {
}
}

def buildSslServerContext(
sslConfig: SSLConfig,
certInputStream: InputStream,
keyInputStream: InputStream,
trustCertCollectionPath: Option[InputStream],
): SslContext = {
val sslServerContext = SslContextBuilder
.forServer(certInputStream, keyInputStream)

trustCertCollectionPath.foreach { stream =>
sslServerContext.trustManager(stream)
}

sslServerContext.buildWithDefaultOptions(sslConfig)
}

def sslConfigToSslContext(sslConfig: SSLConfig): SslContext = sslConfig.data match {
case SSLConfig.Data.Generate =>
val selfSigned = new SelfSignedCertificate()
SslContextBuilder
.forServer(selfSigned.key, selfSigned.cert)
.buildWithDefaultOptions(sslConfig)

case SSLConfig.Data.FromFile(certPath, keyPath) =>
val certInputStream = new FileInputStream(certPath)
val keyInputStream = new FileInputStream(keyPath)
SslContextBuilder
.forServer(certInputStream, keyInputStream)
.buildWithDefaultOptions(sslConfig)
case SSLConfig.Data.FromFile(certPath, keyPath, trustCertCollectionPath) =>
Using.Manager { use =>
val certInputStream = use(new FileInputStream(certPath))
val keyInputStream = use(new FileInputStream(keyPath))
val trustCertInputStream = trustCertCollectionPath.map(path => use(new FileInputStream(path)))

case SSLConfig.Data.FromResource(certPath, keyPath) =>
val certInputStream = getClass().getClassLoader().getResourceAsStream(certPath)
val keyInputStream = getClass().getClassLoader().getResourceAsStream(keyPath)
SslContextBuilder
.forServer(certInputStream, keyInputStream)
.buildWithDefaultOptions(sslConfig)
buildSslServerContext(
sslConfig,
certInputStream,
keyInputStream,
trustCertInputStream,
)
}.get

case SSLConfig.Data.FromResource(certPath, keyPath, trustCertCollectionPath) =>
val classLoader = getClass().getClassLoader

Using.Manager { use =>
val certInputStream = use(classLoader.getResourceAsStream(certPath))
val keyInputStream = use(classLoader.getResourceAsStream(keyPath))
val trustCertInputStream = trustCertCollectionPath.map(path => use(classLoader.getResourceAsStream(path)))

buildSslServerContext(
sslConfig,
certInputStream,
keyInputStream,
trustCertInputStream,
)
}.get
}

}
Expand Down
22 changes: 22 additions & 0 deletions zio-http/jvm/src/test/resources/ca.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDmTCCAoGgAwIBAgIUOUA01yJSoIOIXgw52kZO1n1A0iwwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCTkwxFjAUBgNVBAgMDU5vb3JkLUhvbGxhbmQxEjAQBgNV
BAcMCUFtc3RlcmRhbTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk
MB4XDTI0MDQxODE0NTc1MVoXDTI5MDQxNzE0NTc1MVowXDELMAkGA1UEBhMCTkwx
FjAUBgNVBAgMDU5vb3JkLUhvbGxhbmQxEjAQBgNVBAcMCUFtc3RlcmRhbTEhMB8G
A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAu3izvbZx/DeREE8GQIbQ5Zr17NNHbRjjW/0Zx+ITQ6xE
Huld7N1cSZCl9v2qZPft2BvGYw/tzeTdEsMCnPQ6F7R3y8P1eAUV3vZqzVgn0ZYV
1X7N0Tolmv4Xm64J9CY5gLWe93wovj+dZyTFswG8A2EVDCCV3a34Tzr6Ql+35SUc
aAG4Qst740KdlPRBjXAY0wsLTd9F1r44DrnKsuF+6Vkco5R4v0/9xESVUla7Rr3h
1MAcgIjz8+k5hzWVh8RResyvJ6uy+y56xqd6KWYLV8FNOTUsUEurK7VWfn4dtqYa
dGugzRbvDGD/0REf9xCaxwYqTgPDWq9jqnUibWNf2QIDAQABo1MwUTAdBgNVHQ4E
FgQUcL6Yh691BHY7gKGy58OpEqgXmLMwHwYDVR0jBBgwFoAUcL6Yh691BHY7gKGy
58OpEqgXmLMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEASFHl
N/kmCize9d/V0I6AwGUBxDN8GMyrJikz4lCpXkRgJ9V1BwC1RAwuIDAsJ9HfwW07
e9HJ1LHtMBREyE3L9G4Bq8BUKrSZqfEVB4MYpYC7QrweGViHgocfjbr+eOX/yz8T
WekYwYr1AKRFfwrcF2BBoZoHadOMqKBXd3hP3JxuVx+zkT4QtY4EjZnyF57egRpT
hYvfQhgVlD+k3GXA3M5BpVsmHGcl8CKHuqgwlp9DIe4JkmPBYDVAI3WJnaNPUjOj
9Kwwg1I49nz2T7M+iVyZd0DZqzKQewh59Qmke6ilYLvtpRzsJAmi77D1vZ/fcfWd
sWuTDAY6Ns95pqB2cw==
-----END CERTIFICATE-----
21 changes: 21 additions & 0 deletions zio-http/jvm/src/test/resources/client.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDizCCAnOgAwIBAgIUGn83H/sShAhZiPRjNBVgCpEqHEswDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCTkwxFjAUBgNVBAgMDU5vb3JkLUhvbGxhbmQxEjAQBgNV
BAcMCUFtc3RlcmRhbTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk
MB4XDTI0MDQxODE3NTYyOFoXDTI2MDcyMjE3NTYyOFowNDELMAkGA1UEBhMCQVUx
EzARBgNVBAgMClNvbWUtU3RhdGUxEDAOBgNVBAoMB2NsaWVudDEwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0waqpZyBTwMV3Kxwy3HSoM7jZ3KKCopGa
rB7mIovzQDKI501oykZh4dzD2rSFUNw0yBqxGcWMZzwjdB835rNwuarIIJfsJ+Dk
uVjsxsicicXUCBAzZW14WDpFbXwmaXtojHcZbTaPAWORS7BbFcOCbQ9CqV5zxn9X
Oy5BkgZSXvhnLOcZyxAygV8a/IF6ivZ+IspPIOZOvtFfU/hqDRJdkl9RxCxeA2PR
7J2siUJjtRXJkqdECrNr0o1BH7HdqQuKl0x0Afd/LWV8k6mrth91AsEC6KxfHKsw
bOefW87XUGIQZOD8eOyF143JfPOYazTcO2SJHdtqukFFZTTpUVsDAgMBAAGjbTBr
MB8GA1UdIwQYMBaAFHC+mIevdQR2O4ChsufDqRKoF5izMAkGA1UdEwQCMAAwCwYD
VR0PBAQDAgTwMBEGA1UdEQQKMAiCBmNsaWVudDAdBgNVHQ4EFgQU2lGiFkDHeEyG
9tmOSxU2fX9D6tUwDQYJKoZIhvcNAQELBQADggEBACmTF/O+0Y2M5sCtQCEg+xPM
xrCXdGRDBb5i/AHfATnKpOMmcrr824xI7GTlcoOmdEjKRJPdkVQC/ME9gkcf3GGU
ppjzZLQlTrkl0YFQDKBjeAafFUe2kfjq5Agx6AK6WMuQ819Vhqr98HQmS24nrldE
1ycXX7V3HAOFeC7lyhQyNAO7XYHcwkqTzvO/AEd92S+0Qr4fD9Tn7tigPDqfAa6A
05OrVsu+77362Gzc4xRUjfHk1Fe7P8RIu01FCkiA2g2lpYhoOw50EHtfl6q8waom
1KnbxU2InqXIlSrMG2JdgHV/xIAJcLeeCrt2oFqL91eZlev+Z5M+T8Wf6K5V8ac=
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions zio-http/jvm/src/test/resources/client.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC0waqpZyBTwMV3
Kxwy3HSoM7jZ3KKCopGarB7mIovzQDKI501oykZh4dzD2rSFUNw0yBqxGcWMZzwj
dB835rNwuarIIJfsJ+DkuVjsxsicicXUCBAzZW14WDpFbXwmaXtojHcZbTaPAWOR
S7BbFcOCbQ9CqV5zxn9XOy5BkgZSXvhnLOcZyxAygV8a/IF6ivZ+IspPIOZOvtFf
U/hqDRJdkl9RxCxeA2PR7J2siUJjtRXJkqdECrNr0o1BH7HdqQuKl0x0Afd/LWV8
k6mrth91AsEC6KxfHKswbOefW87XUGIQZOD8eOyF143JfPOYazTcO2SJHdtqukFF
ZTTpUVsDAgMBAAECggEAA0YatDTE9h9gtDbYrOcnScCeEXwYiWsuXfHOIXbwt/gq
siYWVdaMDcvMdtOoP13mjyIssZZ46IEaKr44roeEKn+CY1uLxOA4B4x/2tIC4irw
pVV8DaJBqFNYwtcnz35LmOoqYhCDU3W/+uq3B/9N6jfVdeyhZFFtDwRoYxGlAark
xsPCvKne6B1SBDWnLioggR7orxmc6HccujXDY+XuEY9CCuGH//6gMmI5TIZAJHZ/
u/59wtP/+BXDtaYVKyJHCoFRvo+hNytDKOGBm/G3dJXvKzn59RGfl+lX9VMO72Ai
whymng/dtOET188fCYWE6jVd5DNCudtaSzoGb/CpmQKBgQD/QsAWnDyNFcLROUk2
+/9RJuS+pDDuAGfGQiakniBhqijFmaqdTPcjDTVjrkgd1rs6oIuo6rpLX3DO5Jx+
b2N6sMfPLMgxuCWKrYHNi0HVGTPpzmd6MOjR83v/J4Ab1AW3GZRVSUQzx3ZYH5VL
cNTu04/26FRDfYza+EoGT3uFvwKBgQC1R63Wp0P7rMNFTWQOXJ57BB7QybbQ0lNV
4cMWnpCbPnL1x7hRRYlM2AkIqLsRavo+sX9j3Eb18NVjCz/zi7jpUoICloLuzFHC
MQ5NsAUiZP9rVebz15f+AJEtxRB7ff4TgrIx0+IriPCwnrKin8+ML23w2UKZoGHG
XW2j7smjvQKBgQDNBTH/dxFzSA0nutlBXnmdFp4XOQ1Lu+ud47fh9FeR2ffRbjEJ
Eq5U6uD85RhrTvMmjZhe3dkvfMLAigHf01uVSbPShOdud7c/gUiGbNk9bXayzeNy
0yHMr8HiGDnfIBZPEC6Bqc0rwYMeGI/y/fcos6gn6kbzxj2CCnZ6RxCxRQKBgQCA
jOX/k/mnnSmUGX9cl91iitd3Y7OafavO+RupDKSrEivfktNn/pGQxpoRY+XUQjJX
BvO5UtmXSkNnyvB8upZUDshXIXiT8pv/w13LRYk+jLR73xy5yibkXm6VMGuxxvTH
zSSkJQb7rE5SUReILx+032VtKAQMgSkUy+gWGh0vWQKBgQCDg4OSXZel1MT0usbw
DnS0aR/TACreEJGjvij4EpqWXo4PkySYjPKFCkjlJV3b6wmBESih+Nnj+ViJ/cFp
9puyZ3pQuZLhrp4tdf5lAKgkKP6/A14rW9rrA5lxGX+TriKOvvVYRhXVHXuJYKAF
2FDmwSZ9xd6tOyzQRBw/4/z2xg==
-----END PRIVATE KEY-----
21 changes: 21 additions & 0 deletions zio-http/jvm/src/test/resources/client_other.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDizCCAnOgAwIBAgIUNp8kgGT57Ccvfd4ecinbl1rKGz0wDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA0MTgxNzI2MjVaFw0yNjA3
MjIxNzI2MjVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDJ1T+Ga7OnvrHQbzfkITacSXiWgxugCHUg0+q13hBA
fOy4XU/bjmxnCm+pDty9yJxh9khmmZGhL9MtYZo1Jjnc4avLJKJU2qqPKfBqD4rZ
bJTJoX5hKFsdtyvPgP9d0ToohUW6JMk3czoaYLUfng8uP5PMrLoOrjFaojkIkquq
swsodmeJ9HSb9cmmlG+EOk9bmzhYQRDaHm0cCbL8in9heQ715pgdDFpzNaAD0T/j
A/lvrHH3QdxstpjHxym+OK1OA4NcD+pbipLN0bYt0yHHBXYajMFRn16EFNKPObPi
82t+hapIOIHTJCCLrefcZe/+SAQWQ1hFfjCIAc50+fqXAgMBAAGjczBxMB8GA1Ud
IwQYMBaAFOGAgca2g2UySe41CYVpitPIq5d5MAkGA1UdEwQCMAAwCwYDVR0PBAQD
AgTwMBcGA1UdEQQQMA6CDGNsaWVudF9vdGhlcjAdBgNVHQ4EFgQUpIPDdrrThQGa
YUG6+zNhq1SgUNEwDQYJKoZIhvcNAQELBQADggEBAIXlIl8u1L9EKzzT1erA6PtC
ZXv68ZrcI9awv4gVcxlxCeM/SBqiLhEzIczDG++98q3Pwfq0rN3rFWfFMp3fgIfX
AdvucRnmNY5EoG/rkexVv+0bXJa9AT9wyGyXyaE7DVagVX1onRb4NmlbCASQOc0L
oqvX2QUmNfDEkvkXmDuDIyToZ+CmTQY72qRQpZukTUfGQpppxHgcNbxJVVeSxj8D
VFYcFJr+FG/i5CKcNYulXZ1fomFLNumCF/rmN4tN3/K6+q3jCmFeNzxCxkQ98GEY
11ORKjKiI4SvQOq8HCpVq4LbtCiZHz8njuMRAzU6/1yyokEudftf/iGG2TcejJE=
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions zio-http/jvm/src/test/resources/client_other.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQDJ1T+Ga7OnvrHQ
bzfkITacSXiWgxugCHUg0+q13hBAfOy4XU/bjmxnCm+pDty9yJxh9khmmZGhL9Mt
YZo1Jjnc4avLJKJU2qqPKfBqD4rZbJTJoX5hKFsdtyvPgP9d0ToohUW6JMk3czoa
YLUfng8uP5PMrLoOrjFaojkIkquqswsodmeJ9HSb9cmmlG+EOk9bmzhYQRDaHm0c
CbL8in9heQ715pgdDFpzNaAD0T/jA/lvrHH3QdxstpjHxym+OK1OA4NcD+pbipLN
0bYt0yHHBXYajMFRn16EFNKPObPi82t+hapIOIHTJCCLrefcZe/+SAQWQ1hFfjCI
Ac50+fqXAgMBAAECggEAESF271dT29B9MiZGdaf2BSTdRdUh8p7VJjCmcsJC2d5g
jzNoVEfN3p8NpCk9RiRxVYU8Vu0A5c/z2h23I3y3m45R8P+Ogao/oMAcBjyEefjT
1htNaTyuIyMJZmbaBeH80DSjmu2FYJ34mAWOSf0156SAzHX6osVZLgqn0Zj1VhsO
qGzSB3PAWQhRD7wUmHGChIqMw3nMibJFD8vAgwkUeonZDAeE1slDqfs1sHlnIAi/
hMTUV1R4aEZPjqTu64WsPKFB5lE2+kBp1n5FjsqBEA7Yc8dq9Up33kWNN8qwAlk3
UbYgEICJNFXDXb1nQgGyIjBz4EWax2djzrHfSWPoPQKBgQDlGW43zsqVBVIK0tq9
DyRirY9dE382U4TGFT/jBRh9zfSS/VSTGfWZwdfoCf5zGKxLB+EHGKP0eBFkI6Ns
OTZkj9+A8ya56R5UL4C1B54yoIaoP6OWnFPdCVX+a5Vg/RoOu8a9paOZbidsN2/d
qp5DgDTcvL/DPHDQPhggTztHkwKBgQDhiDXL/OTNiNksVc0mP7HZt1zddBamPpV5
vMUO0e7LoehTehMYfRESO70L3cqgW81ngc3lycziVGKSpOZAVyqQnawM7ClmaOyY
327ky+hn0YTg7PsJIAwmBZNuEY81azY6XL1S6gblXOljg3LxWLGxDdJxb32Yzw5g
/4hAZRAbbQKBgDDBP7Z6uJwMHNCW3NwdK9YgL4FUU99A2OG6xfTPMc4cFW9uoCAK
Bz1ohkUoU1E1L/1ruNTkxoeRQco411+BsXSnNZ5goJ4x4TVzQoXEEEOf6+hEWtED
hcllYjqZtJmGY5Q9G+diJ7XQPm2GQrU+yR1+XEGZnYm0BCIdBM9jbS8nAoGANl9Y
WO2HW+X6Na4Z33bLsel7AgjSzRGZh9fyyjfikkVedYGgyO6eBv9P/GRkTXTUeHfa
4+Na+dXOTfxjZm+dW3An4pWtBXuWTuTPH2nphd4FYxS/ENwhEamWeZrSI9bNJBTn
r+2r6/ASdGNkersfW8gxTBfM9+IzXT894gh7caECf2gwEfKwi9jvGr2uLoA+Msov
UxbbKtFaTO9sfs9Lq06t09t/OMouSFJC9Gos51D/VVvqSVrjjJaTiM2gvL30PndG
0zYI3e/2SE6h5ymFOzArnJtDPBVM4X1i4rWDjLb3P5bBq3Xo+B0YCT+2YtEh9QGx
WlQH2EwIgVZsViJIvl4=
-----END PRIVATE KEY-----
Loading

0 comments on commit 37e74bc

Please sign in to comment.