diff --git a/src/main/java/app/coronawarn/verification/config/SecurityConfig.java b/src/main/java/app/coronawarn/verification/config/LocalSecurityConfig.java similarity index 60% rename from src/main/java/app/coronawarn/verification/config/SecurityConfig.java rename to src/main/java/app/coronawarn/verification/config/LocalSecurityConfig.java index 08afbe41..f79a4b32 100644 --- a/src/main/java/app/coronawarn/verification/config/SecurityConfig.java +++ b/src/main/java/app/coronawarn/verification/config/LocalSecurityConfig.java @@ -21,42 +21,22 @@ package app.coronawarn.verification.config; -import java.util.Arrays; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.web.firewall.HttpFirewall; -import org.springframework.security.web.firewall.StrictHttpFirewall; @EnableWebSecurity @Configuration -public class SecurityConfig extends WebSecurityConfigurerAdapter { - - @Bean - protected HttpFirewall strictFirewall() { - StrictHttpFirewall firewall = new StrictHttpFirewall(); - firewall.setAllowedHttpMethods(Arrays.asList( - HttpMethod.GET.name(), - HttpMethod.POST.name(), - HttpMethod.HEAD.name() - )); - return firewall; - } - +@ConditionalOnProperty(name = "server.ssl.client-auth", havingValue = "none", matchIfMissing = true) +public class LocalSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() - .mvcMatchers("/api/**").permitAll().and().requiresChannel().mvcMatchers("/api/**").requiresSecure().and() - .authorizeRequests() - .mvcMatchers("/version/**").permitAll() - .mvcMatchers("/actuator/**").permitAll() - .anyRequest().denyAll() + .anyRequest().permitAll() .and().csrf().disable(); } - } diff --git a/src/main/java/app/coronawarn/verification/config/MtlsSecurityConfig.java b/src/main/java/app/coronawarn/verification/config/MtlsSecurityConfig.java new file mode 100644 index 00000000..16762621 --- /dev/null +++ b/src/main/java/app/coronawarn/verification/config/MtlsSecurityConfig.java @@ -0,0 +1,118 @@ +/* + * Corona-Warn-App / cwa-verification + * + * (C) 2020, T-Systems International GmbH + * + * Deutsche Telekom AG and all other contributors / + * copyright owners license this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package app.coronawarn.verification.config; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.codec.Hex; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.StrictHttpFirewall; +import org.springframework.web.server.ResponseStatusException; + +@Configuration +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(name = "server.ssl.client-auth", havingValue = "need") +public class MtlsSecurityConfig extends WebSecurityConfigurerAdapter { + + private final VerificationApplicationConfig config; + + @Bean + protected HttpFirewall strictFirewall() { + StrictHttpFirewall firewall = new StrictHttpFirewall(); + firewall.setAllowedHttpMethods(Arrays.asList( + HttpMethod.GET.name(), + HttpMethod.POST.name() + )); + return firewall; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .mvcMatchers("/api/**").authenticated().and() + .requiresChannel().mvcMatchers("/api/**").requiresSecure().and() + .x509().x509PrincipalExtractor(new ThumbprintX509PrincipalExtractor()).userDetailsService(userDetailsService()) + .and().authorizeRequests() + .mvcMatchers("/version/**").permitAll() + .mvcMatchers("/actuator/**").permitAll() + .anyRequest().denyAll() + .and().csrf().disable(); + } + + @Override + public UserDetailsService userDetailsService() { + return hash -> { + + boolean allowed = Stream.of(config.getAllowedClientCertificates() + .split(",")) + .map(String::trim) + .anyMatch(entry -> entry.equalsIgnoreCase(hash)); + + if (allowed) { + return new User(hash, "", Collections.emptyList()); + } else { + log.error("Failed to authenticate cert with hash {}", hash); + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + }; + } + + private static class ThumbprintX509PrincipalExtractor implements X509PrincipalExtractor { + + MessageDigest messageDigest; + + private ThumbprintX509PrincipalExtractor() throws NoSuchAlgorithmException { + messageDigest = MessageDigest.getInstance("SHA-256"); + } + + @Override + public Object extractPrincipal(X509Certificate x509Certificate) { + try { + return String.valueOf(Hex.encode(messageDigest.digest(x509Certificate.getEncoded()))); + } catch (CertificateEncodingException e) { + log.error("Failed to extract bytes from certificate"); + return null; + } + } + } +} + diff --git a/src/main/java/app/coronawarn/verification/config/VerificationApplicationConfig.java b/src/main/java/app/coronawarn/verification/config/VerificationApplicationConfig.java index b5637620..a76caede 100644 --- a/src/main/java/app/coronawarn/verification/config/VerificationApplicationConfig.java +++ b/src/main/java/app/coronawarn/verification/config/VerificationApplicationConfig.java @@ -36,6 +36,7 @@ public class VerificationApplicationConfig { private Long initialFakeDelayMilliseconds; private Long fakeDelayMovingAverageSamples; + private String allowedClientCertificates; private Tan tan = new Tan(); private AppSession appsession = new AppSession(); diff --git a/src/main/resources/application-cloud.yml b/src/main/resources/application-cloud.yml index 69cb2a7d..dfcf284e 100644 --- a/src/main/resources/application-cloud.yml +++ b/src/main/resources/application-cloud.yml @@ -24,3 +24,4 @@ cwa-testresult-server: trust-store: ${SSL_VERIFICATION_TRUSTSTORE_PATH} trust-store-password: ${SSL_VERIFICATION_TRUSTSTORE_PASSWORD} disable-dob-hash-check-for-external-test-result: ${DISABLE_DOB_EXTERNAL_TR} +allowed-client-certificates: ${VERIFICATION_ALLOWEDCLIENTCERTIFICATES} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 80f11814..e2fbe2d5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -83,4 +83,4 @@ request: cwa-testresult-server: url: http://localhost:8088 - \ No newline at end of file +allowed-client-certificates: