diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml
index 94525fc6d..3398184a2 100644
--- a/.github/workflows/build_pull_request.yml
+++ b/.github/workflows/build_pull_request.yml
@@ -27,9 +27,9 @@ jobs:
--health-timeout 5s
--health-retries 5
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up JDK 11
- uses: actions/setup-java@v2
+ uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'adopt'
diff --git a/.github/workflows/run_trivy.yml b/.github/workflows/run_trivy.yml
index 548cd92ad..4636bd311 100644
--- a/.github/workflows/run_trivy.yml
+++ b/.github/workflows/run_trivy.yml
@@ -19,7 +19,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- id: trivy-db
name: Check trivy db sha
env:
@@ -31,7 +31,7 @@ jobs:
sha=$(gh api -H "${headers}" "${endpoint}" | jq --raw-output "${jqFilter}")
echo "Trivy DB sha256:${sha}"
echo "::set-output name=sha::${sha}"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: .trivy
key: ${{ runner.os }}-trivy-db-${{ steps.trivy-db.outputs.sha }}
@@ -49,6 +49,6 @@ jobs:
- name: Fix .trivy permissions
run: sudo chown -R $(stat . -c %u:%g) .trivy
- name: Upload Trivy scan results to GitHub Security tab
- uses: github/codeql-action/upload-sarif@v1
+ uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
\ No newline at end of file
diff --git a/obp-api/src/main/docs/glossary/TPP_Certificate_Verification.md b/obp-api/src/main/docs/glossary/TPP_Certificate_Verification.md
new file mode 100644
index 000000000..099e740fa
--- /dev/null
+++ b/obp-api/src/main/docs/glossary/TPP_Certificate_Verification.md
@@ -0,0 +1,87 @@
+### TPP Certificate Verification
+
+The signing and verification flow of certificates for TPP requests is a follows:
+
+#### Generation of the Request by TPP
+
+**Step 1: Calculating the Hash for the Digest Field**
+
+* TPP creates the body of the request (e.g., JSON containing transaction or consent data).
+* TPP applies the SHA-256 algorithm to the body of the request to generate a unique hash.
+* The resulting hash is encoded in Base64 and placed in the request header under the `Digest` field.
+
+**Step 2: Creating the Signing String**
+
+* TPP prepares a signing string based on specific fields from the request header.
+* The signing string is a concatenation of the values of the signed fields in a specific format.
+* The order of the fields is critical and follows the specifications.
+
+**Step 3: Signing the Signing String**
+
+* TPP uses its RSA private key associated with its certificate to generate a digital signature.
+* The signature is applied to the signing string using the RSA-SHA256 algorithm.
+* The resulting digital signature is Base64-encoded and placed in the `Signature` field.
+
+**Step 4: Adding the Certificate to the TPP-Signature-Certificate Field**
+
+* TPP includes its public certificate in the request header under the `TPP-Signature-Certificate` field.
+* The certificate is issued by a trusted certification authority (CA) and contains the TPP's public key.
+
+**Step 5: Sending the Request to OBP**
+
+* TPP sends the complete request, including the `Digest`, `Signature`, and `TPP-Signature-Certificate` headers, to the OBP endpoint.
+
+---
+
+#### Verification of the Request by OBP
+
+**Step 1: Validating the TPP Certificate**
+
+* OBP verifies the TPP certificate included in the `TPP-Signature-Certificate` field.\
+ Steps:
+
+1. Verify the certificate's trust chain up to a trusted certification authority.
+2. Ensure the certificate is valid (not expired or revoked).
+3. Confirm the certificate is issued for the appropriate usage (e.g., digital signatures for Open Banking), based on the information from the National Bank registry (e.g., certificate SN and CA found in the `Signature` field).
+
+* **Result**: If the certificate is valid, the TPP's identity is confirmed.
+
+**Step 2: Verifying the Signature in the `Signature` Field**
+
+* OBP extracts the fields included in the signing string based on the information in the `Signature` field.
+* OBP recreates the signing string in the same format used by the TPP.
+* OBP uses the public key from the certificate in the `TPP-Signature-Certificate` field to verify the signature in the `Signature` field.
+* **Result**: If the signature is valid, the authenticity of the request is confirmed.
+
+**Step 3: Verifying the Hash in the `Digest` Field**
+
+* OBP computes its own SHA-256 hash of the received request body.
+* The computed hash is compared with the value in the `Digest` header.
+* **Result**: If the two hashes match, the integrity of the request body is confirmed.
+
+**Step 4: Authorizing the Request**
+
+* If all verifications (integrity, signature, and certificate) pass, OBP processes the request.
+* Otherwise, the request is rejected with an error message (e.g., `401 Unauthorized` or `403 Forbidden`).
+
+---
+
+#### Additional Details
+
+**How does OBP verify the signature in the `Signature` field?**
+
+1. **Extracts the public key from the `TPP-Signature-Certificate`**:
+ * OBP retrieves the certificate (`TPP-Signature-Certificate`) from the request header.
+ * From this certificate, OBP extracts the TPP's public key.
+2. **Recreates the Signing String**:
+ * OBP analyzes the `Signature` header to identify the signed fields (e.g., `Digest`, `X-Request-ID`, `Date`, etc.).
+ * OBP recreates the signing string from the actual values of these fields in the specified order.
+3. **Decodes the Signature**:
+ * OBP decodes the value in the `Signature.signature` field (the digital signature generated by TPP).
+ * Decoding is done using the public key extracted from the `TPP-Signature-Certificate` and the algorithm specified in `Signature.algorithm` (e.g., RSA-SHA256).
+4. **Compares the Resulting Hash**:
+ * During decoding, OBP obtains a hash calculated by TPP at the time of signing.
+ * OBP compares this hash with the hash generated internally from the recreated signing string.
+5. **Possible Outcomes**:
+ * If the hashes match, the signature is valid.
+ * If the hashes do not match, the signature is invalid, and the request is rejected.
\ No newline at end of file
diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template
index e2a7ddba7..385f196ea 100644
--- a/obp-api/src/main/resources/props/sample.props.template
+++ b/obp-api/src/main/resources/props/sample.props.template
@@ -178,6 +178,12 @@ jwt.use.ssl=false
# truststore.password.redis = truststore-password
+## Trust stores is a list of trusted CA certificates
+## 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
+
+
## Enable writing API metrics (which APIs are called) to RDBMS
write_metrics=true
## Enable writing connector metrics (which methods are called)to RDBMS
diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
index fc2935f7d..ff0d92a9a 100644
--- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
+++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
@@ -519,7 +519,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
case Some(STATIC) => {
val cacheValueFromRedis = Caching.getStaticResourceDocCache(cacheKey)
- val dynamicDocs: Box[JValue] =
+ val staticDocs: Box[JValue] =
if (cacheValueFromRedis.isDefined) {
Full(json.parse(cacheValueFromRedis.get))
} else {
@@ -530,12 +530,12 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
Full(resourceDocJsonJValue)
}
- Future(dynamicDocs.map(successJsonResponse(_)))
+ Future(staticDocs.map(successJsonResponse(_)))
}
case _ => {
val cacheValueFromRedis = Caching.getAllResourceDocCache(cacheKey)
- val dynamicDocs: Box[JValue] =
+ val bothStaticAndDyamicDocs: Box[JValue] =
if (cacheValueFromRedis.isDefined) {
Full(json.parse(cacheValueFromRedis.get))
} else {
@@ -546,7 +546,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
Full(resourceDocJsonJValue)
}
- Future(dynamicDocs.map(successJsonResponse(_)))
+ Future(bothStaticAndDyamicDocs.map(successJsonResponse(_)))
}
}
}
@@ -715,13 +715,11 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
swaggerJValue <- NewStyle.function.tryons(s"$UnknownError Can not convert internal swagger file.", 400, cc.callContext) {
val cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey)
- val dynamicDocs: JValue =
- if (cacheValueFromRedis.isDefined) {
- json.parse(cacheValueFromRedis.get)
- } else {
- convertResourceDocsToSwaggerJvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered)
- }
- dynamicDocs
+ if (cacheValueFromRedis.isDefined) {
+ json.parse(cacheValueFromRedis.get)
+ } else {
+ convertResourceDocsToSwaggerJvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered)
+ }
}
} yield {
(swaggerJValue, HttpCode.`200`(cc.callContext))
diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala
index 4c5412125..ed5d6856c 100644
--- a/obp-api/src/main/scala/code/api/cache/Redis.scala
+++ b/obp-api/src/main/scala/code/api/cache/Redis.scala
@@ -61,7 +61,7 @@ object Redis extends MdcLoggable {
// Load the CA certificate
val trustStore = KeyStore.getInstance(KeyStore.getDefaultType)
- val trustStorePassword = APIUtil.getPropsValue("keystore.password.redis")
+ val trustStorePassword = APIUtil.getPropsValue("truststore.password.redis")
.getOrElse(APIUtil.initPasswd).toCharArray
val truststorePath = APIUtil.getPropsValue("truststore.path.redis").getOrElse("")
val trustStoreStream = new FileInputStream(truststorePath)
diff --git a/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json b/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json
index 640c91a4d..f6ee011d8 100644
--- a/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json
+++ b/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json
@@ -12,47 +12,48 @@
"logo":"https://static.openbankproject.com/images/sandbox/bank_y.png",
"website":"https://www.example.com"
}],
- "users":[{
- "email":"robert.xuk.x@example.com",
- "user_name": "robert.xuk.x@example.com",
- "password":"5232e7",
- "display_name":"Robert XUk X"
- },{
- "email":"susan.xuk.x@example.com",
- "user_name": "susan.xuk.x@example.com",
- "password":"43ca4d",
- "display_name":"Susan XUk X"
- },{
- "email":"anil.xuk.x@example.com",
- "user_name": "anil.xuk.x@example.com",
- "password":"d8c716",
- "display_name":"Anil XUk X"
- },{
- "email":"ellie.xuk.x@example.com",
- "user_name": "ellie.xuk.x@example.com",
- "password":"6187b9",
- "display_name":"Ellie XUk X"
- },{
- "email":"robert.yuk.y@example.com",
- "user_name": "robert.yuk.y@example.com",
- "password":"e5046a",
- "display_name":"Robert YUk Y"
- },{
- "email":"susan.yuk.y@example.com",
- "user_name": "susan.yuk.y@example.com",
- "password":"5b38a6",
- "display_name":"Susan YUk Y"
- },{
- "email":"anil.yuk.y@example.com",
- "user_name": "anil.yuk.y@example.com",
- "password":"dcf03d",
- "display_name":"Anil YUk Y"
- },{
- "email":"ellie.yuk.y@example.com",
- "user_name": "ellie.yuk.y@example.com",
- "password":"4f9eaa",
- "display_name":"Ellie YUk Y"
- }],
+ "users": [
+ {
+ "email": "robert.x.0.gh@example.com",
+ "password": "V8%Ktssl(L",
+ "user_name": "Robert.X.0.GH"
+ },
+ {
+ "email": "susan.x.0.gh@example.com",
+ "password": "naW9u3C%bh",
+ "user_name": "Susan.X.0.GH"
+ },
+ {
+ "email": "anil.x.0.gh@example.com",
+ "password": "9W0RIrX-6f",
+ "user_name": "Anil.X.0.GH"
+ },
+ {
+ "email": "ellie.x.0.gh@example.com",
+ "password": "rMf_OHM0dW",
+ "user_name": "Ellie.X.0.GH"
+ },
+ {
+ "email": "robert.y.9.gh@example.com",
+ "password": "%1Z43kzt2L",
+ "user_name": "Robert.Y.9.GH"
+ },
+ {
+ "email": "susan.y.9.gh@example.com",
+ "password": "oITehM!B2V",
+ "user_name": "Susan.Y.9.GH"
+ },
+ {
+ "email": "anil.y.9.gh@example.com",
+ "password": "TuKaNO8oI-",
+ "user_name": "Anil.Y.9.GH"
+ },
+ {
+ "email": "ellie.y.9.gh@example.com",
+ "password": "SkJDH+ds2_",
+ "user_name": "Ellie.Y.9.GH"
+ }
+ ],
"accounts":[{
"id":"05237266-b334-4704-a087-5b460a2ecf04",
"bank":"psd201-bank-x--uk",
diff --git a/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json b/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json
index 91418c4d6..0126ddd05 100644
--- a/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json
+++ b/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json
@@ -12,39 +12,48 @@
"logo":"https://static.openbankproject.com/images/sandbox/bank_y.png",
"website":"https://www.example.com"
}],
- "users":[{
- "email":"robert.x.0.gh@example.com",
- "password":"X!d1edcafd",
- "user_name":"Robert.X.0.GH"
- },{
- "email":"susan.x.0.gh@example.com",
- "password":"X!90e4e3e4",
- "user_name":"Susan.X.0.GH"
- },{
- "email":"anil.x.0.gh@example.com",
- "password":"X!eb06b005",
- "user_name":"Anil.X.0.GH"
- },{
- "email":"ellie.x.0.gh@example.com",
- "password":"X!5bc94405",
- "user_name":"Ellie.X.0.GH"
- },{
- "email":"robert.y.9.gh@example.com",
- "password":"X!039941de",
- "user_name":"Robert.Y.9.GH"
- },{
- "email":"susan.y.9.gh@example.com",
- "password":"X!bb4efa3d",
- "user_name":"Susan.Y.9.GH"
- },{
- "email":"anil.y.9.gh@example.com",
- "password":"X!098915cd",
- "user_name":"Anil.Y.9.GH"
- },{
- "email":"ellie.y.9.gh@example.com",
- "password":"X!6170b37b",
- "user_name":"Ellie.Y.9.GH"
- }],
+ "users": [
+ {
+ "email": "robert.x.0.gh@example.com",
+ "password": "V8%Ktssl(L",
+ "user_name": "Robert.X.0.GH"
+ },
+ {
+ "email": "susan.x.0.gh@example.com",
+ "password": "naW9u3C%bh",
+ "user_name": "Susan.X.0.GH"
+ },
+ {
+ "email": "anil.x.0.gh@example.com",
+ "password": "9W0RIrX-6f",
+ "user_name": "Anil.X.0.GH"
+ },
+ {
+ "email": "ellie.x.0.gh@example.com",
+ "password": "rMf_OHM0dW",
+ "user_name": "Ellie.X.0.GH"
+ },
+ {
+ "email": "robert.y.9.gh@example.com",
+ "password": "%1Z43kzt2L",
+ "user_name": "Robert.Y.9.GH"
+ },
+ {
+ "email": "susan.y.9.gh@example.com",
+ "password": "oITehM!B2V",
+ "user_name": "Susan.Y.9.GH"
+ },
+ {
+ "email": "anil.y.9.gh@example.com",
+ "password": "TuKaNO8oI-",
+ "user_name": "Anil.Y.9.GH"
+ },
+ {
+ "email": "ellie.y.9.gh@example.com",
+ "password": "SkJDH+ds2_",
+ "user_name": "Ellie.Y.9.GH"
+ }
+ ],
"accounts":[{
"id":"f65e28a5-9abe-428f-85bb-6c3c38122adb",
"bank":"obp-bank-x-gh",
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 121cc6bd7..745417dd9 100644
--- a/obp-api/src/main/scala/code/api/util/APIUtil.scala
+++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala
@@ -784,16 +784,24 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
* (?=.*\d) //should contain at least one digit
* (?=.*[a-z]) //should contain at least one lower case
* (?=.*[A-Z]) //should contain at least one upper case
- * (?=.*[!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~]) //should contain at least one special character
- * ([A-Za-z0-9!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~]{10,16}) //should contain 10 to 16 valid characters
+ * (?=.*[!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]]) //should contain at least one special character
+ * ([A-Za-z0-9!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]]{10,16}) //should contain 10 to 16 valid characters
**/
val regex =
- """^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~])([A-Za-z0-9!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~]{10,16})$""".r
- password match {
- case password if(password.length > 16 && password.length <= 512 && basicPasswordValidation(password) ==SILENCE_IS_GOLDEN) => true
- case regex(password) if(basicPasswordValidation(password) ==SILENCE_IS_GOLDEN) => true
- case _ => false
+ """^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]])([A-Za-z0-9!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]]{10,16})$""".r
+
+ // first check `basicPasswordValidation`
+ if (basicPasswordValidation(password) != SILENCE_IS_GOLDEN) {
+ return false
}
+
+ // 2nd: check the password length between 10 and 512
+ if (password.length > 16 && password.length <= 512) {
+ return true
+ }
+
+ // 3rd: check the regular expression
+ regex.pattern.matcher(password).matches()
}
/** only A-Z, a-z, 0-9,-,_,. =, & and max length <= 2048 */
@@ -847,12 +855,17 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
/** also support space now */
def basicPasswordValidation(value:String): String ={
val valueLength = value.length
- val regex = """^([A-Za-z0-9!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~ ]+)$""".r
- value match {
- case regex(e) if(valueLength <= 512) => SILENCE_IS_GOLDEN
- case regex(e) if(valueLength > 512) => ErrorMessages.InvalidValueLength
- case _ => ErrorMessages.InvalidValueCharacters
+ val regex = """^([A-Za-z0-9!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~ \[\]]+)$""".r
+
+ if (!regex.pattern.matcher(value).matches()) {
+ return ErrorMessages.InvalidValueCharacters
}
+
+ if (valueLength > 512) {
+ return ErrorMessages.InvalidValueLength
+ }
+
+ SILENCE_IS_GOLDEN
}
/** only A-Z, a-z, 0-9, -, _, ., @, and max length <= 512 */
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 3cb253db0..c63fc89cc 100644
--- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala
+++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala
@@ -130,7 +130,12 @@ object BerlinGroupSigning {
val signatureHeaderValue = getHeaderValue(RequestHeader.Signature, requestHeaders)
val signature = parseSignatureHeader(signatureHeaderValue).getOrElse("signature", "NONE")
val isVerified = verifySignature(signingString, signature, certificatePem)
- if (isVerified) forwardResult else (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2)
+ val isValidated = CertificateVerifier.validateCertificate(certificatePem)
+ (isVerified, isValidated) match {
+ case (true, true) => forwardResult
+ case (true, false) => (Failure(ErrorMessages.X509PublicKeyCannotBeValidated), forwardResult._2)
+ case (false, _) => (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2)
+ }
case Failure(msg, t, c) => (Failure(msg, t, c), forwardResult._2) // PEM certificate is not valid
case _ => (Failure(ErrorMessages.X509GeneralError), forwardResult._2) // PEM certificate cannot be validated
}
diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
new file mode 100644
index 000000000..cefb24abe
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
@@ -0,0 +1,153 @@
+package code.api.util
+
+import code.util.Helper.MdcLoggable
+
+import java.io.{ByteArrayInputStream, FileInputStream}
+import java.security.KeyStore
+import java.security.cert._
+import java.util.{Base64, Collections}
+import javax.net.ssl.TrustManagerFactory
+import scala.io.Source
+import scala.jdk.CollectionConverters._
+import scala.util.{Failure, Success, Try}
+
+object CertificateVerifier extends MdcLoggable {
+
+ /**
+ * Loads a trust store (`.p12` file) from a configured path.
+ *
+ * This function:
+ * - Reads the trust store password from the application properties (`truststore.path.tpp_signature`).
+ * - Uses Java's `KeyStore` class to load the certificates.
+ *
+ * @return An `Option[KeyStore]` containing the loaded trust store, or `None` if loading fails.
+ */
+ private def loadTrustStore(): Option[KeyStore] = {
+ val trustStorePath = APIUtil.getPropsValue("truststore.path.tpp_signature")
+ .or(APIUtil.getPropsValue("truststore.path")).getOrElse("")
+ val trustStorePassword = APIUtil.getPropsValue("truststore.password.tpp_signature", "").toCharArray
+
+ Try {
+ val trustStore = KeyStore.getInstance("PKCS12")
+ val trustStoreInputStream = new FileInputStream(trustStorePath)
+ try {
+ trustStore.load(trustStoreInputStream, trustStorePassword)
+ } finally {
+ trustStoreInputStream.close()
+ }
+ trustStore
+ } match {
+ case Success(store) =>
+ logger.info(s"Loaded trust store from: $trustStorePath")
+ Some(store)
+ case Failure(e) =>
+ logger.info(s"Failed to load trust store: ${e.getMessage}")
+ None
+ }
+ }
+
+ /**
+ * Verifies an X.509 certificate against the loaded trust store.
+ *
+ * This function:
+ * - Parses the PEM certificate into an `X509Certificate` using `parsePemToX509Certificate`.
+ * - Loads the trust store using `loadTrustStore()`.
+ * - Extracts trusted root CAs from the trust store.
+ * - Creates PKIX validation parameters and disables revocation checking.
+ * - Validates the certificate using Java's `CertPathValidator`.
+ *
+ * @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 = {
+ Try {
+ val certificate = parsePemToX509Certificate(pemCertificate)
+
+ // Load trust store
+ val trustStore = loadTrustStore()
+ .getOrElse(throw new Exception("Trust store could not be loaded."))
+
+ val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
+ trustManagerFactory.init(trustStore)
+
+ // Get trusted CAs from the trust store
+ val trustAnchors = trustStore.aliases().asScala
+ .filter(trustStore.isCertificateEntry)
+ .map(alias => trustStore.getCertificate(alias).asInstanceOf[X509Certificate])
+ .map(cert => new TrustAnchor(cert, null))
+ .toSet
+ .asJava // Convert Scala Set to Java Set
+
+ if (trustAnchors.isEmpty) throw new Exception("No trusted certificates found in trust store.")
+
+ // Set up PKIX parameters for validation
+ val pkixParams = new PKIXParameters(trustAnchors)
+ pkixParams.setRevocationEnabled(false) // Disable CRL checks
+
+ // Validate certificate chain
+ val certPath = CertificateFactory.getInstance("X.509").generateCertPath(Collections.singletonList(certificate))
+ val validator = CertPathValidator.getInstance("PKIX")
+ validator.validate(certPath, pkixParams)
+
+ logger.info("Certificate is valid and trusted.")
+ true
+ } match {
+ case Success(_) => true
+ case Failure(e: CertPathValidatorException) =>
+ logger.info(s"Certificate validation failed: ${e.getMessage}")
+ false
+ case Failure(e) =>
+ logger.info(s"Error: ${e.getMessage}")
+ false
+ }
+ }
+
+ /**
+ * Converts a PEM certificate (Base64-encoded) into an `X509Certificate` object.
+ *
+ * This function:
+ * - Removes the PEM header and footer (`-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----`).
+ * - Decodes the Base64-encoded certificate data.
+ * - Generates and returns an `X509Certificate` object.
+ *
+ * @param pem The X.509 certificate in PEM format.
+ * @return The parsed `X509Certificate` object.
+ */
+ private def parsePemToX509Certificate(pem: String): X509Certificate = {
+ val cleanedPem = pem.replaceAll("-----BEGIN CERTIFICATE-----", "")
+ .replaceAll("-----END CERTIFICATE-----", "")
+ .replaceAll("\\s", "")
+
+ val decoded = Base64.getDecoder.decode(cleanedPem)
+ val certFactory = CertificateFactory.getInstance("X.509")
+ certFactory.generateCertificate(new ByteArrayInputStream(decoded)).asInstanceOf[X509Certificate]
+ }
+
+ def loadPemCertificateFromFile(filePath: String): Option[String] = {
+ Try {
+ val source = Source.fromFile(filePath)
+ try source.getLines().mkString("\n") // Read entire file into a single string
+ finally source.close()
+ } match {
+ case Success(pem) => Some(pem)
+ case Failure(exception) =>
+ logger.error(s"Failed to load PEM certificate from file: ${exception.getMessage}")
+ None
+ }
+ }
+
+ def main(args: Array[String]): Unit = {
+ // change the following path if using this function to test on your localhost
+ val certificatePath = "/path/to/certificate.pem"
+ val pemCertificate = loadPemCertificateFromFile(certificatePath)
+
+ pemCertificate.foreach { pem =>
+ val isValid = validateCertificate(pem)
+ 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/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
index 37061e2c2..e26c48520 100644
--- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
+++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
@@ -279,6 +279,7 @@ object ErrorMessages {
val X509ThereAreNoPsd2Roles = "OBP-20308: PEM Encoded Certificate does not contain PSD2 roles."
val X509CannotGetPublicKey = "OBP-20309: Public key cannot be found in the PEM Encoded Certificate."
val X509PublicKeyCannotVerify = "OBP-20310: Certificate's public key cannot be used to verify signed request."
+ val X509PublicKeyCannotBeValidated = "OBP-20312: Certificate's public key cannot be validated."
val X509RequestIsNotSigned = "OBP-20311: The Request is not signed."
// OpenID Connect
diff --git a/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala b/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala
index 22e366afc..0024598c0 100644
--- a/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala
+++ b/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala
@@ -72,7 +72,9 @@ trait OBPDataImport extends MdcLoggable {
protected def dataOrFirstFailure[T](boxes : List[Box[T]]) = {
val firstFailure = boxes.collectFirst{case f: Failure => f}
firstFailure match {
- case Some(f) => f
+ case Some(f) =>
+ logger.debug(f)
+ f
case None => Full(boxes.flatten) //no failures, so we can return the results
}
}
diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala
index 9b8956ef9..59bcbfb57 100644
--- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala
+++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala
@@ -1,89 +1,302 @@
/**
-Open Bank Project - API
-Copyright (C) 2011-2019, TESOBE GmbH.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see
+ {formTextHtml} +
+Allowed actions:
+Read account details
+Read account balances
+Read transactions
+Accounts:
+This consent will end on date {json.validUntil}.
+I understand that I can revoke this consent at any time.
+