diff --git a/io/js/src/test/scala/fs2/io/net/tls/TLSSuite.scala b/io/js/src/test/scala/fs2/io/net/tls/TLSSuite.scala index 5b1f195329..f41752aadb 100644 --- a/io/js/src/test/scala/fs2/io/net/tls/TLSSuite.scala +++ b/io/js/src/test/scala/fs2/io/net/tls/TLSSuite.scala @@ -26,31 +26,23 @@ package tls import cats.effect.IO import cats.syntax.all._ -import fs2.io.file.Files -import fs2.io.file.Path - -import scala.scalajs.js abstract class TLSSuite extends Fs2Suite { def testTlsContext( privateKey: Boolean, version: Option[SecureContext.SecureVersion] = None - ): IO[TLSContext[IO]] = Files[IO] - .readAll(Path("io/shared/src/test/resources/keystore.json")) - .through(text.utf8.decode) - .compile - .string - .flatMap(s => IO(js.JSON.parse(s).asInstanceOf[js.Dictionary[CertKey]]("server"))) - .map { certKey => + ): IO[TLSContext[IO]] = + TestCertificateProvider.getCertificateAndPrivateKey.map { certPair => Network[IO].tlsContext.fromSecureContext( SecureContext( minVersion = version, maxVersion = version, - ca = List(certKey.cert.asRight).some, - cert = List(certKey.cert.asRight).some, + ca = List(certPair.certificatePem.asRight).some, + cert = List(certPair.certificatePem.asRight).some, key = - if (privateKey) List(SecureContext.Key(certKey.key.asRight, "password".some)).some + if (privateKey) + List(SecureContext.Key(certPair.privateKeyPem.asRight, None)).some else None ) ) @@ -60,9 +52,3 @@ abstract class TLSSuite extends Fs2Suite { // val logger = TLSLogger.Enabled(msg => IO(println(s"\u001b[33m${msg}\u001b[0m"))) } - -@js.native -trait CertKey extends js.Object { - def cert: String = js.native - def key: String = js.native -} diff --git a/io/jvm/src/test/scala/fs2/io/IoPlatformSuite.scala b/io/jvm/src/test/scala/fs2/io/IoPlatformSuite.scala index c5bc17b561..983832e95d 100644 --- a/io/jvm/src/test/scala/fs2/io/IoPlatformSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/IoPlatformSuite.scala @@ -275,8 +275,8 @@ class IoPlatformSuite extends Fs2Suite { bar.assertEquals("bar") } test("classloader") { - val size = readClassLoaderResource[IO]("keystore.jks", 8192).as(1L).compile.foldMonoid - size.assertEquals(2591L) + val size = readClassLoaderResource[IO]("fs2/io/foo", 8192).as(1L).compile.foldMonoid + size.assertEquals(3L) } } } diff --git a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSuite.scala b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSuite.scala index ea3d10a18d..8cbcc229ff 100644 --- a/io/jvm/src/test/scala/fs2/io/net/tls/TLSSuite.scala +++ b/io/jvm/src/test/scala/fs2/io/net/tls/TLSSuite.scala @@ -28,12 +28,35 @@ import cats.effect.IO abstract class TLSSuite extends Fs2Suite { def testTlsContext: IO[TLSContext[IO]] = - Network[IO].tlsContext - .fromKeyStoreResource( - "keystore.jks", - "password".toCharArray, - "password".toCharArray - ) + TestCertificateProvider.getCertificateAndPrivateKey.flatMap { certPair => + IO.blocking { + val keyStore = java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType) + keyStore.load(null, null) + + val certFactory = java.security.cert.CertificateFactory.getInstance("X.509") + val cert = certFactory.generateCertificate( + new java.io.ByteArrayInputStream( + certPair.certificatePem.getBytes(java.nio.charset.StandardCharsets.UTF_8) + ) + ) + + val keyFactory = java.security.KeyFactory.getInstance("RSA") + val keyPem = certPair.privateKeyPem + .replaceAll("-----BEGIN (.*)-----", "") + .replaceAll("-----END (.*)-----", "") + .replaceAll("\\s", "") + val keyBytes = java.util.Base64.getDecoder.decode(keyPem) + val keySpec = new java.security.spec.PKCS8EncodedKeySpec(keyBytes) + val key = keyFactory.generatePrivate(keySpec) + + keyStore.setKeyEntry("alias", key, "password".toCharArray, Array(cert)) + keyStore.setCertificateEntry("ca", cert) + + keyStore + }.flatMap { ks => + Network[IO].tlsContext.fromKeyStore(ks, "password".toCharArray) + } + } val logger = TLSLogger.Disabled // val logger = TLSLogger.Enabled(msg => IO(println(s"\u001b[33m${msg}\u001b[0m"))) diff --git a/io/native/src/main/scala/fs2/io/net/tls/CertChainAndKey.scala b/io/native/src/main/scala/fs2/io/net/tls/CertChainAndKey.scala index 9c3c7d3bdd..0bc9e850bf 100644 --- a/io/native/src/main/scala/fs2/io/net/tls/CertChainAndKey.scala +++ b/io/native/src/main/scala/fs2/io/net/tls/CertChainAndKey.scala @@ -55,4 +55,10 @@ final class CertChainAndKey private (chainPem: ByteVector, privateKeyPem: ByteVe object CertChainAndKey { def apply(chainPem: ByteVector, privateKeyPem: ByteVector): CertChainAndKey = new CertChainAndKey(chainPem, privateKeyPem) + + def apply(chainPem: String, privateKeyPem: String): CertChainAndKey = + new CertChainAndKey(bytesFromPemString(chainPem), bytesFromPemString(privateKeyPem)) + + private def bytesFromPemString(s: String): ByteVector = + ByteVector.view(s.getBytes(java.nio.charset.StandardCharsets.UTF_8)) } diff --git a/io/native/src/test/scala/fs2/io/net/tls/TLSSuite.scala b/io/native/src/test/scala/fs2/io/net/tls/TLSSuite.scala index fce760199e..d618838a87 100644 --- a/io/native/src/test/scala/fs2/io/net/tls/TLSSuite.scala +++ b/io/native/src/test/scala/fs2/io/net/tls/TLSSuite.scala @@ -26,30 +26,22 @@ package tls import cats.effect.IO import cats.effect.kernel.Resource -import fs2.io.file.Files -import fs2.io.file.Path -import scodec.bits.ByteVector abstract class TLSSuite extends Fs2Suite { def testTlsContext: Resource[IO, TLSContext[IO]] = for { - cert <- Resource.eval { - Files[IO].readAll(Path("io/shared/src/test/resources/cert.pem")).compile.to(ByteVector) - } - key <- Resource.eval { - Files[IO].readAll(Path("io/shared/src/test/resources/key.pem")).compile.to(ByteVector) - } + certPair <- Resource.eval(TestCertificateProvider.getCertificateAndPrivateKey) cfg <- S2nConfig.builder - .withCertChainAndKeysToStore(List(CertChainAndKey(cert, key))) - .withPemsToTrustStore(List(cert.decodeAscii.toOption.get)) + .withCertChainAndKeysToStore( + List(CertChainAndKey(certPair.certificatePem, certPair.privateKeyPem)) + ) + .withPemsToTrustStore(List(certPair.certificatePem)) .build[IO] } yield Network[IO].tlsContext.fromS2nConfig(cfg) def testClientTlsContext: Resource[IO, TLSContext[IO]] = for { - cert <- Resource.eval { - Files[IO].readAll(Path("io/shared/src/test/resources/cert.pem")).compile.to(ByteVector) - } + certPair <- Resource.eval(TestCertificateProvider.getCertificateAndPrivateKey) cfg <- S2nConfig.builder - .withPemsToTrustStore(List(cert.decodeAscii.toOption.get)) + .withPemsToTrustStore(List(certPair.certificatePem)) .build[IO] } yield Network[IO].tlsContext.fromS2nConfig(cfg) diff --git a/io/shared/src/test/resources/cert.pem b/io/shared/src/test/resources/cert.pem deleted file mode 100644 index 7515c37d5f..0000000000 --- a/io/shared/src/test/resources/cert.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDfTCCAmWgAwIBAgIEW6AYvzANBgkqhkiG9w0BAQsFADBuMRAwDgYDVQQGEwdV -bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD -VQQKEwdVbmtub3duMRIwEAYDVQQLEwlGUzIgVGVzdHMxEDAOBgNVBAMTB1Vua25v -d24wIBcNMTkxMjAyMTMyOTM3WhgPMjExODA2MjYxMzI5MzdaMG4xEDAOBgNVBAYT -B1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vua25vd24xEDAO -BgNVBAoTB1Vua25vd24xEjAQBgNVBAsTCUZTMiBUZXN0czEQMA4GA1UEAxMHVW5r -bm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN4RczREVYw7GQF4 -E/dhwmFiGKi5G+0cGcyPQzv1Say3U6V8Haqpn0/aeVVY30T6/Nttu+0kGKv9uQlY -yeHeyPzqdjkRhSVoDCLahAxWSK8cV/B4r9SyynLxsDIoHAzkE0mGcBfsZ1Hpxe6/ -tLZ9SMZunpuQBMLKlE6sELgeY5Iz4ZptlFADr8SIqxhrnc3GYFWCubu3A7d6h5K1 -XMfXn+itYmb/y6TGr/8T5YjipvQuJFPv0WNahErZzOkDM/r4mp1BNKDWawJuyh3/ -6TudzkJUxK2eQowE6TWOeIB9WioMC0YlGUgylZe2vq9AWJkTjPq3duXZlj+Hqnqq -ZOQvWBcCAwEAAaMhMB8wHQYDVR0OBBYEFMEuQ/pix1rrPmvAh/KqSfxFLKfhMA0G -CSqGSIb3DQEBCwUAA4IBAQDX/Nr2FuVhXPkZRrFFVrzqIbNO3ROnkURXXJSXus5v -nvmQOXWHtE5Uy1f6z1iKCYUs6l+M5+YLmxooTZvaoAC6tjPDskIyPGQGVG4MB03E -ahLSBaRGJEZXIHLU9s6lgK0YoZCnhHdD+TXalxS4Jv6ieZJCKbWpkQomYYPWyTC6 -lR5XSEBPhPNRzo0wjla9a6th+Zc3KnTzTsQHg65IU0DIQeAIuxG0v3xh4NOmGi2Z -LX5q0qhf9uk88HUwocILO9TLshlTPAF4puf0vS4MICw46g4YonDz7k5VQmzy0ZAV -c6ew1WF0PfBk/3o4F0plkj5nbem57iOU0znKfI0ZYXoR ------END CERTIFICATE----- diff --git a/io/shared/src/test/resources/key.pem b/io/shared/src/test/resources/key.pem deleted file mode 100644 index 7096c07b66..0000000000 --- a/io/shared/src/test/resources/key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDeEXM0RFWMOxkB -eBP3YcJhYhiouRvtHBnMj0M79Umst1OlfB2qqZ9P2nlVWN9E+vzbbbvtJBir/bkJ -WMnh3sj86nY5EYUlaAwi2oQMVkivHFfweK/Usspy8bAyKBwM5BNJhnAX7GdR6cXu -v7S2fUjGbp6bkATCypROrBC4HmOSM+GabZRQA6/EiKsYa53NxmBVgrm7twO3eoeS -tVzH15/orWJm/8ukxq//E+WI4qb0LiRT79FjWoRK2czpAzP6+JqdQTSg1msCbsod -/+k7nc5CVMStnkKMBOk1jniAfVoqDAtGJRlIMpWXtr6vQFiZE4z6t3bl2ZY/h6p6 -qmTkL1gXAgMBAAECggEBAMJ6v8zvZ4hXHVAnDD1jlStaEMR60NU3/fQjJzu0VqB3 -MT9FUmnrAUWazRYMrgQoVxgIo0NMkHrXypw/8RXp2VV+NKlICbY3yCEiA/EWA7Ov -++fymfKJ3jkKJ0fVzrMPb0C+Bx88f0PCmwC7TZVgZUK7EBam6zR426eGk2Hb41He -kcUG0Q68modQO172m3bPqNsa32bsjvAZ813KLSs8xFHhJajTjX5kXG+PmN1fenTx -8cvzOAET62O4XEXsYqD+iENyvqkRkysfqa9uzWyhLiip6NZXJHPuNKi9usVL+ZNj -glJXFMP0K/TFYqgMBnoJF3Y60NMBlTpTyDu1kP0/nCkCgYEA+g/cxpz+1yPEIs+y -cX4qEp2VJpj4HuJdZKingtApg1aSAlAcNf9UZl7EsGz3vf/x/hX6YtTLF9cbyuIR -P+CYEevWJwiLjTjMbd+Z86oeYr/sLqz4+98W3RGTAcRfLyEeM/fc7MEwTSeyIhpZ -Gi+HbxGJMvbj+8tUh6KFd9lDu8MCgYEA41dpfo2MnyA9f2OrEC2Joll/9Kgz+3l9 -jZIRQ4YID3Rawi9xLi9Z6aiIudAGFT9hcFU11akIVz6yXvhRGmE6gQ6bim5ebaJe -69EHchuBl9Pszi7/UUS4cxksVnNwJjrAHXnoESU0DTWRh3AUkBEsKOd0DkSGOma+ -MEI+JDe2cR0CgYEA9Jgfc4aNHxM0/nf6K1kk/iB1i9OEn3D7uUHe1+2VLYq4Ntr1 -PTwK6jc4XPm5Onfn1Ija6WELZr5ZyRFnnfupw53TU0rgdbpg+/gDNnvoTN89vkoj -IPsN+h7+lHPoRsk2Kc8AofQ1ssJpU0JCdYKYDuQwN1GXnus8O4+Uza4OutECgYAr -IeCABDcT0bgZPT2tWhZs2PIv5uHF6mzpuTbRStKoq/i0MvAURSOX80PNjSw6R8Yi -2+fU27cbZmfNIOuyR5Qj/DOCdiIwRsgfkY8KFTHnLmwVSlFih9k+7R2+YTR77FWa -whBHgHl5sBomSht8oeVw9UjNlC6rUebvnQHROUjB+QKBgQDj1g/t+B5lZ03Pc00h -NQxMqTR6wAp3hPX5Wen44+o0ktGkArRP3Iw3x5764EgUudn2p1NibCrjCnj2avCi -Gkkbl0wWcFZiOsvmBL/0ngxdPA5bfwiKKeIavltWufXXHl1fs0C4UIu9fHWpV6z7 -NRVO87Wp3BK13bCGUD0DvAdtpA== ------END PRIVATE KEY----- diff --git a/io/shared/src/test/resources/keystore.jks b/io/shared/src/test/resources/keystore.jks deleted file mode 100644 index d2b70c37fd..0000000000 Binary files a/io/shared/src/test/resources/keystore.jks and /dev/null differ diff --git a/io/shared/src/test/resources/keystore.json b/io/shared/src/test/resources/keystore.json deleted file mode 100644 index c6064a8e2e..0000000000 --- a/io/shared/src/test/resources/keystore.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "server": { - "key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDeEXM0RFWMOxkB\neBP3YcJhYhiouRvtHBnMj0M79Umst1OlfB2qqZ9P2nlVWN9E+vzbbbvtJBir/bkJ\nWMnh3sj86nY5EYUlaAwi2oQMVkivHFfweK/Usspy8bAyKBwM5BNJhnAX7GdR6cXu\nv7S2fUjGbp6bkATCypROrBC4HmOSM+GabZRQA6/EiKsYa53NxmBVgrm7twO3eoeS\ntVzH15/orWJm/8ukxq//E+WI4qb0LiRT79FjWoRK2czpAzP6+JqdQTSg1msCbsod\n/+k7nc5CVMStnkKMBOk1jniAfVoqDAtGJRlIMpWXtr6vQFiZE4z6t3bl2ZY/h6p6\nqmTkL1gXAgMBAAECggEBAMJ6v8zvZ4hXHVAnDD1jlStaEMR60NU3/fQjJzu0VqB3\nMT9FUmnrAUWazRYMrgQoVxgIo0NMkHrXypw/8RXp2VV+NKlICbY3yCEiA/EWA7Ov\n++fymfKJ3jkKJ0fVzrMPb0C+Bx88f0PCmwC7TZVgZUK7EBam6zR426eGk2Hb41He\nkcUG0Q68modQO172m3bPqNsa32bsjvAZ813KLSs8xFHhJajTjX5kXG+PmN1fenTx\n8cvzOAET62O4XEXsYqD+iENyvqkRkysfqa9uzWyhLiip6NZXJHPuNKi9usVL+ZNj\nglJXFMP0K/TFYqgMBnoJF3Y60NMBlTpTyDu1kP0/nCkCgYEA+g/cxpz+1yPEIs+y\ncX4qEp2VJpj4HuJdZKingtApg1aSAlAcNf9UZl7EsGz3vf/x/hX6YtTLF9cbyuIR\nP+CYEevWJwiLjTjMbd+Z86oeYr/sLqz4+98W3RGTAcRfLyEeM/fc7MEwTSeyIhpZ\nGi+HbxGJMvbj+8tUh6KFd9lDu8MCgYEA41dpfo2MnyA9f2OrEC2Joll/9Kgz+3l9\njZIRQ4YID3Rawi9xLi9Z6aiIudAGFT9hcFU11akIVz6yXvhRGmE6gQ6bim5ebaJe\n69EHchuBl9Pszi7/UUS4cxksVnNwJjrAHXnoESU0DTWRh3AUkBEsKOd0DkSGOma+\nMEI+JDe2cR0CgYEA9Jgfc4aNHxM0/nf6K1kk/iB1i9OEn3D7uUHe1+2VLYq4Ntr1\nPTwK6jc4XPm5Onfn1Ija6WELZr5ZyRFnnfupw53TU0rgdbpg+/gDNnvoTN89vkoj\nIPsN+h7+lHPoRsk2Kc8AofQ1ssJpU0JCdYKYDuQwN1GXnus8O4+Uza4OutECgYAr\nIeCABDcT0bgZPT2tWhZs2PIv5uHF6mzpuTbRStKoq/i0MvAURSOX80PNjSw6R8Yi\n2+fU27cbZmfNIOuyR5Qj/DOCdiIwRsgfkY8KFTHnLmwVSlFih9k+7R2+YTR77FWa\nwhBHgHl5sBomSht8oeVw9UjNlC6rUebvnQHROUjB+QKBgQDj1g/t+B5lZ03Pc00h\nNQxMqTR6wAp3hPX5Wen44+o0ktGkArRP3Iw3x5764EgUudn2p1NibCrjCnj2avCi\nGkkbl0wWcFZiOsvmBL/0ngxdPA5bfwiKKeIavltWufXXHl1fs0C4UIu9fHWpV6z7\nNRVO87Wp3BK13bCGUD0DvAdtpA==\n-----END PRIVATE KEY-----\n", - "cert": "-----BEGIN CERTIFICATE-----\nMIIDfTCCAmWgAwIBAgIEW6AYvzANBgkqhkiG9w0BAQsFADBuMRAwDgYDVQQGEwdV\nbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD\nVQQKEwdVbmtub3duMRIwEAYDVQQLEwlGUzIgVGVzdHMxEDAOBgNVBAMTB1Vua25v\nd24wIBcNMTkxMjAyMTMyOTM3WhgPMjExODA2MjYxMzI5MzdaMG4xEDAOBgNVBAYT\nB1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vua25vd24xEDAO\nBgNVBAoTB1Vua25vd24xEjAQBgNVBAsTCUZTMiBUZXN0czEQMA4GA1UEAxMHVW5r\nbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN4RczREVYw7GQF4\nE/dhwmFiGKi5G+0cGcyPQzv1Say3U6V8Haqpn0/aeVVY30T6/Nttu+0kGKv9uQlY\nyeHeyPzqdjkRhSVoDCLahAxWSK8cV/B4r9SyynLxsDIoHAzkE0mGcBfsZ1Hpxe6/\ntLZ9SMZunpuQBMLKlE6sELgeY5Iz4ZptlFADr8SIqxhrnc3GYFWCubu3A7d6h5K1\nXMfXn+itYmb/y6TGr/8T5YjipvQuJFPv0WNahErZzOkDM/r4mp1BNKDWawJuyh3/\n6TudzkJUxK2eQowE6TWOeIB9WioMC0YlGUgylZe2vq9AWJkTjPq3duXZlj+Hqnqq\nZOQvWBcCAwEAAaMhMB8wHQYDVR0OBBYEFMEuQ/pix1rrPmvAh/KqSfxFLKfhMA0G\nCSqGSIb3DQEBCwUAA4IBAQDX/Nr2FuVhXPkZRrFFVrzqIbNO3ROnkURXXJSXus5v\nnvmQOXWHtE5Uy1f6z1iKCYUs6l+M5+YLmxooTZvaoAC6tjPDskIyPGQGVG4MB03E\nahLSBaRGJEZXIHLU9s6lgK0YoZCnhHdD+TXalxS4Jv6ieZJCKbWpkQomYYPWyTC6\nlR5XSEBPhPNRzo0wjla9a6th+Zc3KnTzTsQHg65IU0DIQeAIuxG0v3xh4NOmGi2Z\nLX5q0qhf9uk88HUwocILO9TLshlTPAF4puf0vS4MICw46g4YonDz7k5VQmzy0ZAV\nc6ew1WF0PfBk/3o4F0plkj5nbem57iOU0znKfI0ZYXoR\n-----END CERTIFICATE-----\n" - } -} \ No newline at end of file diff --git a/io/shared/src/test/scala/fs2/io/TestCertificateProvider.scala b/io/shared/src/test/scala/fs2/io/TestCertificateProvider.scala new file mode 100644 index 0000000000..e042b77e15 --- /dev/null +++ b/io/shared/src/test/scala/fs2/io/TestCertificateProvider.scala @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2.io + +import cats.effect.{IO, Ref, SyncIO} +import cats.effect.std.Mutex +import fs2.Stream +import fs2.io.file.Files + +object TestCertificateProvider { + + /** Represents a self-signed certificate and private key for testing. + * Both values are PEM encoded. + */ + case class CertificateAndPrivateKey( + certificatePem: String, + privateKeyPem: String + ) + + private val mutex: Mutex[IO] = Mutex.in[SyncIO, IO].unsafeRunSync() + private val cache: Ref[IO, Option[CertificateAndPrivateKey]] = Ref.unsafe(None) + + /** Returns a cached certificate and private key, generating it if necessary. + * The generation happens once per test suite execution using a self-signed certificate. + */ + def getCertificateAndPrivateKey: IO[CertificateAndPrivateKey] = + mutex.lock.surround { + cache.get.flatMap { + case Some(c) => IO.pure(c) + case None => generateCertificateAndPrivateKey.flatTap(c => cache.set(Some(c))) + } + } + + private def generateCertificateAndPrivateKey: IO[CertificateAndPrivateKey] = + Files[IO].tempDirectory.use { tempDir => + val certPath = tempDir / "cert.pem" + val keyPath = tempDir / "key.pem" + val configPath = tempDir / "openssl.cnf" + + val config = """[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_req +prompt = no + +[req_distinguished_name] +CN = localhost +O = FS2 Tests + +[v3_req] +subjectAltName = DNS:localhost,IP:127.0.0.1,DNS:Unknown +basicConstraints = critical,CA:TRUE +keyUsage = digitalSignature,keyEncipherment,keyCertSign +extendedKeyUsage = serverAuth,clientAuth +""" + + val cmd = List( + "openssl", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + keyPath.toString, + "-out", + certPath.toString, + "-days", + "365", + "-nodes", + "-config", + configPath.toString, + "-sha256" + ) + + def run(cmd: List[String]): IO[Unit] = + fs2.io.process.ProcessBuilder(cmd.head, cmd.tail: _*).spawn[IO].use { p => + for { + out <- p.stdout.through(fs2.text.utf8.decode).compile.string + err <- p.stderr.through(fs2.text.utf8.decode).compile.string + exitCode <- p.exitValue + _ <- + if (exitCode == 0) IO.unit + else + IO.raiseError( + new RuntimeException( + s"Command ${cmd.mkString(" ")} failed with exit code $exitCode\nStdout: $out\nStderr: $err" + ) + ) + } yield () + } + + for { + _ <- Stream(config).through(Files[IO].writeUtf8Lines(configPath)).compile.drain + _ <- run(cmd) + certString <- Files[IO].readUtf8(certPath).compile.string + keyString <- Files[IO].readUtf8(keyPath).compile.string + } yield CertificateAndPrivateKey( + certString, + keyString + ) + } +}