How to replace core classes/functionalities with a custom SPI?

368 Views Asked by At

I'm trying to add some post quantum key algorithms (from liboqs-java) to Keycloak via custom SPI's. I can generate keys with the algorithms i've added, but I ran into some issues while manipulating them.

Keycloak is having trouble while handling keys with my new algorithms... I think Java BouncyCastle does not recognizes post quantum algorithms (such as Dilithium2) and it causes system to crash.

I'm having issues specifically with JcaPEMWriter BouncyCastle class, on BCPemUtilsProvider Keycloak class. My solution was to rewrite BCPemUtilsProvider so I can replace BouncyCastle functions but, in order to do this, I would need to change a core file from Keycloak and recompile the entire project, what would take an enormous amount of time, for each minor change.

I'd like to fix this via SPI (if possible) or some lightweight solution, so I can do tests in a practical time. Is there any way to change core functionalities without recompiling the entiry Keycloak (or, maybe, another solution I'm not seeing)?

Thanks in advance!!

By the way, here is the code for key generating:

public AbstractGeneratedDLSecretKeyProvider(ComponentModel model, KeyUse use, String type, String algorithm) {
        this.status = KeyStatus.from(model.get(Attributes.ACTIVE_KEY, true), model.get(Attributes.ENABLED_KEY, true));
        this.kid = model.get(Attributes.KID_KEY);
        this.model = model;
        this.use = use;
        this.type = type;
        this.algorithm = algorithm;

        if (model.hasNote(PrivateKey.class.getName()) && model.hasNote(PublicKey.class.getName())) {
            privateKey = model.getNote(PrivateKey.class.getName());
            publicKey = model.getNote(PublicKey.class.getName());
        } else {
            Signature signer = new Signature("Dilithium2");

            signer.generate_keypair();

            privateKey = new DLPrivateKey(signer.export_secret_key());
            publicKey = new DLPublicKey(signer.export_public_key());

            model.setNote(PrivateKey.class.getName(), privateKey);
            model.setNote(PublicKey.class.getName(), publicKey);
        }
    }

And this is the error I'm facing:

2022-08-25 21:06:18,476 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-0) Uncaught server error: org.keycloak.common.util.PemException: java.lang.IllegalArgumentException: failed to construct sequence from byte[]: Extra data detected in stream
    at org.keycloak.crypto.def.BCPemUtilsProvider.encode(BCPemUtilsProvider.java:56)
    at org.keycloak.common.crypto.PemUtilsProvider.encodeKey(PemUtilsProvider.java:129)
    at org.keycloak.common.util.PemUtils.encodeKey(PemUtils.java:98)
    at org.keycloak.services.resources.admin.KeyResource.toKeyMetadataRepresentation(KeyResource.java:83)
    at org.keycloak.services.resources.admin.KeyResource.lambda$getKeyMetadata$0(KeyResource.java:67)
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
    at java.base/java.util.stream.Streams$StreamBuilderImpl.forEachRemaining(Streams.java:411)
    at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
    at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:274)
    at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
    at org.keycloak.services.resources.admin.KeyResource.getKeyMetadata(KeyResource.java:69)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:170)
    at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:130)
    at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:660)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:524)
    at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:474)
    at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:476)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:434)
    at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:192)
    at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:152)
    at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:183)
    at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:152)
    at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:183)
    at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:141)
    at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:32)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:492)
    at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:261)
    at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:161)
    at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364)
    at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:164)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:247)
    at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:73)
    at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:151)
    at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:82)
    at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:42)
    at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1212)
    at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:163)
    at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141)
    at io.quarkus.vertx.http.runtime.StaticResourcesRecorder$2.handle(StaticResourcesRecorder.java:67)
    at io.quarkus.vertx.http.runtime.StaticResourcesRecorder$2.handle(StaticResourcesRecorder.java:55)
    at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1212)
    at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:163)
    at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141)
    at io.quarkus.vertx.http.runtime.VertxHttpRecorder$5.handle(VertxHttpRecorder.java:380)
    at io.quarkus.vertx.http.runtime.VertxHttpRecorder$5.handle(VertxHttpRecorder.java:358)
    at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1212)
    at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:163)
    at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141)
    at org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter.lambda$createBlockingHandler$1(QuarkusRequestFilter.java:90)
    at io.vertx.core.impl.ContextImpl.lambda$null$0(ContextImpl.java:159)
    at io.vertx.core.impl.AbstractContext.dispatch(AbstractContext.java:100)
    at io.vertx.core.impl.ContextImpl.lambda$executeBlocking$1(ContextImpl.java:157)
    at io.quarkus.vertx.core.runtime.VertxCoreRecorder$13.runWith(VertxCoreRecorder.java:545)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478)
    at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
    at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: java.lang.IllegalArgumentException: failed to construct sequence from byte[]: Extra data detected in stream
    at org.bouncycastle.asn1.ASN1Sequence.getInstance(ASN1Sequence.java:92)
    at org.bouncycastle.asn1.x509.SubjectPublicKeyInfo.getInstance(SubjectPublicKeyInfo.java:43)
    at org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator.convertObject(Unknown Source)
    at org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator.<init>(Unknown Source)
    at org.bouncycastle.openssl.jcajce.JcaPEMWriter.writeObject(Unknown Source)
    at org.bouncycastle.openssl.jcajce.JcaPEMWriter.writeObject(Unknown Source)
    at org.keycloak.crypto.def.BCPemUtilsProvider.encode(BCPemUtilsProvider.java:50)
    ... 68 more
