diff --git a/waltid-libraries/protocols/waltid-openid4vc/build.gradle.kts b/waltid-libraries/protocols/waltid-openid4vc/build.gradle.kts index 38f2fc035..a0c29eecf 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/build.gradle.kts +++ b/waltid-libraries/protocols/waltid-openid4vc/build.gradle.kts @@ -130,6 +130,8 @@ kotlin { val jvmMain by getting { dependencies { implementation("io.ktor:ktor-client-okhttp:$ktor_version") + implementation("com.augustcellars.cose:cose-java:1.1.0") + implementation("com.nimbusds:nimbus-jose-jwt:9.41.1") } } val jvmTest by getting { diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VC.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VC.kt new file mode 100644 index 000000000..cff710602 --- /dev/null +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VC.kt @@ -0,0 +1,322 @@ +package id.walt.oid4vc + +import id.walt.crypto.keys.Key +import id.walt.crypto.keys.jwk.JWKKey +import id.walt.crypto.utils.Base64Utils.base64UrlDecode +import id.walt.crypto.utils.Base64Utils.decodeFromBase64Url +import id.walt.crypto.utils.JsonUtils.toJsonElement +import id.walt.did.dids.DidService +import id.walt.did.dids.DidUtils +import id.walt.mdoc.dataelement.MapElement +import id.walt.oid4vc.data.* +import id.walt.oid4vc.data.ResponseType.Companion.getResponseTypeString +import id.walt.oid4vc.data.dif.PresentationDefinition +import id.walt.oid4vc.definitions.JWTClaims +import id.walt.oid4vc.errors.AuthorizationError +import id.walt.oid4vc.errors.TokenError +import id.walt.oid4vc.errors.TokenVerificationError +import id.walt.oid4vc.providers.TokenTarget +import id.walt.oid4vc.requests.AuthorizationRequest +import id.walt.oid4vc.requests.TokenRequest +import id.walt.oid4vc.responses.* +import id.walt.oid4vc.util.COSESign1Utils +import id.walt.oid4vc.util.randomUUID +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.utils.io.core.* +import kotlinx.datetime.Clock +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.* +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +object OpenID4VC { + private val log = KotlinLogging.logger { } + + suspend fun generateToken(sub: String, issuer: String, audience: TokenTarget, tokenId: String? = null, tokenKey: Key): String { + return signToken(audience, buildJsonObject { + put(JWTClaims.Payload.subject, sub) + put(JWTClaims.Payload.issuer, issuer) + put(JWTClaims.Payload.audience, audience.name) + tokenId?.let { put(JWTClaims.Payload.jwtID, it) } + }, tokenKey) + } + + suspend fun verifyAndParseToken(token: String, issuer: String, target: TokenTarget, tokenKey: Key? = null): JsonObject? { + if (verifyTokenSignature(target, token, tokenKey)) { + val payload = parseTokenPayload(token) + if (payload.keys.containsAll( + setOf( + JWTClaims.Payload.subject, + JWTClaims.Payload.audience, + JWTClaims.Payload.issuer + ) + ) && + payload[JWTClaims.Payload.audience]!!.jsonPrimitive.content == target.name && + payload[JWTClaims.Payload.issuer]!!.jsonPrimitive.content == issuer + ) { + return payload + } + } + return null + } + + suspend fun verifyAndParseIdToken(token: String, tokenKey: Key? = null): JsonObject { + // 1. Validate Header + val header = parseTokenHeader(token) + if (!header.keys.containsAll( + setOf( + JWTClaims.Header.type, + JWTClaims.Header.keyID, + JWTClaims.Header.algorithm, + ) + ) + ) { + throw IllegalStateException("Invalid header in token") + } + + // 2. Validate Payload + val payload = parseTokenPayload(token) + if (!payload.keys.containsAll( + setOf( + JWTClaims.Payload.issuer, + JWTClaims.Payload.subject, + JWTClaims.Payload.audience, + JWTClaims.Payload.expirationTime, + JWTClaims.Payload.issuedAtTime, + JWTClaims.Payload.nonce, + ) + ) + ) { + throw IllegalArgumentException("Invalid payload in token") + } + + // 3. Verify iss = sub = did + val sub = payload[JWTClaims.Payload.subject]!!.jsonPrimitive.content + val iss = payload[JWTClaims.Payload.issuer]!!.jsonPrimitive.content + val kid = header[JWTClaims.Header.keyID]!!.jsonPrimitive.content + val did = kid.substringBefore("#") + + if (iss != sub || iss != did || sub != did) { + log.debug { "$sub $iss $did" } + throw IllegalArgumentException("Invalid payload in token. sub != iss != did") + } + + // 4. Verify Signature + if (!verifyTokenSignature(TokenTarget.TOKEN, token, tokenKey)) + throw IllegalArgumentException("Invalid token - cannot verify signature") + + return payload + } + + + suspend fun generateAuthorizationCodeFor(sessionId: String, issuer: String, tokenKey: Key): String { + return generateToken(sessionId, issuer, TokenTarget.TOKEN, null, tokenKey) + } + + suspend fun validateAndParseTokenRequest(tokenRequest: TokenRequest, issuer: String, tokenKey: Key? = null): JsonObject { + val code = when (tokenRequest.grantType) { + GrantType.authorization_code -> tokenRequest.code ?: throw TokenError( + tokenRequest = tokenRequest, + errorCode = TokenErrorCode.invalid_grant, + message = "No code parameter found on token request" + ) + + GrantType.pre_authorized_code -> tokenRequest.preAuthorizedCode ?: throw TokenError( + tokenRequest = tokenRequest, + errorCode = TokenErrorCode.invalid_grant, + message = "No pre-authorized_code parameter found on token request" + ) + + else -> throw TokenError(tokenRequest, TokenErrorCode.unsupported_grant_type, "Grant type not supported") + } + return verifyAndParseToken(code, issuer, TokenTarget.TOKEN, tokenKey) ?: throw TokenError( + tokenRequest = tokenRequest, + errorCode = TokenErrorCode.invalid_grant, + message = "Authorization code could not be verified" + ) + } + + // Create an ID or VP Token request using JAR OAuth2.0 specification https://www.rfc-editor.org/rfc/rfc9101.html + @OptIn(ExperimentalUuidApi::class) + suspend fun processCodeFlowAuthorizationWithAuthorizationRequest( + authorizationRequest: AuthorizationRequest, + responseType: ResponseType, + providerMetadata: OpenIDProviderMetadata, + tokenKey: Key, + isJar: Boolean? = true, + presentationDefinition: PresentationDefinition? = null, + ): AuthorizationCodeWithAuthorizationRequestResponse { + if (!authorizationRequest.responseType.contains(ResponseType.Code)) + throw AuthorizationError( + authorizationRequest, + AuthorizationErrorCode.invalid_request, + message = "Invalid response type ${authorizationRequest.responseType}, for authorization code flow." + ) + + // Bind authentication request with state + val authorizationRequestServerState = Uuid.random().toString() + val authorizationRequestServerNonce = Uuid.random().toString() + val authorizationResponseServerMode = ResponseMode.direct_post + + val clientId = providerMetadata.issuer!! + val redirectUri = providerMetadata.issuer + "/direct_post" + val scope = setOf("openid") + + // Create a session with the state of the ID Token request since it is needed in the direct_post endpoint + //initializeAuthorization(authorizationRequest, 5.minutes, authorizationRequestServerState) + + return AuthorizationCodeWithAuthorizationRequestResponse.success( + state = authorizationRequestServerState, + clientId = clientId, + redirectUri = redirectUri, + responseType = getResponseTypeString(responseType), + responseMode = authorizationResponseServerMode, + scope = scope, + nonce = authorizationRequestServerNonce, + requestUri = null, + request = when (isJar) { + // Create a jwt as request object as defined in JAR OAuth2.0 specification + true -> signToken( + TokenTarget.TOKEN, + buildJsonObject { + put(JWTClaims.Payload.issuer, providerMetadata.issuer) + put(JWTClaims.Payload.audience, authorizationRequest.clientId) + put(JWTClaims.Payload.nonce, authorizationRequestServerNonce) + put("state", authorizationRequestServerState) + put("client_id", clientId) + put("redirect_uri", redirectUri) + put("response_type", getResponseTypeString(responseType)) + put("response_mode", authorizationResponseServerMode.name) + put("scope", "openid") + when (responseType) { + ResponseType.VpToken -> put("presentation_definition", presentationDefinition!!.toJSON()) + else -> null + } + }, tokenKey) + + else -> null + }, + presentationDefinition = when (responseType) { + ResponseType.VpToken -> presentationDefinition!!.toJSONString() + else -> null + } + ) + } + + suspend fun processCodeFlowAuthorization(authorizationRequest: AuthorizationRequest, sessionId: String, providerMetadata: OpenIDProviderMetadata, tokenKey: Key): AuthorizationCodeResponse { + if (!authorizationRequest.responseType.contains(ResponseType.Code)) + throw AuthorizationError( + authorizationRequest, + AuthorizationErrorCode.invalid_request, + message = "Invalid response type ${authorizationRequest.responseType}, for authorization code flow." + ) + val issuer = providerMetadata.issuer ?: throw AuthorizationError(authorizationRequest, AuthorizationErrorCode.server_error,"No issuer configured in given provider metadata") + val code = generateAuthorizationCodeFor(sessionId, issuer, tokenKey) + return AuthorizationCodeResponse.success(code, mapOf("state" to listOf(authorizationRequest.state ?: randomUUID()))) + } + + suspend fun processImplicitFlowAuthorization(authorizationRequest: AuthorizationRequest, sessionId: String, providerMetadata: OpenIDProviderMetadata, tokenKey: Key): TokenResponse { + log.debug { "> processImplicitFlowAuthorization for $authorizationRequest" } + if (!authorizationRequest.responseType.contains(ResponseType.Token) && !authorizationRequest.responseType.contains(ResponseType.VpToken) + && !authorizationRequest.responseType.contains(ResponseType.IdToken) + ) + throw AuthorizationError( + authorizationRequest, + AuthorizationErrorCode.invalid_request, + message = "Invalid response type ${authorizationRequest.responseType}, for implicit authorization flow." + ) + log.debug { "> processImplicitFlowAuthorization: generateTokenResponse..." } + val issuer = providerMetadata.issuer ?: throw AuthorizationError(authorizationRequest, AuthorizationErrorCode.server_error,"No issuer configured in given provider metadata") + return TokenResponse.success( + generateToken(sessionId, issuer, TokenTarget.ACCESS, null, tokenKey), + "bearer", state = authorizationRequest.state, + expiresIn = Clock.System.now().epochSeconds + 864000L // ten days in seconds + ) + } + + suspend fun processDirectPost(authorizationRequest: AuthorizationRequest, sessionId: String, providerMetadata: OpenIDProviderMetadata, tokenKey: Key): AuthorizationCodeResponse { + // Verify nonce - need to add Id token nonce session + // if (payload[JWTClaims.Payload.nonce] != session.) + + // Generate code and proceed as regular authorization request + val mappedState = mapOf("state" to listOf(authorizationRequest.state!!)) + val issuer = providerMetadata.issuer ?: throw AuthorizationError(authorizationRequest, AuthorizationErrorCode.server_error,"No issuer configured in given provider metadata") + val code = generateAuthorizationCodeFor(sessionId, issuer, tokenKey) + + return AuthorizationCodeResponse.success(code, mappedState) + } + + const val PUSHED_AUTHORIZATION_REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:" + fun getPushedAuthorizationRequestUri(sessionId: String): String = "$PUSHED_AUTHORIZATION_REQUEST_URI_PREFIX${sessionId}" + fun getPushedAuthorizationSessionId(requestUri: String): String = requestUri.substringAfter( + PUSHED_AUTHORIZATION_REQUEST_URI_PREFIX) + + // ------------------------------------------ + // Simple cryptographics operation interface implementations + suspend fun signToken( + target: TokenTarget, + payload: JsonObject, + privKey: Key, + header: JsonObject? = null) : String + { + val keyId = privKey.getKeyId() + log.debug { "Signing JWS: $payload" } + log.debug { "JWS Signature: target: $target, keyId: $keyId, header: $header" } + + val headers = (header?.toMutableMap() ?: mutableMapOf()) + .plus(mapOf("alg" to "ES256".toJsonElement(), "type" to "jwt".toJsonElement(), "kid" to keyId.toJsonElement())) + + return privKey.signJws(payload.toString().toByteArray(), headers).also { + log.debug { "Signed JWS: >> $it" } + } + } + + suspend fun signCWTToken( + target: TokenTarget, + payload: MapElement, + privKey: Key, + header: MapElement? = null, + ): String { + TODO("Not yet implemented, may not be required anymore (removed from https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#cwt-proof-type)") + } + + suspend fun verifyTokenSignature(target: TokenTarget, token: String, tokenKey: Key? = null): Boolean { + log.debug { "Verifying JWS: $token" } + log.debug { "JWS Verification: target: $target" } + + val tokenHeader = Json.parseToJsonElement(token.split(".")[0].base64UrlDecode().decodeToString()).jsonObject + val key = (if (tokenHeader["jwk"] != null) { + JWKKey.importJWK(tokenHeader["jwk"].toString()).getOrThrow() + } else if (tokenHeader["kid"] != null) { + val kid = tokenHeader["kid"]!!.jsonPrimitive.content.split("#")[0] + if(DidUtils.isDidUrl(kid)) { + log.debug { "Resolving DID: $kid" } + DidService.resolveToKey(kid).getOrThrow() + } else if(tokenKey != null && kid.equals(tokenKey.getKeyId())) { + tokenKey + } else null + } else tokenKey) ?: throw TokenVerificationError(token, target, "Could not resolve key for given token") + return key.verifyJws(token).also { log.debug { "VERIFICATION IS: $it" } }.isSuccess + } + + @OptIn(ExperimentalSerializationApi::class) + suspend fun verifyCOSESign1Signature(target: TokenTarget, token: String): Boolean { + // May not be required anymore (removed from https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#cwt-proof-type) + log.debug { "Verifying JWS: $token" } + log.debug { "JWS Verification: target: $target" } + // requires currently JVM specific implementation for COSE_Sign1 signature verification + return COSESign1Utils.verifyCOSESign1Signature(target, token) + } + + fun parseTokenPayload(token: String): JsonObject { + return token.substringAfter(".").substringBefore(".").let { + Json.decodeFromString(it.decodeFromBase64Url().decodeToString()) + } + } + + fun parseTokenHeader(token: String): JsonObject { + return token.substringBefore(".").let { + Json.decodeFromString(it.decodeFromBase64Url().decodeToString()) + } + } +} diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt index e93abf8e4..4bc80d78e 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt @@ -1,31 +1,59 @@ package id.walt.oid4vc +import cbor.Cbor +import id.walt.credentials.issuance.dataFunctions +import id.walt.credentials.utils.CredentialDataMergeUtils.mergeSDJwtVCPayloadWithMapping import id.walt.credentials.utils.VCFormat import id.walt.crypto.keys.Key +import id.walt.crypto.keys.jwk.JWKKey +import id.walt.crypto.utils.Base64Utils.base64UrlDecode +import id.walt.crypto.utils.JsonUtils.toJsonElement +import id.walt.crypto.utils.JsonUtils.toJsonObject +import id.walt.did.dids.DidUtils +import id.walt.mdoc.cose.COSESign1 +import id.walt.mdoc.dataelement.ByteStringElement +import id.walt.mdoc.dataelement.MapKey +import id.walt.mdoc.dataelement.StringElement import id.walt.oid4vc.data.* import id.walt.oid4vc.data.ResponseType.Companion.getResponseTypeString import id.walt.oid4vc.data.dif.PresentationDefinition import id.walt.oid4vc.data.dif.PresentationSubmission import id.walt.oid4vc.definitions.CROSS_DEVICE_CREDENTIAL_OFFER_URL import id.walt.oid4vc.definitions.JWTClaims +import id.walt.oid4vc.errors.CredentialError import id.walt.oid4vc.errors.TokenError +import id.walt.oid4vc.providers.TokenTarget import id.walt.oid4vc.requests.AuthorizationRequest import id.walt.oid4vc.requests.CredentialOfferRequest +import id.walt.oid4vc.requests.CredentialRequest import id.walt.oid4vc.requests.TokenRequest import id.walt.oid4vc.responses.AuthorizationCodeWithAuthorizationRequestResponse +import id.walt.oid4vc.responses.CredentialErrorCode import id.walt.oid4vc.responses.TokenErrorCode import id.walt.oid4vc.util.JwtUtils import id.walt.oid4vc.util.http import id.walt.policies.Verifier import id.walt.policies.models.PolicyRequest.Companion.parsePolicyRequests +import id.walt.sdjwt.SDJwt +import id.walt.sdjwt.SDJwtVC +import id.walt.sdjwt.SDJwtVC.Companion.SD_JWT_VC_TYPE_HEADER +import id.walt.sdjwt.SDJwtVC.Companion.defaultPayloadProperties +import id.walt.sdjwt.SDMap +import id.walt.sdjwt.SDPayload +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.util.* import io.ktor.utils.io.core.* +import kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.json.* object OpenID4VCI { + private val log = KotlinLogging.logger { } + + class CredentialRequestValidationResult(val success: Boolean, errorCode: CredentialErrorCode? = null, val message: String? = null) {} + fun getCredentialOfferRequestUrl( credOffer: CredentialOffer, credentialOfferEndpoint: String = CROSS_DEVICE_CREDENTIAL_OFFER_URL, @@ -53,6 +81,15 @@ object OpenID4VCI { }.buildString() } + fun getCredentialOfferRequestUrl( + credOfferReq: CredentialOfferRequest, + credentialOfferEndpoint: String = CROSS_DEVICE_CREDENTIAL_OFFER_URL + ): String { + return URLBuilder(credentialOfferEndpoint).apply { + parameters.appendAll(parametersOf(credOfferReq.toHttpParameters())) + }.buildString() + } + fun parseCredentialOfferRequestUrl(credOfferReqUrl: String): CredentialOfferRequest { return CredentialOfferRequest.fromHttpParameters(Url(credOfferReqUrl).parameters.toMap()) } @@ -241,7 +278,7 @@ object OpenID4VCI { val did = kid.substringBefore("#") if (iss != sub || iss != did || sub != did) { - println("$sub $iss $did") + log.debug { "$sub $iss $did" } throw IllegalArgumentException("Invalid payload in token. sub != iss != did") } @@ -263,4 +300,137 @@ object OpenID4VCI { return payload } + + fun createDefaultProviderMetadata(baseUrl: String) = OpenIDProviderMetadata( + issuer = baseUrl, + authorizationEndpoint = "$baseUrl/authorize", + pushedAuthorizationRequestEndpoint = "$baseUrl/par", + tokenEndpoint = "$baseUrl/token", + credentialEndpoint = "$baseUrl/credential", + batchCredentialEndpoint = "$baseUrl/batch_credential", + deferredCredentialEndpoint = "$baseUrl/credential_deferred", + jwksUri = "$baseUrl/jwks", + grantTypesSupported = setOf(GrantType.authorization_code, GrantType.pre_authorized_code), + requestUriParameterSupported = true, + subjectTypesSupported = setOf(SubjectType.public), + authorizationServer = baseUrl, + credentialIssuer = baseUrl, // (EBSI) this should be just "$baseUrl" https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-11.2.1 + responseTypesSupported = setOf( + "code", + "vp_token", + "id_token" + ), // (EBSI) this is required one https://www.rfc-editor.org/rfc/rfc8414.html#section-2 + idTokenSigningAlgValuesSupported = setOf("ES256"), // (EBSI) https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-self-issued-openid-provider- + codeChallengeMethodsSupported = listOf("S256") + ) + + fun getNonceFromProof(proofOfPossession: ProofOfPossession) = when (proofOfPossession.proofType) { + ProofType.jwt -> JwtUtils.parseJWTPayload(proofOfPossession.jwt!!)[JWTClaims.Payload.nonce]?.jsonPrimitive?.content + ProofType.cwt -> Cbor.decodeFromByteArray(proofOfPossession.cwt!!.base64UrlDecode()).decodePayload()?.let { payload -> + payload.value[MapKey(ProofOfPossession.CWTProofBuilder.LABEL_NONCE)].let { + when (it) { + is ByteStringElement -> io.ktor.utils.io.core.String(it.value) + is StringElement -> it.value + else -> throw Error("Invalid nonce type") + } + } + } + + else -> null + } + + suspend fun validateProofOfPossession(credentialRequest: CredentialRequest, nonce: String): Boolean { + log.debug { "VALIDATING: ${credentialRequest.proof} with nonce $nonce" } + log.debug { "VERIFYING ITS SIGNATURE" } + if (credentialRequest.proof == null) return false + return when { + credentialRequest.proof.isJwtProofType -> OpenID4VC.verifyTokenSignature( + TokenTarget.PROOF_OF_POSSESSION, credentialRequest.proof.jwt!! + ) && getNonceFromProof(credentialRequest.proof) == nonce + + credentialRequest.proof.isCwtProofType -> OpenID4VC.verifyCOSESign1Signature( + TokenTarget.PROOF_OF_POSSESSION, credentialRequest.proof.cwt!! + ) && getNonceFromProof(credentialRequest.proof) == nonce + + else -> false + } + } + + suspend fun validateCredentialRequest(credentialRequest: CredentialRequest, nonce: String, openIDProviderMetadata: OpenIDProviderMetadata): CredentialRequestValidationResult { + log.debug { "Credential request to validate: $credentialRequest" } + if (credentialRequest.proof == null || !validateProofOfPossession(credentialRequest, nonce)) { + return CredentialRequestValidationResult( + false, + CredentialErrorCode.invalid_or_missing_proof, + "Invalid proof of possession" + ) + } + val supportedCredentialFormats = openIDProviderMetadata.credentialConfigurationsSupported?.values?.map { it.format }?.toSet() ?: setOf() + if (!supportedCredentialFormats.contains(credentialRequest.format)) + return CredentialRequestValidationResult( + false, + CredentialErrorCode.unsupported_credential_format, + "Credential format not supported" + ) + + return CredentialRequestValidationResult(true) + } + + suspend fun generateDeferredCredentialToken(sessionId: String, issuer: String, credentialId: String, tokenKey: Key): String { + return OpenID4VC.generateToken(sessionId, issuer, TokenTarget.DEFERRED_CREDENTIAL, credentialId, tokenKey) + } + + suspend fun generateSdJwtVC(credentialRequest: CredentialRequest, + credentialData: JsonObject, dataMapping: JsonObject?, + selectiveDisclosure: SDMap?, vct: String, + issuerDid: String?, issuerKid: String?, x5Chain: List?, + issuerKey: Key): SDJwtVC { + val proofHeader = credentialRequest.proof?.jwt?.let { JwtUtils.parseJWTHeader(it) } ?: throw CredentialError( + credentialRequest, CredentialErrorCode.invalid_or_missing_proof, message = "Proof must be JWT proof" + ) + val holderKid = proofHeader[JWTClaims.Header.keyID]?.jsonPrimitive?.content + val holderKey = proofHeader[JWTClaims.Header.jwk]?.jsonObject + if (holderKey.isNullOrEmpty() && holderKid.isNullOrEmpty()) throw CredentialError( + credentialRequest, + CredentialErrorCode.invalid_or_missing_proof, + message = "Proof JWT header must contain kid or jwk claim" + ) + val holderDid = if (!holderKid.isNullOrEmpty() && DidUtils.isDidUrl(holderKid)) holderKid.substringBefore("#") else null + val holderKeyJWK = JWKKey.importJWK(holderKey.toString()).getOrNull()?.exportJWKObject()?.plus("kid" to JWKKey.importJWK(holderKey.toString()).getOrThrow().getKeyId())?.toJsonObject() + + val sdPayload = SDPayload.createSDPayload( + credentialData.mergeSDJwtVCPayloadWithMapping( + mapping = dataMapping ?: JsonObject(emptyMap()), + context = mapOf( + "issuerDid" to issuerDid, + "subjectDid" to holderDid + ).filterValues { !it.isNullOrEmpty() }.mapValues { JsonPrimitive(it.value) }, + dataFunctions + ), + selectiveDisclosure ?: SDMap(mapOf()) + ) + val cnf = holderDid?.let { buildJsonObject { put("kid", holderDid) } } ?: + holderKeyJWK?.let { buildJsonObject { put("jwk", holderKeyJWK) } } + ?: throw IllegalArgumentException("Either holderKey or holderDid must be given") + + val defaultPayloadProperties = defaultPayloadProperties( + issuerDid ?: issuerKid ?: issuerKey.getKeyId(), + cnf, vct, null, null, null, null) + val undisclosedPayload = sdPayload.undisclosedPayload.plus(defaultPayloadProperties).let { JsonObject(it) } + val fullPayload = sdPayload.fullPayload.plus(defaultPayloadProperties).let { JsonObject(it) } + + val headers = mapOf( + "kid" to issuerKid, + "typ" to SD_JWT_VC_TYPE_HEADER + ).plus(x5Chain?.let { + mapOf("x5c" to JsonArray(it.map { cert -> cert.toJsonElement() })) + } ?: mapOf()) + + val finalSdPayload = SDPayload.createSDPayload(fullPayload, undisclosedPayload) + + val jwt = issuerKey.signJws(finalSdPayload.undisclosedPayload.toString().encodeToByteArray(), + headers.mapValues { it.value.toJsonElement() }) + return SDJwtVC(SDJwt.createFromSignedJwt(jwt, finalSdPayload)) + } + } diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/data/OpenIDProviderMetadata.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/data/OpenIDProviderMetadata.kt index e4057f6f5..ddcda332d 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/data/OpenIDProviderMetadata.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/data/OpenIDProviderMetadata.kt @@ -126,6 +126,23 @@ data class OpenIDProviderMetadata @OptIn(ExperimentalSerializationApi::class) co override fun fromJSON(jsonObject: JsonObject): OpenIDProviderMetadata = Json.decodeFromJsonElement(OpenIDProviderMetadataSerializer, jsonObject) } + + fun getVctByCredentialConfigurationId(credentialConfigurationId: String) = credentialConfigurationsSupported?.get(credentialConfigurationId)?.vct + + fun getVctBySupportedCredentialConfiguration( + baseUrl: String, + credType: String + ): CredentialSupported { + val expectedVct = "$baseUrl/$credType" + + credentialConfigurationsSupported?.entries?.forEach { entry -> + if (getVctByCredentialConfigurationId(entry.key) == expectedVct) { + return entry.value + } + } + + throw IllegalArgumentException("Invalid type value: $credType. The $credType type is not supported") + } } object OpenIDProviderMetadataSerializer : diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/errors/TokenVerificationError.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/errors/TokenVerificationError.kt new file mode 100644 index 000000000..09e61920d --- /dev/null +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/errors/TokenVerificationError.kt @@ -0,0 +1,6 @@ +package id.walt.oid4vc.errors + +import id.walt.oid4vc.providers.TokenTarget + +class TokenVerificationError(val token: String, val target: TokenTarget, override val message: String? = null) : + Exception() diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDCredentialIssuer.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDCredentialIssuer.kt index a5f983d37..71420c218 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDCredentialIssuer.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDCredentialIssuer.kt @@ -6,6 +6,7 @@ import id.walt.mdoc.cose.COSESign1 import id.walt.mdoc.dataelement.ByteStringElement import id.walt.mdoc.dataelement.MapKey import id.walt.mdoc.dataelement.StringElement +import id.walt.oid4vc.OpenID4VCI import id.walt.oid4vc.data.* import id.walt.oid4vc.definitions.CROSS_DEVICE_CREDENTIAL_OFFER_URL import id.walt.oid4vc.definitions.JWTClaims @@ -328,21 +329,6 @@ abstract class OpenIDCredentialIssuer( } } - protected fun getNonceFromProof(proofOfPossession: ProofOfPossession) = when (proofOfPossession.proofType) { - ProofType.jwt -> parseTokenPayload(proofOfPossession.jwt!!)[JWTClaims.Payload.nonce]?.jsonPrimitive?.content - ProofType.cwt -> Cbor.decodeFromByteArray(proofOfPossession.cwt!!.base64UrlDecode()).decodePayload()?.let { payload -> - payload.value[MapKey(ProofOfPossession.CWTProofBuilder.LABEL_NONCE)].let { - when (it) { - is ByteStringElement -> io.ktor.utils.io.core.String(it.value) - is StringElement -> it.value - else -> throw Error("Invalid nonce type") - } - } - } - - else -> null - } - private fun validateProofOfPossession(credentialRequest: CredentialRequest, nonce: String): Boolean { log.debug { "VALIDATING: ${credentialRequest.proof} with nonce $nonce" } log.debug { "VERIFYING ITS SIGNATURE" } @@ -350,11 +336,11 @@ abstract class OpenIDCredentialIssuer( return when { credentialRequest.proof.isJwtProofType -> verifyTokenSignature( TokenTarget.PROOF_OF_POSSESSION, credentialRequest.proof.jwt!! - ) && getNonceFromProof(credentialRequest.proof) == nonce + ) && OpenID4VCI.getNonceFromProof(credentialRequest.proof) == nonce credentialRequest.proof.isCwtProofType -> verifyCOSESign1Signature( TokenTarget.PROOF_OF_POSSESSION, credentialRequest.proof.cwt!! - ) && getNonceFromProof(credentialRequest.proof) == nonce + ) && OpenID4VCI.getNonceFromProof(credentialRequest.proof) == nonce else -> false } diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDProvider.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDProvider.kt index 91c756733..ce6f71354 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDProvider.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/providers/OpenIDProvider.kt @@ -1,6 +1,8 @@ package id.walt.oid4vc.providers import id.walt.crypto.keys.Key +import id.walt.oid4vc.OpenID4VC +import id.walt.oid4vc.OpenID4VCI import id.walt.oid4vc.data.* import id.walt.oid4vc.data.ResponseType.Companion.getResponseTypeString import id.walt.oid4vc.data.dif.PresentationDefinition @@ -31,28 +33,7 @@ abstract class OpenIDProvider( abstract val metadata: OpenIDProviderMetadata abstract val config: OpenIDProviderConfig - protected open fun createDefaultProviderMetadata() = OpenIDProviderMetadata( - issuer = baseUrl, - authorizationEndpoint = "$baseUrl/authorize", - pushedAuthorizationRequestEndpoint = "$baseUrl/par", - tokenEndpoint = "$baseUrl/token", - credentialEndpoint = "$baseUrl/credential", - batchCredentialEndpoint = "$baseUrl/batch_credential", - deferredCredentialEndpoint = "$baseUrl/credential_deferred", - jwksUri = "$baseUrl/jwks", - grantTypesSupported = setOf(GrantType.authorization_code, GrantType.pre_authorized_code), - requestUriParameterSupported = true, - subjectTypesSupported = setOf(SubjectType.public), - authorizationServer = baseUrl, - credentialIssuer = baseUrl, // (EBSI) this should be just "$baseUrl" https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-11.2.1 - responseTypesSupported = setOf( - "code", - "vp_token", - "id_token" - ), // (EBSI) this is required one https://www.rfc-editor.org/rfc/rfc8414.html#section-2 - idTokenSigningAlgValuesSupported = setOf("ES256"), // (EBSI) https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-self-issued-openid-provider- - codeChallengeMethodsSupported = listOf("S256") - ) + protected open fun createDefaultProviderMetadata() = OpenID4VCI.createDefaultProviderMetadata(baseUrl) fun getCommonProviderMetadataUrl(): String { return URLBuilder(baseUrl).apply { @@ -344,7 +325,7 @@ abstract class OpenIDProvider( } fun getPushedAuthorizationSuccessResponse(authorizationSession: S) = PushedAuthorizationResponse.success( - requestUri = "urn:ietf:params:oauth:request_uri:${authorizationSession.id}", + requestUri = "${OpenID4VC.PUSHED_AUTHORIZATION_REQUEST_URI_PREFIX}${authorizationSession.id}", expiresIn = authorizationSession.expirationTimestamp - Clock.System.now() ) @@ -358,7 +339,7 @@ abstract class OpenIDProvider( fun getPushedAuthorizationSession(authorizationRequest: AuthorizationRequest): S { val session = authorizationRequest.requestUri?.let { getVerifiedSession( - it.substringAfter("urn:ietf:params:oauth:request_uri:") + it.substringAfter(OpenID4VC.PUSHED_AUTHORIZATION_REQUEST_URI_PREFIX) ) ?: throw AuthorizationError( authorizationRequest, AuthorizationErrorCode.invalid_request, diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/requests/AuthorizationRequest.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/requests/AuthorizationRequest.kt index 893591f21..6027c8dca 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/requests/AuthorizationRequest.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/requests/AuthorizationRequest.kt @@ -1,5 +1,6 @@ package id.walt.oid4vc.requests +import id.walt.oid4vc.OpenID4VC import id.walt.oid4vc.data.* import id.walt.oid4vc.data.dif.PresentationDefinition import id.walt.oid4vc.util.JwtUtils @@ -52,7 +53,7 @@ data class AuthorizationRequest( val idTokenHint: String? = null, override val customParameters: Map> = mapOf() ) : HTTPDataObject(), IAuthorizationRequest { - val isReferenceToPAR get() = requestUri?.startsWith("urn:ietf:params:oauth:request_uri:") ?: false + val isReferenceToPAR get() = requestUri?.startsWith(OpenID4VC.PUSHED_AUTHORIZATION_REQUEST_URI_PREFIX) ?: false override fun toHttpParameters(): Map> { return buildMap { put("response_type", listOf(ResponseType.getResponseTypeString(responseType))) diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.kt new file mode 100644 index 000000000..addb25a44 --- /dev/null +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.kt @@ -0,0 +1,7 @@ +package id.walt.oid4vc.util + +import id.walt.oid4vc.providers.TokenTarget + +expect object COSESign1Utils { + fun verifyCOSESign1Signature(target: TokenTarget, token: String): Boolean +} diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/jsMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.js.kt b/waltid-libraries/protocols/waltid-openid4vc/src/jsMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.js.kt new file mode 100644 index 000000000..af7c75b1c --- /dev/null +++ b/waltid-libraries/protocols/waltid-openid4vc/src/jsMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.js.kt @@ -0,0 +1,12 @@ +package id.walt.oid4vc.util + +import id.walt.oid4vc.providers.TokenTarget + +actual object COSESign1Utils { + actual fun verifyCOSESign1Signature( + target: TokenTarget, + token: String + ): Boolean { + TODO("Not yet implemented") + } +} diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/jvmMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.jvm.kt b/waltid-libraries/protocols/waltid-openid4vc/src/jvmMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.jvm.kt new file mode 100644 index 000000000..8d490d877 --- /dev/null +++ b/waltid-libraries/protocols/waltid-openid4vc/src/jvmMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.jvm.kt @@ -0,0 +1,50 @@ +package id.walt.oid4vc.util + +import COSE.AlgorithmID +import COSE.OneKey +import cbor.Cbor +import com.nimbusds.jose.util.X509CertUtils +import com.upokecenter.cbor.CBORObject +import id.walt.crypto.utils.Base64Utils.base64UrlDecode +import id.walt.mdoc.COSECryptoProviderKeyInfo +import id.walt.mdoc.SimpleCOSECryptoProvider +import id.walt.mdoc.cose.COSESign1 +import id.walt.mdoc.dataelement.ByteStringElement +import id.walt.mdoc.dataelement.ListElement +import id.walt.mdoc.dataelement.MapKey +import id.walt.oid4vc.data.ProofOfPossession +import id.walt.oid4vc.providers.TokenTarget +import kotlinx.serialization.decodeFromByteArray + +actual object COSESign1Utils { + actual fun verifyCOSESign1Signature(target: TokenTarget, token: String): Boolean { + // May not be required anymore (removed from https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#cwt-proof-type) + println("Verifying JWS: $token") + println("JWS Verification: target: $target") + val coseSign1 = Cbor.decodeFromByteArray(token.base64UrlDecode()) + val keyInfo = extractHolderKey(coseSign1) + val cryptoProvider = SimpleCOSECryptoProvider(listOf(keyInfo)) + return cryptoProvider.verify1(coseSign1, "pub-key") + } + + fun extractHolderKey(coseSign1: COSESign1): COSECryptoProviderKeyInfo { + val tokenHeader = coseSign1.decodeProtectedHeader() + return if (tokenHeader.value.containsKey(MapKey(ProofOfPossession.CWTProofBuilder.HEADER_LABEL_COSE_KEY))) { + val rawKey = (tokenHeader.value[MapKey(ProofOfPossession.CWTProofBuilder.HEADER_LABEL_COSE_KEY)] as ByteStringElement).value + COSECryptoProviderKeyInfo( + "pub-key", AlgorithmID.ECDSA_256, + OneKey(CBORObject.DecodeFromBytes(rawKey)).AsPublicKey() + ) + } else { + val x5c = tokenHeader.value[MapKey(ProofOfPossession.CWTProofBuilder.HEADER_LABEL_X5CHAIN)] + val x5Chain = when (x5c) { + is ListElement -> x5c.value.map { X509CertUtils.parse((it as ByteStringElement).value) } + else -> listOf(X509CertUtils.parse((x5c as ByteStringElement).value)) + } + COSECryptoProviderKeyInfo( + "pub-key", AlgorithmID.ECDSA_256, + x5Chain.first().publicKey, x5Chain = x5Chain + ) + } + } +} diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/OpenID4VCI_Test.kt b/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/OpenID4VCI_Test.kt new file mode 100644 index 000000000..763fa82bb --- /dev/null +++ b/waltid-libraries/protocols/waltid-openid4vc/src/jvmTest/kotlin/id/walt/oid4vc/OpenID4VCI_Test.kt @@ -0,0 +1,708 @@ +package id.walt.oid4vc + +import id.walt.credentials.CredentialBuilder +import id.walt.credentials.CredentialBuilderType +import id.walt.credentials.issuance.Issuer.baseIssue +import id.walt.crypto.keys.KeyType +import id.walt.crypto.keys.jwk.JWKKey +import id.walt.did.dids.DidService +import id.walt.oid4vc.data.* +import id.walt.oid4vc.data.dif.PresentationDefinition +import id.walt.oid4vc.definitions.JWTClaims +import id.walt.oid4vc.providers.TokenTarget +import id.walt.oid4vc.requests.AuthorizationRequest +import id.walt.oid4vc.requests.CredentialRequest +import id.walt.oid4vc.requests.TokenRequest +import id.walt.oid4vc.responses.AuthorizationCodeResponse +import id.walt.oid4vc.responses.CredentialResponse +import id.walt.oid4vc.responses.TokenResponse +import id.walt.oid4vc.util.JwtUtils +import id.walt.policies.policies.JwtSignaturePolicy +import id.walt.sdjwt.SDJwt +import io.ktor.http.* +import io.ktor.util.reflect.* +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Clock +import kotlinx.serialization.json.* +import org.junit.jupiter.api.BeforeAll +import kotlin.test.* + +class OpenID4VCI_Test { + val ISSUER_BASE_URL = "https://test" + val ISSUER_METADATA = OpenID4VCI.createDefaultProviderMetadata(ISSUER_BASE_URL).copy( + credentialConfigurationsSupported = mapOf( + "VerifiableId" to CredentialSupported( + CredentialFormat.jwt_vc_json, + cryptographicBindingMethodsSupported = setOf("did"), + credentialSigningAlgValuesSupported = setOf("ES256K"), + credentialDefinition = CredentialDefinition(type = listOf("VerifiableCredential", "VerifiableId")), + customParameters = mapOf("foo" to JsonPrimitive("bar")) + ), + "VerifiableDiploma" to CredentialSupported( + CredentialFormat.jwt_vc_json, + cryptographicBindingMethodsSupported = setOf("did"), + credentialSigningAlgValuesSupported = setOf("ES256K"), + credentialDefinition = CredentialDefinition(type = listOf("VerifiableCredential", "VerifiableAttestation", "VerifiableDiploma")) + ) + ) + ) + companion object { + @BeforeAll + @JvmStatic + fun init() = runTest { + DidService.minimalInit() + assertContains(DidService.registrarMethods.keys, "jwk") + } + } + + val ISSUER_TOKEN_KEY = runBlocking { JWKKey.generate(KeyType.RSA) } + val ISSUER_DID_KEY = runBlocking { JWKKey.generate(KeyType.Ed25519) } + val ISSUER_DID = runBlocking { DidService.registerByKey("jwk", ISSUER_DID_KEY).did } + val WALLET_CLIENT_ID = "test-client" + val WALLET_REDIRECT_URI = "http://blank" + val WALLET_KEY = + "{\"kty\":\"EC\",\"d\":\"uD-uxub011cplvr5Bd6MrIPSEUBsgLk-C1y3tnmfetQ\",\"use\":\"sig\",\"crv\":\"secp256k1\",\"kid\":\"48d8a34263cf492aa7ff61b6183e8bcf\",\"x\":\"TKaQ6sCocTDsmuj9tTR996tFXpEcS2EJN-1gOadaBvk\",\"y\":\"0TrIYHcfC93VpEuvj-HXTnyKt0snayOMwGSJA1XiDX8\"}" + val WALLET_DID = "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6IjQ4ZDhhMzQyNjNjZjQ5MmFhN2ZmNjFiNjE4M2U4YmNmIiwieCI6IlRLYVE2c0NvY1REc211ajl0VFI5OTZ0RlhwRWNTMkVKTi0xZ09hZGFCdmsiLCJ5IjoiMFRySVlIY2ZDOTNWcEV1dmotSFhUbnlLdDBzbmF5T013R1NKQTFYaURYOCJ9" + + @Test + fun testCredentialIssuanceIsolatedFunctions() = runTest { + // TODO: consider re-implementing CITestProvider, making use of new lib functions + println("// -------- CREDENTIAL ISSUER ----------") + // init credential offer for full authorization code flow + val credOffer = CredentialOffer.Builder(ISSUER_BASE_URL) + .addOfferedCredential("VerifiableId") + .addAuthorizationCodeGrant("test-state") + .build() + val issueReqUrl = OpenID4VCI.getCredentialOfferRequestUrl(credOffer) + + // Show credential offer request as QR code + println(issueReqUrl) + + println("// -------- WALLET ----------") + assertEquals(expected = credOffer.credentialIssuer, actual = ISSUER_METADATA.credentialIssuer) + + println("// resolve offered credentials") + val offeredCredentials = OpenID4VCI.resolveOfferedCredentials(credOffer, ISSUER_METADATA) + println("offeredCredentials: $offeredCredentials") + assertEquals(expected = 1, actual = offeredCredentials.size) + assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) + assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) + val offeredCredential = offeredCredentials.first() + println("offeredCredentials[0]: $offeredCredential") + + println("// go through full authorization code flow to receive offered credential") + println("// auth request (short-cut, without pushed authorization request)") + val authReq = AuthorizationRequest( + setOf(ResponseType.Code), WALLET_CLIENT_ID, + redirectUri = WALLET_REDIRECT_URI, + issuerState = credOffer.grants[GrantType.authorization_code.value]!!.issuerState + ) + println("authReq: $authReq") + + println("// -------- CREDENTIAL ISSUER ----------") + + // create issuance session and generate authorization code + val authCodeResponse = OpenID4VC.processCodeFlowAuthorization(authReq, authReq.issuerState!!, ISSUER_METADATA, ISSUER_TOKEN_KEY) + //val authCodeResponse: AuthorizationCodeResponse = AuthorizationCodeResponse.success("test-code") + val redirectUri = authCodeResponse.toRedirectUri(authReq.redirectUri ?: TODO(), authReq.responseMode ?: ResponseMode.query) + Url(redirectUri).let { + assertContains(iterable = it.parameters.names(), element = ResponseType.Code.name.lowercase()) + assertEquals( + expected = authCodeResponse.code, + actual = it.parameters[ResponseType.Code.name.lowercase()] + ) + } + + println("// -------- WALLET ----------") + println("// token req") + val tokenReq = + TokenRequest( + GrantType.authorization_code, + WALLET_CLIENT_ID, + code = authCodeResponse.code!! + ) + println("tokenReq: $tokenReq") + + println("// -------- CREDENTIAL ISSUER ----------") + + // TODO: Validate authorization code + // TODO: generate access token + val accessToken = ISSUER_TOKEN_KEY.signJws( + buildJsonObject { + put(JWTClaims.Payload.subject, "test-issuance-session") + put(JWTClaims.Payload.issuer, ISSUER_BASE_URL) + put(JWTClaims.Payload.audience, TokenTarget.ACCESS.name) + put(JWTClaims.Payload.jwtID, "token-id") + }.toString().toByteArray()) + val cNonce = "pop-nonce" + val tokenResponse: TokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cNonce) + + println("// -------- WALLET ----------") + assertTrue(actual = tokenResponse.isSuccess) + assertNotNull(actual = tokenResponse.accessToken) + assertNotNull(actual = tokenResponse.cNonce) + + println("// receive credential") + val nonce = tokenResponse.cNonce!! + val holderDid = WALLET_DID + val holderKey = JWKKey.importJWK(WALLET_KEY).getOrThrow() + val holderKeyId = holderKey.getKeyId() + val proofKeyId = "$holderDid#$holderKeyId" + val proofOfPossession = + ProofOfPossession.JWTProofBuilder(ISSUER_BASE_URL, WALLET_CLIENT_ID, nonce, proofKeyId).build(holderKey) + + val credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) + println("credReq: $credReq") + + println("// -------- CREDENTIAL ISSUER ----------") + val parsedHolderKeyId = credReq.proof?.jwt?.let { JwtUtils.parseJWTHeader(it) }?.get("kid")?.jsonPrimitive?.content + assertNotNull(actual = parsedHolderKeyId) + assertTrue(actual = parsedHolderKeyId.startsWith("did:")) + val parsedHolderDid = parsedHolderKeyId.substringBefore("#") + val resolvedKeyForHolderDid = DidService.resolveToKey(parsedHolderDid).getOrThrow() + + val validPoP = credReq.proof?.validateJwtProof(resolvedKeyForHolderDid, ISSUER_BASE_URL,WALLET_CLIENT_ID, nonce, parsedHolderKeyId) + assertTrue(actual = validPoP!!) + + val generatedCredential: JsonElement = runBlocking { + CredentialBuilder(CredentialBuilderType.W3CV2CredentialBuilder).apply { + type = credReq.credentialDefinition?.type ?: listOf("VerifiableCredential") + issuerDid = ISSUER_DID + subjectDid = parsedHolderKeyId + }.buildW3C().baseIssue(ISSUER_DID_KEY, ISSUER_DID, parsedHolderKeyId, mapOf(), mapOf(), mapOf(), mapOf()) + }.let { JsonPrimitive(it) } + + assertNotNull(generatedCredential) + val credentialResponse: CredentialResponse = CredentialResponse.success(credReq.format, generatedCredential) + + println("// -------- WALLET ----------") + assertTrue(actual = credentialResponse.isSuccess) + assertFalse(actual = credentialResponse.isDeferred) + assertEquals(expected = CredentialFormat.jwt_vc_json, actual = credentialResponse.format!!) + assertTrue(actual = credentialResponse.credential!!.instanceOf(JsonPrimitive::class)) + + println("// parse and verify credential") + val credential = credentialResponse.credential!!.jsonPrimitive.content + println(">>> Issued credential: $credential") + verifyIssuerAndSubjectId( + SDJwt.parse(credential).fullPayload["vc"]?.jsonObject!!, + ISSUER_DID, WALLET_DID + ) + assertTrue(actual = JwtSignaturePolicy().verify(credential, null, mapOf()).isSuccess) + } + + // Test case for available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PRE_AUTHORIZED PWD(Handled by third party authorization server) + //@Test + fun testCredentialIssuanceIsolatedFunctionsAuthCodeFlow() = runTest { + // TODO: consider re-implementing CITestProvider, making use of new lib functions + // is it ok to generate the credential offer using the ciTestProvider (OpenIDCredentialIssuer class) ? + val issuedCredentialId = "VerifiableId" + + println("// -------- CREDENTIAL ISSUER ----------") + // Init credential offer for full authorization code flow + + // Issuer Client stores the authentication method in session. + // Available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PWD(Handled by third party authorization server), PRE_AUTHORIZED. The response for each method is a redirect to the proper location. + println("// --Authentication method is NONE--") + var issuerState = "test-state-none-auth" + var credOffer = CredentialOffer.Builder(ISSUER_BASE_URL) + .addOfferedCredential(issuedCredentialId) + .addAuthorizationCodeGrant(issuerState) + .build() + + // Issuer Client shows credential offer request as QR code + println(OpenID4VCI.getCredentialOfferRequestUrl(credOffer)) + + println("// -------- WALLET ----------") + var providerMetadata = ISSUER_METADATA + assertEquals(expected = credOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) + + println("// resolve offered credentials") + var offeredCredentials = OpenID4VCI.resolveOfferedCredentials(credOffer, providerMetadata) + println("offeredCredentials: $offeredCredentials") + assertEquals(expected = 1, actual = offeredCredentials.size) + assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) + assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) + var offeredCredential = offeredCredentials.first() + println("offeredCredentials[0]: $offeredCredential") + + + println("// go through authorization code flow to receive offered credential") + println("// auth request (short-cut, without pushed authorization request)") + var authReqWalletState = "secured_state" + var authReqWallet = AuthorizationRequest( + setOf(ResponseType.Code), WALLET_CLIENT_ID, + redirectUri = WALLET_REDIRECT_URI, + issuerState = credOffer.grants[GrantType.authorization_code.value]!!.issuerState, + state = authReqWalletState + ) + println("authReq: $authReqWallet") + + // Wallet client calls /authorize endpoint + + + println("// -------- CREDENTIAL ISSUER ----------") + var authReq = OpenID4VCI.validateAuthorizationRequestQueryString(authReqWallet.toHttpQueryString()) + + // Issuer Client retrieves issuance session based on issuer state and stores the credential request, including authReqWallet state + // Available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PWD(Handled by third party authorization server). The response for each method is a redirect to the proper location. + // Issuer Client checks the authentication method of the session + + println("// --Authentication method is NONE--") + // Issuer Client generates authorization code + var authorizationCode = "secured_code" + var authCodeResponse: AuthorizationCodeResponse = AuthorizationCodeResponse.success(authorizationCode, mapOf("state" to listOf(authReqWallet.state!!))) + var redirectUri = testCredentialIssuanceIsolatedFunctionsAuthCodeFlowRedirectWithCode(authCodeResponse, authReqWallet) + + // Issuer client redirects the request to redirectUri + + println("// -------- WALLET ----------") + println("// token req") + var tokenReq = + TokenRequest( + GrantType.authorization_code, + WALLET_CLIENT_ID, + code = authCodeResponse.code!! + ) + println("tokenReq: $tokenReq") + + + println("// -------- CREDENTIAL ISSUER ----------") + // Validate token request against authorization code + OpenID4VCI.validateTokenRequestRaw(tokenReq.toHttpParameters(), authorizationCode) + + // Generate Access Token + var expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds + + var accessToken = OpenID4VCI.signToken( + privateKey = ISSUER_TOKEN_KEY, + payload = buildJsonObject { + put(JWTClaims.Payload.audience, ISSUER_BASE_URL) + put(JWTClaims.Payload.subject, authReq.clientId) + put(JWTClaims.Payload.issuer, ISSUER_BASE_URL) + put(JWTClaims.Payload.expirationTime, expirationTime) + put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) + } + ) + + // Issuer client creates cPoPnonce + var cPoPNonce = "secured_cPoPnonce" + var tokenResponse: TokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) + + // Issuer client sends successful response with tokenResponse + + + println("// -------- WALLET ----------") + assertTrue(actual = tokenResponse.isSuccess) + assertNotNull(actual = tokenResponse.accessToken) + assertNotNull(actual = tokenResponse.cNonce) + + println("// receive credential") + var nonce = tokenResponse.cNonce!! + val holderDid = WALLET_DID + val holderKey = JWKKey.importJWK(WALLET_KEY).getOrThrow() + val holderKeyId = holderKey.getKeyId() + val proofKeyId = "$holderDid#$holderKeyId" + var proofOfPossession = + ProofOfPossession.JWTProofBuilder(ISSUER_BASE_URL, null, nonce, proofKeyId).build(holderKey) + + var credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) + println("credReq: $credReq") + + println("// -------- CREDENTIAL ISSUER ----------") + // Issuer Client extracts Access Token from header + OpenID4VCI.verifyToken(tokenResponse.accessToken.toString(), ISSUER_TOKEN_KEY.getPublicKey()) + + //Then VC Stuff + + // ---------------------------------- + // Authentication Method is ID_TOKEN + // ---------------------------------- + println("// --Authentication method is ID_TOKEN--") + issuerState = "test-state-idtoken-auth" + credOffer = CredentialOffer.fromJSONString(testIsolatedFunctionsCreateCredentialOffer(ISSUER_BASE_URL, issuerState, issuedCredentialId)) + + // Issuer Client shows credential offer request as QR code + println(OpenID4VCI.getCredentialOfferRequestUrl(credOffer)) + + println("// -------- WALLET ----------") + //parsedCredOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) + //providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) + assertEquals(expected = credOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) + + println("// resolve offered credentials") + offeredCredentials = OpenID4VCI.resolveOfferedCredentials(credOffer, providerMetadata) + println("offeredCredentials: $offeredCredentials") + assertEquals(expected = 1, actual = offeredCredentials.size) + assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) + assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) + offeredCredential = offeredCredentials.first() + println("offeredCredentials[0]: $offeredCredential") + + + authReqWalletState = "secured_state_idtoken" + authReqWallet = AuthorizationRequest( + setOf(ResponseType.Code), WALLET_CLIENT_ID, + redirectUri = WALLET_REDIRECT_URI, + issuerState = credOffer.grants[GrantType.authorization_code.value]!!.issuerState, + state = authReqWalletState + ) + println("authReq: $authReqWallet") + + // Wallet client calls /authorize endpoint + + + println("// -------- CREDENTIAL ISSUER ----------") + authReq = OpenID4VCI.validateAuthorizationRequestQueryString(authReqWallet.toHttpQueryString()) + + // Issuer Client retrieves issuance session based on issuer state and stores the credential request, including authReqWallet state + // Available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PWD(Handled by third party authorization server). The response for each method is a redirect to the proper location. + // Issuer Client checks the authentication method of the session + + // Issuer Client generates authorization code + // Issuer client creates state and nonce for the id token authorization request + var authReqIssuerState = "secured_state_issuer_idtoken" + var authReqIssuerNonce = "secured_nonce_issue_idtoken" + + var authReqIssuer = OpenID4VCI.generateAuthorizationRequest(authReq, ISSUER_BASE_URL, ISSUER_TOKEN_KEY, ResponseType.IdToken, authReqIssuerState, authReqIssuerNonce) + + // Redirect uri is located in the client_metadata.authorization_endpoint or "openid://" + var redirectUriReq = authReqIssuer.toRedirectUri(authReq.clientMetadata?.customParameters?.get("authorization_endpoint")?.jsonPrimitive?.content ?: "openid://", authReq.responseMode ?: ResponseMode.query) + Url(redirectUriReq).let { + assertContains(iterable = it.parameters.names(), element = "request") + assertContains(iterable = it.parameters.names(), element = "redirect_uri") + assertEquals(expected = ISSUER_BASE_URL + "/direct_post", actual = it.parameters["redirect_uri"]) + } + + // Issuer Client redirects the request to redirectUri + + + println("// -------- WALLET ----------") + // wallet creates id token + val idToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDprZXk6ejJkbXpEODFjZ1B4OFZraTdKYnV1TW1GWXJXUGdZb3l0eWtVWjNleXFodDFqOUtib2o3ZzlQZlhKeGJiczRLWWVneXI3RUxuRlZucERNemJKSkRETlpqYXZYNmp2dERtQUxNYlhBR1c2N3BkVGdGZWEyRnJHR1NGczhFanhpOTZvRkxHSGNMNFA2YmpMRFBCSkV2UlJIU3JHNExzUG5lNTJmY3p0Mk1XakhMTEpCdmhBQyN6MmRtekQ4MWNnUHg4VmtpN0pidXVNbUZZcldQZ1lveXR5a1VaM2V5cWh0MWo5S2JvajdnOVBmWEp4YmJzNEtZZWd5cjdFTG5GVm5wRE16YkpKREROWmphdlg2anZ0RG1BTE1iWEFHVzY3cGRUZ0ZlYTJGckdHU0ZzOEVqeGk5Nm9GTEdIY0w0UDZiakxEUEJKRXZSUkhTckc0THNQbmU1MmZjenQyTVdqSExMSkJ2aEFDIn0.eyJub25jZSI6ImE4YWE1NDYwLTRmN2UtNDRmNy05ZGE3LWU1NmQ0YjIxMWE1MSIsInN1YiI6ImRpZDprZXk6ejJkbXpEODFjZ1B4OFZraTdKYnV1TW1GWXJXUGdZb3l0eWtVWjNleXFodDFqOUtib2o3ZzlQZlhKeGJiczRLWWVneXI3RUxuRlZucERNemJKSkRETlpqYXZYNmp2dERtQUxNYlhBR1c2N3BkVGdGZWEyRnJHR1NGczhFanhpOTZvRkxHSGNMNFA2YmpMRFBCSkV2UlJIU3JHNExzUG5lNTJmY3p0Mk1XakhMTEpCdmhBQyIsImlzcyI6ImRpZDprZXk6ejJkbXpEODFjZ1B4OFZraTdKYnV1TW1GWXJXUGdZb3l0eWtVWjNleXFodDFqOUtib2o3ZzlQZlhKeGJiczRLWWVneXI3RUxuRlZucERNemJKSkRETlpqYXZYNmp2dERtQUxNYlhBR1c2N3BkVGdGZWEyRnJHR1NGczhFanhpOTZvRkxHSGNMNFA2YmpMRFBCSkV2UlJIU3JHNExzUG5lNTJmY3p0Mk1XakhMTEpCdmhBQyIsImF1ZCI6Imh0dHBzOi8vMDFiYi01LTIwMy0xNzQtNjcubmdyb2stZnJlZS5hcHAiLCJpYXQiOjE3MjExNDQ3MzYsImV4cCI6MTcyMTE0NTAzNn0.VPWyLkMQAlcc40WCNSRH-Vxaj4LHi-wf2P9kcEKDvcdyVec2xJIwkg0JF4INMbLCkF0Y89lT0oswALd345wdUg" + // wallet calls POST /direct_post (e.g. redirect_uri of Issuer Auth Req) providing the id_token + + + println("// -------- CREDENTIAL ISSUER ----------") + // Create validateIdTokenResponse() + val idTokenPayload = OpenID4VCI.validateAuthorizationRequestToken(idToken) + + // Issuer Client validates states and nonces based on idTokenPayload + + // Issuer client generates authorization code + authorizationCode = "secured_code_idtoken" + authCodeResponse = AuthorizationCodeResponse.success(authorizationCode, mapOf("state" to listOf(authReqWallet.state!!))) + + redirectUri = authCodeResponse.toRedirectUri(authReq.redirectUri ?: TODO(), authReq.responseMode ?: ResponseMode.query) + Url(redirectUri).let { + assertContains(iterable = it.parameters.names(), element = ResponseType.Code.name.lowercase()) + assertEquals( + expected = authCodeResponse.code, + actual = it.parameters[ResponseType.Code.name.lowercase()] + ) + } + + + println("// -------- WALLET ----------") + println("// token req") + tokenReq = + TokenRequest( + GrantType.authorization_code, + WALLET_CLIENT_ID, + code = authCodeResponse.code!! + ) + println("tokenReq: $tokenReq") + + + println("// -------- CREDENTIAL ISSUER ----------") + // Validate token request against authorization code + OpenID4VCI.validateTokenRequestRaw(tokenReq.toHttpParameters(), authorizationCode) + + // Generate Access Token + expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds + + accessToken = OpenID4VCI.signToken( + privateKey = ISSUER_TOKEN_KEY, + payload = buildJsonObject { + put(JWTClaims.Payload.audience, ISSUER_BASE_URL) + put(JWTClaims.Payload.subject, authReq.clientId) + put(JWTClaims.Payload.issuer, ISSUER_BASE_URL) + put(JWTClaims.Payload.expirationTime, expirationTime) + put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) + } + ) + + // Issuer client creates cPoPnonce + cPoPNonce = "secured_cPoPnonce_idtoken" + tokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) + + // Issuer client sends successful response with tokenResponse + + println("// -------- WALLET ----------") + assertTrue(actual = tokenResponse.isSuccess) + assertNotNull(actual = tokenResponse.accessToken) + assertNotNull(actual = tokenResponse.cNonce) + + println("// receive credential") + nonce = tokenResponse.cNonce!! + proofOfPossession = ProofOfPossession.JWTProofBuilder(ISSUER_BASE_URL, null, nonce, proofKeyId).build(holderKey) + + credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) + println("credReq: $credReq") + + println("// -------- CREDENTIAL ISSUER ----------") + // Issuer Client extracts Access Token from header + OpenID4VCI.verifyToken(tokenResponse.accessToken.toString(), ISSUER_TOKEN_KEY.getPublicKey()) + + //Then VC Stuff + + + + // ---------------------------------- + // Authentication Method is VP_TOKEN + // ---------------------------------- + println("// --Authentication method is VP_TOKEN--") + issuerState = "test-state-vptoken-auth" + credOffer = CredentialOffer.Builder(ISSUER_BASE_URL) + .addOfferedCredential(issuedCredentialId) + .addAuthorizationCodeGrant(issuerState) + .build() + + // Issuer Client shows credential offer request as QR code + println(OpenID4VCI.getCredentialOfferRequestUrl(credOffer)) + + println("// -------- WALLET ----------") + //parsedCredOffer = OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) + //providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) + assertEquals(expected = credOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) + + println("// resolve offered credentials") + offeredCredentials = OpenID4VCI.resolveOfferedCredentials(credOffer, providerMetadata) + println("offeredCredentials: $offeredCredentials") + assertEquals(expected = 1, actual = offeredCredentials.size) + assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) + assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) + offeredCredential = offeredCredentials.first() + println("offeredCredentials[0]: $offeredCredential") + + + authReqWalletState = "secured_state_vptoken" + authReqWallet = AuthorizationRequest( + setOf(ResponseType.Code), WALLET_CLIENT_ID, + redirectUri = WALLET_REDIRECT_URI, + issuerState = credOffer.grants[GrantType.authorization_code.value]!!.issuerState, + state = authReqWalletState + ) + println("authReq: $authReqWallet") + + // Wallet client calls /authorize endpoint + + + println("// -------- CREDENTIAL ISSUER ----------") + authReq = OpenID4VCI.validateAuthorizationRequestQueryString(authReqWallet.toHttpQueryString()) + + // Issuer Client retrieves issuance session based on issuer state and stores the credential request, including authReqWallet state + // Available authentication methods are: NONE, ID_TOKEN, VP_TOKEN, PWD(Handled by third party authorization server). The response for each method is a redirect to the proper location. + val requestedCredentialId = "OpenBadgeCredential" + val vpProfile = OpenId4VPProfile.EBSIV3 + val requestCredentialsArr = buildJsonArray { add(requestedCredentialId) } + val requestedTypes = requestCredentialsArr.map { + when (it) { + is JsonPrimitive -> it.contentOrNull + is JsonObject -> it["credential"]?.jsonPrimitive?.contentOrNull + else -> throw IllegalArgumentException("Invalid JSON type for requested credential: $it") + } ?: throw IllegalArgumentException("Invalid VC type for requested credential: $it") + } + + val presentationDefinition = PresentationDefinition.defaultGenerationFromVcTypesForCredentialFormat(requestedTypes, CredentialFormat.jwt_vc) + + // Issuer Client creates state and nonce for the vp_token authorization request + authReqIssuerState = "secured_state_issuer_vptoken" + authReqIssuerNonce = "secured_nonce_issuer_vptoken" + + authReqIssuer = OpenID4VCI.generateAuthorizationRequest(authReq, ISSUER_BASE_URL, ISSUER_TOKEN_KEY, ResponseType.VpToken, authReqIssuerState, authReqIssuerNonce, true, presentationDefinition) + + // Redirect uri is located in the client_metadata.authorization_endpoint or "openid://" + redirectUriReq = authReqIssuer.toRedirectUri(authReq.clientMetadata?.customParameters?.get("authorization_endpoint")?.jsonPrimitive?.content ?: "openid://", authReq.responseMode ?: ResponseMode.query) + Url(redirectUriReq).let { + assertContains(iterable = it.parameters.names(), element = "request") + assertContains(iterable = it.parameters.names(), element = "redirect_uri") + assertContains(iterable = it.parameters.names(), element = "presentation_definition") + assertEquals(expected = ISSUER_BASE_URL + "/direct_post", actual = it.parameters["redirect_uri"]) + } + // Issuer Client redirects the request to redirectUri + + + println("// -------- WALLET ----------") + // wallet creates vp token + val vpToken = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa3A3QVZ3dld4bnNORHVTU2JmMTlzZ0t6cngyMjNXWTk1QXFaeUFHaWZGVnlWI3o2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViJ9.eyJzdWIiOiJkaWQ6a2V5Ono2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViIsIm5iZiI6MTcyMDc2NDAxOSwiaWF0IjoxNzIwNzY0MDc5LCJqdGkiOiJ1cm46dXVpZDpiNzE2YThlOC0xNzVlLTRhMTYtODZlMC0xYzU2Zjc4NTFhZDEiLCJpc3MiOiJkaWQ6a2V5Ono2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViIsIm5vbmNlIjoiNDY0YTAwMTUtNzQ1OS00Y2Y4LWJmNjgtNDg0ODQyYTE5Y2FmIiwiYXVkIjoiZGlkOmtleTp6Nk1rcDdBVnd2V3huc05EdVNTYmYxOXNnS3pyeDIyM1dZOTVBcVp5QUdpZkZWeVYiLCJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwiaWQiOiJ1cm46dXVpZDpiNzE2YThlOC0xNzVlLTRhMTYtODZlMC0xYzU2Zjc4NTFhZDEiLCJob2xkZXIiOiJkaWQ6a2V5Ono2TWtwN0FWd3ZXeG5zTkR1U1NiZjE5c2dLenJ4MjIzV1k5NUFxWnlBR2lmRlZ5ViIsInZlcmlmaWFibGVDcmVkZW50aWFsIjpbImV5SmhiR2NpT2lKRlpFUlRRU0lzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW1ScFpEcHJaWGs2ZWpaTmEzQTNRVlozZGxkNGJuTk9SSFZUVTJKbU1UbHpaMHQ2Y25neU1qTlhXVGsxUVhGYWVVRkhhV1pHVm5sV0luMC5leUpwYzNNaU9pSmthV1E2YTJWNU9ubzJUV3R3TjBGV2QzWlhlRzV6VGtSMVUxTmlaakU1YzJkTGVuSjRNakl6VjFrNU5VRnhXbmxCUjJsbVJsWjVWaUlzSW5OMVlpSTZJbVJwWkRwclpYazZlalpOYTJwdE1tZGhSM052WkVkamFHWkhOR3M0VURaTGQwTklXbk5XUlZCYWFHODFWblZGWWxrNU5IRnBRa0k1SWl3aWRtTWlPbnNpUUdOdmJuUmxlSFFpT2xzaWFIUjBjSE02THk5M2QzY3Vkek11YjNKbkx6SXdNVGd2WTNKbFpHVnVkR2xoYkhNdmRqRWlMQ0pvZEhSd2N6b3ZMM0IxY213dWFXMXpaMnh2WW1Gc0xtOXlaeTl6Y0dWakwyOWlMM1l6Y0RBdlkyOXVkR1Y0ZEM1cWMyOXVJbDBzSW1sa0lqb2lkWEp1T25WMWFXUTZNVEl6SWl3aWRIbHdaU0k2V3lKV1pYSnBabWxoWW14bFEzSmxaR1Z1ZEdsaGJDSXNJazl3Wlc1Q1lXUm5aVU55WldSbGJuUnBZV3dpWFN3aWJtRnRaU0k2SWtwR1JpQjRJSFpqTFdWa2RTQlFiSFZuUm1WemRDQXpJRWx1ZEdWeWIzQmxjbUZpYVd4cGRIa2lMQ0pwYzNOMVpYSWlPbnNpZEhsd1pTSTZXeUpRY205bWFXeGxJbDBzSW1sa0lqb2laR2xrT21WNFlXMXdiR1U2TVRJeklpd2libUZ0WlNJNklrcHZZbk1nWm05eUlIUm9aU0JHZFhSMWNtVWdLRXBHUmlraUxDSjFjbXdpT2lKb2RIUndjem92TDNkM2R5NXFabVl1YjNKbkx5SXNJbWx0WVdkbElqcDdJbWxrSWpvaWFIUjBjSE02THk5M00yTXRZMk5uTG1kcGRHaDFZaTVwYnk5Mll5MWxaQzl3YkhWblptVnpkQzB4TFRJd01qSXZhVzFoWjJWekwwcEdSbDlNYjJkdlRHOWphM1Z3TG5CdVp5SXNJblI1Y0dVaU9pSkpiV0ZuWlNKOWZTd2lhWE56ZFdGdVkyVkVZWFJsSWpvaU1qQXlNeTB3TnkweU1GUXdOem93TlRvME5Gb2lMQ0psZUhCcGNtRjBhVzl1UkdGMFpTSTZJakl3TXpNdE1EY3RNakJVTURjNk1EVTZORFJhSWl3aVkzSmxaR1Z1ZEdsaGJGTjFZbXBsWTNRaU9uc2lhV1FpT2lKa2FXUTZaWGhoYlhCc1pUb3hNak1pTENKMGVYQmxJanBiSWtGamFHbGxkbVZ0Wlc1MFUzVmlhbVZqZENKZExDSmhZMmhwWlhabGJXVnVkQ0k2ZXlKcFpDSTZJblZ5YmpwMWRXbGtPbUZqTWpVMFltUTFMVGhtWVdRdE5HSmlNUzA1WkRJNUxXVm1aRGt6T0RVek5qa3lOaUlzSW5SNWNHVWlPbHNpUVdOb2FXVjJaVzFsYm5RaVhTd2libUZ0WlNJNklrcEdSaUI0SUhaakxXVmtkU0JRYkhWblJtVnpkQ0F6SUVsdWRHVnliM0JsY21GaWFXeHBkSGtpTENKa1pYTmpjbWx3ZEdsdmJpSTZJbFJvYVhNZ2QyRnNiR1YwSUhOMWNIQnZjblJ6SUhSb1pTQjFjMlVnYjJZZ1Z6TkRJRlpsY21sbWFXRmliR1VnUTNKbFpHVnVkR2xoYkhNZ1lXNWtJR2hoY3lCa1pXMXZibk4wY21GMFpXUWdhVzUwWlhKdmNHVnlZV0pwYkdsMGVTQmtkWEpwYm1jZ2RHaGxJSEJ5WlhObGJuUmhkR2x2YmlCeVpYRjFaWE4wSUhkdmNtdG1iRzkzSUdSMWNtbHVaeUJLUmtZZ2VDQldReTFGUkZVZ1VHeDFaMFpsYzNRZ015NGlMQ0pqY21sMFpYSnBZU0k2ZXlKMGVYQmxJam9pUTNKcGRHVnlhV0VpTENKdVlYSnlZWFJwZG1VaU9pSlhZV3hzWlhRZ2MyOXNkWFJwYjI1eklIQnliM1pwWkdWeWN5QmxZWEp1WldRZ2RHaHBjeUJpWVdSblpTQmllU0JrWlcxdmJuTjBjbUYwYVc1bklHbHVkR1Z5YjNCbGNtRmlhV3hwZEhrZ1pIVnlhVzVuSUhSb1pTQndjbVZ6Wlc1MFlYUnBiMjRnY21WeGRXVnpkQ0IzYjNKclpteHZkeTRnVkdocGN5QnBibU5zZFdSbGN5QnpkV05qWlhOelpuVnNiSGtnY21WalpXbDJhVzVuSUdFZ2NISmxjMlZ1ZEdGMGFXOXVJSEpsY1hWbGMzUXNJR0ZzYkc5M2FXNW5JSFJvWlNCb2IyeGtaWElnZEc4Z2MyVnNaV04wSUdGMElHeGxZWE4wSUhSM2J5QjBlWEJsY3lCdlppQjJaWEpwWm1saFlteGxJR055WldSbGJuUnBZV3h6SUhSdklHTnlaV0YwWlNCaElIWmxjbWxtYVdGaWJHVWdjSEpsYzJWdWRHRjBhVzl1TENCeVpYUjFjbTVwYm1jZ2RHaGxJSEJ5WlhObGJuUmhkR2x2YmlCMGJ5QjBhR1VnY21WeGRXVnpkRzl5TENCaGJtUWdjR0Z6YzJsdVp5QjJaWEpwWm1sallYUnBiMjRnYjJZZ2RHaGxJSEJ5WlhObGJuUmhkR2x2YmlCaGJtUWdkR2hsSUdsdVkyeDFaR1ZrSUdOeVpXUmxiblJwWVd4ekxpSjlMQ0pwYldGblpTSTZleUpwWkNJNkltaDBkSEJ6T2k4dmR6TmpMV05qWnk1bmFYUm9kV0l1YVc4dmRtTXRaV1F2Y0d4MVoyWmxjM1F0TXkweU1ESXpMMmx0WVdkbGN5OUtSa1l0VmtNdFJVUlZMVkJNVlVkR1JWTlVNeTFpWVdSblpTMXBiV0ZuWlM1d2JtY2lMQ0owZVhCbElqb2lTVzFoWjJVaWZYMTlMQ0pqY21Wa1pXNTBhV0ZzVTJOb1pXMWhJanA3SW1sa0lqb2lhSFIwY0hNNkx5OXdkWEpzTG1sdGMyZHNiMkpoYkM1dmNtY3ZjM0JsWXk5dllpOTJNM0F3TDNOamFHVnRZUzlxYzI5dUwyOWlYM1l6Y0RCZllXTm9hV1YyWlcxbGJuUmpjbVZrWlc1MGFXRnNYM05qYUdWdFlTNXFjMjl1SWl3aWRIbHdaU0k2SWtaMWJHeEtjMjl1VTJOb1pXMWhWbUZzYVdSaGRHOXlNakF5TVNKOWZTd2lhblJwSWpvaWRYSnVPblYxYVdRNk1USXpJaXdpWlhod0lqb3lNREExTkRVMU9UUTBMQ0pwWVhRaU9qRTJPRGs0TXpZM05EUXNJbTVpWmlJNk1UWTRPVGd6TmpjME5IMC5PRHZUQXVMN2JrME1pX3hNLVFualg4azByZ3VUeWtiYzJ6bFdFMVU2SGlmVXFjWTdFVU5GcUdUZWFUWHRESkxrODBuZWN6YkNNTGh1YlZseEFkdl9DdyJdfX0.zTXluOVIP0sQzc5GzNvtVvWRiaC-x9qMZg0d-EvCuRIg7QSgY0hmrfVlAzh2IDEvaXZ1ahM3hSVDx_YI74ToAw" + // wallet calls POST /direct_post (e.g. redirect_uri of Issuer Auth Req) providing the vp_token and presentation submission + + println("// -------- CREDENTIAL ISSUER ----------") + val vpTokenPayload = OpenID4VCI.validateAuthorizationRequestToken(vpToken) + + // Issuer Client validates states and nonces based on vpTokenPayload + + // Issuer client generates authorization code + authorizationCode = "secured_code_vptoken" + authCodeResponse = AuthorizationCodeResponse.success(authorizationCode, mapOf("state" to listOf(authReqWallet.state!!))) + + redirectUri = authCodeResponse.toRedirectUri(authReq.redirectUri ?: TODO(), authReq.responseMode ?: ResponseMode.query) + Url(redirectUri).let { + assertContains(iterable = it.parameters.names(), element = ResponseType.Code.name.lowercase()) + assertEquals( + expected = authCodeResponse.code, + actual = it.parameters[ResponseType.Code.name.lowercase()] + ) + } + + + println("// -------- WALLET ----------") + println("// token req") + tokenReq = + TokenRequest( + GrantType.authorization_code, + WALLET_CLIENT_ID, + code = authCodeResponse.code!! + ) + println("tokenReq: $tokenReq") + + + println("// -------- CREDENTIAL ISSUER ----------") + // Validate token request against authorization code + OpenID4VCI.validateTokenRequestRaw(tokenReq.toHttpParameters(), authorizationCode) + + // Generate Access Token + expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds + + accessToken = OpenID4VCI.signToken( + privateKey = ISSUER_TOKEN_KEY, + payload = buildJsonObject { + put(JWTClaims.Payload.audience, ISSUER_BASE_URL) + put(JWTClaims.Payload.subject, authReq.clientId) + put(JWTClaims.Payload.issuer, ISSUER_BASE_URL) + put(JWTClaims.Payload.expirationTime, expirationTime) + put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) + } + ) + + // Issuer client creates cPoPnonce + cPoPNonce = "secured_cPoPnonce_idtoken" + tokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) + + // Issuer client sends successful response with tokenResponse + + println("// -------- WALLET ----------") + assertTrue(actual = tokenResponse.isSuccess) + assertNotNull(actual = tokenResponse.accessToken) + assertNotNull(actual = tokenResponse.cNonce) + + println("// receive credential") + nonce = tokenResponse.cNonce!! + proofOfPossession = ProofOfPossession.JWTProofBuilder(ISSUER_BASE_URL, null, nonce, proofKeyId).build(holderKey) + + credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) + println("credReq: $credReq") + + println("// -------- CREDENTIAL ISSUER ----------") + // Issuer Client extracts Access Token from header + OpenID4VCI.verifyToken(tokenResponse.accessToken.toString(), ISSUER_TOKEN_KEY.getPublicKey()) + + //Then VC Stuff + + + // ---------------------------------- + // Authentication Method is PRE_AUTHORIZED + // ---------------------------------- + println("// --Authentication method is PRE_AUTHORIZED--") + val preAuthCode = "test-state-pre_auth" + credOffer = CredentialOffer.Builder(ISSUER_BASE_URL) + .addOfferedCredential(issuedCredentialId) + .addPreAuthorizedCodeGrant(preAuthCode) + .build() + + val issueReqUrl = OpenID4VCI.getCredentialOfferRequestUrl(credOffer) + // Issuer Client shows credential offer request as QR code + println(issueReqUrl) + + println("// -------- WALLET ----------") + credOffer = credOffer // OpenID4VCI.parseAndResolveCredentialOfferRequestUrl(issueReqUrl) + //providerMetadata = OpenID4VCI.resolveCIProviderMetadata(parsedCredOffer) + assertEquals(expected = credOffer.credentialIssuer, actual = providerMetadata.credentialIssuer) + + println("// resolve offered credentials") + offeredCredentials = OpenID4VCI.resolveOfferedCredentials(credOffer, providerMetadata) + println("offeredCredentials: $offeredCredentials") + assertEquals(expected = 1, actual = offeredCredentials.size) + assertEquals(expected = CredentialFormat.jwt_vc_json, actual = offeredCredentials.first().format) + assertEquals(expected = "VerifiableId", actual = offeredCredentials.first().credentialDefinition?.type?.last()) + offeredCredential = offeredCredentials.first() + println("offeredCredentials[0]: $offeredCredential") + assertNotNull(actual = credOffer.grants[GrantType.pre_authorized_code.value]?.preAuthorizedCode) + + + println("// token req") + tokenReq = TokenRequest( + grantType = GrantType.pre_authorized_code, + //clientId = testCIClientConfig.clientID, + redirectUri = WALLET_REDIRECT_URI, + preAuthorizedCode = credOffer.grants[GrantType.pre_authorized_code.value]!!.preAuthorizedCode, + txCode = null + ) + + + println("// -------- CREDENTIAL ISSUER ----------") + // Validate token request against authorization code + OpenID4VCI.validateTokenRequestRaw(tokenReq.toHttpParameters(), preAuthCode) + + // Generate Access Token + expirationTime = (Clock.System.now().epochSeconds + 864000L) // ten days in milliseconds + + accessToken = OpenID4VCI.signToken( + privateKey = ISSUER_TOKEN_KEY, + payload = buildJsonObject { + put(JWTClaims.Payload.audience, ISSUER_BASE_URL) + put(JWTClaims.Payload.subject, authReq.clientId) + put(JWTClaims.Payload.issuer, ISSUER_BASE_URL) + put(JWTClaims.Payload.expirationTime, expirationTime) + put(JWTClaims.Payload.notBeforeTime, Clock.System.now().epochSeconds) + } + ) + + // Issuer client creates cPoPnonce + cPoPNonce = "secured_cPoPnonce_preauthorized" + tokenResponse = TokenResponse.success(accessToken, "bearer", cNonce = cPoPNonce, expiresIn = expirationTime) + + // Issuer client sends successful response with tokenResponse + + + println("// -------- WALLET ----------") + assertTrue(actual = tokenResponse.isSuccess) + assertNotNull(actual = tokenResponse.accessToken) + assertNotNull(actual = tokenResponse.cNonce) + + println("// receive credential") + nonce = tokenResponse.cNonce!! + proofOfPossession = ProofOfPossession.JWTProofBuilder(ISSUER_BASE_URL, null, nonce, proofKeyId).build(holderKey) + + credReq = CredentialRequest.forOfferedCredential(offeredCredential, proofOfPossession) + println("credReq: $credReq") + + println("// -------- CREDENTIAL ISSUER ----------") + // Issuer Client extracts Access Token from header + OpenID4VCI.verifyToken(tokenResponse.accessToken.toString(), ISSUER_TOKEN_KEY.getPublicKey()) + + //Then VC Stuff + + + } + + private fun verifyIssuerAndSubjectId(credential: JsonObject, issuerId: String, subjectId: String) { + assertEquals(expected = issuerId, actual = credential["issuer"]?.jsonPrimitive?.contentOrNull) + assertEquals( + expected = subjectId, + actual = credential["credentialSubject"]?.jsonObject?.get("id")?.jsonPrimitive?.contentOrNull?.substringBefore("#") + ) + } +} diff --git a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt index 0823f42e3..3fb1c7f3d 100644 --- a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt +++ b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt @@ -34,19 +34,21 @@ import id.walt.mdoc.dataelement.* import id.walt.mdoc.doc.MDocBuilder import id.walt.mdoc.mso.DeviceKeyInfo import id.walt.mdoc.mso.ValidityInfo +import id.walt.oid4vc.OpenID4VC +import id.walt.oid4vc.OpenID4VCI import id.walt.oid4vc.data.* import id.walt.oid4vc.definitions.JWTClaims +import id.walt.oid4vc.definitions.OPENID_CREDENTIAL_AUTHORIZATION_TYPE +import id.walt.oid4vc.errors.AuthorizationError import id.walt.oid4vc.errors.CredentialError import id.walt.oid4vc.errors.DeferredCredentialError +import id.walt.oid4vc.errors.TokenError import id.walt.oid4vc.interfaces.CredentialResult -import id.walt.oid4vc.providers.CredentialIssuerConfig -import id.walt.oid4vc.providers.IssuanceSession -import id.walt.oid4vc.providers.OpenIDCredentialIssuer -import id.walt.oid4vc.providers.TokenTarget -import id.walt.oid4vc.requests.BatchCredentialRequest -import id.walt.oid4vc.requests.CredentialRequest -import id.walt.oid4vc.responses.BatchCredentialResponse -import id.walt.oid4vc.responses.CredentialErrorCode +import id.walt.oid4vc.providers.* +import id.walt.oid4vc.requests.* +import id.walt.oid4vc.responses.* +import id.walt.oid4vc.util.COSESign1Utils +import id.walt.oid4vc.util.JwtUtils import id.walt.oid4vc.util.randomUUID import id.walt.sdjwt.* import io.github.oshai.kotlinlogging.KotlinLogging @@ -63,24 +65,24 @@ import kotlinx.datetime.plus import kotlinx.serialization.* import kotlinx.serialization.json.* import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid -val supportedCredentialTypes = ConfigManager.getConfig().parse() - /** * OIDC for Verifiable Credential Issuance service provider, implementing abstract service provider from OIDC4VC library. */ @OptIn(ExperimentalUuidApi::class) -open class CIProvider : OpenIDCredentialIssuer( - baseUrl = let { - ConfigManager.getConfig().baseUrl - }, config = CredentialIssuerConfig( - credentialConfigurationsSupported = supportedCredentialTypes - ) +open class CIProvider( + val baseUrl: String = let { ConfigManager.getConfig().baseUrl }, + val config: CredentialIssuerConfig = CredentialIssuerConfig(credentialConfigurationsSupported = ConfigManager.getConfig().parse()) ) { private val log = KotlinLogging.logger { } + val metadata + get() = OpenID4VCI.createDefaultProviderMetadata(baseUrl).copy( + credentialConfigurationsSupported = config.credentialConfigurationsSupported + ) companion object { @@ -98,7 +100,7 @@ open class CIProvider : OpenIDCredentialIssuer( // TODO: make configurable // private val CI_TOKEN_KEY by lazy { KeyManager.resolveSerializedKeyBlocking("""""") } - private val CI_TOKEN_KEY = + val CI_TOKEN_KEY = runBlocking { KeyManager.resolveSerializedKey(ConfigManager.getConfig().ciTokenKey) } // private val CI_TOKEN_KEY by lazy { runBlocking { JWKKey.generate(KeyType.Ed25519) } } } @@ -119,12 +121,12 @@ open class CIProvider : OpenIDCredentialIssuer( decoding = { Json.decodeFromString(it) }, ) - override fun getSession(id: String): IssuanceSession? { + fun getSession(id: String): IssuanceSession? { log.debug { "RETRIEVING CI AUTH SESSION: $id" } return authSessions[id] } - override fun getSessionByAuthServerState(authServerState: String): IssuanceSession? { + fun getSessionByAuthServerState(authServerState: String): IssuanceSession? { log.debug { "RETRIEVING CI AUTH SESSION by authServerState: $authServerState" } var properSession: IssuanceSession? = null authSessions.getAll().forEach { session -> @@ -135,85 +137,36 @@ open class CIProvider : OpenIDCredentialIssuer( return properSession } + fun getSessionForAccessToken(parsedAccessToken: JsonObject): IssuanceSession? { + val sessionId = parsedAccessToken.get(JWTClaims.Payload.subject)?.jsonPrimitive?.content ?: throw IllegalArgumentException("Access token has no subject or invalid subject type") + return getSession(sessionId) + } + - override fun putSession(id: String, session: IssuanceSession) { + fun putSession(id: String, session: IssuanceSession) { log.debug { "SETTING CI AUTH SESSION: $id = $session" } authSessions[id] = session } - override fun removeSession(id: String) { + fun removeSession(id: String) { log.debug { "REMOVING CI AUTH SESSION: $id" } authSessions.remove(id) } - - // ------------------------------------------ - // Simple cryptographics operation interface implementations - override fun signToken( - target: TokenTarget, - payload: JsonObject, - header: JsonObject?, - keyId: String?, - privKey: Key?, - ) = - runBlocking { - log.debug { "Signing JWS: $payload" } - log.debug { "JWS Signature: target: $target, keyId: $keyId, header: $header" } - if (header != null && keyId != null && privKey != null) { - val headers = header.toMutableMap() - .plus(mapOf("alg" to "ES256".toJsonElement(), "type" to "jwt".toJsonElement(), "kid" to keyId.toJsonElement())) - privKey.signJws(payload.toString().toByteArray(), headers).also { - log.debug { "Signed JWS: >> $it" } - } - + fun getVerifiedSession(sessionId: String): IssuanceSession? { + return getSession(sessionId)?.let { + if (it.isExpired) { + removeSession(sessionId) + null } else { - CI_TOKEN_KEY.signJws(payload.toString().toByteArray()).also { - log.debug { "Signed JWS: >> $it" } - } + it } } - - override fun signCWTToken( - target: TokenTarget, - payload: MapElement, - header: MapElement?, - keyId: String?, - privKey: Key?, - ): String { - TODO("Not yet implemented") - } - - override fun verifyTokenSignature(target: TokenTarget, token: String) = runBlocking { - log.debug { "Verifying JWS: $token" } - log.debug { "JWS Verification: target: $target" } - - val tokenHeader = Json.parseToJsonElement(token.split(".")[0].base64UrlDecode().decodeToString()).jsonObject - val key = if (tokenHeader["jwk"] != null) { - JWKKey.importJWK(tokenHeader["jwk"].toString()).getOrThrow() - } else if (tokenHeader["kid"] != null) { - val did = tokenHeader["kid"]!!.jsonPrimitive.content.split("#")[0] - log.debug { "Resolving DID: $did" } - DidService.resolveToKey(did).getOrThrow() - } else { - CI_TOKEN_KEY - } - key.verifyJws(token).also { log.debug { "VERIFICATION IS: $it" } } - }.isSuccess - - @OptIn(ExperimentalSerializationApi::class) - override fun verifyCOSESign1Signature(target: TokenTarget, token: String) = runBlocking { - println("Verifying JWS: $token") - println("JWS Verification: target: $target") - val coseSign1 = Cbor.decodeFromByteArray(token.base64UrlDecode()) - val keyInfo = extractHolderKey(coseSign1) - val cryptoProvider = SimpleCOSECryptoProvider(listOf(keyInfo)) - - cryptoProvider.verify1(coseSign1, "pub-key") } // ------------------------------------- // Implementation of abstract issuer service provider interface - override fun generateCredential(credentialRequest: CredentialRequest): CredentialResult { + fun generateCredential(credentialRequest: CredentialRequest): CredentialResult { log.debug { "GENERATING CREDENTIAL:" } log.debug { "Credential request: $credentialRequest" } log.debug { "CREDENTIAL REQUEST JSON -------:" } @@ -229,7 +182,7 @@ open class CIProvider : OpenIDCredentialIssuer( } } - override fun getDeferredCredential(credentialID: String): CredentialResult { + fun getDeferredCredential(credentialID: String): CredentialResult { return deferredCredentialRequests[credentialID]?.let { when (it.format) { CredentialFormat.mso_mdoc -> runBlocking { doGenerateMDoc(it) } @@ -247,10 +200,7 @@ open class CIProvider : OpenIDCredentialIssuer( credentialRequest, CredentialErrorCode.unsupported_credential_format ) - val proofPayload = credentialRequest.proof?.jwt?.let { parseTokenPayload(it) } ?: throw CredentialError( - credentialRequest, CredentialErrorCode.invalid_or_missing_proof, message = "Proof must be JWT proof" - ) - val proofHeader = credentialRequest.proof?.jwt?.let { parseTokenHeader(it) } ?: throw CredentialError( + val proofHeader = credentialRequest.proof?.jwt?.let { JwtUtils.parseJWTHeader(it) } ?: throw CredentialError( credentialRequest, CredentialErrorCode.invalid_or_missing_proof, message = "Proof must be JWT proof" ) @@ -263,7 +213,7 @@ open class CIProvider : OpenIDCredentialIssuer( message = "Proof JWT header must contain kid or jwk claim" ) val holderDid = if (!holderKid.isNullOrEmpty() && DidUtils.isDidUrl(holderKid)) holderKid.substringBefore("#") else null - val nonce = proofPayload["nonce"]?.jsonPrimitive?.content ?: throw CredentialError( + val nonce = OpenID4VCI.getNonceFromProof(credentialRequest.proof!!) ?: throw CredentialError( credentialRequest, CredentialErrorCode.invalid_or_missing_proof, message = "Proof must contain nonce" ) @@ -313,37 +263,16 @@ open class CIProvider : OpenIDCredentialIssuer( val holderKeyJWK = JWKKey.importJWK(holderKey.toString()).getOrNull()?.exportJWKObject()?.plus("kid" to JWKKey.importJWK(holderKey.toString()).getOrThrow().getKeyId())?.toJsonObject() when (data.request.credentialFormat) { - CredentialFormat.sd_jwt_vc -> sdJwtVc( - holderKeyJWK, - vc, - holderDid, issuerKid) + CredentialFormat.sd_jwt_vc -> OpenID4VCI.generateSdJwtVC(credentialRequest, vc, request.mapping, + request.selectiveDisclosure, vct = metadata.credentialConfigurationsSupported?.get(request.credentialConfigurationId)?.vct ?: throw ConfigurationException( + ConfigException("No vct configured for given credential configuration id: ${request.credentialConfigurationId}") + ), issuerDid, issuerKid, request.x5Chain, data.issuerKey.key).toString() else -> w3cSdJwtVc(W3CVC(vc), issuerKid, holderDid, holderKey) } }.also { log.debug { "Respond VC: $it" } } })) } - private fun extractHolderKey(coseSign1: COSESign1): COSECryptoProviderKeyInfo { - val tokenHeader = coseSign1.decodeProtectedHeader() - return if (tokenHeader.value.containsKey(MapKey(ProofOfPossession.CWTProofBuilder.HEADER_LABEL_COSE_KEY))) { - val rawKey = (tokenHeader.value[MapKey(ProofOfPossession.CWTProofBuilder.HEADER_LABEL_COSE_KEY)] as ByteStringElement).value - COSECryptoProviderKeyInfo( - "pub-key", AlgorithmID.ECDSA_256, - OneKey(CBORObject.DecodeFromBytes(rawKey)).AsPublicKey() - ) - } else { - val x5c = tokenHeader.value[MapKey(ProofOfPossession.CWTProofBuilder.HEADER_LABEL_X5CHAIN)] - val x5Chain = when (x5c) { - is ListElement -> x5c.value.map { X509CertUtils.parse((it as ByteStringElement).value) } - else -> listOf(X509CertUtils.parse((x5c as ByteStringElement).value)) - } - COSECryptoProviderKeyInfo( - "pub-key", AlgorithmID.ECDSA_256, - x5Chain.first().publicKey, x5Chain = x5Chain - ) - } - } - @OptIn(ExperimentalSerializationApi::class) private suspend fun doGenerateMDoc( credentialRequest: CredentialRequest, @@ -354,8 +283,8 @@ open class CIProvider : OpenIDCredentialIssuer( CredentialErrorCode.invalid_or_missing_proof, message = "No CWT proof found on credential request" ) ) - val holderKey = extractHolderKey(coseSign1) - val nonce = getNonceFromProof(credentialRequest.proof!!) ?: throw CredentialError( + val holderKey = COSESign1Utils.extractHolderKey(coseSign1) + val nonce = OpenID4VCI.getNonceFromProof(credentialRequest.proof!!) ?: throw CredentialError( credentialRequest, CredentialErrorCode.invalid_or_missing_proof, message = "No nonce found on proof" @@ -427,9 +356,9 @@ open class CIProvider : OpenIDCredentialIssuer( } @OptIn(ExperimentalSerializationApi::class) - override fun generateBatchCredentialResponse( + fun generateBatchCredentialResponse( batchCredentialRequest: BatchCredentialRequest, - accessToken: String, + session: IssuanceSession, ): BatchCredentialResponse { val credentialRequestFormats = batchCredentialRequest.credentialRequests .map { it.format } @@ -437,7 +366,7 @@ open class CIProvider : OpenIDCredentialIssuer( require(credentialRequestFormats.distinct().size < 2) { "Credential requests don't have the same format: ${credentialRequestFormats.joinToString { it.value }}" } val keyIdsDistinct = batchCredentialRequest.credentialRequests.map { credReq -> - credReq.proof?.jwt?.let { jwt -> parseTokenHeader(jwt) } + credReq.proof?.jwt?.let { jwt -> JwtUtils.parseJWTHeader(jwt) } ?.get(JWTClaims.Header.keyID) ?.jsonPrimitive?.content ?: throw CredentialError( @@ -450,7 +379,7 @@ open class CIProvider : OpenIDCredentialIssuer( require(keyIdsDistinct.size < 2) { "More than one key id requested" } return BatchCredentialResponse.success( - batchCredentialRequest.credentialRequests.map { generateCredentialResponse(it, accessToken) } + batchCredentialRequest.credentialRequests.map { generateCredentialResponse(it, session) } ) } @@ -531,39 +460,6 @@ open class CIProvider : OpenIDCredentialIssuer( } } - private suspend fun IssuanceSessionData.sdJwtVc( - holderKey: JsonObject?, - vc: JsonObject, - holderDid: String?, issuerKid: String? - ): String = SDJwtVC.sign( - sdPayload = SDPayload.createSDPayload( - vc.mergeSDJwtVCPayloadWithMapping( - mapping = request.mapping ?: JsonObject(emptyMap()), - context = mapOf( - "issuerDid" to issuerDid, - "subjectDid" to holderDid - ).filterValues { !it.isNullOrEmpty() }.mapValues { JsonPrimitive(it.value) }, - dataFunctions - ), - request.selectiveDisclosure ?: SDMap(mapOf()) - ), - jwtCryptoProvider = jwtCryptoProvider, - issuerDid = (issuerDid ?: "").ifEmpty { issuerKey.key.getKeyId() }, - holderDid = holderDid, - holderKeyJWK = holderKey, - issuerKeyId = issuerKey.key.getKeyId(), - vct = metadata.credentialConfigurationsSupported?.get(request.credentialConfigurationId)?.vct ?: throw ConfigurationException( - ConfigException("No vct configured for given credential configuration id: ${request.credentialConfigurationId}") - ), - additionalJwtHeader = request.x5Chain?.let { - mapOf("x5c" to JsonArray(it.map { cert -> cert.toJsonElement() })) - } ?: mapOf() - ).toString().also { - sendCallback("sdjwt_issue", buildJsonObject { - put("sdjwt", it) - }) - } - private suspend fun IssuanceSessionData.w3cSdJwtVc( vc: W3CVC, issuerKid: String, @@ -595,7 +491,14 @@ open class CIProvider : OpenIDCredentialIssuer( } suspend fun getJwksSessions() : JsonObject{ - var jwksList = buildJsonObject {} + var jwksList = buildJsonObject { + put("keys", buildJsonArray { add(buildJsonObject { + CI_TOKEN_KEY.getPublicKey().exportJWKObject().forEach { + put(it.key, it.value) + } + put("kid", CI_TOKEN_KEY.getKeyId()) + }) }) + } sessionCredentialPreMapping.getAll().forEach { it.forEach { jwksList = buildJsonObject { @@ -618,23 +521,6 @@ open class CIProvider : OpenIDCredentialIssuer( return jwksList } - fun getVctByCredentialConfigurationId(credentialConfigurationId: String) = OidcApi.metadata.credentialConfigurationsSupported?.get(credentialConfigurationId)?.vct - - fun getVctBySupportedCredentialConfiguration( - baseUrl: String, - credType: String - ): CredentialSupported { - val expectedVct = "$baseUrl/$credType" - - metadata.credentialConfigurationsSupported?.entries?.forEach { entry -> - if (getVctByCredentialConfigurationId(entry.key) == expectedVct) { - return entry.value - } - } - - throw IllegalArgumentException("Invalid type value: $credType. The $credType type is not supported") - } - fun getFormatByCredentialConfigurationId(id: String) = metadata.credentialConfigurationsSupported?.get(id)?.format fun getTypesByCredentialConfigurationId(id: String) = metadata.credentialConfigurationsSupported?.get(id)?.credentialDefinition?.type @@ -658,7 +544,7 @@ open class CIProvider : OpenIDCredentialIssuer( types?.containsAll(credentialRequest.credentialDefinition?.type ?: emptyList()) ?: false } CredentialFormat.sd_jwt_vc -> { - val vct = getVctByCredentialConfigurationId(credentialConfigurationId) + val vct = metadata.getVctByCredentialConfigurationId(credentialConfigurationId) vct == credentialRequest.vct } else -> { @@ -670,4 +556,168 @@ open class CIProvider : OpenIDCredentialIssuer( additionalMatches } } + + private fun generateProofOfPossessionNonceFor(session: IssuanceSession): IssuanceSession { + return session.copy( + cNonce = randomUUID() + ).also { + putSession(it.id, it) + } + } + + private fun isSupportedAuthorizationDetails(authorizationDetails: AuthorizationDetails): Boolean { + return authorizationDetails.type == OPENID_CREDENTIAL_AUTHORIZATION_TYPE && + config.credentialConfigurationsSupported.values.any { credentialSupported -> + credentialSupported.format == authorizationDetails.format && + ((authorizationDetails.credentialDefinition?.type != null && credentialSupported.credentialDefinition?.type?.containsAll( + authorizationDetails.credentialDefinition!!.type!! + ) == true) || + (authorizationDetails.docType != null && credentialSupported.docType == authorizationDetails.docType) + ) + // TODO: check other supported credential parameters + } + } + + fun validateAuthorizationRequest(authorizationRequest: AuthorizationRequest): Boolean { + return authorizationRequest.authorizationDetails != null && authorizationRequest.authorizationDetails!!.any { + isSupportedAuthorizationDetails(it) + } + } + + fun initializeIssuanceSession( + authorizationRequest: AuthorizationRequest, + expiresIn: Duration, + authServerState: String?, //the state used for additional authentication with pwd, id_token or vp_token. + ): IssuanceSession { + return if (authorizationRequest.issuerState.isNullOrEmpty()) { + if (!validateAuthorizationRequest(authorizationRequest)) { + throw AuthorizationError( + authorizationRequest, AuthorizationErrorCode.invalid_request, + "No valid authorization details for credential issuance found on authorization request" + ) + } + IssuanceSession( + randomUUID(), authorizationRequest, + Clock.System.now().plus(expiresIn), authServerState = authServerState + ) + } else { + getVerifiedSession(authorizationRequest.issuerState!!)?.copy(authorizationRequest = authorizationRequest) + ?: throw AuthorizationError( + authorizationRequest, AuthorizationErrorCode.invalid_request, + "No valid issuance session found for given issuer state" + ) + }.also { + val updatedSession = IssuanceSession( + id = it.id, + authorizationRequest = authorizationRequest, + expirationTimestamp = Clock.System.now().plus(5.minutes), + authServerState = authServerState, + txCode = it.txCode, + txCodeValue = it.txCodeValue, + credentialOffer = it.credentialOffer, + cNonce = it.cNonce, + customParameters = it.customParameters + ) + putSession(it.id, updatedSession) + } + } + + open fun initializeCredentialOffer( + credentialOfferBuilder: CredentialOffer.Builder, + expiresIn: Duration, + allowPreAuthorized: Boolean, + txCode: TxCode? = null, txCodeValue: String? = null, + ): IssuanceSession = runBlocking { + val sessionId = randomUUID() + credentialOfferBuilder.addAuthorizationCodeGrant(sessionId) + if (allowPreAuthorized) + credentialOfferBuilder.addPreAuthorizedCodeGrant( + OpenID4VC.generateAuthorizationCodeFor(sessionId, metadata.issuer!!, CI_TOKEN_KEY), + txCode + ) + return@runBlocking IssuanceSession( + id = sessionId, + authorizationRequest = null, + expirationTimestamp = Clock.System.now().plus(expiresIn), + txCode = txCode, + txCodeValue = txCodeValue, + credentialOffer = credentialOfferBuilder.build() + ).also { + putSession(it.id, it) + } + } + + private fun createCredentialResponseFor(credentialResult: CredentialResult, session: IssuanceSession): CredentialResponse = runBlocking { + return@runBlocking credentialResult.credential?.let { credential -> + CredentialResponse.success(credentialResult.format, credential, customParameters = credentialResult.customParameters) + } ?: generateProofOfPossessionNonceFor(session).let { updatedSession -> + CredentialResponse.deferred( + credentialResult.format, + OpenID4VCI.generateDeferredCredentialToken(session.id, + metadata.issuer ?: throw Exception("No issuer defined in provider metadata"), + credentialResult.credentialId ?: throw Exception("credentialId must not be null, if credential issuance is deferred."), + CI_TOKEN_KEY), + updatedSession.cNonce, + updatedSession.expirationTimestamp - Clock.System.now() + ) + } + } + + fun generateCredentialResponse(credentialRequest: CredentialRequest, session: IssuanceSession): CredentialResponse = runBlocking { + // access_token should be validated on API level and issuance session extracted + // Validate credential request (proof of possession, etc) + val nonce = session.cNonce ?: throw CredentialError(credentialRequest, CredentialErrorCode.invalid_or_missing_proof, message = "No cNonce found on current issuance session") + val validationResult = OpenID4VCI.validateCredentialRequest(credentialRequest, nonce, metadata) + if(!validationResult.success) + throw CredentialError(credentialRequest, CredentialErrorCode.invalid_request, message = validationResult.message) + // create credential result + val credentialResult = generateCredential(credentialRequest) + return@runBlocking createCredentialResponseFor(credentialResult, session) + } + + fun generateDeferredCredentialResponse(acceptanceToken: String): CredentialResponse = runBlocking { + val accessInfo = + OpenID4VC.verifyAndParseToken(acceptanceToken, metadata.issuer!!, TokenTarget.DEFERRED_CREDENTIAL, CI_TOKEN_KEY) ?: throw DeferredCredentialError( + CredentialErrorCode.invalid_token, + message = "Invalid acceptance token" + ) + val sessionId = accessInfo[JWTClaims.Payload.subject]!!.jsonPrimitive.content + val credentialId = accessInfo[JWTClaims.Payload.jwtID]!!.jsonPrimitive.content + val session = getVerifiedSession(sessionId) ?: throw DeferredCredentialError( + CredentialErrorCode.invalid_token, + "Session not found for given access token, or session expired." + ) + // issue credential for credential request + return@runBlocking createCredentialResponseFor(getDeferredCredential(credentialId), session) + } + + fun processTokenRequest(tokenRequest: TokenRequest): TokenResponse = runBlocking { + val payload = OpenID4VC.validateAndParseTokenRequest(tokenRequest, metadata.issuer!!, CI_TOKEN_KEY) ?: throw TokenError(tokenRequest, TokenErrorCode.invalid_request, "Token request could not be validated") + val sessionId = payload[JWTClaims.Payload.subject]?.jsonPrimitive?.content ?: throw TokenError(tokenRequest, TokenErrorCode.invalid_request, "Token contains no session ID in subject") + val session = getVerifiedSession(sessionId) ?: throw TokenError( + tokenRequest = tokenRequest, + errorCode = TokenErrorCode.invalid_request, + message = "No authorization session found for given authorization code, or session expired." + ) + if (tokenRequest.grantType == GrantType.pre_authorized_code && session.txCode != null && + session.txCodeValue != tokenRequest.txCode + ) { + throw TokenError( + tokenRequest, + TokenErrorCode.invalid_grant, + message = "User PIN required for this issuance session has not been provided or PIN is wrong." + ) + } + // Expiration time required by EBSI + val currentTime = Clock.System.now().epochSeconds + val expirationTime = (currentTime + 864000L) // ten days in milliseconds + return@runBlocking TokenResponse.success( + OpenID4VC.generateToken(sessionId, metadata.issuer!!, TokenTarget.ACCESS, null, CI_TOKEN_KEY), + tokenType = "bearer", + expiresIn = expirationTime, + cNonce = generateProofOfPossessionNonceFor(session).cNonce, + cNonceExpiresIn = session.expirationTimestamp - Clock.System.now(), + state = session.authorizationRequest?.state + ) + } } diff --git a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuerApi.kt b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuerApi.kt index c6149df40..b579d893a 100644 --- a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuerApi.kt +++ b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/IssuerApi.kt @@ -5,7 +5,7 @@ import id.walt.crypto.keys.KeyManager import id.walt.crypto.keys.KeySerialization import id.walt.did.dids.DidService import id.walt.issuer.issuance.OidcApi.getFormatByCredentialConfigurationId -import id.walt.issuer.issuance.OidcApi.getVctByCredentialConfigurationId +import id.walt.oid4vc.OpenID4VCI import id.walt.oid4vc.data.AuthenticationMethod import id.walt.oid4vc.data.CredentialFormat import id.walt.oid4vc.definitions.CROSS_DEVICE_CREDENTIAL_OFFER_URL @@ -39,7 +39,7 @@ suspend fun createCredentialOfferUri( val overwrittenIssuanceRequests = issuanceRequests.map { it.copy( credentialFormat = credentialFormat, - vct = if (credentialFormat == CredentialFormat.sd_jwt_vc) getVctByCredentialConfigurationId(it.credentialConfigurationId) ?: throw IllegalArgumentException("VCT not found") else null) + vct = if (credentialFormat == CredentialFormat.sd_jwt_vc) OidcApi.metadata.getVctByCredentialConfigurationId(it.credentialConfigurationId) ?: throw IllegalArgumentException("VCT not found") else null) } val credentialOfferBuilder = @@ -72,8 +72,7 @@ suspend fun createCredentialOfferUri( CredentialOfferRequest(null, "${OidcApi.baseUrl}/openid4vc/credentialOffer?id=${issuanceSession.id}") logger.debug { "offerRequest: $offerRequest" } - val offerUri = OidcApi.getCredentialOfferRequestUrl( - offerRequest, + val offerUri = OpenID4VCI.getCredentialOfferRequestUrl(offerRequest, CROSS_DEVICE_CREDENTIAL_OFFER_URL + OidcApi.baseUrl.removePrefix("https://").removePrefix("http://") + "/" ) logger.debug { "Offer URI: $offerUri" } diff --git a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/OidcApi.kt b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/OidcApi.kt index fe3eb9ccf..bcf476a38 100644 --- a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/OidcApi.kt +++ b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/OidcApi.kt @@ -4,16 +4,21 @@ package id.walt.issuer.issuance import id.walt.policies.Verifier import id.walt.policies.models.PolicyRequest.Companion.parsePolicyRequests import id.walt.crypto.utils.Base64Utils.base64UrlDecode +import id.walt.oid4vc.OpenID4VC import id.walt.oid4vc.data.* import id.walt.oid4vc.data.dif.PresentationDefinition import id.walt.oid4vc.data.dif.PresentationSubmission +import id.walt.oid4vc.definitions.JWTClaims import id.walt.oid4vc.errors.* +import id.walt.oid4vc.providers.IssuanceSession import id.walt.oid4vc.providers.TokenTarget import id.walt.oid4vc.requests.AuthorizationRequest import id.walt.oid4vc.requests.BatchCredentialRequest import id.walt.oid4vc.requests.CredentialRequest import id.walt.oid4vc.requests.TokenRequest import id.walt.oid4vc.responses.AuthorizationErrorCode +import id.walt.oid4vc.responses.CredentialErrorCode +import id.walt.oid4vc.responses.PushedAuthorizationResponse import id.walt.sdjwt.JWTVCIssuerMetadata import id.walt.sdjwt.SDJWTVCTypeMetadata import io.github.oshai.kotlinlogging.KotlinLogging @@ -28,6 +33,7 @@ import io.ktor.server.routing.* import io.ktor.util.* import io.ktor.util.pipeline.* import kotlinx.coroutines.runBlocking +import kotlinx.datetime.Clock import kotlinx.serialization.Serializable import kotlinx.serialization.json.* import kotlin.io.encoding.ExperimentalEncodingApi @@ -77,7 +83,7 @@ object OidcApi : CIProvider() { val credType = call.parameters["type"] ?: throw IllegalArgumentException("Type required") // issuer api is the - val vctMetadata = getVctBySupportedCredentialConfiguration(baseUrl, credType) + val vctMetadata = metadata.getVctBySupportedCredentialConfiguration(baseUrl, credType) call.respond( HttpStatusCode.OK, when (vctMetadata.sdJwtVcTypeMetadata != null) { @@ -95,8 +101,12 @@ object OidcApi : CIProvider() { post("/par") { val authReq = AuthorizationRequest.fromHttpParameters(call.receiveParameters().toMap()) try { - val session = initializeAuthorization(authReq, 5.minutes, null) - call.respond(getPushedAuthorizationSuccessResponse(session).toJSON()) + val session = initializeIssuanceSession(authReq, 5.minutes, null) + call.respond( + PushedAuthorizationResponse.success( + requestUri = "${OpenID4VC.PUSHED_AUTHORIZATION_REQUEST_URI_PREFIX}${session.id}", + expiresIn = session.expirationTimestamp - Clock.System.now() + ).toJSON()) } catch (exc: AuthorizationError) { logger.error(exc) { "Authorization error: " } call.respond(HttpStatusCode.BadRequest, exc.toPushedAuthorizationErrorResponse().toJSON()) @@ -130,11 +140,10 @@ object OidcApi : CIProvider() { AuthenticationMethod.ID_TOKEN -> { val idTokenRequestJwtKid = issuanceSessionData.first().issuerKey.key.getKeyId() val idTokenRequestJwtPrivKey = issuanceSessionData.first().issuerKey - processCodeFlowAuthorizationWithAuthorizationRequest( + OpenID4VC.processCodeFlowAuthorizationWithAuthorizationRequest( authReq, - idTokenRequestJwtKid, - idTokenRequestJwtPrivKey.key, ResponseType.IdToken, + metadata, CI_TOKEN_KEY, issuanceSessionData.first().request.useJar ) } @@ -164,18 +173,16 @@ object OidcApi : CIProvider() { val presentationDefinition = PresentationDefinition.defaultGenerationFromVcTypesForCredentialFormat(requestedTypes, credFormat) - processCodeFlowAuthorizationWithAuthorizationRequest( + OpenID4VC.processCodeFlowAuthorizationWithAuthorizationRequest( authReq, - vpTokenRequestJwtKid, - vpTokenRequestJwtPrivKey.key, - ResponseType.VpToken, + ResponseType.VpToken, metadata, CI_TOKEN_KEY, issuanceSessionData.first().request.useJar, presentationDefinition ) } - AuthenticationMethod.NONE -> processCodeFlowAuthorization(authReq) - + AuthenticationMethod.NONE -> OpenID4VC.processCodeFlowAuthorization( + authReq, issuanceSessionData.first().id, metadata, CI_TOKEN_KEY) else -> { throw AuthorizationError( authReq, @@ -186,7 +193,8 @@ object OidcApi : CIProvider() { } } - ResponseType.Token in authReq.responseType -> processImplicitFlowAuthorization(authReq) + ResponseType.Token in authReq.responseType -> OpenID4VC.processImplicitFlowAuthorization( + authReq, issuanceSessionData.first().id, metadata, CI_TOKEN_KEY) else -> { throw AuthorizationError( @@ -202,7 +210,8 @@ object OidcApi : CIProvider() { ?: "openid://" else -> if (authReq.isReferenceToPAR) { - getPushedAuthorizationSession(authReq).authorizationRequest?.redirectUri + val pushedSession = getPushedAuthorizationSession(authReq) + pushedSession.authorizationRequest?.redirectUri } else { authReq.redirectUri } ?: throw AuthorizationError( @@ -254,7 +263,7 @@ object OidcApi : CIProvider() { val idToken = params["id_token"]?.get(0)!! // Verify and Parse ID Token - verifyAndParseIdToken(idToken) + OpenID4VC.verifyAndParseIdToken(idToken) } else { val vpToken = params["vp_token"]?.get(0)!! @@ -275,7 +284,10 @@ object OidcApi : CIProvider() { } // Process response - val resp = processDirectPost(state, buildJsonObject { }) + val session = getSessionByAuthServerState(state) ?: throw IllegalStateException("No session found for given state parameter") + val resp = OpenID4VC.processDirectPost( + session.authorizationRequest ?: throw IllegalStateException("Session for given state has no authorization request"), + session.id, metadata, CI_TOKEN_KEY) // Get the authorization_endpoint parameter which is the redirect_uri from the Authorization Request Parameter val redirectUri = getSessionByAuthServerState(state)!!.authorizationRequest!!.redirectUri!! @@ -325,12 +337,15 @@ object OidcApi : CIProvider() { } post("/credential") { val accessToken = call.request.header(HttpHeaders.Authorization)?.substringAfter(" ") - if (accessToken.isNullOrEmpty() || !verifyTokenSignature(TokenTarget.ACCESS, accessToken)) { + val parsedToken = accessToken?.let { OpenID4VC.verifyAndParseToken(it, metadata.issuer!!, TokenTarget.ACCESS, CI_TOKEN_KEY) } + if (parsedToken == null) { call.respond(HttpStatusCode.Unauthorized) } else { val credReq = CredentialRequest.fromJSON(call.receive()) try { - call.respond(generateCredentialResponse(credReq, accessToken).toJSON()) + val session = parsedToken.get(JWTClaims.Payload.subject)?.jsonPrimitive?.content?.let { getSession(it) } + ?: throw CredentialError(credReq, CredentialErrorCode.invalid_request, "Session not found for access token") + call.respond(generateCredentialResponse(credReq, session).toJSON()) } catch (exc: CredentialError) { logger.error(exc) { "Credential error: " } call.respond(HttpStatusCode.BadRequest, exc.toCredentialErrorResponse().toJSON()) @@ -339,7 +354,7 @@ object OidcApi : CIProvider() { } post("/credential_deferred") { val accessToken = call.request.header(HttpHeaders.Authorization)?.substringAfter(" ") - if (accessToken.isNullOrEmpty() || !verifyTokenSignature( + if (accessToken.isNullOrEmpty() || !OpenID4VC.verifyTokenSignature( TokenTarget.DEFERRED_CREDENTIAL, accessToken ) @@ -356,12 +371,15 @@ object OidcApi : CIProvider() { } post("/batch_credential") { val accessToken = call.request.header(HttpHeaders.Authorization)?.substringAfter(" ") - if (accessToken.isNullOrEmpty() || !verifyTokenSignature(TokenTarget.ACCESS, accessToken)) { + val parsedToken = accessToken?.let { OpenID4VC.verifyAndParseToken(it, metadata.issuer!!, TokenTarget.ACCESS, CI_TOKEN_KEY) } + if (parsedToken == null) { call.respond(HttpStatusCode.Unauthorized) } else { val req = BatchCredentialRequest.fromJSON(call.receive()) try { - call.respond(generateBatchCredentialResponse(req, accessToken).toJSON()) + val session = parsedToken.get(JWTClaims.Payload.subject)?.jsonPrimitive?.content?.let { getSession(it) } + ?: throw BatchCredentialError(req, CredentialErrorCode.invalid_request, "Session not found for access token") + call.respond(generateBatchCredentialResponse(req, session).toJSON()) } catch (exc: BatchCredentialError) { logger.error(exc) { "BatchCredentialError: " } call.respond(HttpStatusCode.BadRequest, exc.toBatchCredentialErrorResponse().toJSON()) @@ -387,7 +405,7 @@ object OidcApi : CIProvider() { else -> runBlocking { AuthorizationRequest.fromHttpQueryString(internalAuthReqParams) } } if (authReq != null && externalAuthReq != null) { - initializeAuthorization(authReq, 5.minutes, externalAuthReq.state) + initializeIssuanceSession(authReq, 5.minutes, externalAuthReq.state) } } @@ -402,7 +420,7 @@ object OidcApi : CIProvider() { // should redirect to authorization request redirect uri with the code val session = getSessionByAuthServerState(call.request.rawQueryParameters.toMap()["state"]!![0]) - val authResp = processCodeFlowAuthorization(session?.authorizationRequest!!) + val authResp = OpenID4VC.processCodeFlowAuthorization(session?.authorizationRequest!!, session.id, metadata, CI_TOKEN_KEY) val redirectUri = when (session.authorizationRequest!!.isReferenceToPAR) { true -> getPushedAuthorizationSession(session.authorizationRequest!!).authorizationRequest?.redirectUri @@ -432,6 +450,19 @@ object OidcApi : CIProvider() { } } + private fun getPushedAuthorizationSession(authorizationRequest: AuthorizationRequest): IssuanceSession { + return authorizationRequest.requestUri?.let { + getVerifiedSession(OpenID4VC.getPushedAuthorizationSessionId(it)) ?: throw AuthorizationError( + authorizationRequest, + AuthorizationErrorCode.invalid_request, + "No session found for given request URI, or session expired" + ) + } ?: throw AuthorizationError( + authorizationRequest, + AuthorizationErrorCode.invalid_request, + "Authorization request does not refer to a pushed authorization session" + ) + } /* private val sessionCache = mutableMapOf() diff --git a/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/OidcIssuanceTest.kt b/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/OidcIssuanceTest.kt index 5c702fa60..5cd8b0071 100644 --- a/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/OidcIssuanceTest.kt +++ b/waltid-services/waltid-issuer-api/src/test/kotlin/id/walt/OidcIssuanceTest.kt @@ -2,6 +2,7 @@ package id.walt import id.walt.commons.config.ConfigManager import id.walt.issuer.issuance.CIProvider +import id.walt.oid4vc.OpenID4VCI import id.walt.oid4vc.data.CredentialOffer import id.walt.oid4vc.requests.CredentialOfferRequest import kotlin.test.Test @@ -22,7 +23,7 @@ class OidcIssuanceTest { ) val offerRequest = CredentialOfferRequest(issuanceSession.credentialOffer!!) - val offerUri = ciTestProvider.getCredentialOfferRequestUrl(offerRequest) + val offerUri = OpenID4VCI.getCredentialOfferRequestUrl(offerRequest) println("Offer URI: $offerUri") }