Skip to content

Commit

Permalink
Added UnlockSignature to PopSigner.Jwt
Browse files Browse the repository at this point in the history
  • Loading branch information
babisRoutis committed Jul 19, 2024
1 parent d3f15a9 commit 6350579
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 4 deletions.
51 changes: 48 additions & 3 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ package eu.europa.ec.eudi.openid4vci

import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.JWSSigner
import com.nimbusds.jose.crypto.ECDSASigner
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory
import com.nimbusds.jose.crypto.opts.UserAuthenticationRequired
import com.nimbusds.jose.jwk.ECKey
import com.nimbusds.jose.jwk.JWK
import eu.europa.ec.eudi.openid4vci.internal.ClaimSetSerializer
Expand Down Expand Up @@ -175,18 +177,36 @@ interface RequestBatchIssuance {
): Result<AuthorizedRequestAnd<SubmissionOutcome>>
}

/**
* A function that represents a user's authentication using
* biometrics or PIN to authorize the use of a key by [Signature]
*/
fun interface UnlockSignature {
/**
* Implement this method with a user action that authorize the
* use of the [signature] by biometrics or PIN
*
* @param signature The signature that needs user's authentication.
* It will have initialized with the private key
*/
suspend operator fun invoke(signature: Signature)
}

sealed interface PopSigner {
/**
* A signer for proof of possession JWTs
* @param algorithm The algorithm used by the singing key
* @param bindingKey The public key to be included to the proof. It should correspond to the key
* used to sign the proof.
* @param jwsSigner A function to sign the JWT
* @param unlockSignature the ability to unlock the [Signature] using PIN or biometric. If provided, you must
* also set to the [jwsSigner] the [UserAuthenticationRequired] option in order to be taken into account.
*/
data class Jwt(
val algorithm: JWSAlgorithm,
val bindingKey: JwtBindingKey,
val jwsSigner: JWSSigner,
val unlockSignature: UnlockSignature?,
) : PopSigner

/**
Expand All @@ -209,6 +229,29 @@ sealed interface PopSigner {

companion object {

/**
* Factory method for creating a [PopSigner.Jwt]
* that uses [JwtBindingKey.Jwk] to represent the public key in the JWS header.
*
* @param privateKey the key that will be used to sign the JWT
* @param algorithm The algorithm for signing the JWT
* @param unlockSignature
*
* @return the JWT signer
*/
fun jwtPopSigner(
privateKey: ECKey,
algorithm: JWSAlgorithm,
unlockSignature: UnlockSignature?,
): Jwt {
val bindingKey = JwtBindingKey.Jwk(privateKey.toPublicJWK())
val options =
if (unlockSignature != null) setOf(UserAuthenticationRequired.getInstance())
else emptySet()
val jwsSigner = ECDSASigner(privateKey, options)
return Jwt(algorithm, bindingKey, jwsSigner, unlockSignature)
}

/**
* Factory method for creating a [PopSigner.Jwt]
*
Expand All @@ -221,6 +264,7 @@ sealed interface PopSigner {
*
* @return the JWT signer
*/
@Deprecated(message = "Deprecated and will be removed. Use Nimbus to create PopSigner.Jwt")
fun jwtPopSigner(
privateKey: JWK,
algorithm: JWSAlgorithm,
Expand All @@ -235,8 +279,8 @@ sealed interface PopSigner {
},
) { "Public/private key don't match" }

val signer = DefaultJWSSignerFactory().createJWSSigner(privateKey, algorithm)
return Jwt(algorithm, publicKey, signer)
val signer: JWSSigner = DefaultJWSSignerFactory().createJWSSigner(privateKey, algorithm)
return Jwt(algorithm, publicKey, signer, unlockSignature = null)
}

/**
Expand Down Expand Up @@ -330,7 +374,8 @@ sealed class CredentialIssuanceError(message: String) : Throwable(message) {
* It is marked as irrecoverable because it is raised only after the library
* has automatically retried to recover from an [InvalidProof] error and failed
*/
data class IrrecoverableInvalidProof(val errorDescription: String? = null) : CredentialIssuanceError("Irrecoverable invalid proof ")
data class IrrecoverableInvalidProof(val errorDescription: String? = null) :
CredentialIssuanceError("Irrecoverable invalid proof ")

/**
* Issuer has not issued yet deferred credential. Retry interval (in seconds) is provided to caller
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 European Commission
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.europa.ec.eudi.openid4vci.internal

import com.nimbusds.jose.ActionRequiredForJWSCompletionException
import com.nimbusds.jose.CompletableJWSObjectSigning
import eu.europa.ec.eudi.openid4vci.UnlockSignature
import kotlinx.coroutines.coroutineScope

internal suspend fun ActionRequiredForJWSCompletionException.recoverUsing(unlockSignature: UnlockSignature) =
completableJWSObjectSigning.unlockAndComplete(unlockSignature)

internal suspend fun CompletableJWSObjectSigning.unlockAndComplete(unlock: UnlockSignature): Unit =
coroutineScope {
val initializedSignature =
checkNotNull(initializedSignature) { "Missing signature" }
unlock(initializedSignature)
complete()
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import com.authlete.cbor.CBORByteArray
import com.authlete.cose.*
import com.authlete.cwt.CWT
import com.authlete.cwt.CWTClaimsSetBuilder
import com.nimbusds.jose.ActionRequiredForJWSCompletionException
import com.nimbusds.jose.JOSEObjectType
import com.nimbusds.jose.JWSHeader
import com.nimbusds.jose.util.Base64
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
import eu.europa.ec.eudi.openid4vci.*
import kotlinx.coroutines.coroutineScope
import java.time.Clock
import java.util.*

Expand Down Expand Up @@ -77,10 +79,20 @@ internal class JwtProofBuilder(
override suspend fun build(): Proof.Jwt {
val header = header()
val claimSet = claimSet()
val jwt = SignedJWT(header, claimSet).apply { sign(popSigner.jwsSigner) }
val jwt = SignedJWT(header, claimSet).apply { sign(popSigner) }
return Proof.Jwt(jwt)
}

private suspend fun SignedJWT.sign(popSigner: PopSigner.Jwt) = coroutineScope {
try {
sign(popSigner.jwsSigner)
} catch (actionRequired: ActionRequiredForJWSCompletionException) {
popSigner.unlockSignature
?.let { userAction -> actionRequired.recoverUsing(userAction) }
?: throw actionRequired
}
}

private fun header(): JWSHeader {
val algorithm = popSigner.algorithm
val headerBuilder = JWSHeader.Builder(algorithm)
Expand Down

0 comments on commit 6350579

Please sign in to comment.