From 40b5f53077b8943ab38c028aa66ff0d3fc44bdf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 3 Apr 2025 10:20:49 +0200 Subject: [PATCH 1/7] refactor/Enhance Berlin Group Signing/Verifying Request Process --- .../code/api/util/BerlinGroupSigning.scala | 159 +++++++++--------- .../code/api/util/CertificateVerifier.scala | 11 +- .../src/main/scala/code/api/util/X509.scala | 16 ++ 3 files changed, 99 insertions(+), 87 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index 05fc9b9d9..7034f54e0 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -10,23 +10,19 @@ import java.nio.charset.StandardCharsets import java.nio.file.{Files, Paths} import java.security._ import java.security.cert.{CertificateFactory, X509Certificate} -import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec} -import java.util.Base64 +import java.security.spec.PKCS8EncodedKeySpec +import java.text.SimpleDateFormat +import java.util.{Base64, Date, UUID} import scala.util.matching.Regex object BerlinGroupSigning extends MdcLoggable { // Step 1: Calculate Digest (SHA-256 Hash of the Body) - def calculateDigest(body: String): String = { - def removeFirstAndLastQuotes(input: String): String = { - if (input.startsWith("\"") && input.endsWith("\"") && input.length > 1) { - input.tail.init - } else { - input - } - } - val digest = MessageDigest.getInstance("SHA-256").digest(removeFirstAndLastQuotes(body).getBytes(StandardCharsets.UTF_8)) - Base64.getEncoder.encodeToString(digest) + def generateDigest(body: String): String = { + val sha256Digest = MessageDigest.getInstance("SHA-256") + val digest = sha256Digest.digest(body.getBytes("UTF-8")) + val base64Digest = Base64.getEncoder.encodeToString(digest) + s"SHA-256=$base64Digest" } // Step 2: Create Signing String (Concatenation of required headers) @@ -63,42 +59,30 @@ object BerlinGroupSigning extends MdcLoggable { } // Step 4: Attach Certificate (Load from PEM String) - def loadCertificate(certPem: String) = { - val certString = certPem - .replaceAll("-----BEGIN CERTIFICATE-----", "") // Remove the BEGIN header - .replaceAll("-----END CERTIFICATE-----", "") // Remove the END footer - .replaceAll("\\s", "") // Remove all whitespace and new lines - // Decode Base64 public key - val keyBytes = Base64.getDecoder.decode(certPem) - val keySpec = new X509EncodedKeySpec(keyBytes) - val keyFactory = KeyFactory.getInstance("RSA") - val publicKey = keyFactory.generatePublic(keySpec) - publicKey - // val certBytes = Base64.getDecoder.decode(certString) - // Base64.getEncoder.encodeToString(certBytes) - } // Step 5: Verify Request on ASPSP Side - def verifySignature(signingString: String, signatureStr: String, certPem: String): Boolean = { - val publicKey = loadPublicKeyFromCert(certPem) + def verifySignature(signingString: String, signatureStr: String, publicKey: PublicKey): Boolean = { + logger.debug(s"signingString: $signingString") + logger.debug(s"signatureStr: $signatureStr") val signature = Signature.getInstance("SHA256withRSA") signature.initVerify(publicKey) signature.update(signingString.getBytes(StandardCharsets.UTF_8)) signature.verify(Base64.getDecoder.decode(signatureStr)) } - // Extract Public Key from PEM Certificate String - def loadPublicKeyFromCert(certPem: String): PublicKey = { - val certString = certPem - .replaceAll("-----BEGIN CERTIFICATE-----", "") // Remove the BEGIN header - .replaceAll("-----END CERTIFICATE-----", "") // Remove the END footer - .replaceAll("\\s", "") // Remove all whitespace and new lines + def parseCertificate(certString: String): X509Certificate = { + // Decode Base64 certificate + val certBytes = Base64.getDecoder.decode( + certString.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replaceAll("\\s", "") + ) - val certBytes = Base64.getDecoder.decode(certString) + // Parse certificate val certFactory = CertificateFactory.getInstance("X.509") - val cert = certFactory.generateCertificate(new java.io.ByteArrayInputStream(certBytes)).asInstanceOf[X509Certificate] - cert.getPublicKey + val certificate = certFactory.generateCertificate(new java.io.ByteArrayInputStream(certBytes)).asInstanceOf[X509Certificate] + certificate } @@ -123,10 +107,10 @@ object BerlinGroupSigning extends MdcLoggable { forwardResult case true => val requestHeaders = forwardResult._2.map(_.requestHeaders).getOrElse(Nil) - val certificatePem: String = getPem(requestHeaders) - X509.validate(certificatePem) match { + val certificate = getCertificateFromTppSignatureCertificate(requestHeaders) + X509.validateCertificate(certificate) match { case Full(true) => // PEM certificate is ok - val digest = calculateDigest(body.getOrElse("")) + val digest = generateDigest(body.getOrElse("")) val signatureHeaderValue = getHeaderValue(RequestHeader.Signature, requestHeaders) @@ -134,14 +118,14 @@ object BerlinGroupSigning extends MdcLoggable { val headersToSign = parseSignatureHeader(signatureHeaderValue).getOrElse("headers", "").split(" ").toList val headers = headersToSign.map(h => if(h.toLowerCase() == RequestHeader.Digest.toLowerCase()) { - s"$h: SHA-256=$digest" + s"$h: $digest" } else { s"$h: ${getHeaderValue(h, requestHeaders)}" } ) val signingString = headers.mkString("\n") - val isVerified = verifySignature(signingString, signature, certificatePem) - val isValidated = CertificateVerifier.validateCertificate(certificatePem) + val isVerified = verifySignature(signingString, signature, certificate.getPublicKey) + val isValidated = CertificateVerifier.validateCertificate(certificate) val bypassValidation = APIUtil.getPropsAsBoolValue("bypass_tpp_signature_validation", defaultValue = false) (isVerified, isValidated) match { case (true, true) => forwardResult @@ -159,13 +143,18 @@ object BerlinGroupSigning extends MdcLoggable { requestHeaders.find(_.name.toLowerCase() == name.toLowerCase()).map(_.values.mkString) .getOrElse(SecureRandomUtil.csprng.nextLong().toString) } - private def getPem(requestHeaders: List[HTTPParam]): String = { + private def getCertificateFromTppSignatureCertificate(requestHeaders: List[HTTPParam]): X509Certificate = { val certificate = getHeaderValue(RequestHeader.`TPP-Signature-Certificate`, requestHeaders) // Decode the Base64 string val decodedBytes = Base64.getDecoder.decode(certificate) // Convert the bytes to a string (it could be PEM format for public key) val decodedString = new String(decodedBytes, StandardCharsets.UTF_8) + val certificatePemString = getCertificatePem(decodedString) + parseCertificate(certificatePemString) + } + + private def getCertificatePem(decodedString: String) = { // Extract the certificate portion from the decoded string val certStart = "-----BEGIN CERTIFICATE-----" val certEnd = "-----END CERTIFICATE-----" @@ -185,11 +174,24 @@ object BerlinGroupSigning extends MdcLoggable { "" } } + private def getPrivateKeyPem(decodedString: String) = { + // Extract the certificate portion from the decoded string + val certStart = "-----BEGIN PRIVATE KEY-----" + val certEnd = "-----END PRIVATE KEY-----" - def getTppSignatureCertificate(requestHeaders: List[HTTPParam]): Option[String] = { - getPem(requestHeaders) match { - case value if value.isEmpty => None - case value => Some(value) + // Find the start and end indices of the certificate + val startIndex = decodedString.indexOf(certStart) + val endIndex = decodedString.indexOf(certEnd, startIndex) + certEnd.length + + if (startIndex >= 0 && endIndex >= 0) { + // Extract and print the certificate part + val extractedCert = decodedString.substring(startIndex, endIndex) + logger.debug("|---> Extracted Private Key:") + logger.debug(extractedCert) + extractedCert + } else { + logger.debug("|---> Private Key not found in the decoded string.") + "" } } @@ -198,63 +200,60 @@ object BerlinGroupSigning extends MdcLoggable { regex.findAllMatchIn(signatureHeader).map(m => m.group("key") -> m.group("value")).toMap } + private def getCurrentDate: String = { + val sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz") + sdf.format(new Date()) + } + // Example Usage def main(args: Array[String]): Unit = { - val requestBody = """"{ - | "access": { - | "accounts": [ - | { - | "iban": "RS35260005601001611379" - | } - | ], - | "balances": [ - | { - | "iban": "RS35260005601001611379" - | } - | ] - | }, - | "recurringIndicator": true, - | "validUntil": "2025-01-20T11:04:20Z", - | "frequencyPerDay": 10, - | "combinedServiceIndicator": false - |}"""".stripMargin - val digest = calculateDigest(requestBody) + // Digest for request + val body = new String(Files.readAllBytes(Paths.get("/path/to/request_body.json")), "UTF-8") + val digest = generateDigest(body) + + // Generate UUID for X-Request-ID + val xRequestId = UUID.randomUUID().toString + + // Get current date in RFC 7231 format + val dateHeader = getCurrentDate + - val xRequestId = "12345678" - val date = "Tue, 13 Feb 2024 10:00:00 GMT" val redirectUri = "www.redirect-uri.com" val headers = Map( RequestHeader.Digest -> s"SHA-256=$digest", RequestHeader.`X-Request-ID` -> xRequestId, - RequestHeader.Date -> date, + RequestHeader.Date -> dateHeader, RequestHeader.`TPP-Redirect-URL` -> redirectUri, ) val signingString = createSigningString(headers) // Load PEM files as strings - val privateKeyPath = "/home/marko/Downloads/BerlinGroupSigning/private_key.pem" - val certificatePath = "/home/marko/Downloads/BerlinGroupSigning/certificate.pem" + val privateKeyPath = "/path/to/private_key.pem" + val certificatePath = "/path/to/certificate.pem" - val privateKeyPem = new String(Files.readAllBytes(Paths.get(privateKeyPath))) - val certificatePem = new String(Files.readAllBytes(Paths.get(certificatePath))) + val privateKeyPem = getPrivateKeyPem(new String(Files.readAllBytes(Paths.get(privateKeyPath)))) + + val certificateFullString = new String(Files.readAllBytes(Paths.get(certificatePath))) + val certificate: X509Certificate = parseCertificate(getCertificatePem(certificateFullString)) val signature = signString(signingString, privateKeyPem) - val certificate = loadCertificate(certificatePem) println(s"1) Digest: SHA-256=$digest") println(s"2) ${RequestHeader.`X-Request-ID`}: $xRequestId") - println(s"3) ${RequestHeader.Date}: $date") + println(s"3) ${RequestHeader.Date}: $dateHeader") println(s"4) ${RequestHeader.`TPP-Redirect-URL`}: $redirectUri") val signatureHeaderValue = - s"""keyId="SN=4000000010FC01D520258AB15EAF, CA=CN=D-eSystemTrustIB, O=IP STISC 1003600096694, C-MD", algorithm="rsa-sha256", headers="digest date x-request-id tpp-redirect-uri", signature="$signature"""".stripMargin + s"""keyId="SN=43A, CA=CN=MAIB Prisacaru Sergiu (Test), O=MAIB", algorithm="rsa-sha256", headers="digest date x-request-id", signature="$signature"""" println(s"5) Signature: $signatureHeaderValue") - println(s"6) TPP-Signature-Certificate: $certificate") - val isVerified = verifySignature(signingString, signature, certificatePem) + // Convert public certificate to Base64 for Signature-Certificate header + val certificateBase64 = Base64.getEncoder.encodeToString(certificateFullString.getBytes(StandardCharsets.UTF_8)) + println(s"6) TPP-Signature-Certificate: $certificateBase64") + + val isVerified = verifySignature(signingString, signature, certificate.getPublicKey) println(s"Signature Verification: $isVerified") - val parsedSignature = parseSignatureHeader(signatureHeaderValue) println(s"Parsed Signature Header: $parsedSignature") } diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index cefb24abe..019aabd6e 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -59,10 +59,8 @@ object CertificateVerifier extends MdcLoggable { * @param pemCertificate The X.509 certificate in PEM format. * @return `true` if the certificate is valid and trusted, otherwise `false`. */ - def validateCertificate(pemCertificate: String): Boolean = { + def validateCertificate(certificate: X509Certificate): Boolean = { Try { - val certificate = parsePemToX509Certificate(pemCertificate) - // Load trust store val trustStore = loadTrustStore() .getOrElse(throw new Exception("Trust store could not be loaded.")) @@ -140,11 +138,10 @@ object CertificateVerifier extends MdcLoggable { // change the following path if using this function to test on your localhost val certificatePath = "/path/to/certificate.pem" val pemCertificate = loadPemCertificateFromFile(certificatePath) + val certificate = BerlinGroupSigning.parseCertificate(pemCertificate.getOrElse("")) - pemCertificate.foreach { pem => - val isValid = validateCertificate(pem) - logger.info(s"Certificate verification result: $isValid") - } + val isValid = validateCertificate(certificate) + logger.info(s"Certificate verification result: $isValid") loadTrustStore().foreach { trustStore => logger.info(s"Trust Store contains ${trustStore.size()} certificates.") 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 051353e07..717840cbc 100644 --- a/obp-api/src/main/scala/code/api/util/X509.scala +++ b/obp-api/src/main/scala/code/api/util/X509.scala @@ -126,6 +126,22 @@ object X509 extends MdcLoggable { Failure(ErrorMessages.X509CertificateNotYetValid) } } + } /** + * The certificate must be validated before it may be used. + * @param encodedCert PEM (BASE64) encoded certificates, suitable for copy and paste operations. + * @return Full(true) or an Failure + */ + def validateCertificate(certificate: X509Certificate): Box[Boolean] = { + try { + certificate.checkValidity() + Full(true) + } + catch { + case _: CertificateExpiredException => + Failure(ErrorMessages.X509CertificateExpired) + case _: CertificateNotYetValidException => + Failure(ErrorMessages.X509CertificateNotYetValid) + } } /** From d77ded76d2a4936a38bf00c9ec30d242bbcfed6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 4 Apr 2025 13:28:11 +0200 Subject: [PATCH 2/7] feature/Add functions in order to use p12 store --- .../scala/code/api/util/P12StoreUtil.scala | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 obp-api/src/main/scala/code/api/util/P12StoreUtil.scala diff --git a/obp-api/src/main/scala/code/api/util/P12StoreUtil.scala b/obp-api/src/main/scala/code/api/util/P12StoreUtil.scala new file mode 100644 index 000000000..9430cd358 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/P12StoreUtil.scala @@ -0,0 +1,83 @@ +package code.api.util + + +import code.api.{CertificateConstants, PrivateKeyConstants} + +import java.io.FileInputStream +import java.security.cert.X509Certificate +import java.security.{KeyStore, PrivateKey} +import java.util.Base64 +object P12StoreUtil { + + /** + * Loads a private key and its certificate from a .p12 (PKCS#12) keystore file. + * + * @param p12Path Path to the .p12 file + * @param p12Password Password for the keystore + * @param alias Alias under which the key is stored + * @return A tuple of (PrivateKey, X509Certificate) + */ + def loadPrivateKey(p12Path: String, p12Password: String, alias: String): (PrivateKey, X509Certificate) = { + // Create an instance of a KeyStore of type PKCS12 + val keyStore = KeyStore.getInstance("PKCS12") + + // Open an input stream to the .p12 file + val fis = new FileInputStream(p12Path) + + // Load the keystore with the password + keyStore.load(fis, p12Password.toCharArray) + + // Always close the input stream when done + fis.close() + + // Retrieve the private key from the keystore + val key = keyStore.getKey(alias, p12Password.toCharArray).asInstanceOf[PrivateKey] + + // Retrieve the certificate associated with the key (optional but often useful) + val cert = keyStore.getCertificate(alias).asInstanceOf[X509Certificate] + + // Return both the key and the certificate + (key, cert) + } + + def privateKeyToPEM(privateKey: PrivateKey): String = { + val base64 = Base64.getEncoder.encodeToString(privateKey.getEncoded) + + // Format as PEM with BEGIN/END markers and line breaks every 64 characters + val formatted = base64.grouped(64).mkString("\n") + + s"""${PrivateKeyConstants.BEGIN_KEY} + |$formatted + |${PrivateKeyConstants.END_KEY}""".stripMargin + } + + def certificateToPEM(cert: X509Certificate): String = { + val base64 = Base64.getEncoder.encodeToString(cert.getEncoded) + + // Format as PEM with BEGIN/END markers and line breaks every 64 characters + val formatted = base64.grouped(64).mkString("\n") + + s"""${CertificateConstants.BEGIN_CERT} + |$formatted + |${CertificateConstants.END_CERT}""".stripMargin + } + + + def main(args: Array[String]): Unit = { + val p12Path = APIUtil.getPropsValue("truststore.path.tpp_signature") + .or(APIUtil.getPropsValue("truststore.path")).getOrElse("") + val p12Password = APIUtil.getPropsValue("truststore.password.tpp_signature", "") + // Load the private key and certificate from the keystore + val (privateKey, cert) = loadPrivateKey( + p12Path = p12Path, // Replace with the actual file path + p12Password = p12Password, // Replace with the keystore password + alias = "bnm test" // Replace with the key alias + ) + + // Print information to confirm successful loading + println(s"Private key algorithm: ${privateKey.getAlgorithm}") + println(s"Certificate subject: ${cert.getSubjectDN}") + } + + +} From b021c540d1052b0fb09358ebff65dd0be226b895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 4 Apr 2025 15:23:10 +0200 Subject: [PATCH 3/7] feature/Check certificate by TPP list --- .../resources/props/sample.props.template | 6 ++- .../scala/code/api/constant/constant.scala | 4 ++ .../main/scala/code/api/util/APIUtil.scala | 25 +++++++-- .../code/api/util/BerlinGroupError.scala | 2 + .../code/api/util/BerlinGroupSigning.scala | 54 ++++++++++++++----- 5 files changed, 71 insertions(+), 20 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index bb781c6a5..f19a1bbf7 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -161,6 +161,7 @@ jwt.use.ssl=false ## Public certificate for the CA (used by clients and servers to validate signatures) # truststore.path.tpp_signature = path/to/ca.p12 # truststore.password.tpp_signature = truststore-password +# truststore.alias.tpp_signature = alias-name # Bypass TPP signature validation # bypass_tpp_signature_validation = false @@ -939,8 +940,9 @@ database_messages_scheduler_interval=3600 # # -- PSD2 Certificates -------------------------- -# In case isn't defined default value is "false" -# requirePsd2Certificates=false +# Possible cases: ONLINE, CERTIFICATE, NONE +# In case isn't defined default value is "NONE" +# requirePsd2Certificates=NONE # ----------------------------------------------- # -- OBP-API mode ------------------------------- diff --git a/obp-api/src/main/scala/code/api/constant/constant.scala b/obp-api/src/main/scala/code/api/constant/constant.scala index abc83957f..fd374593b 100644 --- a/obp-api/src/main/scala/code/api/constant/constant.scala +++ b/obp-api/src/main/scala/code/api/constant/constant.scala @@ -124,6 +124,10 @@ object CertificateConstants { final val BEGIN_CERT: String = "-----BEGIN CERTIFICATE-----" final val END_CERT: String = "-----END CERTIFICATE-----" } +object PrivateKeyConstants { + final val BEGIN_KEY: String = "-----BEGIN PRIVATE KEY-----" + final val END_KEY: String = "-----END PRIVATE KEY-----" +} object JedisMethod extends Enumeration { type JedisMethod = Value diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 8460be9ec..987492cdb 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -45,6 +45,7 @@ import code.api.oauth1a.OauthParams._ import code.api.util.APIUtil.ResourceDoc.{findPathVariableNames, isPathVariable} import code.api.util.ApiRole._ import code.api.util.ApiTag.{ResourceDocTag, apiTagBank} +import code.api.util.BerlinGroupSigning.getCertificateFromTppSignatureCertificate import code.api.util.FutureUtil.{EndpointContext, EndpointTimeout} import code.api.util.Glossary.GlossaryItem import code.api.v1_2.ErrorMessage @@ -3925,9 +3926,23 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } private def passesPsd2ServiceProviderCommon(cc: Option[CallContext], serviceProvider: String) = { - val result: Box[Boolean] = getPropsAsBoolValue("requirePsd2Certificates", false) match { - case false => Full(true) - case true => + val result = getPropsValue("requirePsd2Certificates", "NONE") match { + case value if value.toUpperCase == "ONLINE" => + val requestHeaders = cc.map(_.requestHeaders).getOrElse(Nil) + val consumerName = cc.flatMap(_.consumer.map(_.name.get)).getOrElse("") + val certificate = getCertificateFromTppSignatureCertificate(requestHeaders) + val tpp = BerlinGroupSigning.checkTpp(consumerName, certificate) + if (tpp.nonEmpty) { + val hasRole = tpp.exists(_.services.contains(serviceProvider)) + if (hasRole) { + Full(true) + } else { + Failure(X509ActionIsNotAllowed) + } + } else { + Failure("No valid Tpp") + } + case value if value.toUpperCase == "CERTIFICATE" => `getPSD2-CERT`(cc.map(_.requestHeaders).getOrElse(Nil)) match { case Some(pem) => logger.debug("PSD2-CERT pem: " + pem) @@ -3947,6 +3962,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } case None => Failure(X509CannotGetCertificate) } + case _ => + Full(true) } result } @@ -3954,7 +3971,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ def passesPsd2ServiceProvider(cc: Option[CallContext], serviceProvider: String): OBPReturnType[Box[Boolean]] = { val result = passesPsd2ServiceProviderCommon(cc, serviceProvider) Future(result) map { - x => (fullBoxOrException(x ~> APIFailureNewStyle(X509GeneralError, 400, cc.map(_.toLight))), cc) + x => (fullBoxOrException(x ~> APIFailureNewStyle(X509GeneralError, 401, cc.map(_.toLight))), cc) } } def passesPsd2Aisp(cc: Option[CallContext]): OBPReturnType[Box[Boolean]] = { diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala index 269a6d84d..320f6ec17 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala @@ -71,7 +71,9 @@ object BerlinGroupError { case "403" if message.contains("OBP-35001") => "CONSENT_UNKNOWN" + case "401" if message.contains("OBP-20300") => "CERTIFICATE_BLOCKED" case "401" if message.contains("OBP-20312") => "CERTIFICATE_INVALID" + case "401" if message.contains("OBP-20300") => "CERTIFICATE_INVALID" case "401" if message.contains("OBP-20310") => "SIGNATURE_INVALID" case "401" if message.contains("OBP-20060") => "ROLE_INVALID" diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index 7034f54e0..0c3db2242 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -1,8 +1,9 @@ package code.api.util import code.api.RequestHeader +import code.regulatedentities.MappedRegulatedEntityProvider import code.util.Helper.MdcLoggable -import com.openbankproject.commons.model.User +import com.openbankproject.commons.model.{RegulatedEntityTrait, User} import net.liftweb.common.{Box, Failure, Full} import net.liftweb.http.provider.HTTPParam @@ -17,6 +18,17 @@ import scala.util.matching.Regex object BerlinGroupSigning extends MdcLoggable { + lazy val p12Path = APIUtil.getPropsValue("truststore.path.tpp_signature") + .or(APIUtil.getPropsValue("truststore.path")).getOrElse("") + lazy val p12Password = APIUtil.getPropsValue("truststore.password.tpp_signature", "") + lazy val alias = APIUtil.getPropsValue("truststore.alias.tpp_signature", "") + // Load the private key and certificate from the keystore + lazy val (privateKey, certificate) = P12StoreUtil.loadPrivateKey( + p12Path = p12Path, // Replace with the actual file path + p12Password = p12Password, // Replace with the keystore password + alias = alias // Replace with the key alias + ) + // Step 1: Calculate Digest (SHA-256 Hash of the Body) def generateDigest(body: String): String = { val sha256Digest = MessageDigest.getInstance("SHA-256") @@ -39,7 +51,7 @@ object BerlinGroupSigning extends MdcLoggable { // Step 3: Generate Signature using RSA Private Key def signString(signingString: String, privateKeyPem: String): String = { - val privateKey = loadPrivateKey(privateKeyPem) + val privateKey: PrivateKey = loadPrivateKey(privateKeyPem) val signature = Signature.getInstance("SHA256withRSA") signature.initSign(privateKey) signature.update(signingString.getBytes(StandardCharsets.UTF_8)) @@ -85,6 +97,23 @@ object BerlinGroupSigning extends MdcLoggable { certificate } + def checkTpp(consumerName: String, certificate: X509Certificate): List[RegulatedEntityTrait] = { + // Define a regular expression to extract the value of CN, allowing for optional spaces around '=' + val cnPattern: Regex = """CN\s*=\s*([^,]+)""".r + + // Use the regular expression to find the value of CN + val extractedCN = cnPattern.findFirstMatchIn(certificate.getIssuerDN.getName) match { + case Some(m) => m.group(1) // Extract the value of CN + case None => "CN not found" + } + val issuerCommonName = extractedCN // Certificate.caCert + val serialNumber = certificate.getSerialNumber.toString + val regulatedEntities: List[RegulatedEntityTrait] = + MappedRegulatedEntityProvider.getRegulatedEntities() + .filter(_.entityName == consumerName) + regulatedEntities + } + /** * Verifies Signed Request. It assumes that Customers has a sored certificate. @@ -112,12 +141,11 @@ object BerlinGroupSigning extends MdcLoggable { case Full(true) => // PEM certificate is ok val digest = generateDigest(body.getOrElse("")) - val signatureHeaderValue = getHeaderValue(RequestHeader.Signature, requestHeaders) val signature = parseSignatureHeader(signatureHeaderValue).getOrElse("signature", "NONE") val headersToSign = parseSignatureHeader(signatureHeaderValue).getOrElse("headers", "").split(" ").toList val headers = headersToSign.map(h => - if(h.toLowerCase() == RequestHeader.Digest.toLowerCase()) { + if (h.toLowerCase() == RequestHeader.Digest.toLowerCase()) { s"$h: $digest" } else { s"$h: ${getHeaderValue(h, requestHeaders)}" @@ -143,7 +171,7 @@ object BerlinGroupSigning extends MdcLoggable { requestHeaders.find(_.name.toLowerCase() == name.toLowerCase()).map(_.values.mkString) .getOrElse(SecureRandomUtil.csprng.nextLong().toString) } - private def getCertificateFromTppSignatureCertificate(requestHeaders: List[HTTPParam]): X509Certificate = { + def getCertificateFromTppSignatureCertificate(requestHeaders: List[HTTPParam]): X509Certificate = { val certificate = getHeaderValue(RequestHeader.`TPP-Signature-Certificate`, requestHeaders) // Decode the Base64 string val decodedBytes = Base64.getDecoder.decode(certificate) @@ -200,7 +228,7 @@ object BerlinGroupSigning extends MdcLoggable { regex.findAllMatchIn(signatureHeader).map(m => m.group("key") -> m.group("value")).toMap } - private def getCurrentDate: String = { + def getCurrentDate: String = { val sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz") sdf.format(new Date()) } @@ -229,17 +257,13 @@ object BerlinGroupSigning extends MdcLoggable { val signingString = createSigningString(headers) // Load PEM files as strings - val privateKeyPath = "/path/to/private_key.pem" val certificatePath = "/path/to/certificate.pem" - - val privateKeyPem = getPrivateKeyPem(new String(Files.readAllBytes(Paths.get(privateKeyPath)))) - val certificateFullString = new String(Files.readAllBytes(Paths.get(certificatePath))) - val certificate: X509Certificate = parseCertificate(getCertificatePem(certificateFullString)) - val signature = signString(signingString, privateKeyPem) - println(s"1) Digest: SHA-256=$digest") + val signature = signString(signingString, P12StoreUtil.privateKeyToPEM(privateKey)) + + println(s"1) Digest: $digest") println(s"2) ${RequestHeader.`X-Request-ID`}: $xRequestId") println(s"3) ${RequestHeader.Date}: $dateHeader") println(s"4) ${RequestHeader.`TPP-Redirect-URL`}: $redirectUri") @@ -249,7 +273,9 @@ object BerlinGroupSigning extends MdcLoggable { // Convert public certificate to Base64 for Signature-Certificate header val certificateBase64 = Base64.getEncoder.encodeToString(certificateFullString.getBytes(StandardCharsets.UTF_8)) - println(s"6) TPP-Signature-Certificate: $certificateBase64") + println(s"6.1) TPP-Signature-Certificate: $certificateBase64") + val certificate2Base64 = Base64.getEncoder.encodeToString(P12StoreUtil.certificateToPEM(certificate).getBytes(StandardCharsets.UTF_8)) + println(s"6.2) TPP-Signature-Certificate 2: ${certificate2Base64}") val isVerified = verifySignature(signingString, signature, certificate.getPublicKey) println(s"Signature Verification: $isVerified") From 36e52e3edd189836369f65fd4c1531cd0da54d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 7 Apr 2025 10:50:00 +0200 Subject: [PATCH 4/7] feature/Check TPP via regulated entities --- .../main/scala/code/api/util/APIUtil.scala | 47 +++++++------------ .../code/api/util/BerlinGroupSigning.scala | 15 ++++-- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 987492cdb..28081e3f9 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -76,7 +76,6 @@ import com.openbankproject.commons.model._ import com.openbankproject.commons.model.enums.StrongCustomerAuthentication.SCA import com.openbankproject.commons.model.enums.{ContentParam, PemCertificateRole, StrongCustomerAuthentication} import com.openbankproject.commons.util.Functions.Implicits._ -import com.openbankproject.commons.util.Functions.Memo import com.openbankproject.commons.util._ import dispatch.url import javassist.expr.{ExprEditor, MethodCall} @@ -107,8 +106,8 @@ import java.util.regex.Pattern import java.util.{Calendar, Date, UUID} import scala.collection.JavaConverters._ import scala.collection.immutable.{List, Nil} +import scala.collection.mutable import scala.collection.mutable.{ArrayBuffer, ListBuffer} -import scala.collection.{immutable, mutable} import scala.concurrent.Future import scala.io.BufferedSource import scala.util.control.Breaks.{break, breakable} @@ -3931,18 +3930,22 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val requestHeaders = cc.map(_.requestHeaders).getOrElse(Nil) val consumerName = cc.flatMap(_.consumer.map(_.name.get)).getOrElse("") val certificate = getCertificateFromTppSignatureCertificate(requestHeaders) - val tpp = BerlinGroupSigning.checkTpp(consumerName, certificate) - if (tpp.nonEmpty) { - val hasRole = tpp.exists(_.services.contains(serviceProvider)) - if (hasRole) { - Full(true) + val tpp = BerlinGroupSigning.checkTpp(consumerName, certificate, cc) + for { + tpp <- BerlinGroupSigning.checkTpp(consumerName, certificate, cc) + } yield { + if (tpp.nonEmpty) { + val hasRole = tpp.exists(_.services.contains(serviceProvider)) + if (hasRole) { + Full(true) + } else { + Failure(X509ActionIsNotAllowed) + } } else { - Failure(X509ActionIsNotAllowed) + Failure("No valid Tpp") } - } else { - Failure("No valid Tpp") } - case value if value.toUpperCase == "CERTIFICATE" => + case value if value.toUpperCase == "CERTIFICATE" => Future { `getPSD2-CERT`(cc.map(_.requestHeaders).getOrElse(Nil)) match { case Some(pem) => logger.debug("PSD2-CERT pem: " + pem) @@ -3962,15 +3965,16 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } case None => Failure(X509CannotGetCertificate) } + } case _ => - Full(true) + Future(Full(true)) } result } def passesPsd2ServiceProvider(cc: Option[CallContext], serviceProvider: String): OBPReturnType[Box[Boolean]] = { val result = passesPsd2ServiceProviderCommon(cc, serviceProvider) - Future(result) map { + result map { x => (fullBoxOrException(x ~> APIFailureNewStyle(X509GeneralError, 401, cc.map(_.toLight))), cc) } } @@ -3988,23 +3992,6 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } - def passesPsd2ServiceProviderOldStyle(cc: Option[CallContext], serviceProvider: String): Box[Boolean] = { - passesPsd2ServiceProviderCommon(cc, serviceProvider) ?~! X509GeneralError - } - def passesPsd2AispOldStyle(cc: Option[CallContext]): Box[Boolean] = { - passesPsd2ServiceProviderOldStyle(cc, PemCertificateRole.PSP_AI.toString()) - } - def passesPsd2PispOldStyle(cc: Option[CallContext]): Box[Boolean] = { - passesPsd2ServiceProviderOldStyle(cc, PemCertificateRole.PSP_PI.toString()) - } - def passesPsd2IcspOldStyle(cc: Option[CallContext]): Box[Boolean] = { - passesPsd2ServiceProviderOldStyle(cc, PemCertificateRole.PSP_IC.toString()) - } - def passesPsd2AsspOldStyle(cc: Option[CallContext]): Box[Boolean] = { - passesPsd2ServiceProviderOldStyle(cc, PemCertificateRole.PSP_AS.toString()) - } - - def getMaskedPrimaryAccountNumber(accountNumber: String): String = { val (first, second) = accountNumber.splitAt(accountNumber.size/2) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index 0c3db2242..9a63e0aa6 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -1,8 +1,9 @@ package code.api.util import code.api.RequestHeader -import code.regulatedentities.MappedRegulatedEntityProvider +import code.api.util.newstyle.RegulatedEntityNewStyle.getRegulatedEntitiesNewStyle import code.util.Helper.MdcLoggable +import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.{RegulatedEntityTrait, User} import net.liftweb.common.{Box, Failure, Full} import net.liftweb.http.provider.HTTPParam @@ -14,6 +15,7 @@ import java.security.cert.{CertificateFactory, X509Certificate} import java.security.spec.PKCS8EncodedKeySpec import java.text.SimpleDateFormat import java.util.{Base64, Date, UUID} +import scala.concurrent.Future import scala.util.matching.Regex object BerlinGroupSigning extends MdcLoggable { @@ -97,7 +99,7 @@ object BerlinGroupSigning extends MdcLoggable { certificate } - def checkTpp(consumerName: String, certificate: X509Certificate): List[RegulatedEntityTrait] = { + def checkTpp(consumerName: String, certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = { // Define a regular expression to extract the value of CN, allowing for optional spaces around '=' val cnPattern: Regex = """CN\s*=\s*([^,]+)""".r @@ -108,9 +110,12 @@ object BerlinGroupSigning extends MdcLoggable { } val issuerCommonName = extractedCN // Certificate.caCert val serialNumber = certificate.getSerialNumber.toString - val regulatedEntities: List[RegulatedEntityTrait] = - MappedRegulatedEntityProvider.getRegulatedEntities() - .filter(_.entityName == consumerName) + val regulatedEntities: Future[List[RegulatedEntityTrait]] = + for { + (entities, callContext) <- getRegulatedEntitiesNewStyle(callContext) + } yield { + entities.filter(i => i.entityName == consumerName) + } regulatedEntities } From bc203c184ae030ba45e0b0575b572b9e991ebc42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 7 Apr 2025 19:42:53 +0200 Subject: [PATCH 5/7] feature/Check TPP via regulated entities 2 --- .../main/scala/code/api/util/APIUtil.scala | 3 +- .../code/api/util/BerlinGroupSigning.scala | 31 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 28081e3f9..21977be8c 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -3930,9 +3930,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val requestHeaders = cc.map(_.requestHeaders).getOrElse(Nil) val consumerName = cc.flatMap(_.consumer.map(_.name.get)).getOrElse("") val certificate = getCertificateFromTppSignatureCertificate(requestHeaders) - val tpp = BerlinGroupSigning.checkTpp(consumerName, certificate, cc) for { - tpp <- BerlinGroupSigning.checkTpp(consumerName, certificate, cc) + tpp <- BerlinGroupSigning.checkTppByConsumerName(consumerName, certificate, cc) } yield { if (tpp.nonEmpty) { val hasRole = tpp.exists(_.services.contains(serviceProvider)) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index 9a63e0aa6..5e23b6636 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -2,11 +2,14 @@ package code.api.util import code.api.RequestHeader import code.api.util.newstyle.RegulatedEntityNewStyle.getRegulatedEntitiesNewStyle +import code.consumer.Consumers +import code.model.Consumer import code.util.Helper.MdcLoggable import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.{RegulatedEntityTrait, User} import net.liftweb.common.{Box, Failure, Full} import net.liftweb.http.provider.HTTPParam +import net.liftweb.util.Helpers import java.nio.charset.StandardCharsets import java.nio.file.{Files, Paths} @@ -31,6 +34,9 @@ object BerlinGroupSigning extends MdcLoggable { alias = alias // Replace with the key alias ) + // Define a regular expression to extract the value of CN, allowing for optional spaces around '=' + val cnPattern: Regex = """CN\s*=\s*([^,]+)""".r + // Step 1: Calculate Digest (SHA-256 Hash of the Body) def generateDigest(body: String): String = { val sha256Digest = MessageDigest.getInstance("SHA-256") @@ -99,10 +105,7 @@ object BerlinGroupSigning extends MdcLoggable { certificate } - def checkTpp(consumerName: String, certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = { - // Define a regular expression to extract the value of CN, allowing for optional spaces around '=' - val cnPattern: Regex = """CN\s*=\s*([^,]+)""".r - + def getTppByCertificate(certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = { // Use the regular expression to find the value of CN val extractedCN = cnPattern.findFirstMatchIn(certificate.getIssuerDN.getName) match { case Some(m) => m.group(1) // Extract the value of CN @@ -110,11 +113,27 @@ object BerlinGroupSigning extends MdcLoggable { } val issuerCommonName = extractedCN // Certificate.caCert val serialNumber = certificate.getSerialNumber.toString + val regulatedEntities: Future[List[RegulatedEntityTrait]] = for { + (entities, _) <- getRegulatedEntitiesNewStyle(callContext) + } yield { + entities.filter { entity => + val hasSerialNumber = entity.attributes.exists(_.exists(a => + a.name == "CERTIFICATE_SERIAL_NUMBER" && a.value == serialNumber + )) + val hasCaName = entity.attributes.exists(_.exists(a => + a.name == "CERTIFICATE_CA_NAME" && a.value == issuerCommonName + )) + hasSerialNumber && hasCaName + } + } + regulatedEntities + } + def checkTppByConsumerName(consumerName: String, certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = { val regulatedEntities: Future[List[RegulatedEntityTrait]] = for { - (entities, callContext) <- getRegulatedEntitiesNewStyle(callContext) + entities <- getTppByCertificate(certificate, callContext) // Find TPP via certificate } yield { - entities.filter(i => i.entityName == consumerName) + entities.filter(i => i.entityName == consumerName) // Match the name of TPP and Consumer name } regulatedEntities } From 71c4c98aacaddb3eab02fb955fd97fb64a785f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 8 Apr 2025 14:24:49 +0200 Subject: [PATCH 6/7] feature/Create consumer on the fly based on TPP-Signature-Certificate --- .../main/scala/code/api/util/APIUtil.scala | 8 +- .../code/api/util/BerlinGroupCheck.scala | 23 ++++-- .../code/api/util/BerlinGroupSigning.scala | 80 ++++++++++++++++--- .../code/consumer/ConsumerProvider.scala | 21 +++-- obp-api/src/main/scala/code/model/OAuth.scala | 7 +- 5 files changed, 112 insertions(+), 27 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 21977be8c..6cdbe0334 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -3238,7 +3238,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ * @param cc The call context of an request * @return Failure in case we exceeded rate limit */ - def anonymousAccess(cc: CallContext): Future[(Box[User], Option[CallContext])] = { + def anonymousAccess(cc: CallContext): OBPReturnType[Box[User]] = { getUserAndSessionContextFuture(cc) map { result => val url = result._2.map(_.url).getOrElse("None") val verb = result._2.map(_.verb).getOrElse("None") @@ -3246,7 +3246,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val reqHeaders = result._2.map(_.requestHeaders).getOrElse(Nil) // Verify signed request JwsUtil.verifySignedRequest(body, verb, url, reqHeaders, result) - } map { result => + } flatMap { result => val url = result._2.map(_.url).getOrElse("None") val verb = result._2.map(_.verb).getOrElse("None") val body = result._2.flatMap(_.httpBody) @@ -3302,7 +3302,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val reqHeaders = result._2.map(_.requestHeaders).getOrElse(Nil) // Verify signed request if need be JwsUtil.verifySignedRequest(body, verb, url, reqHeaders, result) - } map { result => + } flatMap { result => val url = result._2.map(_.url).getOrElse("None") val verb = result._2.map(_.verb).getOrElse("None") val body = result._2.flatMap(_.httpBody) @@ -3931,7 +3931,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val consumerName = cc.flatMap(_.consumer.map(_.name.get)).getOrElse("") val certificate = getCertificateFromTppSignatureCertificate(requestHeaders) for { - tpp <- BerlinGroupSigning.checkTppByConsumerName(consumerName, certificate, cc) + tpp <- BerlinGroupSigning.getTppByCertificate(certificate, cc) } yield { if (tpp.nonEmpty) { val hasRole = tpp.exists(_.services.contains(serviceProvider)) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala index d6d0ca462..61544ffcd 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala @@ -1,13 +1,17 @@ package code.api.util import code.api.APIFailureNewStyle -import code.api.util.APIUtil.fullBoxOrException +import code.api.util.APIUtil.{OBPReturnType, fullBoxOrException} +import code.util.Helper.MdcLoggable import com.openbankproject.commons.model.User import com.openbankproject.commons.util.ApiVersion import net.liftweb.common.{Box, Empty} import net.liftweb.http.provider.HTTPParam -object BerlinGroupCheck { +import scala.concurrent.Future +import com.openbankproject.commons.ExecutionContext.Implicits.global + +object BerlinGroupCheck extends MdcLoggable { private val defaultMandatoryHeaders = "Content-Type,Date,Digest,PSU-Device-ID,PSU-Device-Name,PSU-IP-Address,Signature,TPP-Signature-Certificate,X-Request-ID" @@ -35,17 +39,24 @@ object BerlinGroupCheck { } } - def validate(body: Box[String], verb: String, url: String, reqHeaders: List[HTTPParam], forwardResult: (Box[User], Option[CallContext])): (Box[User], Option[CallContext]) = { + def validate(body: Box[String], verb: String, url: String, reqHeaders: List[HTTPParam], forwardResult: (Box[User], Option[CallContext])): OBPReturnType[Box[User]] = { if(url.contains(ApiVersion.berlinGroupV13.urlPrefix)) { validateHeaders(verb, url, reqHeaders, forwardResult) match { case (user, _) if user.isDefined || user == Empty => // All good. Chain another check // Verify signed request (Berlin Group) - BerlinGroupSigning.verifySignedRequest(body, verb, url, reqHeaders, forwardResult) + BerlinGroupSigning.verifySignedRequest(body, verb, url, reqHeaders, forwardResult) match { + case (user, cc) if (user.isDefined || user == Empty) && cc.exists(_.consumer.isEmpty) => // There is no Consumer in the database + // Create Consumer on the fly on a first usage of RequestHeader.`TPP-Signature-Certificate` + logger.info(s"Start BerlinGroupSigning.getOrCreateConsumer") + BerlinGroupSigning.getOrCreateConsumer(reqHeaders, forwardResult) + case forwardError => // Forward error case + Future(forwardError) + } case forwardError => // Forward error case - forwardError + Future(forwardError) } } else { - forwardResult + Future(forwardResult) } } diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index 5e23b6636..5e9b3ac42 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -1,6 +1,7 @@ package code.api.util import code.api.RequestHeader +import code.api.util.APIUtil.OBPReturnType import code.api.util.newstyle.RegulatedEntityNewStyle.getRegulatedEntitiesNewStyle import code.consumer.Consumers import code.model.Consumer @@ -36,6 +37,10 @@ object BerlinGroupSigning extends MdcLoggable { // Define a regular expression to extract the value of CN, allowing for optional spaces around '=' val cnPattern: Regex = """CN\s*=\s*([^,]+)""".r + // Define a regular expression to extract the value of EMAILADDRESS, allowing for optional spaces around '=' + val emailPattern: Regex = """EMAILADDRESS\s*=\s*([^,]+)""".r + // Define a regular expression to extract the value of Organization, allowing for optional spaces around '=' + val organisationlPattern: Regex = """O\s*=\s*([^,]+)""".r // Step 1: Calculate Digest (SHA-256 Hash of the Body) def generateDigest(body: String): String = { @@ -116,6 +121,7 @@ object BerlinGroupSigning extends MdcLoggable { val regulatedEntities: Future[List[RegulatedEntityTrait]] = for { (entities, _) <- getRegulatedEntitiesNewStyle(callContext) } yield { + logger.debug("Regulated Entities: " + entities) entities.filter { entity => val hasSerialNumber = entity.attributes.exists(_.exists(a => a.name == "CERTIFICATE_SERIAL_NUMBER" && a.value == serialNumber @@ -128,15 +134,6 @@ object BerlinGroupSigning extends MdcLoggable { } regulatedEntities } - def checkTppByConsumerName(consumerName: String, certificate: X509Certificate, callContext: Option[CallContext]): Future[List[RegulatedEntityTrait]] = { - val regulatedEntities: Future[List[RegulatedEntityTrait]] = - for { - entities <- getTppByCertificate(certificate, callContext) // Find TPP via certificate - } yield { - entities.filter(i => i.entityName == consumerName) // Match the name of TPP and Consumer name - } - regulatedEntities - } /** @@ -257,6 +254,71 @@ object BerlinGroupSigning extends MdcLoggable { sdf.format(new Date()) } + def getOrCreateConsumer(requestHeaders: List[HTTPParam], forwardResult: (Box[User], Option[CallContext])): OBPReturnType[Box[User]] = { + val certificate = getCertificateFromTppSignatureCertificate(requestHeaders) + // Use the regular expression to find the value of EMAILADDRESS + val extractedEmail = emailPattern.findFirstMatchIn(certificate.getSubjectDN.getName) match { + case Some(m) => Some(m.group(1)) // Extract the value of EMAILADDRESS + case None => None + } + // Use the regular expression to find the value of Organisation + val extractOrganisation = organisationlPattern.findFirstMatchIn(certificate.getSubjectDN.getName) match { + case Some(m) => Some(m.group(1)) // Extract the value of Organisation + case None => None + } + + for { + entities <- getTppByCertificate(certificate, forwardResult._2) // Find TPP via certificate + } yield { + // Certificate can be changed but this value is permanent per Regulated entity + val idno = entities.map(_.entityCode).headOption.getOrElse("") + + val entityName = entities.map(_.entityName).headOption + + // Get or create consumer by the unique key (azp, iss) + val consumer: Box[Consumer] = Consumers.consumers.vend.getOrCreateConsumer( + consumerId = None, + key = Some(Helpers.randomString(40).toLowerCase), + secret = Some(Helpers.randomString(40).toLowerCase), + aud = None, + azp = Some(idno), // The pair (azp, iss) is a unique key in case of Client of an Identity Provider + iss = Some(RequestHeader.`TPP-Signature-Certificate`), + sub = None, + Some(true), + name = entityName, + appType = None, + description = Some(s"Certificate serial number:${certificate.getSerialNumber}"), + developerEmail = extractedEmail, + redirectURL = None, + createdByUserId = None, + certificate = None + ) + + // Set or update certificate + consumer match { + case Full(consumer) => + val certificateFromHeader = getHeaderValue(RequestHeader.`TPP-Signature-Certificate`, requestHeaders) + Consumers.consumers.vend.updateConsumer( + id = consumer.id.get, + name = entityName, + certificate = Some(certificateFromHeader) + ) match { + case Full(consumer) => + // Update call context with a created consumer + (forwardResult._1, forwardResult._2.map(_.copy(consumer = Full(consumer)))) + case error => + logger.debug(error) + (Failure(s"${ErrorMessages.CreateConsumerError} Regulated entity: $idno"), forwardResult._2) + } + case error => + logger.debug(error) + (Failure(s"${ErrorMessages.CreateConsumerError} Regulated entity: $idno"), forwardResult._2) + } + } + + + } + // Example Usage def main(args: Array[String]): Unit = { // Digest for request diff --git a/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala b/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala index 0e0fac5cb..ac927d726 100644 --- a/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala +++ b/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala @@ -43,12 +43,18 @@ trait ConsumersProvider { logoURL: Option[String] ): Box[Consumer] def deleteConsumer(consumer: Consumer): Boolean - def updateConsumer(id: Long, key: Option[String], secret: Option[String], isActive: Option[Boolean], name: Option[String], - appType: Option[AppType], description: Option[String], developerEmail: Option[String], - redirectURL: Option[String], - createdByUserId: Option[String], - LogoURL: Option[String], - certificate: Option[String], + def updateConsumer(id: Long, + key: Option[String] = None, + secret: Option[String] = None, + isActive: Option[Boolean] = None, + name: Option[String] = None, + appType: Option[AppType] = None, + description: Option[String] = None, + developerEmail: Option[String] = None, + redirectURL: Option[String] = None, + createdByUserId: Option[String] = None, + LogoURL: Option[String] = None, + certificate: Option[String] = None, ): Box[Consumer] def updateConsumerCallLimits(id: Long, perSecond: Option[String], perMinute: Option[String], perHour: Option[String], perDay: Option[String], perWeek: Option[String], perMonth: Option[String]): Future[Box[Consumer]] def getOrCreateConsumer(consumerId: Option[String], @@ -64,7 +70,8 @@ trait ConsumersProvider { description: Option[String], developerEmail: Option[String], redirectURL: Option[String], - createdByUserId: Option[String]): Box[Consumer] + createdByUserId: Option[String], + certificate: Option[String] = None): Box[Consumer] def populateMissingUUIDs(): Boolean } \ No newline at end of file diff --git a/obp-api/src/main/scala/code/model/OAuth.scala b/obp-api/src/main/scala/code/model/OAuth.scala index e694468db..c8016ecf8 100644 --- a/obp-api/src/main/scala/code/model/OAuth.scala +++ b/obp-api/src/main/scala/code/model/OAuth.scala @@ -388,7 +388,8 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { description: Option[String], developerEmail: Option[String], redirectURL: Option[String], - createdByUserId: Option[String]): Box[Consumer] = { + createdByUserId: Option[String], + certificate: Option[String]): Box[Consumer] = { val consumer: Box[Consumer] = // 1st try to find via UUID issued by OBP-API back end @@ -468,6 +469,10 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { case Some(v) => c.createdByUserId(v) case None => } + certificate match { + case Some(v) => c.clientCertificate(v) + case None => + } consumerId match { case Some(v) => c.consumerId(v) case None => From 2c4c25c5c09e0d08905b82cca4c0689fd9c6ea06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 9 Apr 2025 08:56:14 +0200 Subject: [PATCH 7/7] test/Create consumer on the fly based on TPP-Signature-Certificate - fix tests --- .../code/api/util/BerlinGroupSigning.scala | 115 +++++++++--------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index 5e9b3ac42..6bd338f86 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -255,68 +255,71 @@ object BerlinGroupSigning extends MdcLoggable { } def getOrCreateConsumer(requestHeaders: List[HTTPParam], forwardResult: (Box[User], Option[CallContext])): OBPReturnType[Box[User]] = { - val certificate = getCertificateFromTppSignatureCertificate(requestHeaders) - // Use the regular expression to find the value of EMAILADDRESS - val extractedEmail = emailPattern.findFirstMatchIn(certificate.getSubjectDN.getName) match { - case Some(m) => Some(m.group(1)) // Extract the value of EMAILADDRESS - case None => None - } - // Use the regular expression to find the value of Organisation - val extractOrganisation = organisationlPattern.findFirstMatchIn(certificate.getSubjectDN.getName) match { - case Some(m) => Some(m.group(1)) // Extract the value of Organisation - case None => None - } + val tppSignatureCert: String = APIUtil.getRequestHeader(RequestHeader.`TPP-Signature-Certificate`, requestHeaders) + if (tppSignatureCert.isEmpty) { + Future(forwardResult) + } else { // Dynamic consumer creation/update works in case that RequestHeader.`TPP-Signature-Certificate is present in the current call + val certificate = getCertificateFromTppSignatureCertificate(requestHeaders) + // Use the regular expression to find the value of EMAILADDRESS + val extractedEmail = emailPattern.findFirstMatchIn(certificate.getSubjectDN.getName) match { + case Some(m) => Some(m.group(1)) // Extract the value of EMAILADDRESS + case None => None + } + // Use the regular expression to find the value of Organisation + val extractOrganisation = organisationlPattern.findFirstMatchIn(certificate.getSubjectDN.getName) match { + case Some(m) => Some(m.group(1)) // Extract the value of Organisation + case None => None + } - for { - entities <- getTppByCertificate(certificate, forwardResult._2) // Find TPP via certificate - } yield { - // Certificate can be changed but this value is permanent per Regulated entity - val idno = entities.map(_.entityCode).headOption.getOrElse("") + for { + entities <- getTppByCertificate(certificate, forwardResult._2) // Find TPP via certificate + } yield { + // Certificate can be changed but this value is permanent per Regulated entity + val idno = entities.map(_.entityCode).headOption.getOrElse("") - val entityName = entities.map(_.entityName).headOption + val entityName = entities.map(_.entityName).headOption - // Get or create consumer by the unique key (azp, iss) - val consumer: Box[Consumer] = Consumers.consumers.vend.getOrCreateConsumer( - consumerId = None, - key = Some(Helpers.randomString(40).toLowerCase), - secret = Some(Helpers.randomString(40).toLowerCase), - aud = None, - azp = Some(idno), // The pair (azp, iss) is a unique key in case of Client of an Identity Provider - iss = Some(RequestHeader.`TPP-Signature-Certificate`), - sub = None, - Some(true), - name = entityName, - appType = None, - description = Some(s"Certificate serial number:${certificate.getSerialNumber}"), - developerEmail = extractedEmail, - redirectURL = None, - createdByUserId = None, - certificate = None - ) + // Get or create consumer by the unique key (azp, iss) + val consumer: Box[Consumer] = Consumers.consumers.vend.getOrCreateConsumer( + consumerId = None, + key = Some(Helpers.randomString(40).toLowerCase), + secret = Some(Helpers.randomString(40).toLowerCase), + aud = None, + azp = Some(idno), // The pair (azp, iss) is a unique key in case of Client of an Identity Provider + iss = Some(RequestHeader.`TPP-Signature-Certificate`), + sub = None, + Some(true), + name = entityName, + appType = None, + description = Some(s"Certificate serial number:${certificate.getSerialNumber}"), + developerEmail = extractedEmail, + redirectURL = None, + createdByUserId = None, + certificate = None + ) - // Set or update certificate - consumer match { - case Full(consumer) => - val certificateFromHeader = getHeaderValue(RequestHeader.`TPP-Signature-Certificate`, requestHeaders) - Consumers.consumers.vend.updateConsumer( - id = consumer.id.get, - name = entityName, - certificate = Some(certificateFromHeader) - ) match { - case Full(consumer) => - // Update call context with a created consumer - (forwardResult._1, forwardResult._2.map(_.copy(consumer = Full(consumer)))) - case error => - logger.debug(error) - (Failure(s"${ErrorMessages.CreateConsumerError} Regulated entity: $idno"), forwardResult._2) - } - case error => - logger.debug(error) - (Failure(s"${ErrorMessages.CreateConsumerError} Regulated entity: $idno"), forwardResult._2) + // Set or update certificate + consumer match { + case Full(consumer) => + val certificateFromHeader = getHeaderValue(RequestHeader.`TPP-Signature-Certificate`, requestHeaders) + Consumers.consumers.vend.updateConsumer( + id = consumer.id.get, + name = entityName, + certificate = Some(certificateFromHeader) + ) match { + case Full(consumer) => + // Update call context with a created consumer + (forwardResult._1, forwardResult._2.map(_.copy(consumer = Full(consumer)))) + case error => + logger.debug(error) + (Failure(s"${ErrorMessages.CreateConsumerError} Regulated entity: $idno"), forwardResult._2) + } + case error => + logger.debug(error) + (Failure(s"${ErrorMessages.CreateConsumerError} Regulated entity: $idno"), forwardResult._2) + } } } - - } // Example Usage