Skip to content

Commit

Permalink
Simplify ports and refactor.
Browse files Browse the repository at this point in the history
  • Loading branch information
dzarras committed Nov 15, 2024
1 parent 8ab4bac commit 844e14c
Show file tree
Hide file tree
Showing 17 changed files with 152 additions and 291 deletions.
21 changes: 11 additions & 10 deletions src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ import eu.europa.ec.eudi.pidissuer.adapter.input.web.WalletApi
import eu.europa.ec.eudi.pidissuer.adapter.input.web.security.*
import eu.europa.ec.eudi.pidissuer.adapter.out.IssuerSigningKey
import eu.europa.ec.eudi.pidissuer.adapter.out.credential.CredentialRequestFactory
import eu.europa.ec.eudi.pidissuer.adapter.out.credential.DefaultGenerateCNonce
import eu.europa.ec.eudi.pidissuer.adapter.out.credential.DecryptCNonceWithNimbusAndVerify
import eu.europa.ec.eudi.pidissuer.adapter.out.credential.DefaultResolveCredentialRequestByCredentialIdentifier
import eu.europa.ec.eudi.pidissuer.adapter.out.credential.GenerateCNonceAndEncryptWithNimbus
import eu.europa.ec.eudi.pidissuer.adapter.out.jose.*
import eu.europa.ec.eudi.pidissuer.adapter.out.jose.EncryptCNonceWithNimbus
import eu.europa.ec.eudi.pidissuer.adapter.out.mdl.*
import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryDeferredCredentialRepository
import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryIssuedCredentialRepository
Expand Down Expand Up @@ -361,9 +361,8 @@ fun beans(clock: Clock) = beans {
//
// CNonce
//
bean { DefaultGenerateCNonce(clock = clock, expiresIn = Duration.ofMinutes(5L)) }
bean { EncryptCNonceWithNimbus(issuerPublicUrl, ref<RSAKey>("cnonce-encryption-key")) }
bean { DecryptCNonceWithNimbus(issuerPublicUrl, ref<RSAKey>("cnonce-encryption-key")) }
bean { GenerateCNonceAndEncryptWithNimbus(clock, issuerPublicUrl, ref<RSAKey>("cnonce-encryption-key")) }
bean { DecryptCNonceWithNimbusAndVerify(issuerPublicUrl, ref<RSAKey>("cnonce-encryption-key")) }

//
// Credentials
Expand All @@ -388,7 +387,7 @@ fun beans(clock: Clock) = beans {
//
bean { ValidateJwtProof(issuerPublicUrl) }
bean { DefaultExtractJwkFromCredentialKey }
bean { ValidateProofs(ref(), clock, ref()) }
bean { ValidateProofs(ref(), ref(), ref()) }
bean {
CredentialIssuerMetaData(
id = issuerPublicUrl,
Expand All @@ -408,7 +407,6 @@ fun beans(clock: Clock) = beans {
clock = clock,
storeIssuedCredentials = ref(),
validateProofs = ref(),
decryptCNonce = ref(),
)
add(issueMsoMdocPid)
}
Expand Down Expand Up @@ -439,7 +437,6 @@ fun beans(clock: Clock) = beans {
generateNotificationId = ref(),
storeIssuedCredentials = ref(),
validateProofs = ref(),
decryptCNonce = ref(),
)

val deferred = env.getProperty<Boolean>("issuer.pid.sd_jwt_vc.deferred") ?: false
Expand All @@ -458,7 +455,6 @@ fun beans(clock: Clock) = beans {
clock = clock,
storeIssuedCredentials = ref(),
validateProofs = ref(),
decryptCNonce = ref(),
)
add(mdlIssuer)
}
Expand All @@ -480,7 +476,12 @@ fun beans(clock: Clock) = beans {
//
bean(::GetCredentialIssuerMetaData)
bean {
IssueCredential(ref(), ref(), ref(), ref(), ref())
IssueCredential(
credentialIssuerMetadata = ref(),
resolveCredentialRequestByCredentialIdentifier = ref(),
generateCNonce = ref(),
encryptCredentialResponse = ref(),
)
}
bean {
GetDeferredCredential(ref(), ref())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.europa.ec.eudi.pidissuer.adapter.out.jose
package eu.europa.ec.eudi.pidissuer.adapter.out.credential

import arrow.core.raise.result
import com.nimbusds.jose.EncryptionMethod
Expand All @@ -30,19 +30,18 @@ import com.nimbusds.jwt.EncryptedJWT
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier
import com.nimbusds.jwt.proc.DefaultJWTProcessor
import eu.europa.ec.eudi.pidissuer.domain.CNonce
import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerId
import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce
import eu.europa.ec.eudi.pidissuer.port.out.credential.VerifyCNonce
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.time.Duration
import java.time.Instant

/**
* Nimbus implementation of [DecryptCNonce] that decrypts an [EncryptedJWT] as a [CNonce].
* Decrypts an [EncryptedJWT] using Nimbus and verifies it's still active.
*/
internal class DecryptCNonceWithNimbus(
internal class DecryptCNonceWithNimbusAndVerify(
private val issuer: CredentialIssuerId,
private val decryptionKey: RSAKey,
) : DecryptCNonce {
) : VerifyCNonce {
init {
require(decryptionKey.isPrivate) { "a private key is required for decryption" }
}
Expand All @@ -65,17 +64,17 @@ internal class DecryptCNonceWithNimbus(
.issuer(issuer.externalForm)
.audience(issuer.externalForm)
.build(),
setOf("iss", "aud", "cnonce", "exi", "iat"),
setOf("iss", "aud", "cnonce", "iat", "exp"),
)
}

override suspend fun invoke(encrypted: String): Result<CNonce> = result {
val jwt = EncryptedJWT.parse(encrypted)
val claimSet = processor.process(jwt, null)
CNonce(
requireNotNull(claimSet.getStringClaim("cnonce")) { "missing 'cnonce' claim" },
requireNotNull(claimSet.issueTime.toInstant()) { "missing 'iat' claim" },
requireNotNull(claimSet.getLongClaim("exi")?.let { Duration.ofSeconds(it) }) { "missing 'exi' claim" },
)
}
override suspend fun invoke(value: String?, at: Instant): Boolean =
value?.let {
result {
val jwt = EncryptedJWT.parse(it)
val claimSet = processor.process(jwt, null)
val expiresAt = requireNotNull(claimSet.expirationTime) { "expirationTime is required" }
at < expiresAt.toInstant()
}.getOrElse { false }
} ?: false
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.europa.ec.eudi.pidissuer.adapter.out.jose
package eu.europa.ec.eudi.pidissuer.adapter.out.credential

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JOSEObjectType
Expand All @@ -23,40 +23,48 @@ import com.nimbusds.jose.crypto.RSAEncrypter
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jwt.EncryptedJWT
import com.nimbusds.jwt.JWTClaimsSet
import eu.europa.ec.eudi.pidissuer.domain.CNonce
import com.nimbusds.openid.connect.sdk.Nonce
import eu.europa.ec.eudi.pidissuer.domain.CredentialIssuerId
import eu.europa.ec.eudi.pidissuer.port.out.jose.EncryptCNonce
import eu.europa.ec.eudi.pidissuer.port.out.credential.GenerateCNonce
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.time.Clock
import java.time.Duration
import java.util.*

/**
* Nimbus implementation of [EncryptCNonce] that encrypts [CNonce] as an [EncryptedJWT].
* Generates a CNonce and encrypts it as a [EncryptedJWT] with Nimbus.
*/
internal class EncryptCNonceWithNimbus(
internal class GenerateCNonceAndEncryptWithNimbus(
private val clock: Clock,
private val issuer: CredentialIssuerId,
encryptionKey: RSAKey,
) : EncryptCNonce {
private val generator: suspend () -> String = { Nonce(128).value },
) : GenerateCNonce {

private val encrypter = RSAEncrypter(encryptionKey)
.apply {
jcaContext.provider = BouncyCastleProvider()
}

override suspend fun invoke(cnonce: CNonce): String =
EncryptedJWT(
override suspend fun invoke(cnonceExpiresIn: Duration): String {
val issuedAt = clock.instant()
val expiresAt = issuedAt + cnonceExpiresIn

return EncryptedJWT(
JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_512, EncryptionMethod.XC20P)
.type(JOSEObjectType("cnonce+jwt"))
.build(),
JWTClaimsSet.Builder()
.apply {
issuer(issuer.externalForm)
audience(issuer.externalForm)
claim("cnonce", cnonce.nonce)
claim("exi", cnonce.expiresIn.seconds)
issueTime(Date.from(cnonce.activatedAt))
claim("cnonce", generator())
issueTime(Date.from(issuedAt))
expirationTime(Date.from(expiresAt))
}
.build(),
).apply {
encrypt(encrypter)
}.serialize()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import com.nimbusds.jwt.proc.DefaultJWTProcessor
import com.nimbusds.jwt.proc.JWTProcessor
import eu.europa.ec.eudi.pidissuer.domain.*
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError
import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce
import java.security.interfaces.ECPublicKey
import java.security.interfaces.EdECPublicKey
import java.security.interfaces.RSAPublicKey
Expand All @@ -51,23 +50,17 @@ internal class ValidateJwtProof(
private val credentialIssuerId: CredentialIssuerId,
) {
context(Raise<IssueCredentialError.InvalidProof>)
suspend operator fun invoke(
operator fun invoke(
unvalidatedProof: UnvalidatedProof.Jwt,
credentialConfiguration: CredentialConfiguration,
decryptCNonce: DecryptCNonce?,
): Pair<CredentialKey, CNonce?> {
): Pair<CredentialKey, String?> {
val proofType = credentialConfiguration.proofTypesSupported[ProofTypeEnum.JWT]
ensureNotNull(proofType) {
IssueCredentialError.InvalidProof("credential configuration '${credentialConfiguration.id.value}' doesn't support 'jwt' proofs")
}
check(proofType is ProofType.Jwt)

val (credentialKey, nonce) = credentialKeyAndNonce(unvalidatedProof, proofType)
val cnonce = decryptCNonce?.let {
ensureNotNull(nonce) { IssueCredentialError.InvalidProof("Missing CNonce") }
decryptCNonce(nonce).getOrElse { raise(IssueCredentialError.InvalidProof("Invalid CNonce", it)) }
}
return credentialKey to cnonce
return credentialKeyAndNonce(unvalidatedProof, proofType)
}

context(Raise<IssueCredentialError.InvalidProof>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,46 +22,41 @@ import arrow.core.toNonEmptyListOrNull
import com.nimbusds.jose.jwk.JWK
import eu.europa.ec.eudi.pidissuer.domain.CredentialConfiguration
import eu.europa.ec.eudi.pidissuer.domain.UnvalidatedProof
import eu.europa.ec.eudi.pidissuer.domain.isExpired
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError.InvalidProof
import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.time.Clock
import eu.europa.ec.eudi.pidissuer.port.out.credential.VerifyCNonce
import kotlinx.coroutines.*
import java.time.Instant

/**
* Validators for Proofs.
*/
internal class ValidateProofs(
private val validateJwtProof: ValidateJwtProof,
private val clock: Clock,
private val verifyCNonce: VerifyCNonce,
private val extractJwkFromCredentialKey: ExtractJwkFromCredentialKey,
) {

context(Raise<InvalidProof>)
suspend operator fun invoke(
unvalidatedProofs: NonEmptyList<UnvalidatedProof>,
credentialConfiguration: CredentialConfiguration,
decryptCNonce: DecryptCNonce?,
at: Instant,
): NonEmptyList<JWK> = coroutineScope {
val credentialKeysAndCNonces = unvalidatedProofs.map {
when (it) {
is UnvalidatedProof.Jwt -> async { validateJwtProof(it, credentialConfiguration, decryptCNonce) }
is UnvalidatedProof.Jwt -> async {
withContext(Dispatchers.Default) {
validateJwtProof(it, credentialConfiguration)
}
}
is UnvalidatedProof.LdpVp -> raise(InvalidProof("Supporting only JWT proof"))
}
}.awaitAll()

val cnonces = credentialKeysAndCNonces.map { it.second }.toNonEmptyListOrNull()
checkNotNull(cnonces)
ensure(cnonces.distinct().size == 1) {
InvalidProof("The Proofs of a Credential Request must contain the same CNonce")
}

cnonces.head?.let { cnonce ->
ensure(!cnonce.isExpired(clock.instant())) {
InvalidProof("CNonce is expired")
}
ensure(verifyCNonce(cnonces, at)) {
InvalidProof("CNonce is not valid")
}

val jwks = credentialKeysAndCNonces.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import eu.europa.ec.eudi.pidissuer.port.input.AuthorizationContext
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError.InvalidProof
import eu.europa.ec.eudi.pidissuer.port.out.IssueSpecificCredential
import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce
import eu.europa.ec.eudi.pidissuer.port.out.persistence.GenerateNotificationId
import eu.europa.ec.eudi.pidissuer.port.out.persistence.StoreIssuedCredentials
import kotlinx.coroutines.*
Expand Down Expand Up @@ -303,7 +302,6 @@ internal class IssueMobileDrivingLicence(
private val generateNotificationId: GenerateNotificationId,
private val clock: Clock,
private val storeIssuedCredentials: StoreIssuedCredentials,
private val decryptCNonce: DecryptCNonce,
) : IssueSpecificCredential {

override val supportedCredential: MsoMdocCredentialConfiguration
Expand All @@ -319,7 +317,7 @@ internal class IssueMobileDrivingLicence(
credentialIdentifier: CredentialIdentifier?,
): CredentialResponse = coroutineScope {
log.info("Issuing mDL")
val holderKeys = validateProofs(request.unvalidatedProofs, supportedCredential, decryptCNonce)
val holderKeys = validateProofs(request.unvalidatedProofs, supportedCredential, clock.instant())
.map { it.toECKeyOrFail { InvalidProof("Only EC Key is supported") } }
val licence = ensureNotNull(getMobileDrivingLicenceData(authorizationContext)) {
IssueCredentialError.Unexpected("Unable to fetch mDL data")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import eu.europa.ec.eudi.pidissuer.port.input.AuthorizationContext
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError.InvalidProof
import eu.europa.ec.eudi.pidissuer.port.out.IssueSpecificCredential
import eu.europa.ec.eudi.pidissuer.port.out.jose.DecryptCNonce
import eu.europa.ec.eudi.pidissuer.port.out.persistence.GenerateNotificationId
import eu.europa.ec.eudi.pidissuer.port.out.persistence.StoreIssuedCredentials
import kotlinx.coroutines.async
Expand Down Expand Up @@ -264,7 +263,6 @@ internal class IssueMsoMdocPid(
private val generateNotificationId: GenerateNotificationId,
private val clock: Clock,
private val storeIssuedCredentials: StoreIssuedCredentials,
private val decryptCNonce: DecryptCNonce,
) : IssueSpecificCredential {

private val log = LoggerFactory.getLogger(IssueMsoMdocPid::class.java)
Expand All @@ -280,7 +278,7 @@ internal class IssueMsoMdocPid(
credentialIdentifier: CredentialIdentifier?,
): CredentialResponse = coroutineScope {
log.info("Handling issuance request ...")
val holderPubKeys = validateProofs(request.unvalidatedProofs, supportedCredential, decryptCNonce)
val holderPubKeys = validateProofs(request.unvalidatedProofs, supportedCredential, clock.instant())
.map { it.toECKeyOrFail { InvalidProof("Only EC Key is supported") } }

val pidData = async { getPidData(authorizationContext) }
Expand Down
Loading

0 comments on commit 844e14c

Please sign in to comment.