From 5ce9eca5bfba0a128280ebb7a88c73f4f2b0955e Mon Sep 17 00:00:00 2001
From: Stefan Wiedemann <wistefan@googlemail.com>
Date: Wed, 13 Dec 2023 12:08:43 +0100
Subject: [PATCH] make it independent of the vc libs

---
 crypto/pom.xml                                |    2 +-
 distribution/pom.xml                          |    5 -
 pom.xml                                       |   15 -
 quarkus/runtime/pom.xml                       |    4 -
 services/pom.xml                              |   28 +-
 ...C4VPClientRegistrationProviderFactory.java |    2 +-
 .../oidc4vp/OIDC4VPIssuerEndpoint.java        | 1020 ++++++-----
 .../oidc4vp/OIDC4VPLoginProtocolFactory.java  |  202 +--
 .../oidc4vp/mappers/OIDC4VPMapper.java        |   12 +-
 .../oidc4vp/mappers/OIDC4VPMapperFactory.java |    1 +
 .../mappers/OIDC4VPStaticClaimMapper.java     |    6 +-
 .../mappers/OIDC4VPSubjectIdMapper.java       |    6 +-
 .../mappers/OIDC4VPTargetRoleMapper.java      |   13 +-
 .../oidc4vp/mappers/OIDC4VPTypeMapper.java    |   75 +
 .../mappers/OIDC4VPUserAttributeMapper.java   |    6 +-
 .../oidc4vp/model/CredentialSubject.java      |   38 +
 .../protocol/oidc4vp/model/LdProof.java       |   82 +
 .../oidc4vp/model/VerifiableCredential.java   |  100 ++
 .../oidc4vp/signing/AlgorithmType.java        |   28 -
 .../oidc4vp/signing/FileBasedKeyLoader.java   |   30 +
 .../oidc4vp/signing/JWTSigningService.java    |  173 +-
 .../protocol/oidc4vp/signing/KeyLoader.java   |    7 +
 .../oidc4vp/signing/LDSigningService.java     |  145 +-
 .../oidc4vp/signing/SigningService.java       |   96 +-
 .../oidc4vp/signing/VCSigningService.java     |    5 +-
 .../signing/signatures/Ed255192018Suite.java  |  133 ++
 .../EdDSASignatureSignerContext.java          |   48 +
 .../signatures/RsaSignature2018Suite.java     |   48 +
 .../signing/signatures/SecuritySuite.java     |   29 +
 .../oidc4vp/OIDC4VPIssuerEndpointTest.java    | 1490 +++++++++--------
 .../signing/JWTSigningServiceTest.java        |   89 +
 .../oidc4vp/signing/LDSigningServiceTest.java |  109 ++
 .../oidc4vp/signing/SigningServiceTest.java   |   30 +
 services/src/test/resources/eckey.tls         |    4 +
 34 files changed, 2424 insertions(+), 1657 deletions(-)
 create mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPTypeMapper.java
 create mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/model/CredentialSubject.java
 create mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/model/LdProof.java
 create mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/model/VerifiableCredential.java
 delete mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/signing/AlgorithmType.java
 create mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/signing/FileBasedKeyLoader.java
 create mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/signing/KeyLoader.java
 create mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/Ed255192018Suite.java
 create mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/EdDSASignatureSignerContext.java
 create mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/RsaSignature2018Suite.java
 create mode 100644 services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/SecuritySuite.java
 create mode 100644 services/src/test/java/org/keycloak/protocol/oidc4vp/signing/JWTSigningServiceTest.java
 create mode 100644 services/src/test/java/org/keycloak/protocol/oidc4vp/signing/LDSigningServiceTest.java
 create mode 100644 services/src/test/java/org/keycloak/protocol/oidc4vp/signing/SigningServiceTest.java
 create mode 100644 services/src/test/resources/eckey.tls

diff --git a/crypto/pom.xml b/crypto/pom.xml
index de2f6d339653..f3f554480949 100644
--- a/crypto/pom.xml
+++ b/crypto/pom.xml
@@ -32,7 +32,7 @@
 
     <modules>
         <module>default</module>
-        <module>fips1402</module>
+<!--        <module>fips1402</module>-->
         <module>elytron</module>
     </modules>
 </project>
\ No newline at end of file
diff --git a/distribution/pom.xml b/distribution/pom.xml
index 358724ef147a..d5ba3d6a1533 100755
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -74,11 +74,6 @@
                 <enabled>false</enabled>
             </snapshots>
         </repository>
-
-        <repository>
-            <id>danubetech-maven-public</id>
-            <url>https://repo.danubetech.com/repository/maven-public/</url>
-        </repository>
     </repositories>
 
     <profiles>
diff --git a/pom.xml b/pom.xml
index c00649034984..9436a31b8802 100644
--- a/pom.xml
+++ b/pom.xml
@@ -311,16 +311,6 @@
         <module>js</module>
         <module>quarkus</module>
     </modules>
-    <repositories>
-        <repository>
-            <id>mavenCentral</id>
-            <url>https://repo1.maven.org/maven2/</url>
-        </repository>
-        <repository>
-            <id>danubetech-maven-public</id>
-            <url>https://repo.danubetech.com/repository/maven-public/</url>
-        </repository>
-    </repositories>
     <dependencyManagement>
 
         <dependencies>
@@ -1737,11 +1727,6 @@
                 <artifactId>jboss-servlet-api_4.0_spec</artifactId>
                 <version>${jboss-servlet-api_4.0_spec}</version>
             </dependency>
-            <dependency>
-                <groupId>com.danubetech</groupId>
-                <artifactId>verifiable-credentials-java</artifactId>
-                <version>1.7.0</version>
-            </dependency>
         </dependencies>
     </dependencyManagement>
 
diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml
index 08db3e09bf31..68aa3768669a 100644
--- a/quarkus/runtime/pom.xml
+++ b/quarkus/runtime/pom.xml
@@ -153,10 +153,6 @@
                 </exclusion>
             </exclusions>
         </dependency>
-        <dependency>
-            <groupId>com.danubetech</groupId>
-            <artifactId>verifiable-credentials-java</artifactId>
-        </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
             <artifactId>keycloak-crypto-default</artifactId>
diff --git a/services/pom.xml b/services/pom.xml
index 10f0896f13c2..15fc3cdce6ad 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -34,13 +34,24 @@
         <maven.compiler.release>17</maven.compiler.release>
         <maven.compiler.source>17</maven.compiler.source>
         <maven.compiler.target>17</maven.compiler.target>
-        <version.org.projectlombok>1.18.24</version.org.projectlombok>
+        <version.org.projectlombok>1.18.30</version.org.projectlombok>
         <mockito.version>1.9.5</mockito.version>
         <mockito.junit-jupiter.version>5.0.0</mockito.junit-jupiter.version>
         <junit.jupiter.version>5.9.2</junit.jupiter.version>
     </properties>
 
     <dependencies>
+        <dependency>
+            <groupId>com.apicatalog</groupId>
+            <artifactId>titanium-json-ld</artifactId>
+            <version>1.3.3</version>
+        </dependency>
+        <dependency>
+            <groupId>io.setl</groupId>
+            <artifactId>rdf-urdna</artifactId>
+            <version>1.1</version>
+        </dependency>
+
         <dependency>
             <groupId>org.keycloak</groupId>
             <artifactId>keycloak-core</artifactId>
@@ -87,21 +98,6 @@
             <groupId>org.twitter4j</groupId>
             <artifactId>twitter4j-core</artifactId>
         </dependency>
