Merge pull request #2524 from constantine2nd/develop

Berlin Group
This commit is contained in:
Simon Redfern 2025-04-09 11:44:07 +02:00 committed by GitHub
commit ea83a663c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 380 additions and 136 deletions

View File

@ -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 -------------------------------

View File

@ -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

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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"

View File

@ -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")
}

View File

@ -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.")

View 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}")
}
}

View File

@ -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)
}
}
/**

View File

@ -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
}

View File

@ -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 =>