mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 15:27:01 +00:00
Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
ab24e1767b
@ -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.
|
||||
@ -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))
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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!") // ❌ false(too short)
|
||||
fullPasswordValidation("alllowercase123!") // ❌ false(no capital letter)
|
||||
fullPasswordValidation("ALLUPPERCASE123!") // ❌ false(no smaller case letter)
|
||||
fullPasswordValidation("NoSpecialChar123") // ❌ false(not 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!") // ❌ false(too short)
|
||||
fullPasswordValidation("alllowercase123!") // ❌ false(no capital letter)
|
||||
fullPasswordValidation("ALLUPPERCASE123!") // ❌ false(no smaller case letter)
|
||||
fullPasswordValidation("NoSpecialChar123") // ❌ false(not special character)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user