Tumgik
#how could I not draw THE SOUP REQUEST FROM SOMEONE WITH SOUP IN THEIR USERNAME???? HOW COULD I NOT???
iyarpage · 6 years
Text
X.509 client certificates with Spring Security
A disclaimer: this blogpost is a story about the reasons why I ended up securing my API using the X.509 client certificate, in addition to a step-by-step guide on how to implement this yourself. Someone will hopefully find it useful.
Securing your application or an API is always a challenge, and lack of experience with the topic makes it even more complicated. Deciding on what security approach to take, how to properly implement it, what vectors of attacks you’ll be vulnerable to, dealing with the soup of acronyms and nouns such as SSL/TLS, CA, CRT, public/private keys, keystore, truststore – you quickly find yourself with a panicky feeling in your stomach. And this is a pretty common reaction.
First of all, X.509 is a digital certificate which uses the X.509 public key infrastructure standard to verify that a public key, which belongs to a user, service or a server, is contained within the certificate, as well as the identity of said user, service, or server. The certificate can be signed by a trusted certificate authority, or self-signed. SSL and TLS are most widely known protocols which use the X.509 format. They are routinely used to verify the identity of servers each time you open your browser and visit a webpage via HTTPS.
The goal in mind is to secure communication from a known server to my service. The decision ultimately came down to use the client certificate approach since authenticating users is not my concern – users do not interact with me directly. This means that there are no username/passwords being sent back and forth, no cookies and no sessions – which means that we maintain statelessness of our REST API. Also, as I am the certificate authority, I’m always going to stay in control of who gets a valid certificate, meaning I only trust myself to manage and maintain who can talk to my service.
The general workflow
In order to secure and authenticate communication between client and the server, they both need to have valid certificates. When you send a browser request to an HTTPS website, your browser would just verify that the site is certified by a trusted authority. In this case, not only the server’s identity is verified, but also the server gets to verify the client.
Credit to Daniel Marks for his wonderful drawing skills.jpg
The first thing the client has to do in order to communicate with the secured service is to generate a private key and a certificate signing request (CSR). This CSR is then sent to a Certificate Authority (CA) to be signed. In my use case, I represent both the server and the CA, since I want to be in charge of managing who gets to talk to my service. Signing the CSR produces the client certificate which is then sent back to the client. In order to send a valid and authenticated HTTPS request, the client also needs to provide the signed certificate (unlocked with the client’s private key), which is then validated during the SSL handshake with the trusted CA certificate in the Java truststore on the server side.
Enough theory, let’s see what the implementation looks like.
Spring Security Configuration
My REST service is a regular spring-boot 2.0.2 app using the spring-boot-starter-security dependency:
org.springframework.boot spring-boot-starter-security
The configuration class:
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { /* * Enables x509 client authentication. */ @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http .authorizeRequests() .anyRequest() .authenticated() .and() .x509() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.NEVER) .and() .csrf() .disable(); // @formatter:on } /* * Create an in-memory authentication manager. We create 1 user (localhost which * is the CN of the client certificate) which has a role of USER. */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("localhost").password("none").roles("USER"); } }
Usually known to be cumbersome, in this case the SpringSecurityConfig class is pretty lightweight, since we want to authenticate all requests coming into the service, and we want to do so using x509 authentication. SessionCreationPolicy.NEVER tells Spring to not bother creating sessions since all requests must have a certificate. We can also disable cross-site request forgery protection since we aren’t using HTML forms, but only send REST calls back and forth. You must do so if you’re going to follow this blog to the end, because CURL requests won’t pass through Spring’s csrf filter.
Enabling HTTPS on the REST service itself is just a manner of setting a couple of properties in our application.properties file:
server.port=8443 server.ssl.key-store=classpath:keystore.p12 server.ssl.key-store-password=changeit server.ssl.trust-store=classpath:truststore.jks server.ssl.trust-store-password=changeit server.ssl.client-auth=need
And this is pretty much it, you can go on and create your @RestControllers with endpoints fully secured behind a x509 certificate.
Generating a server CA certificate
Let’s see what has to be done on the server’s side with regards to creating the certificate:
openssl genrsa -aes256 -out serverprivate.key 2048
First of all, we have to generate an rsa key encrypted by aes256 encryption which is 2048 bits long. 4096 length would be more secure, but the handshake would be slowed down quite significantly. 1024 is also an option for faster handshakes but is obviously less secure. Used server as pass phrase here.
openssl req -x509 -new -nodes -key serverprivate.key -sha256 -days 1024 -out serverCA.crt
Now, we use the generated key in order to create a x509 certificate and sign it with our key. A form must be filled out which will map the certificate to an identity. Most of the fields can be filled out subjectively, except the CN (common name) which must match the domain we are securing (in this case, it’s localhost).
keytool -import -file serverCA.crt -alias serverCA -keystore truststore.jks
imports our server CA certificate to our Java truststore. The stored password in this case is changeit.
openssl pkcs12 -export -in serverCA.crt -inkey serverprivate.key -certfile serverCA.crt -out keystore.p12
exports the server CA certificate to our keystore. The stored password is again changeit.
Note: you could use .jks as the format of the keystore instead of .p12, you can easily convert it with:
keytool -importkeystore -srckeystore keystore.p12 -srcstoretype pkcs12 -destkeystore keystore.jks -deststoretype JKS
Generating a client certificate
The client has to go through a similar process:
openssl genrsa -aes256 clientprivate.key 2048
Again, the first thing we have to do is to create the private key. Interactively asks for a passphrase, I’m using client here.
openssl req -new -key clientprivate.key -out client.csr
Now we create the certificate signing request and sign it with the client’s private key. We are asked to fill a form to map the identity to the output certificate. Similar to the step 2 when generating the Server CA, the CN is the most important field and must match the domain.
Client sends the CSR to the CA
openssl x509 -req -in client.csr -CA serverCA.crt -CAkey serverprivate.key -CAcreateserial -out client.crt -days 365 -sha256
CA does this step, not the client. We sign the certificate signing request using the server’s private key and the CA.crt. client.crt is produced, and it has to be securely sent back to the client.
Certificates in action
Now that we have everything configured and signed, it’s time to see if it all ties in properly. First thing, we can send a request without the certificate:
curl -ik "https://localhost:8443/foo/"
and this will produce an error, just as we would have hoped:
curl: (35) error:14094412:SSL routines:SSL3_READ_BYTES:sslv3 alert bad certificate
This time we create a request with the certificate (using the client’s private key):
curl -ik --cert client.crt --key clientprivate.key "https://localhost:8443/foo/"
at this point we are asked for the key’s passphrase, type in client produces a nice “200 OK” response!
HTTP/1.1 200 X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 Strict-Transport-Security: max-age=31536000 ; includeSubDomains X-Frame-Options: DENY Content-Type: text/plain;charset=UTF-8 Content-Length: 12 Date: Fri, 10 Aug 2018 11:39:51 GMT hello there!%
Example POST request:
curl -ik --cert client.crt --key clientprivate.key -X POST -d '{"greeting": "Hello there"}' "https://localhost:8443/foo/"
type in client as before
HTTP/1.1 201 X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 Strict-Transport-Security: max-age=31536000 ; includeSubDomains X-Frame-Options: DENY Content-Type: text/plain;charset=UTF-8 Content-Length: 15 Date: Fri, 10 Aug 2018 12:02:33 GMT Hello there GENERAL KENOBI!%
You can set
logging.level.org.springframework.security=DEBUG
in your application.properties to trace the handshake.
2018-08-16 16:24:40.190 DEBUG 7206 --- [nio-8443-exec-3] o.s.s.w.a.p.x.X509AuthenticationFilter : X.509 client authentication certificate:[ [ Version: V1 Subject: [email protected], CN=localhost, O=DS, L=Berlin, ST=Who even knows at this point, C=DE Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11 Key: Sun RSA public key, 2048 bits modulus: 2378026949349077149739661818238276092512323423424567832352996635790995122159840933949972327793790189970024798612439632633724982673364484809428691398923428004247310754863945150807792627712558813908791623601497931450739871341026099867732456702955088658091162530456218851145877831865961036637685012583440079032243774378463018497851565983485066259457033740417226709148321675286715367166340015131812147321619943539868370944770507019591372067335310435075401719933452132656596915712312312312347438076525959407549710102054016537474852860499356560314974040838659325953995234234078263724509076739574167 public exponent: 65537 Validity: [From: Fri Aug 10 13:35:10 CEST 2018, To: Sat Aug 10 13:35:10 CEST 2019] Issuer: [email protected], CN=localhost, OU=Banja Luka office, O=cc, L=Solingen, ST=Whatever, C=DE SerialNumber: [ aecc9b1c 2b56df2d] ] Algorithm: [SHA256withRSA] Signature: 0000: 69 97 0A EF 5C F8 64 58 50 C8 A4 A5 33 86 0B 6A i...\.dXP...3..j 0010: 64 24 D9 90 BF CF FB EC 7B AC E9 3C 23 88 81 7E d$.........
We can see that the received certificate is signed by our own trusted serverCA.crt (Issuer: EMAILADDRESS being [email protected] - the email was set in the second step when generating the serverCA.crt, and the Subject: EMAILADDRESS is [email protected], the value that was set when the client was generating the CSR).
The security principal:
o.s.s.w.a.p.x.X509AuthenticationFilter : Authentication success: org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken@c7017942: Principal: org.springframework.security.core.userdetails.User@b8332793: Username: localhost; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_USER
And that would be it!
Special thanks to Jonas Hecht, whose example helped me quite a bit to grasp the workflow of this topic (you can find it here: https://github.com/jonashackt/spring-boot-rest-clientcertificate) and to Daniel Marks, for helping me fill out the missing pieces of the puzzle.
Der Beitrag X.509 client certificates with Spring Security erschien zuerst auf codecentric AG Blog.
X.509 client certificates with Spring Security published first on https://medium.com/@koresol
0 notes