Skip to content

Commit

Permalink
fix #2050 and fix #2049
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieuancelin committed Dec 11, 2024
1 parent c21f1e0 commit 8cb8555
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 42 deletions.
2 changes: 1 addition & 1 deletion otoroshi/app/gateway/handlers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ class GatewayRequestHandler(

def ocsp() =
actionBuilder.async(sourceBodyParser) { req =>
env.ocspResponder.respond(req, req.body)
env.ocspResponder.respond(req, req.body, Seq.empty)
}

def aia(id: String) =
Expand Down
95 changes: 83 additions & 12 deletions otoroshi/app/next/plugins/otoroshi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,44 @@ class OtoroshiInfos extends NgRequestTransformer {
}
}

case class PossibleCerts(certIds: Seq[String]) extends NgPluginConfig {
override def json: JsValue = PossibleCerts.format.writes(this)
}

object PossibleCerts {
val default = PossibleCerts(Seq.empty)
val format = new Format[PossibleCerts] {
override def writes(o: PossibleCerts): JsValue = Json.obj(
"cert_ids" -> o.certIds
)
override def reads(json: JsValue): JsResult[PossibleCerts] = Try {
PossibleCerts(
certIds = json.select("cert_ids").asOpt[Seq[String]].getOrElse(Seq.empty),
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(e) => JsSuccess(e)
}
}
val configFlow: Seq[String] = Seq("cert_ids")
val configSchema: Option[JsObject] = Some(
Json.obj(
"cert_ids" -> Json.obj(
"type" -> "select",
"array" -> true,
"label" -> s"Allowed certificates",
"props" -> Json.obj(
"optionsFrom" -> s"/bo/api/proxy/api/certificates",
"optionsTransformer" -> Json.obj(
"label" -> "name",
"value" -> "id"
)
)
)
)
)
}

class OtoroshiOCSPResponderEndpoint extends NgBackendCall {

override def steps: Seq[NgStep] = Seq(NgStep.CallBackend)
Expand All @@ -490,14 +528,15 @@ class OtoroshiOCSPResponderEndpoint extends NgBackendCall {
override def core: Boolean = true
override def name: String = "Otoroshi OCSP Responder endpoint"
override def description: Option[String] = "This plugin provide an endpoint to act as the Otoroshi OCSP Responder".some
override def defaultConfigObject: Option[NgPluginConfig] = None
override def defaultConfigObject: Option[NgPluginConfig] = PossibleCerts.default.some
override def useDelegates: Boolean = false
override def noJsForm: Boolean = true
override def configFlow: Seq[String] = Seq.empty
override def configSchema: Option[JsObject] = None
override def configFlow: Seq[String] = PossibleCerts.configFlow
override def configSchema: Option[JsObject] = PossibleCerts.configSchema

override def callBackend(ctx: NgbBackendCallContext, delegates: () => Future[Either[NgProxyEngineError, BackendCallResponse]])(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[NgProxyEngineError, BackendCallResponse]] = {
env.ocspResponder.respond(ctx.rawRequest, ctx.request.body).map { res =>
val config = ctx.cachedConfig(internalName)(PossibleCerts.format).getOrElse(PossibleCerts.default)
env.ocspResponder.respond(ctx.rawRequest, ctx.request.body, config.certIds).map { res =>
Right(BackendCallResponse(NgPluginHttpResponse.fromResult(res), None))
}
}
Expand Down Expand Up @@ -542,14 +581,15 @@ class OtoroshiJWKSEndpoint extends NgBackendCall {
override def core: Boolean = true
override def name: String = "Otoroshi JWKS endpoint"
override def description: Option[String] = "This plugin provide an endpoint to return Otoroshi JWKS data".some
override def defaultConfigObject: Option[NgPluginConfig] = None
override def defaultConfigObject: Option[NgPluginConfig] = PossibleCerts.default.some
override def useDelegates: Boolean = false
override def noJsForm: Boolean = true
override def configFlow: Seq[String] = Seq.empty
override def configSchema: Option[JsObject] = None
override def configFlow: Seq[String] = PossibleCerts.configFlow
override def configSchema: Option[JsObject] = PossibleCerts.configSchema

override def callBackend(ctx: NgbBackendCallContext, delegates: () => Future[Either[NgProxyEngineError, BackendCallResponse]])(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[NgProxyEngineError, BackendCallResponse]] = {
JWKSHelper.jwks(ctx.rawRequest, Seq.empty).map {
val config = ctx.cachedConfig(internalName)(PossibleCerts.format).getOrElse(PossibleCerts.default)
JWKSHelper.jwks(ctx.rawRequest, config.certIds).map {
case Left(body) => Results.NotFound(body)
case Right(body) => Results.Ok(body)
} map { res =>
Expand Down Expand Up @@ -583,6 +623,36 @@ class OtoroshiHealthEndpoint extends NgBackendCall {
}
}

case class OtoroshiMetricsEndpointConfig(filter: Option[String]) extends NgPluginConfig {
override def json: JsValue = OtoroshiMetricsEndpointConfig.format.writes(this)
}

object OtoroshiMetricsEndpointConfig {
val default = OtoroshiMetricsEndpointConfig(None)
val format = new Format[OtoroshiMetricsEndpointConfig] {
override def writes(o: OtoroshiMetricsEndpointConfig): JsValue = Json.obj(
"filter" -> o.filter.map(_.json).getOrElse(JsNull).asValue
)
override def reads(json: JsValue): JsResult[OtoroshiMetricsEndpointConfig] = Try {
OtoroshiMetricsEndpointConfig(
filter = json.select("filter").asOpt[String],
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(e) => JsSuccess(e)
}
}
val configFlow: Seq[String] = Seq("filter")
val configSchema: Option[JsObject] = Some(
Json.obj(
"cert_ids" -> Json.obj(
"type" -> "string",
"label" -> s"Filter metrics"
)
)
)
}

class OtoroshiMetricsEndpoint extends NgBackendCall {

override def steps: Seq[NgStep] = Seq(NgStep.CallBackend)
Expand All @@ -592,15 +662,16 @@ class OtoroshiMetricsEndpoint extends NgBackendCall {
override def core: Boolean = true
override def name: String = "Otoroshi Metrics endpoint"
override def description: Option[String] = "This plugin provide an endpoint to return Otoroshi metrics data for the current node".some
override def defaultConfigObject: Option[NgPluginConfig] = None
override def defaultConfigObject: Option[NgPluginConfig] = OtoroshiMetricsEndpointConfig.default.some
override def useDelegates: Boolean = false
override def noJsForm: Boolean = true
override def configFlow: Seq[String] = Seq.empty
override def configSchema: Option[JsObject] = None
override def configFlow: Seq[String] = OtoroshiMetricsEndpointConfig.configFlow
override def configSchema: Option[JsObject] = OtoroshiMetricsEndpointConfig.configSchema

override def callBackend(ctx: NgbBackendCallContext, delegates: () => Future[Either[NgProxyEngineError, BackendCallResponse]])(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[NgProxyEngineError, BackendCallResponse]] = {
val config = ctx.cachedConfig(internalName)(OtoroshiMetricsEndpointConfig.format).getOrElse(OtoroshiMetricsEndpointConfig.default)
val format = ctx.rawRequest.getQueryString("format")
val filter = ctx.rawRequest.getQueryString("filter")
val filter = ctx.rawRequest.getQueryString("filter").orElse(config.filter)
val acceptsJson = ctx.rawRequest.accepts("application/json")
val acceptsProm = ctx.rawRequest.accepts("application/prometheus")
val res = HealthController.fetchMetrics(format, acceptsJson, acceptsProm, filter)
Expand Down
47 changes: 18 additions & 29 deletions otoroshi/app/ssl/ocsp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,9 @@ import otoroshi.env.Env
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers
import org.bouncycastle.asn1.x509.{CRLReason, Extension, Extensions, SubjectPublicKeyInfo}
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.ocsp.{
BasicOCSPRespBuilder,
CertificateID,
CertificateStatus,
OCSPReq,
OCSPResp,
OCSPRespBuilder,
Req,
RespID,
RevokedStatus,
UnknownStatus
}
import org.bouncycastle.cert.ocsp.{BasicOCSPRespBuilder, CertificateID, CertificateStatus, OCSPReq, OCSPResp, OCSPRespBuilder, Req, RespID, RevokedStatus, UnknownStatus}
import org.bouncycastle.operator.{ContentSigner, DefaultDigestAlgorithmIdentifierFinder, DigestCalculatorProvider}
import org.bouncycastle.operator.jcajce.{
JcaContentSignerBuilder,
JcaContentVerifierProviderBuilder,
JcaDigestCalculatorProviderBuilder
}
import org.bouncycastle.operator.jcajce.{JcaContentSignerBuilder, JcaContentVerifierProviderBuilder, JcaDigestCalculatorProviderBuilder}
import play.api.mvc.{RequestHeader, Result, Results}
import play.api.libs.json.Json
import otoroshi.ssl._
Expand All @@ -39,6 +24,7 @@ import otoroshi.utils.http.DN
import otoroshi.utils.syntax.implicits._
import otoroshi.ssl.SSLImplicits.EnhancedX509Certificate

import java.math.BigInteger
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}

Expand Down Expand Up @@ -141,7 +127,7 @@ class OcspResponder(env: Env, implicit val ec: ExecutionContext) {
}
}

def respond(req: RequestHeader, body: Source[ByteString, _])(implicit ec: ExecutionContext): Future[Result] = {
def respond(req: RequestHeader, body: Source[ByteString, _], possibleCerts: Seq[String])(implicit ec: ExecutionContext): Future[Result] = {
body.runFold(ByteString.empty)(_ ++ _).flatMap { bs =>
if (bs.isEmpty) {
FastFuture.successful(
Expand All @@ -153,7 +139,7 @@ class OcspResponder(env: Env, implicit val ec: ExecutionContext) {
if (ocspReq.isSigned && !isSignatureValid(ocspReq)) {
Results.BadRequest(new OCSPRespBuilder().build(OCSPRespBuilder.MALFORMED_REQUEST, null).getEncoded).future
} else {
manageRequest(ocspReq).map { response =>
manageRequest(ocspReq, possibleCerts.flatMap(id => env.proxyState.certificate(id).flatMap(_.serialNumberLng))).map { response =>
Results.Ok(response.getEncoded)
} recover { case e: Throwable =>
logger.error("error while checking certificate", e)
Expand All @@ -164,7 +150,7 @@ class OcspResponder(env: Env, implicit val ec: ExecutionContext) {
}
}

def manageRequest(ocspReq: OCSPReq): Future[OCSPResp] = {
private def manageRequest(ocspReq: OCSPReq, possibleCerts: Seq[BigInteger]): Future[OCSPResp] = {
for {
optRootCA <- env.datastores.certificatesDataStore.findById(Cert.OtoroshiCA)(ec, env)
optIntermediateCA <- env.datastores.certificatesDataStore.findById(Cert.OtoroshiIntermediateCA)(ec, env)
Expand Down Expand Up @@ -205,7 +191,7 @@ class OcspResponder(env: Env, implicit val ec: ExecutionContext) {
// Check that each request is valid and put the appropriate response in the builder
val requests = ocspReq.getRequestList
requests.foreach { request =>
addResponse(responseBuilder, request, issuingCertificate, digestCalculatorProvider)
addResponse(responseBuilder, request, issuingCertificate, digestCalculatorProvider, possibleCerts)
}

val signingCertificateChain: Array[X509CertificateHolder] =
Expand All @@ -222,11 +208,12 @@ class OcspResponder(env: Env, implicit val ec: ExecutionContext) {
}
}

def addResponse(
private def addResponse(
responseBuilder: BasicOCSPRespBuilder,
request: Req,
issuingCertificate: JcaX509CertificateHolder,
digestCalculatorProvider: DigestCalculatorProvider
digestCalculatorProvider: DigestCalculatorProvider,
possibleCerts: Seq[BigInteger],
): Unit = {
val certificateID = request.getCertID

Expand All @@ -252,23 +239,25 @@ class OcspResponder(env: Env, implicit val ec: ExecutionContext) {
)

} else {
val certificateStatus = DynamicSSLEngineProvider._ocspProjectionCertificates.get(certificateID.getSerialNumber)

val r = DynamicSSLEngineProvider._ocspProjectionCertificates.get(certificateID.getSerialNumber)
val certificateStatus = if (possibleCerts.isEmpty) r else {
if (possibleCerts.contains(certificateID.getSerialNumber)) r else None
}
getOCSPCertificateStatus(certificateStatus).foreach(value => {
responseBuilder.addResponse(request.getCertID, value._1, value._2.toDate, value._3.toDate, extensions)
})
}
}

def getUnknownStatus: CertificateStatus = {
private def getUnknownStatus: CertificateStatus = {
if (rejectUnknown) {
new RevokedStatus(DateTime.now().toDate, CRLReason.unspecified)
} else {
new UnknownStatus()
}
}

def getOCSPCertificateStatus(
private def getOCSPCertificateStatus(
certData: Option[OCSPCertProjection]
): Option[(CertificateStatus, DateTime, DateTime)] = {
certData match {
Expand All @@ -288,7 +277,7 @@ class OcspResponder(env: Env, implicit val ec: ExecutionContext) {
}
}

def getCRLReason(revocationReason: String): Int = {
private def getCRLReason(revocationReason: String): Int = {
revocationReason match {
case "UNSPECIFIED" => CRLReason.unspecified
case "KEY_COMPROMISE" => CRLReason.keyCompromise
Expand All @@ -304,7 +293,7 @@ class OcspResponder(env: Env, implicit val ec: ExecutionContext) {
}
}

def isSignatureValid(ocspReq: OCSPReq): Boolean =
private def isSignatureValid(ocspReq: OCSPReq): Boolean =
ocspReq.isSignatureValid(
new JcaContentVerifierProviderBuilder()
.setProvider("BC")
Expand Down

0 comments on commit 8cb8555

Please sign in to comment.