Unable to use Elliptic Curve Self Signed Certificate in Spring boot
First Environment Details
JDK 17 Spring Boot 3.0.2 Spring Web Tomcat (inbuilt) Mac OS 13.x
Second Environment Details
JDK 17 Spring Boot 3.0.2 Spring Webflux Netty (inbuilt) Mac OS 13.x
Third Environment Details
JDK 17 Spring Boot 2.7.8 Spring Web Tomcat (inbuilt) Mac OS 13.x
For tomcat server I get below error:-
java.io.IOException: EOF during handshake.
at org.apache.tomcat.util.net.SecureNioChannel.handshakeUnwrap(SecureNioChannel.java:470) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.SecureNioChannel.handshake(SecureNioChannel.java:213) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1709) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
2023-02-19T16:25:08.523+05:30 DEBUG 43160 --- [nio-8443-exec-5] o.apache.coyote.http11.Http11Processor : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@5e2b7fa0:org.apache.tomcat.util.net.SecureNioChannel@27f27fb1:java.nio.channels.SocketChannel[connected local=/[0:0:0:0:0:0:0:1]:8443 remote=/[0:0:0:0:0:0:0:1]:55280]], Status in: [CONNECT_FAIL], State out: [CLOSED]
2023-02-19T16:25:08.523+05:30 DEBUG 43160 --- [nio-8443-exec-5] o.apache.tomcat.util.threads.LimitLatch : Counting down[https-jsse-nio-8443-exec-5] latch=1
2023-02-19T16:25:08.523+05:30 DEBUG 43160 --- [nio-8443-exec-5] org.apache.tomcat.util.net.NioEndpoint : Calling [org.apache.tomcat.util.net.NioEndpoint@3f6fedae].closeSocket([org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@5e2b7fa0:org.apache.tomcat.util.net.SecureNioChannel@27f27fb1:java.nio.channels.SocketChannel[connected local=/[0:0:0:0:0:0:0:1]:8443 remote=/[0:0:0:0:0:0:0:1]:55280]])
2023-02-19T16:25:08.524+05:30 ERROR 43160 --- [nio-8443-exec-5] org.apache.tomcat.util.net.NioEndpoint : Failed to close channel
java.io.IOException: Invalid close state, will not send network data.
at org.apache.tomcat.util.net.SecureNioChannel.close(SecureNioChannel.java:540) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.SecureNioChannel.close(SecureNioChannel.java:555) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doClose(NioEndpoint.java:1208) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.SocketWrapperBase.close(SocketWrapperBase.java:425) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
For netty it is slightly different but almost same
ava.nio.channels.ClosedChannelException: null
at io.netty.handler.ssl.SslHandler.channelInactive(SslHandler.java:1065) ~[netty-handler-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:305) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:281) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelInactive(AbstractChannelHandlerContext.java:274) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelInactive(DefaultChannelPipeline.java:1405) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:301) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:281) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelInactive(DefaultChannelPipeline.java:901) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannel$AbstractUnsafe$7.run(AbstractChannel.java:813) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:174) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:167) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:566) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
2023-02-19T18:17:29.840+05:30 DEBUG 45870 --- [ctor-http-nio-3] r.netty.transport.ServerTransport : [2dbfdbf8, L:/[0:0:0:0:0:0:0:1]:8443 ! R:/[0:0:0:0:0:0:0:1]:56166] onUncaughtException(SimpleConnection{channel=[id: 0x2dbfdbf8, L:/[0:0:0:0:0:0:0:1]:8443 ! R:/[0:0:0:0:0:0:0:1]:56166]})
java.nio.channels.ClosedChannelException: null
at io.netty.handler.ssl.SslHandler.channelInactive(SslHandler.java:1065) ~[netty-handler-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:305) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:281) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelInactive(AbstractChannelHandlerContext.java:274) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelInactive(DefaultChannelPipeline.java:1405) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:301) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:281) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelInactive(DefaultChannelPipeline.java:901) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.AbstractChannel$AbstractUnsafe$7.run(AbstractChannel.java:813) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:174) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:167) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:566) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
spring boot application properties
server.ssl.enabled=true
server.port=8443
server.ssl.certificate=/path-to/cert.pem
server.ssl.certificate-private-key=/path-to/private-key.pem
steps to generate self signed cert were as below:-
openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pem
openssl req -new -x509 -key private-key.pem -out cert.pem -days 360
Example cert and key files are below:-
cert:-
-----BEGIN CERTIFICATE-----
MIIB5zCCAYwCCQDIZlhtOQc80jAKBggqhkjOPQQDAjB7MQswCQYDVQQGEwJJTjEL
MAkGA1UECAwCUEIxDzANBgNVBAcMBk1PSEFMSTENMAsGA1UECgwETmV1dzENMAsG
A1UECwwETmV1dzESMBAGA1UEAwwJbG9jYWxob3N0MRwwGgYJKoZIhvcNAQkBFg10
ZXN0QHRlc3QuY29tMB4XDTIzMDIxOTA5MjM0M1oXDTI0MDIxOTA5MjM0M1owezEL
MAkGA1UEBhMCSU4xCzAJBgNVBAgMAlBCMQ8wDQYDVQQHDAZNT0hBTEkxDTALBgNV
BAoMBE5ldXcxDTALBgNVBAsMBE5ldXcxEjAQBgNVBAMMCWxvY2FsaG9zdDEcMBoG
CSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEH
A0IABITJYdvCvlkdu+IyVISe+KqoplP1A8f26hpKecqxkS5U2VDd8JaqhsPWlC/Y
bLbBAg6DibTEWMTK8Ab5+aE/ZgIwCgYIKoZIzj0EAwIDSQAwRgIhANS1nxKGXevL
i9jkaXZiFh5IZ2DMTDwQ5ETduj7X/ZMaAiEAntNWKb+SF299sxA5Hv6i0oYK1Gl6
nh82yO5SqCvjECk=
-----END CERTIFICATE-----
key:-
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDsXD9BbsGGOnFjbMCDIXjGPI4rqDKGqlRUH4oiWxOjboAoGCCqGSM49
AwEHoUQDQgAEhMlh28K+WR274jJUhJ74qqimU/UDx/bqGkp5yrGRLlTZN3wlqqG
w9aUL9hstsECDoOJtMRYxMrwBvn5oT9mAg==
-----END EC PRIVATE KEY-----
The other strange part is - the same cert works with nodejs - expressJS
If this is a bug, some missing config or JDK support issues relating to SSL?
Comment From: mhalbritter
If you'd like us to spend some time investigating, please take the time to provide a complete minimal sample (something that we can unzip or git clone, build, and deploy) that reproduces the problem.
Comment From: krnbr
here is the reference code, let me know if I missed anything
https://github.com/krnbr/spring-https-ec-curve-issue-34232
Comment From: krnbr
Just adding some extra info @mhalbritter - RSA type cert/ key works in a single go
elliptic curve on the other side, either has some JDK config requirements or cipher stuff, or something we do not know
Comment From: mhalbritter
This looks like a bug in our PrivateKeyParser, we hardcode the secp384r1 curve in org.springframework.boot.web.server.PrivateKeyParser#EC_PARAMETERS. Your private key uses the prime256v1 curve, the parsed PrivateKey uses the secp384r1 curve.
This matches the error message curl gives me:
curl: (35) error:0A00017A:SSL routines::wrong curve
Using this works:
openssl ecparam -name secp384r1 -genkey -noout -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pem
openssl req -new -x509 -key private-key.pem -out cert.pem -days 360
Comment From: krnbr
What all curves are supported?
Comment From: mhalbritter
The secp384r1 curve works.
Comment From: krnbr
Is the PrivateKeyParser common in both spring boot 2.7.x and 3.0.x?
Comment From: krnbr
Sorry for accidentally clicking the close and comment, the button is actually kinda wrongly placed
Comment From: mhalbritter
Yes, the code for the EC parsing is the same in 2.7.x and 3.0.x, that's why i labelled it as a 2.7.x bug.
Comment From: krnbr
Sorry for disturbing you again, can you share the place where the specification for only supported curve is defined!
Comment From: mhalbritter
Sure, it's here.
The OID 1.3.132.0.34 is for the secp384r1 curve.
Comment From: wilkinsona
The JDK supports a large number of curves by default. From Java 8 using the following code:
String attribute = Security.getProviders("AlgorithmParameters.EC")[0]
.getService("AlgorithmParameters", "EC").getAttribute("SupportedCurves");
for (String curve : attribute.split("\\|")) {
System.out.println(curve.substring(1, curve.length() - 1));
}
secp112r1,1.3.132.0.6
secp112r2,1.3.132.0.7
secp128r1,1.3.132.0.28
secp128r2,1.3.132.0.29
secp160k1,1.3.132.0.9
secp160r1,1.3.132.0.8
secp160r2,1.3.132.0.30
secp192k1,1.3.132.0.31
secp192r1,NIST P-192,X9.62 prime192v1,1.2.840.10045.3.1.1
secp224k1,1.3.132.0.32
secp224r1,NIST P-224,1.3.132.0.33
secp256k1,1.3.132.0.10
secp256r1,NIST P-256,X9.62 prime256v1,1.2.840.10045.3.1.7
secp384r1,NIST P-384,1.3.132.0.34
secp521r1,NIST P-521,1.3.132.0.35
X9.62 prime192v2,1.2.840.10045.3.1.2
X9.62 prime192v3,1.2.840.10045.3.1.3
X9.62 prime239v1,1.2.840.10045.3.1.4
X9.62 prime239v2,1.2.840.10045.3.1.5
X9.62 prime239v3,1.2.840.10045.3.1.6
sect113r1,1.3.132.0.4
sect113r2,1.3.132.0.5
sect131r1,1.3.132.0.22
sect131r2,1.3.132.0.23
sect163k1,NIST K-163,1.3.132.0.1
sect163r1,1.3.132.0.2
sect163r2,NIST B-163,1.3.132.0.15
sect193r1,1.3.132.0.24
sect193r2,1.3.132.0.25
sect233k1,NIST K-233,1.3.132.0.26
sect233r1,NIST B-233,1.3.132.0.27
sect239k1,1.3.132.0.3
sect283k1,NIST K-283,1.3.132.0.16
sect283r1,NIST B-283,1.3.132.0.17
sect409k1,NIST K-409,1.3.132.0.36
sect409r1,NIST B-409,1.3.132.0.37
sect571k1,NIST K-571,1.3.132.0.38
sect571r1,NIST B-571,1.3.132.0.39
X9.62 c2tnb191v1,1.2.840.10045.3.0.5
X9.62 c2tnb191v2,1.2.840.10045.3.0.6
X9.62 c2tnb191v3,1.2.840.10045.3.0.7
X9.62 c2tnb239v1,1.2.840.10045.3.0.11
X9.62 c2tnb239v2,1.2.840.10045.3.0.12
X9.62 c2tnb239v3,1.2.840.10045.3.0.13
X9.62 c2tnb359v1,1.2.840.10045.3.0.18
X9.62 c2tnb431r1,1.2.840.10045.3.0.20
brainpoolP160r1,1.3.36.3.3.2.8.1.1.1
brainpoolP192r1,1.3.36.3.3.2.8.1.1.3
brainpoolP224r1,1.3.36.3.3.2.8.1.1.5
brainpoolP256r1,1.3.36.3.3.2.8.1.1.7
brainpoolP320r1,1.3.36.3.3.2.8.1.1.9
brainpoolP384r1,1.3.36.3.3.2.8.1.1.11
brainpoolP512r1,1.3.36.3.3.2.8.1.1.13
Comment From: scottfrederick
A private key with the header -----BEGIN EC PRIVATE KEY----- is in a format similar to PKCS#1 but with a slightly different structure (there is a good explanation of the differences here). A PKCS#8 EC private key has the header -----BEGIN PRIVATE KEY-----.
Spring Boot supports PKCS#8 EC private keys with all curves, as demonstrated in the unit tests for PrivateKeyParser. Spring Boot has limited support for PKCS#1 and the EC analog, only supporting the secp384r1 curve currently.
Supporting additional EC curves in non-PKCS#8 private keys would require Boot to do more parsing and decoding of the key content. I would argue that this would be an enhancement, not a bug, and I'm not sure it is worth doing at all given that PKCS#8 is the widely preferred format.
The steps shown in the original report for creating the private key and certificate can be modified to create a PKCS#8 key as in this example:
openssl ecparam -name prime256v1 -genkey -noout -out private-key-ec.pem
openssl pkcs8 -topk8 -in private-key-ec.pem -nocrypt -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pem
openssl req -new -x509 -key private-key.pem -out cert.pem -days 360
Comment From: wilkinsona
We discussed this today and think that this should become a documentation issue guiding people towards using PKCS8. The last remaining piece is to decide what to do with our limited support for BEGIN EC PRIVATE KEY keys. We could just keep it and document its limitations or we may want to consider deprecating it and removing it in the future. We don't think that it's worth trying to support for BEGIN EC PRIVATE KEY keys with the fully range of possible curves.
Comment From: philwebb
I had a deeper dig into this today and it's not too much additional code to extract the params from the data. Given that we we added EC PRIVATE KEY support at the request of a user (#32646) I think we should try and keep it for now if we can.
I've opened #37170 to improve our docs.
Comment From: aDramaQueen
Would you consider implementing the Bouncy Castle Library? They are running under the MIT License.
This could solve your PEM parsing problem: DOCs - PEMParser
List of all supported Elliptic Curves
Iterator<String> it = ECNamedCurveTable.getNames().asIterator();
while (it.hasNext()) {
System.out.println(it.next());
}
c2pnb272w1
c2tnb191v3
c2pnb208w1
c2tnb191v2
c2tnb191v1
prime192v3
c2tnb359v1
prime192v2
prime192v1
c2tnb239v3
c2pnb163v3
c2tnb239v2
c2pnb163v2
c2tnb239v1
c2pnb163v1
c2pnb176w1
prime256v1
c2pnb304w1
c2pnb368w1
c2tnb431r1
prime239v3
prime239v2
prime239v1
sect283r1
sect283k1
sect163r2
secp256k1
secp160k1
secp160r1
secp112r2
secp112r1
sect113r2
sect113r1
sect239k1
secp128r2
sect163r1
secp128r1
sect233r1
sect163k1
sect233k1
sect193r2
sect193r1
sect131r2
sect131r1
secp256r1
sect571r1
sect571k1
secp192r1
sect409r1
sect409k1
secp521r1
secp384r1
secp224r1
secp224k1
secp192k1
secp160r2
B-163
P-521
P-256
K-163
B-233
P-224
P-384
K-233
B-409
B-283
B-571
K-409
K-283
P-192
K-571
brainpoolP224t1
brainpoolP512t1
brainpoolP224r1
brainpoolP512r1
brainpoolP192t1
brainpoolP384t1
brainpoolP192r1
brainpoolP384r1
brainpoolP160t1
brainpoolP320t1
brainpoolP160r1
brainpoolP320r1
brainpoolP256t1
brainpoolP256r1
FRP256v1
Tc26-Gost-3410-12-256-paramSetA
Tc26-Gost-3410-12-512-paramSetC
GostR3410-2001-CryptoPro-C
Tc26-Gost-3410-12-512-paramSetB
GostR3410-2001-CryptoPro-B
Tc26-Gost-3410-12-512-paramSetA
GostR3410-2001-CryptoPro-A
GostR3410-2001-CryptoPro-XchB
GostR3410-2001-CryptoPro-XchA
wapip192v1
sm2p256v1
Comment From: wilkinsona
@aDramaQueen With the changes made in https://github.com/spring-projects/spring-boot/commit/fd8cb74b46f17c0d2e8ca0f302561d9a5378172b, we don't believe there's much to be gained by relying on Bouncy Castle as you should now be able to use any curve that's supported by the JDK. While that support isn't as broad as Bouncy Castle's we haven't seen demand for other curves that would justify pulling in a new dependency.
Comment From: aDramaQueen
I really like your Documentation, therefore this last suggestion for this topic: You could add the list of supported curves to your documentation. This would save some time for any developer. Maybe even better directly link to Java DOCs - Supported Elliptic Curve Names. With this approach, you could completely offload the problem to Java and not have to worry about anything. But be careful here though, since I don't know if OpenJDK uses the same implementation than Oracle does. The link is Oracles Documentation. OpenJDK sadly has nothing in comparison. I just found this Overview.