Skip to content

Commit c718b27

Browse files
Generate ephemeral certificates for TLS tests (#3611)
Signed-off-by: guptapratykshh <[email protected]>
1 parent 77396ff commit c718b27

File tree

13 files changed

+456
-100
lines changed

13 files changed

+456
-100
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright (c) 2013 Functional Streams for Scala
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
5+
* this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to
7+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8+
* the Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16+
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18+
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package fs2.io
23+
24+
import cats.effect.IO
25+
import fs2.io.file.Files
26+
import scodec.bits.ByteVector
27+
import scala.concurrent.duration._
28+
29+
/** Platform-specific implementation for JS platform using OpenSSL.
30+
*/
31+
private[io] object PlatformSpecificCertificateProvider {
32+
33+
def create: IO[TestCertificateProvider] = IO.pure(new JsCertificateProvider)
34+
35+
private class JsCertificateProvider extends TestCertificateProvider {
36+
37+
def getCertificatePair: IO[TestCertificateProvider.CertificatePair] =
38+
Files[IO].tempDirectory.use { tempDir =>
39+
val certPath = tempDir / "cert.pem"
40+
val keyPath = tempDir / "key.pem"
41+
42+
val cmd = List(
43+
"openssl",
44+
"req",
45+
"-x509",
46+
"-newkey",
47+
"rsa:2048",
48+
"-keyout",
49+
keyPath.toString,
50+
"-out",
51+
certPath.toString,
52+
"-days",
53+
"365",
54+
"-nodes",
55+
"-subj",
56+
"/CN=localhost/O=FS2 Tests",
57+
"-addext",
58+
"subjectAltName=DNS:localhost,IP:127.0.0.1",
59+
"-sha256"
60+
)
61+
62+
def run(cmd: List[String]): IO[Unit] =
63+
fs2.io.process.ProcessBuilder(cmd.head, cmd.tail: _*).spawn[IO].use { p =>
64+
p.exitValue.flatMap {
65+
case 0 => IO.unit
66+
case n =>
67+
IO.raiseError(new RuntimeException(s"Command ${cmd.head} failed with exit code $n"))
68+
}
69+
}
70+
71+
for {
72+
_ <- run(cmd)
73+
_ <- IO.sleep(1.second)
74+
cert <- Files[IO].readAll(certPath).compile.to(ByteVector)
75+
key <- Files[IO].readAll(keyPath).compile.to(ByteVector)
76+
certString <- Files[IO].readAll(certPath).through(fs2.text.utf8.decode).compile.string
77+
keyString <- Files[IO].readAll(keyPath).through(fs2.text.utf8.decode).compile.string
78+
} yield TestCertificateProvider.CertificatePair(
79+
certificate = cert,
80+
privateKey = key,
81+
certificateString = certString,
82+
privateKeyString = keyString
83+
)
84+
}
85+
}
86+
}

io/js/src/test/scala/fs2/io/net/tls/TLSSuite.scala

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,43 +26,30 @@ package tls
2626

2727
import cats.effect.IO
2828
import cats.syntax.all._
29-
import fs2.io.file.Files
30-
import fs2.io.file.Path
31-
32-
import scala.scalajs.js
3329

3430
abstract class TLSSuite extends Fs2Suite {
3531

3632
def testTlsContext(
3733
privateKey: Boolean,
3834
version: Option[SecureContext.SecureVersion] = None
39-
): IO[TLSContext[IO]] = Files[IO]
40-
.readAll(Path("io/shared/src/test/resources/keystore.json"))
41-
.through(text.utf8.decode)
42-
.compile
43-
.string
44-
.flatMap(s => IO(js.JSON.parse(s).asInstanceOf[js.Dictionary[CertKey]]("server")))
45-
.map { certKey =>
35+
): IO[TLSContext[IO]] = TestCertificateProvider.getCachedProvider.flatMap { provider =>
36+
provider.getCertificatePair.map { certPair =>
4637
Network[IO].tlsContext.fromSecureContext(
4738
SecureContext(
4839
minVersion = version,
4940
maxVersion = version,
50-
ca = List(certKey.cert.asRight).some,
51-
cert = List(certKey.cert.asRight).some,
41+
ca = List(certPair.certificateString.asRight).some,
42+
cert = List(certPair.certificateString.asRight).some,
5243
key =
53-
if (privateKey) List(SecureContext.Key(certKey.key.asRight, "password".some)).some
44+
if (privateKey)
45+
List(SecureContext.Key(certPair.privateKeyString.asRight, "password".some)).some
5446
else None
5547
)
5648
)
5749
}
50+
}
5851

5952
val logger = TLSLogger.Disabled
6053
// val logger = TLSLogger.Enabled(msg => IO(println(s"\u001b[33m${msg}\u001b[0m")))
6154

6255
}
63-
64-
@js.native
65-
trait CertKey extends js.Object {
66-
def cert: String = js.native
67-
def key: String = js.native
68-
}

io/jvm/src/test/scala/fs2/io/IoPlatformSuite.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,8 @@ class IoPlatformSuite extends Fs2Suite {
275275
bar.assertEquals("bar")
276276
}
277277
test("classloader") {
278-
val size = readClassLoaderResource[IO]("keystore.jks", 8192).as(1L).compile.foldMonoid
279-
size.assertEquals(2591L)
278+
val size = readClassLoaderResource[IO]("fs2/io/foo", 8192).as(1L).compile.foldMonoid
279+
size.assertEquals(3L)
280280
}
281281
}
282282
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright (c) 2013 Functional Streams for Scala
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
5+
* this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to
7+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8+
* the Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16+
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18+
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package fs2.io
23+
24+
import cats.effect.IO
25+
import fs2.io.file.Files
26+
import scodec.bits.ByteVector
27+
import scala.concurrent.duration._
28+
29+
/** Platform-specific implementation for JVM platform using OpenSSL.
30+
*/
31+
private[io] object PlatformSpecificCertificateProvider {
32+
33+
def create: IO[TestCertificateProvider] = IO.pure(new JvmCertificateProvider)
34+
35+
private class JvmCertificateProvider extends TestCertificateProvider {
36+
37+
def getCertificatePair: IO[TestCertificateProvider.CertificatePair] =
38+
Files[IO].tempDirectory.use { tempDir =>
39+
val certPath = tempDir / "cert.pem"
40+
val keyPath = tempDir / "key.pem"
41+
42+
val cmd = List(
43+
"openssl",
44+
"req",
45+
"-x509",
46+
"-newkey",
47+
"rsa:2048",
48+
"-keyout",
49+
keyPath.toString,
50+
"-out",
51+
certPath.toString,
52+
"-days",
53+
"365",
54+
"-nodes",
55+
"-subj",
56+
"/CN=localhost/O=FS2 Tests",
57+
"-addext",
58+
"subjectAltName=DNS:localhost,IP:127.0.0.1",
59+
"-sha256"
60+
)
61+
62+
def run(cmd: List[String]): IO[Unit] =
63+
fs2.io.process.ProcessBuilder(cmd.head, cmd.tail: _*).spawn[IO].use { p =>
64+
for {
65+
out <- p.stdout.through(fs2.text.utf8.decode).compile.string
66+
err <- p.stderr.through(fs2.text.utf8.decode).compile.string
67+
exitCode <- p.exitValue
68+
_ <-
69+
if (exitCode == 0) IO.unit
70+
else
71+
IO.raiseError(
72+
new RuntimeException(
73+
s"Command ${cmd.mkString(" ")} failed with exit code $exitCode\nStdout: $out\nStderr: $err"
74+
)
75+
)
76+
} yield ()
77+
}
78+
79+
for {
80+
_ <- run(cmd)
81+
_ <- IO.sleep(1.second)
82+
cert <- Files[IO].readAll(certPath).compile.to(ByteVector)
83+
key <- Files[IO].readAll(keyPath).compile.to(ByteVector)
84+
certString <- Files[IO].readAll(certPath).through(fs2.text.utf8.decode).compile.string
85+
keyString <- Files[IO].readAll(keyPath).through(fs2.text.utf8.decode).compile.string
86+
} yield TestCertificateProvider.CertificatePair(
87+
certificate = cert,
88+
privateKey = key,
89+
certificateString = certString,
90+
privateKeyString = keyString
91+
)
92+
}
93+
}
94+
}

