Skip to content
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

Add PoP check + exception handling #4

Merged
merged 10 commits into from
Jun 28, 2024
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
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
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}")
@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)
)]
)
@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 id: String
@RequestBody request: AttestationRequest, @PathVariable walletId: String
): AttestationResponse {
return walletApiService.requestAttestation(request, id)
return walletApiService.requestAttestation(request, walletId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package software.tice.wallet.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<String> =
ResponseEntity(ex.message, HttpStatus.NOT_FOUND)

@ExceptionHandler(PopVerificationException::class)
fun handlePopVerificationFailed(ex: PopVerificationException): ResponseEntity<String> =
ResponseEntity(ex.message, HttpStatus.UNAUTHORIZED)

@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
@@ -0,0 +1,3 @@
package software.tice.wallet.attestation.exceptions

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

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

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

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


@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?
var keyAttestationNonce: String?,
var randomId: String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ import org.springframework.stereotype.Repository


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

Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package software.tice.wallet.attestation.requests

data class NonceRequest(val walletInstanceId: String)
data class NonceRequest(val walletId: String)
Original file line number Diff line number Diff line change
@@ -1,52 +1,98 @@
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.repositories.UserEntity
import software.tice.wallet.attestation.repositories.UserRepository
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
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.spec.PKCS8EncodedKeySpec
import java.security.PrivateKey
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec
import java.util.*


@Service
class WalletApiService @Autowired constructor(
@Value("\${private.key}")
private val privateKey: String,
private val userRepository: UserRepository,
private val privateKey: PrivateKey,
private val walletRepository: WalletRepository,


) {
fun requestNonces(walletInstanceId: String): NonceResponse {
) {
fun requestNonces(walletId: String): NonceResponse {
val (popNonce, keyAttestationNonce) = List(2) { UUID.randomUUID().toString() }

val user = UserEntity(
walletInstanceId = walletInstanceId,
popNonce = popNonce,
keyAttestationNonce = keyAttestationNonce,
id = null
)
val existingWallet = walletRepository.findByWalletId(walletId)

userRepository.save(user)
return NonceResponse(popNonce = popNonce, keyAttestationNonce = keyAttestationNonce )
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 privateKey = privateKey
val pem = privateKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
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")
}
// <--- End: check the PoP --->

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

val keySpec = PKCS8EncodedKeySpec(decodedKey)
val keyFactory = KeyFactory.getInstance("EC")
val privateKeyReloaded = keyFactory.generatePrivate(keySpec)
existingWallet.randomId = randomId
existingWallet.popNonce = null
existingWallet.keyAttestationNonce = null

walletRepository.save(existingWallet)

// <--- 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 --->

val walletAttestation: String = Jwts.builder().subject("Joe").signWith(privateKeyReloaded).compact()
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)
}
}
Loading