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..6cdbe0334 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 @@ -75,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} @@ -106,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} @@ -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) @@ -3925,9 +3925,26 @@ 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) + for { + tpp <- BerlinGroupSigning.getTppByCertificate(certificate, cc) + } yield { + 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" => Future { `getPSD2-CERT`(cc.map(_.requestHeaders).getOrElse(Nil)) match { case Some(pem) => logger.debug("PSD2-CERT pem: " + pem) @@ -3947,14 +3964,17 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } case None => Failure(X509CannotGetCertificate) } + } + case _ => + Future(Full(true)) } result } 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) + result map { + x => (fullBoxOrException(x ~> APIFailureNewStyle(X509GeneralError, 401, cc.map(_.toLight))), cc) } } def passesPsd2Aisp(cc: Option[CallContext]): OBPReturnType[Box[Boolean]] = { @@ -3971,23 +3991,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/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/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 05fc9b9d9..6bd338f86 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -1,32 +1,53 @@ 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 import code.util.Helper.MdcLoggable -import com.openbankproject.commons.model.User +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} 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.concurrent.Future 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 + ) + + // 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 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) @@ -43,7 +64,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)) @@ -63,42 +84,55 @@ 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 + } + + 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 + case None => "CN not found" + } + val issuerCommonName = extractedCN // Certificate.caCert + val serialNumber = certificate.getSerialNumber.toString + 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 + )) + val hasCaName = entity.attributes.exists(_.exists(a => + a.name == "CERTIFICATE_CA_NAME" && a.value == issuerCommonName + )) + hasSerialNumber && hasCaName + } + } + regulatedEntities } @@ -123,25 +157,24 @@ 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) 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()) { - s"$h: SHA-256=$digest" + if (h.toLowerCase() == RequestHeader.Digest.toLowerCase()) { + 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 +192,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 = { + 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 +223,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 +249,126 @@ object BerlinGroupSigning extends MdcLoggable { regex.findAllMatchIn(signatureHeader).map(m => m.group("key") -> m.group("value")).toMap } + def getCurrentDate: String = { + val sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz") + sdf.format(new Date()) + } + + def getOrCreateConsumer(requestHeaders: List[HTTPParam], forwardResult: (Box[User], Option[CallContext])): OBPReturnType[Box[User]] = { + 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("") + + 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 = { - 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 certificatePath = "/path/to/certificate.pem" + val certificateFullString = new String(Files.readAllBytes(Paths.get(certificatePath))) - val privateKeyPem = new String(Files.readAllBytes(Paths.get(privateKeyPath))) - val certificatePem = new String(Files.readAllBytes(Paths.get(certificatePath))) - val signature = signString(signingString, privateKeyPem) - val certificate = loadCertificate(certificatePem) + val signature = signString(signingString, P12StoreUtil.privateKeyToPEM(privateKey)) - println(s"1) Digest: SHA-256=$digest") + println(s"1) Digest: $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.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") - 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/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}") + } + + +} 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) + } } /** 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 =>