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 UnlockSignature to PopSigner.Jwt #290

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
25 changes: 23 additions & 2 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vci/Issuance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ package eu.europa.ec.eudi.openid4vci
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.JWSSigner
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory
import com.nimbusds.jose.crypto.opts.UserAuthenticationRequired
import com.nimbusds.jose.jwk.JWK
import eu.europa.ec.eudi.openid4vci.internal.ClaimSetSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
import java.security.Signature

/**
* Represents the credential as it is serialized by the credential issuer
Expand Down Expand Up @@ -184,18 +186,36 @@ interface RequestIssuance {
): 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

companion object {
Expand All @@ -212,6 +232,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 @@ -226,8 +247,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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,
) {
coroutineScope {
val initializedSignature = checkNotNull(initializedSignature) { "Missing signature" }
unlock(initializedSignature)
complete()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
*/
package eu.europa.ec.eudi.openid4vci.internal

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 @@ -68,10 +70,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
Loading