Skip to content

Commit

Permalink
Add: Generic JWS validation
Browse files Browse the repository at this point in the history
  • Loading branch information
acrusage-iaik committed Dec 9, 2024
1 parent 7a29f5c commit 79ffead
Show file tree
Hide file tree
Showing 48 changed files with 1,436 additions and 200 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ class OidcSiopVerifier(
private val keyMaterial: KeyMaterial = EphemeralKeyWithoutCert(),
private val verifier: Verifier = VerifierAgent(identifier = clientIdScheme.clientId),
private val jwsService: JwsService = DefaultJwsService(DefaultCryptoService(keyMaterial)),
private val verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(DefaultVerifierCryptoService()),
private val verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(
DefaultVerifierCryptoService()
),
timeLeewaySeconds: Long = 300L,
private val clock: Clock = Clock.System,
private val nonceService: NonceService = DefaultNonceService(),
Expand Down Expand Up @@ -124,8 +126,9 @@ class OidcSiopVerifier(
) : ClientIdScheme(PreRegistered, clientId)
}

private val containerJwt =
FormatContainerJwt(algorithmStrings = verifierJwsService.supportedAlgorithms.map { it.identifier })
private val containerJwt = FormatContainerJwt(
algorithmStrings = verifierJwsService.supportedAlgorithms.map { it.identifier },
)


/**
Expand All @@ -147,7 +150,11 @@ class OidcSiopVerifier(
RelyingPartyMetadata(
redirectUris = listOfNotNull((clientIdScheme as? ClientIdScheme.RedirectUri)?.clientId),
jsonWebKeySet = JsonWebKeySet(listOf(keyMaterial.publicKey.toJsonWebKey())),
subjectSyntaxTypesSupported = setOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY, BINDING_METHOD_JWK),
subjectSyntaxTypesSupported = setOf(
URN_TYPE_JWK_THUMBPRINT,
PREFIX_DID_KEY,
BINDING_METHOD_JWK
),
vpFormats = FormatHolder(
msoMdoc = containerJwt,
jwtVp = containerJwt,
Expand Down Expand Up @@ -331,9 +338,11 @@ class OidcSiopVerifier(
requestOptions: RequestOptions,
): KmmResult<JwsSigned<AuthenticationRequestParameters>> = catching {
val requestObject = createAuthnRequest(requestOptions)
val attestationJwt = (clientIdScheme as? ClientIdScheme.VerifierAttestation)?.attestationJwt?.serialize()
val attestationJwt =
(clientIdScheme as? ClientIdScheme.VerifierAttestation)?.attestationJwt?.serialize()
val certificateChain = (clientIdScheme as? ClientIdScheme.CertificateSanDns)?.chain
val issuer = (clientIdScheme as? ClientIdScheme.PreRegistered)?.clientId ?: "https://self-issued.me/v2"
val issuer = (clientIdScheme as? ClientIdScheme.PreRegistered)?.clientId
?: "https://self-issued.me/v2"
jwsService.createSignedJwsAddingParams(
header = JwsHeader(
algorithm = jwsService.algorithm,
Expand Down Expand Up @@ -490,7 +499,8 @@ class OidcSiopVerifier(
/**
* Wallet provided an `id_token`, no `vp_token` (as requested by us!)
*/
data class IdToken(val idToken: at.asitplus.openid.IdToken, val state: String?) : AuthnResponseResult()
data class IdToken(val idToken: at.asitplus.openid.IdToken, val state: String?) :
AuthnResponseResult()

