#951 addresses the issue that SunEC becomes mandatory for GraalVM native-image binaries doing TLS. While this is fine for some uses of native-image, people sometimes want good TLS support, including ECC, in a single binary. My example in point for this is shipping CLI tools.
In #951, a workaround was discovered that easily disables the SunEC provider. A downside is that only DHE/RSA is supported. The purpose of this ticket is to examine how to get single binary that still supports ECDHE.
One way that this could be resolved is if native-image supported e.g. Sulong's LLVM implementation for System.loadLibrary, or native-image learned how to System.loadLibrary libraries that are actually embedded in the image. I have no idea if that's possible or realistic: but I'm not an Oracle employee so deep engineering on GraalVM itself feels a little scary :) I'm calling this approach "loading embedded dynamic libraries".
Another way to hack this together is to add libsunec.so (or .dylib or .dll) as an embedded resource, set java.library.path to a few well-known locations that are likely to be writable across platforms (presumably more than one such location), and write the dylib out at runtime (and delete it when you're done). I'm assuming the runtime impact of that is not too terrible, but the implementation. I'm calling this approach "loading libraries shipped as resources". Because the code that calls System.loadLibrary is generated by SubstrateVM, it's not clear to me if I can make that use System.load with an explicit path, or if you have to modify java.library.path.
Another way that perhaps doesn't require as much hacking on Graal internals is to use a different provider. The best (on the metric of "satisfying my proclivities as a cryptographic engineer") provider available is probably conscrypt, a provider published by Google using BoringSSL. I believe it's the default in modern Android. Unfortunately because BoringSSL itself is a C product, you end up with exactly the same problem that the SunEC provider has: a runtime dependency on a dynamic library.
While I generally don't love BouncyCastle (I am singularly convinced my dinky HTTPS-speaking CLI tool should not have a qTESLA implementation...) it is a pure-Java implementation and therefore "easy" for GraalVM to get working. For the rest of this ticket I'll be focusing on BouncyCastle.
To get replace the SunEC provider, two things need to happen:
- Add an override that prevents SunEC from loading in the first place with
-J-Djava.security.properties=java.security.overrides, where java.security.overrides has lines like security.provider.3=something (3 is the default index of SunEC)
- Add the replacement provider in a static block (or just as a toplevel form of a namespace, because I'm running Clojure) using
Security/addProvider or Security/insertProviderAt.
I created a Clojure program that just downloads https://www.latacora.com via a URL opener. I wrote [nscap][nscap], a tool to take a piece of code and run it in a Linux network namespace together with tcpdump, enabling capture of just the packets from 1 process. I then analyze the resulting dumps with ssldump to see what TLS conversations actually take place. (Eventually I want to do this for macOS and Windows too but I'm not sure how to capture packets from one process across platforms; suggestions welcome.) Finally I summarize the results in the following table:
| Experiment name |
Successful download? |
SunEC warning? |
SunEC idx |
BC idx |
BCJSSE idx |
Best advertised handshake |
Client -> server records |
Server -> client records |
Runtime exception? |
| nooverrides+noinjects |
❌ |
⚠️ |
3 |
🚫 |
🚫 |
💥 |
💥 |
💥 |
|
| nooverrides+addbcprov |
❌ |
⚠️ |
3 |
10 |
🚫 |
💥 |
💥 |
💥 |
|
| nooverrides+addbctls |
❌ |
⚠️ |
3 |
🚫 |
10 |
💥 |
💥 |
💥 |
|
| nooverrides+addbcprov&addbctls |
❌ |
⚠️ |
3 |
10 |
11 |
💥 |
💥 |
💥 |
|
| 3=bcprov+noinjects |
✔️ |
|
🚫 |
🚫 |
🚫 |
DHE |
4 |
4 |
|
| 3=bcprov+addbcprov |
❌ |
|
🚫 |
9 |
🚫 |
💥 |
💥 |
💥 |
Caused by: java.lang.RuntimeException: java.security.NoSuchAlgorithmException: Error constructing implementation (algorithm: EC, provider: BC, class: org.bouncycastle.jcajce.provider.asymmetric.ec.AlgorithmParametersSpi) |
| 3=bcprov+addbctls |
✔️ |
|
🚫 |
🚫 |
9 |
DHE |
2 |
4 |
|
| 3=bcprov+addbcprov&addbctls |
❌ |
|
🚫 |
9 |
10 |
ECDHE |
1 |
💥 |
Caused by: java.lang.RuntimeException: java.security.NoSuchAlgorithmException: Error constructing implementation (algorithm: EC, provider: BC, class: org.bouncycastle.jcajce.provider.asymmetric.ec.AlgorithmParametersSpi) |
| 3=bctls+noinjects |
✔️ |
|
🚫 |
🚫 |
🚫 |
DHE |
2 |
4 |
|
| 3=bctls+addbcprov |
❌ |
|
🚫 |
9 |
🚫 |
💥 |
💥 |
💥 |
Caused by: java.lang.RuntimeException: java.security.NoSuchAlgorithmException: Error constructing implementation (algorithm: EC, provider: BC, class: org.bouncycastle.jcajce.provider.asymmetric.ec.AlgorithmParametersSpi) |
| 3=bctls+addbctls |
✔️ |
|
🚫 |
🚫 |
9 |
DHE |
1 |
4 |
|
| 3=bctls+addbcprov&addbctls |
❌ |
|
🚫 |
9 |
10 |
💥 |
💥 |
💥 |
Caused by: java.lang.RuntimeException: java.security.NoSuchAlgorithmException: Error constructing implementation (algorithm: EC, provider: BC, class: org.bouncycastle.jcajce.provider.asymmetric.ec.AlgorithmParametersSpi) |
| 3=bcprov&4=bctls+noinjects |
❌ |
|
🚫 |
3 |
🚫 |
💥 |
💥 |
💥 |
Caused by: java.security.NoSuchAlgorithmException: Default SSLContext not available |
| 3=bcprov&4=bctls+addbcprov |
❌ |
|
🚫 |
8 |
🚫 |
💥 |
💥 |
💥 |
Caused by: java.security.NoSuchAlgorithmException: Default SSLContext not available |
| 3=bcprov&4=bctls+addbctls |
❌ |
|
🚫 |
🚫 |
8 |
💥 |
💥 |
💥 |
Caused by: java.security.NoSuchAlgorithmException: Unable to invoke creator for DEFAULT: Default key/trust managers unavailable |
| 3=bcprov&4=bctls+addbcprov&addbctls |
❌ |
|
🚫 |
8 |
9 |
💥 |
💥 |
💥 |
Caused by: java.security.NoSuchAlgorithmException: Unable to invoke creator for DEFAULT: Default key/trust managers unavailable |
| 3=empty+noinjects |
❌ |
|
🚫 |
🚫 |
🚫 |
💥 |
💥 |
💥 |
Caused by: java.security.NoSuchAlgorithmException: Default SSLContext not available |
| 3=empty+addbcprov |
❌ |
|
🚫 |
3 |
🚫 |
💥 |
💥 |
💥 |
Caused by: java.security.NoSuchAlgorithmException: Default SSLContext not available |
| 3=empty+addbctls |
❌ |
|
🚫 |
🚫 |
3 |
💥 |
💥 |
💥 |
Caused by: java.security.NoSuchAlgorithmException: Unable to invoke creator for DEFAULT: Default key/trust managers unavailable |
| 3=empty+addbcprov&addbctls |
❌ |
|
🚫 |
3 |
4 |
💥 |
💥 |
💥 |
Caused by: java.security.NoSuchAlgorithmException: Unable to invoke creator for DEFAULT: Default key/trust managers unavailable |
| 3=nil+noinjects |
✔️ |
|
🚫 |
🚫 |
🚫 |
DHE |
5 |
6 |
|
| 3=nil+addbcprov |
❌ |
|
🚫 |
9 |
🚫 |
💥 |
💥 |
💥 |
Caused by: java.lang.RuntimeException: java.security.NoSuchAlgorithmException: Error constructing implementation (algorithm: EC, provider: BC, class: org.bouncycastle.jcajce.provider.asymmetric.ec.AlgorithmParametersSpi) |
| 3=nil+addbctls |
✔️ |
|
🚫 |
🚫 |
9 |
DHE |
4 |
4 |
|
| 3=nil+addbcprov&addbctls |
❌ |
|
🚫 |
9 |
10 |
ECDHE |
3 |
4 |
Caused by: java.lang.RuntimeException: java.security.NoSuchAlgorithmException: Error constructing implementation (algorithm: EC, provider: BC, class: org.bouncycastle.jcajce.provider.asymmetric.ec.AlgorithmParametersSpi) |
The name column is "overrides+injects". Overrides are statements in java.security.overrides. Injects are parsed as follows: if there's an addbcprov, the code includes (Security/addProvider (BouncyCastleProvider.)), if there's an addbctls, the code includes (Security/addProvider (BouncyCastleJsseProvider.)). In all cases, both providers are available on the classpath (but presumably, if they're not being statically loaded they're not otherwise in use, and possibly elided from the final image).
"Successful download?" checks if I could find the <title> tag of the HTML page I'm downloading in the output. "SunEC warning?" shows if the runtime warns it couldn't find the SunEC library, which I'm using as a proxy to see if SunEC is being loaded at all. (SunEC|BC|BCJSSE) idx shows if a provider is in the security providers list at runtime and what index it's at. Best advertised handshake shows "ECDHE" if the client advertises ECDHE, otherwise DHE if it advertises that, otherwise nothing if only RSA suites are supported. This allows me to distinguish between "provider claims to support ECC, but doesn't actually do it" and "no provider with ECC is loaded at all". C->S/S->C records is the number of records sent back and forth (these are records in the TLS protocol sense), runtime exception is what it sounds like.
My interpretation of these results is that if I want TLS to work reliably and I don't care about ECDHE I should just disable the SunEC provider via override, which was the conclusion of #951. Effectively, the builtin TLS support is fine, so bctls is a red herring; we just need an ECC provider implementation. BC does not actually work in any of these cases, which is evidenced by the fact that the table only shows DHE successful attempts.
I think the next step is to try and debug why the native image can't create an instance of bcprov.
Things that would be useful to me:
- Any indication from someone working on SubstrateVM that either I'm on the right path or horribly on the wrong path -- if the answer is "you should forget about this for 3 months and then SubstrateVM will just do the thing you want", that's great!
- Anyone who wants to try the alternative approaches
- Anyone who can help me get bcprov working :)
#951 addresses the issue that SunEC becomes mandatory for GraalVM native-image binaries doing TLS. While this is fine for some uses of native-image, people sometimes want good TLS support, including ECC, in a single binary. My example in point for this is shipping CLI tools.
In #951, a workaround was discovered that easily disables the SunEC provider. A downside is that only DHE/RSA is supported. The purpose of this ticket is to examine how to get single binary that still supports ECDHE.
One way that this could be resolved is if native-image supported e.g. Sulong's LLVM implementation for System.loadLibrary, or native-image learned how to System.loadLibrary libraries that are actually embedded in the image. I have no idea if that's possible or realistic: but I'm not an Oracle employee so deep engineering on GraalVM itself feels a little scary :) I'm calling this approach "loading embedded dynamic libraries".
Another way to hack this together is to add libsunec.so (or .dylib or .dll) as an embedded resource, set
java.library.pathto a few well-known locations that are likely to be writable across platforms (presumably more than one such location), and write the dylib out at runtime (and delete it when you're done). I'm assuming the runtime impact of that is not too terrible, but the implementation. I'm calling this approach "loading libraries shipped as resources". Because the code that calls System.loadLibrary is generated by SubstrateVM, it's not clear to me if I can make that use System.load with an explicit path, or if you have to modifyjava.library.path.Another way that perhaps doesn't require as much hacking on Graal internals is to use a different provider. The best (on the metric of "satisfying my proclivities as a cryptographic engineer") provider available is probably conscrypt, a provider published by Google using BoringSSL. I believe it's the default in modern Android. Unfortunately because BoringSSL itself is a C product, you end up with exactly the same problem that the SunEC provider has: a runtime dependency on a dynamic library.
While I generally don't love BouncyCastle (I am singularly convinced my dinky HTTPS-speaking CLI tool should not have a qTESLA implementation...) it is a pure-Java implementation and therefore "easy" for GraalVM to get working. For the rest of this ticket I'll be focusing on BouncyCastle.
To get replace the SunEC provider, two things need to happen:
-J-Djava.security.properties=java.security.overrides, where java.security.overrides has lines likesecurity.provider.3=something(3 is the default index of SunEC)Security/addProviderorSecurity/insertProviderAt.I created a Clojure program that just downloads
https://www.latacora.comvia a URL opener. I wrote [nscap][nscap], a tool to take a piece of code and run it in a Linux network namespace together withtcpdump, enabling capture of just the packets from 1 process. I then analyze the resulting dumps withssldumpto see what TLS conversations actually take place. (Eventually I want to do this for macOS and Windows too but I'm not sure how to capture packets from one process across platforms; suggestions welcome.) Finally I summarize the results in the following table:The name column is "overrides+injects". Overrides are statements in java.security.overrides. Injects are parsed as follows: if there's an addbcprov, the code includes
(Security/addProvider (BouncyCastleProvider.)), if there's an addbctls, the code includes(Security/addProvider (BouncyCastleJsseProvider.)). In all cases, both providers are available on the classpath (but presumably, if they're not being statically loaded they're not otherwise in use, and possibly elided from the final image)."Successful download?" checks if I could find the
<title>tag of the HTML page I'm downloading in the output. "SunEC warning?" shows if the runtime warns it couldn't find the SunEC library, which I'm using as a proxy to see if SunEC is being loaded at all. (SunEC|BC|BCJSSE) idx shows if a provider is in the security providers list at runtime and what index it's at. Best advertised handshake shows "ECDHE" if the client advertises ECDHE, otherwise DHE if it advertises that, otherwise nothing if only RSA suites are supported. This allows me to distinguish between "provider claims to support ECC, but doesn't actually do it" and "no provider with ECC is loaded at all". C->S/S->C records is the number of records sent back and forth (these are records in the TLS protocol sense), runtime exception is what it sounds like.My interpretation of these results is that if I want TLS to work reliably and I don't care about ECDHE I should just disable the SunEC provider via override, which was the conclusion of #951. Effectively, the builtin TLS support is fine, so bctls is a red herring; we just need an ECC provider implementation. BC does not actually work in any of these cases, which is evidenced by the fact that the table only shows DHE successful attempts.
I think the next step is to try and debug why the native image can't create an instance of bcprov.
Things that would be useful to me: