Skip to content

Commit e26295b

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

File tree

8 files changed

+439
-83
lines changed

8 files changed

+439
-83
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-
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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 scodec.bits.ByteVector
26+
27+
import java.math.BigInteger
28+
import java.security.KeyPairGenerator
29+
import java.security.SecureRandom
30+
import java.util.Date
31+
import sun.security.x509.AlgorithmId
32+
import sun.security.x509.CertificateAlgorithmId
33+
import sun.security.x509.CertificateIssuerName
34+
import sun.security.x509.CertificateSerialNumber
35+
import sun.security.x509.CertificateSubjectName
36+
import sun.security.x509.CertificateValidity
37+
import sun.security.x509.CertificateVersion
38+
import sun.security.x509.CertificateX509Key
39+
import sun.security.x509.X500Name
40+
import sun.security.x509.X509CertImpl
41+
import sun.security.x509.X509CertInfo
42+
43+
/** Platform-specific implementation for JVM and JS platforms that can generate certificates.
44+
*/
45+
private[io] object PlatformSpecificCertificateProvider {
46+
47+
def create: IO[TestCertificateProvider] = IO.pure(new JvmJsCertificateProvider)
48+
49+
private class JvmJsCertificateProvider extends TestCertificateProvider {
50+
51+
def getCertificatePair: IO[TestCertificateProvider.CertificatePair] =
52+
IO(KeyPairGenerator.getInstance("RSA")).flatMap { keyPairGen =>
53+
for {
54+
_ <- IO(keyPairGen.initialize(2048, new SecureRandom()))
55+
keyPair <- IO(keyPairGen.generateKeyPair())
56+
57+
// Create certificate info
58+
certInfo = new X509CertInfo()
59+
60+
// Set validity period (1 year from now)
61+
now = new Date()
62+
expiration = new Date(now.getTime + 365L * 24 * 60 * 60 * 1000)
63+
validity = new CertificateValidity(now, expiration)
64+
_ <- IO(certInfo.set(CertificateValidity.NAME, validity))
65+
66+
// Set serial number
67+
serial = new BigInteger(64, new SecureRandom())
68+
_ <- IO(certInfo.set(CertificateSerialNumber.NAME, new CertificateSerialNumber(serial)))
69+
70+
// Set subject and issuer names
71+
subject = new X500Name("CN=Unknown,O=Unknown,OU=FS2 Tests,L=Unknown,ST=Unknown,C=Unknown")
72+
_ <- IO(certInfo.set(CertificateSubjectName.NAME, new CertificateSubjectName(subject)))
73+
_ <- IO(certInfo.set(CertificateIssuerName.NAME, new CertificateIssuerName(subject)))
74+
75+
// Set public key
76+
publicKey = keyPair.getPublic()
77+
_ <- IO(certInfo.set(CertificateX509Key.NAME, new CertificateX509Key(publicKey)))
78+
79+
// Set version
80+
_ <- IO(
81+
certInfo.set(CertificateVersion.NAME, new CertificateVersion(CertificateVersion.V3))
82+
)
83+
84+
// Set algorithm
85+
algorithm = AlgorithmId.get("SHA256WithRSA")
86+
_ <- IO(certInfo.set(CertificateAlgorithmId.NAME, new CertificateAlgorithmId(algorithm)))
87+
88+
// Sign the certificate
89+
cert = new X509CertImpl(certInfo)
90+
_ <- IO(cert.sign(keyPair.getPrivate(), "SHA256WithRSA"))
91+
92+
// Convert to PEM format
93+
certBytes = cert.getEncoded
94+
certPem = pemEncode("CERTIFICATE", certBytes)
95+
96+
// Convert private key to PEM format
97+
privateKeyBytes = keyPair.getPrivate().getEncoded()
98+
keyPem = pemEncode("PRIVATE KEY", privateKeyBytes)
99+
100+
} yield TestCertificateProvider.CertificatePair(
101+
certificate = ByteVector.view(certBytes),
102+
privateKey = ByteVector.view(privateKeyBytes),
103+
certificateString = certPem,
104+
privateKeyString = keyPem
105+
)
106+
}
107+
108+
/** Encodes binary data in PEM format
109+
*/
110+
private def pemEncode(header: String, data: Array[Byte]): String = {
111+
val base64 = java.util.Base64.getEncoder.encodeToString(data)
112+
val lines = base64.grouped(64).toList
113+
s"""-----BEGIN $header-----
114+
|${lines.mkString("\n")}
115+
|-----END $header-----""".stripMargin
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)