Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Marko Milić 2025-02-21 17:09:29 +01:00
commit ab24e1767b
4 changed files with 181 additions and 23 deletions

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ import code.api.util._
import code.setup.PropsReset
import code.util.Helper.SILENCE_IS_GOLDEN
import com.openbankproject.commons.model.UserAuthContextCommons
import com.github.dwickern.macros.NameOf.nameOf
import net.liftweb.common.{Box, Empty, Full}
import net.liftweb.http.provider.HTTPParam
import net.liftweb.json.{JValue, parse}
@ -838,6 +839,65 @@ class APIUtilTest extends FeatureSpec with Matchers with GivenWhenThen with Prop
actualValue6 contains (InvalidValueLength) shouldBe (true)
}
scenario(s"Test the ${nameOf(APIUtil.basicPasswordValidation _)} method") {
val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]"
basicPasswordValidation(firefoxStrongPasswordProposal) shouldBe (SILENCE_IS_GOLDEN) // SILENCE_IS_GOLDEN
basicPasswordValidation("Abc!123 xyz") shouldBe (SILENCE_IS_GOLDEN) // SILENCE_IS_GOLDEN
basicPasswordValidation("SuperStrong#123") shouldBe (SILENCE_IS_GOLDEN) // SILENCE_IS_GOLDEN
basicPasswordValidation("Hello World!") shouldBe (SILENCE_IS_GOLDEN) // SILENCE_IS_GOLDEN
basicPasswordValidation(" ") shouldBe (SILENCE_IS_GOLDEN) // SILENCE_IS_GOLDEN allow space so far
basicPasswordValidation("short💥") shouldBe (InvalidValueCharacters) // ErrorMessages.InvalidValueCharacters
basicPasswordValidation("a" * 513) shouldBe (InvalidValueLength) // ErrorMessages.InvalidValueLength
}
scenario(s"Test the ${nameOf(APIUtil.fullPasswordValidation _)} method") {
val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]"
fullPasswordValidation(firefoxStrongPasswordProposal) // true
fullPasswordValidation("Abc!123xyz") // true
fullPasswordValidation("SuperStrong#123") // true
fullPasswordValidation("Abcdefg!1") // true
fullPasswordValidation("short1!") // falsetoo short
fullPasswordValidation("alllowercase123!") // falseno capital letter
fullPasswordValidation("ALLUPPERCASE123!") // falseno smaller case letter
fullPasswordValidation("NoSpecialChar123") // falsenot special character
}
}
feature(s"test ${nameOf(APIUtil.basicPasswordValidation _)} and ${nameOf(APIUtil.fullPasswordValidation _)}") {
scenario(s"Test the ${nameOf(APIUtil.basicPasswordValidation _)} method") {
val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]"
basicPasswordValidation(firefoxStrongPasswordProposal) shouldBe (SILENCE_IS_GOLDEN) // SILENCE_IS_GOLDEN
basicPasswordValidation("Abc!123 xyz") shouldBe (SILENCE_IS_GOLDEN) // SILENCE_IS_GOLDEN
basicPasswordValidation("SuperStrong#123") shouldBe (SILENCE_IS_GOLDEN) // SILENCE_IS_GOLDEN
basicPasswordValidation("Hello World!") shouldBe (SILENCE_IS_GOLDEN) // SILENCE_IS_GOLDEN
basicPasswordValidation(" ") shouldBe (SILENCE_IS_GOLDEN) // SILENCE_IS_GOLDEN allow space so far
basicPasswordValidation("short💥") shouldBe (InvalidValueCharacters) // ErrorMessages.InvalidValueCharacters
basicPasswordValidation("a" * 513) shouldBe (InvalidValueLength) // ErrorMessages.InvalidValueLength
}
scenario(s"Test the ${nameOf(APIUtil.fullPasswordValidation _)} method") {
val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]"
fullPasswordValidation(firefoxStrongPasswordProposal) // true
fullPasswordValidation("Abc!123xyz") // true
fullPasswordValidation("SuperStrong#123") // true
fullPasswordValidation("Abcdefg!1") // true
fullPasswordValidation("short1!") // falsetoo short
fullPasswordValidation("alllowercase123!") // falseno capital letter
fullPasswordValidation("ALLUPPERCASE123!") // falseno smaller case letter
fullPasswordValidation("NoSpecialChar123") // falsenot special character
}
}