feature/keyId.CN is mandatory and SN is checked in dec and hex systems

This commit is contained in:
Marko Milić 2025-07-08 14:41:00 +02:00
parent 667b14a0b7
commit ac17aa5de7
2 changed files with 98 additions and 26 deletions

View File

@ -117,22 +117,32 @@ object BerlinGroupCheck extends MdcLoggable {
maybeSignature.flatMap { header =>
BerlinGroupSignatureHeaderParser.parseSignatureHeader(header) match {
case Right(parsed) =>
// Log parsed values
logger.debug(s"Parsed Signature Header:")
logger.debug(s" SN: ${parsed.keyId.sn}")
logger.debug(s" CA: ${parsed.keyId.ca}")
logger.debug(s" CN: ${parsed.keyId.cn}")
logger.debug(s" O: ${parsed.keyId.o}")
logger.debug(s" Headers: ${parsed.headers.mkString(", ")}")
logger.debug(s" Signature: ${parsed.signature}")
val certificate = getCertificateFromTppSignatureCertificate(reqHeaders)
val serialNumber = certificate.getSerialNumber.toString
if(parsed.keyId.sn != serialNumber) {
logger.debug(s"Serial number from certificate: $serialNumber")
logger.debug(s"keyId.SN:${parsed.keyId.sn}")
val certSerialNumber = certificate.getSerialNumber
logger.debug(s"Certificate serial number (decimal): ${certSerialNumber.toString}")
logger.debug(s"Certificate serial number (hex): ${certSerialNumber.toString(16).toUpperCase}")
val snMatches = BerlinGroupSignatureHeaderParser.doesSerialNumberMatch(parsed.keyId.sn, certSerialNumber)
if (!snMatches) {
logger.debug(s"Serial number mismatch. Parsed SN: ${parsed.keyId.sn}, Certificate decimal: ${certSerialNumber.toString}, Certificate hex: ${certSerialNumber.toString(16).toUpperCase}")
Some(
(
fullBoxOrException(
Empty ~> APIFailureNewStyle(s"${ErrorMessages.InvalidSignatureHeader}keyId.SN does not match the serial number from certificate", 400, forwardResult._2.map(_.toLight))
Empty ~> APIFailureNewStyle(
s"${ErrorMessages.InvalidSignatureHeader} keyId.SN does not match the serial number from certificate",
400,
forwardResult._2.map(_.toLight)
)
),
forwardResult._2
)
@ -140,11 +150,16 @@ object BerlinGroupCheck extends MdcLoggable {
} else {
None // All good
}
case Left(error) =>
Some(
(
fullBoxOrException(
Empty ~> APIFailureNewStyle(s"${ErrorMessages.InvalidSignatureHeader}$error", 400, forwardResult._2.map(_.toLight))
Empty ~> APIFailureNewStyle(
s"${ErrorMessages.InvalidSignatureHeader} $error",
400,
forwardResult._2.map(_.toLight)
)
),
forwardResult._2
)

View File

@ -1,8 +1,10 @@
package code.api.util
object BerlinGroupSignatureHeaderParser {
import code.util.Helper.MdcLoggable
case class ParsedKeyId(sn: String, ca: String, o: String)
object BerlinGroupSignatureHeaderParser extends MdcLoggable {
case class ParsedKeyId(sn: String, ca: String, cn: String, o: String)
case class ParsedSignature(keyId: ParsedKeyId, headers: List[String], signature: String)
@ -18,13 +20,32 @@ object BerlinGroupSignatureHeaderParser {
}
}.toMap
val caValue = kvMap.get("CA").map(_.stripPrefix("CN="))
// mandatory fields
val snOpt = kvMap.get("SN")
val oOpt = kvMap.get("O")
val caOpt = kvMap.get("CA")
val cnOpt = kvMap.get("CN")
val (caFinal, cnFinal): (String, String) = (caOpt, cnOpt) match {
case (Some(caRaw), Some(cnRaw)) =>
// Both CA and CN are present: use them as-is
(caRaw, cnRaw)
case (Some(caRaw), None) if caRaw.startsWith("CN=") =>
// Special case: CA=CN=... set both CA and CN to value after CN=
val value = caRaw.stripPrefix("CN=")
(value, value)
(kvMap.get("SN"), caValue, kvMap.get("O")) match {
case (Some(sn), Some(ca), Some(o)) =>
Right(ParsedKeyId(sn, ca, o))
case _ =>
Left(s"Invalid or missing fields in keyId: found keys ${kvMap.keys.mkString(", ")}")
return Left(s"Missing mandatory 'CN' field or invalid CA format: found keys ${kvMap.keys.mkString(", ")}")
}
(snOpt, oOpt) match {
case (Some(sn), Some(o)) =>
Right(ParsedKeyId(sn, caFinal, cnFinal, o))
case _ =>
Left(s"Missing mandatory 'SN' or 'O' field: found keys ${kvMap.keys.mkString(", ")}")
}
}
@ -46,21 +67,57 @@ object BerlinGroupSignatureHeaderParser {
} yield ParsedSignature(keyId, headers, sig)
}
/**
* Detect and match incoming SN as decimal or hex against certificate serial number.
*/
def doesSerialNumberMatch(incomingSn: String, certSerial: java.math.BigInteger): Boolean = {
try {
val incomingAsDecimal = new java.math.BigInteger(incomingSn, 10)
if (incomingAsDecimal == certSerial) {
logger.debug(s"SN matched in decimal")
return true
}
} catch {
case _: NumberFormatException =>
logger.debug(s"Incoming SN is not valid decimal: $incomingSn")
}
try {
val incomingAsHex = new java.math.BigInteger(incomingSn, 16)
if (incomingAsHex == certSerial) {
logger.debug(s"SN matched in hex")
return true
}
} catch {
case _: NumberFormatException =>
logger.debug(s"Incoming SN is not valid hex: $incomingSn")
}
false
}
// Example usage
def main(args: Array[String]): Unit = {
val header =
"""keyId="CA=CN=MAIB Prisacaru Sergiu (Test), SN=43A, O=MAIB", headers="digest date x-request-id", signature="abc123+==" """
val testHeaders = List(
"""keyId="CA=CN=MAIB Prisacaru Sergiu (Test), SN=43A, O=MAIB", headers="digest date x-request-id", signature="abc123+=="""",
"""keyId="CA=SomeCAValue, CN=SomeCNValue, SN=43A, O=MAIB", headers="digest date x-request-id", signature="def456+=="""",
"""keyId="CA=MissingCN, SN=43A, O=MAIB", headers="digest date x-request-id", signature="should_fail""""
)
parseSignatureHeader(header) match {
case Right(parsed) =>
println("Parsed Signature Header:")
println(s"SN: ${parsed.keyId.sn}")
println(s"CA: ${parsed.keyId.ca}")
println(s"O: ${parsed.keyId.o}")
println(s"Headers: ${parsed.headers.mkString(", ")}")
println(s"Signature: ${parsed.signature}")
case Left(error) =>
println(s"Error: $error")
testHeaders.foreach { header =>
println(s"\nParsing header:\n$header")
parseSignatureHeader(header) match {
case Right(parsed) =>
println("Parsed Signature Header:")
println(s" SN: ${parsed.keyId.sn}")
println(s" CA: ${parsed.keyId.ca}")
println(s" CN: ${parsed.keyId.cn}")
println(s" O: ${parsed.keyId.o}")
println(s" Headers: ${parsed.headers.mkString(", ")}")
println(s" Signature: ${parsed.signature}")
case Left(error) =>
println(s"Error: $error")
}
}
}
}