feat(middleware): Add middleware
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub_middleware
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class LegalconsenthubMiddlewareApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<LegalconsenthubMiddlewareApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub_middleware.exception
|
||||
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware.smartcard.SmartCardNotFoundException
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware.smartcard.SmartCardReadException
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware.smartcard.SmartCardUnavailableException
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
import java.time.Instant
|
||||
|
||||
@RestControllerAdvice
|
||||
class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(SmartCardNotFoundException::class)
|
||||
fun handleSmartCardNotFoundException(ex: SmartCardNotFoundException): ResponseEntity<Map<String, Any>> {
|
||||
val problem = mutableMapOf<String, Any>()
|
||||
problem["type"] = "https://problems-registry.smartbear.com/not-found"
|
||||
problem["title"] = "Smart Card Not Found"
|
||||
problem["status"] = HttpStatus.NOT_FOUND.value()
|
||||
problem["detail"] = ex.message ?: "Smart card not found"
|
||||
problem["instance"] = "/smart-card/info"
|
||||
problem["timestamp"] = Instant.now().toString()
|
||||
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.header("Content-Type", "application/problem+json")
|
||||
.body(problem)
|
||||
}
|
||||
|
||||
@ExceptionHandler(SmartCardReadException::class)
|
||||
fun handleSmartCardReadException(ex: SmartCardReadException): ResponseEntity<Map<String, Any>> {
|
||||
val problem = mutableMapOf<String, Any>()
|
||||
problem["type"] = "https://problems-registry.smartbear.com/server-error"
|
||||
problem["title"] = "Smart Card Read Error"
|
||||
problem["status"] = HttpStatus.INTERNAL_SERVER_ERROR.value()
|
||||
problem["detail"] = ex.message ?: "Error reading smart card"
|
||||
problem["instance"] = "/smart-card/info"
|
||||
problem["timestamp"] = Instant.now().toString()
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.header("Content-Type", "application/problem+json")
|
||||
.body(problem)
|
||||
}
|
||||
|
||||
@ExceptionHandler(SmartCardUnavailableException::class)
|
||||
fun handleSmartCardUnavailableException(ex: SmartCardUnavailableException): ResponseEntity<Map<String, Any>> {
|
||||
val problem = mutableMapOf<String, Any>()
|
||||
problem["type"] = "https://problems-registry.smartbear.com/service-unavailable"
|
||||
problem["title"] = "Smart Card Service Unavailable"
|
||||
problem["status"] = HttpStatus.SERVICE_UNAVAILABLE.value()
|
||||
problem["detail"] = ex.message ?: "Smart card service unavailable"
|
||||
problem["instance"] = "/smart-card/info"
|
||||
problem["timestamp"] = Instant.now().toString()
|
||||
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.header("Content-Type", "application/problem+json")
|
||||
.body(problem)
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception::class)
|
||||
fun handleGenericException(ex: Exception): ResponseEntity<Map<String, Any>> {
|
||||
val problem = mutableMapOf<String, Any>()
|
||||
problem["type"] = "about:blank"
|
||||
problem["title"] = "Internal Server Error"
|
||||
problem["status"] = HttpStatus.INTERNAL_SERVER_ERROR.value()
|
||||
problem["detail"] = "An unexpected error occurred: ${ex.message ?: "Unknown error"}"
|
||||
problem["timestamp"] = Instant.now().toString()
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.header("Content-Type", "application/problem+json")
|
||||
.body(problem)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub_middleware.signature
|
||||
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware_api.api.SignatureApi
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware_api.model.VerifySignatureResponseDto
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
|
||||
@RestController
|
||||
class SignatureController(
|
||||
private val signatureService: SignatureService
|
||||
) : SignatureApi {
|
||||
|
||||
override fun signPdfHash(
|
||||
document: MultipartFile,
|
||||
certificateId: String,
|
||||
hashAlgorithm: String
|
||||
): ResponseEntity<String> {
|
||||
val signature = signatureService.signPdfHash(
|
||||
document = document.bytes,
|
||||
certificateId = certificateId,
|
||||
hashAlgorithm = hashAlgorithm
|
||||
)
|
||||
return ResponseEntity.ok(signature)
|
||||
}
|
||||
|
||||
override fun verifySignature(
|
||||
document: MultipartFile,
|
||||
signature: String,
|
||||
certificateId: String?,
|
||||
hashAlgorithm: String
|
||||
): ResponseEntity<VerifySignatureResponseDto> {
|
||||
val verificationResult = signatureService.verifySignature(
|
||||
document = document.bytes,
|
||||
signature = signature,
|
||||
certificateId = certificateId,
|
||||
hashAlgorithm = hashAlgorithm
|
||||
)
|
||||
return ResponseEntity.ok(verificationResult)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub_middleware.signature
|
||||
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware.smartcard.SmartCardService
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware_api.model.VerifySignatureResponseDto
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware_api.model.CertificateDto
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.*
|
||||
|
||||
@Service
|
||||
class SignatureService(
|
||||
private val smartCardService: SmartCardService
|
||||
) {
|
||||
|
||||
fun signPdfHash(
|
||||
document: ByteArray,
|
||||
certificateId: String,
|
||||
hashAlgorithm: String
|
||||
): String {
|
||||
val hashBase64 = smartCardService.calculateHash(document, hashAlgorithm)
|
||||
|
||||
val certificates = smartCardService.getSmartCardCertificates()
|
||||
certificates.find { it.id == certificateId }
|
||||
?: throw IllegalArgumentException("Certificate not found: $certificateId")
|
||||
|
||||
// Sign the hash using smart card (PIN will be entered on device)
|
||||
return smartCardService.signHash(hashBase64, certificateId)
|
||||
}
|
||||
|
||||
fun verifySignature(
|
||||
document: ByteArray,
|
||||
signature: String,
|
||||
certificateId: String?,
|
||||
hashAlgorithm: String
|
||||
): VerifySignatureResponseDto {
|
||||
try {
|
||||
val certificates = smartCardService.getSmartCardCertificates()
|
||||
val certificate = if (certificateId != null) {
|
||||
certificates.find { it.id == certificateId }
|
||||
?: throw IllegalArgumentException("Certificate not found: $certificateId")
|
||||
} else {
|
||||
// If no specific certificate ID provided, try to find the certificate from the signature
|
||||
// For now, we'll use the first available certificate
|
||||
certificates.firstOrNull()
|
||||
?: throw IllegalArgumentException("No certificates available for verification")
|
||||
}
|
||||
|
||||
val isValid = smartCardService.verifySignature(document, signature, certificate.id, hashAlgorithm)
|
||||
|
||||
return VerifySignatureResponseDto(
|
||||
isValid = isValid,
|
||||
certificateInfo = certificate,
|
||||
verificationDetails = "Signature verified using pkcs11-tool with ${hashAlgorithm} algorithm"
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
return VerifySignatureResponseDto(
|
||||
isValid = false,
|
||||
certificateInfo = null,
|
||||
verificationDetails = "Verification failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub_middleware.smartcard
|
||||
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware_api.api.SmartCardApi
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware_api.model.CertificateDto
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware_api.model.SmartCardInfoDto
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@RestController
|
||||
class SmartCardController(
|
||||
private val smartCardService: SmartCardService
|
||||
) : SmartCardApi {
|
||||
|
||||
override fun getSmartCardInfo(): ResponseEntity<SmartCardInfoDto> {
|
||||
val smartCardInfo = smartCardService.getSmartCardInfo()
|
||||
return ResponseEntity.ok(smartCardInfo)
|
||||
}
|
||||
|
||||
override fun getSmartCardCertificates(): ResponseEntity<List<CertificateDto>> {
|
||||
val certificates = smartCardService.getSmartCardCertificates()
|
||||
return ResponseEntity.ok(certificates)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub_middleware.smartcard
|
||||
|
||||
class SmartCardNotFoundException(message: String) : RuntimeException(message)
|
||||
class SmartCardReadException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
|
||||
class SmartCardUnavailableException(message: String) : RuntimeException(message)
|
||||
@@ -0,0 +1,281 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub_middleware.smartcard
|
||||
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware_api.model.CertificateDto
|
||||
import com.betriebsratkanzlei.legalconsenthub_middleware_api.model.SmartCardInfoDto
|
||||
import jakarta.annotation.PostConstruct
|
||||
import org.apache.commons.exec.CommandLine
|
||||
import org.apache.commons.exec.DefaultExecutor
|
||||
import org.apache.commons.exec.PumpStreamHandler
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.core.io.ResourceLoader
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
@Service
|
||||
class SmartCardService(
|
||||
@Value("\${opensc.pkcs11.library.path}") private val openscPkcs11LibPath: String,
|
||||
private val resourceLoader: ResourceLoader
|
||||
) {
|
||||
|
||||
private lateinit var resolvedLibraryPath: String
|
||||
|
||||
@PostConstruct
|
||||
fun init() {
|
||||
resolvedLibraryPath = resolveLibraryPath()
|
||||
}
|
||||
|
||||
|
||||
// macOS doesn't allow loading native libraries using classpath URLs. The library needs to be loaded from an absolute file path.
|
||||
private fun resolveLibraryPath(): String {
|
||||
val resource = resourceLoader.getResource(openscPkcs11LibPath)
|
||||
if (!resource.exists()) {
|
||||
throw IllegalStateException("PKCS11 library not found: $openscPkcs11LibPath")
|
||||
}
|
||||
|
||||
val tempFile = Files.createTempFile("opensc-pkcs11", ".so").toFile()
|
||||
tempFile.deleteOnExit()
|
||||
|
||||
resource.inputStream.use { input ->
|
||||
Files.copy(input, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
|
||||
tempFile.setExecutable(true)
|
||||
tempFile.setReadable(true)
|
||||
|
||||
return tempFile.absolutePath
|
||||
}
|
||||
|
||||
fun getSmartCardInfo(): SmartCardInfoDto {
|
||||
val commandLine = CommandLine.parse("pkcs11-tool --module $resolvedLibraryPath --show-info")
|
||||
val executor = DefaultExecutor()
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val errorStream = ByteArrayOutputStream()
|
||||
|
||||
executor.streamHandler = PumpStreamHandler(outputStream, errorStream)
|
||||
|
||||
return try {
|
||||
val exitCode = executor.execute(commandLine)
|
||||
|
||||
if (exitCode == 0) {
|
||||
val output = outputStream.toString()
|
||||
parseSmartCardInfo(output)
|
||||
} else {
|
||||
val errorOutput = errorStream.toString()
|
||||
throw SmartCardNotFoundException("No smart card detected: $errorOutput")
|
||||
}
|
||||
} catch (e: SmartCardNotFoundException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw SmartCardReadException("Error reading smart card: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSmartCardCertificates(): List<CertificateDto> {
|
||||
val commandLine = CommandLine.parse("pkcs11-tool --module $resolvedLibraryPath --list-objects --type cert")
|
||||
val executor = DefaultExecutor()
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val errorStream = ByteArrayOutputStream()
|
||||
|
||||
executor.streamHandler = PumpStreamHandler(outputStream, errorStream)
|
||||
|
||||
return try {
|
||||
val exitCode = executor.execute(commandLine)
|
||||
|
||||
if (exitCode == 0) {
|
||||
val output = outputStream.toString()
|
||||
parseCertificates(output)
|
||||
} else {
|
||||
val errorOutput = errorStream.toString()
|
||||
throw SmartCardNotFoundException("No smart card detected or no certificates found: $errorOutput")
|
||||
}
|
||||
} catch (e: SmartCardNotFoundException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw SmartCardReadException("Error reading smart card certificates: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun signHash(hash: String, certificateId: String): String {
|
||||
val hashBytes = Base64.getDecoder().decode(hash)
|
||||
|
||||
val tempFile = File.createTempFile("hash_to_sign", ".bin")
|
||||
|
||||
return try {
|
||||
tempFile.writeBytes(hashBytes)
|
||||
|
||||
// Build pkcs11-tool command to sign the hash (PIN will be prompted on device)
|
||||
val commandLine =
|
||||
CommandLine.parse("pkcs11-tool --module $resolvedLibraryPath --sign --mechanism RSA-PKCS --id $certificateId --input-file ${tempFile.absolutePath}")
|
||||
|
||||
val executor = DefaultExecutor()
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val errorStream = ByteArrayOutputStream()
|
||||
|
||||
executor.streamHandler = PumpStreamHandler(outputStream, errorStream)
|
||||
|
||||
val exitCode = executor.execute(commandLine)
|
||||
|
||||
if (exitCode == 0) {
|
||||
val signature = outputStream.toByteArray()
|
||||
Base64.getEncoder().encodeToString(signature)
|
||||
} else {
|
||||
val errorOutput = errorStream.toString()
|
||||
throw RuntimeException("Failed to sign hash: $errorOutput")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Error signing hash: ${e.message}", e)
|
||||
} finally {
|
||||
if (tempFile.exists()) {
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateHash(document: ByteArray, hashAlgorithm: String): String {
|
||||
return try {
|
||||
val javaHashAlgorithm = when (hashAlgorithm.uppercase()) {
|
||||
"SHA1" -> "SHA-1"
|
||||
"SHA256" -> "SHA-256"
|
||||
"SHA384" -> "SHA-384"
|
||||
"SHA512" -> "SHA-512"
|
||||
else -> "SHA-256"
|
||||
}
|
||||
|
||||
val messageDigest = java.security.MessageDigest.getInstance(javaHashAlgorithm)
|
||||
val hashBytes = messageDigest.digest(document)
|
||||
|
||||
Base64.getEncoder().encodeToString(hashBytes)
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Error calculating hash: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun verifySignature(originalData: ByteArray, signature: String, certificateId: String, hashAlgorithm: String = "SHA256"): Boolean {
|
||||
return try {
|
||||
val signatureBytes = Base64.getDecoder().decode(signature)
|
||||
|
||||
// Calculate hash of the original data using the same algorithm that was used for signing
|
||||
val hashBase64 = calculateHash(originalData, hashAlgorithm)
|
||||
val hashBytes = Base64.getDecoder().decode(hashBase64)
|
||||
|
||||
val hashFile = File.createTempFile("hash_to_verify", ".bin")
|
||||
val signatureFile = File.createTempFile("signature_to_verify", ".bin")
|
||||
|
||||
try {
|
||||
hashFile.writeBytes(hashBytes)
|
||||
signatureFile.writeBytes(signatureBytes)
|
||||
|
||||
// Verify against the hash, not the original document, since the signature was created from the hash
|
||||
val verifyCommand = CommandLine.parse("pkcs11-tool --module $resolvedLibraryPath -m RSA-PKCS --verify --id $certificateId --input-file ${hashFile.absolutePath} --signature-file ${signatureFile.absolutePath}")
|
||||
|
||||
val executor = DefaultExecutor()
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val errorStream = ByteArrayOutputStream()
|
||||
|
||||
executor.streamHandler = PumpStreamHandler(outputStream, errorStream)
|
||||
val exitCode = executor.execute(verifyCommand)
|
||||
|
||||
if (exitCode == 0) {
|
||||
println("Signature verification successful")
|
||||
true
|
||||
} else {
|
||||
println("Signature verification failed: ${errorStream.toString()}")
|
||||
false
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (hashFile.exists()) hashFile.delete()
|
||||
if (signatureFile.exists()) signatureFile.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Error during signature verification: ${e.message}")
|
||||
// If any error occurs during verification, consider it invalid
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSmartCardInfo(output: String): SmartCardInfoDto {
|
||||
val lines = output.split("\n")
|
||||
var label = "Unknown"
|
||||
var serialNumber: String? = null
|
||||
var manufacturer: String? = null
|
||||
var model: String? = null
|
||||
|
||||
for (line in lines) {
|
||||
when {
|
||||
line.contains("Token label") -> {
|
||||
label = line.split(":").getOrNull(1)?.trim() ?: "Unknown"
|
||||
}
|
||||
|
||||
line.contains("Serial number") -> {
|
||||
serialNumber = line.split(":").getOrNull(1)?.trim()
|
||||
}
|
||||
|
||||
line.contains("Manufacturer") -> {
|
||||
manufacturer = line.split(":").getOrNull(1)?.trim()
|
||||
}
|
||||
|
||||
line.contains("Model") -> {
|
||||
model = line.split(":").getOrNull(1)?.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SmartCardInfoDto(
|
||||
isPresent = true,
|
||||
label = label,
|
||||
serialNumber = serialNumber,
|
||||
manufacturer = manufacturer,
|
||||
model = model
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseCertificates(output: String): List<CertificateDto> {
|
||||
val certificates = mutableListOf<CertificateDto>()
|
||||
val lines = output.split("\n")
|
||||
|
||||
// This is a simplified parser - in reality, you'd need more sophisticated parsing
|
||||
var currentId: String? = null
|
||||
var currentLabel: String? = null
|
||||
|
||||
for (line in lines) {
|
||||
when {
|
||||
line.contains("Certificate Object") -> {
|
||||
currentId = UUID.randomUUID().toString()
|
||||
currentLabel = null
|
||||
}
|
||||
|
||||
line.contains("label:") -> {
|
||||
currentLabel = line.split(":").getOrNull(1)?.trim()
|
||||
}
|
||||
|
||||
line.contains("ID:") -> {
|
||||
val id = line.split(":").getOrNull(1)?.trim()
|
||||
if (currentId != null && currentLabel != null) {
|
||||
certificates.add(createCertificateDto(currentLabel, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return certificates
|
||||
}
|
||||
|
||||
// TODO: Replace with actual certificate parsing logic
|
||||
private fun createCertificateDto(label: String, keyId: String?): CertificateDto {
|
||||
return CertificateDto(
|
||||
id = keyId ?: "",
|
||||
subject = label,
|
||||
issuer = "Unknown Issuer",
|
||||
validFrom = LocalDateTime.now().minusYears(1),
|
||||
validTo = LocalDateTime.now().plusYears(3),
|
||||
keyUsage = listOf("digitalSignature", "keyEncipherment"),
|
||||
fingerprint = "Unknown"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
spring:
|
||||
application:
|
||||
name: legalconsenthub-middleware
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 10MB
|
||||
max-request-size: 10MB
|
||||
|
||||
server:
|
||||
port: 8081
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.betriebsratkanzlei.legalconsenthub_middleware: DEBUG
|
||||
org.springframework.security: DEBUG
|
||||
|
||||
#springdoc:
|
||||
# api-docs:
|
||||
# path: /v3/api-docs
|
||||
# swagger-ui:
|
||||
# path: /swagger-ui.html
|
||||
# operationsSorter: method
|
||||
|
||||
opensc:
|
||||
pkcs11:
|
||||
library:
|
||||
path: classpath:binaries/opensc-pkcs11.so
|
||||
BIN
legalconsenthub-middleware/src/main/resources/binaries/opensc-pkcs11.so
Executable file
BIN
legalconsenthub-middleware/src/main/resources/binaries/opensc-pkcs11.so
Executable file
Binary file not shown.
Reference in New Issue
Block a user