Using graalvm-ce-java11-22.3.3 I am trying to include BouncyCastle into my generated binary. I have been looking high and low for a how-to. In the end I copied stuff from pod-babashka-buddy :
I added a Feature class:
public class BCFeature implements Feature {
@Override
public void afterRegistration(AfterRegistrationAccess access) {
Security.addProvider(new BouncyCastleProvider());
}
}
and I added these switches to native-image run:
--report-unsupported-elements-at-runtime
--features=clj_easy.graal_build_time.InitClojureClasses,devtools.BCFeature
--rerun-class-initialization-at-runtime=org.bouncycastle.jcajce.provider.drbg.DRBG\$Default,org.bouncycastle.jcajce.provider.drbg.DRBG\$NonceAndIV
--initialize-at-build-time=org.bouncycastle
to reflect config.json I added:
"java.security.KeyFactory"
"clojure.lang.RT"
"org.bouncycastle.crypto.util.PrivateKeyFactory"
"org.bouncycastle.crypto.util.PublicKeyFactory"
"org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyFactorySpi"
"org.bouncycastle.jcajce.provider.asymmetric.rsa.DigestSignatureSpi$SHA256"
"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings"
"org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPrivateKey"
"org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey"
I get this error from native-image:
Fatal error: com.oracle.graal.pointsto.util.AnalysisError$ParsingError: Error encountered while parsing org.bouncycastle.jcajce.provider.drbg.DRBG$Default.engineNextBytes(byte[])
Parsing context:
at org.bouncycastle.jcajce.provider.drbg.DRBG$Default.engineNextBytes(Unknown Source)
at java.security.SecureRandom.nextBytes(SecureRandom.java:751)
at org.bouncycastle.util.BigIntegers.createRandom(BigIntegers.java:362)
at org.bouncycastle.util.BigIntegers.createRandomBigInteger(BigIntegers.java:292)
at org.bouncycastle.util.BigIntegers.createRandomInRange(BigIntegers.java:139)
at org.bouncycastle.math.Primes.enhancedMRProbablePrimeTest(Primes.java:181)
at org.bouncycastle.crypto.params.RSAKeyParameters.validate(Unknown Source)
at org.bouncycastle.crypto.params.RSAKeyParameters.<init>(Unknown Source)
at org.bouncycastle.crypto.params.RSAKeyParameters.<init>(Unknown Source)
Caused by: org.graalvm.compiler.java.BytecodeParser$BytecodeParserError: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected. Object has been initialized by the org.bouncycastle.jcajce.provider.drbg.DRBG$Default class initializer with a trace:
at org.bouncycastle.crypto.prng.SP800SecureRandom.<init>(Unknown Source)
at org.bouncycastle.crypto.prng.SP800SecureRandomBuilder.buildHash(Unknown Source)
at org.bouncycastle.jcajce.provider.drbg.DRBG.createBaseRandom(Unknown Source)
at org.bouncycastle.jcajce.provider.drbg.DRBG.access$100(Unknown Source)
at org.bouncycastle.jcajce.provider.drbg.DRBG$Default.<clinit>(Unknown Source)
. Try avoiding to initialize the class that caused initialization of the object. The object was probably created by a class initializer and is reachable from a static field. You can request class initialization at image runtime by using the option --initialize-at-run-time=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Caused by: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected. Object has been initialized by the org.bouncycastle.jcajce.provider.drbg.DRBG$Default class initializer with a trace:
at org.bouncycastle.crypto.prng.SP800SecureRandom.<init>(Unknown Source)
at org.bouncycastle.crypto.prng.SP800SecureRandomBuilder.buildHash(Unknown Source)
at org.bouncycastle.jcajce.provider.drbg.DRBG.createBaseRandom(Unknown Source)
at org.bouncycastle.jcajce.provider.drbg.DRBG.access$100(Unknown Source)
at org.bouncycastle.jcajce.provider.drbg.DRBG$Default.<clinit>(Unknown Source)
There’s 4 more such errors. I am at a loss of how to proceedFWIW, graalvm-ce-java11-22.3.3 is pretty old
yeah I know, but its a larger project and this was supposed to be a simple task and now it has ballooned into multiple day troubleshooting
Also: unbalanced monitor stuff indicates you or something else is using an older clojure version than 1.11
unbalanced monitor?
Sorry, I scrolled up too much
that was a different post ;)
> Detected an instance of Random/SplittableRandom class in the image heap. This indicates that .. what is says and this usually happens when something calls a random expression on the top level, rather than at runtime in a function body
how would I detect where this happens?
bisecting and following graal's output
run the most minimal example you can come up with
start with an an empty -main function. don't load any libraries. Then start adding stuff until you hit the error and then investigate
I thought that the whole point of rerun-class-initialization-at-runtime option was to reinitialize random generators at runtime
and that would fix that
yes, that would in fact be nice if it worked :)
that's a new feature though?
hmm, no, it seems to be mentioned way back, but I do remember something was announced around this in recent release notes
my "dumb" solution to this is just to avoid any top level randomness
Might be hard to do. For instance in taoensso.encore thereis:
#?(:clj
(compile-if (fn [] (java.security.SecureRandom/getInstanceStrong)) ; Java 8+, blocking
(def ^:private srng* (thread-local-proxy (java.security.SecureRandom/getInstanceStrong)))
(def ^:private srng* (thread-local-proxy (java.security.SecureRandom/getInstance "SHA1SRNG")))))This dependency is transitively used in bb as well. I haven't had any problems with this specific case
Perhaps I haven't upgraded it
which version?
but I know for a fact that @ptaoussanis is pretty open to making stuff work with graalvm native-image
which version, let me check
My main namespace doesn’t include any of these classes
so I am guessing that graalvm also looks at other namespaces that are not directly included
no, it just follows whatever is reachable from your main function
I mean I get this warning:
Warning: Using a deprecated option --rerun-class-initialization-at-runtime from command line. Currently there is no replacement for this option. Try using --initialize-at-run-time or use the non-API option -H:ClassInitialization directly.I guess I have to use an even older graalvm?
yes, I would definitely upgrade and consult the most recent docs
FWIW this is what is used in bb: [com.taoensso/timbre "6.5.0"] [com.taoensso/encore "3.85.0"] [com.taoensso/truss "1.11.0"] [org.clj-commons/pretty "2.2.1"]
My main is just:
(ns cli.release
(:gen-class)
(:import (java.security Signature)))
(defn -main [& args]
(Signature/getInstance "EDDSA" "BC"))ok, but because of your bountycastle feature stuff is still loaded. I'm not sure. have you tried compiling the buddy pod?
you could perhaps just start with that and then morph it into your project
But I do have a bunch of classes in --initialize-at-build-time option
Hm good idea
oh yes, you should try to avoid built-time for Java classes whenever you can
only use this fro clojure namespaces
This plugin will take care of that for you: https://github.com/clj-easy/graal-build-time
but your pod also has --initialize-at-build-time=org.bouncycastle
hmm
I don't remember why I did that, it's been a while
I'll trigger CI to see if it still builds
just ran it and it builds
ok
but I guess only the classes that are actually used by the main function will get initialized at build time
and perhaps org.bouncycastle.jcajce.provider.drbg isn't reached in this case?
If I delete that option from the pod build it fails
rg.bouncycastle.jcajce.provider.asymmetric.edec.KeyFactorySpi was unintentionally initialized at build time. and so on and on about all the bouncy castle classes
now I have changed the entry point to:
(ns cli.release
(:gen-class))
(defn -main [& args]
1)
and it still failsWould clj_easy.graal_build_time.InitClojureClasses do more class initializations at build time?
How would that class know which classes that are clojure namespaces to initialize?
Ah:
List var2 = var1.getApplicationClassPath();
String[] var3 = packages.list(var2);
String var4 = packages.listStr(var3);
System.out.println("[clj-easy/graal-build-time] Registering packages for build time initialization: " + var4);
RuntimeClassInitialization.initializeAtBuildTime(var3);
So basically, regardless of how many namespaces I include from the entry point, all clojure namespaces are initializedOk managed to do it
So the thing is that the option rerun-class-initialization-at-runtime is crucial. And at my version of graalvm it is deprecated and ignored. I had to add code to that Feature:
@Override
public void afterRegistration(AfterRegistrationAccess access) {
RuntimeClassInitialization.initializeAtBuildTime("org.bouncycastle");
RuntimeClassInitializationSupport rci = ImageSingletons.lookup(RuntimeClassInitializationSupport.class);
rci.rerunInitialization("org.bouncycastle.jcajce.provider.drbg.DRBG$Default", "");
rci.rerunInitialization("org.bouncycastle.jcajce.provider.drbg.DRBG$NonceAndIV", "");
Security.addProvider(new BouncyCastleProvider());
}
}
But that doesn’t play with the module system as the RuntimeClassInitializationSupport is not exported. So I had to also add option --add-exports=org.graalvm.sdk/org.graalvm.nativeimage.impl=ALL-UNNAMED . If the Feature class was in a module then I would have to name that moduleI think I read something in recent release notes that there is a new thing in addition to initialize-at-runtime and build-time which re-runs the class
but I can't actually find it now
@borkdude out of curiosity about how long do your linux native-image builds of bb take?
it depends on your computer ;)
on my m1 mac it takes 2-3 minutes
on circleci it takes longer, on github actions it takes even longer since that tends to not be as fast
ok I feel in the right ballpark then... thanks