From ad6770742cb60442d4bf255b710e7874ea21a909 Mon Sep 17 00:00:00 2001 From: Tim Schauder Date: Fri, 21 Jun 2024 11:53:50 +0200 Subject: [PATCH 01/10] Rename routes --- .../wallet/attestation/controllers/IssuerApi.kt | 2 +- .../wallet/attestation/controllers/WalletApi.kt | 16 +++++++--------- .../{UserEntity.kt => WalletEntity.kt} | 4 ++-- .../{UserRepository.kt => WalletRepository.kt} | 4 +++- .../wallet/attestation/requests/NonceRequest.kt | 2 +- .../attestation/services/WalletApiService.kt | 10 +++++----- .../services/WalletApiServiceTests.kt | 10 +++++----- 7 files changed, 24 insertions(+), 24 deletions(-) rename src/main/kotlin/software/tice/wallet/attestation/repositories/{UserEntity.kt => WalletEntity.kt} (81%) rename src/main/kotlin/software/tice/wallet/attestation/repositories/{UserRepository.kt => WalletRepository.kt} (59%) diff --git a/src/main/kotlin/software/tice/wallet/attestation/controllers/IssuerApi.kt b/src/main/kotlin/software/tice/wallet/attestation/controllers/IssuerApi.kt index c7c3c66..88b6448 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/controllers/IssuerApi.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/controllers/IssuerApi.kt @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.* import software.tice.wallet.attestation.requests.ValidationRequest @RestController -@RequestMapping("attestation") +@RequestMapping("wallet") class IssuerApi { @PostMapping("/validation") diff --git a/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt b/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt index 9a26aee..40fc9b4 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt @@ -7,21 +7,19 @@ import software.tice.wallet.attestation.responses.AttestationResponse import software.tice.wallet.attestation.responses.NonceResponse import software.tice.wallet.attestation.services.WalletApiService + @RestController -@RequestMapping("attestation") +@RequestMapping("wallet") class WalletApi(val walletApiService: WalletApiService) { - @PostMapping("/nonces") + @PostMapping() fun requestNonces(@RequestBody request: NonceRequest): NonceResponse { - return walletApiService.requestNonces(request.walletInstanceId) + return walletApiService.requestNonces(request.walletId) } - @PostMapping("/request/{id}") - fun requestAttestation( - @RequestBody request: AttestationRequest, - @PathVariable id: String - ): AttestationResponse { - return walletApiService.requestAttestation(request, id) + @PostMapping("/{walletId}/attestation") + fun requestAttestation(@RequestBody request: AttestationRequest, @PathVariable walletId: String): AttestationResponse { + return walletApiService.requestAttestation(request, walletId) } } \ No newline at end of file diff --git a/src/main/kotlin/software/tice/wallet/attestation/repositories/UserEntity.kt b/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt similarity index 81% rename from src/main/kotlin/software/tice/wallet/attestation/repositories/UserEntity.kt rename to src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt index b3f7bc6..b98b133 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/repositories/UserEntity.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt @@ -4,11 +4,11 @@ import jakarta.persistence.* @Entity(name = "users") -data class UserEntity( +data class WalletEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long?, - var walletInstanceId: String, + var walletId: String, var popNonce: String?, var keyAttestationNonce: String? ) \ No newline at end of file diff --git a/src/main/kotlin/software/tice/wallet/attestation/repositories/UserRepository.kt b/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletRepository.kt similarity index 59% rename from src/main/kotlin/software/tice/wallet/attestation/repositories/UserRepository.kt rename to src/main/kotlin/software/tice/wallet/attestation/repositories/WalletRepository.kt index f3501d3..58355c7 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/repositories/UserRepository.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletRepository.kt @@ -5,5 +5,7 @@ import org.springframework.stereotype.Repository @Repository -interface UserRepository : JpaRepository +interface WalletRepository : JpaRepository { + fun findByWalletId(walletId: String): WalletEntity? +} diff --git a/src/main/kotlin/software/tice/wallet/attestation/requests/NonceRequest.kt b/src/main/kotlin/software/tice/wallet/attestation/requests/NonceRequest.kt index 8fbe07f..5bcc7e6 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/requests/NonceRequest.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/requests/NonceRequest.kt @@ -1,3 +1,3 @@ package software.tice.wallet.attestation.requests -data class NonceRequest(val walletInstanceId: String) \ No newline at end of file +data class NonceRequest(val walletId: String) \ No newline at end of file diff --git a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt index 926271e..33422ba 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt @@ -4,8 +4,8 @@ import io.jsonwebtoken.Jwts import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service -import software.tice.wallet.attestation.repositories.UserEntity -import software.tice.wallet.attestation.repositories.UserRepository +import software.tice.wallet.attestation.repositories.WalletEntity +import software.tice.wallet.attestation.repositories.WalletRepository import software.tice.wallet.attestation.requests.AttestationRequest import software.tice.wallet.attestation.responses.AttestationResponse import software.tice.wallet.attestation.responses.NonceResponse @@ -17,14 +17,14 @@ import java.util.* class WalletApiService @Autowired constructor( @Value("\${private.key}") private val privateKey: String, - private val userRepository: UserRepository, + private val userRepository: WalletRepository, ) { fun requestNonces(walletInstanceId: String): NonceResponse { val (popNonce, keyAttestationNonce) = List(2) { UUID.randomUUID().toString() } - val user = UserEntity( - walletInstanceId = walletInstanceId, + val user = WalletEntity( + walletId = walletInstanceId, popNonce = popNonce, keyAttestationNonce = keyAttestationNonce, id = null diff --git a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt index a38e0d2..9364fe2 100644 --- a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt +++ b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt @@ -5,8 +5,8 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.* import org.mockito.Mockito.verify -import software.tice.wallet.attestation.repositories.UserEntity -import software.tice.wallet.attestation.repositories.UserRepository +import software.tice.wallet.attestation.repositories.WalletEntity +import software.tice.wallet.attestation.repositories.WalletRepository import software.tice.wallet.attestation.requests.AttestationRequest import java.security.KeyPair import java.util.* @@ -16,14 +16,14 @@ import kotlin.test.assertEquals internal class WalletApiServiceTests { @Mock - private lateinit var userRepository: UserRepository + private lateinit var userRepository: WalletRepository private lateinit var privateKey: String private lateinit var walletApiService: WalletApiService @Captor - private lateinit var userCaptor: ArgumentCaptor + private lateinit var userCaptor: ArgumentCaptor private val keyPair: KeyPair = Jwts.SIG.ES256.keyPair().build() @@ -43,7 +43,7 @@ internal class WalletApiServiceTests { verify(userRepository).save(userCaptor.capture()) val savedUser = userCaptor.value - assertEquals(walletInstanceId, savedUser.walletInstanceId) + assertEquals(walletInstanceId, savedUser.walletId) assertEquals(response.popNonce, savedUser.popNonce) assertEquals(response.keyAttestationNonce, savedUser.keyAttestationNonce) } From af949a9fc02629e7873e18785269cc64bdf242bc Mon Sep 17 00:00:00 2001 From: Tim Schauder Date: Fri, 21 Jun 2024 11:55:30 +0200 Subject: [PATCH 02/10] Add check for existing wallet and for pop --- .../attestation/services/WalletApiService.kt | 82 +++++++++++++++++-- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt index 33422ba..2da0c7e 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt @@ -10,7 +10,11 @@ import software.tice.wallet.attestation.requests.AttestationRequest import software.tice.wallet.attestation.responses.AttestationResponse import software.tice.wallet.attestation.responses.NonceResponse import java.security.KeyFactory +import java.security.PrivateKey +import java.security.PublicKey +import java.security.Signature import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec import java.util.* @Service @@ -30,7 +34,19 @@ class WalletApiService @Autowired constructor( id = null ) - userRepository.save(user) + if (existingWallet != null) { + existingWallet.popNonce = popNonce + existingWallet.keyAttestationNonce = keyAttestationNonce + walletRepository.save(existingWallet) + } else { + val newWallet = WalletEntity( + walletId = walletId, + popNonce = popNonce, + keyAttestationNonce = keyAttestationNonce, + id = null + ) + walletRepository.save(newWallet) + } return NonceResponse(popNonce = popNonce, keyAttestationNonce = keyAttestationNonce ) } @@ -40,13 +56,67 @@ class WalletApiService @Autowired constructor( .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") - val decodedKey = Base64.getDecoder().decode(pem) + val existingWallet = walletRepository.findByWalletId(walletId) + ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Wallet not found") + + // <--- Start: check the PoP ---> + val publicKey: PublicKey = decodePublicKey(requestAttestation.attestationPublicKey) + val parts = requestAttestation.proofOfPossession.split(":") + if (parts.size != 2) { + throw IllegalArgumentException("Expected format 'nonce:signature'") + } + val (nonce, signatureBytes) = parts.map { Base64.getDecoder().decode(it) } + + val signature = Signature.getInstance("SHA256withECDSA") + signature.initVerify(publicKey) + signature.update(nonce) + + val isSignatureValid = signature.verify(signatureBytes) + if (!isSignatureValid) { + throw SecurityException("Invalid signature") + } + + // <--- End: check the PoP ---> + + // <--- End: check the request ---> + + + + // <--- Start: throw away nonces and create random ID ---> + existingWallet.popNonce = null + existingWallet.keyAttestationNonce = null + walletRepository.save(existingWallet) + + val randomId: String = UUID.randomUUID().toString() + // <--- End: throw away nonces and create random ID ---> - val keySpec = PKCS8EncodedKeySpec(decodedKey) - val keyFactory = KeyFactory.getInstance("EC") - val privateKeyReloaded = keyFactory.generatePrivate(keySpec) - val walletAttestation: String = Jwts.builder().subject("Joe").signWith(privateKeyReloaded).compact() + // <--- Start: create walletAttestation ---> + val privateKey = privateKey?.let { decodePrivateKey(it) } + + val walletAttestation: String = Jwts.builder().subject("Joe").claim("publicKey", requestAttestation.attestationPublicKey).claim("randomId", randomId).signWith(privateKey).compact() + // <--- End: create walletAttestation ---> + return AttestationResponse(walletAttestation) } + + fun decodePublicKey(key: String): PublicKey { + val pem = key + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + val keyBytes = Base64.getDecoder().decode(pem) + val keySpec = X509EncodedKeySpec(keyBytes) + val keyFactory = KeyFactory.getInstance("EC") + return keyFactory.generatePublic(keySpec) + } + + fun decodePrivateKey(key: String): PrivateKey { + val pem = key + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + val keyBytes = Base64.getDecoder().decode(pem) + val keySpec = PKCS8EncodedKeySpec(keyBytes) + val keyFactory = KeyFactory.getInstance("EC") + return keyFactory.generatePrivate(keySpec) + } } \ No newline at end of file From 9366c25fd04473543dcf4000d1e75849fe3cb2d7 Mon Sep 17 00:00:00 2001 From: Tim Schauder Date: Fri, 21 Jun 2024 14:12:37 +0200 Subject: [PATCH 03/10] Add ControllerAdvice for exception handling --- .../wallet/attestation/controllers/WalletApi.kt | 13 +++++++++++++ .../attestation/services/WalletApiService.kt | 2 +- .../attestation/exceptions/ControllerAdvice.kt | 14 ++++++++++++++ .../exceptions/WalletNotFoundException.kt | 3 +++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/wallet_server/attestation/exceptions/ControllerAdvice.kt create mode 100644 src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt diff --git a/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt b/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt index 40fc9b4..a791c0c 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt @@ -1,5 +1,11 @@ package software.tice.wallet.attestation.controllers +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import org.springframework.http.MediaType import org.springframework.web.bind.annotation.* import software.tice.wallet.attestation.requests.AttestationRequest import software.tice.wallet.attestation.requests.NonceRequest @@ -19,6 +25,13 @@ class WalletApi(val walletApiService: WalletApiService) { } @PostMapping("/{walletId}/attestation") + @Operation(summary = "Request attestation for a wallet", + description = "Submits an attestation request for a given wallet ID.", + responses = [ + ApiResponse(description = "The wallet attestation certificate", responseCode = "200"), + ApiResponse(description = "Wallet not found", responseCode = "404", content = [Content(mediaType = MediaType.TEXT_PLAIN_VALUE, + schema = Schema(implementation = String::class))]) + ]) fun requestAttestation(@RequestBody request: AttestationRequest, @PathVariable walletId: String): AttestationResponse { return walletApiService.requestAttestation(request, walletId) } diff --git a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt index 2da0c7e..2c053bc 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt @@ -57,7 +57,7 @@ class WalletApiService @Autowired constructor( .replace("-----END PRIVATE KEY-----", "") val existingWallet = walletRepository.findByWalletId(walletId) - ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Wallet not found") + ?: throw WalletNotFoundException("Wallet with id ${walletId} not found") // <--- Start: check the PoP ---> val publicKey: PublicKey = decodePublicKey(requestAttestation.attestationPublicKey) diff --git a/src/main/kotlin/wallet_server/attestation/exceptions/ControllerAdvice.kt b/src/main/kotlin/wallet_server/attestation/exceptions/ControllerAdvice.kt new file mode 100644 index 0000000..184d645 --- /dev/null +++ b/src/main/kotlin/wallet_server/attestation/exceptions/ControllerAdvice.kt @@ -0,0 +1,14 @@ +package wallet_server.attestation.exceptions + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler + + +@ControllerAdvice +class GlobalControllerAdvice { + @ExceptionHandler(WalletNotFoundException::class) + fun handleWalletNotFound(ex: WalletNotFoundException): ResponseEntity = + ResponseEntity(ex.message, HttpStatus.NOT_FOUND) +} \ No newline at end of file diff --git a/src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt b/src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt new file mode 100644 index 0000000..ec98f59 --- /dev/null +++ b/src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt @@ -0,0 +1,3 @@ +package wallet_server.attestation.exceptions + +class WalletNotFoundException(message: String): RuntimeException(message) \ No newline at end of file From 42668cedbd2371ef8d928583b535833ccaf18921 Mon Sep 17 00:00:00 2001 From: Tim Schauder Date: Fri, 21 Jun 2024 17:27:52 +0200 Subject: [PATCH 04/10] Change signature verification of popNonce to correct structure --- .../attestation/controllers/WalletApi.kt | 20 +++++++--- .../attestation/services/WalletApiService.kt | 38 ++++++++++--------- .../exceptions/PopVerificationException.kt | 3 ++ .../exceptions/WalletNotFoundException.kt | 2 +- 4 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 src/main/kotlin/wallet_server/attestation/exceptions/PopVerificationException.kt diff --git a/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt b/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt index a791c0c..e73bb6a 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse -import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.http.MediaType import org.springframework.web.bind.annotation.* import software.tice.wallet.attestation.requests.AttestationRequest @@ -25,14 +24,23 @@ class WalletApi(val walletApiService: WalletApiService) { } @PostMapping("/{walletId}/attestation") - @Operation(summary = "Request attestation for a wallet", + @Operation( + summary = "Request attestation for a wallet", description = "Submits an attestation request for a given wallet ID.", responses = [ ApiResponse(description = "The wallet attestation certificate", responseCode = "200"), - ApiResponse(description = "Wallet not found", responseCode = "404", content = [Content(mediaType = MediaType.TEXT_PLAIN_VALUE, - schema = Schema(implementation = String::class))]) - ]) - fun requestAttestation(@RequestBody request: AttestationRequest, @PathVariable walletId: String): AttestationResponse { + ApiResponse( + description = "Wallet not found", responseCode = "404", content = [Content( + mediaType = MediaType.TEXT_PLAIN_VALUE, + schema = Schema(implementation = String::class) + )] + ) + ] + ) + fun requestAttestation( + @RequestBody request: AttestationRequest, + @PathVariable walletId: String + ): AttestationResponse { return walletApiService.requestAttestation(request, walletId) } } \ No newline at end of file diff --git a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt index 2c053bc..67b8024 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt @@ -1,5 +1,9 @@ package software.tice.wallet.attestation.services +import io.github.cdimascio.dotenv.Dotenv +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jws +import io.jsonwebtoken.JwtException import io.jsonwebtoken.Jwts import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value @@ -12,11 +16,11 @@ import software.tice.wallet.attestation.responses.NonceResponse import java.security.KeyFactory import java.security.PrivateKey import java.security.PublicKey -import java.security.Signature import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec import java.util.* + @Service class WalletApiService @Autowired constructor( @Value("\${private.key}") @@ -47,7 +51,7 @@ class WalletApiService @Autowired constructor( ) walletRepository.save(newWallet) } - return NonceResponse(popNonce = popNonce, keyAttestationNonce = keyAttestationNonce ) + return NonceResponse(popNonce = popNonce, keyAttestationNonce = keyAttestationNonce) } fun requestAttestation(requestAttestation: AttestationRequest, id: String): AttestationResponse { @@ -57,31 +61,29 @@ class WalletApiService @Autowired constructor( .replace("-----END PRIVATE KEY-----", "") val existingWallet = walletRepository.findByWalletId(walletId) - ?: throw WalletNotFoundException("Wallet with id ${walletId} not found") + ?: throw WalletNotFoundException("Wallet with id $walletId not found") // <--- Start: check the PoP ---> val publicKey: PublicKey = decodePublicKey(requestAttestation.attestationPublicKey) - val parts = requestAttestation.proofOfPossession.split(":") - if (parts.size != 2) { - throw IllegalArgumentException("Expected format 'nonce:signature'") - } - val (nonce, signatureBytes) = parts.map { Base64.getDecoder().decode(it) } - val signature = Signature.getInstance("SHA256withECDSA") - signature.initVerify(publicKey) - signature.update(nonce) - val isSignatureValid = signature.verify(signatureBytes) - if (!isSignatureValid) { - throw SecurityException("Invalid signature") + try { + val nonce: String? = Jwts.parser().verifyWith(publicKey).build() + .parseSignedClaims(requestAttestation.proofOfPossession).payload["nonce"] as? String + + if (nonce != existingWallet.popNonce) { + throw PopVerificationException("Nonce mismatch") + } + } catch (e: JwtException) { + throw PopVerificationException("Signature invalid") } + // <--- End: check the PoP ---> // <--- End: check the request ---> - // <--- Start: throw away nonces and create random ID ---> existingWallet.popNonce = null existingWallet.keyAttestationNonce = null @@ -94,7 +96,9 @@ class WalletApiService @Autowired constructor( // <--- Start: create walletAttestation ---> val privateKey = privateKey?.let { decodePrivateKey(it) } - val walletAttestation: String = Jwts.builder().subject("Joe").claim("publicKey", requestAttestation.attestationPublicKey).claim("randomId", randomId).signWith(privateKey).compact() + val walletAttestation: String = + Jwts.builder().subject("Joe").claim("publicKey", requestAttestation.attestationPublicKey) + .claim("randomId", randomId).signWith(privateKey).compact() // <--- End: create walletAttestation ---> return AttestationResponse(walletAttestation) @@ -117,6 +121,6 @@ class WalletApiService @Autowired constructor( val keyBytes = Base64.getDecoder().decode(pem) val keySpec = PKCS8EncodedKeySpec(keyBytes) val keyFactory = KeyFactory.getInstance("EC") - return keyFactory.generatePrivate(keySpec) + return keyFactory.generatePrivate(keySpec) } } \ No newline at end of file diff --git a/src/main/kotlin/wallet_server/attestation/exceptions/PopVerificationException.kt b/src/main/kotlin/wallet_server/attestation/exceptions/PopVerificationException.kt new file mode 100644 index 0000000..5253ea1 --- /dev/null +++ b/src/main/kotlin/wallet_server/attestation/exceptions/PopVerificationException.kt @@ -0,0 +1,3 @@ +package wallet_server.attestation.exceptions + +class PopVerificationException(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt b/src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt index ec98f59..366be95 100644 --- a/src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt +++ b/src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt @@ -1,3 +1,3 @@ package wallet_server.attestation.exceptions -class WalletNotFoundException(message: String): RuntimeException(message) \ No newline at end of file +class WalletNotFoundException(message: String) : RuntimeException(message) \ No newline at end of file From 8bbbd058a74412a60e7fc5562301da0592696169 Mon Sep 17 00:00:00 2001 From: Tim Schauder Date: Wed, 26 Jun 2024 16:54:26 +0200 Subject: [PATCH 05/10] Fixes from the rebase --- .../attestation/repositories/WalletEntity.kt | 1 + .../attestation/services/WalletApiService.kt | 30 ++++++------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt b/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt index b98b133..4e5726f 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt @@ -1,6 +1,7 @@ package software.tice.wallet.attestation.repositories import jakarta.persistence.* +import java.util.* @Entity(name = "users") diff --git a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt index 67b8024..7c04d18 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt @@ -1,8 +1,5 @@ package software.tice.wallet.attestation.services -import io.github.cdimascio.dotenv.Dotenv -import io.jsonwebtoken.Claims -import io.jsonwebtoken.Jws import io.jsonwebtoken.JwtException import io.jsonwebtoken.Jwts import org.springframework.beans.factory.annotation.Autowired @@ -13,6 +10,8 @@ import software.tice.wallet.attestation.repositories.WalletRepository import software.tice.wallet.attestation.requests.AttestationRequest import software.tice.wallet.attestation.responses.AttestationResponse import software.tice.wallet.attestation.responses.NonceResponse +import wallet_server.attestation.exceptions.PopVerificationException +import wallet_server.attestation.exceptions.WalletNotFoundException import java.security.KeyFactory import java.security.PrivateKey import java.security.PublicKey @@ -25,18 +24,13 @@ import java.util.* class WalletApiService @Autowired constructor( @Value("\${private.key}") private val privateKey: String, - private val userRepository: WalletRepository, + private val walletRepository: WalletRepository, -) { - fun requestNonces(walletInstanceId: String): NonceResponse { + ) { + fun requestNonces(walletId: String): NonceResponse { val (popNonce, keyAttestationNonce) = List(2) { UUID.randomUUID().toString() } - val user = WalletEntity( - walletId = walletInstanceId, - popNonce = popNonce, - keyAttestationNonce = keyAttestationNonce, - id = null - ) + val existingWallet = walletRepository.findByWalletId(walletId) if (existingWallet != null) { existingWallet.popNonce = popNonce @@ -55,18 +49,12 @@ class WalletApiService @Autowired constructor( } fun requestAttestation(requestAttestation: AttestationRequest, id: String): AttestationResponse { - val privateKey = privateKey - val pem = privateKey - .replace("-----BEGIN PRIVATE KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - - val existingWallet = walletRepository.findByWalletId(walletId) - ?: throw WalletNotFoundException("Wallet with id $walletId not found") + val existingWallet = walletRepository.findByWalletId(id) + ?: throw WalletNotFoundException("Wallet with id $id not found") // <--- Start: check the PoP ---> val publicKey: PublicKey = decodePublicKey(requestAttestation.attestationPublicKey) - try { val nonce: String? = Jwts.parser().verifyWith(publicKey).build() .parseSignedClaims(requestAttestation.proofOfPossession).payload["nonce"] as? String @@ -94,7 +82,7 @@ class WalletApiService @Autowired constructor( // <--- Start: create walletAttestation ---> - val privateKey = privateKey?.let { decodePrivateKey(it) } + val privateKey = decodePrivateKey(privateKey) val walletAttestation: String = Jwts.builder().subject("Joe").claim("publicKey", requestAttestation.attestationPublicKey) From ecbc7b9124ecd151f54feadb52ab5ea989eaff8e Mon Sep 17 00:00:00 2001 From: Tim Schauder Date: Wed, 26 Jun 2024 16:54:47 +0200 Subject: [PATCH 06/10] Add tests for request attestation --- .../services/WalletApiServiceTests.kt | 149 ++++++++++++++---- 1 file changed, 118 insertions(+), 31 deletions(-) diff --git a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt index 9364fe2..27ac2ce 100644 --- a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt +++ b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt @@ -2,63 +2,150 @@ package software.tice.wallet.attestation.services import io.jsonwebtoken.Jwts import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.mockito.* import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import software.tice.wallet.attestation.repositories.WalletEntity import software.tice.wallet.attestation.repositories.WalletRepository import software.tice.wallet.attestation.requests.AttestationRequest +import wallet_server.attestation.exceptions.PopVerificationException +import wallet_server.attestation.exceptions.WalletNotFoundException import java.security.KeyPair +import java.security.PrivateKey import java.util.* +import java.util.UUID.randomUUID import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.random.Random internal class WalletApiServiceTests { - @Mock - private lateinit var userRepository: WalletRepository - - private lateinit var privateKey: String + private lateinit var walletRepository: WalletRepository private lateinit var walletApiService: WalletApiService - @Captor - private lateinit var userCaptor: ArgumentCaptor + private lateinit var walletId: String + + private lateinit var privateKey: PrivateKey private val keyPair: KeyPair = Jwts.SIG.ES256.keyPair().build() + private val internalWalletId: Long = Random.nextLong() + + @Captor + private lateinit var walletCaptor: ArgumentCaptor @BeforeEach fun setup() { MockitoAnnotations.openMocks(this) - privateKey = Base64.getEncoder().encodeToString(keyPair.private.encoded) - walletApiService = WalletApiService(privateKey, userRepository) - } - - @Test - fun `should return correct NonceResponse`() { - val walletInstanceId = "f74813c9-3435-4028-8e0c-018dd34d3b60" - - val response = walletApiService.requestNonces(walletInstanceId) + walletId = randomUUID().toString() + privateKey = keyPair.private - verify(userRepository).save(userCaptor.capture()) - val savedUser = userCaptor.value - assertEquals(walletInstanceId, savedUser.walletId) - assertEquals(response.popNonce, savedUser.popNonce) - assertEquals(response.keyAttestationNonce, savedUser.keyAttestationNonce) + walletApiService = WalletApiService(Base64.getEncoder().encodeToString(privateKey.encoded), walletRepository) } + @Nested + inner class RequestNoncesTests { + @Test + fun `should return correct NonceResponse and update existing user`() { + val existingWallet = WalletEntity(internalWalletId, walletId, "popNonce", "keyAttestation") + `when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet) + + val response = walletApiService.requestNonces(walletId) + + verify(walletRepository).save(walletCaptor.capture()) + val savedwallet = walletCaptor.value + assertEquals(internalWalletId, savedwallet.id) + assertEquals(walletId, savedwallet.walletId) + assertEquals(response.popNonce, savedwallet.popNonce) + assertEquals(response.keyAttestationNonce, savedwallet.keyAttestationNonce) + } + + @Test + fun `should return correct NonceResponse and add new user`() { + `when`(walletRepository.findByWalletId(walletId)).thenReturn(null) + + val response = walletApiService.requestNonces(walletId) + + verify(walletRepository).save(walletCaptor.capture()) + val newWallet = walletCaptor.value + assertNull(newWallet.id) + assertEquals(walletId, newWallet.walletId) + assertEquals(response.popNonce, newWallet.popNonce) + assertEquals(response.keyAttestationNonce, newWallet.keyAttestationNonce) + + } + } - @Test - fun `should return correct wallet attestation`() { - val request = AttestationRequest("PUBLIC_KEY","POP","KEY_ATTESTATION", "APP_ATTESTATION") - val walletInstanceId = "f74813c9-3435-4028-8e0c-018dd34d3b60" - - val response = walletApiService.requestAttestation(request, walletInstanceId) - - val parser = Jwts.parser() - .verifyWith(keyPair.public) - .build() - assertEquals(parser.parseSignedClaims(response.walletAttestation).payload.subject, "Joe") + @Nested + inner class RequestAttestationTests { + @Test + fun `should return correct wallet attestation`() { + val publicKey = Base64.getEncoder().encodeToString(keyPair.public.encoded) + val popNonce = randomUUID().toString() + val mockPop = Jwts.builder().claim("nonce", popNonce).signWith(privateKey).compact() + val existingWallet = WalletEntity(internalWalletId, walletId, popNonce, "keyAttestation") + `when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet) + val request = AttestationRequest(publicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION") + + val response = walletApiService.requestAttestation(request, walletId) + + verify(walletRepository).save(walletCaptor.capture()) + val newWallet = walletCaptor.value + val claims = Jwts.parser().verifyWith(keyPair.public).build().parseSignedClaims(response.walletAttestation) + assertEquals(claims.payload.subject, "Joe") + assertEquals(claims.payload["publicKey"], publicKey) + assertNull(newWallet.popNonce) + assertNull(newWallet.keyAttestationNonce) + } + + @Test + fun `should throw WalletNotFoundException if wallet can not be found`() { + val publicKey = Base64.getEncoder().encodeToString(keyPair.public.encoded) + val request = AttestationRequest(publicKey, "POP", "KEY_ATTESTATION", "APP_ATTESTATION") + `when`(walletRepository.findByWalletId(walletId)).thenReturn(null) + + val exception = assertThrows { + walletApiService.requestAttestation(request, walletId) + } + assertEquals("Wallet with id $walletId not found", exception.message) + } + + @Test + fun `should throw PopVerificationException if nonce does not match`() { + val publicKey = Base64.getEncoder().encodeToString(keyPair.public.encoded) + val popNonceOne = randomUUID().toString() + val popNonceTwo = randomUUID().toString() + val mockPop = Jwts.builder().claim("nonce", popNonceOne).signWith(privateKey).compact() + val existingWallet = WalletEntity(internalWalletId, walletId, popNonceTwo, "keyAttestation") + `when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet) + + val request = AttestationRequest(publicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION") + + val exception = assertThrows { + walletApiService.requestAttestation(request, walletId) + } + assertEquals("Nonce mismatch", exception.message) + } + + @Test + fun `should throw PopVerificationException if signature is invalid`() { + val publicKey = Base64.getEncoder().encodeToString(keyPair.public.encoded) + val popNonce = randomUUID().toString() + val maliciousPrivateKey = Jwts.SIG.ES256.keyPair().build().private + val mockPop = Jwts.builder().claim("nonce", popNonce).signWith(maliciousPrivateKey).compact() + val existingWallet = WalletEntity(internalWalletId, walletId, popNonce, "keyAttestation") + `when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet) + val request = AttestationRequest(publicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION") + + val exception = assertThrows { + walletApiService.requestAttestation(request, walletId) + } + assertEquals("Signature invalid", exception.message) + } } } From adcde97cd034836aeb51634bde2d664467b91630 Mon Sep 17 00:00:00 2001 From: Tim Schauder Date: Thu, 27 Jun 2024 10:29:45 +0200 Subject: [PATCH 07/10] Restore exceptions class --- .../tice/wallet}/attestation/exceptions/ControllerAdvice.kt | 6 +++++- .../attestation/exceptions/DecodingFailedException.kt | 3 +++ .../attestation/exceptions/PopVerificationException.kt | 2 +- .../attestation/exceptions/WalletNotFoundException.kt | 2 +- .../wallet/attestation/services/WalletApiServiceTests.kt | 4 ++-- 5 files changed, 12 insertions(+), 5 deletions(-) rename src/main/kotlin/{wallet_server => software/tice/wallet}/attestation/exceptions/ControllerAdvice.kt (64%) create mode 100644 src/main/kotlin/software/tice/wallet/attestation/exceptions/DecodingFailedException.kt rename src/main/kotlin/{wallet_server => software/tice/wallet}/attestation/exceptions/PopVerificationException.kt (59%) rename src/main/kotlin/{wallet_server => software/tice/wallet}/attestation/exceptions/WalletNotFoundException.kt (59%) diff --git a/src/main/kotlin/wallet_server/attestation/exceptions/ControllerAdvice.kt b/src/main/kotlin/software/tice/wallet/attestation/exceptions/ControllerAdvice.kt similarity index 64% rename from src/main/kotlin/wallet_server/attestation/exceptions/ControllerAdvice.kt rename to src/main/kotlin/software/tice/wallet/attestation/exceptions/ControllerAdvice.kt index 184d645..bfce8ac 100644 --- a/src/main/kotlin/wallet_server/attestation/exceptions/ControllerAdvice.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/exceptions/ControllerAdvice.kt @@ -1,4 +1,4 @@ -package wallet_server.attestation.exceptions +package software.tice.wallet.attestation.exceptions import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -11,4 +11,8 @@ class GlobalControllerAdvice { @ExceptionHandler(WalletNotFoundException::class) fun handleWalletNotFound(ex: WalletNotFoundException): ResponseEntity = ResponseEntity(ex.message, HttpStatus.NOT_FOUND) + + @ExceptionHandler(PopVerificationException::class) + fun handlePopVerificationFailed(ex: PopVerificationException): ResponseEntity = + ResponseEntity(ex.message, HttpStatus.UNAUTHORIZED) } \ No newline at end of file diff --git a/src/main/kotlin/software/tice/wallet/attestation/exceptions/DecodingFailedException.kt b/src/main/kotlin/software/tice/wallet/attestation/exceptions/DecodingFailedException.kt new file mode 100644 index 0000000..b4011f2 --- /dev/null +++ b/src/main/kotlin/software/tice/wallet/attestation/exceptions/DecodingFailedException.kt @@ -0,0 +1,3 @@ +package software.tice.wallet.attestation.exceptions + +class DecodingFailedException(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/src/main/kotlin/wallet_server/attestation/exceptions/PopVerificationException.kt b/src/main/kotlin/software/tice/wallet/attestation/exceptions/PopVerificationException.kt similarity index 59% rename from src/main/kotlin/wallet_server/attestation/exceptions/PopVerificationException.kt rename to src/main/kotlin/software/tice/wallet/attestation/exceptions/PopVerificationException.kt index 5253ea1..afdd150 100644 --- a/src/main/kotlin/wallet_server/attestation/exceptions/PopVerificationException.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/exceptions/PopVerificationException.kt @@ -1,3 +1,3 @@ -package wallet_server.attestation.exceptions +package software.tice.wallet.attestation.exceptions class PopVerificationException(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt b/src/main/kotlin/software/tice/wallet/attestation/exceptions/WalletNotFoundException.kt similarity index 59% rename from src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt rename to src/main/kotlin/software/tice/wallet/attestation/exceptions/WalletNotFoundException.kt index 366be95..d2169ae 100644 --- a/src/main/kotlin/wallet_server/attestation/exceptions/WalletNotFoundException.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/exceptions/WalletNotFoundException.kt @@ -1,3 +1,3 @@ -package wallet_server.attestation.exceptions +package software.tice.wallet.attestation.exceptions class WalletNotFoundException(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt index 27ac2ce..4936d89 100644 --- a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt +++ b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt @@ -11,8 +11,8 @@ import org.mockito.Mockito.`when` import software.tice.wallet.attestation.repositories.WalletEntity import software.tice.wallet.attestation.repositories.WalletRepository import software.tice.wallet.attestation.requests.AttestationRequest -import wallet_server.attestation.exceptions.PopVerificationException -import wallet_server.attestation.exceptions.WalletNotFoundException +import software.tice.wallet.attestation.exceptions.PopVerificationException +import software.tice.wallet.attestation.exceptions.WalletNotFoundException import java.security.KeyPair import java.security.PrivateKey import java.util.* From f6c699581df11860d1c8756d04f0aa47276ad8d8 Mon Sep 17 00:00:00 2001 From: Tim Schauder Date: Thu, 27 Jun 2024 10:30:34 +0200 Subject: [PATCH 08/10] Add key configuration class for private key --- .../wallet/attestation/KeyConfiguration.kt | 28 +++++++++++++++++++ .../attestation/services/WalletApiService.kt | 23 ++++----------- .../services/WalletApiServiceTests.kt | 2 +- 3 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/software/tice/wallet/attestation/KeyConfiguration.kt diff --git a/src/main/kotlin/software/tice/wallet/attestation/KeyConfiguration.kt b/src/main/kotlin/software/tice/wallet/attestation/KeyConfiguration.kt new file mode 100644 index 0000000..9286727 --- /dev/null +++ b/src/main/kotlin/software/tice/wallet/attestation/KeyConfiguration.kt @@ -0,0 +1,28 @@ +package software.tice.wallet.attestation + +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.spec.PKCS8EncodedKeySpec +import java.util.* + +@Configuration +class KeyConfiguration { + + @Value("\${private.key}") + private lateinit var privateKey: String + + @Bean + fun decodePrivateKey(): PrivateKey { + val pem = privateKey + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + val keyBytes = Base64.getDecoder().decode(pem) + val keySpec = PKCS8EncodedKeySpec(keyBytes) + val keyFactory = KeyFactory.getInstance("EC") + return keyFactory.generatePrivate(keySpec) + } +} \ No newline at end of file diff --git a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt index 7c04d18..0f696fa 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt @@ -3,27 +3,25 @@ package software.tice.wallet.attestation.services import io.jsonwebtoken.JwtException import io.jsonwebtoken.Jwts import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service +import software.tice.wallet.attestation.exceptions.DecodingFailedException import software.tice.wallet.attestation.repositories.WalletEntity import software.tice.wallet.attestation.repositories.WalletRepository import software.tice.wallet.attestation.requests.AttestationRequest import software.tice.wallet.attestation.responses.AttestationResponse import software.tice.wallet.attestation.responses.NonceResponse -import wallet_server.attestation.exceptions.PopVerificationException -import wallet_server.attestation.exceptions.WalletNotFoundException +import software.tice.wallet.attestation.exceptions.PopVerificationException +import software.tice.wallet.attestation.exceptions.WalletNotFoundException import java.security.KeyFactory import java.security.PrivateKey import java.security.PublicKey -import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec import java.util.* @Service class WalletApiService @Autowired constructor( - @Value("\${private.key}") - private val privateKey: String, + private val privateKey: PrivateKey, private val walletRepository: WalletRepository, ) { @@ -82,8 +80,6 @@ class WalletApiService @Autowired constructor( // <--- Start: create walletAttestation ---> - val privateKey = decodePrivateKey(privateKey) - val walletAttestation: String = Jwts.builder().subject("Joe").claim("publicKey", requestAttestation.attestationPublicKey) .claim("randomId", randomId).signWith(privateKey).compact() @@ -93,6 +89,7 @@ class WalletApiService @Autowired constructor( } fun decodePublicKey(key: String): PublicKey { + val pem = key .replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", "") @@ -102,13 +99,5 @@ class WalletApiService @Autowired constructor( return keyFactory.generatePublic(keySpec) } - fun decodePrivateKey(key: String): PrivateKey { - val pem = key - .replace("-----BEGIN PRIVATE KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - val keyBytes = Base64.getDecoder().decode(pem) - val keySpec = PKCS8EncodedKeySpec(keyBytes) - val keyFactory = KeyFactory.getInstance("EC") - return keyFactory.generatePrivate(keySpec) - } + } \ No newline at end of file diff --git a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt index 4936d89..25e057a 100644 --- a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt +++ b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt @@ -45,7 +45,7 @@ internal class WalletApiServiceTests { walletId = randomUUID().toString() privateKey = keyPair.private - walletApiService = WalletApiService(Base64.getEncoder().encodeToString(privateKey.encoded), walletRepository) + walletApiService = WalletApiService(privateKey, walletRepository) } @Nested From c3e1443db772a719104d227f0803a6909acbf45e Mon Sep 17 00:00:00 2001 From: Tim Schauder Date: Thu, 27 Jun 2024 11:14:47 +0200 Subject: [PATCH 09/10] Add decoding failed exception and test --- .../attestation/controllers/WalletApi.kt | 30 +++++++-------- .../exceptions/ControllerAdvice.kt | 4 ++ .../attestation/services/WalletApiService.kt | 37 +++++++------------ .../services/WalletApiServiceTests.kt | 18 +++++++++ 4 files changed, 51 insertions(+), 38 deletions(-) diff --git a/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt b/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt index e73bb6a..83e98ba 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/controllers/WalletApi.kt @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses import org.springframework.http.MediaType import org.springframework.web.bind.annotation.* import software.tice.wallet.attestation.requests.AttestationRequest @@ -18,28 +19,27 @@ import software.tice.wallet.attestation.services.WalletApiService class WalletApi(val walletApiService: WalletApiService) { - @PostMapping() + @PostMapping fun requestNonces(@RequestBody request: NonceRequest): NonceResponse { return walletApiService.requestNonces(request.walletId) } @PostMapping("/{walletId}/attestation") - @Operation( - summary = "Request attestation for a wallet", - description = "Submits an attestation request for a given wallet ID.", - responses = [ - ApiResponse(description = "The wallet attestation certificate", responseCode = "200"), - ApiResponse( - description = "Wallet not found", responseCode = "404", content = [Content( - mediaType = MediaType.TEXT_PLAIN_VALUE, - schema = Schema(implementation = String::class) - )] - ) - ] + @Operation(summary = "Submits an attestation request for a given wallet ID.") + + @ApiResponse(description = "Wallet attestation issued", responseCode = "200") + @ApiResponse( + description = "Wallet not found", responseCode = "404", content = [Content( + mediaType = MediaType.TEXT_PLAIN_VALUE, schema = Schema(implementation = String::class) + )] + ) + @ApiResponse( + description = "Public Key wrong", responseCode = "400", content = [Content( + mediaType = MediaType.TEXT_PLAIN_VALUE, schema = Schema(implementation = String::class) + )] ) fun requestAttestation( - @RequestBody request: AttestationRequest, - @PathVariable walletId: String + @RequestBody request: AttestationRequest, @PathVariable walletId: String ): AttestationResponse { return walletApiService.requestAttestation(request, walletId) } diff --git a/src/main/kotlin/software/tice/wallet/attestation/exceptions/ControllerAdvice.kt b/src/main/kotlin/software/tice/wallet/attestation/exceptions/ControllerAdvice.kt index bfce8ac..bcac4f1 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/exceptions/ControllerAdvice.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/exceptions/ControllerAdvice.kt @@ -15,4 +15,8 @@ class GlobalControllerAdvice { @ExceptionHandler(PopVerificationException::class) fun handlePopVerificationFailed(ex: PopVerificationException): ResponseEntity = ResponseEntity(ex.message, HttpStatus.UNAUTHORIZED) + + @ExceptionHandler(DecodingFailedException::class) + fun handleDecodingFailedException(ex: DecodingFailedException): ResponseEntity = + ResponseEntity(ex.message, HttpStatus.BAD_REQUEST) } \ No newline at end of file diff --git a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt index 0f696fa..3afd7cd 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt @@ -24,6 +24,7 @@ class WalletApiService @Autowired constructor( private val privateKey: PrivateKey, private val walletRepository: WalletRepository, + ) { fun requestNonces(walletId: String): NonceResponse { val (popNonce, keyAttestationNonce) = List(2) { UUID.randomUUID().toString() } @@ -36,10 +37,7 @@ class WalletApiService @Autowired constructor( walletRepository.save(existingWallet) } else { val newWallet = WalletEntity( - walletId = walletId, - popNonce = popNonce, - keyAttestationNonce = keyAttestationNonce, - id = null + walletId = walletId, popNonce = popNonce, keyAttestationNonce = keyAttestationNonce, id = null ) walletRepository.save(newWallet) } @@ -47,11 +45,15 @@ class WalletApiService @Autowired constructor( } fun requestAttestation(requestAttestation: AttestationRequest, id: String): AttestationResponse { - val existingWallet = walletRepository.findByWalletId(id) - ?: throw WalletNotFoundException("Wallet with id $id not found") + val existingWallet = + walletRepository.findByWalletId(id) ?: throw WalletNotFoundException("Wallet with id $id not found") // <--- Start: check the PoP ---> - val publicKey: PublicKey = decodePublicKey(requestAttestation.attestationPublicKey) + val publicKey: PublicKey = try { + decodePublicKey(requestAttestation.attestationPublicKey) + } catch (e: Exception) { + throw DecodingFailedException("Public Key could not be decoded") + } try { val nonce: String? = Jwts.parser().verifyWith(publicKey).build() @@ -63,13 +65,8 @@ class WalletApiService @Autowired constructor( } catch (e: JwtException) { throw PopVerificationException("Signature invalid") } - - // <--- End: check the PoP ---> - // <--- End: check the request ---> - - // <--- Start: throw away nonces and create random ID ---> existingWallet.popNonce = null existingWallet.keyAttestationNonce = null @@ -78,7 +75,6 @@ class WalletApiService @Autowired constructor( val randomId: String = UUID.randomUUID().toString() // <--- End: throw away nonces and create random ID ---> - // <--- Start: create walletAttestation ---> val walletAttestation: String = Jwts.builder().subject("Joe").claim("publicKey", requestAttestation.attestationPublicKey) @@ -89,15 +85,10 @@ class WalletApiService @Autowired constructor( } fun decodePublicKey(key: String): PublicKey { - - val pem = key - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PUBLIC KEY-----", "") - val keyBytes = Base64.getDecoder().decode(pem) - val keySpec = X509EncodedKeySpec(keyBytes) - val keyFactory = KeyFactory.getInstance("EC") - return keyFactory.generatePublic(keySpec) + val pem = key.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "") + val keyBytes = Base64.getDecoder().decode(pem) + val keySpec = X509EncodedKeySpec(keyBytes) + val keyFactory = KeyFactory.getInstance("EC") + return keyFactory.generatePublic(keySpec) } - - } \ No newline at end of file diff --git a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt index 25e057a..1556c5f 100644 --- a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt +++ b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt @@ -8,6 +8,7 @@ import org.junit.jupiter.api.assertThrows import org.mockito.* import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import software.tice.wallet.attestation.exceptions.DecodingFailedException import software.tice.wallet.attestation.repositories.WalletEntity import software.tice.wallet.attestation.repositories.WalletRepository import software.tice.wallet.attestation.requests.AttestationRequest @@ -115,6 +116,23 @@ internal class WalletApiServiceTests { assertEquals("Wallet with id $walletId not found", exception.message) } + @Test + fun `should throw DecodingFailedException if public key can not be decoded`() { + val publicKey = Base64.getEncoder().encodeToString(keyPair.public.encoded) + val corruptedPublicKey = "$publicKey???" + val popNonce = randomUUID().toString() + val mockPop = Jwts.builder().claim("nonce", popNonce).signWith(privateKey).compact() + val existingWallet = WalletEntity(internalWalletId, walletId, popNonce, "keyAttestation") + `when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet) + val request = AttestationRequest(corruptedPublicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION") + + val exception = assertThrows { + walletApiService.requestAttestation(request, walletId) + } + assertEquals("Public Key could not be decoded", exception.message) + } + + @Test fun `should throw PopVerificationException if nonce does not match`() { val publicKey = Base64.getEncoder().encodeToString(keyPair.public.encoded) From b88a2a90d85fc25723d76eaf0764079fa5609ab1 Mon Sep 17 00:00:00 2001 From: Tim Schauder Date: Thu, 27 Jun 2024 11:52:33 +0200 Subject: [PATCH 10/10] Add randomId persistence --- .../wallet/attestation/repositories/WalletEntity.kt | 3 ++- .../wallet/attestation/services/WalletApiService.kt | 8 ++++++-- .../attestation/services/WalletApiServiceTests.kt | 12 +++++++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt b/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt index 4e5726f..8ed4a49 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/repositories/WalletEntity.kt @@ -11,5 +11,6 @@ data class WalletEntity( var id: Long?, var walletId: String, var popNonce: String?, - var keyAttestationNonce: String? + var keyAttestationNonce: String?, + var randomId: String? ) \ No newline at end of file diff --git a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt index 3afd7cd..d858fd4 100644 --- a/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt +++ b/src/main/kotlin/software/tice/wallet/attestation/services/WalletApiService.kt @@ -4,6 +4,7 @@ import io.jsonwebtoken.JwtException import io.jsonwebtoken.Jwts import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import software.tice.wallet.attestation.exceptions.DecodingFailedException import software.tice.wallet.attestation.repositories.WalletEntity import software.tice.wallet.attestation.repositories.WalletRepository @@ -37,7 +38,7 @@ class WalletApiService @Autowired constructor( walletRepository.save(existingWallet) } else { val newWallet = WalletEntity( - walletId = walletId, popNonce = popNonce, keyAttestationNonce = keyAttestationNonce, id = null + walletId = walletId, popNonce = popNonce, keyAttestationNonce = keyAttestationNonce, id = null, randomId = null ) walletRepository.save(newWallet) } @@ -68,11 +69,14 @@ class WalletApiService @Autowired constructor( // <--- End: check the PoP ---> // <--- Start: throw away nonces and create random ID ---> + val randomId: String = UUID.randomUUID().toString() + + existingWallet.randomId = randomId existingWallet.popNonce = null existingWallet.keyAttestationNonce = null + walletRepository.save(existingWallet) - val randomId: String = UUID.randomUUID().toString() // <--- End: throw away nonces and create random ID ---> // <--- Start: create walletAttestation ---> diff --git a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt index 1556c5f..bc07ab0 100644 --- a/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt +++ b/src/test/kotlin/software/tice/wallet/attestation/services/WalletApiServiceTests.kt @@ -21,6 +21,7 @@ import java.util.UUID.randomUUID import kotlin.test.assertEquals import kotlin.test.assertNull import kotlin.random.Random +import kotlin.test.assertNotNull internal class WalletApiServiceTests { @@ -53,7 +54,7 @@ internal class WalletApiServiceTests { inner class RequestNoncesTests { @Test fun `should return correct NonceResponse and update existing user`() { - val existingWallet = WalletEntity(internalWalletId, walletId, "popNonce", "keyAttestation") + val existingWallet = WalletEntity(internalWalletId, walletId, "popNonce", "keyAttestation", randomId = null) `when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet) val response = walletApiService.requestNonces(walletId) @@ -89,7 +90,7 @@ internal class WalletApiServiceTests { val publicKey = Base64.getEncoder().encodeToString(keyPair.public.encoded) val popNonce = randomUUID().toString() val mockPop = Jwts.builder().claim("nonce", popNonce).signWith(privateKey).compact() - val existingWallet = WalletEntity(internalWalletId, walletId, popNonce, "keyAttestation") + val existingWallet = WalletEntity(internalWalletId, walletId, popNonce, "keyAttestation", randomId = null) `when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet) val request = AttestationRequest(publicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION") @@ -100,6 +101,7 @@ internal class WalletApiServiceTests { val claims = Jwts.parser().verifyWith(keyPair.public).build().parseSignedClaims(response.walletAttestation) assertEquals(claims.payload.subject, "Joe") assertEquals(claims.payload["publicKey"], publicKey) + assertNotNull(newWallet.randomId) assertNull(newWallet.popNonce) assertNull(newWallet.keyAttestationNonce) } @@ -122,7 +124,7 @@ internal class WalletApiServiceTests { val corruptedPublicKey = "$publicKey???" val popNonce = randomUUID().toString() val mockPop = Jwts.builder().claim("nonce", popNonce).signWith(privateKey).compact() - val existingWallet = WalletEntity(internalWalletId, walletId, popNonce, "keyAttestation") + val existingWallet = WalletEntity(internalWalletId, walletId, popNonce, "keyAttestation", randomId = null) `when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet) val request = AttestationRequest(corruptedPublicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION") @@ -139,7 +141,7 @@ internal class WalletApiServiceTests { val popNonceOne = randomUUID().toString() val popNonceTwo = randomUUID().toString() val mockPop = Jwts.builder().claim("nonce", popNonceOne).signWith(privateKey).compact() - val existingWallet = WalletEntity(internalWalletId, walletId, popNonceTwo, "keyAttestation") + val existingWallet = WalletEntity(internalWalletId, walletId, popNonceTwo, "keyAttestation", randomId = null) `when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet) val request = AttestationRequest(publicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION") @@ -156,7 +158,7 @@ internal class WalletApiServiceTests { val popNonce = randomUUID().toString() val maliciousPrivateKey = Jwts.SIG.ES256.keyPair().build().private val mockPop = Jwts.builder().claim("nonce", popNonce).signWith(maliciousPrivateKey).compact() - val existingWallet = WalletEntity(internalWalletId, walletId, popNonce, "keyAttestation") + val existingWallet = WalletEntity(internalWalletId, walletId, popNonce, "keyAttestation", randomId = null) `when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet) val request = AttestationRequest(publicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION")