Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Clojure CLI by

Hi,

I'm getting a PKIX certificate exception when trying to download project dependnecies with the clojure tool behind a strict firewall on MS-Windows:

Could not transfer artifact ... from/to central
(https://repo1.maven.org/maven2/):
sun.security.validator.ValidatorException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException: unable
to find valid certification path to requested target

I believe this is due to self-signed root certificate utilized by the Firewall to control & monitor the traffic.

Analysis follows and apologies for the long read.


It is a standard practice in big organisations for workstations to sit behind a firewall that controls and monitors all traffic to and from the Internet. It is also common for the company to create their own self-signed root certificate and replace with it the certificates found in the https traffic so as to decrypt and analyze the data flows, utilizing in effect the man-in-the-middle attack to their advantage.

For this to work, the user workstation where the connection originates from needs to have the self-signed certificates installed in their trusted certificate store. On Windows, this will be the trusted root authority certification keystore.

Java comes with its own certificate store, the java KeyStore, which is separate from the general keystore available on the workstation. Thus, any java https connections to the Internet through the firewall is most likely to fail with an unknown certificate error, since the self-signed certs are not installed on the java keystore.

This adversely affects the clojure/clj command line tools when downloading libs because it can't find the self-signed cert in the java keystore. For example, an exception is thrown when trying to access the Internet from such computers:

 > clojure -P
 
 Error building classpath ... 
 org.eclipse.aether.resolution.ArtifactDescriptorException:   Failed to
 read artifact descriptor for ....  ...  Caused by:
 org.eclipse.aether.resolution.ArtifactResolutionException:   Could not
 transfer artifact ... from/to central
 (https://repo1.maven.org/maven2/):   
 sun.security.validator.ValidatorException: PKIX path building failed:
      sun.security.provider.certpath.SunCertPathBuilderException:
        unable to find valid certification path to requested target  ...
  ...

I can think of at least two solution to circumvent this problem:
1. Install the missing certificates in the java keystore, or
2. Instruct the java instance to look into the general keystore.

There does not seem to be a straightforward way to discover the missing certificates and would probably require some user effort to install them into the java keystore using the java keytool (See Working with Certificates and SSL at https://docs.oracle.com/cd/E19830-01/819-4712/ablqw/index.html).

Instead, it seems easier to instruct java to look at the general or alternative keystores using the java.next.ssl.* properties (https://stackoverflow.com/questions/5871279/ssl-and-cert-keystore).

On Windows, which I assume most of users with such restrictive firewalls are sitting on, it is a just matter of setting a simple java property to instruct java to use the general windows keystore (https://stackoverflow.com/questions/41257366/import-windows-certificates-to-java):

javax.net.ssl.trustStoreType=Windows-ROOT

Given this, I was expecting the following to work on windows

clojure -J-D'javax.net.ssl.trustStoreType=Windows-ROOT' -P

but it doesn't. It turns out the $JvmOpts variable is not passed onto the prep command by the powershell script here https://github.com/clojure/brew-install/blob/b91fb78e321b5e39bedda594f5d578579d448d19/src/main/resources/clojure/install/ClojureTools.psm1#L388 :

& $JavaCmd -classpath $ToolsCp clojure.main -m clojure.tools.deps.alpha.script.make-classpath2 --config-user $ConfigUser --config-project $ConfigProject --basis-file $BasisFile --libs-file $LibsFile --cp-file $CpFile --jvm-file $JvmFile --main-file $MainFile --manifest-file $ManifestFile @ToolsArgs

Some options I can think of to utilise java properties to solve the general issue, from most intrusive/specific to the general case:

  1. Have the clojure.tools.deps.alpha.script.make-classpath2 fn which is responsible for downloading the dependencies set javax.net.ssl.trustStoreType=Windows-ROOT at runtime, if running on Windows,
    • .e.g. (System/setProperty "javax.net.ssl.trustStoreType" "Windows-ROOT").
    • cons:
      • This only works on windows.
      • forces to use of the general keystore, which might not be what the user always want.
        • This can be mitigated by having a new script option (say -W) that is passed onto clojure.tools.deps.alpha as an option. The drawback is that users/tooling has to always pass this flag on invocation.
      • setting the javax.net.ssl.tustStoreType property at runtime with System/setProperty could have no effect if any SSL connection was previously made from other parts of the code.
  2. Same as previous, but with a new script option (say -Sssl EDN) whose EDN are the javax.net.ssl.* properties, and will be passed as such to clojure.tools.deps.alpha for setting at runtime.
    • e.g. clojure -Sssl {:trustStoreType "Windows-ROOT"} -P
    • cons:
      • Same disadvantages as in #1, and moreover is more cumbersome for the user to type an edn map each time it invokes clojure.
  3. Invent a new deps.edn key (e.g. :clojure.tools.deps.alpha/ssl) whose pairs shall be java.net.ssl.* properties, that can be set in the user config deps.edn and parsed by clojure.tools.deps.alpha s make-classpath2 fn setting the properties using System/setPropertyat runtime.
    • e.g. example clj user config {:clojure.tools.deps.alpha/ssl {:trustStore "Windows-ROOT"}}
    • cons
      • setting the javax.net.ssl.* properties at runtime with System/setProperty might have no effect if any SSL connection was previously made from other parts of the code.
  4. Update script so that -J jvm options are passed onto the java invocation of the -P command (I have tested this to work).
    • e.g. clojure -J-D'javax.net.ssl.trustStoreType=Windows-ROOT' -P
    • cons:
      • User/tooling still have to provide the -J-Djavax.net.ssl.* options at each clojure invocation.
  5. Same as previous, but introduce a CLJ_JVMOPTS environment variable whose value is inserted in the script's $JvmOpts variable.
    • e.g. set CLJ_JVMOPTS=-D'javax.net.ssl.trustStoreType=Windows-ROOT', and then clojure -P

I'm more in favor of #5, since this does not seem to have any disadvantages and can be set once and be applied on all clojure invocations, creating a better experience for the user/tooling.

Let me know your thoughts. Happy to look after the changes in the scripts and/or tools.deps.alpha lib.

Thanks

PS: I have also tried to set the properties in the MAVEN_OPTS variable as pe r https://maven.apache.org/guides/mini/guide-repository-ssl.html, but it had no effect. I suspect this only take effect when invoking the mvn command line tool directly.

1 Answer

+1 vote
by

I think this is all correct analysis and we've run into a few cases where jvm properties may need to be set on the java call that generates the classpath, which as you've detected cannot currently be set.

We do have an existing ticket for this at https://clojure.atlassian.net/browse/TDEPS-165, but I haven't worked on it yet. I suspect the end solution will be to open up a new clj option or env var to allow passing jvm properties to the classpath construction java call.

by
Shall I go ahead and create a prototype patch introducing a new CLJ_JVMOPTS environment variables that will be merged with the standard -J options and passed to all java invocation in the scripts?

or Is there a concern of passing the jvm options to all java invocation, not just that the used for the class path constructions/downloads?
by
I have not done enough thinking about this to tell you what a patch should do. I don't think it makes sense to pass these to every java invocation (particularly the user's application).
by
Could you please elaborate a bit more  why it doesn't make sense to pass the jvm opts to every java invocation in the script? I can't think of any disadvantages. Thanks
by
I've updated the ticket with plans, and actually implemented and did a prerelease (1.11.1.1161) with support for a new CLJ_JVM_OPTS. It does not yet support Windows though and would love any recs on the right equivalent incantation for https://github.com/clojure/brew-install/commit/d97cf4145ccfee712ec99b906f7bf19b1406c131 in the Powershell version - the tricky thing is to handle multiple args correctly.
by
Hi,

I have done a little bit of research how to pass the extra jvm arguments to the java invocation, and the only good way I came up with that handles them gracefully under most (all?) circumstances is to pass them via the `JAVA_TOOL_OPTION`S env variable. If we can add the extra arguments to the end of this env variable, then  they will be picked up by the java tool and processed as they are supposed to.

I've created a draft implementation to demonstrate: https://github.com/ikappaki/brew-install/commit/3584b02caced8e4a15c6993c73ceb4c202a6c623. The only slight impl complication with this method is that we have to restore the env var after java command invocation has finished so its updated value is not leaked in the environment.

The other option I consider was parsing the env variable with `iex` like `$CljJvmOpts = iex "echo $env:CLJ_JVM_OPTS"`, but this messes up with the standard quoting mechanism and is very hard to pass in quotes and double quotes.

The problem with passing any user value to the $java command line invocation, is that we might end up with undesirable situations, like a semicolon appearing in the command line or any other powershell operator, that can mess up with the invocation syntax, not to mention it could end up as a security risk (e.g. setting the env variable to something like `;rm -fr ./;'.

What do you think? thanks
by
Yeah, I don't like that much. I've pushed up a change with added support in windows to split the env var on spaces and splat to the command line, which seems to work pretty well with everything I've tried. That's in version 1.11.1.1165 if you can give it a try (installer is at https://download.clojure.org/install/win-install-1.11.1.1165.ps1 )
by
It looks good with the caveat that an unconventional value might adversely affect the java invocation as mentioned in my earlier comment. It works well for my test case.

May I please enquire the purpose of the second new JAVA_OPTS env variable, and why the java invocations are split between this and CLJ_JVM_OPTS? I was expecting a single CLJ_JVM_OPTS env var passed to all java invocations.

https://github.com/clojure/brew-install/commit/7914954030ca21f7b928b6f064ace086efdc9057

Thanks
by
Ok, this is now released as 1.11.1.1165.

There are two properties because there are two contexts with (potentially) different needs and lots of cases where you might want a property on one but not the other.
by
Thanks!

And here how this example use case can be solved with the new environment variables, in case anyone else is interested.

To use the Windows certificate store when downloading project dependencies by setting the env variable in the PowerShell prompt:

$env:CLJ_JVM_OPTS = "-Djavax.net.ssl.trustStoreType=Windows-ROOT"

To use the Windows certificate store when running the project via the Clojure Tools, by setting the environment in the PowerShell prompt:

$env:JAVA_OPTS = "-Djavax.net.ssl.trustStoreType=Windows-ROOT"
...