mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 14:46:49 +00:00
commit
ea83a663c1
@ -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 -------------------------------
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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.")
|
||||
|
||||
83
obp-api/src/main/scala/code/api/util/P12StoreUtil.scala
Normal file
83
obp-api/src/main/scala/code/api/util/P12StoreUtil.scala
Normal file
@ -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}")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
|
||||
}
|
||||
@ -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 =>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user