/**
* Validation results of all returned verifiable presentations
Expand Down Expand Up @@ -571,7 +581,11 @@ class OidcSiopVerifier(
return validateAuthnResponse(jarmResponse.payload)
}
JweEncrypted.deserialize(response).getOrNull()?.let { jarmResponse ->
jwsService.decryptJweObject(jarmResponse, response, AuthenticationResponseParameters.serializer())
jwsService.decryptJweObject(
jarmResponse,
response,
AuthenticationResponseParameters.serializer()
)
.getOrNull()?.let { decrypted ->
return validateAuthnResponse(decrypted.payload)
}
Expand Down Expand Up @@ -608,7 +622,8 @@ class OidcSiopVerifier(

val validationResults = descriptors.map { descriptor ->
val relatedPresentation =
JsonPath(descriptor.cumulativeJsonPath).query(verifiablePresentation).first().value
JsonPath(descriptor.cumulativeJsonPath).query(verifiablePresentation)
.first().value
val result = runCatching {
verifyPresentationResult(descriptor, relatedPresentation, expectedNonce)
}.getOrElse {
Expand All @@ -630,9 +645,11 @@ class OidcSiopVerifier(

@Throws(IllegalArgumentException::class, CancellationException::class)
private suspend fun extractValidatedIdToken(idTokenJws: String): IdToken {
val jwsSigned = JwsSigned.deserialize<IdToken>(IdToken.serializer(), idTokenJws, vckJsonSerializer).getOrNull()
?: throw IllegalArgumentException("idToken")
.also { Napier.w("Could not parse JWS from idToken: $idTokenJws") }
val jwsSigned =
JwsSigned.deserialize<IdToken>(IdToken.serializer(), idTokenJws, vckJsonSerializer)
.getOrNull()
?: throw IllegalArgumentException("idToken")
.also { Napier.w("Could not parse JWS from idToken: $idTokenJws") }
if (!verifierJwsService.verifyJwsObject(jwsSigned))
throw IllegalArgumentException("idToken")
.also { Napier.w { "JWS of idToken not verified: $idTokenJws" } }
Expand Down Expand Up @@ -675,7 +692,7 @@ class OidcSiopVerifier(
ClaimFormat.JWT_SD,
ClaimFormat.MSO_MDOC,
ClaimFormat.JWT_VP,
-> when (relatedPresentation) {
-> when (relatedPresentation) {
is JsonPrimitive -> verifier.verifyPresentation(
relatedPresentation.content,
challenge
Expand All @@ -687,32 +704,33 @@ class OidcSiopVerifier(
else -> throw IllegalArgumentException()
}

private fun Verifier.VerifyPresentationResult.mapToAuthnResponseResult(state: String) = when (this) {
is Verifier.VerifyPresentationResult.InvalidStructure ->
AuthnResponseResult.Error("parse vp failed", state)
.also { Napier.w("VP error: $this") }

is Verifier.VerifyPresentationResult.NotVerified ->
AuthnResponseResult.ValidationError("vpToken", state)
.also { Napier.w("VP error: $this") }

is Verifier.VerifyPresentationResult.Success ->
AuthnResponseResult.Success(vp, state)
.also { Napier.i("VP success: $this") }

is Verifier.VerifyPresentationResult.SuccessIso ->
AuthnResponseResult.SuccessIso(documents, state)
.also { Napier.i("VP success: $this") }

is Verifier.VerifyPresentationResult.SuccessSdJwt ->
AuthnResponseResult.SuccessSdJwt(
sdJwtSigned = sdJwtSigned,
verifiableCredentialSdJwt = verifiableCredentialSdJwt,
reconstructed = reconstructedJsonObject,
disclosures = disclosures,
state = state
).also { Napier.i("VP success: $this") }
}
private fun Verifier.VerifyPresentationResult.mapToAuthnResponseResult(state: String) =
when (this) {
is Verifier.VerifyPresentationResult.InvalidStructure ->
AuthnResponseResult.Error("parse vp failed", state)
.also { Napier.w("VP error: $this") }

is Verifier.VerifyPresentationResult.NotVerified ->
AuthnResponseResult.ValidationError("vpToken", state)
.also { Napier.w("VP error: $this") }

is Verifier.VerifyPresentationResult.Success ->
AuthnResponseResult.Success(vp, state)
.also { Napier.i("VP success: $this") }

is Verifier.VerifyPresentationResult.SuccessIso ->
AuthnResponseResult.SuccessIso(documents, state)
.also { Napier.i("VP success: $this") }

is Verifier.VerifyPresentationResult.SuccessSdJwt ->
AuthnResponseResult.SuccessSdJwt(
sdJwtSigned = sdJwtSigned,
verifiableCredentialSdJwt = verifiableCredentialSdJwt,
reconstructed = reconstructedJsonObject,
disclosures = disclosures,
state = state
).also { Napier.i("VP success: $this") }
}

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import at.asitplus.dif.*
import at.asitplus.jsonpath.core.NodeList
import at.asitplus.jsonpath.core.NormalizedJsonPath
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.rfc.tokenStatusList.agents.communication.primitives.StatusListTokenMediaType
import at.asitplus.wallet.lib.iso.IssuerSigned
import kotlinx.serialization.Serializable

Expand Down Expand Up @@ -33,8 +34,7 @@ interface Holder {
*
* @return `true` if the revocation list has been validated and set, `false` otherwise
*/
fun setRevocationList(it: String): Boolean
fun setRevocationStatusListJwt(it: String): Boolean
fun setRevocationStatusList(type: StatusListTokenMediaType, it: Any): Boolean