1

There are 1 best solutions below

4
On

Brendon Vicente, Custom ClassLoader can helper your.

public class LoadClassInfo extends ClassLoader{
    private ClassLoader classLoader;
    private String pack_name;
    public LoadClassInfo(ClassLoader parent,String pack_name) {
        super(parent);
        this.classLoader = parent;
        this.pack_name = pack_name;
    }
    public Class<?> loadClass(String name) throws ClassNotFoundException {
       if(!name.contains(pack_name))return super.loadClass(name);
        try {
            if(!name.matches("(.+)\\.(.+)\\.(.+)"))return null;
            InputStream input = classLoader.getResourceAsStream(String.format("%s.class",name.replace(".","/")));
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            int data = input.read();
            while(data != -1){
                buffer.write(data);
                data = input.read();
            }
            input.close();
            byte[] classData = buffer.toByteArray();
            System.out.println("name "+name);
            return defineClass(name,classData, 0, classData.length);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}
ScheduledThreadPoolExecutor scheduleds = new ScheduledThreadPoolExecutor(1);
    ClassLoader classLoader;
    String file_path = (classLoader = ClassLoader.getSystemClassLoader())
            .getResource("com.imageutil".replace(".","/")).getPath()
            .substring(1);
    try {
        Map<String,String> maps = Files.list(Paths.get(file_path))
        .filter(x->x.getFileName().toString().endsWith(".class"))
        .collect(Collectors.toMap(m->{
            return m.toString();
        },m->{
            try {
                return String.valueOf(Files.size(m));
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        },(a,b)->a));
        maps.forEach((m,n)->{
            System.out.println(m+" : "+n);
        });
        
        scheduleds.scheduleAtFixedRate(()->{
            try {
                Path[] paths = Files.list(Paths.get(file_path)).toArray(Path[]::new);
                for (Path string : paths) {
                    String class_path = string.toString();
                    String last_time = String.valueOf(Files.size(string));
                    if(Objects.equals(last_time,maps.get(class_path)))continue;
                    String pack_name = class_path.split("classes")[1].replace("\\", ".").substring(1);
                    String class_name = pack_name.substring(0, pack_name.lastIndexOf("."));
                    LoadClassInfo loadClassInfos = new LoadClassInfo(classLoader,"com.imageutil");
                    Class<?> classs = loadClassInfos.loadClass(class_name);
                    classs.newInstance();
                    maps.put(class_path,last_time);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        },1,1,TimeUnit.SECONDS);
        
        
    } catch (IOException e) {
        e.printStackTrace();

    }