io/jvm/src/test/scala/fs2/io/net/tls/TLSSuite.scala

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,36 @@ package tls
2727
import cats.effect.IO
2828

2929
abstract class TLSSuite extends Fs2Suite {
30-
def testTlsContext: IO[TLSContext[IO]] =
31-
Network[IO].tlsContext
32-
.fromKeyStoreResource(
33-
"keystore.jks",
34-
"password".toCharArray,
35-
"password".toCharArray
36-
)
30+
def testTlsContext: IO[TLSContext[IO]] = TestCertificateProvider.getCachedProvider.flatMap {
31+
provider =>
32+
provider.getCertificatePair.flatMap { certPair =>
33+
IO.blocking {
34+
val keyStore = java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType)
35+
keyStore.load(null, null)
36+
37+
val certFactory = java.security.cert.CertificateFactory.getInstance("X.509")
38+
val cert = certFactory.generateCertificate(
39+
new java.io.ByteArrayInputStream(certPair.certificate.toArray)
40+
)
41+
42+
val keyFactory = java.security.KeyFactory.getInstance("RSA")
43+
val keyPem = certPair.privateKeyString
44+
.replaceAll("-----BEGIN (.*)-----", "")
45+
.replaceAll("-----END (.*)-----", "")
46+
.replaceAll("\\s", "")
47+
val keyBytes = java.util.Base64.getDecoder.decode(keyPem)
48+
val keySpec = new java.security.spec.PKCS8EncodedKeySpec(keyBytes)
49+
val key = keyFactory.generatePrivate(keySpec)
50+
51+
keyStore.setKeyEntry("alias", key, "password".toCharArray, Array(cert))
52+
keyStore.setCertificateEntry("ca", cert)
53+
54+
keyStore
55+
}.flatMap { ks =>
56+
Network[IO].tlsContext.fromKeyStore(ks, "password".toCharArray)
57+
}
58+
}
59+
}
3760

3861
val logger = TLSLogger.Disabled
3962
// val logger = TLSLogger.Enabled(msg => IO(println(s"\u001b[33m${msg}\u001b[0m")))

0 commit comments

Comments
 (0)