sealed class StoreCredentialInput {
data class Vc(
Expand All @@ -57,15 +57,15 @@ interface Holder {
* Stores the verifiable credential in [credential] if it parses and validates,
* and returns it for future reference.
*
* Note: Revocation credentials should not be stored, but set with [setRevocationStatusListJwt].
* Note: Revocation credentials should not be stored, but set with [setRevocationStatusList].
*/
suspend fun storeCredential(credential: StoreCredentialInput): KmmResult<StoredCredential>

/**
* Gets a list of all stored credentials, with a revocation status.
*
* Note that the revocation status may be [Validator.RevocationStatus.UNKNOWN] if no revocation list
* has been set with [setRevocationStatusListJwt]
* has been set with [setRevocationStatusList]
*/
suspend fun getCredentials(): Collection<StoredCredential>?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ package at.asitplus.wallet.lib.agent
import at.asitplus.KmmResult
import at.asitplus.KmmResult.Companion.wrap
import at.asitplus.catching
import at.asitplus.dif.*
import at.asitplus.dif.ClaimFormat
import at.asitplus.dif.FormatHolder
import at.asitplus.dif.InputDescriptor
import at.asitplus.dif.PresentationDefinition
import at.asitplus.dif.PresentationSubmission
import at.asitplus.dif.PresentationSubmissionDescriptor
import at.asitplus.jsonpath.core.NormalizedJsonPath
import at.asitplus.signum.indispensable.cosef.CoseKey
import at.asitplus.signum.indispensable.cosef.toCoseKey
Expand All @@ -13,6 +18,7 @@ import at.asitplus.wallet.lib.cbor.DefaultCoseService
import at.asitplus.wallet.lib.data.CredentialToJsonConverter
import at.asitplus.wallet.lib.data.dif.InputEvaluator
import at.asitplus.wallet.lib.data.dif.PresentationSubmissionValidator
import at.asitplus.wallet.lib.data.rfc.tokenStatusList.agents.communication.primitives.StatusListTokenMediaType
import at.asitplus.wallet.lib.jws.DefaultJwsService
import at.asitplus.wallet.lib.jws.JwsService
import com.benasher44.uuid.uuid4
Expand Down Expand Up @@ -52,19 +58,26 @@ class HolderAgent(
*
* @return `true` if the revocation list has been validated and set, `false` otherwise
*/
override fun setRevocationList(it: String): Boolean {
return validator.setRevocationListCredential(it)
override fun setRevocationStatusList(type: StatusListTokenMediaType, it: Any): Boolean {
return when(type) {
StatusListTokenMediaType.Jwt -> setRevocationStatusListJwt(it as String)
StatusListTokenMediaType.Cwt -> setRevocationStatusListCwt(it as ByteArray)
}
}

override fun setRevocationStatusListJwt(it: String): Boolean {
fun setRevocationStatusListJwt(it: String): Boolean {
return validator.setRevocationStatusListJwt(it)
}

fun setRevocationStatusListCwt(it: ByteArray): Boolean {
return validator.setRevocationStatusListCwt(it)
}

/**
* Stores the verifiable credential in [credential] if it parses and validates,
* and returns it for future reference.
*
* Note: Revocation credentials should not be stored, but set with [setRevocationStatusListJwt].
* Note: Revocation credentials should not be stored, but set with [setRevocationStatusList].
*/
override suspend fun storeCredential(credential: Holder.StoreCredentialInput) = catching {
when (credential) {
Expand Down Expand Up @@ -114,7 +127,7 @@ class HolderAgent(
* Gets a list of all stored credentials, with a revocation status.
*
* Note that the revocation status may be [Validator.RevocationStatus.UNKNOWN] if no revocation list
* has been set with [setRevocationStatusListJwt]
* has been set with [setRevocationStatusList]
*/
override suspend fun getCredentials(): Collection<Holder.StoredCredential>? {
val credentials = subjectCredentialStore.getCredentials().getOrNull()
Expand Down
27 changes: 2 additions & 25 deletions vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Issuer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import kotlinx.datetime.Instant
* It can issue Verifiable Credentials, revoke credentials and build a revocation list.
*/
@OptIn(ExperimentalUnsignedTypes::class)
interface Issuer<WebToken: Any> : ReferencedTokenIssuer, StatusIssuer<String, ByteArray>, StatusProvider<WebToken> {
interface Issuer : ReferencedTokenIssuer<CredentialToBeIssued, KmmResult<Issuer.IssuedCredential>>, StatusIssuer<String, ByteArray>, StatusProvider<Any> {

/**
* A credential issued by an [Issuer], in a specific format
Expand Down Expand Up @@ -65,30 +65,7 @@ interface Issuer<WebToken: Any> : ReferencedTokenIssuer, StatusIssuer<String, By
* according to the representation, i.e. it essentially signs the credential with the issuer key.
*/
suspend fun issueCredential(credential: CredentialToBeIssued): KmmResult<IssuedCredential>

/**
* @return a status list jwt.
* @param timePeriod time Period to issue a revocation list for
*/
suspend fun issueStatusListJwt(timePeriod: Int? = null): String?

/**
* @return a status list json string.
* @param timePeriod time Period to issue a revocation list for
*/
suspend fun issueStatusListJson(timePeriod: Int? = null): String?

/**
* @return a status list cwt.
* @param timePeriod time Period to issue a revocation list for
*/
suspend fun issueStatusListCwt(timePeriod: Int? = null): ByteArray?

/**
* @return a status list cbor byte array.
* @param timePeriod time Period to issue a revocation list for
*/
suspend fun issueStatusListCbor(timePeriod: Int? = null): ByteArray?
override suspend fun issueToken(tokenRequest: CredentialToBeIssued) = issueCredential(credential = tokenRequest)

/**
* Returns a status list as defined in [TokenListStatus](https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-06.html)
Expand Down
Loading

0 comments on commit 79ffead

Please sign in to comment.