Skip to content

Conversation

@lqhuang
Copy link

@lqhuang lqhuang commented Aug 20, 2025

Hey @lolgab,

I'm Lanqing Huang, and working on the Issue Support for java.net.http.HttpClient.

In general, I'm trying to implement a Java HttpClient shim for Scala Native. My repo lqhuang/scala-native-http is also forked from your lolgab/scala-native-http-client-async. Thank you for your initial work!

And obviously, the SSLContext is a crucial component of the HttpClient. Hence, I recognize project scala-native-crypto as an upstream project of scala-native-http. I hope to contribute several essential classes into scala-native-crypto.

I have finished some classes with signature or basic implementations. It wouldn't be easy to review, I'm thinking I should ask your opinions first :) Like:

  1. Did you wish me to split the PR into multiple small PRs?
  2. Or just make some MVP examples and let's review together.
  3. Did you have other suggestions?

And after few works, I found SSLContext have many abstractions about SSLSocket (well, they're layouted under javax.net.ssl)

So, I'm also thinking I'm probably just need to port related classes under java.security and javax.crypto into scala-native-crypto and left SSLContext and others in javax.net back to scala-native-http?

Regards,
Lanqing


First update

I have spent a lot of time to learn

  1. Java Cryptography Architecture (JCA) Reference Guide
  2. Java SE: Security Algorithm Implementation Requirements

And now the goal of current PR has shifted. Bascially, I add all major interfaces of Java Crypto modules and do some refactors without changing current done features.

I think we could merge something first and then in next PR, I will try to implement crypto algorithms required by SSLContext. It should be eaiser for both you and me?

Here are the updates to help you with your review:

  1. Major interfaces or abstract classes required by JCA, including

    • AlgorithmParameterGenerator
    • AlgorithmParameters
    • CertificateFactory
    • CertPathBuilder
    • CertPathValidator
    • CertStore
    • Cipher
    • ExemptionMechanism
    • KDF
    • KEM
    • KeyAgreement
    • KeyFactory
    • KeyGenerator
    • KeyPairGenerator
    • KeyStore
    • MessageDigest
    • SecretKeyFactory
    • SecureRandom
    • Signature

    All the methods and their exceptions have aligned with JDK docs

  2. Implement a "Provider" to allow using different crypto backends.

    Then we can register custom implementations of providers (openssl / boringssl / s2n-tls / ...) in the future. e.g.: While installing scala-native-crypto-boringssl will register BoringSSLProvider

    But it's not perfect. Java's crypto and security modules are "finished" since JDK 1.4 and it have been designed to use dynamic loading for classes which is not usable for Scala Native.

    They are two classes need to implement under JCA (Service and SericeSpi, like Cipher and CipherSpi, here Spi stands Service Provider Interface). But I would say it's quite messy and chaos, I found sometime the Service extends the SerivceSpi, sometime the SericeSpi is passing into Service constructor via composition while I reading JDK docs.

    The good news is both Service and ServiceSpi are not exposed to user. So in this PR, I decide to remove all spi interfaces and keep abstract Service classes, which may not exactly the same with JDK architectures.

    Then, I tried to refactor two current existed services Mac and MessageDigest into an unified OpenSslProvider (The underlying internal implementations haven't been modified), you could check related to see is the design suitable or not?

    IMHO, we're impossible to get a perfect Java shims, but the current solution just work and it will be clearer to complete all crypto algorithms in the future.

  3. Improve README to provide a list of implemented algorithms for users and contributors

  4. Improve build.mill

    • Stripe .scala
    • Add a conditional include path finder as flag in different platform.
  5. Upgrade CI workflow

    • Upgrade Ubuntu 22.04 to 24.04
    • Upgrade mill to 1.0.6 (patched by @lolgab)
    • Add JDK 25 to jvm matrix
    • Upgrade actions/checkout@v3 -> actions/checkout@v5, actions/setup-java@v3 -> actions/setup-java@v5

Testing results (comment out IllegalArgumentException tests for SecretKeySpec constructor)

[939/958] tests.native[2.12.20].nativeLink
[939] [info] Linking (multithreadingEnabled=true, disable if not used) (2487 ms)
[939] [info] Discovered 2026 classes and 15112 methods after classloading
[939] [info] Loaded 0 service provider(s) for 1 referenced service(s):
[939] [info] |------------------------------------------------------------------|
[939] [info] | Service                              | Service Provider | Status |
[939] [info] |------------------------------------------------------------------|
[939] [info] | java.nio.charset.spi.CharsetProvider | ---              | NoProviders |
[939] [info] |------------------------------------------------------------------|
[939] [info] Checking intermediate code (quick) (85 ms)
[938/958] tests.native[2.13.15].nativeLink
[938] [info] Linking (multithreadingEnabled=true, disable if not used) (2735 ms)
[938] [info] Discovered 2105 classes and 14917 methods after classloading
[938] [info] Loaded 0 service provider(s) for 1 referenced service(s):
[938] [info] |------------------------------------------------------------------|
[938] [info] | Service                              | Service Provider | Status |
[938] [info] |------------------------------------------------------------------|
[938] [info] | java.nio.charset.spi.CharsetProvider | ---              | NoProviders |
[938] [info] |------------------------------------------------------------------|
[940/958] tests.native[3.3.4].nativeLink
[940] [info] Linking (multithreadingEnabled=true, disable if not used) (2816 ms)
[940] [info] Discovered 2133 classes and 14718 methods after classloading
[940] [info] Loaded 0 service provider(s) for 1 referenced service(s):
[940] [info] |------------------------------------------------------------------|
[940] [info] | Service                              | Service Provider | Status |
[940] [info] |------------------------------------------------------------------|
[940] [info] | java.nio.charset.spi.CharsetProvider | ---              | NoProviders |
[940] [info] |------------------------------------------------------------------|
[938] [info] Checking intermediate code (quick) (496 ms)
[942/958] jwt-scala-tests.native[3.3.4].nativeLink
[942] [info] Linking (multithreadingEnabled=true, disable if not used) (3309 ms)
[942] [info] Discovered 2688 classes and 19296 methods after classloading
[942] [info] Loaded 0 service provider(s) for 1 referenced service(s):
[942] [info] |------------------------------------------------------------------|
[942] [info] | Service                              | Service Provider | Status |
[942] [info] |------------------------------------------------------------------|
[942] [info] | java.nio.charset.spi.CharsetProvider | ---              | NoProviders |
[942] [info] |------------------------------------------------------------------|
[940] [info] Checking intermediate code (quick) (513 ms)
[942] [info] Checking intermediate code (quick) (553 ms)
[939] [info] Discovered 1984 classes and 11605 methods after optimization
[939] [info] Optimizing (debug mode) (2463 ms)
[939] [info] Discovered 2061 classes and 11369 methods after optimization
[939] [info] Optimizing (debug mode) (3086 ms)
[939] [info] Discovered 2084 classes and 11437 methods after optimization
[939] [info] Optimizing (debug mode) (3686 ms)
[939] [info] Discovered 2632 classes and 15190 methods after optimization
[939] [info] Optimizing (debug mode) (4355 ms)
[939] [info] Produced 21 LLVM IR files
[939] [info] Generating intermediate code (3688 ms)
[939] [info] Produced 21 LLVM IR files
[939] [info] Generating intermediate code (3364 ms)
[939] [info] Compiling to native code (2278 ms)
[939] [info] Linking with [pthread, dl, crypto]
[939] [info] Linking native code (immix gc, none lto) (457 ms)
[939] [info] Postprocessing (0 ms)
[939] [info] Total (11569 ms)
[939] [info] Produced 21 LLVM IR files
[939] [info] Generating intermediate code (3498 ms)
[955/958] tests.native[2.12.20].test
[955] [info] Starting process '/home/lqhuang/Git/scala-native-crypto/out/tests/native/2.12.20/nativeLink.dest/out' on port '40631'.
-------------------------------- Running Tests -------------------------------- 13s
+ scalanativecrypto.MacSuite.HmacSHA256 4ms
+ scalanativecrypto.MacSuite.HmacSHA256 - update not initialized 18ms
+ scalanativecrypto.MacSuite.HmacSHA256 - doFinal not initialized 1ms
+ scalanativecrypto.MacSuite.HmacSHA3-256 0ms
+ scalanativecrypto.MacSuite.HmacSHA3-256 - update not initialized 0ms
+ scalanativecrypto.MacSuite.HmacSHA3-256 - doFinal not initialized 0ms
[955] Test result: 1 completed.
-------------------------------- Running Tests -------------------------------- 13s
+ scalanativecrypto.MessageDigestSuite.memoryLeak 1ms
+ scalanativecrypto.MessageDigestSuite.basicMd5DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.emptyMd5DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA1DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.variousUpdatesSHA1DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.digestWithResultBuffer 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA224Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA256Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA384Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA512Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-224Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-256Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-384Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-512Digest 0ms
[955] Test result: 2 completed.
-------------------------------- Running Tests -------------------------------- 13s
+ scalanativecrypto.SecretKeySpecSuite 0ms
[955] Test result: 3 completed.
-------------------------------- Running Tests -------------------------------- 13s
+ scalanativecrypto.SecureRandomSuite.basicSecureRandomTest 1ms
+ scalanativecrypto.SecureRandomSuite.basicRandomUUIDTest 0ms
+ scalanativecrypto.SecureRandomSuite.basicNextIntTest 0ms
[955] Test result: 4 completed.
[955] Tests: 24, Passed: 24, Failed: 0
[939] [info] Compiling to native code (3055 ms)
[939] [info] Linking with [pthread, dl, crypto]
[939] [info] Compiling to native code (2445 ms)
[939] [info] Linking with [pthread, dl, crypto]
[939] [info] Produced 21 LLVM IR files
[939] [info] Generating intermediate code (4310 ms)
[939] [info] Linking native code (immix gc, none lto) (515 ms)
[939] [info] Postprocessing (0 ms)
[939] [info] Total (13811 ms)
[956/958] tests.native[2.13.15].test
[956] [info] Starting process '/home/lqhuang/Git/scala-native-crypto/out/tests/native/2.13.15/nativeLink.dest/out' on port '33433'.
-------------------------------- Running Tests -------------------------------- 15s
[939] [info] Linking native code (immix gc, none lto) (621 ms)
[939] [info] Postprocessing (0 ms)
[939] [info] Total (14107 ms)
+ scalanativecrypto.MacSuite.HmacSHA256 9ms  est ============================== 15s
[957/958] tests.native[3.3.4].test
+ scalanativecrypto.MacSuite.HmacSHA256 - update not initialized 23ms  ======== 15s
[957] [info] Starting process '/home/lqhuang/Git/scala-native-crypto/out/tests/native/3.3.4/nativeLink.dest/out' on port '37671'.
+ scalanativecrypto.MacSuite.HmacSHA256 - doFinal not initialized 3ms  ======== 15s
+ scalanativecrypto.MacSuite.HmacSHA3-256 0ms  14s
+ scalanativecrypto.MacSuite.HmacSHA3-256 - update not initialized 0ms
+ scalanativecrypto.MacSuite.HmacSHA3-256 - doFinal not initialized 0ms
-------------------------------- Running Tests --------------------------------
[956] Test result: 1 completed.
+ scalanativecrypto.MessageDigestSuite.memoryLeak 1ms  ======================== 15s
+ scalanativecrypto.MessageDigestSuite.basicMd5DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.emptyMd5DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA1DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.variousUpdatesSHA1DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.digestWithResultBuffer 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA224Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA256Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA384Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA512Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-224Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-256Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-384Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-512Digest 0ms
[956] Test result: 2 completed.
-------------------------------- Running Tests -------------------------------- 15s
+ scalanativecrypto.SecretKeySpecSuite 0ms  nk 14s
[956] Test result: 3 completed.
-------------------------------- Running Tests -------------------------------- 15s
+ scalanativecrypto.SecureRandomSuite.basicSecureRandomTest 0ms
+ scalanativecrypto.SecureRandomSuite.basicRandomUUIDTest 0ms
+ scalanativecrypto.SecureRandomSuite.basicNextIntTest 0ms
[956] Test result: 4 completed.
[956] Tests: 24, Passed: 24, Failed: 0
-------------------------------- Running Tests -------------------------------- 15s
+ scalanativecrypto.MacSuite.HmacSHA256 3ms  k 14s
+ scalanativecrypto.MacSuite.HmacSHA256 - update not initialized 7ms
+ scalanativecrypto.MacSuite.HmacSHA256 - doFinal not initialized 0ms
+ scalanativecrypto.MacSuite.HmacSHA3-256 0ms
+ scalanativecrypto.MacSuite.HmacSHA3-256 - update not initialized 0ms
+ scalanativecrypto.MacSuite.HmacSHA3-256 - doFinal not initialized 0ms
[957] Test result: 1 completed.
-------------------------------- Running Tests -------------------------------- 15s
+ scalanativecrypto.MessageDigestSuite.memoryLeak 1ms
+ scalanativecrypto.MessageDigestSuite.basicMd5DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.emptyMd5DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA1DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.variousUpdatesSHA1DigestTest 0ms
+ scalanativecrypto.MessageDigestSuite.digestWithResultBuffer 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA224Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA256Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA384Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA512Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-224Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-256Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-384Digest 0ms
+ scalanativecrypto.MessageDigestSuite.basicSHA3-512Digest 0ms
[957] Test result: 2 completed.
-------------------------------- Running Tests -------------------------------- 16s
+ scalanativecrypto.SecretKeySpecSuite 0ms  nk 14s
[957] Test result: 3 completed.
-------------------------------- Running Tests -------------------------------- 16s
+ scalanativecrypto.SecureRandomSuite.basicSecureRandomTest 0ms
+ scalanativecrypto.SecureRandomSuite.basicRandomUUIDTest 0ms
+ scalanativecrypto.SecureRandomSuite.basicNextIntTest 0ms
[957] Test result: 4 completed.
[957] Tests: 24, Passed: 24, Failed: 0
[940] [info] Compiling to native code (1884 ms)
[939] [info] Linking with [pthread, dl, crypto]
[939] [info] Linking native code (immix gc, none lto) (195 ms)
[939] [info] Postprocessing (0 ms)
[940] [info] Total (15162 ms)
[958/958] jwt-scala-tests.native[3.3.4].test
[958] [info] Starting process '/home/lqhuang/Git/scala-native-crypto/out/jwt-scala-tests/native/3.3.4/nativeLink.dest/out' on port '37247'.
-------------------------------- Running Tests -------------------------------- 16s
+ scalanativecrypto.JwtExampleSuite.JWT should be correctly generated, signed, and verified 3ms
[958] Test result: 1 completed.
[958] Tests: 1, Passed: 1, Failed: 0
[958/958] ============================== __.test ============================== 17s

Flaky parts: I cannot understand why SecretKeySpec constructor tests cannot pass now. I have tuned to throw IllegalArgumentExcSeption instead of NullPointerException ...

X scalanativecrypto.MessageDigestSuite.SecretKeySpec.null key 8ms
  utest.AssertionError: new SecretKeySpec(null, "")
  java.lang.NullPointerException
    scala.scalanative.runtime.package$.throwNullPointer(Unknown)
    <none>.(Unknown)
    scalanativecrypto.MessageDigestSuite$$$Lambda$5.apply(Unknown)
    utest.asserts.Util$.runAssertionEntry(Unknown)
    utest.asserts.Asserts$.interceptImpl(Unknown)
    scalanativecrypto.MessageDigestSuite$.$anonfun$tests$20(Unknown)
+ scalanativecrypto.MessageDigestSuite.SecretKeySpec.null algorithm 6ms
+ scalanativecrypto.MessageDigestSuite.SecretKeySpec.empty key 0ms
[955] Test result: 2 completed, 1 failures.
-------------------------------- Running Tests -------------------------------- 14s
X scalanativecrypto.SecretKeySpecSuite.SecretKeySpec.null key 1ms
  utest.AssertionError: new SecretKeySpec(null, "")
  java.lang.NullPointerException.4].nativeLink 12s
    scala.scalanative.runtime.package$.throwNullPointer(Unknown)
    <none>.(Unknown)
    scalanativecrypto.SecretKeySpecSuite$$$Lambda$1.apply(Unknown)
    utest.asserts.Util$.runAssertionEntry(Unknown)
    utest.asserts.Asserts$.interceptImpl(Unknown)
    scalanativecrypto.SecretKeySpecSuite$.$anonfun$tests$3(Unknown)

Second update

lolgab help me to upgrade mill version to v1.0.6 which fix the Java 25 CI jobs. And he also address the NPE issue. Nice!

All tests have passed. All CI jobs are also resolved.

@lolgab
Copy link
Owner

lolgab commented Aug 21, 2025

  1. Did you wish me to split the PR into multiple small PRs?

No, it's not necessary, we can merge everything in one go.

  1. Or just make some MVP examples and let's review together.

What we need is some tests that cover the classes and can run on JVM as well and have the same results in both JVM and Native.

  1. Did you have other suggestions?

The only suggestion from skimming over the code is that we don't need the @throws annotation since that is for java compatibility. So it's not needed on Scala Native I think.

So, I'm also thinking I'm probably just need to port related classes under java.security and javax.crypto into scala-native-crypto and left SSLContext and others in javax.net back to scala-native-http?

I agree with you. It seems to be the best approach.

@lolgab lolgab changed the title feat: Try to impl ssl context required classes feature: Implement SSLContext required classes Aug 26, 2025
@lqhuang
Copy link
Author

lqhuang commented Aug 30, 2025

A mark for signatures of major classes have been added and programs are compiled successfully under Scala 2.12, 2.13 and 3.

Then next step I will try to implement the details.

@lqhuang lqhuang changed the title feature: Implement SSLContext required classes refactor: Implement major interfaces of Java Cryptography Architecture and refactor existed services into OpenSslProvider Oct 18, 2025
@lqhuang lqhuang changed the title refactor: Implement major interfaces of Java Cryptography Architecture and refactor existed services into OpenSslProvider refactor: Implement major interfaces of Java Cryptography Architecture and reorganize existed services into OpenSslProvider Oct 18, 2025
@lqhuang lqhuang marked this pull request as ready for review October 18, 2025 09:55
@lqhuang
Copy link
Author

lqhuang commented Oct 18, 2025

@lolgab Hey, I have updated the content and goal of current PR, could you check the first update for more details?

I'm glad to explain anything that's unclear.

Any feedback and advice is appreciated!

Thanks a lot 😄

@lqhuang

This comment was marked as resolved.

@lqhuang

This comment was marked as outdated.

@lolgab
Copy link
Owner

lolgab commented Oct 23, 2025

I'm updating Mill in #26. Let's see if it fixes your problems with Java 25

@lqhuang
Copy link
Author

lqhuang commented Oct 23, 2025

I'm updating Mill in #26. Let's see if it fixes your problems with Java 25

Yes, it works. I add Java 25 back to CI matrix.

But now, ./mill --no-server __.test cannot work anymore.

I changed the CI command to ./mill --no-server __.testLocal (other two options are testForked, testOnly, am I wrong here?)

runner.dialect = scala213

# automatically appended by scalafmt itself
project.excludePaths = ["glob:**/out/**", "glob:**/jwt-scala-tests/src/**"]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why excluding jwt-scala-tests? Doesn't sound correct.

Copy link
Author

@lqhuang lqhuang Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea, I can remove it. Since these also aren't added by myself either. scalafmt (via metals) will hint these while opening current project with IDE combo: VS Code + metals + mill

Comment on lines +66 to +76
val osName = {
val _osName = Properties.osName.toLowerCase()
if (_osName.contains("mac")) "darwin"
else if (_osName.contains("windows")) "windows"
else "linux"
}
val archName = Properties.propOrEmpty("os.arch").toLowerCase() match {
case "amd64" => "x86_64" // try to follow llvm triple
case "arm64" => "aarch64"
case s => s
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid this indirection to make java os.arch and osName to look like clang target triple values.

Copy link
Author

@lqhuang lqhuang Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or follow the GCC triple? I copy similar conditional detections from mill project to detect platform (win/linux/mac). But the os.arch triple is added by myself. I thought scala-native is LLVM based, so I use the LLVM style.

The main purpose is to provide basic snippets for finding proper include libs. Do you have better ideas?

Copy link
Owner

@lolgab lolgab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments but haven't gone over the whole PR yet.

Comment on lines 6 to 13
sealed abstract class CryptoPrimitive
object CryptoPrimitive {

/** Symmetric primitive: block cipher */
case object BLOCK_CIPHER extends CryptoPrimitive

/** Asymmetric primitive: key agreement and key distribution */
case object KEY_AGREEMENT extends CryptoPrimitive
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the way to convert a Java enum in Scala. Check for examples in the scala-native main repository

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed and add a test to ensure the behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants