Skip to content

Enable linting and fix formatting #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 28, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -54,7 +54,7 @@ tasks.withType<Test> {

spotless {
kotlin {
// ktlint()
ktlint()
}
kotlinGradle {
ktlint()
Original file line number Diff line number Diff line change
@@ -7,5 +7,5 @@ import org.springframework.boot.runApplication
class WalletServerApplication

fun main(args: Array<String>) {
runApplication<WalletServerApplication>(*args)
runApplication<WalletServerApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
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.*
import java.util.Base64

@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 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)
}
}
}
Original file line number Diff line number Diff line change
@@ -2,15 +2,19 @@ package software.tice.wallet.attestation.controllers

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import software.tice.wallet.attestation.requests.ValidationRequest

@RestController
@RequestMapping("wallet")
class IssuerApi {

@PostMapping("/validation")
fun validateAttestation(@RequestBody request: ValidationRequest): ResponseEntity<Void> {
fun validateAttestation(
@RequestBody request: ValidationRequest,
): ResponseEntity<Void> {
return ResponseEntity.status(HttpStatus.OK).body(null)
}
}
}
Original file line number Diff line number Diff line change
@@ -4,43 +4,55 @@ 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 org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import software.tice.wallet.attestation.requests.AttestationRequest
import software.tice.wallet.attestation.requests.NonceRequest
import software.tice.wallet.attestation.responses.AttestationResponse
import software.tice.wallet.attestation.responses.NonceResponse
import software.tice.wallet.attestation.services.WalletApiService


@RestController
@RequestMapping("wallet")

class WalletApi(val walletApiService: WalletApiService) {

@PostMapping
fun requestNonces(@RequestBody request: NonceRequest): NonceResponse {
fun requestNonces(
@RequestBody request: NonceRequest,
): NonceResponse {
return walletApiService.requestNonces(request.walletId)
}

@PostMapping("/{walletId}/attestation")
@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)
)]
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)
)]
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package software.tice.wallet.attestation.exceptions

class DecodingFailedException(message: String) : RuntimeException(message)
class DecodingFailedException(message: String) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -5,12 +5,10 @@ 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<String> =
ResponseEntity(ex.message, HttpStatus.NOT_FOUND)
fun handleWalletNotFound(ex: WalletNotFoundException): ResponseEntity<String> = ResponseEntity(ex.message, HttpStatus.NOT_FOUND)

@ExceptionHandler(PopVerificationException::class)
fun handlePopVerificationFailed(ex: PopVerificationException): ResponseEntity<String> =
@@ -19,4 +17,4 @@ class GlobalControllerAdvice {
@ExceptionHandler(DecodingFailedException::class)
fun handleDecodingFailedException(ex: DecodingFailedException): ResponseEntity<String> =
ResponseEntity(ex.message, HttpStatus.BAD_REQUEST)
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package software.tice.wallet.attestation.exceptions

class PopVerificationException(message: String) : RuntimeException(message)
class PopVerificationException(message: String) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package software.tice.wallet.attestation.exceptions

class WalletNotFoundException(message: String) : RuntimeException(message)
class WalletNotFoundException(message: String) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package software.tice.wallet.attestation.repositories

import jakarta.persistence.*
import java.util.*

import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id

@Entity(name = "users")
data class WalletEntity(
@@ -12,5 +13,5 @@ data class WalletEntity(
var walletId: String,
var popNonce: String?,
var keyAttestationNonce: String?,
var randomId: String?
)
var randomId: String?,
)
Original file line number Diff line number Diff line change
@@ -3,9 +3,7 @@ package software.tice.wallet.attestation.repositories
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository


@Repository
interface WalletRepository : JpaRepository<WalletEntity, Long> {
fun findByWalletId(walletId: String): WalletEntity?
}

Original file line number Diff line number Diff line change
@@ -4,5 +4,5 @@ data class AttestationRequest(
val attestationPublicKey: String,
val proofOfPossession: String,
val keyAttestation: String,
val appAttestation: String
)
val appAttestation: String,
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package software.tice.wallet.attestation.requests

data class NonceRequest(val walletId: String)
data class NonceRequest(val walletId: String)
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package software.tice.wallet.attestation.requests

data class ValidationRequest(val walletAttestation: String)
data class ValidationRequest(val walletAttestation: String)
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package software.tice.wallet.attestation.responses

data class AttestationResponse(val walletAttestation: String)
data class AttestationResponse(val walletAttestation: String)
Original file line number Diff line number Diff line change
@@ -2,5 +2,5 @@ package software.tice.wallet.attestation.responses

data class NonceResponse(
val popNonce: String,
val keyAttestationNonce: String
)
val keyAttestationNonce: String,
)
Original file line number Diff line number Diff line change
@@ -4,95 +4,104 @@ 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.exceptions.PopVerificationException
import software.tice.wallet.attestation.exceptions.WalletNotFoundException
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 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.X509EncodedKeySpec
import java.util.*

import java.util.Base64
import java.util.UUID

@Service
class WalletApiService @Autowired constructor(
private val privateKey: PrivateKey,
private val walletRepository: WalletRepository,


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() }

val existingWallet = walletRepository.findByWalletId(walletId)

if (existingWallet != null) {
existingWallet.popNonce = popNonce
existingWallet.keyAttestationNonce = keyAttestationNonce
walletRepository.save(existingWallet)
} else {
val newWallet = WalletEntity(
walletId = walletId, popNonce = popNonce, keyAttestationNonce = keyAttestationNonce, id = null, randomId = null
)
walletRepository.save(newWallet)
}
return NonceResponse(popNonce = popNonce, keyAttestationNonce = keyAttestationNonce)
}

fun requestAttestation(requestAttestation: AttestationRequest, id: String): AttestationResponse {
val existingWallet =
walletRepository.findByWalletId(id) ?: throw WalletNotFoundException("Wallet with id $id not found")

// <--- Start: check the PoP --->
val publicKey: PublicKey = try {
decodePublicKey(requestAttestation.attestationPublicKey)
} catch (e: Exception) {
throw DecodingFailedException("Public Key could not be decoded")
fun requestNonces(walletId: String): NonceResponse {
val (popNonce, keyAttestationNonce) = List(2) { UUID.randomUUID().toString() }

val existingWallet = walletRepository.findByWalletId(walletId)

if (existingWallet != null) {
existingWallet.popNonce = popNonce
existingWallet.keyAttestationNonce = keyAttestationNonce
walletRepository.save(existingWallet)
} else {
val newWallet =
WalletEntity(
walletId = walletId,
popNonce = popNonce,
keyAttestationNonce = keyAttestationNonce,
id = null,
randomId = null,
)
walletRepository.save(newWallet)
}
return NonceResponse(popNonce = popNonce, keyAttestationNonce = keyAttestationNonce)
}

try {
val nonce: String? = Jwts.parser().verifyWith(publicKey).build()
.parseSignedClaims(requestAttestation.proofOfPossession).payload["nonce"] as? String

if (nonce != existingWallet.popNonce) {
throw PopVerificationException("Nonce mismatch")
fun requestAttestation(
requestAttestation: AttestationRequest,
id: String,
): AttestationResponse {
val existingWallet =
walletRepository.findByWalletId(id) ?: throw WalletNotFoundException("Wallet with id $id not found")

// <--- Start: check the PoP --->
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()
.parseSignedClaims(requestAttestation.proofOfPossession).payload["nonce"] as? String

if (nonce != existingWallet.popNonce) {
throw PopVerificationException("Nonce mismatch")
}
} catch (e: JwtException) {
throw PopVerificationException("Signature invalid")
}
} catch (e: JwtException) {
throw PopVerificationException("Signature invalid")
}
// <--- End: check the PoP --->
// <--- End: check the PoP --->

// <--- Start: throw away nonces and create random ID --->
val randomId: String = UUID.randomUUID().toString()
// <--- Start: throw away nonces and create random ID --->
val randomId: String = UUID.randomUUID().toString()

existingWallet.randomId = randomId
existingWallet.popNonce = null
existingWallet.keyAttestationNonce = null
existingWallet.randomId = randomId
existingWallet.popNonce = null
existingWallet.keyAttestationNonce = null

walletRepository.save(existingWallet)
walletRepository.save(existingWallet)

// <--- End: throw away nonces and create random ID --->
// <--- End: throw away nonces and create random ID --->

// <--- Start: create walletAttestation --->
val walletAttestation: String =
Jwts.builder().subject("Joe").claim("publicKey", requestAttestation.attestationPublicKey)
.claim("randomId", randomId).signWith(privateKey).compact()
// <--- End: create walletAttestation --->
// <--- Start: create walletAttestation --->
val walletAttestation: String =
Jwts.builder().subject("Joe").claim("publicKey", requestAttestation.attestationPublicKey)
.claim("randomId", randomId).signWith(privateKey).compact()
// <--- End: create walletAttestation --->

return AttestationResponse(walletAttestation)
}
return AttestationResponse(walletAttestation)
}

fun decodePublicKey(key: String): PublicKey {
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -5,24 +5,26 @@ 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.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import software.tice.wallet.attestation.exceptions.DecodingFailedException
import software.tice.wallet.attestation.exceptions.PopVerificationException
import software.tice.wallet.attestation.exceptions.WalletNotFoundException
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.exceptions.PopVerificationException
import software.tice.wallet.attestation.exceptions.WalletNotFoundException
import java.security.KeyPair
import java.security.PrivateKey
import java.util.*
import java.util.Base64
import java.util.UUID.randomUUID
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

import kotlin.test.assertNull

internal class WalletApiServiceTests {
@Mock
@@ -79,7 +81,6 @@ internal class WalletApiServiceTests {
assertEquals(walletId, newWallet.walletId)
assertEquals(response.popNonce, newWallet.popNonce)
assertEquals(response.keyAttestationNonce, newWallet.keyAttestationNonce)

}
}

@@ -112,9 +113,10 @@ internal class WalletApiServiceTests {
val request = AttestationRequest(publicKey, "POP", "KEY_ATTESTATION", "APP_ATTESTATION")
`when`(walletRepository.findByWalletId(walletId)).thenReturn(null)

val exception = assertThrows<WalletNotFoundException> {
walletApiService.requestAttestation(request, walletId)
}
val exception =
assertThrows<WalletNotFoundException> {
walletApiService.requestAttestation(request, walletId)
}
assertEquals("Wallet with id $walletId not found", exception.message)
}

@@ -128,13 +130,13 @@ internal class WalletApiServiceTests {
`when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet)
val request = AttestationRequest(corruptedPublicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION")

val exception = assertThrows<DecodingFailedException> {
walletApiService.requestAttestation(request, walletId)
}
val exception =
assertThrows<DecodingFailedException> {
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)
@@ -146,9 +148,10 @@ internal class WalletApiServiceTests {

val request = AttestationRequest(publicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION")

val exception = assertThrows<PopVerificationException> {
walletApiService.requestAttestation(request, walletId)
}
val exception =
assertThrows<PopVerificationException> {
walletApiService.requestAttestation(request, walletId)
}
assertEquals("Nonce mismatch", exception.message)
}

@@ -162,9 +165,10 @@ internal class WalletApiServiceTests {
`when`(walletRepository.findByWalletId(walletId)).thenReturn(existingWallet)
val request = AttestationRequest(publicKey, mockPop, "KEY_ATTESTATION", "APP_ATTESTATION")

val exception = assertThrows<PopVerificationException> {
walletApiService.requestAttestation(request, walletId)
}
val exception =
assertThrows<PopVerificationException> {
walletApiService.requestAttestation(request, walletId)
}
assertEquals("Signature invalid", exception.message)
}
}