ssl.ssl.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otoroshi_2.12 Show documentation
Show all versions of otoroshi_2.12 Show documentation
Lightweight api management on top of a modern http reverse proxy
The newest version!
package otoroshi.ssl
import java.io._
import java.lang.reflect.Field
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.nio.charset.StandardCharsets.US_ASCII
import java.security._
import java.security.cert._
import java.security.spec.{KeySpec, PKCS8EncodedKeySpec}
import java.util.concurrent.{Executors, TimeUnit}
import java.util.concurrent.atomic.{AtomicBoolean, AtomicLong, AtomicReference}
import java.util.regex.Pattern.CASE_INSENSITIVE
import java.util.regex.{Matcher, Pattern}
import java.util.{Base64, Date}
import otoroshi.actions.ApiAction
import akka.http.scaladsl.util.FastFuture
import akka.stream.{Materializer, TLSClientAuth}
import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.util.ByteString
import com.github.blemale.scaffeine.Scaffeine
import com.google.common.hash.Hashing
import com.typesafe.sslconfig.ssl.SSLConfigSettings
import otoroshi.metrics.{FakeHasMetrics, HasMetrics}
import otoroshi.env.Env
import otoroshi.events.{Alerts, CertAlmostExpiredAlert, CertExpiredAlert, CertRenewalAlert}
import otoroshi.gateway.Errors
import javax.crypto.Cipher.DECRYPT_MODE
import javax.crypto.spec.PBEKeySpec
import javax.crypto.{Cipher, EncryptedPrivateKeyInfo, SecretKey, SecretKeyFactory}
import javax.net.ssl._
import otoroshi.models._
import org.apache.commons.codec.binary.Hex
import org.apache.commons.codec.digest.DigestUtils
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.x509.{ExtendedKeyUsage, KeyPurposeId}
import org.bouncycastle.openssl.jcajce.{
JcaPEMKeyConverter,
JceOpenSSLPKCS8DecryptorProviderBuilder,
JcePEMDecryptorProviderBuilder
}
import org.bouncycastle.openssl.{PEMEncryptedKeyPair, PEMKeyPair, PEMParser}
import org.bouncycastle.operator.DefaultAlgorithmNameFinder
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.util.io.pem.PemReader
import org.joda.time.{DateTime, Interval}
import otoroshi.api.OtoroshiEnvHolder
import otoroshi.ssl.pki.models.{GenCertResponse, GenCsrQuery, GenKeyPairQuery}
import otoroshi.utils.letsencrypt.LetsEncryptHelper
import otoroshi.utils.{RegexPool, TypedMap}
import play.api.libs.json._
import play.api.libs.ws.WSProxyServer
import play.api.mvc._
import play.api.{Configuration, Logger}
import play.core.ApplicationProvider
import play.server.api.SSLEngineProvider
import redis.RedisClientMasterSlaves
import otoroshi.security.IdGenerator
import otoroshi.storage.{BasicStore, RedisLike, RedisLikeStore}
import otoroshi.utils.cache.types.UnboundedTrieMap
import otoroshi.utils.http.DN
import otoroshi.utils.syntax.implicits._
import scala.collection.concurrent.TrieMap
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
import scala.concurrent.duration._
import otoroshi.utils.syntax.implicits._
import java.nio.file.Files
import java.util
import java.util.function.BiFunction
/**
* git over http works with otoroshi
* ssh and other => http tunneling like https://github.com/mathieuancelin/node-httptunnel or https://github.com/larsbrinkhoff/httptunnel or https://github.com/luizluca/bridge
*/
sealed trait ClientAuth {
def name: String
def toAkkaClientAuth: TLSClientAuth
}
case object ClientAuthNone extends ClientAuth {
def name: String = "None"
def toAkkaClientAuth: TLSClientAuth = TLSClientAuth.None
}
case object ClientAuthWant extends ClientAuth {
def name: String = "Want"
def toAkkaClientAuth: TLSClientAuth = TLSClientAuth.Want
}
case object ClientAuthNeed extends ClientAuth {
def name: String = "Need"
def toAkkaClientAuth: TLSClientAuth = TLSClientAuth.Need
}
object ClientAuth {
val None = ClientAuthNone
val Want = ClientAuthWant
val Need = ClientAuthNeed
def values: Seq[ClientAuth] = Seq(None, Want, Need)
def apply(name: String): Option[ClientAuth] = {
name.toLowerCase match {
case "None" => Some(None)
case "none" => Some(None)
case "Want" => Some(Want)
case "want" => Some(Want)
case "Need" => Some(Need)
case "need" => Some(Need)
case _ => scala.None
}
}
}
case class OCSPCertProjection(
revoked: Boolean,
valid: Boolean,
expired: Boolean,
revocationReason: String,
from: Date,
to: Date
)
case class Cert(
id: String,
name: String,
description: String,
chain: String,
privateKey: String,
caRef: Option[String],
domain: String = "--",
selfSigned: Boolean = false,
ca: Boolean = false,
valid: Boolean = false,
exposed: Boolean = false,
revoked: Boolean,
autoRenew: Boolean = false,
letsEncrypt: Boolean = false,
client: Boolean = false,
keypair: Boolean = false,
subject: String = "--",
from: DateTime = DateTime.now(),
to: DateTime = DateTime.now(),
sans: Seq[String] = Seq.empty,
entityMetadata: Map[String, String] = Map.empty,
tags: Seq[String] = Seq.empty,
password: Option[String] = None,
location: otoroshi.models.EntityLocation = otoroshi.models.EntityLocation()
) extends otoroshi.models.EntityLocationSupport {
def json: JsValue = toJson
def internalId: String = id
def theDescription: String = description
def theMetadata: Map[String, String] = entityMetadata
def theName: String = name
def theTags: Seq[String] = tags
lazy val cleanChain: String = {
val matcher: Matcher = DynamicSSLEngineProvider.CERT_PATTERN.matcher(chain)
var certificates = Seq.empty[String]
var start = 0
while ({ matcher.find(start) }) {
certificates = certificates :+ matcher.group(1)
start = matcher.end
}
certificates
.map(c => s"${PemHeaders.BeginCertificate}\n$c${PemHeaders.EndCertificate}")
.flatMap(_.split("\\n"))
.filterNot(_.trim.isEmpty)
.mkString("\n")
}
lazy val certType = {
if (client) "client"
else if (ca) "ca"
else if (letsEncrypt) "letsEncrypt"
else if (keypair) "keypair"
else if (selfSigned) "selfSigned"
else "certificate"
}
lazy val notRevoked: Boolean = !revoked
lazy val cacheKey: String = s"$id###$contentHash"
lazy val contentHash: String = Hashing.sha256().hashString(s"$chain:$privateKey", StandardCharsets.UTF_8).toString
lazy val bundle: String = s"${privateKey}\n\n${chain}\n"
lazy val allDomains: Seq[String] = {
val enriched = enrich()
(Seq(enriched.domain) ++ enriched.sans).filter(_.trim.nonEmpty).filterNot(_ == "--").distinct
}
def signature: Option[String] = this.metadata.map(v => (v \ "signature").as[String])
def serialNumber: Option[String] = this.metadata.map(v => (v \ "serialNumber").as[String])
def serialNumberLng: Option[java.math.BigInteger] =
this.metadata.map(v => (v \ "serialNumberLng").as[java.math.BigInteger])
def matchesDomain(dom: String): Boolean =
allDomains.exists(d => sanMatchesDomain(dom, d)) // allDomains.exists(d => RegexPool.apply(d).matches(dom))
def sanMatchesDomain(dom: String, san: String): Boolean = {
if (san.startsWith("*.")) {
dom.endsWith(san.substring(1)) && dom.split("\\.").tail.mkString(".") == san.substring(2)
// RegexPool.apply(san).matches(dom)
} else {
dom == san
}
}
def renew(
_duration: Option[FiniteDuration] = None
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Cert] = {
import SSLImplicits._
val duration = _duration.getOrElse(FiniteDuration(365, TimeUnit.DAYS))
this match {
case original if original.letsEncrypt => LetsEncryptHelper.renew(this)
case _ => {
env.datastores.certificatesDataStore.findAll().map { certificates =>
val cas = certificates.filter(cert => cert.ca)
caRef.flatMap(ref => cas.find(_.id == ref)) match {
case None if ca =>
val resp = FakeKeyStore
.createCA(subject, duration, Some(cryptoKeyPair), certificate.map(_.getSerialNumber.longValue()))
copy(chain = resp.cert.asPem, privateKey = resp.key.asPem).enrich()
case None if selfSigned =>
val resp = FakeKeyStore.createSelfSignedCertificate(
domain,
duration,
Some(cryptoKeyPair),
certificate.map(_.getSerialNumber.longValue())
)
copy(chain = resp.cert.asPem, privateKey = resp.key.asPem).enrich()
case None if keypair =>
val resp = FakeKeyStore.createSelfSignedCertificate(
domain,
duration,
Some(cryptoKeyPair),
certificate.map(_.getSerialNumber.longValue())
)
copy(chain = resp.cert.asPem, privateKey = resp.key.asPem).enrich()
case None => // should not happens
val resp = FakeKeyStore.createSelfSignedCertificate(
domain,
duration,
Some(cryptoKeyPair),
certificate.map(_.getSerialNumber.longValue())
)
copy(chain = resp.cert.asPem, privateKey = resp.key.asPem).enrich()
case Some(caCert) if ca =>
val resp = FakeKeyStore.createSubCa(
domain,
duration,
Some(cryptoKeyPair),
certificate.map(_.getSerialNumber.longValue()),
caCert.certificate.get,
caCert.certificates.tail,
caCert.cryptoKeyPair
)
copy(chain = resp.cert.asPem + "\n" + caCert.chain, privateKey = resp.key.asPem).enrich()
case Some(caCert) =>
val resp = FakeKeyStore.createCertificateFromCA(
domain,
duration,
Some(cryptoKeyPair),
certificate.map(_.getSerialNumber.longValue()),
caCert.certificate.get,
caCert.certificates.tail,
caCert.cryptoKeyPair
)
copy(chain = resp.cert.asPem + "\n" + caCert.chain, privateKey = resp.key.asPem).enrich()
case _ =>
// println("wait what ???")
val resp = FakeKeyStore.createSelfSignedCertificate(
domain,
duration,
Some(cryptoKeyPair),
certificate.map(_.getSerialNumber.longValue())
)
copy(chain = resp.cert.asPem, privateKey = resp.key.asPem).enrich()
}
}
}
}
}
// def password: Option[String] = None
def save()(implicit ec: ExecutionContext, env: Env) = {
val current = this.enrich()
env.datastores.certificatesDataStore.set(current)
}
lazy val notExpired: Boolean = from.isBefore(org.joda.time.DateTime.now()) && to.isAfter(org.joda.time.DateTime.now())
def notExpiredAt(date: DateTime): Boolean = from.isBefore(date) && to.isAfter(date)
lazy val notExpiredSoon: Boolean = notExpiredAt(to.minusDays(15))
lazy val expired: Boolean = !notExpired
def enrich() = {
val meta = this.metadata.get
this.copy(
domain = (meta \ "domain").asOpt[String].getOrElse("--"),
selfSigned = (meta \ "selfSigned").asOpt[Boolean].getOrElse(false),
ca = (meta \ "ca").asOpt[Boolean].getOrElse(false),
// client = (meta \ "client").asOpt[Boolean].getOrElse(false),
valid = this.isValid,
subject = (meta \ "subjectDN").as[String],
from = (meta \ "notBefore").asOpt[Long].map(v => new DateTime(v)).getOrElse(DateTime.now()),
to = (meta \ "notAfter").asOpt[Long].map(v => new DateTime(v)).getOrElse(DateTime.now()),
sans = (meta \ "subAltNames").asOpt[Seq[String]].getOrElse(Seq.empty)
)
}
def delete()(implicit ec: ExecutionContext, env: Env) = env.datastores.certificatesDataStore.delete(this)
def exists()(implicit ec: ExecutionContext, env: Env) = env.datastores.certificatesDataStore.exists(this)
def toJson = Cert.toJson(this)
lazy val isUsable = notRevoked && notExpired && isValid
lazy val certificatesRaw: Seq[String] = Try {
cleanChain
.split(PemHeaders.BeginCertificate)
.toSeq
.tail
.map(_.replace(PemHeaders.EndCertificate, "").trim())
.map(c => s"${PemHeaders.BeginCertificate}\n$c\n${PemHeaders.EndCertificate}")
}.toOption.toSeq.flatten
lazy val certificates: Seq[X509Certificate] = certificatesChain.toSeq /*{
val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
certificatesRaw
.map(
content =>
Try(
certificateFactory
.generateCertificate(new ByteArrayInputStream(DynamicSSLEngineProvider.base64Decode(content)))
.asInstanceOf[X509Certificate]
)
)
.collect {
case Success(cert) => cert
}
}*/
lazy val certificatesChain: Array[X509Certificate] = { //certificates.toArray
Try {
cleanChain
.split(PemHeaders.BeginCertificate)
.toSeq
.map(_.trim)
.filterNot(_.isEmpty)
.map { content =>
content.replace(PemHeaders.BeginCertificate, "").replace(PemHeaders.EndCertificate, "")
}
.map { content =>
val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
certificateFactory
.generateCertificate(new ByteArrayInputStream(DynamicSSLEngineProvider.base64Decode(content)))
.asInstanceOf[X509Certificate]
}
.toArray
}.getOrElse(Array.empty)
}
lazy val certificate: Option[X509Certificate] = Try {
cleanChain.split(PemHeaders.BeginCertificate).toSeq.tail.headOption.map { cert =>
val content: String = cert.replace(PemHeaders.EndCertificate, "")
val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
certificateFactory
.generateCertificate(new ByteArrayInputStream(DynamicSSLEngineProvider.base64Decode(content)))
.asInstanceOf[X509Certificate]
}
}.toOption.flatten
lazy val caFromChain: Option[X509Certificate] = Try {
cleanChain.split(PemHeaders.BeginCertificate).toSeq.tail.lastOption.map { cert =>
val content: String = cert.replace(PemHeaders.EndCertificate, "")
val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
certificateFactory
.generateCertificate(new ByteArrayInputStream(DynamicSSLEngineProvider.base64Decode(content)))
.asInstanceOf[X509Certificate]
}
}.toOption.flatten
lazy val metadata: Option[JsValue] = {
cleanChain.split(PemHeaders.BeginCertificate).toSeq.tail.headOption.map { cert =>
val content: String = cert.replace(PemHeaders.EndCertificate, "")
CertificateData(content)
}
}
lazy val isValid: Boolean = {
Try {
val keyStore: KeyStore = KeyStore.getInstance("JKS")
keyStore.load(null, null)
DynamicSSLEngineProvider.readPrivateKeyUniversal(this.id, this.privateKey, this.password, false).toOption.exists {
key: PrivateKey =>
// val key: PrivateKey = DynamicSSLEngineProvider.readPrivateKey(encodedKeySpec) /*Try(KeyFactory.getInstance("RSA")).map(_.generatePrivate(encodedKeySpec))
// .orElse(Try(KeyFactory.getInstance("EC")).map(_.generatePrivate(encodedKeySpec)))
// .orElse(Try(KeyFactory.getInstance("DSA")).map(_.generatePrivate(encodedKeySpec)))
// .orElse(Try(KeyFactory.getInstance("DiffieHellman")).map(_.generatePrivate(encodedKeySpec)))
// .get*/
val certificateChain: Seq[X509Certificate] =
DynamicSSLEngineProvider.readCertificateChain(this.id, this.cleanChain, false)
if (certificateChain.isEmpty) {
DynamicSSLEngineProvider.logger.error(s"[${this.id}] Certificate file does not contain any certificates :(")
false
} else {
keyStore.setKeyEntry(
this.id,
key,
this.password.getOrElse("").toCharArray,
certificateChain.toArray[java.security.cert.Certificate]
)
true
}
}
} recover { case e =>
DynamicSSLEngineProvider.logger.error(s"Error while checking certificate validity (${name})")
false
} getOrElse false
}
lazy val cryptoKeyPair: KeyPair = {
val privkey = DynamicSSLEngineProvider.readPrivateKeyUniversal(domain, privateKey, password).right.get
//val privkey: PrivateKey = DynamicSSLEngineProvider.readPrivateKey(privkeySpec) /*Try(KeyFactory.getInstance("RSA"))
// .orElse(Try(KeyFactory.getInstance("DSA")))
// .map(_.generatePrivate(privkeySpec))
// .get
val pubkey: PublicKey = certificate.get.getPublicKey
new KeyPair(pubkey, privkey)
}
def toGenCertResponse(implicit env: Env): GenCertResponse = {
val query = GenCsrQuery(
hosts = Seq(domain),
subject = Some(subject)
)
GenCertResponse(
serial = serialNumberLng.get,
cert = certificate.get,
csr = Await
.result(env.pki.genCsr(query, None)(env.otoroshiExecutionContext), 10.seconds)
.right
.get
.csr,
csrQuery = query.some,
key = cryptoKeyPair.getPrivate,
ca = caFromChain.get,
caChain = certificates.tail
)
}
}
object Cert {
import SSLImplicits._
val OtoroshiCaDN = s"CN=Otoroshi Default Root CA Certificate, OU=Otoroshi Certificates, O=Otoroshi"
val OtoroshiCA = "otoroshi-root-ca"
val OtoroshiIntermediateCaDN =
s"CN=Otoroshi Default Intermediate CA Certificate, OU=Otoroshi Certificates, O=Otoroshi"
val OtoroshiIntermediateCA = "otoroshi-intermediate-ca"
val OtoroshiJwtSigningDn = s"CN=Otoroshi Default Jwt Signing Keypair, OU=Otoroshi Certificates, O=Otoroshi"
val OtoroshiJwtSigning = "otoroshi-jwt-signing"
val OtoroshiWildcard = "otoroshi-wildcard"
val OtoroshiClientDn = s"CN=Otoroshi Default Client Certificate, OU=Otoroshi Certificates, O=Otoroshi"
val OtoroshiClient = "otoroshi-client"
lazy val logger = Logger("otoroshi-cert")
def apply(name: String, cert: String, privateKey: String): Cert = {
Cert(
id = IdGenerator.token(32),
name = name,
description = name,
chain = cert,
privateKey = privateKey,
caRef = None,
autoRenew = false,
client = false,
exposed = false,
revoked = false
).enrich()
}
def apply(cert: X509Certificate, keyPair: KeyPair, caRef: Option[String], client: Boolean): Cert = {
val c = Cert(
id = IdGenerator.token(32),
name = "none",
description = "none",
chain = cert.asPem,
privateKey = keyPair.getPrivate.asPem,
caRef = caRef,
autoRenew = false,
client = client,
exposed = false,
revoked = false
).enrich()
c.copy(name = c.domain, description = s"Certificate for ${c.subject}")
}
def apply(cert: X509Certificate, keyPair: KeyPair, ca: Cert, client: Boolean): Cert = {
val c = Cert(
id = IdGenerator.token(32),
name = "none",
description = "none",
chain = cert.asPem + "\n" + ca.chain,
privateKey = keyPair.getPrivate.asPem,
caRef = Some(ca.id),
autoRenew = false,
client = client,
exposed = false,
revoked = false
).enrich()
c.copy(name = c.domain, description = s"Certificate for ${c.subject}")
}
def apply(cert: X509Certificate, keyPair: KeyPair, ca: X509Certificate, client: Boolean): Cert = {
val c = Cert(
id = IdGenerator.token(32),
name = "none",
description = "none",
chain = cert.asPem + "\n" + ca.asPem,
//s"${PemHeaders.BeginCertificate}\n${Base64.getEncoder
//.encodeToString(cert.getEncoded)}\n${PemHeaders.EndCertificate}\n${PemHeaders.BeginCertificate}\n${Base64.getEncoder
//.encodeToString(ca.getEncoded)}\n${PemHeaders.EndCertificate}\n",
privateKey = keyPair.getPrivate.asPem,
// s"${PemHeaders.BeginPrivateKey}\n${Base64.getEncoder.encodeToString(keyPair.getPrivate.getEncoded)}\n${PemHeaders.EndPrivateKey}",
caRef = None,
autoRenew = false,
client = client,
exposed = false,
revoked = false
).enrich()
c.copy(name = c.domain, description = s"Certificate for ${c.subject}")
}
val _fmt: Format[Cert] = new Format[Cert] {
override def writes(cert: Cert): JsValue =
cert.location.jsonWithKey ++ Json.obj(
"id" -> cert.id,
"domain" -> cert.domain,
"name" -> cert.name,
"description" -> cert.description,
"chain" -> cert.chain,
"caRef" -> cert.caRef,
"privateKey" -> cert.privateKey,
"selfSigned" -> cert.selfSigned,
"ca" -> cert.ca,
"valid" -> cert.valid,
"exposed" -> cert.exposed,
"revoked" -> cert.revoked,
"autoRenew" -> cert.autoRenew,
"letsEncrypt" -> cert.letsEncrypt,
"subject" -> cert.subject,
"from" -> cert.from.getMillis,
"to" -> cert.to.getMillis,
"client" -> cert.client,
"keypair" -> cert.keypair,
"sans" -> JsArray(cert.sans.map(JsString.apply)),
"certType" -> cert.certType,
"password" -> cert.password,
"metadata" -> cert.entityMetadata,
"tags" -> JsArray(cert.tags.map(JsString.apply))
)
override def reads(json: JsValue): JsResult[Cert] =
Try {
Cert(
location = otoroshi.models.EntityLocation.readFromKey(json),
id = (json \ "id").as[String],
name = (json \ "name").asOpt[String].orElse((json \ "domain").asOpt[String]).getOrElse("none"),
description = (json \ "description")
.asOpt[String]
.orElse((json \ "domain").asOpt[String].map(v => s"Certificate for $v"))
.getOrElse("none"),
domain = (json \ "domain").as[String],
sans = (json \ "sans").asOpt[Seq[String]].getOrElse(Seq.empty),
chain = (json \ "chain").as[String],
caRef = (json \ "caRef").asOpt[String],
password = (json \ "password").asOpt[String].filter(_.trim.nonEmpty),
privateKey = (json \ "privateKey").asOpt[String].getOrElse(""),
selfSigned = (json \ "selfSigned").asOpt[Boolean].getOrElse(false),
ca = (json \ "ca").asOpt[Boolean].getOrElse(false),
client = (json \ "client").asOpt[Boolean].getOrElse(false),
keypair = (json \ "keypair").asOpt[Boolean].getOrElse(false),
valid = (json \ "valid").asOpt[Boolean].getOrElse(false),
exposed = (json \ "exposed").asOpt[Boolean].getOrElse(false),
revoked = (json \ "revoked").asOpt[Boolean].getOrElse(false),
autoRenew = (json \ "autoRenew").asOpt[Boolean].getOrElse(false),
letsEncrypt = (json \ "letsEncrypt").asOpt[Boolean].getOrElse(false),
subject = (json \ "subject").asOpt[String].getOrElse("--"),
from = (json \ "from").asOpt[Long].map(v => new DateTime(v)).getOrElse(DateTime.now()),
to = (json \ "to").asOpt[Long].map(v => new DateTime(v)).getOrElse(DateTime.now()),
entityMetadata = (json \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty),
tags = (json \ "tags").asOpt[Seq[String]].getOrElse(Seq.empty[String])
)
} map { case sd =>
JsSuccess(sd)
} recover { case t =>
logger.error("Error while reading Cert", t)
JsError(t.getMessage)
} get
}
def toJson(value: Cert): JsValue = _fmt.writes(value)
def fromJsons(value: JsValue): Cert =
try {
_fmt.reads(value).get
} catch {
case e: Throwable => {
logger.error(s"Try to deserialize ${Json.prettyPrint(value)}")
throw e
}
}
def fromJsonSafe(value: JsValue): JsResult[Cert] = _fmt.reads(value)
def createFromServices()(implicit ec: ExecutionContext, env: Env, mat: Materializer): Future[Unit] = {
env.datastores.certificatesDataStore.findAll().flatMap { certificates =>
env.datastores.serviceDescriptorDataStore.findAll().flatMap { services =>
val certs = certificates.filterNot(_.letsEncrypt)
val letsEncryptServicesHosts = services
.filter(_.issueCert)
.flatMap(s => s.allHosts.map(h => (s, h)))
.filterNot(s => certs.exists(c => RegexPool(c.domain).matches(s._2)))
Source(letsEncryptServicesHosts.toList)
.mapAsync(1) { case (service, host) =>
env.datastores.rawDataStore.get(s"${env.storageRoot}:certs-issuer:local:create:$host").flatMap {
case Some(_) =>
logger.warn(s"Certificate already in creating process: $host")
FastFuture.successful(())
case None => {
env.datastores.rawDataStore
.set(
s"${env.storageRoot}:certs-issuer:local:create:$host",
ByteString("true"),
Some(1.minutes.toMillis)
)
.flatMap { _ =>
val cert = certs.find(c => RegexPool(c.domain).matches(host)).get
if (cert.autoRenew) {
cert.renew()
} else {
FastFuture.successful(cert)
}
}
.andThen { case _ =>
env.datastores.rawDataStore.del(Seq(s"${env.storageRoot}:certs-issuer:local:create:$host"))
}
}
}
}
.map {
case (host, Left(err)) => logger.error(s"Error while creating certificate for $host. $err")
case (host, Right(_)) => logger.info(s"Successfully created certificate for $host")
}
.runWith(Sink.ignore)
.map(_ => ())
}
}
}
}
trait CertificateDataStore extends BasicStore[Cert] {
def syncTemplate(env: Env): Cert = {
Cert(
id = IdGenerator.namedId("cert", env),
name = "a new certificate",
description = "a new certificate",
chain = "",
privateKey = "",
caRef = None,
revoked = false
)
}
def nakedTemplate(env: Env): Future[Cert] = {
val defaultCert = syncTemplate(env)
env.datastores.globalConfigDataStore
.latest()(env.otoroshiExecutionContext, env)
.templates
.certificate
.map { template =>
Cert._fmt.reads(defaultCert.json.asObject.deepMerge(template)).get
}
.getOrElse {
defaultCert
}
.vfuture
}
def template(implicit ec: ExecutionContext, env: Env): Future[Cert] = {
nakedTemplate(env)
// env.pki
// .genSelfSignedCert(
// GenCsrQuery(
// hosts = Seq("www.oto.tools"),
// subject = Some("C=FR, OU=Foo, O=Bar")
// )
// )
// .map { c =>
// c.toOption.get.toCert.copy(
// id = IdGenerator.namedId("cert", env),
// name = "a new certificate",
// description = "a new certificate",
// chain = "",
// privateKey = ""
// )
// }
}
def renewCertificates()(implicit ec: ExecutionContext, env: Env, mat: Materializer): Future[Unit] = {
def willBeInvalidSoon(cert: Cert): Boolean = {
val enriched = cert.enrich()
val globalInterval = new Interval(enriched.from, enriched.to)
if (enriched.to.isBefore(DateTime.now())) {
false
} else {
val nowInterval = new Interval(DateTime.now(), enriched.to)
val percentage: Long = (nowInterval.toDurationMillis * 100) / globalInterval.toDurationMillis
percentage < 20
}
}
def renewCAs(certificates: Seq[Cert]): Future[Unit] = {
val renewableCas = certificates
.filter(_.notRevoked)
.filter(_.autoRenew)
.filter(cert => cert.ca)
.filter(willBeInvalidSoon)
.filterNot(c =>
c.entityMetadata.get("untilExpiration").contains("true") || c.name.startsWith("[UNTIL EXPIRATION] ")
)
Source(renewableCas.toList)
.mapAsync(1) { case c =>
c.renew()
.flatMap(d =>
c.copy(
id = IdGenerator.token,
name = "[UNTIL EXPIRATION] " + c.name,
entityMetadata = c.entityMetadata ++ Map(
"untilExpiration" -> "true",
"nextCertificate" -> c.id
)
).save()
.map(_ => d)
)
.flatMap(c => c.save().map(_ => c))
}
.map { c =>
Alerts.send(
CertRenewalAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
c
)
)
}
.runWith(Sink.ignore)
.map(_ => ())
}
def renewNonCaCertificates(certificates: Seq[Cert]): Future[Unit] = {
val renewableCertificates = certificates
.filter(_.notRevoked)
.filter(_.autoRenew)
.filterNot(_.ca)
.filter(willBeInvalidSoon) // TODO: fix
.filterNot(c =>
c.entityMetadata.get("untilExpiration").contains("true") || c.name.startsWith("[UNTIL EXPIRATION] ")
)
Source(renewableCertificates.toList)
.mapAsync(1) { case c =>
c.renew()
.flatMap(d =>
c.copy(
id = IdGenerator.token,
name = "[UNTIL EXPIRATION] " + c.name,
entityMetadata = c.entityMetadata ++ Map(
"untilExpiration" -> "true",
"nextCertificate" -> c.id
)
).save()
.map(_ => d)
)
.flatMap(c => c.save().map(_ => c))
}
.map { c =>
Alerts.send(
CertRenewalAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
c
)
)
}
.runWith(Sink.ignore)
.map(_ => ())
}
def markExpiredCertsAsExpired(certificates: Seq[Cert]): Future[Unit] = {
val expiredWithFlagCertificates = certificates
.map(_.enrich())
.filterNot(_.entityMetadata.get("expired").contains("true"))
val expiredCertificates = certificates
.map(_.enrich())
.filter(_.notRevoked)
.filterNot { cert =>
cert.from.isBefore(org.joda.time.DateTime.now()) && cert.to.isAfter(org.joda.time.DateTime.now())
}
for {
_ <- Source(expiredCertificates.toList)
.mapAsync(1) {
case c if c.entityMetadata.get("expired").contains("true") || c.name.startsWith("[EXPIRED] ") =>
c.applyOn(d => d.save().map(_ => d))
case c if !(c.entityMetadata.get("expired").contains("true") || c.name.startsWith("[EXPIRED] ")) =>
c.copy(name = "[EXPIRED] " + c.name, entityMetadata = c.entityMetadata ++ Map("expired" -> "true"))
.applyOn(d => d.save().map(_ => d))
}
.map { c =>
Alerts.send(
CertExpiredAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
c
)
)
}
.runWith(Sink.ignore)
.map(_ => ())
_ <- Source(expiredWithFlagCertificates.toList)
.filter { cert =>
cert.from.isBefore(org.joda.time.DateTime.now()) && cert.to.isAfter(org.joda.time.DateTime.now())
}
.mapAsync(1) { cert =>
cert
.copy(name = cert.name.replace("[EXPIRED] ", ""), entityMetadata = cert.entityMetadata - "expired")
.save()
}
.runWith(Sink.ignore)
.map(_ => ())
} yield ()
}
def alertAlmostExpiredCerts(certificates: Seq[Cert]): Future[Unit] = {
val almostExpiredCertificates = certificates
.filter(_.notRevoked)
.filterNot(_.ca)
.filter(willBeInvalidSoon)
.filterNot(c =>
c.entityMetadata.get("untilExpiration").contains("true") || c.name.startsWith("[UNTIL EXPIRATION] ")
)
almostExpiredCertificates.foreach { c =>
Alerts.send(
CertAlmostExpiredAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
c
)
)
}
().future
}
(for {
certificates <- findAll()
_ <- markExpiredCertsAsExpired(certificates)
_ <- renewCAs(certificates)
ncertificates <- findAll()
_ <- renewNonCaCertificates(ncertificates)
nncertificates <- findAll()
_ <- alertAlmostExpiredCerts(nncertificates)
} yield ()).andThen { case Failure(e) =>
DynamicSSLEngineProvider.logger.error("error during the certificate renew job", e)
}
}
def readCertOrKey(conf: Configuration, path: String, env: Env): Option[String] = {
conf.getOptionalWithFileSupport[String](path).flatMap { cacert =>
if (
(cacert.contains(PemHeaders.BeginCertificate) && cacert.contains(PemHeaders.EndCertificate)) ||
(cacert.contains(PemHeaders.BeginPrivateKey) && cacert.contains(PemHeaders.EndPrivateKey)) ||
(cacert.contains(PemHeaders.BeginPrivateECKey) && cacert.contains(PemHeaders.EndPrivateECKey)) ||
(cacert.contains(PemHeaders.BeginPrivateRSAKey) && cacert.contains(PemHeaders.EndPrivateRSAKey))
) {
Some(cacert)
} else {
val file = new File(cacert)
if (file.exists()) {
val content = new String(java.nio.file.Files.readAllBytes(file.toPath))
if (
(content.contains(PemHeaders.BeginCertificate) && content.contains(PemHeaders.EndCertificate)) ||
(content.contains(PemHeaders.BeginPrivateKey) && content.contains(PemHeaders.EndPrivateKey)) ||
(content.contains(PemHeaders.BeginPrivateECKey) && content.contains(PemHeaders.EndPrivateECKey)) ||
(content.contains(PemHeaders.BeginPrivateRSAKey) && content.contains(PemHeaders.EndPrivateRSAKey))
) {
Some(content)
} else {
None
}
} else {
None
}
}
}
}
def importOneCert(
name: String,
conf: Configuration,
caPath: String,
certPath: String,
keyPath: String,
logger: Logger,
id: Option[String] = None,
importCa: Boolean
)(implicit
env: Env,
ec: ExecutionContext
): Unit = {
if (importCa) {
readCertOrKey(conf, caPath, env).foreach { cacert =>
val _cert = Cert(
id = IdGenerator.uuid,
name = "none",
description = "none",
chain = cacert,
privateKey = "",
caRef = None,
ca = true,
client = false,
exposed = false,
revoked = false
).enrich()
val cert = _cert.copy(name = _cert.domain, description = s"Certificate for ${_cert.subject}")
findAll().map { certs =>
val found = certs
.map(_.enrich())
.exists(c =>
(c.signature.isDefined && c.signature == cert.signature) && (c.serialNumber.isDefined && c.serialNumber == cert.serialNumber)
)
if (!found) {
cert.save()(ec, env).andThen {
case Success(e) => logger.info(s"successful import of ${name} !")
case Failure(e) => logger.error(s"error while storing ${name} ...", e)
}
} else {
logger.info(s"${name} already exists, canceling import")
}
}
}
}
for {
caContent <- readCertOrKey(conf, caPath, env).orElse(Some(""))
certContent <- readCertOrKey(conf, certPath, env)
keyContent <- readCertOrKey(conf, keyPath, env)
} yield {
logger.info(s"importing ${name} ...")
val _cert = Cert(
id = IdGenerator.uuid,
name = "none",
description = "none",
chain = certContent + s"\n${caContent}",
privateKey = keyContent,
caRef = None,
client = false,
exposed = false,
revoked = false
).enrich()
val cert = _cert.copy(name = _cert.domain, description = s"Certificate for ${_cert.subject}")
findAll().map { certs =>
val found = certs
.map(_.enrich())
.exists(c =>
(c.signature.isDefined && c.signature == cert.signature) && (c.serialNumber.isDefined && c.serialNumber == cert.serialNumber)
)
if (!found) {
cert.save()(ec, env).andThen {
case Success(e) => logger.info(s"successful import of ${name} !")
case Failure(e) => logger.error(s"error while storing ${name} ...", e)
}
} else {
logger.info(s"${name} already exists, canceling import")
}
}
}
}
def importInitialCerts(logger: Logger)(implicit env: Env, ec: ExecutionContext) = {
importOneCert(
"root CA certificate",
env.configuration,
"otoroshi.ssl.rootCa.ca",
"otoroshi.ssl.rootCa.cert",
"otoroshi.ssl.rootCa.key",
logger,
Some(Cert.OtoroshiCA),
env.configuration.getOptional[Boolean]("otoroshi.ssl.rootCa.importCa").getOrElse(false)
)(env, ec)
importOneCert(
"initial certificate",
env.configuration,
"otoroshi.ssl.initialCacert",
"otoroshi.ssl.initialCert",
"otoroshi.ssl.initialCertKey",
logger,
None,
env.configuration.getOptional[Boolean]("otoroshi.ssl.initialCertImportCa").getOrElse(false)
)(env, ec)
env.configuration
.getOptionalWithFileSupport[Seq[Configuration]]("otoroshi.ssl.initialCerts")
.getOrElse(Seq.empty[Configuration])
.zipWithIndex
.foreach { case (conf, idx) =>
importOneCert(
s"initial certificate ${idx}",
conf,
"ca",
"cert",
"key",
logger,
None,
conf.getOptional[Boolean]("importCa").getOrElse(false)
)(env, ec)
}
}
def hasInitialCerts()(implicit env: Env, ec: ExecutionContext): Boolean = {
val hasInitialCert =
env.configuration.betterHas("otoroshi.ssl.initialCert") &&
env.configuration.betterHas("otoroshi.ssl.initialCertKey")
val hasInitialCerts = env.configuration.betterHas("otoroshi.ssl.initialCerts")
val hasRootCA = env.configuration.betterHas("otoroshi.ssl.rootCa.cert") && env.configuration.betterHas(
"otoroshi.ssl.rootCa.key"
)
hasInitialCert || hasInitialCerts || hasRootCA
}
def autoGenerateCertificateForDomain(
domain: String
)(implicit env: Env, ec: ExecutionContext): Future[Option[Cert]] = {
env.datastores.globalConfigDataStore.latestSafe match {
case None => FastFuture.successful(None)
case Some(config) => {
config.autoCert match {
case AutoCert(true, Some(ref), allowed, notAllowed, replyNicely) => {
env.datastores.certificatesDataStore.findById(ref).flatMap {
case None =>
DynamicSSLEngineProvider.logger.error(s"CA cert not found to generate certificate for $domain")
FastFuture.successful(None)
case Some(cert) => {
!notAllowed.exists(p => otoroshi.utils.RegexPool.apply(p).matches(domain)) && allowed
.exists(p => RegexPool.apply(p).matches(domain)) match {
case true => {
env.datastores.certificatesDataStore.findAll().flatMap { certificates =>
certificates.find(c => (c.sans :+ c.domain).contains(domain)) match {
case Some(_) => FastFuture.successful(None)
case _ => {
env.pki
.genCert(
GenCsrQuery(
hosts = Seq(domain),
subject = Some(
s"CN=$domain,OU=Auto Generated Certificates, OU=Otoroshi Certificates, O=Otoroshi"
)
),
cert.certificate.get,
cert.certificates.tail,
cert.cryptoKeyPair.getPrivate
)
.flatMap {
case Left(err) =>
DynamicSSLEngineProvider.logger
.error(s"error while generating certificate for $domain: $err")
FastFuture.successful(None)
case Right(resp) => {
val cert = resp.toCert
.copy(
name = s"Certificate for $domain",
description = s"Auto Generated Certificate for $domain",
autoRenew = true
)
cert.save().map { _ =>
Some(cert)
}
}
}
}
}
}
}
case false if replyNicely => {
env.pki
.genCert(
GenCsrQuery(
hosts = Seq(domain),
subject = Some(SSLSessionJavaHelper.BadDN)
),
cert.certificate.get,
cert.certificates.tail,
cert.cryptoKeyPair.getPrivate
)
.flatMap {
case Left(err) =>
DynamicSSLEngineProvider.logger.error(s"error while generating certificate for $domain: $err")
FastFuture.successful(None)
case Right(resp) => {
val cert = resp.toCert
.copy(
name = s"Certificate for $domain",
description = s"Auto Generated Certificate for $domain",
autoRenew = true
)
FastFuture.successful(Some(cert))
}
}
}
case _ => FastFuture.successful(None)
}
}
}
}
case _ => FastFuture.successful(None)
}
}
}
}
def jautoGenerateCertificateForDomain(domain: String, env: Env): Option[Cert] = {
import scala.concurrent.duration._
Try {
// TODO: blocking ec
implicit val ec = env.otoroshiExecutionContext
implicit val ev = env
// AWAIT: valid
Await.result(env.datastores.certificatesDataStore.autoGenerateCertificateForDomain(domain), 10.seconds)
} match {
case Failure(e) => None
case Success(opt) => opt
}
}
}
object DynamicSSLEngineProvider {
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security
val bouncyCastleProvider = new BouncyCastleProvider()
Security.addProvider(bouncyCastleProvider)
type KeyStoreError = String
private val EMPTY_PASSWORD: Array[Char] = Array.emptyCharArray
val logger = Logger("otoroshi-ssl-provider")
private[ssl] val CERT_PATTERN: Pattern = Pattern.compile(
"-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+" + // Header
"([a-z0-9+/=\\r\\n]+)" + // Base64 text
"-+END\\s+.*CERTIFICATE[^-]*-+", // Footer
CASE_INSENSITIVE
)
val PRIVATE_KEY_PATTERN: Pattern = Pattern.compile(
"-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + "([a-z0-9+/=\\r\\n]+)" + "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+",
CASE_INSENSITIVE
)
val PUBLIC_KEY_PATTERN: Pattern = Pattern.compile(
"-+BEGIN\\s+.*PUBLIC\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + "([a-z0-9+/=\\r\\n]+)" + "-+END\\s+.*PUBLIC\\s+KEY[^-]*-+",
CASE_INSENSITIVE
)
val autogenCerts = Scaffeine().expireAfterWrite(5.minutes).maximumSize(1000).build[String, Cert]()
val _ocspProjectionCertificates = new UnboundedTrieMap[java.math.BigInteger, OCSPCertProjection]()
private def allUnrevokedCertMap: TrieMap[String, Cert] = {
val datastoreCerts = getCurrentEnv().proxyState.allCertificatesMap().filter(_._2.notRevoked)
val genCerts = autogenCerts.asMap()
new UnboundedTrieMap[String, Cert]().++=(datastoreCerts).++=(genCerts)
}
private def allUnrevokedCertSeq: Seq[Cert] = allUnrevokedCertMap.values.toSeq
def certificates: TrieMap[String, Cert] = allUnrevokedCertMap // _certificates.filter(_._2.notRevoked)
private lazy val firstSetupDone = new AtomicBoolean(false)
private lazy val currentKeyManagerServer = new AtomicReference[KeyManager](null)
private lazy val currentTrustManagerServer = new AtomicReference[TrustManager](null)
private lazy val currentContextServer = new AtomicReference[SSLContext](setupContext(FakeHasMetrics, true, Seq.empty))
private lazy val currentContextClient = new AtomicReference[SSLContext](setupContext(FakeHasMetrics, true, Seq.empty))
private lazy val currentSslConfigSettings = new AtomicReference[SSLConfigSettings](null)
private val currentEnv = new AtomicReference[Env](null)
private val defaultSslContext = SSLContext.getDefault
def isFirstSetupDone: Boolean = firstSetupDone.get()
def setCurrentEnv(env: Env): Unit = {
currentEnv.set(env)
}
def getCurrentEnv(): Env = {
currentEnv.get()
}
private def setupContext(env: HasMetrics, includeJdkCa: Boolean, trustedCerts: Seq[String]): SSLContext = {
setupContextAndManagers(env, includeJdkCa, trustedCerts)._1
}
def setupContextAndManagers(
env: HasMetrics,
includeJdkCa: Boolean,
trustedCerts: Seq[String]
): (SSLContext, KeyManager, TrustManager) =
env.metrics.withTimer("otoroshi.core.tls.setup-global-context") {
val certificates = allUnrevokedCertMap // _certificates.filter(_._2.notRevoked)
val trustedCertificates: TrieMap[String, Cert] = if (trustedCerts.nonEmpty) {
new UnboundedTrieMap[String, Cert]() ++ trustedCerts
// .flatMap(k => _certificates.get(k))
.flatMap(k => allUnrevokedCertMap.get(k))
.filter(_.notRevoked)
.map(c => (c.id, c))
.toMap
} else {
allUnrevokedCertMap // _certificates.filter(_._2.notRevoked)
}
// println(s"building context with ${trustedCertificates.size} trusted certificates")
val optEnv = Option(currentEnv.get)
val trustAll: Boolean =
optEnv
.flatMap(e => e.configuration.getOptionalWithFileSupport[Boolean]("otoroshi.ssl.trust.all"))
.getOrElse(false)
val cacertPath = optEnv
.flatMap(e => e.configuration.getOptionalWithFileSupport[String]("otoroshi.ssl.cacert.path"))
.map(path =>
path
.replace("${JAVA_HOME}", System.getProperty("java.home"))
.replace("$JAVA_HOME", System.getProperty("java.home"))
)
.getOrElse(System.getProperty("java.home") + "/lib/security/cacerts")
val cacertPassword = optEnv
.flatMap(e => e.configuration.getOptionalWithFileSupport[String]("otoroshi.ssl.cacert.password"))
.getOrElse("changeit")
val dumpPath: Option[String] =
optEnv.flatMap(e => e.configuration.getOptionalWithFileSupport[String]("play.server.https.keyStoreDumpPath"))
if (logger.isDebugEnabled) logger.debug("Setting up SSL Context ")
val sslContext: SSLContext = SSLContext.getInstance("TLS")
val keyStore: KeyStore = createKeyStore(certificates.values.toSeq) //.filterNot(_.ca))
val trustedKeyStore: KeyStore = createKeyStore(trustedCertificates.values.toSeq) //.filterNot(_.ca))
dumpPath.foreach { path =>
if (logger.isDebugEnabled) logger.debug(s"Dumping keystore at $dumpPath")
keyStore.store(new FileOutputStream(path), EMPTY_PASSWORD)
}
val keyManagerFactory: KeyManagerFactory =
Try(KeyManagerFactory.getInstance("X509")).orElse(Try(KeyManagerFactory.getInstance("SunX509"))).get
keyManagerFactory.init(keyStore, EMPTY_PASSWORD)
val trustedkeyManagerFactory: KeyManagerFactory =
Try(KeyManagerFactory.getInstance("X509")).orElse(Try(KeyManagerFactory.getInstance("SunX509"))).get
trustedkeyManagerFactory.init(trustedKeyStore, EMPTY_PASSWORD)
if (logger.isDebugEnabled) logger.debug("SSL Context init ...")
val keyManagers: Array[KeyManager] = keyManagerFactory.getKeyManagers.map { m =>
KeyManagerCompatibility.keyManager(
() => certificates.values.toSeq,
false,
m.asInstanceOf[X509KeyManager],
optEnv.get
) // new X509KeyManagerSnitch(m.asInstanceOf[X509KeyManager]).asInstanceOf[KeyManager]
}
val tm: Array[TrustManager] =
optEnv
.flatMap(e =>
e.configuration.getOptionalWithFileSupport[Boolean]("play.server.https.trustStore.noCaVerification")
)
.map {
case true => Array[TrustManager](noCATrustManager)
case false if includeJdkCa => createTrustStoreWithJdkCAs(trustedKeyStore, cacertPath, cacertPassword)
case false if !includeJdkCa => createTrustStore(trustedKeyStore)
} getOrElse {
if (trustAll) {
Array[TrustManager](
new VeryNiceTrustManager(Seq.empty[X509TrustManager])
)
} else {
if (includeJdkCa) {
createTrustStoreWithJdkCAs(trustedKeyStore, cacertPath, cacertPassword)
} else {
createTrustStore(trustedKeyStore)
}
}
}
sslContext.init(keyManagers, tm, null)
// dumpPath match {
// case Some(path) => {
// currentSslConfigSettings.set(
// SSLConfigSettings()
// //.withHostnameVerifierClass(classOf[OtoroshiHostnameVerifier])
// .withKeyManagerConfig(
// KeyManagerConfig().withKeyStoreConfigs(
// List(KeyStoreConfig(None, Some(path)).withPassword(Some(String.valueOf(EMPTY_PASSWORD))))
// )
// )
// .withTrustManagerConfig(
// TrustManagerConfig().withTrustStoreConfigs(
// certificates.values.toList.map(c => TrustStoreConfig(Option(c.chain).map(_.trim), None))
// )
// )
// )
// }
// case None => {
// currentSslConfigSettings.set(
// SSLConfigSettings()
// //.withHostnameVerifierClass(classOf[OtoroshiHostnameVerifier])
// .withKeyManagerConfig(
// KeyManagerConfig().withKeyStoreConfigs(
// certificates.values.toList.map(c => KeyStoreConfig(Option(c.chain).map(_.trim), None))
// )
// )
// .withTrustManagerConfig(
// TrustManagerConfig().withTrustStoreConfigs(
// certificates.values.toList.map(c => TrustStoreConfig(Option(c.chain).map(_.trim), None))
// )
// )
// )
// }
// }
if (logger.isDebugEnabled) logger.debug(s"SSL Context init done ! (${keyStore.size()})")
SSLContext.setDefault(sslContext)
(sslContext, keyManagers.head, tm.head)
}
/*
def setupSslContextFor(cert: Cert, env: Env): SSLContext =
env.metrics.withTimer("otoroshi.core.tls.setup-single-context") {
val optEnv = Option(currentEnv.get)
val trustAll: Boolean =
optEnv.flatMap(e => e.configuration.getOptionalWithFileSupport[Boolean]("otoroshi.ssl.trust.all")).getOrElse(false)
val cacertPath = optEnv
.flatMap(e => e.configuration.getOptionalWithFileSupport[String]("otoroshi.ssl.cacert.path"))
.map(
path =>
path
.replace("${JAVA_HOME}", System.getProperty("java.home"))
.replace("$JAVA_HOME", System.getProperty("java.home"))
)
.getOrElse(System.getProperty("java.home") + "/lib/security/cacerts")
val cacertPassword = optEnv
.flatMap(e => e.configuration.getOptionalWithFileSupport[String]("otoroshi.ssl.cacert.password"))
.getOrElse("changeit")
logger.debug("Setting up SSL Context ")
val sslContext: SSLContext = SSLContext.getInstance("TLS")
val keyStore: KeyStore = createKeyStore(Seq(cert))
val keyManagerFactory: KeyManagerFactory =
Try(KeyManagerFactory.getInstance("X509")).orElse(Try(KeyManagerFactory.getInstance("SunX509"))).get
keyManagerFactory.init(keyStore, EMPTY_PASSWORD)
logger.debug("SSL Context init ...")
val keyManagers: Array[KeyManager] = keyManagerFactory.getKeyManagers.map(
m => new X509KeyManagerSnitch(m.asInstanceOf[X509KeyManager]).asInstanceOf[KeyManager]
)
val tm: Array[TrustManager] =
optEnv.flatMap(e => e.configuration.getOptionalWithFileSupport[Boolean]("play.server.https.trustStore.noCaVerification")).map {
case true => Array[TrustManager](noCATrustManager)
case false => createTrustStore(keyStore, cacertPath, cacertPassword)
} getOrElse {
if (trustAll) {
Array[TrustManager](
new VeryNiceTrustManager(Seq.empty[X509TrustManager])
)
} else {
createTrustStore(keyStore, cacertPath, cacertPassword)
}
}
sslContext.init(keyManagers, tm, null)
logger.debug(s"SSL Context init done ! (${keyStore.size()})")
SSLContext.setDefault(sslContext)
sslContext
}
*/
def setupSslContextFor(
_certs: Seq[Cert],
_trustedCerts: Seq[Cert],
forceTrustAll: Boolean,
client: Boolean,
env: Env
): SSLContext = setupSslContextForWithManagers(_certs, _trustedCerts, forceTrustAll, client, env)._1
def setupSslContextForWithManagers(
_certs: Seq[Cert],
_trustedCerts: Seq[Cert],
forceTrustAll: Boolean,
client: Boolean,
env: Env
): (SSLContext, KeyManager, TrustManager) =
env.metrics.withTimer("otoroshi.core.tls.setup-single-context") {
val includeJdkCa: Boolean = if (client) {
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.includeJdkCaClient).getOrElse(true)
} else {
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.includeJdkCaServer).getOrElse(true)
}
val certs = _certs.filter(_.notRevoked)
val trustedCerts = _trustedCerts.filter(_.notRevoked)
val optEnv = Option(env)
val trustAll: Boolean =
if (forceTrustAll) true
else
optEnv
.flatMap(e => e.configuration.getOptionalWithFileSupport[Boolean]("otoroshi.ssl.trust.all"))
.getOrElse(false)
val cacertPath = optEnv
.flatMap(e => e.configuration.getOptionalWithFileSupport[String]("otoroshi.ssl.cacert.path"))
.map(path =>
path
.replace("${JAVA_HOME}", System.getProperty("java.home"))
.replace("$JAVA_HOME", System.getProperty("java.home"))
)
.getOrElse(System.getProperty("java.home") + "/lib/security/cacerts")
val cacertPassword = optEnv
.flatMap(e => e.configuration.getOptionalWithFileSupport[String]("otoroshi.ssl.cacert.password"))
.getOrElse("changeit")
if (logger.isDebugEnabled) logger.debug("Setting up SSL Context ")
val sslContext: SSLContext = SSLContext.getInstance("TLS")
val keyStore1: KeyStore = createKeyStore(certs)
val keyManagerFactory: KeyManagerFactory =
Try(KeyManagerFactory.getInstance("X509")).orElse(Try(KeyManagerFactory.getInstance("SunX509"))).get
keyManagerFactory.init(keyStore1, EMPTY_PASSWORD)
if (logger.isDebugEnabled) logger.debug("SSL Context init ...")
val keyManagers: Array[KeyManager] = keyManagerFactory.getKeyManagers.map(m =>
KeyManagerCompatibility.keyManager(
() => certs,
client,
m.asInstanceOf[X509KeyManager],
optEnv.get
) // new X509KeyManagerSnitch(m.asInstanceOf[X509KeyManager]).asInstanceOf[KeyManager]
)
val keyStore2: KeyStore = if (trustedCerts.nonEmpty) createKeyStore(trustedCerts) else keyStore1
val tm: Array[TrustManager] =
optEnv
.flatMap(e =>
e.configuration.getOptionalWithFileSupport[Boolean]("play.server.https.trustStore.noCaVerification")
)
.map {
case true => Array[TrustManager](noCATrustManager)
case false if includeJdkCa => createTrustStoreWithJdkCAs(keyStore2, cacertPath, cacertPassword)
case false if !includeJdkCa => createTrustStore(keyStore2)
} getOrElse {
if (trustAll) {
Array[TrustManager](
new VeryNiceTrustManager(Seq.empty[X509TrustManager])
)
} else {
if (includeJdkCa) {
createTrustStoreWithJdkCAs(keyStore2, cacertPath, cacertPassword)
} else {
createTrustStore(keyStore2)
}
}
}
sslContext.init(keyManagers, tm, null)
if (logger.isDebugEnabled) logger.debug(s"SSL Context init done ! (${keyStore1.size()} - ${keyStore2.size()})")
SSLContext.setDefault(sslContext)
(sslContext, keyManagers.head, tm.head)
}
def currentServerKeyManager = currentKeyManagerServer.get()
def currentServerTrustManager = currentTrustManagerServer.get()
def currentServer = currentContextServer.get()
def currentClient = currentContextClient.get()
def sslConfigSettings: SSLConfigSettings = currentSslConfigSettings.get()
def getHostNames(): Seq[String] = {
getCurrentEnv().proxyState.allCertificates().filter(_.notRevoked).map(_.domain).distinct
// _certificates.values.filter(_.notRevoked).map(_.domain).toSet.toSeq
}
def addCertificates(certs: Seq[Cert], env: Env): Unit = {
firstSetupDone.compareAndSet(false, true)
certs.filter(_.notRevoked).foreach(crt => autogenCerts.put(crt.id, crt))
val ctxClient = setupContext(
env,
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.includeJdkCaClient).getOrElse(true),
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.trustedCAsServer).getOrElse(Seq.empty)
)
val (ctxServer, keyManagerServer, trustManagerServer) = setupContextAndManagers(
env,
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.includeJdkCaServer).getOrElse(true),
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.trustedCAsServer).getOrElse(Seq.empty)
)
currentContextClient.set(ctxClient)
currentContextServer.set(ctxServer)
currentKeyManagerServer.set(keyManagerServer)
currentTrustManagerServer.set(trustManagerServer)
}
def setCertificates(env: Env): Unit = {
firstSetupDone.compareAndSet(false, true)
//_certificates.clear()
//certs.filter(_.notRevoked).foreach(crt => _certificates.put(crt.id, crt))
allUnrevokedCertSeq
.filter(r => r.serialNumberLng.isDefined && CertParentHelper.fromOtoroshiRootCa(r.certificate.get))
.foreach(crt =>
_ocspProjectionCertificates.put(
crt.serialNumberLng.get,
OCSPCertProjection(
crt.revoked,
crt.isValid,
crt.expired,
crt.entityMetadata.getOrElse("revocationReason", "VALID"),
crt.from.toDate,
crt.to.toDate
)
)
)
val ctxClient = setupContext(
env,
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.includeJdkCaClient).getOrElse(true),
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.trustedCAsServer).getOrElse(Seq.empty)
)
val (ctxServer, keyManagerServer, trustManagerServer) = setupContextAndManagers(
env,
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.includeJdkCaServer).getOrElse(true),
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.trustedCAsServer).getOrElse(Seq.empty)
)
currentContextClient.set(ctxClient)
currentContextServer.set(ctxServer)
currentKeyManagerServer.set(keyManagerServer)
currentTrustManagerServer.set(trustManagerServer)
}
def forceUpdate(env: Env): Unit = {
firstSetupDone.compareAndSet(false, true)
val ctxClient = setupContext(
env,
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.includeJdkCaClient).getOrElse(true),
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.trustedCAsServer).getOrElse(Seq.empty)
)
val (ctxServer, keyManagerServer, trustManagerServer) = setupContextAndManagers(
env,
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.includeJdkCaServer).getOrElse(true),
env.datastores.globalConfigDataStore.latestSafe.map(_.tlsSettings.trustedCAsServer).getOrElse(Seq.empty)
)
currentContextClient.set(ctxClient)
currentContextServer.set(ctxServer)
currentKeyManagerServer.set(keyManagerServer)
currentTrustManagerServer.set(trustManagerServer)
}
def createKeyStore(certificates: Seq[Cert]): KeyStore = {
import SSLImplicits._
if (logger.isDebugEnabled) logger.debug(s"Creating keystore ...")
val keyStore: KeyStore = KeyStore.getInstance("JKS")
keyStore.load(null, null)
certificates.foreach {
case cert if cert.ca => {
cert.certificate.foreach { certificate =>
val id = "ca-" + certificate.getSerialNumber.toString(16)
if (!keyStore.containsAlias(id)) {
keyStore.setCertificateEntry(id, certificate)
}
}
}
case cert if cert.privateKey.trim.isEmpty => {
cert.certificate.foreach { certificate =>
val id = "trusted-" + certificate.getSerialNumber.toString(16)
val certificateChain: Seq[X509Certificate] = readCertificateChain(cert.domain, cert.cleanChain)
val domain = Try {
certificateChain.head.maybeDomain.getOrElse(cert.domain)
}.toOption.getOrElse(cert.domain)
// not sure it's actually needed
if (!keyStore.containsAlias(domain)) {
keyStore.setCertificateEntry(domain, certificate)
}
// Handle SANs, not sure it's actually needed
cert.sans
.filter(name => !keyStore.containsAlias(name))
.foreach(name => {
keyStore.setCertificateEntry(name, certificate)
})
}
}
case cert => {
cert.certificate.foreach { certificate =>
Try {
readPrivateKeyUniversal(cert.domain, cert.privateKey, cert.password).foreach { key: PrivateKey =>
// val key: PrivateKey = readPrivateKey(encodedKeySpec)
val certificateChain: Seq[X509Certificate] = readCertificateChain(cert.domain, cert.cleanChain)
if (certificateChain.isEmpty) {
logger.error(s"[${cert.id}] Certificate file does not contain any certificates :(")
} else {
if (logger.isDebugEnabled)
logger.debug(s"Adding entry for ${cert.domain} with chain of ${certificateChain.size}")
val domain = Try {
certificateChain.head.maybeDomain.getOrElse(cert.domain)
}.toOption.getOrElse(cert.domain)
keyStore.setKeyEntry(
if (cert.client) "client-cert-" + certificate.getSerialNumber.toString(16) else domain,
key,
"".toCharArray,
certificateChain.toArray[java.security.cert.Certificate]
)
// Handle SANs
if (!cert.client) {
cert.sans
.filter(name => !keyStore.containsAlias(name))
.foreach { name =>
keyStore.setKeyEntry(
name,
key,
"".toCharArray,
certificateChain.toArray[java.security.cert.Certificate]
)
}
}
certificateChain.tail.foreach { cert =>
val id = "ca-" + cert.getSerialNumber.toString(16)
if (!keyStore.containsAlias(id)) {
keyStore.setCertificateEntry(id, cert)
}
}
}
}
} match {
case Failure(e) => logger.error(s"Error while handling certificate: ${cert.name}: " + e.getMessage, e)
case Success(e) =>
}
}
}
}
keyStore
}
def createTrustStoreWithJdkCAs(
keyStore: KeyStore,
cacertPath: String,
cacertPassword: String
): Array[TrustManager] = {
if (logger.isDebugEnabled) logger.debug(s"Creating truststore ...")
val tmf = TrustManagerFactory.getInstance("SunX509")
tmf.init(keyStore)
val javaKs = KeyStore.getInstance("JKS")
// TODO: optimize here: avoid reading file all the time
javaKs.load(new FileInputStream(cacertPath), cacertPassword.toCharArray)
val tmf2 = TrustManagerFactory.getInstance("SunX509")
tmf2.init(javaKs)
Array[TrustManager](
new FakeTrustManager((tmf.getTrustManagers ++ tmf2.getTrustManagers).map(_.asInstanceOf[X509TrustManager]).toSeq)
)
}
def createTrustStore(keyStore: KeyStore): Array[TrustManager] = {
if (logger.isDebugEnabled) logger.debug(s"Creating truststore ...")
val tmf = TrustManagerFactory.getInstance("SunX509")
tmf.init(keyStore)
Array[TrustManager](
new FakeTrustManager(tmf.getTrustManagers.map(_.asInstanceOf[X509TrustManager]).toSeq)
)
}
def readCertificateChain(id: String, certificateChain: String, log: Boolean = true): Seq[X509Certificate] = {
if (log && logger.isDebugEnabled) logger.debug(s"Reading cert chain for $id")
val matcher: Matcher = CERT_PATTERN.matcher(certificateChain)
val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
var certificates = Seq.empty[X509Certificate]
var start = 0
while ({ matcher.find(start) }) {
val buffer: Array[Byte] = base64Decode(matcher.group(1))
certificates = certificates :+ certificateFactory
.generateCertificate(new ByteArrayInputStream(buffer))
.asInstanceOf[X509Certificate]
start = matcher.end
}
certificates
}
def _readPrivateKey(encodedKeySpec: KeySpec): Try[PrivateKey] = {
Try(KeyFactory.getInstance("RSA").generatePrivate(encodedKeySpec))
.orElse(Try(KeyFactory.getInstance("EC").generatePrivate(encodedKeySpec)))
.orElse(Try(KeyFactory.getInstance("DSA").generatePrivate(encodedKeySpec)))
.orElse(Try(KeyFactory.getInstance("DiffieHellman").generatePrivate(encodedKeySpec)))
.orElse(Try(KeyFactory.getInstance("XDH").generatePrivate(encodedKeySpec)))
.orElse(Try(KeyFactory.getInstance("X25519").generatePrivate(encodedKeySpec)))
.orElse(Try(KeyFactory.getInstance("X25519").generatePrivate(encodedKeySpec)))
.orElse(Try(KeyFactory.getInstance("RSASSA-PSS").generatePrivate(encodedKeySpec)))
}
def _readPrivateKeySpec(
id: String,
content: String,
keyPassword: Option[String],
log: Boolean = true
): Either[KeyStoreError, KeySpec] = {
if (log && logger.isDebugEnabled) logger.debug(s"Reading private key for $id")
val matcher: Matcher = PRIVATE_KEY_PATTERN.matcher(content)
if (!matcher.find) {
if (logger.isDebugEnabled) logger.debug(s"[$id] Found no private key :(")
Left(s"[$id] Found no private key")
} else {
val encodedKey: Array[Byte] = base64Decode(matcher.group(1))
keyPassword
.map { kpv =>
val encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey)
val algName = encryptedPrivateKeyInfo.getAlgParameters.toString
val keyFactory: SecretKeyFactory = SecretKeyFactory.getInstance(algName)
val secretKey: SecretKey = keyFactory.generateSecret(new PBEKeySpec(kpv.toCharArray))
val cipher: Cipher = Cipher.getInstance(algName)
cipher.init(DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters)
val spec = encryptedPrivateKeyInfo.getKeySpec(cipher)
Right(spec)
}
.getOrElse {
Right(new PKCS8EncodedKeySpec(encodedKey))
}
}
}
def readPrivateKeyUniversal(
id: String,
content: String,
keyPassword: Option[String],
log: Boolean = true
): Either[KeyStoreError, PrivateKey] = {
if (log && logger.isDebugEnabled) logger.debug(s"Reading private key for $id")
val matcher: Matcher = PRIVATE_KEY_PATTERN.matcher(content)
if (!matcher.find) {
if (logger.isDebugEnabled) logger.debug(s"[$id] Found no private key :(")
Left(s"[$id] Found no private key")
} else {
import otoroshi.utils.syntax.implicits._
Try {
// val reader = new PemReader(new StringReader(privateKey))
val parser = new PEMParser(new StringReader(content))
val converter = new JcaPEMKeyConverter().setProvider("BC")
parser.readObject() match {
case ckp: PEMEncryptedKeyPair if keyPassword.isEmpty => None
case ckp: PEMEncryptedKeyPair if keyPassword.isDefined =>
val decProv = new JcePEMDecryptorProviderBuilder().build(keyPassword.get.toCharArray)
val kp = converter.getKeyPair(ckp.decryptKeyPair(decProv))
kp.getPrivate.some
case ukp: PEMKeyPair =>
val kp = converter.getKeyPair(ukp)
kp.getPrivate.some
case upk: org.bouncycastle.asn1.pkcs.PrivateKeyInfo if keyPassword.isEmpty =>
val kp = converter.getPrivateKey(upk)
kp.some
case _: org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo if keyPassword.nonEmpty =>
CertInfo.stringToPrivateKey(content, keyPassword.get).some
case _ => None
}
} match {
case Failure(e) =>
// Left(s"[$id] error while reading private key: ${e.getMessage}".debugPrintln)
_readPrivateKeySpec(id, content, keyPassword, log).flatMap(ks =>
_readPrivateKey(ks).toEither match {
case Left(r) => Left(r.getMessage)
case Right(r) => Right(r)
}
)
case Success(None) =>
// Left(s"[$id] no valid key found".debugPrintln)
_readPrivateKeySpec(id, content, keyPassword, log).flatMap(ks =>
_readPrivateKey(ks).toEither match {
case Left(r) => Left(r.getMessage)
case Right(r) => Right(r)
}
)
case Success(Some(key)) => Right(key)
}
}
}
def isSelfSigned(cert: X509Certificate): Boolean = {
Try { // Try to verify certificate signature with its own public key
val key: PublicKey = cert.getPublicKey
cert.verify(key)
true
} recover { case e =>
false
} get
}
def base64Decode(base64: String): Array[Byte] = Base64.getMimeDecoder.decode(base64.getBytes(US_ASCII))
def createSSLEngine(
clientAuth: ClientAuth,
cipherSuites: Option[Seq[String]],
protocols: Option[Seq[String]],
appProto: Option[String],
env: => Env
): SSLEngine = {
val context: SSLContext = DynamicSSLEngineProvider.currentServer
if (logger.isDebugEnabled) DynamicSSLEngineProvider.logger.debug(s"Create SSLEngine from: $context")
val rawEngine = context.createSSLEngine()
val rawEnabledCipherSuites = rawEngine.getEnabledCipherSuites.toSeq
val rawEnabledProtocols = rawEngine.getEnabledProtocols.toSeq
cipherSuites.foreach(s => rawEngine.setEnabledCipherSuites(s.toArray))
protocols.foreach(p => rawEngine.setEnabledProtocols(p.toArray))
val engine = new CustomSSLEngine(
rawEngine,
appProto,
env.datastores.globalConfigDataStore.latestUnsafe.tlsSettings.bannedAlpnProtocols
)
val sslParameters = new SSLParameters
val matchers = new java.util.ArrayList[SNIMatcher]()
engine.setUseClientMode(false)
clientAuth match {
case ClientAuth.Want =>
engine.setWantClientAuth(true)
sslParameters.setWantClientAuth(true)
case ClientAuth.Need =>
engine.setNeedClientAuth(true)
sslParameters.setNeedClientAuth(true)
case _ =>
}
matchers.add(new SNIMatcher(0) {
override def matches(sniServerName: SNIServerName): Boolean = {
sniServerName match {
case hn: SNIHostName =>
val hostName = hn.getAsciiName
if (logger.isDebugEnabled) logger.debug(s"createSSLEngine - for $hostName")
engine.setEngineHostName(hostName)
case _ =>
if (logger.isDebugEnabled) logger.debug(s"Not a hostname :( $sniServerName")
}
true
}
})
sslParameters.setSNIMatchers(matchers)
cipherSuites.orElse(Some(rawEnabledCipherSuites)).foreach(s => sslParameters.setCipherSuites(s.toArray))
protocols.orElse(Some(rawEnabledProtocols)).foreach(p => sslParameters.setProtocols(p.toArray))
engine.setSSLParameters(sslParameters)
// println("protocols: ", protocols.mkString(", "), "cipherSuites: ", cipherSuites.mkString(", "))
// println("----")
// println("engine supported", engine.getSupportedProtocols().toSeq.mkString(", "))
// println("engine enabled", engine.getEnabledProtocols().toSeq.mkString(", "))
// println("sslParameters", sslParameters.getProtocols().toSeq.mkString(", "))
// println("----")
// println("engine supported", engine.getSupportedCipherSuites().toSeq.mkString(", "))
// println("engine enabled", engine.getEnabledCipherSuites().toSeq.mkString(", "))
// println("sslParameters", sslParameters.getCipherSuites().toSeq.mkString(", "))
// println("----")
engine.locked()
}
}
/*class OtoroshiHostnameVerifier() extends HostnameVerifier {
override def verify(s: String, sslSession: SSLSession): Boolean = {
true
}
}*/
class DynamicSSLEngineProvider(appProvider: ApplicationProvider) extends SSLEngineProvider {
lazy val cipherSuites =
appProvider.get.get.configuration
.getOptionalWithFileSupport[Seq[String]]("otoroshi.ssl.cipherSuites")
.filterNot(_.isEmpty)
lazy val protocols =
appProvider.get.get.configuration
.getOptionalWithFileSupport[Seq[String]]("otoroshi.ssl.protocols")
.filterNot(_.isEmpty)
lazy val clientAuth = {
val auth = appProvider.get.get.configuration
.getOptionalWithFileSupport[String]("otoroshi.ssl.fromOutside.clientAuth")
.flatMap(ClientAuth.apply)
.getOrElse(ClientAuth.None)
if (DynamicSSLEngineProvider.logger.isDebugEnabled)
DynamicSSLEngineProvider.logger.debug(s"Otoroshi client auth: ${auth}")
auth
}
override def createSSLEngine(): SSLEngine = {
DynamicSSLEngineProvider.createSSLEngine(clientAuth, cipherSuites, protocols, None, OtoroshiEnvHolder.get())
}
private def setupSslContext(): SSLContext = {
new SSLContext(
new SSLContextSpi() {
override def engineCreateSSLEngine(): SSLEngine = createSSLEngine()
override def engineCreateSSLEngine(s: String, i: Int): SSLEngine = engineCreateSSLEngine()
override def engineInit(
keyManagers: Array[KeyManager],
trustManagers: Array[TrustManager],
secureRandom: SecureRandom
): Unit = ()
override def engineGetClientSessionContext(): SSLSessionContext =
DynamicSSLEngineProvider.currentServer.getClientSessionContext
override def engineGetServerSessionContext(): SSLSessionContext =
DynamicSSLEngineProvider.currentServer.getServerSessionContext
override def engineGetSocketFactory(): SSLSocketFactory =
DynamicSSLEngineProvider.currentServer.getSocketFactory
override def engineGetServerSocketFactory(): SSLServerSocketFactory =
DynamicSSLEngineProvider.currentServer.getServerSocketFactory
},
new Provider(
"Otoroshi SSlEngineProvider delegate",
1d,
"A provider that delegates calls to otoroshi dynamic one"
) {},
"Otoroshi SSLEngineProvider delegate"
) {}
}
override def sslContext(): SSLContext = setupSslContext()
}
object noCATrustManager extends X509TrustManager {
val nullArray = Array[X509Certificate]()
def checkClientTrusted(x509Certificates: Array[X509Certificate], s: String): Unit = {}
def checkServerTrusted(x509Certificates: Array[X509Certificate], s: String): Unit = {}
def getAcceptedIssuers() = nullArray
}
object CertificateData {
import otoroshi.ssl.SSLImplicits._
import collection.JavaConverters._
private val logger = Logger("otoroshi-cert-data")
private val encoder = Base64.getEncoder
private val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
private def base64Decode(base64: String): Array[Byte] = Base64.getMimeDecoder.decode(base64.getBytes(US_ASCII))
def apply(pemContent: String): JsValue = {
val finContent = PemHeaders.BeginCertificate + "\n" + pemContent + "\n" + PemHeaders.EndCertificate
val buffer = base64Decode(
pemContent.replace(PemHeaders.BeginCertificate, "").replace(PemHeaders.EndCertificate, "")
)
val cert = certificateFactory.generateCertificate(new ByteArrayInputStream(buffer)).asInstanceOf[X509Certificate]
val altNames = cert.altNames
val rawDomain = cert.rawDomain
val domain: String = cert.domain
val pemReader = new org.bouncycastle.openssl.PEMParser(new StringReader(finContent))
val holder = pemReader.readObject().asInstanceOf[org.bouncycastle.cert.X509CertificateHolder]
pemReader.close()
val usages: Array[KeyPurposeId] = Option(holder.getExtensions)
.flatMap(exts => Option(ExtendedKeyUsage.fromExtensions(exts)))
.map(_.getUsages)
.getOrElse(Array.empty)
val client: Boolean = usages.contains(KeyPurposeId.id_kp_clientAuth)
// val client: Boolean = Try(cert.getExtensionValue("2.5.29.37")) match {
Json.obj(
"issuerDN" -> DN(cert.getIssuerDN.getName).stringify,
"notAfter" -> cert.getNotAfter.getTime,
"notBefore" -> cert.getNotBefore.getTime,
"serialNumber" -> cert.getSerialNumber.toString(16),
"serialNumberLng" -> cert.getSerialNumber,
"sigAlgName" -> cert.getSigAlgName,
"sigAlgOID" -> cert.getSigAlgOID,
"_signature" -> new String(encoder.encode(cert.getSignature)),
"signature" -> DigestUtils.sha256Hex(cert.getSignature).toUpperCase().grouped(2).mkString(":"),
"subjectDN" -> DN(cert.getSubjectDN.getName).stringify,
"domain" -> domain,
"rawDomain" -> rawDomain.map(JsString.apply).getOrElse(JsNull).as[JsValue],
"version" -> cert.getVersion,
"type" -> cert.getType,
"publicKey" -> cert.getPublicKey.asPem, // new String(encoder.encode(cert.getPublicKey.getEncoded)),
"selfSigned" -> DynamicSSLEngineProvider.isSelfSigned(cert),
"constraints" -> cert.getBasicConstraints,
"ca" -> (cert.getBasicConstraints != -1),
"client" -> client,
"subAltNames" -> JsArray(altNames.map(JsString.apply)),
"cExtensions" -> JsArray(
Option(cert.getCriticalExtensionOIDs).map(_.asScala.toSeq).getOrElse(Seq.empty[String]).map { oid =>
val ext: String =
Option(cert.getExtensionValue(oid)).map(bytes => ByteString(bytes).utf8String).getOrElse("--")
Json.obj(
"oid" -> oid,
"value" -> ext
)
}
),
"ncExtensions" -> JsArray(
Option(cert.getNonCriticalExtensionOIDs).map(_.asScala.toSeq).getOrElse(Seq.empty[String]).map { oid =>
val ext: String =
Option(cert.getExtensionValue(oid)).map(bytes => ByteString(bytes).utf8String).getOrElse("--")
Json.obj(
"oid" -> oid,
"value" -> ext
)
}
)
)
}
}
object PemHeaders {
val BeginCertificate = "-----BEGIN CERTIFICATE-----"
val EndCertificate = "-----END CERTIFICATE-----"
val BeginPublicKey = "-----BEGIN PUBLIC KEY-----"
val EndPublicKey = "-----END PUBLIC KEY-----"
val BeginPrivateKey = "-----BEGIN PRIVATE KEY-----"
val EndPrivateKey = "-----END PRIVATE KEY-----"
val BeginPrivateRSAKey = "-----BEGIN RSA PRIVATE KEY-----"
val BeginPrivateECKey = "-----BEGIN EC PRIVATE KEY-----"
val EndPrivateRSAKey = "-----END RSA PRIVATE KEY-----"
val EndPrivateECKey = "-----END EC PRIVATE KEY-----"
val BeginCertificateRequest = "-----BEGIN CERTIFICATE REQUEST-----"
val EndCertificateRequest = "-----END CERTIFICATE REQUEST-----"
}
object FakeKeyStore {
import otoroshi.ssl.SSLImplicits._
private val EMPTY_PASSWORD = Array.emptyCharArray
private val encoder = Base64.getEncoder
object SelfSigned {
object Alias {
val trustedCertEntry = "otoroshi-selfsigned-trust"
val PrivateKeyEntry = "otoroshi-selfsigned"
}
def DistinguishedName(host: String) = s"CN=$host, OU=Otoroshi Certificates, O=Otoroshi"
def SubDN(host: String) = s"CN=$host"
}
object KeystoreSettings {
val SignatureAlgorithmName = "SHA256withRSA"
val KeyPairAlgorithmName = "RSA"
val KeyPairKeyLength = 2048 // 2048 is the NIST acceptable key length until 2030
val KeystoreType = "JKS"
}
private implicit val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))
def generateKeyStore(host: String)(implicit env: Env): KeyStore = {
val keyStore: KeyStore = KeyStore.getInstance(KeystoreSettings.KeystoreType)
val (cert, keyPair) = generateX509Certificate(host)
keyStore.load(null, EMPTY_PASSWORD)
keyStore.setKeyEntry(SelfSigned.Alias.PrivateKeyEntry, keyPair.getPrivate, EMPTY_PASSWORD, Array(cert))
keyStore.setCertificateEntry(SelfSigned.Alias.trustedCertEntry, cert)
keyStore
}
def generateX509Certificate(host: String)(implicit env: Env): (X509Certificate, KeyPair) = {
val resp = createSelfSignedCertificate(host, 365.days, None, None)
(resp.cert, resp.keyPair)
}
def generateCert(host: String)(implicit env: Env): Cert = {
val (cert, keyPair) = generateX509Certificate(host)
Cert(
id = IdGenerator.token(32),
name = host,
description = s"Certificate for $host",
domain = host,
chain = cert.asPem,
privateKey = keyPair.getPrivate.asPem,
caRef = None,
autoRenew = false,
client = false,
exposed = false,
revoked = false
)
}
def createSelfSignedCertificate(host: String, duration: FiniteDuration, kp: Option[KeyPair], serial: Option[Long])(
implicit env: Env
): GenCertResponse = {
val f = env.pki.genSelfSignedCert(
GenCsrQuery(
hosts = Seq(host),
key = GenKeyPairQuery(KeystoreSettings.KeyPairAlgorithmName, KeystoreSettings.KeyPairKeyLength),
name = Map("CN" -> host),
duration = duration,
existingKeyPair = kp,
existingSerialNumber = serial
)
)
// AWAIT: valid
val resp = Await.result(f, 30.seconds)
resp.right.get
}
def createClientCertificateFromCA(
dn: String,
duration: FiniteDuration,
kp: Option[KeyPair],
serial: Option[Long],
ca: X509Certificate,
caChain: Seq[X509Certificate],
caKeyPair: KeyPair
)(implicit env: Env): GenCertResponse = {
val f = env.pki.genCert(
GenCsrQuery(
hosts = Seq.empty,
key = GenKeyPairQuery(KeystoreSettings.KeyPairAlgorithmName, KeystoreSettings.KeyPairKeyLength),
subject = Some(dn),
duration = duration,
existingKeyPair = kp,
existingSerialNumber = serial,
client = true
),
ca,
caChain,
caKeyPair.getPrivate
)
// AWAIT: valid
val resp = Await.result(f, 30.seconds)
resp.right.get
}
def createSelfSignedClientCertificate(
dn: String,
duration: FiniteDuration,
kp: Option[KeyPair],
serial: Option[Long]
)(implicit env: Env): GenCertResponse = {
val f = env.pki.genSelfSignedCert(
GenCsrQuery(
hosts = Seq.empty,
key = GenKeyPairQuery(KeystoreSettings.KeyPairAlgorithmName, KeystoreSettings.KeyPairKeyLength),
subject = Some(dn),
duration = duration,
existingKeyPair = kp,
existingSerialNumber = serial,
client = true
)
)
// AWAIT: valid
val resp = Await.result(f, 30.seconds)
resp.right.get
}
def createCertificateFromCA(
host: String,
duration: FiniteDuration,
kp: Option[KeyPair],
serial: Option[Long],
ca: X509Certificate,
caChain: Seq[X509Certificate],
caKeyPair: KeyPair
)(implicit env: Env): GenCertResponse = {
val f = env.pki.genCert(
GenCsrQuery(
hosts = Seq(host),
key = GenKeyPairQuery(KeystoreSettings.KeyPairAlgorithmName, KeystoreSettings.KeyPairKeyLength),
name = Map("CN" -> host),
duration = duration,
existingKeyPair = kp,
existingSerialNumber = serial
),
ca,
caChain,
caKeyPair.getPrivate
)
// AWAIT: valid
val resp = Await.result(f, 30.seconds)
resp.right.get
}
def createSubCa(
cn: String,
duration: FiniteDuration,
kp: Option[KeyPair],
serial: Option[Long],
ca: X509Certificate,
caChain: Seq[X509Certificate],
caKeyPair: KeyPair
)(implicit env: Env): GenCertResponse = {
val f = env.pki.genSubCA(
GenCsrQuery(
hosts = Seq.empty,
key = GenKeyPairQuery(KeystoreSettings.KeyPairAlgorithmName, KeystoreSettings.KeyPairKeyLength),
subject = Some(cn),
duration = duration,
existingKeyPair = kp,
existingSerialNumber = serial,
ca = true
),
ca,
caChain,
caKeyPair.getPrivate
)
// AWAIT: valid
val resp = Await.result(f, 30.seconds)
resp.right.get
}
def createCA(cn: String, duration: FiniteDuration, kp: Option[KeyPair], serial: Option[Long])(implicit
env: Env
): GenCertResponse = {
val f = env.pki.genSelfSignedCA(
GenCsrQuery(
hosts = Seq.empty,
key = GenKeyPairQuery(KeystoreSettings.KeyPairAlgorithmName, KeystoreSettings.KeyPairKeyLength),
subject = Some(cn),
duration = duration,
existingKeyPair = kp,
existingSerialNumber = serial,
ca = true
)
)
// AWAIT: valid
val resp = Await.result(f, 30.seconds)
resp.right.get
}
}
class CustomSSLEngine(delegate: SSLEngine, appProto: Option[String], bannedProtos: Map[String, Seq[String]])
extends SSLEngine {
// println(delegate.getClass.getName)
// sun.security.ssl.SSLEngineImpl
// sun.security.ssl.X509TrustManagerImpl
// javax.net.ssl.X509ExtendedTrustManager
private val hostnameHolder = new AtomicReference[String]()
private var lock = false
def locked(): CustomSSLEngine = {
// it's fine as the set only appears in the same thread/function as object creation
lock = true
this
}
// TODO: add try to avoid future issue ?
private lazy val field: Field = {
val f = Option(classOf[SSLEngine].getDeclaredField("peerHost")).getOrElse(classOf[SSLEngine].getField("peerHost"))
f.setAccessible(true)
f
}
def setEngineHostName(hostName: String): Unit = {
if (DynamicSSLEngineProvider.logger.isDebugEnabled)
DynamicSSLEngineProvider.logger.debug(s"Setting current session hostname to $hostName")
hostnameHolder.set(hostName)
// TODO: add try to avoid future issue ? fixed for now with '--add-opens java.base/javax.net.ssl=ALL-UNNAMED' in the java command line
field.set(this, hostName)
field.set(delegate, hostName)
}
override def getPeerHost: String = Option(hostnameHolder.get()).getOrElse(delegate.getPeerHost)
override def getPeerPort: Int = delegate.getPeerPort
override def wrap(byteBuffers: Array[ByteBuffer], i: Int, i1: Int, byteBuffer: ByteBuffer): SSLEngineResult =
delegate.wrap(byteBuffers, i, i1, byteBuffer)
override def unwrap(byteBuffer: ByteBuffer, byteBuffers: Array[ByteBuffer], i: Int, i1: Int): SSLEngineResult =
delegate.unwrap(byteBuffer, byteBuffers, i, i1)
override def getDelegatedTask: Runnable = delegate.getDelegatedTask
override def closeInbound(): Unit = delegate.closeInbound()
override def isInboundDone: Boolean = delegate.isInboundDone
override def closeOutbound(): Unit = delegate.closeOutbound()
override def isOutboundDone: Boolean = delegate.isOutboundDone
override def getSupportedCipherSuites: Array[String] = delegate.getSupportedCipherSuites
override def getEnabledCipherSuites: Array[String] = delegate.getEnabledCipherSuites
override def setEnabledCipherSuites(strings: Array[String]): Unit = {
if (!lock) {
delegate.setEnabledCipherSuites(strings)
}
}
override def getSupportedProtocols: Array[String] = delegate.getSupportedProtocols
override def getEnabledProtocols: Array[String] = delegate.getEnabledProtocols
override def setEnabledProtocols(strings: Array[String]): Unit = {
if (!lock) {
delegate.setEnabledProtocols(strings)
}
}
override def getSession: SSLSession = delegate.getSession
override def beginHandshake(): Unit = delegate.beginHandshake()
override def getHandshakeStatus: SSLEngineResult.HandshakeStatus = delegate.getHandshakeStatus
override def setUseClientMode(b: Boolean): Unit = {
if (!lock) {
delegate.setUseClientMode(b)
}
}
override def getUseClientMode: Boolean = delegate.getUseClientMode
override def setNeedClientAuth(b: Boolean): Unit = {
if (!lock) {
delegate.setNeedClientAuth(b)
}
}
override def getNeedClientAuth: Boolean = delegate.getNeedClientAuth
override def setWantClientAuth(b: Boolean): Unit = {
if (!lock) {
delegate.setWantClientAuth(b)
}
}
override def getWantClientAuth: Boolean = delegate.getWantClientAuth
override def setEnableSessionCreation(b: Boolean): Unit = {
if (!lock) {
delegate.setEnableSessionCreation(b)
}
}
override def getEnableSessionCreation: Boolean = delegate.getEnableSessionCreation
override def wrap(var1: ByteBuffer, var2: ByteBuffer): SSLEngineResult = delegate.wrap(var1, var2)
override def wrap(var1: Array[ByteBuffer], var2: ByteBuffer): SSLEngineResult = delegate.wrap(var1, var2)
override def unwrap(var1: ByteBuffer, var2: ByteBuffer): SSLEngineResult = delegate.unwrap(var1, var2)
override def unwrap(var1: ByteBuffer, var2: Array[ByteBuffer]): SSLEngineResult = delegate.unwrap(var1, var2)
override def getHandshakeSession: SSLSession = delegate.getHandshakeSession
override def getSSLParameters: SSLParameters = delegate.getSSLParameters
override def setSSLParameters(var1: SSLParameters): Unit = {
if (!lock) {
delegate.setSSLParameters(var1)
}
}
override def setHandshakeApplicationProtocolSelector(
selector: BiFunction[SSLEngine, util.List[String], String]
): Unit = {
import scala.jdk.CollectionConverters._
if (!lock) {
delegate.setHandshakeApplicationProtocolSelector(new BiFunction[SSLEngine, util.List[String], String] {
override def apply(t: SSLEngine, u: util.List[String]): String = {
Try(t.getPeerHost)
.orElse(Try(t.getHandshakeSession).map(_.getPeerHost))
.orElse(Try(t.getSession).map(_.getPeerHost)) match {
case Failure(_) => selector.apply(t, u)
case Success(peerHost) => {
// println(s"get selector on : ${peerHost} - ${u.toArray.mkString(", ")}")
bannedProtos.get(peerHost) match {
case None => selector.apply(t, u)
case Some(banned) => {
val withoutBanned = u.asScala.filterNot(v => banned.contains(v))
// println(s"can just use: ${withoutBanned.mkString(", ")}")
selector.apply(t, withoutBanned.asJava)
}
}
}
}
}
})
}
}
override def getHandshakeApplicationProtocolSelector: BiFunction[SSLEngine, util.List[String], String] =
delegate.getHandshakeApplicationProtocolSelector
override def getHandshakeApplicationProtocol: String = delegate.getHandshakeApplicationProtocol
override def getApplicationProtocol: String = appProto match {
case None => delegate.getApplicationProtocol
case Some(protocol) => protocol
}
}
sealed trait ClientCertificateValidationDataStore extends BasicStore[ClientCertificateValidator] {
def getValidation(key: String)(implicit ec: ExecutionContext, env: Env): Future[Option[Boolean]]
def setValidation(key: String, value: Boolean, ttl: Long)(implicit ec: ExecutionContext, env: Env): Future[Boolean]
def removeValidation(key: String)(implicit ec: ExecutionContext, env: Env): Future[Long]
def template: ClientCertificateValidator = {
ClientCertificateValidator(
id = IdGenerator.token,
name = "validator",
description = "A client certificate validator",
url = "https://validator.oto.tools:8443",
host = "validator.oto.tools",
noCache = false,
alwaysValid = false,
proxy = None
)
}
}
class KvClientCertificateValidationDataStore(redisCli: RedisLike, env: Env)
extends ClientCertificateValidationDataStore
with RedisLikeStore[ClientCertificateValidator] {
def dsKey(k: String)(implicit env: Env): String = s"${env.storageRoot}:certificates:clients:$k"
override def getValidation(key: String)(implicit ec: ExecutionContext, env: Env): Future[Option[Boolean]] =
redisCli.get(dsKey(key)).map(_.map(_.utf8String.toBoolean))
override def setValidation(key: String, value: Boolean, ttl: Long)(implicit
ec: ExecutionContext,
env: Env
): Future[Boolean] =
redisCli.set(dsKey(key), value.toString, pxMilliseconds = Some(ttl))
def removeValidation(key: String)(implicit ec: ExecutionContext, env: Env): Future[Long] = redisCli.del(dsKey(key))
override def fmt: Format[ClientCertificateValidator] = ClientCertificateValidator.fmt
override def redisLike(implicit env: Env): RedisLike = redisCli
override def key(id: String): String = s"${env.storageRoot}:certificates:validators:$id"
override def extractId(value: ClientCertificateValidator): String = value.id
}
// https://tools.ietf.org/html/rfc5280
// https://tools.ietf.org/html/rfc2585
// https://tools.ietf.org/html/rfc2560
// https://en.wikipedia.org/wiki/Public_key_infrastructure
// https://en.wikipedia.org/wiki/Validation_authority
// https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol
object ClientCertificateValidator {
val logger = Logger("otoroshi-client-cert-validator")
val digester = MessageDigest.getInstance("SHA-1")
val fmt = new Format[ClientCertificateValidator] {
override def reads(json: JsValue): JsResult[ClientCertificateValidator] =
Try {
JsSuccess(
ClientCertificateValidator(
location = otoroshi.models.EntityLocation.readFromKey(json),
id = (json \ "id").as[String],
name = (json \ "name").as[String],
description = (json \ "description").asOpt[String].getOrElse("--"),
url = (json \ "url").as[String],
host = (json \ "host").as[String],
goodTtl = (json \ "goodTtl").asOpt[Long].getOrElse(10 * 60000L),
badTtl = (json \ "badTtl").asOpt[Long].getOrElse(1 * 60000L),
method = (json \ "method").asOpt[String].getOrElse("POST"),
path = (json \ "path").asOpt[String].getOrElse("/certificates/_validate"),
timeout = (json \ "timeout").asOpt[Long].getOrElse(10000L),
noCache = (json \ "noCache").asOpt[Boolean].getOrElse(false),
alwaysValid = (json \ "alwaysValid").asOpt[Boolean].getOrElse(false),
headers = (json \ "headers").asOpt[Map[String, String]].getOrElse(Map.empty),
proxy = (json \ "proxy").asOpt[JsValue].flatMap(p => WSProxyServerJson.proxyFromJson(p)),
metadata = (json \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty),
tags = (json \ "tags").asOpt[Seq[String]].getOrElse(Seq.empty[String])
)
)
} recover { case e =>
JsError(e.getMessage)
} get
override def writes(o: ClientCertificateValidator): JsValue =
o.location.jsonWithKey ++ Json.obj(
"id" -> o.id,
"name" -> o.name,
"description" -> o.description,
"url" -> o.url,
"host" -> o.host,
"goodTtl" -> o.goodTtl,
"badTtl" -> o.badTtl,
"method" -> o.method,
"path" -> o.path,
"timeout" -> o.timeout,
"noCache" -> o.noCache,
"alwaysValid" -> o.alwaysValid,
"headers" -> o.headers,
"proxy" -> WSProxyServerJson.maybeProxyToJson(o.proxy),
"metadata" -> o.metadata,
"tags" -> JsArray(o.tags.map(JsString.apply))
)
}
def fromJson(json: JsValue): Either[Seq[(JsPath, Seq[JsonValidationError])], ClientCertificateValidator] =
ClientCertificateValidator.fmt.reads(json).asEither
def fromJsons(value: JsValue): ClientCertificateValidator =
try {
fmt.reads(value).get
} catch {
case e: Throwable => {
logger.error(s"Try to deserialize ${Json.prettyPrint(value)}")
throw e
}
}
}
case class ClientCertificateValidator(
id: String,
name: String,
description: String,
url: String,
host: String,
goodTtl: Long = 10L * 60000L,
badTtl: Long = 1L * 60000L,
method: String = "POST",
path: String = "/certificates/_validate",
timeout: Long = 10000L,
noCache: Boolean,
alwaysValid: Boolean,
headers: Map[String, String] = Map.empty,
proxy: Option[WSProxyServer],
location: otoroshi.models.EntityLocation = otoroshi.models.EntityLocation(),
tags: Seq[String] = Seq.empty,
metadata: Map[String, String] = Map.empty
) extends otoroshi.models.EntityLocationSupport {
def json: JsValue = asJson
def internalId: String = id
def theDescription: String = description
def theMetadata: Map[String, String] = metadata
def theName: String = name
def theTags: Seq[String] = tags
import otoroshi.utils.http.Implicits._
/*
TEST CODE
const express = require('express');
const bodyParser = require('body-parser');
const x509 = require('x509');
const app = express();
app.use(bodyParser.text());
app.use(bodyParser.json());
app.post('/certificates/_validate', (req, res) => {
console.log('need to validate the following certificate chain');
const service = req.body.service;
const identity = req.body.apikey || req.body.user || { email: '[email protected]' };
const cert = x509.parseCert(req.body.chain);
console.log(identity, service, cert);
if (cert.subject.emailAddress === '[email protected]') {
res.send({ status: "good" });
} else {
res.send({ status: "revoked" });
}
});
app.listen(3000, () => console.log('certificate validation server'));
*/
import play.api.http.websocket.{Message => PlayWSMessage}
import otoroshi.ssl.SSLImplicits._
import scala.concurrent.duration._
def save()(implicit ec: ExecutionContext, env: Env) = env.datastores.clientCertificateValidationDataStore.set(this)
def asJson: JsValue = ClientCertificateValidator.fmt.writes(this)
private def validateCertificateChain(
chain: Seq[X509Certificate],
desc: ServiceDescriptor,
apikey: Option[ApiKey] = None,
user: Option[PrivateAppsUser] = None,
config: GlobalConfig
)(implicit ec: ExecutionContext, env: Env): Future[Option[Boolean]] = {
val certPayload = chain
.map { cert =>
cert.asPem
// s"${PemHeaders.BeginCertificate}\n${Base64.getEncoder.encodeToString(cert.getEncoded)}\n${PemHeaders.EndCertificate}"
}
.mkString("\n")
val payload = Json.obj(
"apikey" -> apikey.map(_.toJson.as[JsObject] - "clientSecret").getOrElse(JsNull).as[JsValue],
"user" -> user.map(_.toJson).getOrElse(JsNull).as[JsValue],
"service" -> Json.obj(
"id" -> desc.id,
"name" -> desc.name,
"groups" -> desc.groups,
"domain" -> desc.domain,
"env" -> desc.env,
"subdomain" -> desc.subdomain,
"root" -> desc.root,
"metadata" -> desc.metadata
),
"chain" -> certPayload,
"fingerprints" -> JsArray(chain.map(computeFingerPrint).map(JsString.apply))
)
val finalHeaders: Seq[(String, String)] =
headers.toSeq ++ Seq("Host" -> host, "Content-Type" -> "application/json", "Accept" -> "application/json")
env.Ws // no need for mtls here
.url(url + path)
.withHttpHeaders(finalHeaders: _*)
.withMethod(method)
.withBody(payload)
.withRequestTimeout(Duration(timeout, TimeUnit.MILLISECONDS))
.withMaybeProxyServer(proxy.orElse(config.proxies.authority))
.execute()
.map { resp =>
resp.status match { // TODO: can be good | revoked | unknown
case 200 =>
(resp.json.as[JsObject] \ "status")
.asOpt[String]
.map(_.toLowerCase == "good") // TODO: return custom message, also device identification for logging
case _ =>
resp.ignore()(env.otoroshiMaterializer)
None
}
}
.recover { case e =>
ClientCertificateValidator.logger.error("Error while validating client certificate chain", e)
None
}
}
private def getLocalValidation(key: String)(implicit ec: ExecutionContext, env: Env): Future[Option[Boolean]] = {
env.datastores.clientCertificateValidationDataStore.getValidation(key)
}
private def setGoodLocalValidation(key: String)(implicit ec: ExecutionContext, env: Env): Future[Unit] = {
env.datastores.clientCertificateValidationDataStore.setValidation(key, true, goodTtl).map(_ => ())
}
private def setBadLocalValidation(key: String)(implicit ec: ExecutionContext, env: Env): Future[Unit] = {
env.datastores.clientCertificateValidationDataStore.setValidation(key, false, badTtl).map(_ => ())
}
private def computeFingerPrint(cert: X509Certificate): String = {
Hex.encodeHexString(ClientCertificateValidator.digester.digest(cert.getEncoded())).toLowerCase()
}
private def computeKeyFromChain(chain: Seq[X509Certificate]): String = {
chain.map(computeFingerPrint).mkString("-")
}
private def isCertificateChainValid(
chain: Seq[X509Certificate],
desc: ServiceDescriptor,
apikey: Option[ApiKey] = None,
user: Option[PrivateAppsUser] = None,
config: GlobalConfig
)(implicit ec: ExecutionContext, env: Env): Future[Boolean] = {
val key = computeKeyFromChain(chain) + "-" + apikey
.map(_.clientId)
.orElse(user.map(_.randomId))
.getOrElse("none") + "-" + desc.id
if (noCache) {
validateCertificateChain(chain, desc, apikey, user, config).map {
case Some(bool) => bool
case None => false
}
} else {
getLocalValidation(key).flatMap {
case Some(true) => FastFuture.successful(true)
case Some(false) => FastFuture.successful(false)
case None => {
validateCertificateChain(chain, desc, apikey, user, config).flatMap {
case Some(false) => setBadLocalValidation(key).map(_ => false)
case Some(true) => setGoodLocalValidation(key).map(_ => true)
case None => setBadLocalValidation(key).map(_ => false)
}
}
}
}
}
private def internalValidateClientCertificates[A](
request: RequestHeader,
desc: ServiceDescriptor,
apikey: Option[ApiKey] = None,
user: Option[PrivateAppsUser] = None,
config: GlobalConfig,
attrs: TypedMap
)(
f: => Future[A]
)(implicit ec: ExecutionContext, env: Env): Future[Either[Result, A]] = {
request.clientCertificateChain match {
case Some(chain) if alwaysValid => f.map(Right.apply)
case Some(chain) =>
isCertificateChainValid(chain, desc, apikey, user, config).flatMap {
case true => f.map(Right.apply)
case false =>
Errors
.craftResponseResult(
"You're not authorized here !",
Results.Forbidden,
request,
None,
None,
attrs = attrs
)
.map(Left.apply)
}
case None =>
Errors
.craftResponseResult(
"You're not authorized here !!!",
Results.Forbidden,
request,
None,
None,
attrs = attrs
)
.map(Left.apply)
}
}
def validateClientCertificates(
req: RequestHeader,
desc: ServiceDescriptor,
apikey: Option[ApiKey] = None,
user: Option[PrivateAppsUser] = None,
config: GlobalConfig,
attrs: TypedMap
)(f: => Future[Result])(implicit ec: ExecutionContext, env: Env): Future[Result] = {
internalValidateClientCertificates(req, desc, apikey, user, config, attrs)(f).map {
case Left(badResult) => badResult
case Right(goodResult) => goodResult
}
}
def wsValidateClientCertificates(
req: RequestHeader,
desc: ServiceDescriptor,
apikey: Option[ApiKey] = None,
user: Option[PrivateAppsUser] = None,
config: GlobalConfig,
attrs: TypedMap
)(
f: => Future[Either[Result, Flow[PlayWSMessage, PlayWSMessage, _]]]
)(implicit ec: ExecutionContext, env: Env): Future[Either[Result, Flow[PlayWSMessage, PlayWSMessage, _]]] = {
internalValidateClientCertificates(req, desc, apikey, user, config, attrs)(f).map {
case Left(badResult) => Left[Result, Flow[PlayWSMessage, PlayWSMessage, _]](badResult)
case Right(goodResult) => goodResult
}
}
def validateClientCertificatesGen[A](
req: RequestHeader,
desc: ServiceDescriptor,
apikey: Option[ApiKey] = None,
user: Option[PrivateAppsUser] = None,
config: GlobalConfig,
attrs: TypedMap
)(
f: => Future[Either[Result, A]]
)(implicit ec: ExecutionContext, env: Env): Future[Either[Result, A]] = {
internalValidateClientCertificates(req, desc, apikey, user, config, attrs)(f).map {
case Left(badResult) => Left[Result, A](badResult)
case Right(goodResult) => goodResult
}
}
}
class VeryNiceTrustManager(managers: Seq[X509TrustManager]) extends X509ExtendedTrustManager {
def checkClientTrusted(var1: Array[X509Certificate], var2: String): Unit = ()
def checkServerTrusted(var1: Array[X509Certificate], var2: String): Unit = ()
def getAcceptedIssuers: Array[X509Certificate] = managers.flatMap(_.getAcceptedIssuers).toArray
def checkClientTrusted(var1: Array[X509Certificate], var2: String, var3: Socket): Unit = ()
def checkServerTrusted(var1: Array[X509Certificate], var2: String, var3: Socket): Unit = ()
def checkClientTrusted(var1: Array[X509Certificate], var2: String, var3: SSLEngine): Unit = ()
def checkServerTrusted(var1: Array[X509Certificate], var2: String, var3: SSLEngine): Unit = ()
}
class FakeTrustManager(managers: Seq[X509TrustManager]) extends X509ExtendedTrustManager {
def checkClientTrusted(var1: Array[X509Certificate], var2: String): Unit = {
managers.find(m => Try(m.checkClientTrusted(var1, var2)).isSuccess)
}
def checkServerTrusted(var1: Array[X509Certificate], var2: String): Unit = {
managers.find(m => Try(m.checkServerTrusted(var1, var2)).isSuccess)
}
def getAcceptedIssuers: Array[X509Certificate] = managers.flatMap(_.getAcceptedIssuers).toArray
def checkClientTrusted(var1: Array[X509Certificate], var2: String, var3: Socket): Unit = {
managers.find {
case m: X509ExtendedTrustManager => Try(m.checkClientTrusted(var1, var2, var3)).isSuccess
case m: X509TrustManager => Try(m.checkClientTrusted(var1, var2)).isSuccess
}
}
def checkServerTrusted(var1: Array[X509Certificate], var2: String, var3: Socket): Unit = {
managers.find {
case m: X509ExtendedTrustManager => Try(m.checkServerTrusted(var1, var2, var3)).isSuccess
case m: X509TrustManager => Try(m.checkServerTrusted(var1, var2)).isSuccess
}
}
def checkClientTrusted(var1: Array[X509Certificate], var2: String, var3: SSLEngine): Unit = {
managers.find {
case m: X509ExtendedTrustManager => Try(m.checkClientTrusted(var1, var2, var3)).isSuccess
case m: X509TrustManager => Try(m.checkClientTrusted(var1, var2)).isSuccess
}
}
def checkServerTrusted(var1: Array[X509Certificate], var2: String, var3: SSLEngine): Unit = {
managers.find {
case m: X509ExtendedTrustManager => Try(m.checkServerTrusted(var1, var2, var3)).isSuccess
case m: X509TrustManager => Try(m.checkServerTrusted(var1, var2)).isSuccess
}
}
}
object SSLImplicits {
import collection.JavaConverters._
private val logger = Logger("otoroshi-ssl-implicits")
implicit class EnhancedCertificate(val cert: java.security.cert.Certificate) extends AnyVal {
def asPem: String =
s"${PemHeaders.BeginCertificate}\n${Base64.getEncoder.encodeToString(cert.getEncoded).grouped(64).mkString("\n")}\n${PemHeaders.EndCertificate}\n"
}
implicit class EnhancedX509Certificate(val cert: X509Certificate) extends AnyVal {
def encoded: String = Base64.getEncoder.encodeToString(cert.getEncoded)
def encodedAndPadded: String = encoded.grouped(64).mkString("\n")
def asPem: String =
s"${PemHeaders.BeginCertificate}\n${encodedAndPadded}\n${PemHeaders.EndCertificate}\n"
def altNames: Seq[String] =
CertInfo.getSubjectAlternativeNames(cert.getSubjectDN.toString, cert, logger).asScala.toSeq
def rawDomain: Option[String] = {
Option(DN(cert.getSubjectDN.getName).stringify)
.flatMap(_.split(",").toSeq.map(_.trim).find(_.toLowerCase.startsWith("cn=")))
.map(_.replace("CN=", "").replace("cn=", ""))
}
def maybeDomain: Option[String] = domains.headOption
def domain: String = domains.headOption.getOrElse(cert.getSubjectDN.getName)
def domains: Seq[String] = (rawDomain ++ altNames).toSeq
def asJson: JsObject =
Json.obj(
"subjectDN" -> DN(cert.getSubjectDN.getName).stringify,
"issuerDN" -> DN(cert.getIssuerDN.getName).stringify,
"notAfter" -> cert.getNotAfter.getTime,
"notBefore" -> cert.getNotBefore.getTime,
"serialNumber" -> cert.getSerialNumber.toString(16),
"subjectCN" -> Option(DN(cert.getSubjectDN.getName).stringify)
.flatMap(_.split(",").toSeq.map(_.trim).find(_.startsWith("CN=")))
.map(_.replace("CN=", ""))
.getOrElse(DN(cert.getSubjectDN.getName).stringify)
.asInstanceOf[String],
"issuerCN" -> Option(DN(cert.getIssuerDN.getName).stringify)
.flatMap(_.split(",").toSeq.map(_.trim).find(_.startsWith("CN=")))
.map(_.replace("CN=", ""))
.getOrElse(DN(cert.getIssuerDN.getName).stringify)
.asInstanceOf[String]
)
}
implicit class EnhancedKey(val key: java.security.Key) extends AnyVal {
def asPublicKeyPem: String =
s"${PemHeaders.BeginPublicKey}\n${Base64.getEncoder.encodeToString(key.getEncoded).grouped(64).mkString("\n")}\n${PemHeaders.EndPublicKey}\n"
def asPrivateKeyPem: String =
s"${PemHeaders.BeginPrivateKey}\n${Base64.getEncoder.encodeToString(key.getEncoded).grouped(64).mkString("\n")}\n${PemHeaders.EndPrivateKey}\n"
}
implicit class EnhancedPublicKey(val key: PublicKey) extends AnyVal {
def encoded: String = Base64.getEncoder.encodeToString(key.getEncoded)
def asPem: String =
s"${PemHeaders.BeginPublicKey}\n${Base64.getEncoder.encodeToString(key.getEncoded).grouped(64).mkString("\n")}\n${PemHeaders.EndPublicKey}\n"
}
implicit class EnhancedPrivateKey(val key: PrivateKey) extends AnyVal {
def encoded: String = Base64.getEncoder.encodeToString(key.getEncoded)
def encodedAndPadded: String = encoded.grouped(64).mkString("\n")
def asPem: String =
s"${PemHeaders.BeginPrivateKey}\n${Base64.getEncoder.encodeToString(key.getEncoded).grouped(64).mkString("\n")}\n${PemHeaders.EndPrivateKey}\n"
}
implicit class EnhancedPKCS10CertificationRequest(val csr: PKCS10CertificationRequest) extends AnyVal {
def asPem: String =
s"${PemHeaders.BeginCertificateRequest}\n${Base64.getEncoder.encodeToString(csr.getEncoded).grouped(64).mkString("\n")}\n${PemHeaders.EndCertificateRequest}\n"
}
}
object SSLSessionJavaHelper {
val NotAllowed = "CN=NotAllowedCert"
val BadDN = s"$NotAllowed, OU=Auto Generated Certs, OU=Otoroshi Certificates, O=Otoroshi"
def computeKey(session: SSLSession): Option[String] = {
computeKey(session.toString)
}
def computeKey(session: String): Option[String] = {
Try(session.split(",")(0).replace("[", "")).toOption.map { header =>
val idAndAlg = header.replace("Session(", "").replace(")", "")
idAndAlg.contains("|") match {
case true => idAndAlg.split('|').toSeq.head
case false => idAndAlg
}
}
}
}
import scala.util.control.NoStackTrace
case class NoCertificateFoundException(hostname: String)
extends RuntimeException(s"No certificate found for: $hostname !")
with NoStackTrace
case class NoHostFoundException() extends RuntimeException(s"No hostname or aliases found !") with NoStackTrace
case class NoAliasesFoundException() extends RuntimeException(s"No aliases found in SSLContext !") with NoStackTrace
case class NoHostnameFoundException() extends RuntimeException(s"No hostname found in SSLContext !") with NoStackTrace
case class RawCertificate(
pemChain: List[String],
pemPrivateKey: String,
password: Option[String] = None,
ca: Boolean = false,
client: Boolean = false
) {
import SSLImplicits._
def matchesDomain(dom: String): Boolean = sans.exists(d => RegexPool.apply(d).matches(dom))
lazy val cryptoKeyPair: KeyPair = {
val privkey = DynamicSSLEngineProvider.readPrivateKeyUniversal(domain, pemPrivateKey, password).right.get
val pubkey: PublicKey = certificate.get.getPublicKey
new KeyPair(pubkey, privkey)
}
lazy val certificatesChain: List[X509Certificate] = {
pemChain.flatMap(str => DynamicSSLEngineProvider.readCertificateChain("", str))
}
lazy val certificate: Option[X509Certificate] = certificatesChain.headOption
lazy val certificatesChainArray: Array[X509Certificate] = certificatesChain.toArray
lazy val from: DateTime = new DateTime(certificate.get.getNotBefore)
lazy val to: DateTime = new DateTime(certificate.get.getNotAfter)
lazy val name: String = certificate.get.getSubjectDN.toString
lazy val domain: String = certificate.get.domain
lazy val serial: String = certificate.get.getSerialNumber.toString()
lazy val sans: List[String] = certificate.get.domains.toList
lazy val cleanChain: String = {
pemChain
.map(c => s"${PemHeaders.BeginCertificate}\n$c${PemHeaders.EndCertificate}")
.flatMap(_.split("\\n"))
.filterNot(_.trim.isEmpty)
.mkString("\n")
}
}
object RawCertificate {
def fromBundle(bundle: String): Option[RawCertificate] = Try {
var started = false
var chain = List.empty[String]
var pkey = ""
var current = ""
bundle.split("\\n").foreach { raw =>
raw.trim match {
case line if !started && line.startsWith(PemHeaders.BeginCertificate) => {
started = true
current = line
}
case line if started && line.startsWith(PemHeaders.EndCertificate) => {
started = false
chain = chain :+ (current + "\n" + line)
}
case line if !started && line.startsWith(PemHeaders.BeginPrivateKey) => {
started = true
current = line
}
case line if started && line.startsWith(PemHeaders.EndPrivateKey) => {
started = false
pkey = (current + "\n" + line)
}
case line if !started && line.startsWith(PemHeaders.BeginPrivateRSAKey) => {
started = true
current = line
}
case line if started && line.startsWith(PemHeaders.EndPrivateRSAKey) => {
started = false
pkey = (current + "\n" + line)
}
case line if !started && line.startsWith(PemHeaders.BeginPrivateECKey) => {
started = true
current = line
}
case line if started && line.startsWith(PemHeaders.EndPrivateECKey) => {
started = false
pkey = (current + "\n" + line)
}
case line if !started && line.isEmpty => ()
case line if started && line.isEmpty => ()
case line if started => current = current + "\n" + line
case _ => ()
}
}
RawCertificate(chain, pkey)
}.toOption
def fromChainAndKey(chain: String, pkey: String): Option[RawCertificate] = fromBundle(pkey + "\n\n" + chain)
def fromChainAndKeyFiles(chainFile: File, pkeyFile: File): Option[RawCertificate] =
fromChainAndKey(Files.readString(chainFile.toPath), Files.readString(pkeyFile.toPath))
def fromBundleFile(file: File): Option[RawCertificate] = fromBundle(Files.readString(file.toPath))
def fromChainAndKeyFilesPath(chainPath: String, pkeyPath: String): Option[RawCertificate] =
fromChainAndKeyFiles(new File(chainPath), new File(pkeyPath))
def fromBundleFilePath(path: String): Option[RawCertificate] = fromBundleFile(new File(path))
def from(path: String): Option[RawCertificate] = fromBundleFile(new File(path))
def from(file: File): Option[RawCertificate] = fromBundleFile(file)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy