From d7b28ec5352159632cba1c5223b7ac767b2091ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 17 Feb 2023 11:54:59 +0100 Subject: [PATCH] feature/Add endpoint mtlsClientCertificateInfo v5.1.0 --- .../SwaggerDefinitionsJSON.scala | 12 ++++- .../src/main/scala/code/api/util/X509.scala | 44 +++++++++++++++++++ .../scala/code/api/v5_1_0/APIMethods510.scala | 39 +++++++++++++++- .../code/api/v5_1_0/JSONFactory5.1.0.scala | 9 ++++ 4 files changed, 102 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index cd0a5c7fa..0648ae15d 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -11,7 +11,7 @@ import code.api.dynamic.endpoint.helper.practise.PractiseEndpoint import code.api.util.APIUtil.{defaultJValue, _} import code.api.util.ApiRole._ import code.api.util.ExampleValue._ -import code.api.util.{ApiTrigger, ExampleValue} +import code.api.util.{APIUtil, ApiTrigger, ExampleValue} import code.api.v2_2_0.JSONFactory220.{AdapterImplementationJson, MessageDocJson, MessageDocsJson} import code.api.v3_0_0.JSONFactory300.createBranchJsonV300 import code.api.v3_0_0.custom.JSONFactoryCustom300 @@ -36,6 +36,7 @@ import com.openbankproject.commons.util.{ApiVersion, FieldNameApiVersions, Refle import net.liftweb.json import java.net.URLEncoder +import code.api.v5_1_0.CertificateInfoJsonV510 import code.endpointMapping.EndpointMappingCommons import scala.collection.immutable.List @@ -4154,6 +4155,15 @@ object SwaggerDefinitionsJSON { val oAuth2ServerJWKURIJson = OAuth2ServerJWKURIJson("https://www.googleapis.com/oauth2/v3/certs") val oAuth2ServerJwksUrisJson = OAuth2ServerJwksUrisJson(List(oAuth2ServerJWKURIJson)) + + val certificateInfoJsonV510 = CertificateInfoJsonV510( + subject_dn = "OID.2.5.4.41=VPN, EMAILADDRESS=admin@tesobe.com, CN=TESOBE CA, OU=TESOBE Operations, O=TESOBE, L=Berlin, ST=Berlin, C=DE", + issuer_dn = "CN=localhost, O=TESOBE GmbH, ST=Berlin, C=DE", + not_before = "2022-04-01T10:13:00.000Z", + not_after = "2032-04-01T10:13:00.000Z", + roles = None, + roles_info = Some("PEM Encoded Certificate does not contain PSD2 roles.") + ) val updateAccountRequestJsonV310 = UpdateAccountRequestJsonV310( label = "Label", diff --git a/obp-api/src/main/scala/code/api/util/X509.scala b/obp-api/src/main/scala/code/api/util/X509.scala index 9f2f0108d..42aa7d529 100644 --- a/obp-api/src/main/scala/code/api/util/X509.scala +++ b/obp-api/src/main/scala/code/api/util/X509.scala @@ -5,6 +5,7 @@ import java.security.PublicKey import java.security.cert.{CertificateExpiredException, CertificateNotYetValidException, X509Certificate} import java.security.interfaces.{ECPublicKey, RSAPublicKey} +import code.api.v5_1_0.CertificateInfoJsonV510 import code.util.Helper.MdcLoggable import com.github.dwickern.macros.NameOf import com.nimbusds.jose.jwk.RSAKey @@ -202,5 +203,48 @@ object X509 extends MdcLoggable { val rsaJWK = RSAKey.parse(cert) rsaJWK } + + + private def extractCertificateInfo(pem: String): Box[CertificateInfoJsonV510] = { + // Parse X.509 certificate + val cert: X509Certificate = X509CertUtils.parse(pem) + if (cert == null) { + // Parsing failed + Failure(ErrorMessages.X509ParsingFailed) + } else { + val subjectDN = cert.getSubjectDN().getName() + val issuerDN = cert.getIssuerDN().getName() + val notBefore = cert.getNotBefore() + val notAfter = cert.getNotAfter() + var roles: Option[List[String]] = None + var rolesInfo: Option[String] = None + try { + val qcstatements = extractQcStatements(cert) + val asn1encodable = extractPsd2QcStatements(qcstatements) + roles = Some(getPsd2Roles(asn1encodable: Array[ASN1Encodable])) + } + catch { + case _: Throwable => + Failure(ErrorMessages.X509ThereAreNoPsd2Roles) + rolesInfo = Some("PEM Encoded Certificate does not contain PSD2 roles.") + } + val result = CertificateInfoJsonV510( + subject_dn = subjectDN, + issuer_dn = issuerDN, + not_before = APIUtil.formatDate(notBefore), + not_after = APIUtil.formatDate(notAfter), + roles = roles, + roles_info = rolesInfo + ) + Full(result) + } + } + + def getCertificateInfo(pem: Option[String]): Box[CertificateInfoJsonV510] = { + pem match { + case Some(value) => extractCertificateInfo(value) + case None => Failure(ErrorMessages.X509CannotGetCertificate) + } + } } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 0901f9c1f..3f2d6c539 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -7,7 +7,7 @@ import code.api.util.APIUtil._ import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages.{$UserNotLoggedIn, BankNotFound, ConsentNotFound, InvalidJsonFormat, UnknownError, UserNotFoundByUserId, UserNotLoggedIn, _} -import code.api.util.{ApiRole, NewStyle} +import code.api.util.{APIUtil, ApiRole, NewStyle, X509} import code.api.util.NewStyle.HttpCode import code.api.v3_0_0.JSONFactory300.createAggregateMetricJson import code.api.v3_1_0.ConsentJsonV310 @@ -208,6 +208,43 @@ trait APIMethods510 { } } } + + + staticResourceDocs += ResourceDoc( + mtlsClientCertificateInfo, + implementedInApiVersion, + nameOf(mtlsClientCertificateInfo), + "GET", + "/my/mtls/certificate/info", + "Provide client's certificate info of a current call", + s""" + |Provide client's certificate info of a current call specified by PSD2-CERT value at Request Header + | + |${authenticationRequiredMessage(true)} + | + """.stripMargin, + EmptyBody, + certificateInfoJsonV510, + List( + UserNotLoggedIn, + BankNotFound, + UnknownError + ), + List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2, apiTagNewStyle) + ) + lazy val mtlsClientCertificateInfo: OBPEndpoint = { + case "my" :: "mtls" :: "certificate" :: "info" :: Nil JsonGet _ => { + cc => + for { + (Full(_), callContext) <- authenticatedAccess(cc) + info <- Future(X509.getCertificateInfo(APIUtil.`getPSD2-CERT`(cc.requestHeaders))) map { + unboxFullOrFail(_, callContext, X509GeneralError) + } + } yield { + (info, HttpCode.`200`(callContext)) + } + } + } staticResourceDocs += ResourceDoc( diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index d9a8a5f22..efb59036f 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -47,6 +47,15 @@ case class APIInfoJsonV510( resource_docs_requires_role: Boolean ) +case class CertificateInfoJsonV510( + subject_dn: String, + issuer_dn: String, + not_before: String, + not_after: String, + roles: Option[List[String]], + roles_info: Option[String] = None + ) + object JSONFactory510 { def getApiInfoJSON(apiVersion : ApiVersion, apiVersionStatus: String) = { val organisation = APIUtil.getPropsValue("hosted_by.organisation", "TESOBE")