-        <!-- VC Model -->
-        <dependency>
-            <groupId>com.danubetech</groupId>
-            <artifactId>verifiable-credentials-java</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.glassfish</groupId>
-                    <artifactId>jakarta.json</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.bouncycastle</groupId>
-                    <artifactId>bcprov-jdk18on</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
         <!-- lazy dev -->
         <dependency>
             <groupId>org.projectlombok</groupId>
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPClientRegistrationProviderFactory.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPClientRegistrationProviderFactory.java
index 3d648ffa1f06..89ef818a2a92 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPClientRegistrationProviderFactory.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPClientRegistrationProviderFactory.java
@@ -15,7 +15,7 @@
  */
 public class OIDC4VPClientRegistrationProviderFactory implements ClientRegistrationProviderFactory {
 
-	public static final String PROTOCOL_ID = "OIDC4VP";
+	public static final String PROTOCOL_ID = "oidc4vp";
 
 	@Override public ClientRegistrationProvider create(KeycloakSession session) {
 		return new OIDC4VPClientRegistrationProvider(session);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPIssuerEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPIssuerEndpoint.java
index 327194dc904d..465d4f315f93 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPIssuerEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPIssuerEndpoint.java
@@ -1,41 +1,24 @@
 package org.keycloak.protocol.oidc4vp;
 
-import com.danubetech.verifiablecredentials.CredentialSubject;
-import com.danubetech.verifiablecredentials.VerifiableCredential;
-import com.danubetech.verifiablecredentials.jsonld.VerifiableCredentialContexts;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import info.weboftrust.ldsignatures.LdProof;
 import jakarta.validation.constraints.NotNull;
-import jakarta.ws.rs.BadRequestException;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.OPTIONS;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.QueryParam;
-import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.*;
 import jakarta.ws.rs.core.Response;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import org.jboss.logging.Logger;
 import org.keycloak.TokenVerifier;
-import org.keycloak.authentication.authenticators.client.JWTClientValidator;
 import org.keycloak.common.VerificationException;
 import org.keycloak.common.util.Time;
 import org.keycloak.events.EventBuilder;
-import org.keycloak.models.AuthenticatedClientSessionModel;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ProtocolMapperContainerModel;
-import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.*;
 import org.keycloak.protocol.oidc.utils.OAuth2Code;
 import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
 import org.keycloak.protocol.oidc4vp.mappers.OIDC4VPMapper;
 import org.keycloak.protocol.oidc4vp.mappers.OIDC4VPMapperFactory;
+import org.keycloak.protocol.oidc4vp.model.*;
 import org.keycloak.protocol.oidc4vp.model.CredentialOfferURI;
 import org.keycloak.protocol.oidc4vp.model.CredentialRequest;
 import org.keycloak.protocol.oidc4vp.model.CredentialResponse;
@@ -44,9 +27,8 @@
 import org.keycloak.protocol.oidc4vp.model.Format;
 import org.keycloak.protocol.oidc4vp.model.PreAuthorized;
 import org.keycloak.protocol.oidc4vp.model.PreAuthorizedGrant;
-import org.keycloak.protocol.oidc4vp.model.Proof;
-import org.keycloak.protocol.oidc4vp.model.Role;
 import org.keycloak.protocol.oidc4vp.model.SupportedCredential;
+import org.keycloak.protocol.oidc4vp.signing.FileBasedKeyLoader;
 import org.keycloak.protocol.oidc4vp.signing.JWTSigningService;
 import org.keycloak.protocol.oidc4vp.signing.LDSigningService;
 import org.keycloak.protocol.oidc4vp.signing.SigningServiceException;
@@ -56,23 +38,11 @@
 
 import java.net.URI;
 import java.time.Clock;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.keycloak.protocol.oidc4vp.OIDC4VPClientRegistrationProvider.VC_TYPES_PREFIX;
-import static org.keycloak.protocol.oidc4vp.model.Format.JWT_VC;
-import static org.keycloak.protocol.oidc4vp.model.Format.JWT_VC_JSON;
-import static org.keycloak.protocol.oidc4vp.model.Format.JWT_VC_JSON_LD;
-import static org.keycloak.protocol.oidc4vp.model.Format.LDP_VC;
+import static org.keycloak.protocol.oidc4vp.model.Format.*;
 
 /**
  * Realm-Resource to provide functionality for issuing VerifiableCredentials to users, depending on their roles in
@@ -80,489 +50,499 @@
  */
 public class OIDC4VPIssuerEndpoint {
 
-	private static final Logger LOGGER = Logger.getLogger(OIDC4VPIssuerEndpoint.class);
-
-	public static final String CREDENTIAL_PATH = "credential";
-	public static final String TYPE_VERIFIABLE_CREDENTIAL = "VerifiableCredential";
-	public static final String GRANT_TYPE_PRE_AUTHORIZED_CODE = "urn:ietf:params:oauth:grant-type:pre-authorized_code";
-	private static final String ACCESS_CONTROL_HEADER = "Access-Control-Allow-Origin";
-
-	private final KeycloakSession session;
-	private final AppAuthManager.BearerTokenAuthenticator bearerTokenAuthenticator;
-	private final ObjectMapper objectMapper;
-	private final Clock clock;
-
-	private final String issuerDid;
-
-	private final boolean ldSigningEnabled;
-	private final boolean jwtSigningEnabled;
-	private LDSigningService ldSigningService;
-	private JWTSigningService jwtSigningService;
-
-	public OIDC4VPIssuerEndpoint(KeycloakSession session,
-			String issuerDid,
-			String keyPath,
-			AppAuthManager.BearerTokenAuthenticator authenticator,
-			ObjectMapper objectMapper, Clock clock) {
-		this.session = session;
-		this.bearerTokenAuthenticator = authenticator;
-		this.objectMapper = objectMapper;
-		this.clock = clock;
-		this.issuerDid = issuerDid;
-		var tempJwtSigningEnabled = false;
-		try {
-			this.jwtSigningService = new JWTSigningService(keyPath, Optional.empty());
-			tempJwtSigningEnabled = true;
-		} catch (SigningServiceException e) {
-			LOGGER.warn("Was not able to initialize JWT SigningService, jwt credentials are not supported.", e);
-		}
-		this.jwtSigningEnabled = tempJwtSigningEnabled;
-
-		var tempLdSigningEnabled = false;
-		try {
-			this.ldSigningService = new LDSigningService(keyPath, Optional.empty(), clock);
-			tempLdSigningEnabled = true;
-		} catch (SigningServiceException e) {
-			LOGGER.warn("Was not able to initialize LD SigningService, ld credentials are not supported.", e);
-		}
-		this.ldSigningEnabled = tempLdSigningEnabled;
-	}
-
-	/**
-	 * Provides URI to the OIDC4VCI compliant credentials offer
-	 */
-	@GET
-	@Path("credential-offer-uri")
-	public Response getCredentialOfferURI(@QueryParam("credentialId") String vcId) {
-
-		Map<String, SupportedCredential> credentialsMap = OIDC4VPAbstractWellKnownProvider
-				.getSupportedCredentials(session.getContext()).stream()
-				.collect(Collectors.toMap(SupportedCredential::getId, sc -> sc, (sc1, sc2) -> sc1));
-
-		LOGGER.debugf("Get an offer for %s", vcId);
-		if (!credentialsMap.containsKey(vcId)) {
-			LOGGER.warnf("No credential with id %s exists.", vcId);
-			LOGGER.debugf("Supported credentials are %s.", credentialsMap);
-			throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_REQUEST));
-		}
-		SupportedCredential supportedCredential = credentialsMap.get(vcId);
-		var format = supportedCredential.getFormat();
-
-		// check that the user is allowed to get such credential
-		supportedCredential.getTypes()
-				.forEach(type -> getClientsOfType(type, format));
-
-		String nonce = generateAuthorizationCode();
-
-		AuthenticationManager.AuthResult authResult = getAuthResult();
-		UserSessionModel userSessionModel = getUserSessionModel();
-
-		AuthenticatedClientSessionModel clientSession = userSessionModel.
-				getAuthenticatedClientSessionByClient(
-						authResult.getClient().getId());
-		try {
-			clientSession.setNote(nonce, objectMapper.writeValueAsString(supportedCredential));
-		} catch (JsonProcessingException e) {
-			LOGGER.errorf("Could not convert POJO to JSON: %s", e.getMessage());
-			throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_REQUEST));
-		}
-
-		CredentialOfferURI credentialOfferURI = new CredentialOfferURI();
-		credentialOfferURI.setIssuer(OIDC4VPAbstractWellKnownProvider.getIssuer(session.getContext()));
-		credentialOfferURI.setNonce(nonce);
-
-		LOGGER.debugf("Responding with nonce: %s", nonce);
-		return Response.ok()
-				.entity(credentialOfferURI)
-				.header(ACCESS_CONTROL_HEADER, "*")
-				.build();
-
-	}
-
-	/**
-	 * Provides an OIDC4VCI compliant credentials offer
-	 */
-	@GET
-	@Path("credential-offer/{nonce}")
-	public Response getCredentialOffer(@PathParam("nonce") String nonce) {
-
-		OAuth2CodeParser.ParseResult result = parseNonce(nonce);
-
-		SupportedCredential offeredCredential;
-		try {
-			offeredCredential = objectMapper.readValue(result.getClientSession().getNote(nonce),
-					SupportedCredential.class);
-			LOGGER.debugf("Creating an offer for %s - %s", offeredCredential.getTypes(),
-					offeredCredential.getFormat());
-			result.getClientSession().removeNote(nonce);
-		} catch (JsonProcessingException e) {
-			LOGGER.errorf("Could not convert JSON to POJO: %s", e);
-			throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_REQUEST));
-		}
-
-		String preAuthorizedCode = generateAuthorizationCodeForClientSession(result.getClientSession());
-		CredentialsOffer theOffer = new CredentialsOffer()
-				.credentialIssuer(OIDC4VPAbstractWellKnownProvider.getIssuer(session.getContext()))
-				.credentials(List.of(offeredCredential))
-				.grants(new PreAuthorizedGrant().
-						urnColonIetfColonParamsColonOauthColonGrantTypeColonPreAuthorizedCode(
-								new PreAuthorized().preAuthorizedCode(preAuthorizedCode)
-										.userPinRequired(false)));
-
-		LOGGER.debugf("Responding with offer: %s", theOffer);
-		return Response.ok()
-				.entity(theOffer)
-				.header(ACCESS_CONTROL_HEADER, "*")
-				.build();
-	}
-
-	/**
-	 * Options endpoint to serve the cors-preflight requests.
-	 * Since we cannot know the address of the requesting wallets in advance, we have to accept all origins.
-	 */
-	@OPTIONS
-	@Path("{any: .*}")
-	public Response optionCorsResponse() {
-		return Response.ok().header(ACCESS_CONTROL_HEADER, "*")
-				.header("Access-Control-Allow-Methods", "POST,GET,OPTIONS")
-				.header("Access-Control-Allow-Headers", "Content-Type,Authorization")
-				.build();
-	}
-
-	/**
-	 * Returns a verifiable credential of the given type, containing the information and roles assigned to the
-	 * authenticated user.
-	 * In order to support the often used retrieval method by wallets, the token can also be provided as a
-	 * query-parameter. If the parameter is empty, the token is taken from the authorization-header.
-	 *
-	 * @param vcType type of the VerifiableCredential to be returend.
-	 * @param token  optional JWT to be used instead of retrieving it from the header.
-	 * @return the vc.
-	 */
-	@GET
-	@Path("/")
-	public Response issueVerifiableCredential(@QueryParam("type") String vcType, @QueryParam("token") String
-			token) {
-		LOGGER.debugf("Get a VC of type %s. Token parameter is %s.", vcType, token);
-		if (token != null) {
-			// authenticate with the token
-			bearerTokenAuthenticator.setTokenString(token);
-		}
-		return Response.ok()
-				.entity(getCredential(vcType, LDP_VC))
-				.header(ACCESS_CONTROL_HEADER, "*")
-				.build();
-	}
-
-	/**
-	 * Requests a credential from the issuer
-	 */
-	@POST
-	@Path(CREDENTIAL_PATH)
-	public Response requestCredential(
-			CredentialRequest credentialRequestVO) {
-		LOGGER.debugf("Received credentials request %s.", credentialRequestVO);
-
-		List<String> types = new ArrayList<>(Objects.requireNonNull(Optional.ofNullable(credentialRequestVO.getTypes())
-				.orElseGet(() -> {
-					try {
-						return objectMapper.readValue(credentialRequestVO.getType(), new TypeReference<>() {
-						});
-					} catch (JsonProcessingException e) {
-						LOGGER.warnf("Was not able to read the type parameter: %s", credentialRequestVO.getType(), e);
-						return null;
-					}
-				})));
-
-		// remove the static type
-		types.remove(TYPE_VERIFIABLE_CREDENTIAL);
-
-		if (types.size() != 1) {
-			LOGGER.infof("Credential request contained multiple types. Req: %s", credentialRequestVO);
-			throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_REQUEST));
-		}
-		// verify the proof
-		Optional.ofNullable(credentialRequestVO.getProof()).ifPresent(this::verifyProof);
-
-		Format requestedFormat = credentialRequestVO.getFormat();
-		// workaround to support implementations not differentiating json & json-ld
-		if (requestedFormat == JWT_VC) {
-			requestedFormat = JWT_VC_JSON;
-		}
-		// TODO: check if there can be more
-		String vcType = types.get(0);
-
-		var responseVO = new CredentialResponse();
-		// keep the originally requested here.
-		responseVO.format(credentialRequestVO.getFormat());
-
-		Object theCredential = getCredential(vcType, credentialRequestVO.getFormat());
-		switch (requestedFormat) {
-			case LDP_VC -> responseVO.setCredential(theCredential);
-			case JWT_VC_JSON -> responseVO.setCredential(theCredential);
-			default -> throw new BadRequestException(
-					getErrorResponse(ErrorResponse.ErrorEnum.UNSUPPORTED_CREDENTIAL_TYPE));
-		}
-		return Response.ok().entity(responseVO)
-				.header(ACCESS_CONTROL_HEADER, "*").build();
-	}
-
-	// return the current usersession model
-	private UserSessionModel getUserSessionModel() {
-		return getAuthResult(
-				new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_TOKEN))).getSession();
-	}
-
-	private AuthenticationManager.AuthResult getAuthResult() {
-		return getAuthResult(new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_TOKEN)));
-	}
-
-	// get the auth result from the authentication manager
-	private AuthenticationManager.AuthResult getAuthResult(WebApplicationException errorResponse) {
-		AuthenticationManager.AuthResult authResult = bearerTokenAuthenticator.authenticate();
-		if (authResult == null) {
-			throw errorResponse;
-		}
-		return authResult;
-	}
-
-	protected Object getCredential(String vcType, Format format) {
-		// do first to fail fast on auth
-		UserSessionModel userSessionModel = getUserSessionModel();
-		List<ClientModel> clients = getClientsOfType(vcType, format);
-		List<OIDC4VPMapper> protocolMappers = getProtocolMappers(clients)
-				.stream()
-				.map(OIDC4VPMapperFactory::createOIDC4VPMapper)
-				.toList();
-
-		var credentialToSign = getVCToSign(protocolMappers, vcType, userSessionModel);
-
-		return switch (format) {
-			case LDP_VC -> {
-				if (ldSigningEnabled) {
-					yield ldSigningService.signCredential(credentialToSign);
-				}
-				throw new IllegalArgumentException(
-						String.format("Requested format %s is not supported.", format));
-			}
-			case JWT_VC, JWT_VC_JSON_LD, JWT_VC_JSON -> {
-				if (jwtSigningEnabled) {
-					yield jwtSigningService.signCredential(credentialToSign);
-				}
-				throw new IllegalArgumentException(
-						String.format("Requested format %s is not supported.", format));
-			}
-		};
-	}
-
-	private List<ProtocolMapperModel> getProtocolMappers(List<ClientModel> clientModels) {
-		return clientModels.stream()
-				.flatMap(ProtocolMapperContainerModel::getProtocolMappersStream)
-				.toList();
-
-	}
-
-	private OAuth2CodeParser.ParseResult parseNonce(String nonce) throws BadRequestException {
-		EventBuilder eventBuilder = new EventBuilder(session.getContext().getRealm(), session,
-				session.getContext().getConnection());
-		OAuth2CodeParser.ParseResult result = OAuth2CodeParser.parseCode(session, nonce,
-				session.getContext().getRealm(),
-				eventBuilder);
-		if (result.isExpiredCode() || result.isIllegalCode()) {
-			throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_TOKEN));
-		}
-		return result;
-	}
-
-	private String generateAuthorizationCode() {
-		AuthenticationManager.AuthResult authResult = getAuthResult();
-		UserSessionModel userSessionModel = getUserSessionModel();
-		AuthenticatedClientSessionModel clientSessionModel = userSessionModel.
-				getAuthenticatedClientSessionByClient(authResult.getClient().getId());
-		return generateAuthorizationCodeForClientSession(clientSessionModel);
-	}
-
-	private String generateAuthorizationCodeForClientSession(AuthenticatedClientSessionModel clientSessionModel) {
-		int expiration = Time.currentTime() + clientSessionModel.getUserSession().getRealm().getAccessCodeLifespan();
-
-		String codeId = UUID.randomUUID().toString();
-		String nonce = UUID.randomUUID().toString();
-		OAuth2Code oAuth2Code = new OAuth2Code(codeId, expiration, nonce, null, null, null, null,
-				clientSessionModel.getUserSession().getId());
-
-		return OAuth2CodeParser.persistCode(session, clientSessionModel, oAuth2Code);
-	}
-
-	private Response getErrorResponse(ErrorResponse.ErrorEnum errorType) {
-		var errorResponse = new ErrorResponse();
-		errorResponse.setError(errorType);
-		return Response.status(Response.Status.BAD_REQUEST).entity(errorResponse).build();
-	}
-
-	@NotNull
-	private List<ClientModel> getClientsOfType(String vcType, Format format) {
-		LOGGER.debugf("Retrieve all clients of type %s, supporting format %s", vcType, format.toString());
-
-		List<String> formatStrings = switch (format) {
-			case LDP_VC -> List.of(LDP_VC.toString());
-			case JWT_VC, JWT_VC_JSON -> List.of(JWT_VC.toString(), JWT_VC_JSON.toString());
-			case JWT_VC_JSON_LD -> List.of(JWT_VC.toString(), JWT_VC_JSON_LD.toString());
-
-		};
-
-		Optional.ofNullable(vcType).filter(type -> !type.isEmpty()).orElseThrow(() -> {
-			LOGGER.info("No VC type was provided.");
-			return new BadRequestException("No VerifiableCredential-Type was provided in the request.");
-		});
-
-		String prefixedType = String.format("%s%s", VC_TYPES_PREFIX, vcType);
-		LOGGER.infof("Looking for client supporting %s with format %s", prefixedType, formatStrings);
-		List<ClientModel> vcClients = getClientModelsFromSession().stream()
-				.filter(clientModel -> Optional.ofNullable(clientModel.getAttributes())
-						.filter(attributes -> attributes.containsKey(prefixedType))
-						.filter(attributes -> formatStrings.stream()
-								.anyMatch(formatString -> Arrays.asList(attributes.get(prefixedType).split(","))
-										.contains(formatString)))
-						.isPresent())
-				.toList();
-
-		if (vcClients.isEmpty()) {
-			LOGGER.infof("No OIDC4VP-Client supporting type %s registered.", vcType);
-			throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.UNSUPPORTED_CREDENTIAL_TYPE));
-		}
-		return vcClients;
-	}
-
-	@NotNull
-	private List<ClientModel> getClientModelsFromSession() {
-		return session.clients().getClientsStream(session.getContext().getRealm())
-				.filter(clientModel -> clientModel.getProtocol() != null)
-				.filter(clientModel -> clientModel.getProtocol()
-						.equals(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID))
-				.toList();
-	}
-
-	@NotNull
-	private Role toRolesClaim(ClientRoleModel crm) {
-		Set<String> roleNames = crm
-				.getRoleModels()
-				.stream()
-				.map(RoleModel::getName)
-				.collect(Collectors.toSet());
-		return new Role(roleNames, crm.getClientId());
-	}
-
-	@NotNull
-	private VerifiableCredential getVCToSign(List<OIDC4VPMapper> protocolMappers, String vcType,
-			UserSessionModel userSessionModel) {
-
-		var subjectBuilder = CredentialSubject.builder();
-
-		Map<String, Object> subjectClaims = new HashMap<>();
-
-		protocolMappers
-				.forEach(mapper -> mapper.setClaimsForSubject(subjectClaims, userSessionModel));
-
-		LOGGER.infof("Will set %s", subjectClaims);
-		subjectBuilder.claims(subjectClaims);
-
-		CredentialSubject subject = subjectBuilder.build();
-
-		var credentialBuilder = VerifiableCredential.builder()
-				.types(List.of(vcType))
-				.context(VerifiableCredentialContexts.JSONLD_CONTEXT_W3C_2018_CREDENTIALS_V1)
-				.id(URI.create(String.format("urn:uuid:%s", UUID.randomUUID())))
-				.issuer(URI.create(issuerDid))
-				.issuanceDate(Date.from(clock.instant()))
-				.credentialSubject(subject);
-
-		// use the mappers after the default
-		protocolMappers
-				.forEach(mapper -> mapper.setClaimsForCredential(credentialBuilder, userSessionModel));
-
-		return credentialBuilder.build();
-	}
-
-	private void verifyProof(Proof proof) {
-		switch (proof.getProofType()) {
-			case JWT -> verifyJWTProof(proof.getJwt());
-			case LD_PROOF -> throw new IllegalArgumentException("LD Proofs on the request are not yet supported.");
-		}
-	}
-
-	private void verifyJWTProof(String jwt) {
-
-		var verifier = TokenVerifier.create(jwt, JsonWebToken.class)
-				.withChecks(jsonWebToken -> jsonWebToken.getType().equals("openid4vci-proof+jwt"),
-						jsonWebToken -> jsonWebToken.getAudience().length == 1,
-						jsonWebToken -> jsonWebToken.getAudience()[0].equals(
-								OIDC4VPAbstractWellKnownProvider.getIssuer(session.getContext())),
-						jsonWebToken -> jsonWebToken.getOtherClaims().containsKey("nonce"));
-
-		try {
-			verifier.verify();
-		} catch (VerificationException e) {
-			LOGGER.warnf("Was not able to verify the jwt proof.", e);
-			throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_OR_MISSING_PROOF));
-		}
-
-	}
-
-	@NotNull
-	private List<String> getClaimsToSet(String credentialType, List<ClientModel> clients) {
-		String claims = clients.stream()
-				.map(ClientModel::getAttributes)
-				.filter(Objects::nonNull)
-				.map(Map::entrySet)
-				.flatMap(Set::stream)
-				// get the claims
-				.filter(entry -> entry.getKey().equals(String.format("%s_%s", credentialType, "claims")))
-				.findFirst()
-				.map(Map.Entry::getValue)
-				.orElse("");
-		LOGGER.infof("Should set %s for %s.", claims, credentialType);
-		return Arrays.asList(claims.split(","));
-
-	}
-
-	@NotNull
-	private Optional<Map<String, String>> getAdditionalClaims(List<ClientModel> clients) {
-		Map<String, String> additionalClaims = clients.stream()
-				.map(ClientModel::getAttributes)
-				.filter(Objects::nonNull)
-				.map(Map::entrySet)
-				.flatMap(Set::stream)
-				// only include the claims explicitly intended for vc
-				.filter(entry -> entry.getKey().startsWith(OIDC4VPClientRegistrationProvider.VC_CLAIMS_PREFIX))
-				.collect(
-						Collectors.toMap(
-								// remove the prefix before sending it
-								entry -> entry.getKey()
-										.replaceFirst(OIDC4VPClientRegistrationProvider.VC_CLAIMS_PREFIX, ""),
-								// value is taken untouched if its unique
-								Map.Entry::getValue,
-								// if multiple values for the same key exist, we add them comma separated.
-								// this needs to be improved, once more requirements are known.
-								(entry1, entry2) -> {
-									if (entry1.equals(entry2) || entry1.contains(entry2)) {
-										return entry1;
-									} else {
-										return String.format("%s,%s", entry1, entry2);
-									}
-								}
-						));
-		if (additionalClaims.isEmpty()) {
-			return Optional.empty();
-		} else {
-			return Optional.of(additionalClaims);
-		}
-	}
-
-	@Getter
-	@RequiredArgsConstructor
-	private static class ClientRoleModel {
-		private final String clientId;
-		private final List<RoleModel> roleModels;
-	}
+    private static final Logger LOGGER = Logger.getLogger(OIDC4VPIssuerEndpoint.class);
+
+    public static final String CREDENTIAL_PATH = "credential";
+    public static final String TYPE_VERIFIABLE_CREDENTIAL = "VerifiableCredential";
+    public static final String GRANT_TYPE_PRE_AUTHORIZED_CODE = "urn:ietf:params:oauth:grant-type:pre-authorized_code";
+    private static final String ACCESS_CONTROL_HEADER = "Access-Control-Allow-Origin";
+
+    private final KeycloakSession session;
+    private final AppAuthManager.BearerTokenAuthenticator bearerTokenAuthenticator;
+    private final ObjectMapper objectMapper;
+    private final Clock clock;
+
+    private final String issuerDid;
+
+    private final boolean ldSigningEnabled;
+    private final boolean jwtSigningEnabled;
+    private LDSigningService ldSigningService;
+    private JWTSigningService jwtSigningService;
+
+    public OIDC4VPIssuerEndpoint(KeycloakSession session,
+                                 String issuerDid,
+                                 String keyPath,
+                                 Optional<String> jwtType,
+                                 Optional<String> ldpType,
+                                 AppAuthManager.BearerTokenAuthenticator authenticator,
+                                 ObjectMapper objectMapper, Clock clock) {
+        this.session = session;
+        this.bearerTokenAuthenticator = authenticator;
+        this.objectMapper = objectMapper;
+        this.clock = clock;
+        this.issuerDid = issuerDid;
+        var tempJwtSigningEnabled = false;
+        if (jwtType.isPresent()) {
+            try {
+                this.jwtSigningService = new JWTSigningService(new FileBasedKeyLoader(keyPath), Optional.empty(), clock, jwtType.get());
+                tempJwtSigningEnabled = true;
+            } catch (SigningServiceException e) {
+                LOGGER.warn("Was not able to initialize JWT SigningService, jwt credentials are not supported.", e);
+                throw new IllegalArgumentException("No valid jwt_vc signing configured.", e);
+            }
+        }
+        this.jwtSigningEnabled = tempJwtSigningEnabled;
+
+        var tempLdSigningEnabled = false;
+        if (ldpType.isPresent()) {
+            try {
+                this.ldSigningService = new LDSigningService(new FileBasedKeyLoader(keyPath), Optional.empty(), clock, ldpType.get(), objectMapper);
+                tempLdSigningEnabled = true;
+            } catch (SigningServiceException e) {
+                LOGGER.warn("Was not able to initialize LD SigningService, ld credentials are not supported.", e);
+                throw new IllegalArgumentException("No valid ldp_vc signing configured.", e);
+
+            }
+        }
+        this.ldSigningEnabled = tempLdSigningEnabled;
+    }
+
+    /**
+     * Provides URI to the OIDC4VCI compliant credentials offer
+     */
+    @GET
+    @Path("credential-offer-uri")
+    public Response getCredentialOfferURI(@QueryParam("credentialId") String vcId) {
+
+        Map<String, SupportedCredential> credentialsMap = OIDC4VPAbstractWellKnownProvider
+                .getSupportedCredentials(session.getContext()).stream()
+                .collect(Collectors.toMap(SupportedCredential::getId, sc -> sc, (sc1, sc2) -> sc1));
+
+        LOGGER.debugf("Get an offer for %s", vcId);
+        if (!credentialsMap.containsKey(vcId)) {
+            LOGGER.warnf("No credential with id %s exists.", vcId);
+            LOGGER.debugf("Supported credentials are %s.", credentialsMap);
+            throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_REQUEST));
+        }
+        SupportedCredential supportedCredential = credentialsMap.get(vcId);
+        var format = supportedCredential.getFormat();
+
+        // check that the user is allowed to get such credential
+        supportedCredential.getTypes()
+                .forEach(type -> getClientsOfType(type, format));
+
+        String nonce = generateAuthorizationCode();
+
+        AuthenticationManager.AuthResult authResult = getAuthResult();
+        UserSessionModel userSessionModel = getUserSessionModel();
+
+        AuthenticatedClientSessionModel clientSession = userSessionModel.
+                getAuthenticatedClientSessionByClient(
+                        authResult.getClient().getId());
+        try {
+            clientSession.setNote(nonce, objectMapper.writeValueAsString(supportedCredential));
+        } catch (JsonProcessingException e) {
+            LOGGER.errorf("Could not convert POJO to JSON: %s", e.getMessage());
+            throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_REQUEST));
+        }
+
+        CredentialOfferURI credentialOfferURI = new CredentialOfferURI();
+        credentialOfferURI.setIssuer(OIDC4VPAbstractWellKnownProvider.getIssuer(session.getContext()));
+        credentialOfferURI.setNonce(nonce);
+
+        LOGGER.debugf("Responding with nonce: %s", nonce);
+        return Response.ok()
+                .entity(credentialOfferURI)
+                .header(ACCESS_CONTROL_HEADER, "*")
+                .build();
+
+    }
+
+    /**
+     * Provides an OIDC4VCI compliant credentials offer
+     */
+    @GET
+    @Path("credential-offer/{nonce}")
+    public Response getCredentialOffer(@PathParam("nonce") String nonce) {
+
+        OAuth2CodeParser.ParseResult result = parseNonce(nonce);
+
+        SupportedCredential offeredCredential;
+        try {
+            offeredCredential = objectMapper.readValue(result.getClientSession().getNote(nonce),
+                    SupportedCredential.class);
+            LOGGER.debugf("Creating an offer for %s - %s", offeredCredential.getTypes(),
+                    offeredCredential.getFormat());
+            result.getClientSession().removeNote(nonce);
+        } catch (JsonProcessingException e) {
+            LOGGER.errorf("Could not convert JSON to POJO: %s", e);
+            throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_REQUEST));
+        }
+
+        String preAuthorizedCode = generateAuthorizationCodeForClientSession(result.getClientSession());
+        CredentialsOffer theOffer = new CredentialsOffer()
+                .credentialIssuer(OIDC4VPAbstractWellKnownProvider.getIssuer(session.getContext()))
+                .credentials(List.of(offeredCredential))
+                .grants(new PreAuthorizedGrant().
+                        urnColonIetfColonParamsColonOauthColonGrantTypeColonPreAuthorizedCode(
+                                new PreAuthorized().preAuthorizedCode(preAuthorizedCode)
+                                        .userPinRequired(false)));
+
+        LOGGER.debugf("Responding with offer: %s", theOffer);
+        return Response.ok()
+                .entity(theOffer)
+                .header(ACCESS_CONTROL_HEADER, "*")
+                .build();
+    }
+
+    /**
+     * Options endpoint to serve the cors-preflight requests.
+     * Since we cannot know the address of the requesting wallets in advance, we have to accept all origins.
+     */
+    @OPTIONS
+    @Path("{any: .*}")
+    public Response optionCorsResponse() {
+        return Response.ok().header(ACCESS_CONTROL_HEADER, "*")
+                .header("Access-Control-Allow-Methods", "POST,GET,OPTIONS")
+                .header("Access-Control-Allow-Headers", "Content-Type,Authorization")
+                .build();
+    }
+
+    /**
+     * Returns a verifiable credential of the given type, containing the information and roles assigned to the
+     * authenticated user.
+     * In order to support the often used retrieval method by wallets, the token can also be provided as a
+     * query-parameter. If the parameter is empty, the token is taken from the authorization-header.
+     *
+     * @param vcType type of the VerifiableCredential to be returend.
+     * @param token  optional JWT to be used instead of retrieving it from the header.
+     * @return the vc.
+     */
+    @GET
+    @Path("/")
+    public Response issueVerifiableCredential(@QueryParam("type") String vcType, @QueryParam("token") String
+            token) {
+        LOGGER.debugf("Get a VC of type %s. Token parameter is %s.", vcType, token);
+        if (token != null) {
+            // authenticate with the token
+            bearerTokenAuthenticator.setTokenString(token);
+        }
+        return Response.ok()
+                .entity(getCredential(vcType, LDP_VC))
+                .header(ACCESS_CONTROL_HEADER, "*")
+                .build();
+    }
+
+    /**
+     * Requests a credential from the issuer
+     */
+    @POST
+    @Path(CREDENTIAL_PATH)
+    public Response requestCredential(
+            CredentialRequest credentialRequestVO) {
+        LOGGER.debugf("Received credentials request %s.", credentialRequestVO);
+
+        List<String> types = new ArrayList<>(Objects.requireNonNull(Optional.ofNullable(credentialRequestVO.getTypes())
+                .orElseGet(() -> {
+                    try {
+                        return objectMapper.readValue(credentialRequestVO.getType(), new TypeReference<>() {
+                        });
+                    } catch (JsonProcessingException e) {
+                        LOGGER.warnf("Was not able to read the type parameter: %s", credentialRequestVO.getType(), e);
+                        return null;
+                    }
+                })));
+
+        // remove the static type
+        types.remove(TYPE_VERIFIABLE_CREDENTIAL);
+
+        if (types.size() != 1) {
+            LOGGER.infof("Credential request contained multiple types. Req: %s", credentialRequestVO);
+            throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_REQUEST));
+        }
+        // verify the proof
+//		Optional.ofNullable(credentialRequestVO.getProof()).ifPresent(this::verifyProof);
+
+        Format requestedFormat = credentialRequestVO.getFormat();
+        // workaround to support implementations not differentiating json & json-ld
+        if (requestedFormat == JWT_VC) {
+            requestedFormat = JWT_VC_JSON;
+        }
+        // TODO: check if there can be more
+        String vcType = types.get(0);
+
+        var responseVO = new CredentialResponse();
+        // keep the originally requested here.
+        responseVO.format(credentialRequestVO.getFormat());
+
+        Object theCredential = getCredential(vcType, credentialRequestVO.getFormat());
+        switch (requestedFormat) {
+            case LDP_VC -> responseVO.setCredential(theCredential);
+            case JWT_VC_JSON -> responseVO.setCredential(theCredential);
+            default -> throw new BadRequestException(
+                    getErrorResponse(ErrorResponse.ErrorEnum.UNSUPPORTED_CREDENTIAL_TYPE));
+        }
+        return Response.ok().entity(responseVO)
+                .header(ACCESS_CONTROL_HEADER, "*").build();
+    }
+
+    // return the current usersession model
+    private UserSessionModel getUserSessionModel() {
+        return getAuthResult(
+                new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_TOKEN))).getSession();
+    }
+
+    private AuthenticationManager.AuthResult getAuthResult() {
+        return getAuthResult(new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_TOKEN)));
+    }
+
+    // get the auth result from the authentication manager
+    private AuthenticationManager.AuthResult getAuthResult(WebApplicationException errorResponse) {
+        AuthenticationManager.AuthResult authResult = bearerTokenAuthenticator.authenticate();
+        if (authResult == null) {
+            throw errorResponse;
+        }
+        return authResult;
+    }
+
+    protected Object getCredential(String vcType, Format format) {
+        // do first to fail fast on auth
+        UserSessionModel userSessionModel = getUserSessionModel();
+        List<ClientModel> clients = getClientsOfType(vcType, format);
+        List<OIDC4VPMapper> protocolMappers = getProtocolMappers(clients)
+                .stream()
+                .map(OIDC4VPMapperFactory::createOIDC4VPMapper)
+                .toList();
+
+        var credentialToSign = getVCToSign(protocolMappers, vcType, userSessionModel);
+
+        return switch (format) {
+            case LDP_VC -> {
+                if (ldSigningEnabled) {
+                    yield ldSigningService.signCredential(credentialToSign);
+                }
+                throw new IllegalArgumentException(
+                        String.format("Requested format %s is not supported.", format));
+            }
+            case JWT_VC, JWT_VC_JSON_LD, JWT_VC_JSON -> {
+                if (jwtSigningEnabled) {
+                    yield jwtSigningService.signCredential(credentialToSign);
+                }
+                throw new IllegalArgumentException(
+                        String.format("Requested format %s is not supported.", format));
+            }
+        };
+    }
+
+    private List<ProtocolMapperModel> getProtocolMappers(List<ClientModel> clientModels) {
+        return clientModels.stream()
+                .flatMap(ProtocolMapperContainerModel::getProtocolMappersStream)
+                .toList();
+
+    }
+
+    private OAuth2CodeParser.ParseResult parseNonce(String nonce) throws BadRequestException {
+        EventBuilder eventBuilder = new EventBuilder(session.getContext().getRealm(), session,
+                session.getContext().getConnection());
+        OAuth2CodeParser.ParseResult result = OAuth2CodeParser.parseCode(session, nonce,
+                session.getContext().getRealm(),
+                eventBuilder);
+        if (result.isExpiredCode() || result.isIllegalCode()) {
+            throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_TOKEN));
+        }
+        return result;
+    }
+
+    private String generateAuthorizationCode() {
+        AuthenticationManager.AuthResult authResult = getAuthResult();
+        UserSessionModel userSessionModel = getUserSessionModel();
+        AuthenticatedClientSessionModel clientSessionModel = userSessionModel.
+                getAuthenticatedClientSessionByClient(authResult.getClient().getId());
+        return generateAuthorizationCodeForClientSession(clientSessionModel);
+    }
+
+    private String generateAuthorizationCodeForClientSession(AuthenticatedClientSessionModel clientSessionModel) {
+        int expiration = Time.currentTime() + clientSessionModel.getUserSession().getRealm().getAccessCodeLifespan();
+
+        String codeId = UUID.randomUUID().toString();
+        String nonce = UUID.randomUUID().toString();
+        OAuth2Code oAuth2Code = new OAuth2Code(codeId, expiration, nonce, null, null, null, null,
+                clientSessionModel.getUserSession().getId());
+
+        return OAuth2CodeParser.persistCode(session, clientSessionModel, oAuth2Code);
+    }
+
+    private Response getErrorResponse(ErrorResponse.ErrorEnum errorType) {
+        var errorResponse = new ErrorResponse();
+        errorResponse.setError(errorType);
+        return Response.status(Response.Status.BAD_REQUEST).entity(errorResponse).build();
+    }
+
+    @NotNull
+    private List<ClientModel> getClientsOfType(String vcType, Format format) {
+        LOGGER.debugf("Retrieve all clients of type %s, supporting format %s", vcType, format.toString());
+
+        List<String> formatStrings = switch (format) {
+            case LDP_VC -> List.of(LDP_VC.toString());
+            case JWT_VC, JWT_VC_JSON -> List.of(JWT_VC.toString(), JWT_VC_JSON.toString());
+            case JWT_VC_JSON_LD -> List.of(JWT_VC.toString(), JWT_VC_JSON_LD.toString());
+
+        };
+
+        Optional.ofNullable(vcType).filter(type -> !type.isEmpty()).orElseThrow(() -> {
+            LOGGER.info("No VC type was provided.");
+            return new BadRequestException("No VerifiableCredential-Type was provided in the request.");
+        });
+
+        String prefixedType = String.format("%s%s", VC_TYPES_PREFIX, vcType);
+        LOGGER.infof("Looking for client supporting %s with format %s", prefixedType, formatStrings);
+        List<ClientModel> vcClients = getClientModelsFromSession().stream()
+                .filter(clientModel -> Optional.ofNullable(clientModel.getAttributes())
+                        .filter(attributes -> attributes.containsKey(prefixedType))
+                        .filter(attributes -> formatStrings.stream()
+                                .anyMatch(formatString -> Arrays.asList(attributes.get(prefixedType).split(","))
+                                        .contains(formatString)))
+                        .isPresent())
+                .toList();
+
+        if (vcClients.isEmpty()) {
+            LOGGER.infof("No OIDC4VP-Client supporting type %s registered.", vcType);
+            throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.UNSUPPORTED_CREDENTIAL_TYPE));
+        }
+        return vcClients;
+    }
+
+    @NotNull
+    private List<ClientModel> getClientModelsFromSession() {
+        return session.clients().getClientsStream(session.getContext().getRealm())
+                .filter(clientModel -> clientModel.getProtocol() != null)
+                .filter(clientModel -> clientModel.getProtocol()
+                        .equals(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID))
+                .toList();
+    }
+
+    @NotNull
+    private Role toRolesClaim(ClientRoleModel crm) {
+        Set<String> roleNames = crm
+                .getRoleModels()
+                .stream()
+                .map(RoleModel::getName)
+                .collect(Collectors.toSet());
+        return new Role(roleNames, crm.getClientId());
+    }
+
+    @NotNull
+    private VerifiableCredential getVCToSign(List<OIDC4VPMapper> protocolMappers, String vcType,
+                                             UserSessionModel userSessionModel) {
+
+        var vc = new VerifiableCredential();
+
+        // set the required claims
+        vc.setIssuer(URI.create(issuerDid));
+        vc.setIssuanceDate(Date.from(clock.instant()));
+
+        Map<String, Object> subjectClaims = new HashMap<>();
+
+        protocolMappers
+                .forEach(mapper -> mapper.setClaimsForSubject(subjectClaims, userSessionModel));
+
+        LOGGER.infof("Will set %s", subjectClaims);
+        subjectClaims.entrySet().stream().forEach(entry -> vc.getCredentialSubject().setClaims(entry.getKey(), entry.getValue()));
+        // use the mappers after the default
+        protocolMappers
+                .forEach(mapper -> mapper.setClaimsForCredential(vc, userSessionModel));
+        if (vc.getContext() == null || vc.getContext().isEmpty()) {
+            // add default
+            vc.setContext(List.of("https://www.w3.org/2018/credentials/v1"));
+        }
+
+        if (vc.getId() == null) {
+            vc.setId(URI.create(String.format("uri:uuid:%s", UUID.randomUUID())));
+        }
+        return vc;
+    }
+
+    private void verifyProof(LdProof proof) {
+        switch (proof.getType()) {
+            // TODO: fix types
+            case "JWT" -> verifyJWTProof(proof.getJws());
+            case "LD_PROOF" -> throw new IllegalArgumentException("LD Proofs on the request are not yet supported.");
+        }
+    }
+
+    private void verifyJWTProof(String jwt) {
+
+        var verifier = TokenVerifier.create(jwt, JsonWebToken.class)
+                .withChecks(jsonWebToken -> jsonWebToken.getType().equals("openid4vci-proof+jwt"),
+                        jsonWebToken -> jsonWebToken.getAudience().length == 1,
+                        jsonWebToken -> jsonWebToken.getAudience()[0].equals(
+                                OIDC4VPAbstractWellKnownProvider.getIssuer(session.getContext())),
+                        jsonWebToken -> jsonWebToken.getOtherClaims().containsKey("nonce"));
+
+        try {
+            verifier.verify();
+        } catch (VerificationException e) {
+            LOGGER.warnf("Was not able to verify the jwt proof.", e);
+            throw new BadRequestException(getErrorResponse(ErrorResponse.ErrorEnum.INVALID_OR_MISSING_PROOF));
+        }
+
+    }
+
+    @NotNull
+    private List<String> getClaimsToSet(String credentialType, List<ClientModel> clients) {
+        String claims = clients.stream()
+                .map(ClientModel::getAttributes)
+                .filter(Objects::nonNull)
+                .map(Map::entrySet)
+                .flatMap(Set::stream)
+                // get the claims
+                .filter(entry -> entry.getKey().equals(String.format("%s_%s", credentialType, "claims")))
+                .findFirst()
+                .map(Map.Entry::getValue)
+                .orElse("");
+        LOGGER.infof("Should set %s for %s.", claims, credentialType);
+        return Arrays.asList(claims.split(","));
+
+    }
+
+    @NotNull
+    private Optional<Map<String, String>> getAdditionalClaims(List<ClientModel> clients) {
+        Map<String, String> additionalClaims = clients.stream()
+                .map(ClientModel::getAttributes)
+                .filter(Objects::nonNull)
+                .map(Map::entrySet)
+                .flatMap(Set::stream)
+                // only include the claims explicitly intended for vc
+                .filter(entry -> entry.getKey().startsWith(OIDC4VPClientRegistrationProvider.VC_CLAIMS_PREFIX))
+                .collect(
+                        Collectors.toMap(
+                                // remove the prefix before sending it
+                                entry -> entry.getKey()
+                                        .replaceFirst(OIDC4VPClientRegistrationProvider.VC_CLAIMS_PREFIX, ""),
+                                // value is taken untouched if its unique
+                                Map.Entry::getValue,
+                                // if multiple values for the same key exist, we add them comma separated.
+                                // this needs to be improved, once more requirements are known.
+                                (entry1, entry2) -> {
+                                    if (entry1.equals(entry2) || entry1.contains(entry2)) {
+                                        return entry1;
+                                    } else {
+                                        return String.format("%s,%s", entry1, entry2);
+                                    }
+                                }
+                        ));
+        if (additionalClaims.isEmpty()) {
+            return Optional.empty();
+        } else {
+            return Optional.of(additionalClaims);
+        }
+    }
+
+    @Getter
+    @RequiredArgsConstructor
+    private static class ClientRoleModel {
+        private final String clientId;
+        private final List<RoleModel> roleModels;
+    }
 }
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPLoginProtocolFactory.java
index cac2053cbfa3..74de718782d2 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPLoginProtocolFactory.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/OIDC4VPLoginProtocolFactory.java
@@ -31,101 +31,111 @@
  */
 public class OIDC4VPLoginProtocolFactory implements LoginProtocolFactory {
 
-	private static final Logger LOGGER = Logger.getLogger(OIDC4VPLoginProtocolFactory.class);
-
-	public static final String PROTOCOL_ID = "oidc4vp";
-
-	private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-	private static final String CLIENT_ROLES_MAPPER = "client-roles";
-	private static final String SUBJECT_ID_MAPPER = "subject-id";
-	private static final String USERNAME_MAPPER = "username";
-	private static final String EMAIL_MAPPER = "email";
-	private static final String LAST_NAME_MAPPER = "last-name";
-	private static final String FIRST_NAME_MAPPER = "first-name";
-
-	private final Clock clock = Clock.systemUTC();
-
-	private Map<String, ProtocolMapperModel> builtins = new HashMap<>();
-
-	@Override public void init(Config.Scope config) {
-		LOGGER.info("Initiate the protocol factory");
-		builtins.put(CLIENT_ROLES_MAPPER,
-				OIDC4VPTargetRoleMapper.create("id", "client roles"));
-		builtins.put(SUBJECT_ID_MAPPER,
-				OIDC4VPSubjectIdMapper.create("subject id", "id"));
-		builtins.put(USERNAME_MAPPER,
-				OIDC4VPUserAttributeMapper.create(USERNAME_MAPPER, "username", "username", false));
-		builtins.put(EMAIL_MAPPER,
-				OIDC4VPUserAttributeMapper.create(EMAIL_MAPPER, "email", "email", false));
-		builtins.put(FIRST_NAME_MAPPER,
-				OIDC4VPUserAttributeMapper.create(FIRST_NAME_MAPPER, "firstName", "firstName", false));
-		builtins.put(LAST_NAME_MAPPER,
-				OIDC4VPUserAttributeMapper.create(LAST_NAME_MAPPER, "lastName", "familyName", false));
-	}
-
-	@Override public void postInit(KeycloakSessionFactory factory) {
-		// no-op
-	}
-
-	@Override public void close() {
-		// no-op
-	}
-
-	@Override
-	public Map<String, ProtocolMapperModel> getBuiltinMappers() {
-		return builtins;
-	}
-
-	@Override
-	public Object createProtocolEndpoint(KeycloakSession keycloakSession, EventBuilder event) {
-
-		LOGGER.info("Create vc-issuer protocol endpoint");
-
-		String issuerDid = Optional.ofNullable(keycloakSession.getContext().getRealm().getAttribute("issuerDid"))
-				.orElseThrow(() -> new VCIssuerException("No issuerDid  configured."));
-		String keyPath = Optional.ofNullable(keycloakSession.getContext().getRealm().getAttribute("keyPath"))
-				.orElseThrow(() -> new VCIssuerException("No keyPath configured."));
-		return new OIDC4VPIssuerEndpoint(
-				keycloakSession,
-				issuerDid, keyPath,
-				new AppAuthManager.BearerTokenAuthenticator(
-						keycloakSession),
-				OBJECT_MAPPER,
-				clock
-		);
-	}
-
-	@Override public void createDefaultClientScopes(RealmModel newRealm, boolean addScopesToExistingClients) {
-		LOGGER.debugf("Create default scopes for realm %s", newRealm.getName());
-
-		ClientScopeModel naturalPersonScope = KeycloakModelUtils.getClientScopeByName(newRealm, "natural_person");
-		if (naturalPersonScope == null) {
-			LOGGER.debug("Add natural person scope");
-			naturalPersonScope = newRealm.addClientScope("natural_person");
-			naturalPersonScope.setDescription(
-					"SIOP-2 Scope, that adds all properties required for a natural person.");
-			naturalPersonScope.setProtocol(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
-			naturalPersonScope.addProtocolMapper(builtins.get(SUBJECT_ID_MAPPER));
-			naturalPersonScope.addProtocolMapper(builtins.get(CLIENT_ROLES_MAPPER));
-			naturalPersonScope.addProtocolMapper(builtins.get(EMAIL_MAPPER));
-			naturalPersonScope.addProtocolMapper(builtins.get(FIRST_NAME_MAPPER));
-			naturalPersonScope.addProtocolMapper(builtins.get(LAST_NAME_MAPPER));
-			newRealm.addDefaultClientScope(naturalPersonScope, true);
-		}
-	}
-
-	@Override
-	public void setupClientDefaults(ClientRepresentation rep, ClientModel newClient) {
-		// validate before setting the defaults
-		OIDC4VPClientRegistrationProvider.validate(rep);
-	}
-
-	@Override public LoginProtocol create(KeycloakSession session) {
-		return new OIDC4VPLoginProtocol(session);
-	}
-
-	@Override public String getId() {
-		return PROTOCOL_ID;
-	}
+    private static final Logger LOGGER = Logger.getLogger(OIDC4VPLoginProtocolFactory.class);
+
+    public static final String PROTOCOL_ID = "oidc4vp";
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final String CLIENT_ROLES_MAPPER = "client-roles";
+    private static final String SUBJECT_ID_MAPPER = "subject-id";
+    private static final String USERNAME_MAPPER = "username";
+    private static final String EMAIL_MAPPER = "email";
+    private static final String LAST_NAME_MAPPER = "last-name";
+    private static final String FIRST_NAME_MAPPER = "first-name";
+
+    private final Clock clock = Clock.systemUTC();
+
+    private Map<String, ProtocolMapperModel> builtins = new HashMap<>();
+
+    @Override
+    public void init(Config.Scope config) {
+        LOGGER.info("Initiate the protocol factory");
+        builtins.put(CLIENT_ROLES_MAPPER,
+                OIDC4VPTargetRoleMapper.create("id", "client roles"));
+        builtins.put(SUBJECT_ID_MAPPER,
+                OIDC4VPSubjectIdMapper.create("subject id", "id"));
+        builtins.put(USERNAME_MAPPER,
+                OIDC4VPUserAttributeMapper.create(USERNAME_MAPPER, "username", "username", false));
+        builtins.put(EMAIL_MAPPER,
+                OIDC4VPUserAttributeMapper.create(EMAIL_MAPPER, "email", "email", false));
+        builtins.put(FIRST_NAME_MAPPER,
+                OIDC4VPUserAttributeMapper.create(FIRST_NAME_MAPPER, "firstName", "firstName", false));
+        builtins.put(LAST_NAME_MAPPER,
+                OIDC4VPUserAttributeMapper.create(LAST_NAME_MAPPER, "lastName", "familyName", false));
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+        // no-op
+    }
+
+    @Override
+    public void close() {
+        // no-op
+    }
+
+    @Override
+    public Map<String, ProtocolMapperModel> getBuiltinMappers() {
+        return builtins;
+    }
+
+    @Override
+    public Object createProtocolEndpoint(KeycloakSession keycloakSession, EventBuilder event) {
+
+        LOGGER.info("Create vc-issuer protocol endpoint");
+
+        String issuerDid = Optional.ofNullable(keycloakSession.getContext().getRealm().getAttribute("issuerDid"))
+                .orElseThrow(() -> new VCIssuerException("No issuerDid  configured."));
+        String keyPath = Optional.ofNullable(keycloakSession.getContext().getRealm().getAttribute("keyPath"))
+                .orElseThrow(() -> new VCIssuerException("No keyPath configured."));
+        Optional<String> lpdType = Optional.ofNullable(keycloakSession.getContext().getRealm().getAttribute("ldpType"));
+        Optional<String> jwtType = Optional.ofNullable(keycloakSession.getContext().getRealm().getAttribute("jwtType"));
+
+        return new OIDC4VPIssuerEndpoint(
+                keycloakSession,
+                issuerDid, keyPath,
+                jwtType, lpdType,
+                new AppAuthManager.BearerTokenAuthenticator(
+                        keycloakSession),
+                OBJECT_MAPPER,
+                clock
+        );
+    }
+
+    @Override
+    public void createDefaultClientScopes(RealmModel newRealm, boolean addScopesToExistingClients) {
+        LOGGER.debugf("Create default scopes for realm %s", newRealm.getName());
+
+        ClientScopeModel naturalPersonScope = KeycloakModelUtils.getClientScopeByName(newRealm, "natural_person");
+        if (naturalPersonScope == null) {
+            LOGGER.debug("Add natural person scope");
+            naturalPersonScope = newRealm.addClientScope("natural_person");
+            naturalPersonScope.setDescription(
+                    "OIDC$VP Scope, that adds all properties required for a natural person.");
+            naturalPersonScope.setProtocol(PROTOCOL_ID);
+            naturalPersonScope.addProtocolMapper(builtins.get(SUBJECT_ID_MAPPER));
+            naturalPersonScope.addProtocolMapper(builtins.get(CLIENT_ROLES_MAPPER));
+            naturalPersonScope.addProtocolMapper(builtins.get(EMAIL_MAPPER));
+            naturalPersonScope.addProtocolMapper(builtins.get(FIRST_NAME_MAPPER));
+            naturalPersonScope.addProtocolMapper(builtins.get(LAST_NAME_MAPPER));
+            newRealm.addDefaultClientScope(naturalPersonScope, true);
+        }
+    }
+
+    @Override
+    public void setupClientDefaults(ClientRepresentation rep, ClientModel newClient) {
+        // validate before setting the defaults
+        OIDC4VPClientRegistrationProvider.validate(rep);
+    }
+
+    @Override
+    public LoginProtocol create(KeycloakSession session) {
+        return new OIDC4VPLoginProtocol(session);
+    }
+
+    @Override
+    public String getId() {
+        return PROTOCOL_ID;
+    }
 
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPMapper.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPMapper.java
index 11513919cdf6..f183170fba17 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPMapper.java
@@ -1,6 +1,5 @@
 package org.keycloak.protocol.oidc4vp.mappers;
 
-import com.danubetech.verifiablecredentials.VerifiableCredential;
 import org.keycloak.Config;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -8,13 +7,10 @@
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.ProtocolMapper;
 import org.keycloak.protocol.oidc4vp.OIDC4VPClientRegistrationProviderFactory;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
 import org.keycloak.provider.ProviderConfigProperty;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.stream.Stream;
 
 public abstract class OIDC4VPMapper implements ProtocolMapper {
@@ -87,8 +83,8 @@ public boolean isTypeSupported(String credentialType) {
 	/**
 	 * Set the claims to credential, like f.e. the context
 	 */
-	public abstract void setClaimsForCredential(VerifiableCredential.Builder credentialBuilder,
-			UserSessionModel userSessionModel);
+	public abstract void setClaimsForCredential(VerifiableCredential verifiableCredential,
+												UserSessionModel userSessionModel);
 
 	/**
 	 * Set the claims to the credential subject.
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPMapperFactory.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPMapperFactory.java
index 43be5088027a..19e9f6fcc8e3 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPMapperFactory.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPMapperFactory.java
@@ -14,6 +14,7 @@ public static OIDC4VPMapper createOIDC4VPMapper(ProtocolMapperModel mapperModel)
 			case OIDC4VPSubjectIdMapper.MAPPER_ID -> new OIDC4VPSubjectIdMapper().setMapperModel(mapperModel);
 			case OIDC4VPUserAttributeMapper.MAPPER_ID -> new OIDC4VPUserAttributeMapper().setMapperModel(mapperModel);
 			case OIDC4VPStaticClaimMapper.MAPPER_ID -> new OIDC4VPStaticClaimMapper().setMapperModel(mapperModel);
+			case OIDC4VPTypeMapper.MAPPER_ID -> new OIDC4VPTypeMapper().setMapperModel(mapperModel);
 			default -> throw new OIDC4VPMapperException(
 					String.format("No mapper with id %s exists.", mapperModel.getProtocolMapper()));
 		};
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPStaticClaimMapper.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPStaticClaimMapper.java
index f4e1eed41520..543f1c542789 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPStaticClaimMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPStaticClaimMapper.java
@@ -1,9 +1,9 @@
 package org.keycloak.protocol.oidc4vp.mappers;
 
-import com.danubetech.verifiablecredentials.VerifiableCredential;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.oidc4vp.OIDC4VPClientRegistrationProviderFactory;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
 import org.keycloak.provider.ProviderConfigProperty;
 
 import java.util.ArrayList;
@@ -54,8 +54,8 @@ public static ProtocolMapperModel create(String mapperName, String propertyName,
 		return CONFIG_PROPERTIES;
 	}
 
-	@Override public void setClaimsForCredential(VerifiableCredential.Builder credentialBuilder,
-			UserSessionModel userSessionModel) {
+	public void setClaimsForCredential(VerifiableCredential verifiableCredential,
+									   UserSessionModel userSessionModel) {
 		// nothing to do for the mapper.
 	}
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPSubjectIdMapper.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPSubjectIdMapper.java
index 416033d2c853..2771e5c0189a 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPSubjectIdMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPSubjectIdMapper.java
@@ -1,9 +1,9 @@
 package org.keycloak.protocol.oidc4vp.mappers;
 
-import com.danubetech.verifiablecredentials.VerifiableCredential;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.oidc4vp.OIDC4VPClientRegistrationProviderFactory;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
 import org.keycloak.provider.ProviderConfigProperty;
 
 import java.util.ArrayList;
@@ -47,8 +47,8 @@ public static ProtocolMapperModel create(String name, String subjectId) {
 		return mapperModel;
 	}
 
-	@Override public void setClaimsForCredential(VerifiableCredential.Builder credentialBuilder,
-			UserSessionModel userSessionModel) {
+	public void setClaimsForCredential(VerifiableCredential verifiableCredential,
+									   UserSessionModel userSessionModel) {
 		// nothing to do for the mapper.
 	}
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPTargetRoleMapper.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPTargetRoleMapper.java
index 76902c573960..ea6895f623f0 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPTargetRoleMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPTargetRoleMapper.java
@@ -1,6 +1,5 @@
 package org.keycloak.protocol.oidc4vp.mappers;
 
-import com.danubetech.verifiablecredentials.VerifiableCredential;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import jakarta.validation.constraints.NotNull;
 import lombok.Getter;
@@ -12,14 +11,10 @@
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.oidc4vp.OIDC4VPClientRegistrationProviderFactory;
 import org.keycloak.protocol.oidc4vp.model.Role;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
 import org.keycloak.provider.ProviderConfigProperty;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 
 public class OIDC4VPTargetRoleMapper extends OIDC4VPMapper {
@@ -72,8 +67,8 @@ public static ProtocolMapperModel create(String clientId, String name) {
 	}
 
 	@Override
-	public void setClaimsForCredential(VerifiableCredential.Builder credentialBuilder,
-			UserSessionModel userSessionModel) {
+	public void setClaimsForCredential(VerifiableCredential verifiableCredential,
+									   UserSessionModel userSessionModel) {
 		// nothing to do for the mapper.
 	}
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPTypeMapper.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPTypeMapper.java
new file mode 100644
index 000000000000..037b9a4eee30
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPTypeMapper.java
@@ -0,0 +1,75 @@
+package org.keycloak.protocol.oidc4vp.mappers;
+
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.oidc4vp.OIDC4VPClientRegistrationProviderFactory;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.*;
+
+public class OIDC4VPTypeMapper extends OIDC4VPMapper {
+
+    public static final String MAPPER_ID = "oidc4vp-vc-type-mapper";
+    public static final String TYPE_KEY = "vcTypeProperty";
+
+    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
+
+    public OIDC4VPTypeMapper() {
+        super();
+        ProviderConfigProperty vcTypePropertyNameConfig = new ProviderConfigProperty();
+        vcTypePropertyNameConfig.setName(TYPE_KEY);
+        vcTypePropertyNameConfig.setLabel("Verifiable Credential Type");
+        vcTypePropertyNameConfig.setHelpText("Type of the credential.");
+        vcTypePropertyNameConfig.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(vcTypePropertyNameConfig);
+
+    }
+
+    @Override
+    protected List<ProviderConfigProperty> getIndividualConfigProperties() {
+        return CONFIG_PROPERTIES;
+    }
+
+    public static ProtocolMapperModel create(String name, String subjectId) {
+        var mapperModel = new ProtocolMapperModel();
+        mapperModel.setName(name);
+        Map<String, String> configMap = new HashMap<>();
+        configMap.put(SUPPORTED_CREDENTIALS_KEY, "VerifiableCredential");
+        mapperModel.setConfig(configMap);
+        mapperModel.setProtocol(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
+        mapperModel.setProtocolMapper(MAPPER_ID);
+        return mapperModel;
+    }
+
+    public void setClaimsForCredential(VerifiableCredential verifiableCredential,
+                                       UserSessionModel userSessionModel) {
+        // remove duplicates
+        Set<String> types = new HashSet<>();
+        if (verifiableCredential.getType() != null) {
+            types = new HashSet<>(verifiableCredential.getType());
+        }
+        types.add(mapperModel.getConfig().get(TYPE_KEY));
+        verifiableCredential.setType(new ArrayList<>(types));
+    }
+
+    @Override
+    public void setClaimsForSubject(Map<String, Object> claims, UserSessionModel userSessionModel) {
+        // nothing to do for the mapper.
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "CredentialSubject ID Mapper";
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Assigns a subject ID to the credentials subject. If no specific id is configured, a randomly generated one is used.";
+    }
+
+    @Override
+    public String getId() {
+        return MAPPER_ID;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPUserAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPUserAttributeMapper.java
index 78cce6fc6724..77a205cb8ac7 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPUserAttributeMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/mappers/OIDC4VPUserAttributeMapper.java
@@ -1,11 +1,11 @@
 package org.keycloak.protocol.oidc4vp.mappers;
 
-import com.danubetech.verifiablecredentials.VerifiableCredential;
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.oidc4vp.OIDC4VPClientRegistrationProviderFactory;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
 import org.keycloak.provider.ProviderConfigProperty;
 
 import java.util.ArrayList;
@@ -59,8 +59,8 @@ public OIDC4VPUserAttributeMapper() {
 		return CONFIG_PROPERTIES;
 	}
 
-	@Override public void setClaimsForCredential(VerifiableCredential.Builder credentialBuilder,
-			UserSessionModel userSessionModel) {
+	public void setClaimsForCredential(VerifiableCredential verifiableCredential,
+									   UserSessionModel userSessionModel) {
 		// nothing to do for the mapper.
 	}
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/model/CredentialSubject.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/model/CredentialSubject.java
new file mode 100644
index 000000000000..4d7eff4e2abd
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/model/CredentialSubject.java
@@ -0,0 +1,38 @@
+package org.keycloak.protocol.oidc4vp.model;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class CredentialSubject {
+
+    private String id;
+
+    @JsonIgnore
+    private Map<String, Object> claims = new HashMap<>();
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    @JsonAnyGetter
+    public Map<String, Object> getClaims() {
+        return claims;
+    }
+
+    @JsonAnySetter
+    public void setClaims(String name, Object claim) {
+        claims.put(name, claim);
+    }
+
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/model/LdProof.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/model/LdProof.java
new file mode 100644
index 000000000000..e3b1201c2f33
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/model/LdProof.java
@@ -0,0 +1,82 @@
+package org.keycloak.protocol.oidc4vp.model;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class LdProof {
+
+    private String type;
+    private Date created;
+    private String proofPurpose;
+    private String verificationMethod;
+    private String proofValue;
+    private String jws;
+
+    @JsonIgnore
+    private Map<String, Object> additionalProperties = new HashMap<>();
+
+    @JsonAnyGetter
+    public Map<String, Object> getAdditionalProperties() {
+        return additionalProperties;
+    }
+
+    @JsonAnySetter
+    public void setAdditionalProperties(String name, Object property) {
+        additionalProperties.put(name, property);
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public Date getCreated() {
+        return created;
+    }
+
+    public void setCreated(Date created) {
+        this.created = created;
+    }
+
+    public String getProofPurpose() {
+        return proofPurpose;
+    }
+
+    public void setProofPurpose(String proofPurpose) {
+        this.proofPurpose = proofPurpose;
+    }
+
+    public String getVerificationMethod() {
+        return verificationMethod;
+    }
+
+    public void setVerificationMethod(String verificationMethod) {
+        this.verificationMethod = verificationMethod;
+    }
+
+    public String getProofValue() {
+        return proofValue;
+    }
+
+    public void setProofValue(String proofValue) {
+        this.proofValue = proofValue;
+    }
+
+    public String getJws() {
+        return jws;
+    }
+
+    public void setJws(String jws) {
+        this.jws = jws;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/model/VerifiableCredential.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/model/VerifiableCredential.java
new file mode 100644
index 000000000000..f66492612ca7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/model/VerifiableCredential.java
@@ -0,0 +1,100 @@
+package org.keycloak.protocol.oidc4vp.model;
+
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.databind.DatabindException;
+
+import java.net.URI;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class VerifiableCredential {
+
+    @JsonProperty("@context")
+    private List<String> context;
+    private List<String> type;
+    private URI issuer;
+    private Date issuanceDate;
+    private URI id;
+    private Date expirationDate;
+    private CredentialSubject credentialSubject = new CredentialSubject();
+    private LdProof proof;
+    @JsonIgnore
+    private Map<String, Object> additionalProperties = new HashMap<>();
+
+    @JsonAnyGetter
+    public Map<String, Object> getAdditionalProperties() {
+        return additionalProperties;
+    }
+
+    @JsonAnySetter
+    public void setAdditionalProperties(String name, Object property) {
+        additionalProperties.put(name, property);
+    }
+
+    public List<String> getContext() {
+        return context;
+    }
+
+    public void setContext(List<String> context) {
+        this.context = context;
+    }
+
+    public List<String> getType() {
+        return type;
+    }
+
+    public void setType(List<String> type) {
+        this.type = type;
+    }
+
+    public URI getIssuer() {
+        return issuer;
+    }
+
+    public void setIssuer(URI issuer) {
+        this.issuer = issuer;
+    }
+
+    public Date getIssuanceDate() {
+        return issuanceDate;
+    }
+
+    public void setIssuanceDate(Date issuanceDate) {
+        this.issuanceDate = issuanceDate;
+    }
+
+    public Date getExpirationDate() {
+        return expirationDate;
+    }
+
+    public void setExpirationDate(Date expirationDate) {
+        this.expirationDate = expirationDate;
+    }
+
+    public CredentialSubject getCredentialSubject() {
+        return credentialSubject;
+    }
+
+    public void setCredentialSubject(CredentialSubject credentialSubject) {
+        this.credentialSubject = credentialSubject;
+    }
+
+    public LdProof getProof() {
+        return proof;
+    }
+
+    public void setProof(LdProof proof) {
+        this.proof = proof;
+    }
+
+    public URI getId() {
+        return id;
+    }
+
+    public void setId(URI id) {
+        this.id = id;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/AlgorithmType.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/AlgorithmType.java
deleted file mode 100644
index 13db35d32b17..000000000000
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/AlgorithmType.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.keycloak.protocol.oidc4vp.signing;
-
-import java.util.List;
-
-public enum AlgorithmType {
-
-	ED_DSA_ED25519(List.of("eddsa", "ed25519", "eddsa_ed25519")),
-	ECDSA_SECP256K1(List.of("ecdsa", "secp256k1", "ecdsa_secp256k1")),
-	RSA(List.of("rsa", "ps256", "rs256"));
-
-	private final List<String> values;
-
-	AlgorithmType(List<String> values) {
-		this.values = values;
-	}
-
-	public List<String> getValues() {
-		return values;
-	}
-
-	public static AlgorithmType getByValue(String value) {
-		for (AlgorithmType algorithmType : values())
-			if (algorithmType.values.stream().anyMatch(value::equalsIgnoreCase)) {
-				return algorithmType;
-			}
-		throw new IllegalArgumentException(String.format("No algorithm of type %s exists.", value));
-	}
-}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/FileBasedKeyLoader.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/FileBasedKeyLoader.java
new file mode 100644
index 000000000000..2954b6a52faa
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/FileBasedKeyLoader.java
@@ -0,0 +1,30 @@
+package org.keycloak.protocol.oidc4vp.signing;
+
+import org.jboss.logging.Logger;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class FileBasedKeyLoader implements KeyLoader {
+    private static final Logger LOGGER = Logger.getLogger(FileBasedKeyLoader.class);
+    private final String keyPath;
+
+    public FileBasedKeyLoader(String keyPath) {
+        this.keyPath = keyPath;
+    }
+
+    @Override
+    public String loadKey() {
+        Path keyFilePath = Paths.get(keyPath);
+        try {
+            return Files.readString(keyFilePath);
+        } catch (IOException e) {
+            LOGGER.errorf("Was not able to read the private key from %s", keyPath);
+            throw new SigningServiceException("Was not able to read private key. Cannot initiate the SigningService.",
+                    e);
+        }
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/JWTSigningService.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/JWTSigningService.java
index 5c8f566debec..18d8dc05f47d 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/JWTSigningService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/JWTSigningService.java
@@ -1,48 +1,141 @@
 package org.keycloak.protocol.oidc4vp.signing;
 
-import com.danubetech.verifiablecredentials.VerifiableCredential;
-import com.danubetech.verifiablecredentials.jwt.JwtVerifiableCredential;
-import com.danubetech.verifiablecredentials.jwt.ToJwtConverter;
-import com.nimbusds.jose.JOSEException;
-import org.bitcoinj.core.ECKey;
 
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.crypto.*;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
+import org.keycloak.protocol.oidc4vp.signing.signatures.EdDSASignatureSignerContext;
+import org.keycloak.representations.JsonWebToken;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URI;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.Clock;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
+import java.util.UUID;
+
+import static org.keycloak.protocol.oidc4vp.signing.signatures.EdDSASignatureSignerContext.ED_25519;
 
 public class JWTSigningService extends SigningService<String> {
 
-	private final AlgorithmType algorithmType;
-
-	public JWTSigningService(String keyPath, Optional<String> optionalKeyId){
-		super(keyPath, optionalKeyId);
-		algorithmType = getAlgorithmType();
-	}
-
-	private AlgorithmType getAlgorithmType() {
-		 return AlgorithmType.getByValue(signingKey.getPrivate().getAlgorithm());
-	}
-
-	@Override
-	public String signCredential(VerifiableCredential verifiableCredential) {
-		JwtVerifiableCredential jwtVerifiableCredential = ToJwtConverter.toJwtVerifiableCredential(
-				verifiableCredential);
-		try {
-
-			return switch (algorithmType) {
-				case RSA -> {
-					String concreteAlgorithm = signingKey.getPrivate().getAlgorithm();
-					if (concreteAlgorithm.equalsIgnoreCase("ps256")) {
-						yield jwtVerifiableCredential.sign_RSA_PS256(signingKey);
-					} else {
-						yield jwtVerifiableCredential.sign_RSA_RS256(signingKey);
-					}
-				}
-				case ECDSA_SECP256K1 -> jwtVerifiableCredential.sign_secp256k1_ES256K(
-						ECKey.fromPrivate(signingKey.getPrivate().getEncoded()));
-				case ED_DSA_ED25519 -> jwtVerifiableCredential.sign_Ed25519_EdDSA(signingKey.getPrivate().getEncoded());
-
-			};
-		} catch (JOSEException e) {
-			throw new SigningServiceException("Was not able to sign the credential.", e);
-		}
-	}
+    private static final String ID_TEMPLATE = "urn:uuid:%s";
+
+    private SignatureSignerContext signatureSignerContext;
+
+    public JWTSigningService(KeyLoader keyLoader, Optional<String> optionalKeyId, Clock clock, String algorithmType) {
+        super(keyLoader, optionalKeyId, clock, algorithmType);
+
+        var signingKey = getKeyWrapper(algorithmType);
+        signatureSignerContext = switch (algorithmType) {
+            case ED_25519 -> new EdDSASignatureSignerContext(signingKey);
+            case Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.PS256, Algorithm.PS384, Algorithm.PS512 ->
+                    new AsymmetricSignatureSignerContext(signingKey);
+            case Algorithm.ES256, Algorithm.ES384, Algorithm.ES512 -> new ECDSASignatureSignerContext(signingKey);
+            default ->
+                    throw new SigningServiceException(String.format("Algorithm %s is not supported by the JWTSigningService.", algorithmType));
+        };
+    }
+
+    @Override
+    public String signCredential(VerifiableCredential verifiableCredential) {
+
+        JsonWebToken jsonWebToken = new JsonWebToken();
+        jsonWebToken.exp(clock.instant().plus(1, ChronoUnit.DAYS).getEpochSecond());
+        jsonWebToken.issuer(verifiableCredential.getIssuer().toString());
+        jsonWebToken.nbf(clock.instant().getEpochSecond());
+        jsonWebToken.iat(clock.instant().getEpochSecond());
+        var credentialId = Optional.ofNullable(verifiableCredential.getAdditionalProperties().get("id")).orElse(String.format(ID_TEMPLATE, UUID.randomUUID()));
+        if (credentialId instanceof String idString) {
+            jsonWebToken.id(idString);
+        } else if (credentialId instanceof URI idUri) {
+            jsonWebToken.id(idUri.toString());
+        } else {
+            throw new SigningServiceException("The id needs to be a URI or a string.");
+        }
+        jsonWebToken.subject(verifiableCredential.getCredentialSubject().getId());
+        jsonWebToken.setOtherClaims("vc", verifiableCredential);
+
+        JWSBuilder jwsBuilder = new JWSBuilder();
+        optionalKeyId.ifPresent(jwsBuilder::kid);
+        jwsBuilder.type("JWT");
+        return jwsBuilder.jsonContent(jsonWebToken).sign(signatureSignerContext);
+    }
+
+    private KeyWrapper getKeyWrapper(String algorithm) {
+        KeyPair keyPair = parsePem(keyLoader.loadKey());
+
+        KeyWrapper keyWrapper = new KeyWrapper();
+        optionalKeyId.ifPresent(keyWrapper::setKid);
+
+        keyWrapper.setAlgorithm(algorithm);
+        keyWrapper.setPrivateKey(keyPair.getPrivate());
+
+        if (keyPair.getPublic() != null) {
+            keyWrapper.setPublicKey(keyPair.getPublic());
+            keyWrapper.setKid(KeyUtils.createKeyId(keyPair.getPublic()));
+            keyWrapper.setType(keyPair.getPublic().getAlgorithm());
+        }
+        keyWrapper.setUse(KeyUse.SIG);
+        return keyWrapper;
+    }
+
+    protected KeyPair parsePem(String keyString) {
+        PEMParser pemParser = new PEMParser(new StringReader(keyString));
+        List<Object> parsedObjects = new ArrayList<>();
+        try {
+            var currentObject = pemParser.readObject();
+            while (currentObject != null) {
+                parsedObjects.add(currentObject);
+                currentObject = pemParser.readObject();
+            }
+        } catch (IOException e) {
+            throw new SigningServiceException("Was not able to parse the key-pem", e);
+        }
+        SubjectPublicKeyInfo publicKeyInfo = null;
+        PrivateKeyInfo privateKeyInfo = null;
+        for (Object parsedObject : parsedObjects) {
+            if (parsedObject instanceof SubjectPublicKeyInfo spki) {
+                publicKeyInfo = spki;
+            } else if (parsedObject instanceof PrivateKeyInfo pki) {
+                privateKeyInfo = pki;
+            } else if (parsedObject instanceof PEMKeyPair pkp) {
+                publicKeyInfo = pkp.getPublicKeyInfo();
+                privateKeyInfo = pkp.getPrivateKeyInfo();
+            }
+        }
+        if (privateKeyInfo == null) {
+            throw new SigningServiceException("Was not able to read a private key.");
+        }
+        PublicKey publicKey = null;
+        if (publicKeyInfo != null) {
+            try {
+                KeyFactory keyFactory = KeyFactory.getInstance(publicKeyInfo.getAlgorithm().getAlgorithm().getId());
+                publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyInfo.getEncoded()));
+            } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
+                throw new SigningServiceException("Was not able to get the public key.", e);
+            }
+        }
+        try {
+            KeyFactory privateKeyFactory = KeyFactory.getInstance(
+                    privateKeyInfo.getPrivateKeyAlgorithm().getAlgorithm().getId());
+            PrivateKey privateKey = privateKeyFactory.generatePrivate(
+                    new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded()));
+            return new KeyPair(publicKey, privateKey);
+        } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
+            throw new SigningServiceException("Was not able to get the public key.", e);
+        }
+    }
 }
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/KeyLoader.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/KeyLoader.java
new file mode 100644
index 000000000000..830c7e0f54f6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/KeyLoader.java
@@ -0,0 +1,7 @@
+package org.keycloak.protocol.oidc4vp.signing;
+
+public interface KeyLoader {
+
+    String loadKey();
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/LDSigningService.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/LDSigningService.java
index 43ad89a4072b..c85a46b140a7 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/LDSigningService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/LDSigningService.java
@@ -1,123 +1,64 @@
 package org.keycloak.protocol.oidc4vp.signing;
 
-import com.danubetech.keyformats.crypto.ByteSigner;
-import com.danubetech.keyformats.crypto.impl.Ed25519_EdDSA_PrivateKeySigner;
-import com.danubetech.keyformats.crypto.impl.RSA_PS256_PrivateKeySigner;
-import com.danubetech.keyformats.crypto.impl.RSA_RS256_PrivateKeySigner;
-import com.danubetech.verifiablecredentials.VerifiableCredential;
-import foundation.identity.jsonld.JsonLDException;
-import info.weboftrust.ldsignatures.LdProof;
-import info.weboftrust.ldsignatures.jsonld.LDSecurityKeywords;
-import info.weboftrust.ldsignatures.signer.EcdsaSecp256k1Signature2019LdSigner;
-import info.weboftrust.ldsignatures.signer.Ed25519Signature2018LdSigner;
-import info.weboftrust.ldsignatures.signer.Ed25519Signature2020LdSigner;
-import info.weboftrust.ldsignatures.signer.JcsEd25519Signature2020LdSigner;
-import info.weboftrust.ldsignatures.signer.JsonWebSignature2020LdSigner;
-import info.weboftrust.ldsignatures.signer.LdSigner;
-import info.weboftrust.ldsignatures.signer.RsaSignature2018LdSigner;
-import info.weboftrust.ldsignatures.suites.EcdsaSecp256k1Signature2019SignatureSuite;
-import info.weboftrust.ldsignatures.suites.Ed25519Signature2018SignatureSuite;
-import info.weboftrust.ldsignatures.suites.Ed25519Signature2020SignatureSuite;
-import info.weboftrust.ldsignatures.suites.JcsEd25519Signature2020SignatureSuite;
-import info.weboftrust.ldsignatures.suites.JsonWebSignature2020SignatureSuite;
-import info.weboftrust.ldsignatures.suites.RsaSignature2018SignatureSuite;
-import org.bitcoinj.core.ECKey;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.jboss.logging.Logger;
+import org.keycloak.common.util.Base64;
+import org.keycloak.protocol.oidc4vp.model.LdProof;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
+import org.keycloak.protocol.oidc4vp.signing.signatures.Ed255192018Suite;
+import org.keycloak.protocol.oidc4vp.signing.signatures.RsaSignature2018Suite;
+import org.keycloak.protocol.oidc4vp.signing.signatures.SecuritySuite;
 
 import java.io.IOException;
-import java.security.GeneralSecurityException;
 import java.time.Clock;
 import java.util.Date;
 import java.util.Optional;
 
 public class LDSigningService extends SigningService<VerifiableCredential> {
-	private static final Logger LOGGER = Logger.getLogger(LDSigningService.class);
-
-	private final Clock clock;
-
-	public LDSigningService(String keyPath, Optional<String> keyId,
-			Clock clock) {
-		super(keyPath, keyId);
-		this.clock = clock;
-	}
+    private static final Logger LOGGER = Logger.getLogger(LDSigningService.class);
 
-	@Override
-	public VerifiableCredential signCredential(VerifiableCredential verifiableCredential) {
-		LOGGER.debug("Sign credential with an ld-proof.");
-		String proofType = Optional.ofNullable(verifiableCredential.getLdProof()).map(LdProof::getType)
-				// use a default
-				.orElse(LDSignatureType.RSA_SIGNATURE_2018.getValue());
-		LDSignatureType signatureType = LDSignatureType.getByValue(proofType);
-		AlgorithmType algorithmType = AlgorithmType.getByValue(signingKey.getPrivate().getAlgorithm());
-		var ldSigner = switch (signatureType) {
-			case RSA_SIGNATURE_2018 -> getRsaSigner(algorithmType);
-			case ED_25519_SIGNATURE_2018 -> getEd25519Signature2018Signer(algorithmType);
-			case ED_25519_SIGNATURE_2020 -> getEd25519Signature2020Signer(algorithmType);
-			case ECDSA_SECP_256K1_SIGNATURE_2019 -> getEcdsaSecp256k1Signature2019Signer(algorithmType);
-			case JSON_WEB_SIGNATURE_2020 -> getJsonWebSignature2020Signer(algorithmType);
-			case JCS_ED_25519_SIGNATURE_2020 -> getJcsEd25519Signature2020Signer(algorithmType);
-		};
-		ldSigner.setProofPurpose(LDSecurityKeywords.JSONLD_TERM_ASSERTIONMETHOD);
-		ldSigner.setCreated(Date.from(clock.instant()));
-		try {
-			ldSigner.sign(verifiableCredential);
-		} catch (IOException | GeneralSecurityException | JsonLDException e) {
-			throw new SigningServiceException("Was not able to sign the credential.", e);
-		}
-		return verifiableCredential;
-	}
+    private SecuritySuite securitySuite;
+    private ObjectMapper objectMapper;
 
-	private LdSigner<JcsEd25519Signature2020SignatureSuite> getJcsEd25519Signature2020Signer(AlgorithmType algorithmType) {
-		if (algorithmType != AlgorithmType.ED_DSA_ED25519) {
-			throw new IllegalArgumentException("Signing key does not support JCS_ED_25519_SIGNATURE_2020.");
-		}
-		return new JcsEd25519Signature2020LdSigner(signingKey.getPrivate().getEncoded());
-	}
+    public LDSigningService(KeyLoader keyLoader, Optional<String> keyId,
+                            Clock clock, String ldpType, ObjectMapper objectMapper) {
+        super(keyLoader, keyId, clock, ldpType);
+        this.objectMapper = objectMapper;
 
-	private LdSigner<JsonWebSignature2020SignatureSuite> getJsonWebSignature2020Signer(AlgorithmType algorithmType) {
+        securitySuite = switch (ldpType) {
+            case Ed255192018Suite.PROOF_TYPE -> new Ed255192018Suite(objectMapper);
+            case RsaSignature2018Suite.PROOF_TYPE -> new RsaSignature2018Suite();
+            default -> throw new SigningServiceException(String.format("Proof Type %s is not supported.", ldpType));
+        };
 
-		String concreteAlgorithm = signingKey.getPrivate().getAlgorithm();
+    }
 
-		ByteSigner byteSigner = switch (algorithmType) {
-			case RSA -> {
-				if (concreteAlgorithm.equalsIgnoreCase("rs256")) {
-					yield new RSA_RS256_PrivateKeySigner(signingKey);
-				} else {
-					yield new RSA_PS256_PrivateKeySigner(signingKey);
-				}
-			}
-			case ECDSA_SECP256K1 -> getEcdsaSecp256k1Signature2019Signer(algorithmType).getSigner();
-			case ED_DSA_ED25519 -> new Ed25519_EdDSA_PrivateKeySigner(signingKey.getPrivate().getEncoded());
-		};
-		return new JsonWebSignature2020LdSigner(byteSigner);
-	}
+    @Override
+    public VerifiableCredential signCredential(VerifiableCredential verifiableCredential) {
+        return addProof(verifiableCredential);
+    }
 
-	private LdSigner<EcdsaSecp256k1Signature2019SignatureSuite> getEcdsaSecp256k1Signature2019Signer(AlgorithmType algorithmType) {
-		if (algorithmType != AlgorithmType.ECDSA_SECP256K1) {
-			throw new IllegalArgumentException("Signing key does not support ECDSA_SECP_256K1_SIGNATURE_2019.");
-		}
-		return new EcdsaSecp256k1Signature2019LdSigner(ECKey.fromPrivate(signingKey.getPrivate().getEncoded()));
-	}
 
-	private LdSigner<Ed25519Signature2018SignatureSuite> getEd25519Signature2018Signer(AlgorithmType algorithmType) {
-		if (algorithmType != AlgorithmType.ED_DSA_ED25519) {
-			throw new IllegalArgumentException("Signing key does not support ED_25519_SIGNATURE_2018.");
-		}
-		return new Ed25519Signature2018LdSigner(signingKey.getPrivate().getEncoded());
-	}
+    private VerifiableCredential addProof(VerifiableCredential verifiableCredential) {
 
-	private LdSigner<Ed25519Signature2020SignatureSuite> getEd25519Signature2020Signer(AlgorithmType algorithmType) {
-		if (algorithmType != AlgorithmType.ED_DSA_ED25519) {
-			throw new IllegalArgumentException("Signing key does not support ED_25519_SIGNATURE_2020.");
-		}
-		return new Ed25519Signature2020LdSigner(signingKey.getPrivate().getEncoded());
-	}
+        byte[] transformedData = securitySuite.transform(verifiableCredential);
+        byte[] hashedData = securitySuite.digest(transformedData);
+        byte[] signature = securitySuite.sign(hashedData, keyLoader.loadKey());
+        LdProof ldProof = new LdProof();
+        ldProof.setProofPurpose("assertionMethod");
+        ldProof.setType(securitySuite.getProofType());
+        ldProof.setCreated(Date.from(clock.instant()));
+        optionalKeyId.ifPresent(ldProof::setVerificationMethod);
 
-	private LdSigner<RsaSignature2018SignatureSuite> getRsaSigner(AlgorithmType algorithmType) {
-		if (algorithmType != AlgorithmType.RSA) {
-			throw new IllegalArgumentException("Signing key does not support RSA_SIGNATURE_2018.");
-		}
-		return new RsaSignature2018LdSigner(signingKey);
-	}
+        try {
+            var proofValue = Base64.encodeBytes(signature, Base64.URL_SAFE);
+            ldProof.setProofValue(proofValue);
+            verifiableCredential.setProof(ldProof);
+            return verifiableCredential;
+        } catch (IOException e) {
+            throw new SigningServiceException("Was not able to encode the signature.", e);
+        }
+    }
 
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/SigningService.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/SigningService.java
index eb6e0c00251e..e47dcd6503d0 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/SigningService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/SigningService.java
@@ -1,96 +1,26 @@
 package org.keycloak.protocol.oidc4vp.signing;
 
-import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
-import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
-import org.bouncycastle.openssl.PEMKeyPair;
-import org.bouncycastle.openssl.PEMParser;
 import org.jboss.logging.Logger;
 
-import java.io.IOException;
-import java.io.StringReader;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.security.KeyFactory;
-import java.security.KeyPair;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.PKCS8EncodedKeySpec;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.ArrayList;
-import java.util.List;
+import java.time.Clock;
 import java.util.Optional;
 
 public abstract class SigningService<T> implements VCSigningService<T> {
 
-	private static final Logger LOGGER = Logger.getLogger(SigningService.class);
+    private static final Logger LOGGER = Logger.getLogger(SigningService.class);
 
-	protected final KeyPair signingKey;
-	protected final Optional<String> optionalKeyId;
+    protected final KeyLoader keyLoader;
+    protected final Optional<String> optionalKeyId;
+    protected final Clock clock;
+    // values of the type field are defined by the implementing service. Could f.e. the security suite for ldp_vc or the algorithm to be used for jwt_vc
+    protected final String type;
 
-	protected SigningService(String keyPath, Optional<String> optionalKeyId) {
-		this.signingKey = parsePem(loadPrivateKeyString(keyPath));
-		this.optionalKeyId = optionalKeyId;
-	}
+    protected SigningService(KeyLoader keyLoader, Optional<String> optionalKeyId, Clock clock, String type) {
+        this.keyLoader = keyLoader;
+        this.optionalKeyId = optionalKeyId;
+        this.clock = clock;
+        this.type = type;
+    }
 
-	protected String loadPrivateKeyString(String keyPath) {
-		Path keyFilePath = Paths.get(keyPath);
-		try {
-			return Files.readString(keyFilePath);
-		} catch (IOException e) {
-			LOGGER.errorf("Was not able to read the private key from %s", keyPath);
-			throw new SigningServiceException("Was not able to read private key. Cannot initiate the SigningService.",
-					e);
-		}
-	}
-
-	protected KeyPair parsePem(String keyString) {
-		PEMParser pemParser = new PEMParser(new StringReader(keyString));
-		List<Object> parsedObjects = new ArrayList<>();
-		try {
-			var currentObject = pemParser.readObject();
-			while (currentObject != null) {
-				parsedObjects.add(currentObject);
-				currentObject = pemParser.readObject();
-			}
-		} catch (IOException e) {
-			throw new SigningServiceException("Was not able to parse the key-pem");
-		}
-		SubjectPublicKeyInfo publicKeyInfo = null;
-		PrivateKeyInfo privateKeyInfo = null;
-		for (Object parsedObject : parsedObjects) {
-			if (parsedObject instanceof SubjectPublicKeyInfo spki) {
-				publicKeyInfo = spki;
-			} else if (parsedObject instanceof PrivateKeyInfo pki) {
-				privateKeyInfo = pki;
-			} else if (parsedObject instanceof PEMKeyPair pkp) {
-				publicKeyInfo = pkp.getPublicKeyInfo();
-				privateKeyInfo = pkp.getPrivateKeyInfo();
-			}
-		}
-		if (privateKeyInfo == null) {
-			throw new SigningServiceException("Was not able to read a private key.");
-		}
-		PublicKey publicKey = null;
-		if (publicKeyInfo != null) {
-			try {
-				KeyFactory keyFactory = KeyFactory.getInstance(publicKeyInfo.getAlgorithm().getAlgorithm().getId());
-				publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyInfo.getEncoded()));
-			} catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
-				throw new SigningServiceException("Was not able to get the public key.", e);
-			}
-		}
-		try {
-			KeyFactory privateKeyFactory = KeyFactory.getInstance(
-					privateKeyInfo.getPrivateKeyAlgorithm().getAlgorithm().getId());
-			PrivateKey privateKey = privateKeyFactory.generatePrivate(
-					new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded()));
-			return new KeyPair(publicKey, privateKey);
-		} catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
-			throw new SigningServiceException("Was not able to get the public key.", e);
-		}
-	}
 
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/VCSigningService.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/VCSigningService.java
index e3dc7d88ff9b..b7bb6259c6a8 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/VCSigningService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/VCSigningService.java
@@ -1,8 +1,9 @@
 package org.keycloak.protocol.oidc4vp.signing;
 
-import com.danubetech.verifiablecredentials.VerifiableCredential;
+
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
 
 public interface VCSigningService<T> {
 
-	T signCredential(VerifiableCredential verifiableCredential);
+    T signCredential(VerifiableCredential verifiableCredential);
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/Ed255192018Suite.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/Ed255192018Suite.java
new file mode 100644
index 000000000000..e5f26fc62d77
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/Ed255192018Suite.java
@@ -0,0 +1,133 @@
+package org.keycloak.protocol.oidc4vp.signing.signatures;
+
+import com.apicatalog.jsonld.JsonLd;
+import com.apicatalog.jsonld.JsonLdError;
+import com.apicatalog.jsonld.document.JsonDocument;
+import com.apicatalog.jsonld.http.DefaultHttpClient;
+import com.apicatalog.jsonld.http.media.MediaType;
+import com.apicatalog.jsonld.json.JsonUtils;
+import com.apicatalog.jsonld.loader.HttpLoader;
+import com.apicatalog.rdf.Rdf;
+import com.apicatalog.rdf.RdfDataset;
+import com.apicatalog.rdf.io.RdfWriter;
+import com.apicatalog.rdf.io.error.RdfWriterException;
+import com.apicatalog.rdf.io.error.UnsupportedContentException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.setl.rdf.normalization.RdfNormalize;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonValue;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
+import org.bouncycastle.crypto.signers.Ed25519Signer;
+import org.bouncycastle.crypto.util.PrivateKeyFactory;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
+import org.keycloak.protocol.oidc4vp.signing.SigningServiceException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Optional;
+
+public class Ed255192018Suite implements SecuritySuite {
+
+    private final ObjectMapper objectMapper;
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    private static final String CANONICALIZATION_ALGORITHM = "https://w3id.org/security#URDNA2015";
+    private static final String DIGEST_ALGORITHM = "http://w3id.org/digests#sha256";
+    private static final String SIGNATURE_ALGORITHM = "http://w3id.org/security#ed25519";
+
+    public static final String PROOF_TYPE = "Ed25519Signature2018";
+
+    public Ed255192018Suite(ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public byte[] transform(VerifiableCredential verifiableCredential) {
+
+        try {
+            String credentialString = objectMapper.writeValueAsString(verifiableCredential);
+
+            var credentialDocument = JsonDocument.of(new StringReader(credentialString));
+
+            var expandedDocument = JsonLd.expand(credentialDocument)
+                    .loader(new HttpLoader(DefaultHttpClient.defaultInstance()))
+                    .get();
+            Optional<JsonObject> documentObject = Optional.empty();
+            if (JsonUtils.isArray(expandedDocument)) {
+                documentObject = expandedDocument.asJsonArray().stream().filter(JsonUtils::isObject).map(JsonValue::asJsonObject).findFirst();
+            } else if (JsonUtils.isObject(expandedDocument)) {
+                documentObject = Optional.of(expandedDocument.asJsonObject());
+            }
+            if (documentObject.isPresent()) {
+
+                RdfDataset rdfDataset = JsonLd.toRdf(JsonDocument.of(documentObject.get())).get();
+                RdfDataset canonicalDataset = RdfNormalize.normalize(rdfDataset);
+
+                StringWriter writer = new StringWriter();
+                RdfWriter rdfWriter = Rdf.createWriter(MediaType.N_QUADS, writer);
+                rdfWriter.write(canonicalDataset);
+
+                return writer.toString()
+                        .getBytes(StandardCharsets.UTF_8);
+            } else {
+                throw new SigningServiceException("Was not able to get the expanded json.");
+            }
+        } catch (JsonProcessingException e) {
+            throw new SigningServiceException("Was not able to serialize the credential", e);
+        } catch (JsonLdError e) {
+            throw new SigningServiceException("Was not able to create a JsonLD Document from the serialized string.", e);
+        } catch (UnsupportedContentException | IOException | RdfWriterException e) {
+            throw new SigningServiceException("Was not able to canonicalize the json-ld.", e);
+        }
+
+    }
+
+    @Override
+    public byte[] digest(byte[] transformedData) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-256");
+            return md.digest(transformedData);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public byte[] sign(byte[] hashData, String key) {
+        Ed25519Signer signer = new Ed25519Signer();
+        signer.init(true, parseKey(key));
+        signer.update(hashData, 0, hashData.length);
+        return signer.generateSignature();
+    }
+
+
+    private static AsymmetricKeyParameter parseKey(String key) {
+        PEMParser pemReaderPrivate = new PEMParser(new StringReader(key));
+        try {
+            var pemObject = pemReaderPrivate.readObject();
+            if (pemObject instanceof PEMKeyPair pkp) {
+                return PrivateKeyFactory.createKey(pkp.getPrivateKeyInfo());
+            } else if (pemObject instanceof PrivateKeyInfo pki) {
+                return PrivateKeyFactory.createKey(pki);
+            } else {
+                throw new RuntimeException();
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public String getProofType() {
+        return PROOF_TYPE;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/EdDSASignatureSignerContext.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/EdDSASignatureSignerContext.java
new file mode 100644
index 000000000000..b6dbe60d1641
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/EdDSASignatureSignerContext.java
@@ -0,0 +1,48 @@
+package org.keycloak.protocol.oidc4vp.signing.signatures;
+
+import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey;
+import org.keycloak.crypto.KeyWrapper;
+import org.keycloak.crypto.SignatureException;
+import org.keycloak.crypto.SignatureSignerContext;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
+
+public class EdDSASignatureSignerContext implements SignatureSignerContext {
+
+    public static final String ED_25519 = "Ed25519";
+
+    private final KeyWrapper key;
+
+    public EdDSASignatureSignerContext(KeyWrapper key) {
+        this.key = key;
+    }
+
+    @Override
+    public String getKid() {
+        return key.getKid();
+    }
+
+    @Override
+    public String getAlgorithm() {
+        return key.getAlgorithm();
+    }
+
+    @Override
+    public String getHashAlgorithm() {
+        return key.getAlgorithm();
+    }
+
+    @Override
+    public byte[] sign(byte[] data) throws SignatureException {
+        try {
+            Signature signature = Signature.getInstance(key.getAlgorithm());
+            signature.initSign((PrivateKey) key.getPrivateKey());
+            signature.update(data);
+            return signature.sign();
+        } catch (Exception e) {
+            throw new SignatureException("Signing failed", e);
+        }
+    }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/RsaSignature2018Suite.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/RsaSignature2018Suite.java
new file mode 100644
index 000000000000..2670be7913c8
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/RsaSignature2018Suite.java
@@ -0,0 +1,48 @@
+package org.keycloak.protocol.oidc4vp.signing.signatures;
+
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
+import org.keycloak.protocol.oidc4vp.signing.SigningServiceException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RsaSignature2018Suite implements SecuritySuite {
+
+    private static final String CANONICALIZATION_ALGORITHM = "https://w3id.org/security#GCA2015";
+    private static final String DIGEST_ALGORITHM = "https://www.ietf.org/assignments/jwa-parameters#SHA256";
+    private static final String SIGNATURE_ALGORITHM = "https://www.ietf.org/assignments/jwa-parameters#RS256";
+
+    public static final String PROOF_TYPE = "RsaSignature2018";
+
+    @Override
+    public byte[] transform(VerifiableCredential verifiableCredential) {
+        return new byte[0];
+    }
+
+    @Override
+    public byte[] digest(byte[] transformedData) {
+        return new byte[0];
+    }
+
+    @Override
+    public byte[] sign(byte[] hashData, String key) {
+        return new byte[0];
+    }
+
+    @Override
+    public String getProofType() {
+        return PROOF_TYPE;
+    }
+}
+
+
diff --git a/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/SecuritySuite.java b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/SecuritySuite.java
new file mode 100644
index 000000000000..06bf48b5bae7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc4vp/signing/signatures/SecuritySuite.java
@@ -0,0 +1,29 @@
+package org.keycloak.protocol.oidc4vp.signing.signatures;
+
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
+import org.keycloak.protocol.oidc4vp.signing.SigningServiceException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.List;
+
+public interface SecuritySuite {
+
+    byte[] transform(VerifiableCredential verifiableCredential);
+
+    byte[] digest(byte[] transformedData);
+
+    byte[] sign(byte[] hashData, String key);
+
+    String getProofType();
+
+}
diff --git a/services/src/test/java/org/keycloak/protocol/oidc4vp/OIDC4VPIssuerEndpointTest.java b/services/src/test/java/org/keycloak/protocol/oidc4vp/OIDC4VPIssuerEndpointTest.java
index 572e0fd1e995..e4e8dce4f294 100644
--- a/services/src/test/java/org/keycloak/protocol/oidc4vp/OIDC4VPIssuerEndpointTest.java
+++ b/services/src/test/java/org/keycloak/protocol/oidc4vp/OIDC4VPIssuerEndpointTest.java
@@ -1,6 +1,5 @@
 package org.keycloak.protocol.oidc4vp;
 
-import com.danubetech.verifiablecredentials.CredentialSubject;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.MapperFeature;
@@ -10,6 +9,7 @@
 import jakarta.ws.rs.WebApplicationException;
 import jakarta.ws.rs.core.Response;
 import lombok.extern.slf4j.Slf4j;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -17,45 +17,26 @@
 import org.junit.jupiter.params.provider.MethodSource;
 import org.keycloak.TokenVerifier;
 import org.keycloak.common.VerificationException;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientProvider;
-import org.keycloak.models.KeycloakContext;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
-import org.keycloak.protocol.oidc4vp.mappers.OIDC4VPStaticClaimMapper;
-import org.keycloak.protocol.oidc4vp.mappers.OIDC4VPSubjectIdMapper;
-import org.keycloak.protocol.oidc4vp.mappers.OIDC4VPTargetRoleMapper;
-import org.keycloak.protocol.oidc4vp.mappers.OIDC4VPUserAttributeMapper;
+import org.keycloak.models.*;
+import org.keycloak.protocol.oidc4vp.mappers.*;
+import org.keycloak.protocol.oidc4vp.model.*;
 import org.keycloak.protocol.oidc4vp.model.ErrorResponse;
-import org.keycloak.protocol.oidc4vp.model.ErrorType;
 import org.keycloak.protocol.oidc4vp.model.Format;
-import org.keycloak.protocol.oidc4vp.model.Role;
 import org.keycloak.protocol.oidc4vp.model.SupportedCredential;
 import org.keycloak.representations.JsonWebToken;
 import org.keycloak.services.managers.AppAuthManager;
 import org.keycloak.services.managers.AuthenticationManager;
 
 import java.net.URL;
+import java.security.Security;
 import java.time.Clock;
 import java.time.Instant;
 import java.time.ZoneId;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
@@ -64,696 +45,769 @@
 @Slf4j
 public class OIDC4VPIssuerEndpointTest {
 
-	private static final String ISSUER_DID = "did:key:test";
-
-	private final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
-			.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
-			.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true).build();
-
-	private KeycloakSession keycloakSession;
-	private AppAuthManager.BearerTokenAuthenticator bearerTokenAuthenticator;
-
-	private OIDC4VPIssuerEndpoint testEndpoint;
-
-	private Clock fixedClock = Clock.fixed(Instant.parse("2022-11-10T17:11:09.00Z"),
-			ZoneId.of("Europe/Paris"));
-
-	@BeforeEach
-	public void setUp() throws NoSuchFieldException {
-		URL url = getClass().getClassLoader().getResource("key.tls");
-
-		this.keycloakSession = mock(KeycloakSession.class);
-		this.bearerTokenAuthenticator = mock(AppAuthManager.BearerTokenAuthenticator.class);
-		this.testEndpoint = new OIDC4VPIssuerEndpoint(keycloakSession, ISSUER_DID, url.getPath(),
-				bearerTokenAuthenticator, new ObjectMapper(), fixedClock);
-	}
-
-	@Test
-	public void testGetVCUnauthorized() {
-		KeycloakContext context = mock(KeycloakContext.class);
-		RealmModel realmModel = mock(RealmModel.class);
-		when(keycloakSession.getContext()).thenReturn(context);
-		when(context.getRealm()).thenReturn(realmModel);
-
-		when(bearerTokenAuthenticator.authenticate()).thenReturn(null);
-
-		try {
-			testEndpoint.issueVerifiableCredential(ISSUER_DID, "MyVC");
-			fail("VCs should only be accessible for authorized users.");
-		} catch (WebApplicationException e) {
-			assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), e.getResponse().getStatus(),
-					"The response should be a 400.");
-			ErrorResponse er = OBJECT_MAPPER.convertValue(e.getResponse().getEntity(), ErrorResponse.class);
-			assertEquals(ErrorType.INVALID_TOKEN.getValue(), er.getError().value(),
-					"The response should have been denied because of the invalid token.");
-		}
-	}
-
-	@ParameterizedTest
-	@MethodSource("provideTypesAndClients")
-	public void testGetVCNoSuchType(Stream<ClientModel> clientModelStream,
-			ExpectedResult<Set<SupportedCredential>> ignored) {
-		AuthenticationManager.AuthResult authResult = mock(AuthenticationManager.AuthResult.class);
-		UserModel userModel = mock(UserModel.class);
-		KeycloakContext context = mock(KeycloakContext.class);
-		RealmModel realmModel = mock(RealmModel.class);
-		ClientProvider clientProvider = mock(ClientProvider.class);
-
-		when(bearerTokenAuthenticator.authenticate()).thenReturn(authResult);
-		when(authResult.getUser()).thenReturn(userModel);
-		when(keycloakSession.getContext()).thenReturn(context);
-		when(context.getRealm()).thenReturn(realmModel);
-		when(keycloakSession.clients()).thenReturn(clientProvider);
-		when(clientProvider.getClientsStream(any())).thenReturn(clientModelStream);
-
-		try {
-			testEndpoint.issueVerifiableCredential(ISSUER_DID, "MyNonExistentType");
-			fail("Not found types should be a 400");
-		} catch (WebApplicationException e) {
-			assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), e.getResponse().getStatus(),
-					"Not found types should be a 400");
-			ErrorResponse er = OBJECT_MAPPER.convertValue(e.getResponse().getEntity(), ErrorResponse.class);
-			assertEquals(ErrorType.UNSUPPORTED_CREDENTIAL_TYPE.getValue(), er.getError().value(),
-					"The response should have been denied because of the unsupported type.");
-		}
-	}
-
-	@ParameterizedTest
-	@MethodSource("provideUserAndClients")
-	public void testGetCredential(UserModel userModel, Stream<ClientModel> clientModelStream,
-			Map<ClientModel, Stream<RoleModel>> roleModelStreamMap,
-			ExpectedResult<Map> expectedResult, Format requestedFormat)
-			throws JsonProcessingException, VerificationException {
-		List<ClientModel> clientModels = clientModelStream.toList();
-
-		AuthenticationManager.AuthResult authResult = mock(AuthenticationManager.AuthResult.class);
-		KeycloakContext context = mock(KeycloakContext.class);
-		RealmModel realmModel = mock(RealmModel.class);
-		ClientProvider clientProvider = mock(ClientProvider.class);
-
-		UserSessionModel userSessionModel = mock(UserSessionModel.class);
-		when(userSessionModel.getRealm()).thenReturn(realmModel);
-		when(userSessionModel.getUser()).thenReturn(userModel);
-		clientModels.forEach(cm -> when(realmModel.getClientByClientId(eq(cm.getClientId()))).thenReturn(cm));
-		when(realmModel.getClientsStream()).thenReturn(clientModels.stream());
-
-		when(bearerTokenAuthenticator.authenticate()).thenReturn(authResult);
-
-		when(authResult.getUser()).thenReturn(userModel);
-		when(authResult.getSession()).thenReturn(userSessionModel);
-
-		when(keycloakSession.getContext()).thenReturn(context);
-		when(context.getRealm()).thenReturn(realmModel);
-
-		when(keycloakSession.clients()).thenReturn(clientProvider);
-		when(clientProvider.getClientsStream(any())).thenReturn(clientModels.stream());
-
-		when(userModel.getClientRoleMappingsStream(any())).thenAnswer(i -> roleModelStreamMap.get(i.getArguments()[0]));
-		Object credential = testEndpoint.getCredential("MyType", requestedFormat);
-		switch (requestedFormat) {
-			case LDP_VC -> {
-				Map verifiableCredential = OBJECT_MAPPER.convertValue(credential, Map.class);
-				verifyLDCredential(expectedResult, verifiableCredential);
-			}
-			case JWT_VC_JSON_LD, JWT_VC, JWT_VC_JSON -> verifyJWTCredential(expectedResult, (String) credential);
-		}
-	}
-
-	private void verifyJWTCredential(ExpectedResult<Map> expectedResult, String actualResult)
-			throws VerificationException, JsonProcessingException {
-		TokenVerifier<JsonWebToken> verifier = TokenVerifier.create(actualResult, JsonWebToken.class);
-		JsonWebToken theJWT = verifier.getToken();
-		assertEquals(ISSUER_DID, theJWT.getIssuer(), "The issuer should be properly set.");
-		assertNotNull(theJWT.getSubject(), "A subject should be set.");
-		assertNotNull(theJWT.getId(), "The jwt should have an id.");
-
-		Map theVC = (Map) theJWT.getOtherClaims().get("vc");
-		assertNotNull(theVC, "The vc should be part of the jwt.");
-		List credentialType = (List) theVC.get("type");
-		assertEquals(2, credentialType.size(), "Both types should be included.");
-		assertTrue(credentialType.contains("MyType") && credentialType.contains("VerifiableCredential"),
-				"The correct types should be included.");
-
-		Map retrievedSubject = (Map) theVC.get("credentialSubject");
-		Map expectedCredentialSubject = new HashMap(expectedResult.getExpectedResult());
-
-		verifySubject(expectedResult, expectedCredentialSubject, retrievedSubject);
-
-	}
-
-	@ParameterizedTest
-	@MethodSource("provideUserAndClientsLDP")
-	public void testGetVC(UserModel userModel, Stream<ClientModel> clientModelStream,
-			Map<ClientModel, Stream<RoleModel>> roleModelStreamMap,
-			ExpectedResult<Map> expectedResult) throws JsonProcessingException {
-		List<ClientModel> clientModels = clientModelStream.toList();
-
-		AuthenticationManager.AuthResult authResult = mock(AuthenticationManager.AuthResult.class);
-		KeycloakContext context = mock(KeycloakContext.class);
-		RealmModel realmModel = mock(RealmModel.class);
-		ClientProvider clientProvider = mock(ClientProvider.class);
-		UserSessionModel userSessionModel = mock(UserSessionModel.class);
-		when(userSessionModel.getRealm()).thenReturn(realmModel);
-		when(userSessionModel.getUser()).thenReturn(userModel);
-		clientModels.forEach(cm -> when(realmModel.getClientByClientId(eq(cm.getClientId()))).thenReturn(cm));
-
-		when(bearerTokenAuthenticator.authenticate()).thenReturn(authResult);
-		when(authResult.getUser()).thenReturn(userModel);
-		when(authResult.getSession()).thenReturn(userSessionModel);
-		when(keycloakSession.getContext()).thenReturn(context);
-		when(context.getRealm()).thenReturn(realmModel);
-		when(keycloakSession.clients()).thenReturn(clientProvider);
-		// use then to open a new stream on each invocation
-		when(clientProvider.getClientsStream(any())).then(f -> clientModels.stream());
-
-		when(userModel.getClientRoleMappingsStream(any())).thenAnswer(i -> roleModelStreamMap.get(i.getArguments()[0]));
-
-		Map credentialVO = OBJECT_MAPPER.convertValue(
-				testEndpoint.issueVerifiableCredential("MyType", "myToken").getEntity(),
-				Map.class);
-
-		verifyLDCredential(expectedResult, credentialVO);
-	}
-
-	private void verifyLDCredential(ExpectedResult<Map> expectedResult, Map credentialVO)
-			throws JsonProcessingException {
-		assertEquals("2022-11-10T17:11:09Z", credentialVO.get("issuanceDate"),
-				"The issuance data should be correctly set.");
-		assertNotNull(credentialVO.get("@context"), "The context should be set on an ld-credential.");
-		assertNotNull(credentialVO.get("proof"), "The proof should be included.");
-		assertNotNull(credentialVO.get("id"), "The credential should have an id.");
-		List credentialType = (List) credentialVO.get("type");
-		assertEquals(2, credentialType.size(), "Both types should be included.");
-		assertTrue(credentialType.contains("MyType") && credentialType.contains("VerifiableCredential"),
-				"The correct types should be included.");
-
-		assertEquals(ISSUER_DID, credentialVO.get("issuer"), "The correct issuer should be set.");
-
-		Map expectedCredentialSubject = new HashMap(expectedResult.getExpectedResult());
-		Map retrievedSubject = (Map) credentialVO.get("credentialSubject");
-		assertNotNull(retrievedSubject.get("id"), "The id should have been set.");
-		// remove the id, since its randomly generated.
-		retrievedSubject.remove("id");
-
-		verifySubject(expectedResult, expectedCredentialSubject, retrievedSubject);
-	}
-
-	private void verifySubject(ExpectedResult<Map> expectedResult, Map expectedCredentialSubject, Map retrievedSubject)
-			throws JsonProcessingException {
-		verifyRoles(expectedResult.getMessage(), expectedCredentialSubject, retrievedSubject);
-		// roles are checked, can be removed to not interfer with next checks.
-		expectedCredentialSubject.remove("roles");
-		retrievedSubject.remove("roles");
-
-		String expectedJson = OBJECT_MAPPER.writeValueAsString(expectedCredentialSubject);
-		String retrievedJson = OBJECT_MAPPER.writeValueAsString(retrievedSubject);
-		// we compare the json, to prevent order issues.
-		assertEquals(expectedJson, retrievedJson, expectedResult.getMessage());
-	}
-
-	private void verifyRoles(String message, Map expectedCredentialSubject, Map retrievedSubject) {
-		Set<Role> retrievedRoles = OBJECT_MAPPER.convertValue(retrievedSubject.get("roles"),
-				new TypeReference<Set<Role>>() {
-				});
-		Set<Role> expectedRoles = OBJECT_MAPPER.convertValue(expectedCredentialSubject.get("roles"),
-				new TypeReference<Set<Role>>() {
-				});
-		assertEquals(expectedRoles, retrievedRoles, message);
-	}
-
-	private static Arguments getArguments(UserModel um, Map<ClientModel, List<RoleModel>> clients,
-			ExpectedResult expectedResult) {
-		return Arguments.of(um,
-				clients.keySet().stream(),
-				clients.entrySet()
-						.stream()
-						.filter(e -> e.getValue() != null)
-						.collect(
-								Collectors.toMap(Map.Entry::getKey, e -> ((List) e.getValue()).stream(),
-										(e1, e2) -> e1)),
-				expectedResult);
-	}
-
-	private static Stream<Arguments> provideUserAndClients() {
-		return Stream.concat(provideUserAndClientsLDP().map(a -> {
-					var argObjects = new ArrayList<>(Arrays.asList(a.get()));
-					argObjects.add(Format.LDP_VC);
-					return Arguments.of(argObjects.toArray());
-				}),
-				provideUserAndClientsJWT().map(a -> {
-					var argObjects = new ArrayList<>(Arrays.asList(a.get()));
-					argObjects.add(Format.JWT_VC);
-					return Arguments.of(argObjects.toArray());
-				}));
-	}
-
-	private static Stream<Arguments> provideUserAndClientsJWT() {
-		return Stream.of(
-				getArguments(getUserModel("e@mail.org", "Happy", "User"),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("MyRole")),
-								List.of(getRoleModel("MyRole"))),
-						new ExpectedResult<>(
-								Map.of("email", "e@mail.org", "familyName", "User", "firstName", "Happy", "roles",
-										Set.of(new Role(Set.of("MyRole"), "did:key:1"))),
-								"A valid Credential should have been returned.")
-				),
-				getArguments(getUserModel("e@mail.org", null, "User"),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("MyRole")),
-								List.of(getRoleModel("MyRole"))),
-						new ExpectedResult<>(
-								Map.of("email", "e@mail.org", "familyName", "User", "roles",
-										Set.of(new Role(Set.of("MyRole"), "did:key:1"))),
-								"A valid Credential should have been returned.")
-				),
-				getArguments(
-						getUserModel("e@mail.org", null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("MyRole")),
-								List.of(getRoleModel("MyRole"))),
-						new ExpectedResult<>(
-								Map.of("email", "e@mail.org", "roles",
-										Set.of(new Role(Set.of("MyRole"), "did:key:1"))),
-								"A valid Credential should have been returned.")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("MyRole")),
-								List.of(getRoleModel("MyRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole"), "did:key:1"))),
-								"A valid Credential should have been returned.")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("MyRole", "MySecondRole")),
-								List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"))),
-								"Multiple roles should be included")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("MyRole", "MySecondRole")),
-								List.of(getRoleModel("MyRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole"), "did:key:1"))),
-								"Only assigned roles should be included.")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("MyRole", "MySecondRole")),
-								List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
-								getOidc4VpClient("did:key:2",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("AnotherRole")),
-								List.of(getRoleModel("AnotherRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
-												new Role(Set.of("AnotherRole"), "did:key:2"))),
-								"The request should contain roles from both clients")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("MyRole", "MySecondRole")),
-								List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
-								getOidc4VpClient("did:key:2",
-										Map.of("vctypes_AnotherType", Format.JWT_VC.toString()),
-										List.of("AnotherRole")),
-								List.of(getRoleModel("AnotherRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"))),
-								"Only roles for supported clients should be included.")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("MyRole", "MySecondRole"),
-										Map.of("more", "claims")),
-								List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
-								getOidc4VpClient("did:key:2",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("AnotherRole"),
-										Map.of("additional", "claim")),
-								List.of(getRoleModel("AnotherRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
-												new Role(Set.of("AnotherRole"), "did:key:2")),
-										"additional", "claim", "more", "claims"),
-								"Additional claims should be included.")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("MyRole", "MySecondRole")),
-								List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
-								getOidc4VpClient("did:key:2",
-										Map.of("vctypes_MyType", Format.JWT_VC.toString()),
-										List.of("AnotherRole"),
-										Map.of("additional", "claim")),
-								List.of(getRoleModel("AnotherRole"))),
-						new ExpectedResult<>(
-								Map.of("additional", "claim", "roles",
-										Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
-												new Role(Set.of("AnotherRole"), "did:key:2"))),
-								"Additional claims should be included.")
-				)
-		);
-	}
-
-	private static Stream<Arguments> provideUserAndClientsLDP() {
-		return Stream.of(
-				getArguments(getUserModel("e@mail.org", "Happy", "User"),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("MyRole")),
-								List.of(getRoleModel("MyRole"))),
-						new ExpectedResult<>(
-								Map.of("email", "e@mail.org", "familyName", "User", "firstName", "Happy", "roles",
-										Set.of(new Role(Set.of("MyRole"), "did:key:1"))),
-								"A valid Credential should have been returned.")
-				),
-				getArguments(getUserModel("e@mail.org", null, "User"),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("MyRole")),
-								List.of(getRoleModel("MyRole"))),
-						new ExpectedResult<>(
-								Map.of("email", "e@mail.org", "familyName", "User", "roles",
-										Set.of(new Role(Set.of("MyRole"), "did:key:1"))),
-								"A valid Credential should have been returned.")
-				),
-				getArguments(
-						getUserModel("e@mail.org", null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("MyRole")),
-								List.of(getRoleModel("MyRole"))),
-						new ExpectedResult<>(
-								Map.of("email", "e@mail.org", "roles",
-										Set.of(new Role(Set.of("MyRole"), "did:key:1"))),
-								"A valid Credential should have been returned.")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("MyRole")),
-								List.of(getRoleModel("MyRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole"), "did:key:1"))),
-								"A valid Credential should have been returned.")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("MyRole", "MySecondRole")),
-								List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"))),
-								"Multiple roles should be included")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("MyRole", "MySecondRole")),
-								List.of(getRoleModel("MyRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole"), "did:key:1"))),
-								"Only assigned roles should be included.")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("MyRole", "MySecondRole")),
-								List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
-								getOidc4VpClient("did:key:2",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("AnotherRole")),
-								List.of(getRoleModel("AnotherRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
-												new Role(Set.of("AnotherRole"), "did:key:2"))),
-								"The request should contain roles from both clients")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("MyRole", "MySecondRole")),
-								List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
-								getOidc4VpClient("did:key:2",
-										Map.of("vctypes_AnotherType", Format.LDP_VC.toString()),
-										List.of("AnotherRole")),
-								List.of(getRoleModel("AnotherRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"))),
-								"Only roles for supported clients should be included.")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("MyRole", "MySecondRole"),
-										Map.of("more", "claims")),
-								List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
-								getOidc4VpClient("did:key:2",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("AnotherRole"),
-										Map.of("additional", "claim")),
-								List.of(getRoleModel("AnotherRole"))),
-						new ExpectedResult<>(
-								Map.of("roles",
-										Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
-												new Role(Set.of("AnotherRole"), "did:key:2")),
-										"additional", "claim", "more", "claims"),
-								"Additional claims should be included.")
-				),
-				getArguments(
-						getUserModel(null, null, null),
-						Map.of(getOidc4VpClient("did:key:1",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("MyRole", "MySecondRole")),
-								List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
-								getOidc4VpClient("did:key:2",
-										Map.of("vctypes_MyType", Format.LDP_VC.toString()),
-										List.of("AnotherRole"),
-										Map.of("additional", "claim")),
-								List.of(getRoleModel("AnotherRole"))),
-						new ExpectedResult<>(
-								Map.of("additional", "claim", "roles",
-										Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
-												new Role(Set.of("AnotherRole"), "did:key:2"))),
-								"Additional claims should be included.")
-				)
-		);
-	}
-
-	private static Stream<Arguments> provideTypesAndClients() {
-		return Stream.of(
-				Arguments.of(Stream.of(getOidcClient(), getNullClient(), getOidc4VpClient(
-								Map.of("vctypes_TestType", Format.LDP_VC.toString()))),
-						new ExpectedResult<>(Set.of(getCredential("TestType", Format.LDP_VC)),
-								"The list of configured types should be returned.")),
-				Arguments.of(Stream.of(getOidcClient(), getNullClient()),
-						new ExpectedResult<>(Set.of(), "An empty list should be returned if nothing is configured.")),
-				Arguments.of(Stream.of(),
-						new ExpectedResult<>(Set.of(), "An empty list should be returned if nothing is configured.")),
-				Arguments.of(
-						Stream.of(getOidc4VpClient(Map.of("vctypes_TestType", Format.LDP_VC.toString(),
-								"another", "attribute"))),
-						new ExpectedResult<>(Set.of(getCredential("TestType", Format.LDP_VC)),
-								"The list of configured types should be returned.")),
-				Arguments.of(Stream.of(getOidc4VpClient(
-								Map.of("vctypes_TestTypeA", Format.LDP_VC.toString(), "vctypes_TestTypeB",
-										Format.LDP_VC.toString()))),
-						new ExpectedResult<>(
-								Set.of(getCredential("TestTypeA", Format.LDP_VC),
-										getCredential("TestTypeB", Format.LDP_VC)),
-								"The list of configured types should be returned.")),
-				Arguments.of(Stream.of(
-								getOidc4VpClient(Map.of()),
-								getOidc4VpClient(
-										Map.of("vctypes_TestTypeA", Format.LDP_VC.toString(), "vctypes_TestTypeB",
-												Format.LDP_VC.toString()))),
-						new ExpectedResult<>(
-								Set.of(getCredential("TestTypeA", Format.LDP_VC),
-										getCredential("TestTypeB", Format.LDP_VC)),
-								"The list of configured types should be returned.")),
-				Arguments.of(Stream.of(
-								getOidc4VpClient(null),
-								getOidc4VpClient(
-										Map.of("vctypes_TestTypeA", Format.LDP_VC.toString(), "vctypes_TestTypeB",
-												Format.LDP_VC.toString()))),
-						new ExpectedResult<>(
-								Set.of(getCredential("TestTypeA", Format.LDP_VC),
-										getCredential("TestTypeB", Format.LDP_VC)),
-								"The list of configured types should be returned.")),
-				Arguments.of(Stream.of(
-								getOidc4VpClient(Map.of("vctypes_AnotherType", Format.LDP_VC.toString())),
-								getOidc4VpClient(
-										Map.of("vctypes_TestTypeA", Format.LDP_VC.toString(), "vctypes_TestTypeB",
-												Format.LDP_VC.toString()))),
-						new ExpectedResult<>(
-								Set.of(getCredential("TestTypeA", Format.LDP_VC),
-										getCredential("TestTypeB", Format.LDP_VC),
-										getCredential("AnotherType", Format.LDP_VC)),
-								"The list of configured types should be returned.")),
-				Arguments.of(Stream.of(
-								getOidc4VpClient(
-										Map.of("vctypes_AnotherType", Format.LDP_VC.toString(), "vctypes_AndAnother",
-												Format.LDP_VC.toString())),
-								getOidc4VpClient(
-										Map.of("vctypes_TestTypeA", Format.LDP_VC.toString(), "vctypes_TestTypeB",
-												Format.LDP_VC.toString()))),
-						new ExpectedResult<>(
-								Set.of(getCredential("TestTypeA", Format.LDP_VC),
-										getCredential("TestTypeB", Format.LDP_VC),
-										getCredential("AnotherType", Format.LDP_VC),
-										getCredential("AndAnother", Format.LDP_VC)),
-								"The list of configured types should be returned."))
-		);
-	}
-
-	protected static SupportedCredential getCredential(String type, Format format) {
-		var cred = new SupportedCredential();
-		cred.setTypes(List.of(type));
-		cred.setFormat(format);
-		return cred;
-	}
-
-	private static UserModel getUserModel(String email, String firstName, String lastName) {
-		UserModel userModel = mock(UserModel.class);
-		when(userModel.getEmail()).thenReturn(email);
-		when(userModel.getFirstName()).thenReturn(firstName);
-		when(userModel.getLastName()).thenReturn(lastName);
-		// use answer to allow multiple invocations
-		when(userModel.getAttributeStream(eq("firstName"))).then(f -> Stream.of(firstName));
-		when(userModel.getAttributeStream(eq("familyName"))).then(f -> Stream.of(lastName));
-		when(userModel.getAttributeStream(eq("email"))).then(f -> Stream.of(email));
-		return userModel;
-	}
-
-	private static RoleModel getRoleModel(String name) {
-		RoleModel roleModel = mock(RoleModel.class);
-		when(roleModel.getName()).thenReturn(name);
-		return roleModel;
-	}
-
-	private static ClientModel getOidcClient() {
-		ClientModel clientA = mock(ClientModel.class);
-		when(clientA.getProtocol()).thenReturn("OIDC");
-		return clientA;
-	}
-
-	private static ClientModel getNullClient() {
-		ClientModel clientA = mock(ClientModel.class);
-		when(clientA.getProtocol()).thenReturn(null);
-		return clientA;
-	}
-
-	private static ClientModel getOidc4VpClient(String clientId, Map<String, String> attributes, List<String> roles,
-			Map<String, String> additionalClaims) {
-		Stream<RoleModel> roleModelStream = roles.stream().map(role -> {
-			RoleModel roleModel = mock(RoleModel.class);
-			when(roleModel.getName()).thenReturn(role);
-			return roleModel;
-		});
-		List<ProtocolMapperModel> mapperModels = new ArrayList<>();
-		ProtocolMapperModel idMapperModel = mock(ProtocolMapperModel.class);
-		when(idMapperModel.getProtocolMapper()).thenReturn(OIDC4VPSubjectIdMapper.MAPPER_ID);
-		when(idMapperModel.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
-		when(idMapperModel.getConfig()).thenReturn(Map.of(OIDC4VPSubjectIdMapper.ID_KEY, "urn:uuid:dummy-id"));
-		mapperModels.add(idMapperModel);
-
-		if (clientId != null) {
-			ProtocolMapperModel roleMapperModel = mock(ProtocolMapperModel.class);
-			when(roleMapperModel.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
-			when(roleMapperModel.getProtocolMapper()).thenReturn(OIDC4VPTargetRoleMapper.MAPPER_ID);
-			when(roleMapperModel.getConfig()).thenReturn(
-					Map.of(OIDC4VPTargetRoleMapper.SUBJECT_PROPERTY_CONFIG_KEY, "roles",
-							OIDC4VPTargetRoleMapper.CLIENT_CONFIG_KEY, clientId));
-			mapperModels.add(roleMapperModel);
-		}
-
-		ProtocolMapperModel familyNameMapper = mock(ProtocolMapperModel.class);
-		when(familyNameMapper.getProtocolMapper()).thenReturn(OIDC4VPUserAttributeMapper.MAPPER_ID);
-		when(familyNameMapper.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
-		when(familyNameMapper.getConfig()).thenReturn(
-				Map.of(OIDC4VPUserAttributeMapper.USER_ATTRIBUTE_KEY, "familyName",
-						OIDC4VPUserAttributeMapper.SUBJECT_PROPERTY_CONFIG_KEY, "familyName",
-						OIDC4VPUserAttributeMapper.AGGREGATE_ATTRIBUTES_KEY, "false"));
-		mapperModels.add(familyNameMapper);
-
-		ProtocolMapperModel firstNameMapper = mock(ProtocolMapperModel.class);
-		when(firstNameMapper.getProtocolMapper()).thenReturn(OIDC4VPUserAttributeMapper.MAPPER_ID);
-		when(firstNameMapper.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
-		when(firstNameMapper.getConfig()).thenReturn(Map.of(OIDC4VPUserAttributeMapper.USER_ATTRIBUTE_KEY, "firstName",
-				OIDC4VPUserAttributeMapper.SUBJECT_PROPERTY_CONFIG_KEY, "firstName",
-				OIDC4VPUserAttributeMapper.AGGREGATE_ATTRIBUTES_KEY, "false"));
-		mapperModels.add(firstNameMapper);
-
-		ProtocolMapperModel emailMapper = mock(ProtocolMapperModel.class);
-		when(emailMapper.getProtocolMapper()).thenReturn(OIDC4VPUserAttributeMapper.MAPPER_ID);
-		when(emailMapper.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
-		when(emailMapper.getConfig()).thenReturn(Map.of(OIDC4VPUserAttributeMapper.USER_ATTRIBUTE_KEY, "email",
-				OIDC4VPUserAttributeMapper.SUBJECT_PROPERTY_CONFIG_KEY, "email",
-				OIDC4VPUserAttributeMapper.AGGREGATE_ATTRIBUTES_KEY, "false"));
-		mapperModels.add(emailMapper);
-
-		additionalClaims.entrySet().forEach(entry -> {
-			ProtocolMapperModel claimMapper = mock(ProtocolMapperModel.class);
-			when(claimMapper.getProtocolMapper()).thenReturn(OIDC4VPStaticClaimMapper.MAPPER_ID);
-			when(claimMapper.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
-			when(claimMapper.getConfig()).thenReturn(Map.of(OIDC4VPStaticClaimMapper.STATIC_CLAIM_KEY, entry.getValue(),
-					OIDC4VPStaticClaimMapper.SUBJECT_PROPERTY_CONFIG_KEY, entry.getKey()));
-			mapperModels.add(claimMapper);
-		});
-
-		ClientModel clientA = mock(ClientModel.class);
-		when(clientA.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
-		when(clientA.getClientId()).thenReturn(clientId);
-		when(clientA.getAttributes()).thenReturn(attributes);
-		when(clientA.getProtocolMappersStream()).thenReturn(mapperModels.stream());
-		when(clientA.getRolesStream()).thenReturn(roleModelStream);
-		return clientA;
-	}
-
-	private static ClientModel getOidc4VpClient(String clientId, Map<String, String> attributes, List<String> roles) {
-		return getOidc4VpClient(clientId, attributes, roles, Map.of());
-	}
-
-	private static ClientModel getOidc4VpClient(Map<String, String> attributes) {
-		return getOidc4VpClient(null, attributes, List.of());
-	}
+    private static final String ISSUER_DID = "did:key:test";
+
+    private final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
+            .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
+            .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true).build();
+
+    private KeycloakSession keycloakSession;
+    private AppAuthManager.BearerTokenAuthenticator bearerTokenAuthenticator;
+
+    private OIDC4VPIssuerEndpoint testEndpoint;
+
+    private Clock fixedClock = Clock.fixed(Instant.parse("2022-11-10T17:11:09.00Z"),
+            ZoneId.of("Europe/Paris"));
+
+    @BeforeEach
+    public void setUp() throws NoSuchFieldException {
+        URL url = getClass().getClassLoader().getResource("eckey.tls");
+
+        Security.addProvider(new BouncyCastleProvider());
+        this.keycloakSession = mock(KeycloakSession.class);
+        this.bearerTokenAuthenticator = mock(AppAuthManager.BearerTokenAuthenticator.class);
+        this.testEndpoint = new OIDC4VPIssuerEndpoint(keycloakSession, ISSUER_DID, url.getPath(),
+                Optional.of("Ed25519"),
+                Optional.of("Ed25519Signature2018"),
+                bearerTokenAuthenticator, new ObjectMapper(), fixedClock);
+    }
+
+    @Test
+    public void testGetVCUnauthorized() {
+        KeycloakContext context = mock(KeycloakContext.class);
+        RealmModel realmModel = mock(RealmModel.class);
+        when(keycloakSession.getContext()).thenReturn(context);
+        when(context.getRealm()).thenReturn(realmModel);
+
+        when(bearerTokenAuthenticator.authenticate()).thenReturn(null);
+
+        try {
+            testEndpoint.issueVerifiableCredential(ISSUER_DID, "MyVC");
+            fail("VCs should only be accessible for authorized users.");
+        } catch (WebApplicationException e) {
+            assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), e.getResponse().getStatus(),
+                    "The response should be a 400.");
+            ErrorResponse er = OBJECT_MAPPER.convertValue(e.getResponse().getEntity(), ErrorResponse.class);
+            assertEquals(ErrorType.INVALID_TOKEN.getValue(), er.getError().value(),
+                    "The response should have been denied because of the invalid token.");
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource("provideTypesAndClients")
+    public void testGetVCNoSuchType(Stream<ClientModel> clientModelStream,
+                                    ExpectedResult<Set<SupportedCredential>> ignored) {
+        AuthenticationManager.AuthResult authResult = mock(AuthenticationManager.AuthResult.class);
+        UserModel userModel = mock(UserModel.class);
+        KeycloakContext context = mock(KeycloakContext.class);
+        RealmModel realmModel = mock(RealmModel.class);
+        ClientProvider clientProvider = mock(ClientProvider.class);
+
+        when(bearerTokenAuthenticator.authenticate()).thenReturn(authResult);
+        when(authResult.getUser()).thenReturn(userModel);
+        when(keycloakSession.getContext()).thenReturn(context);
+        when(context.getRealm()).thenReturn(realmModel);
+        when(keycloakSession.clients()).thenReturn(clientProvider);
+        when(clientProvider.getClientsStream(any())).thenReturn(clientModelStream);
+
+        try {
+            testEndpoint.issueVerifiableCredential(ISSUER_DID, "MyNonExistentType");
+            fail("Not found types should be a 400");
+        } catch (WebApplicationException e) {
+            assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), e.getResponse().getStatus(),
+                    "Not found types should be a 400");
+            ErrorResponse er = OBJECT_MAPPER.convertValue(e.getResponse().getEntity(), ErrorResponse.class);
+            assertEquals(ErrorType.UNSUPPORTED_CREDENTIAL_TYPE.getValue(), er.getError().value(),
+                    "The response should have been denied because of the unsupported type.");
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource("provideUserAndClients")
+    public void testGetCredential(UserModel userModel, Stream<ClientModel> clientModelStream,
+                                  Map<ClientModel, Stream<RoleModel>> roleModelStreamMap,
+                                  ExpectedResult<CredentialSubject> expectedResult, Format requestedFormat)
+            throws JsonProcessingException, VerificationException {
+        List<ClientModel> clientModels = clientModelStream.toList();
+
+        AuthenticationManager.AuthResult authResult = mock(AuthenticationManager.AuthResult.class);
+        KeycloakContext context = mock(KeycloakContext.class);
+        RealmModel realmModel = mock(RealmModel.class);
+        ClientProvider clientProvider = mock(ClientProvider.class);
+
+        UserSessionModel userSessionModel = mock(UserSessionModel.class);
+        when(userSessionModel.getRealm()).thenReturn(realmModel);
+        when(userSessionModel.getUser()).thenReturn(userModel);
+        clientModels.forEach(cm -> when(realmModel.getClientByClientId(eq(cm.getClientId()))).thenReturn(cm));
+        when(realmModel.getClientsStream()).thenReturn(clientModels.stream());
+
+        when(bearerTokenAuthenticator.authenticate()).thenReturn(authResult);
+
+        when(authResult.getUser()).thenReturn(userModel);
+        when(authResult.getSession()).thenReturn(userSessionModel);
+
+        when(keycloakSession.getContext()).thenReturn(context);
+        when(context.getRealm()).thenReturn(realmModel);
+
+        when(keycloakSession.clients()).thenReturn(clientProvider);
+        when(clientProvider.getClientsStream(any())).thenReturn(clientModels.stream());
+
+        when(userModel.getClientRoleMappingsStream(any())).thenAnswer(i -> roleModelStreamMap.get(i.getArguments()[0]));
+        Object credential = testEndpoint.getCredential("MyType", requestedFormat);
+        switch (requestedFormat) {
+            case LDP_VC -> {
+                VerifiableCredential verifiableCredential = OBJECT_MAPPER.convertValue(credential, VerifiableCredential.class);
+                verifyLDCredential(expectedResult, verifiableCredential);
+            }
+            case JWT_VC_JSON_LD, JWT_VC, JWT_VC_JSON -> verifyJWTCredential(expectedResult, (String) credential);
+        }
+    }
+
+    private void verifyJWTCredential(ExpectedResult<CredentialSubject> expectedResult, String actualResult)
+            throws VerificationException, JsonProcessingException {
+        TokenVerifier<JsonWebToken> verifier = TokenVerifier.create(actualResult, JsonWebToken.class);
+        JsonWebToken theJWT = verifier.getToken();
+        assertEquals(ISSUER_DID, theJWT.getIssuer(), "The issuer should be properly set.");
+        assertNotNull(theJWT.getSubject(), "A subject should be set.");
+        assertNotNull(theJWT.getId(), "The jwt should have an id.");
+
+        VerifiableCredential theVC = (VerifiableCredential) theJWT.getOtherClaims().get("vc");
+        assertNotNull(theVC, "The vc should be part of the jwt.");
+        List credentialType = (List) theVC.getType();
+        assertEquals(2, credentialType.size(), "Both types should be included.");
+        assertTrue(credentialType.contains("MyType") && credentialType.contains("VerifiableCredential"),
+                "The correct types should be included.");
+
+
+        verifySubject(expectedResult, expectedResult.getExpectedResult(), theVC.getCredentialSubject());
+
+    }
+
+    @ParameterizedTest
+    @MethodSource("provideUserAndClientsLDP")
+    public void testGetVC(UserModel userModel, Stream<ClientModel> clientModelStream,
+                          Map<ClientModel, Stream<RoleModel>> roleModelStreamMap,
+                          ExpectedResult<CredentialSubject> expectedResult) throws JsonProcessingException {
+        List<ClientModel> clientModels = clientModelStream.toList();
+
+        AuthenticationManager.AuthResult authResult = mock(AuthenticationManager.AuthResult.class);
+        KeycloakContext context = mock(KeycloakContext.class);
+        RealmModel realmModel = mock(RealmModel.class);
+        ClientProvider clientProvider = mock(ClientProvider.class);
+        UserSessionModel userSessionModel = mock(UserSessionModel.class);
+        when(userSessionModel.getRealm()).thenReturn(realmModel);
+        when(userSessionModel.getUser()).thenReturn(userModel);
+        clientModels.forEach(cm -> when(realmModel.getClientByClientId(eq(cm.getClientId()))).thenReturn(cm));
+
+        when(bearerTokenAuthenticator.authenticate()).thenReturn(authResult);
+        when(authResult.getUser()).thenReturn(userModel);
+        when(authResult.getSession()).thenReturn(userSessionModel);
+        when(keycloakSession.getContext()).thenReturn(context);
+        when(context.getRealm()).thenReturn(realmModel);
+        when(keycloakSession.clients()).thenReturn(clientProvider);
+        // use then to open a new stream on each invocation
+        when(clientProvider.getClientsStream(any())).then(f -> clientModels.stream());
+
+        when(userModel.getClientRoleMappingsStream(any())).thenAnswer(i -> roleModelStreamMap.get(i.getArguments()[0]));
+
+        VerifiableCredential credentialVO = OBJECT_MAPPER.convertValue(
+                testEndpoint.issueVerifiableCredential("MyType", "myToken").getEntity(),
+                VerifiableCredential.class);
+
+        verifyLDCredential(expectedResult, credentialVO);
+    }
+
+    private void verifyLDCredential(ExpectedResult<CredentialSubject> expectedResult, VerifiableCredential credentialVO)
+            throws JsonProcessingException {
+        assertEquals(Date.from(fixedClock.instant()), credentialVO.getIssuanceDate(),
+                "The issuance date should be correctly set.");
+        assertNotNull(credentialVO.getContext(), "The context should be set on an ld-credential.");
+        assertNotNull(credentialVO.getProof(), "The proof should be included.");
+        assertNotNull(credentialVO.getId(), "The credential should have an id.");
+        List credentialType = (List) credentialVO.getType();
+        assertEquals(2, credentialType.size(), "Both types should be included.");
+        assertTrue(credentialType.contains("MyType") && credentialType.contains("VerifiableCredential"),
+                "The correct types should be included.");
+
+        assertEquals(ISSUER_DID, credentialVO.getIssuer().toString(), "The correct issuer should be set.");
+
+        CredentialSubject retrievedSubject = credentialVO.getCredentialSubject();
+        assertNotNull(retrievedSubject.getId(), "The id should have been set.");
+        // remove the id, since its randomly generated.
+        retrievedSubject.setId(null);
+
+        verifySubject(expectedResult, expectedResult.getExpectedResult(), retrievedSubject);
+    }
+
+    private void verifySubject(ExpectedResult<CredentialSubject> expectedResult, CredentialSubject expectedCredentialSubject, CredentialSubject retrievedSubject)
+            throws JsonProcessingException {
+        verifyRoles(expectedResult.getMessage(), expectedCredentialSubject, retrievedSubject);
+        // roles are checked, can be removed to not interfer with next checks.
+        expectedCredentialSubject.setClaims("roles", null);
+        retrievedSubject.setClaims("roles", null);
+
+        String expectedJson = OBJECT_MAPPER.writeValueAsString(expectedCredentialSubject);
+        String retrievedJson = OBJECT_MAPPER.writeValueAsString(retrievedSubject);
+        // we compare the json, to prevent order issues.
+        assertEquals(expectedJson, retrievedJson, expectedResult.getMessage());
+    }
+
+    private void verifyRoles(String message, CredentialSubject expectedCredentialSubject, CredentialSubject retrievedSubject) {
+        Set<Role> retrievedRoles = OBJECT_MAPPER.convertValue(retrievedSubject.getClaims().get("roles"),
+                new TypeReference<Set<Role>>() {
+                });
+        Set<Role> expectedRoles = OBJECT_MAPPER.convertValue(expectedCredentialSubject.getClaims().get("roles"),
+                new TypeReference<Set<Role>>() {
+                });
+        assertEquals(expectedRoles, retrievedRoles, message);
+    }
+
+    private static Arguments getArguments(UserModel um, Map<ClientModel, List<RoleModel>> clients,
+                                          ExpectedResult expectedResult) {
+        return Arguments.of(um,
+                clients.keySet().stream(),
+                clients.entrySet()
+                        .stream()
+                        .filter(e -> e.getValue() != null)
+                        .collect(
+                                Collectors.toMap(Map.Entry::getKey, e -> ((List) e.getValue()).stream(),
+                                        (e1, e2) -> e1)),
+                expectedResult);
+    }
+
+    private static Stream<Arguments> provideUserAndClients() {
+        return Stream.concat(provideUserAndClientsLDP().map(a -> {
+                    var argObjects = new ArrayList<>(Arrays.asList(a.get()));
+                    argObjects.add(Format.LDP_VC);
+                    return Arguments.of(argObjects.toArray());
+                }),
+                provideUserAndClientsJWT().map(a -> {
+                    var argObjects = new ArrayList<>(Arrays.asList(a.get()));
+                    argObjects.add(Format.JWT_VC);
+                    return Arguments.of(argObjects.toArray());
+                }));
+    }
+
+    private static CredentialSubject getCredentialSubject(Map<String, Object> claims) {
+        CredentialSubject credentialSubject = new CredentialSubject();
+        claims.entrySet().stream().forEach(e -> credentialSubject.setClaims(e.getKey(), e.getValue()));
+        return credentialSubject;
+    }
+
+    private static Stream<Arguments> provideUserAndClientsJWT() {
+        return Stream.of(
+                getArguments(getUserModel("e@mail.org", "Happy", "User"),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("MyRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("email", "e@mail.org", "familyName", "User", "firstName", "Happy", "roles",
+                                                Set.of(new Role(Set.of("MyRole"), "did:key:1")))),
+                                "A valid Credential should have been returned.")
+                ),
+                getArguments(getUserModel("e@mail.org", null, "User"),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("MyRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("email", "e@mail.org", "familyName", "User", "roles",
+                                                Set.of(new Role(Set.of("MyRole"), "did:key:1")))),
+                                "A valid Credential should have been returned.")
+                ),
+                getArguments(
+                        getUserModel("e@mail.org", null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("MyRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("email", "e@mail.org", "roles",
+                                                Set.of(new Role(Set.of("MyRole"), "did:key:1")))),
+                                "A valid Credential should have been returned.")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("MyRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole"), "did:key:1")))),
+                                "A valid Credential should have been returned.")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1")))),
+                                "Multiple roles should be included")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole"), "did:key:1")))),
+                                "Only assigned roles should be included.")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
+                                getOidc4VpClient("did:key:2",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("AnotherRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("AnotherRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
+                                                        new Role(Set.of("AnotherRole"), "did:key:2")))),
+                                "The request should contain roles from both clients")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
+                                getOidc4VpClient("did:key:2",
+                                        Map.of("vctypes_AnotherType", Format.JWT_VC.toString()),
+                                        List.of("AnotherRole"),
+                                        List.of("AnotherType", "VerifiableCredential")),
+                                List.of(getRoleModel("AnotherRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1")))),
+                                "Only roles for supported clients should be included.")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        Map.of("more", "claims"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
+                                getOidc4VpClient("did:key:2",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("AnotherRole"),
+                                        Map.of("additional", "claim"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("AnotherRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
+                                                        new Role(Set.of("AnotherRole"), "did:key:2")),
+                                                "additional", "claim", "more", "claims")),
+                                "Additional claims should be included.")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
+                                getOidc4VpClient("did:key:2",
+                                        Map.of("vctypes_MyType", Format.JWT_VC.toString()),
+                                        List.of("AnotherRole"),
+                                        Map.of("additional", "claim"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("AnotherRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("additional", "claim", "roles",
+                                                Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
+                                                        new Role(Set.of("AnotherRole"), "did:key:2")))),
+                                "Additional claims should be included.")
+                )
+        );
+    }
+
+    private static Stream<Arguments> provideUserAndClientsLDP() {
+        return Stream.of(
+                getArguments(getUserModel("e@mail.org", "Happy", "User"),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("MyRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("email", "e@mail.org", "familyName", "User", "firstName", "Happy", "roles",
+                                                Set.of(new Role(Set.of("MyRole"), "did:key:1")))),
+                                "A valid Credential should have been returned.")
+                ),
+                getArguments(getUserModel("e@mail.org", null, "User"),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("MyRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("email", "e@mail.org", "familyName", "User", "roles",
+                                                Set.of(new Role(Set.of("MyRole"), "did:key:1")))),
+                                "A valid Credential should have been returned.")
+                ),
+                getArguments(
+                        getUserModel("e@mail.org", null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("MyRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("email", "e@mail.org", "roles",
+                                                Set.of(new Role(Set.of("MyRole"), "did:key:1")))),
+                                "A valid Credential should have been returned.")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("MyRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole"), "did:key:1")))),
+                                "A valid Credential should have been returned.")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1")))),
+                                "Multiple roles should be included")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole"), "did:key:1")))),
+                                "Only assigned roles should be included.")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
+                                getOidc4VpClient("did:key:2",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("AnotherRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("AnotherRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
+                                                        new Role(Set.of("AnotherRole"), "did:key:2")))),
+                                "The request should contain roles from both clients")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
+                                getOidc4VpClient("did:key:2",
+                                        Map.of("vctypes_AnotherType", Format.LDP_VC.toString()),
+                                        List.of("AnotherRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("AnotherRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1")))),
+                                "Only roles for supported clients should be included.")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        Map.of("more", "claims"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
+                                getOidc4VpClient("did:key:2",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("AnotherRole"),
+                                        Map.of("additional", "claim"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("AnotherRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("roles",
+                                                Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
+                                                        new Role(Set.of("AnotherRole"), "did:key:2")),
+                                                "additional", "claim", "more", "claims")),
+                                "Additional claims should be included.")
+                ),
+                getArguments(
+                        getUserModel(null, null, null),
+                        Map.of(getOidc4VpClient("did:key:1",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("MyRole", "MySecondRole"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("MyRole"), getRoleModel("MySecondRole")),
+                                getOidc4VpClient("did:key:2",
+                                        Map.of("vctypes_MyType", Format.LDP_VC.toString()),
+                                        List.of("AnotherRole"),
+                                        Map.of("additional", "claim"),
+                                        List.of("MyType", "VerifiableCredential")),
+                                List.of(getRoleModel("AnotherRole"))),
+                        new ExpectedResult<>(
+                                getCredentialSubject(
+                                        Map.of("additional", "claim", "roles",
+                                                Set.of(new Role(Set.of("MyRole", "MySecondRole"), "did:key:1"),
+                                                        new Role(Set.of("AnotherRole"), "did:key:2")))),
+                                "Additional claims should be included.")
+                )
+        );
+    }
+
+    private static Stream<Arguments> provideTypesAndClients() {
+        return Stream.of(
+                Arguments.of(Stream.of(getOidcClient(), getNullClient(), getOidc4VpClient(
+                                Map.of("vctypes_TestType", Format.LDP_VC.toString()),
+                                List.of("TestType", "VerifiableCredential"))),
+                        new ExpectedResult<>(Set.of(getCredential("TestType", Format.LDP_VC)),
+                                "The list of configured types should be returned.")),
+                Arguments.of(Stream.of(getOidcClient(), getNullClient()),
+                        new ExpectedResult<>(Set.of(), "An empty list should be returned if nothing is configured.")),
+                Arguments.of(Stream.of(),
+                        new ExpectedResult<>(Set.of(), "An empty list should be returned if nothing is configured.")),
+                Arguments.of(
+                        Stream.of(getOidc4VpClient(Map.of("vctypes_TestType", Format.LDP_VC.toString(),
+                                        "another", "attribute"),
+                                List.of("MyType", "VerifiableCredential"))),
+                        new ExpectedResult<>(Set.of(getCredential("TestType", Format.LDP_VC)),
+                                "The list of configured types should be returned.")),
+                Arguments.of(Stream.of(getOidc4VpClient(
+                                Map.of("vctypes_TestTypeA", Format.LDP_VC.toString(), "vctypes_TestTypeB",
+                                        Format.LDP_VC.toString()),
+                                List.of("MyType", "VerifiableCredential"))),
+                        new ExpectedResult<>(
+                                Set.of(getCredential("TestTypeA", Format.LDP_VC),
+                                        getCredential("TestTypeB", Format.LDP_VC)),
+                                "The list of configured types should be returned.")),
+                Arguments.of(Stream.of(
+                                getOidc4VpClient(Map.of(), null),
+                                getOidc4VpClient(
+                                        Map.of("vctypes_TestTypeA", Format.LDP_VC.toString(), "vctypes_TestTypeB",
+                                                Format.LDP_VC.toString()),
+                                        List.of("TestTypeA", "TestTypeB", "VerifiableCredential"))),
+                        new ExpectedResult<>(
+                                Set.of(getCredential("TestTypeA", Format.LDP_VC),
+                                        getCredential("TestTypeB", Format.LDP_VC)),
+                                "The list of configured types should be returned.")),
+                Arguments.of(Stream.of(
+                                getOidc4VpClient(null, null),
+                                getOidc4VpClient(
+                                        Map.of("vctypes_TestTypeA", Format.LDP_VC.toString(), "vctypes_TestTypeB",
+                                                Format.LDP_VC.toString()),
+                                        List.of("TestTypeA", "TestTypeB", "VerifiableCredential"))),
+                        new ExpectedResult<>(
+                                Set.of(getCredential("TestTypeA", Format.LDP_VC),
+                                        getCredential("TestTypeB", Format.LDP_VC)),
+                                "The list of configured types should be returned.")),
+                Arguments.of(Stream.of(
+                                getOidc4VpClient(Map.of("vctypes_AnotherType", Format.LDP_VC.toString()),
+                                        List.of("TestTypeA", "TestTypeB", "AnotherType")),
+                                getOidc4VpClient(
+                                        Map.of("vctypes_TestTypeA", Format.LDP_VC.toString(), "vctypes_TestTypeB",
+                                                Format.LDP_VC.toString()),
+                                        List.of("TestTypeA", "TestTypeB", "VerifiableCredential"))),
+                        new ExpectedResult<>(
+                                Set.of(getCredential("TestTypeA", Format.LDP_VC),
+                                        getCredential("TestTypeB", Format.LDP_VC),
+                                        getCredential("AnotherType", Format.LDP_VC)),
+                                "The list of configured types should be returned.")),
+                Arguments.of(Stream.of(
+                                getOidc4VpClient(
+                                        Map.of("vctypes_AnotherType", Format.LDP_VC.toString(), "vctypes_AndAnother",
+                                                Format.LDP_VC.toString()),
+                                        List.of("AnotherType", "AndAnother", "VerfiableCredential")),
+                                getOidc4VpClient(
+                                        Map.of("vctypes_TestTypeA", Format.LDP_VC.toString(), "vctypes_TestTypeB",
+                                                Format.LDP_VC.toString()), List.of("AnotherType", "AndAnother", "VerfiableCredential"))
+                        ),
+                        new ExpectedResult<>(
+                                Set.of(getCredential("TestTypeA", Format.LDP_VC),
+                                        getCredential("TestTypeB", Format.LDP_VC),
+                                        getCredential("AnotherType", Format.LDP_VC),
+                                        getCredential("AndAnother", Format.LDP_VC)),
+                                "The list of configured types should be returned."))
+        );
+    }
+
+    protected static SupportedCredential getCredential(String type, Format format) {
+        var cred = new SupportedCredential();
+        cred.setTypes(List.of(type));
+        cred.setFormat(format);
+        return cred;
+    }
+
+    private static UserModel getUserModel(String email, String firstName, String lastName) {
+        UserModel userModel = mock(UserModel.class);
+        when(userModel.getEmail()).thenReturn(email);
+        when(userModel.getFirstName()).thenReturn(firstName);
+        when(userModel.getLastName()).thenReturn(lastName);
+        // use answer to allow multiple invocations
+        when(userModel.getAttributeStream(eq("firstName"))).then(f -> Stream.of(firstName));
+        when(userModel.getAttributeStream(eq("familyName"))).then(f -> Stream.of(lastName));
+        when(userModel.getAttributeStream(eq("email"))).then(f -> Stream.of(email));
+        return userModel;
+    }
+
+    private static RoleModel getRoleModel(String name) {
+        RoleModel roleModel = mock(RoleModel.class);
+        when(roleModel.getName()).thenReturn(name);
+        return roleModel;
+    }
+
+    private static ClientModel getOidcClient() {
+        ClientModel clientA = mock(ClientModel.class);
+        when(clientA.getProtocol()).thenReturn("OIDC");
+        return clientA;
+    }
+
+    private static ClientModel getNullClient() {
+        ClientModel clientA = mock(ClientModel.class);
+        when(clientA.getProtocol()).thenReturn(null);
+        return clientA;
+    }
+
+    private static ClientModel getOidc4VpClient(String clientId, Map<String, String> attributes, List<String> roles,
+                                                Map<String, String> additionalClaims, List<String> types) {
+        Stream<RoleModel> roleModelStream = roles.stream().map(role -> {
+            RoleModel roleModel = mock(RoleModel.class);
+            when(roleModel.getName()).thenReturn(role);
+            return roleModel;
+        });
+        List<ProtocolMapperModel> mapperModels = new ArrayList<>();
+        ProtocolMapperModel idMapperModel = mock(ProtocolMapperModel.class);
+        when(idMapperModel.getProtocolMapper()).thenReturn(OIDC4VPSubjectIdMapper.MAPPER_ID);
+        when(idMapperModel.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
+        when(idMapperModel.getConfig()).thenReturn(Map.of(OIDC4VPSubjectIdMapper.ID_KEY, "urn:uuid:dummy-id"));
+        mapperModels.add(idMapperModel);
+
+        if (clientId != null) {
+            ProtocolMapperModel roleMapperModel = mock(ProtocolMapperModel.class);
+            when(roleMapperModel.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
+            when(roleMapperModel.getProtocolMapper()).thenReturn(OIDC4VPTargetRoleMapper.MAPPER_ID);
+            when(roleMapperModel.getConfig()).thenReturn(
+                    Map.of(OIDC4VPTargetRoleMapper.SUBJECT_PROPERTY_CONFIG_KEY, "roles",
+                            OIDC4VPTargetRoleMapper.CLIENT_CONFIG_KEY, clientId));
+            mapperModels.add(roleMapperModel);
+        }
+
+        if (types != null) {
+            types.forEach(t -> {
+                ProtocolMapperModel typeMapper = mock(ProtocolMapperModel.class);
+                when(typeMapper.getProtocolMapper()).thenReturn(OIDC4VPTypeMapper.MAPPER_ID);
+                when(typeMapper.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
+                when(typeMapper.getConfig()).thenReturn(
+                        Map.of(OIDC4VPTypeMapper.TYPE_KEY, t));
+                mapperModels.add(typeMapper);
+            });
+        }
+        ProtocolMapperModel familyNameMapper = mock(ProtocolMapperModel.class);
+        when(familyNameMapper.getProtocolMapper()).thenReturn(OIDC4VPUserAttributeMapper.MAPPER_ID);
+        when(familyNameMapper.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
+        when(familyNameMapper.getConfig()).thenReturn(
+                Map.of(OIDC4VPUserAttributeMapper.USER_ATTRIBUTE_KEY, "familyName",
+                        OIDC4VPUserAttributeMapper.SUBJECT_PROPERTY_CONFIG_KEY, "familyName",
+                        OIDC4VPUserAttributeMapper.AGGREGATE_ATTRIBUTES_KEY, "false"));
+        mapperModels.add(familyNameMapper);
+
+        ProtocolMapperModel firstNameMapper = mock(ProtocolMapperModel.class);
+        when(firstNameMapper.getProtocolMapper()).thenReturn(OIDC4VPUserAttributeMapper.MAPPER_ID);
+        when(firstNameMapper.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
+        when(firstNameMapper.getConfig()).thenReturn(Map.of(OIDC4VPUserAttributeMapper.USER_ATTRIBUTE_KEY, "firstName",
+                OIDC4VPUserAttributeMapper.SUBJECT_PROPERTY_CONFIG_KEY, "firstName",
+                OIDC4VPUserAttributeMapper.AGGREGATE_ATTRIBUTES_KEY, "false"));
+        mapperModels.add(firstNameMapper);
+
+        ProtocolMapperModel emailMapper = mock(ProtocolMapperModel.class);
+        when(emailMapper.getProtocolMapper()).thenReturn(OIDC4VPUserAttributeMapper.MAPPER_ID);
+        when(emailMapper.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
+        when(emailMapper.getConfig()).thenReturn(Map.of(OIDC4VPUserAttributeMapper.USER_ATTRIBUTE_KEY, "email",
+                OIDC4VPUserAttributeMapper.SUBJECT_PROPERTY_CONFIG_KEY, "email",
+                OIDC4VPUserAttributeMapper.AGGREGATE_ATTRIBUTES_KEY, "false"));
+        mapperModels.add(emailMapper);
+
+        additionalClaims.entrySet().forEach(entry -> {
+            ProtocolMapperModel claimMapper = mock(ProtocolMapperModel.class);
+            when(claimMapper.getProtocolMapper()).thenReturn(OIDC4VPStaticClaimMapper.MAPPER_ID);
+            when(claimMapper.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
+            when(claimMapper.getConfig()).thenReturn(Map.of(OIDC4VPStaticClaimMapper.STATIC_CLAIM_KEY, entry.getValue(),
+                    OIDC4VPStaticClaimMapper.SUBJECT_PROPERTY_CONFIG_KEY, entry.getKey()));
+            mapperModels.add(claimMapper);
+        });
+
+        ClientModel clientA = mock(ClientModel.class);
+        when(clientA.getProtocol()).thenReturn(OIDC4VPClientRegistrationProviderFactory.PROTOCOL_ID);
+        when(clientA.getClientId()).thenReturn(clientId);
+        when(clientA.getAttributes()).thenReturn(attributes);
+        when(clientA.getProtocolMappersStream()).thenReturn(mapperModels.stream());
+        when(clientA.getRolesStream()).thenReturn(roleModelStream);
+        return clientA;
+    }
+
+    private static ClientModel getOidc4VpClient(String clientId, Map<String, String> attributes, List<String> roles, List<String> types) {
+        return getOidc4VpClient(clientId, attributes, roles, Map.of(), types);
+    }
+
+    private static ClientModel getOidc4VpClient(Map<String, String> attributes, List<String> types) {
+        return getOidc4VpClient(null, attributes, List.of(), types);
+    }
 }
\ No newline at end of file
diff --git a/services/src/test/java/org/keycloak/protocol/oidc4vp/signing/JWTSigningServiceTest.java b/services/src/test/java/org/keycloak/protocol/oidc4vp/signing/JWTSigningServiceTest.java
new file mode 100644
index 000000000000..26b679b4a30a
--- /dev/null
+++ b/services/src/test/java/org/keycloak/protocol/oidc4vp/signing/JWTSigningServiceTest.java
@@ -0,0 +1,89 @@
+package org.keycloak.protocol.oidc4vp.signing;
+
+import org.bouncycastle.crypto.util.PrivateKeyInfoFactory;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeAll;
+import org.keycloak.TokenVerifier;
+import org.keycloak.common.VerificationException;
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.util.TokenUtil;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class JWTSigningServiceTest extends SigningServiceTest {
+    private static final String CONTEXT_URL = "https://www.w3.org/2018/credentials/v1";
+
+    @BeforeAll
+    public static void setup() {
+
+    }
+
+    @Test
+    public void test() {
+        RSAKeyLoader keyLoader = new RSAKeyLoader();
+        JWTSigningService jwtSigningService = new JWTSigningService(
+                keyLoader,
+                Optional.of("my-key-id"),
+                Clock.fixed(Instant.ofEpochSecond(1000), ZoneId.of("UTC")),
+                Algorithm.RS256);
+
+        var testCredential = getTestCredential();
+
+        String jwtCredential = jwtSigningService.signCredential(testCredential);
+        var verifier = TokenVerifier.create(jwtCredential, JsonWebToken.class);
+        verifier.publicKey(keyLoader.getKeyPair().getPublic());
+        try {
+            verifier.verify();
+        } catch (VerificationException e) {
+            fail("The credential should successfully be verified.", e);
+        }
+    }
+
+
+    class RSAKeyLoader implements KeyLoader {
+
+        private KeyPair keyPair;
+
+        public KeyPair getKeyPair() {
+            return keyPair;
+        }
+
+        public RSAKeyLoader() {
+            try {
+                KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+                kpg.initialize(2048);
+                keyPair = kpg.generateKeyPair();
+            } catch (NoSuchAlgorithmException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public String loadKey() {
+
+            StringWriter stringWriter = new StringWriter();
+            JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter);
+            try {
+                pemWriter.writeObject(keyPair);
+                pemWriter.flush();
+                pemWriter.close();
+                return stringWriter.toString();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/src/test/java/org/keycloak/protocol/oidc4vp/signing/LDSigningServiceTest.java b/services/src/test/java/org/keycloak/protocol/oidc4vp/signing/LDSigningServiceTest.java
new file mode 100644
index 000000000000..22d944e52e58
--- /dev/null
+++ b/services/src/test/java/org/keycloak/protocol/oidc4vp/signing/LDSigningServiceTest.java
@@ -0,0 +1,109 @@
+package org.keycloak.protocol.oidc4vp.signing;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.util.StdDateFormat;
+import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator;
+import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
+import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters;
+import org.bouncycastle.crypto.signers.Ed25519Signer;
+import org.bouncycastle.crypto.util.PrivateKeyInfoFactory;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.keycloak.common.util.Base64;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
+import org.keycloak.protocol.oidc4vp.signing.signatures.Ed255192018Suite;
+import org.keycloak.protocol.oidc4vp.signing.signatures.SecuritySuite;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.security.SecureRandom;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class LDSigningServiceTest extends SigningServiceTest {
+    private ObjectMapper objectMapper;
+
+    @BeforeAll
+    public static void setup() {
+
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        objectMapper.setDateFormat(new StdDateFormat().withColonInTimeZone(true));
+    }
+
+    @Test
+    public void testEd25519Signature() throws IOException {
+        Ed25519TestKeyLoader testKeyLoader = new Ed25519TestKeyLoader();
+        LDSigningService ldSigningService = new LDSigningService(
+                testKeyLoader,
+                Optional.of("my-key-id"),
+                Clock.fixed(Instant.ofEpochSecond(1000), ZoneId.of("UTC")),
+                Ed255192018Suite.PROOF_TYPE,
+                objectMapper);
+
+
+        var testCredential = getTestCredential();
+        VerifiableCredential signedCredential = ldSigningService.signCredential(testCredential);
+
+        verify(testCredential, signedCredential.getProof().getProofValue(), testKeyLoader.getPublicKey());
+    }
+
+    private void verify(VerifiableCredential testCredential, String proof, AsymmetricKeyParameter publicKey) throws IOException {
+        Ed25519Signer signer = new Ed25519Signer();
+        signer.init(false, publicKey);
+        SecuritySuite securitySuite = new Ed255192018Suite(objectMapper);
+        testCredential.setProof(null);
+        byte[] transformedData = securitySuite.transform(testCredential);
+        byte[] hashedData = securitySuite.digest(transformedData);
+        signer.update(hashedData, 0, hashedData.length);
+
+        assertTrue(signer.verifySignature(Base64.decode(proof, Base64.URL_SAFE)), "The signature should be valid");
+    }
+
+    class Ed25519TestKeyLoader implements KeyLoader {
+        private AsymmetricKeyParameter publicKey;
+        private AsymmetricKeyParameter privateKey;
+
+        public Ed25519TestKeyLoader() {
+            Ed25519KeyGenerationParameters keygenParams = new Ed25519KeyGenerationParameters(new SecureRandom());
+
+            Ed25519KeyPairGenerator generator = new Ed25519KeyPairGenerator();
+            generator.init(keygenParams);
+            var keyPair = generator.generateKeyPair();
+
+            publicKey = keyPair.getPublic();
+            privateKey = keyPair.getPrivate();
+        }
+
+        public AsymmetricKeyParameter getPublicKey() {
+            return publicKey;
+        }
+
+        public AsymmetricKeyParameter getPrivateKey() {
+            return privateKey;
+        }
+
+        @Override
+        public String loadKey() {
+            try {
+                var keyInfo = PrivateKeyInfoFactory.createPrivateKeyInfo(getPrivateKey());
+                StringWriter stringWriter = new StringWriter();
+                JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter);
+                pemWriter.writeObject(keyInfo);
+                pemWriter.flush();
+                pemWriter.close();
+                return stringWriter.toString();
+
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/services/src/test/java/org/keycloak/protocol/oidc4vp/signing/SigningServiceTest.java b/services/src/test/java/org/keycloak/protocol/oidc4vp/signing/SigningServiceTest.java
new file mode 100644
index 000000000000..9b0215306ba0
--- /dev/null
+++ b/services/src/test/java/org/keycloak/protocol/oidc4vp/signing/SigningServiceTest.java
@@ -0,0 +1,30 @@
+package org.keycloak.protocol.oidc4vp.signing;
+
+import org.keycloak.protocol.oidc4vp.model.CredentialSubject;
+import org.keycloak.protocol.oidc4vp.model.VerifiableCredential;
+
+import java.net.URI;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+public abstract class SigningServiceTest {
+
+
+    protected static final String CONTEXT_URL = "https://www.w3.org/2018/credentials/v1";
+
+    protected VerifiableCredential getTestCredential() {
+        CredentialSubject credentialSubject = new CredentialSubject();
+        credentialSubject.setClaims("id", String.format("uri:uuid:%s", UUID.randomUUID()));
+        credentialSubject.setClaims("test", "test");
+        VerifiableCredential testCredential = new VerifiableCredential();
+        testCredential.setContext(List.of(CONTEXT_URL));
+        testCredential.setType(List.of("VerifiableCredential"));
+        testCredential.setIssuer(URI.create("did:web:test.org"));
+        testCredential.setExpirationDate(Date.from(Instant.ofEpochSecond(2000)));
+        testCredential.setIssuanceDate(Date.from(Instant.ofEpochSecond(1000)));
+        testCredential.setCredentialSubject(credentialSubject);
+        return testCredential;
+    }
+}
diff --git a/services/src/test/resources/eckey.tls b/services/src/test/resources/eckey.tls
new file mode 100644
index 000000000000..f19a5088a3a5
--- /dev/null
+++ b/services/src/test/resources/eckey.tls
@@ -0,0 +1,4 @@
+-----BEGIN PRIVATE KEY-----
+MFECAQEwBQYDK2VwBCIEIIVDLxM1I39HfwfTNCNWKMyeqMYhewD8Jni3COdSE1+u
+gSEABXqBACv6GT0LYpOM2JW3zcbHXMOGR9a2K0sNC+zXM6s=
+-----END PRIVATE KEY-----