From 897fbc893a7495e2521f9374bfe217d04bc5ad7b Mon Sep 17 00:00:00 2001 From: Felix Dittrich <31076102+f11h@users.noreply.github.com> Date: Fri, 22 Oct 2021 14:47:22 +0200 Subject: [PATCH] Feat: Allow-List for Certificate Hashes (#108) * Add Allow-List-Check for mTLS Authentication * Add Allow-List-Check for mTLS Authentication * Change Allow-List to Comma seperated --- ...tyConfig.java => LocalSecurityConfig.java} | 27 +--- .../testresult/config/MtlsSecurityConfig.java | 115 ++++++++++++++++++ .../testresult/config/TestResultConfig.java | 2 + src/main/resources/application-cloud.yml | 2 + src/main/resources/application.yml | 1 + 5 files changed, 126 insertions(+), 21 deletions(-) rename src/main/java/app/coronawarn/testresult/config/{SecurityConfig.java => LocalSecurityConfig.java} (61%) create mode 100644 src/main/java/app/coronawarn/testresult/config/MtlsSecurityConfig.java diff --git a/src/main/java/app/coronawarn/testresult/config/SecurityConfig.java b/src/main/java/app/coronawarn/testresult/config/LocalSecurityConfig.java similarity index 61% rename from src/main/java/app/coronawarn/testresult/config/SecurityConfig.java rename to src/main/java/app/coronawarn/testresult/config/LocalSecurityConfig.java index 357864e..0013234 100644 --- a/src/main/java/app/coronawarn/testresult/config/SecurityConfig.java +++ b/src/main/java/app/coronawarn/testresult/config/LocalSecurityConfig.java @@ -21,35 +21,20 @@ package app.coronawarn.testresult.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.WebSecurityConfigurerAdapter; -import org.springframework.security.web.firewall.HttpFirewall; -import org.springframework.security.web.firewall.StrictHttpFirewall; @Configuration -public class SecurityConfig extends WebSecurityConfigurerAdapter { - - @Bean - protected HttpFirewall strictFirewall() { - StrictHttpFirewall firewall = new StrictHttpFirewall(); - firewall.setAllowedHttpMethods(Arrays.asList( - HttpMethod.GET.name(), - HttpMethod.POST.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() - .mvcMatchers("/actuator/**").permitAll() - .anyRequest().denyAll() + http + .authorizeRequests() + .anyRequest().permitAll() .and().csrf().disable(); } - } diff --git a/src/main/java/app/coronawarn/testresult/config/MtlsSecurityConfig.java b/src/main/java/app/coronawarn/testresult/config/MtlsSecurityConfig.java new file mode 100644 index 0000000..dcfbc9c --- /dev/null +++ b/src/main/java/app/coronawarn/testresult/config/MtlsSecurityConfig.java @@ -0,0 +1,115 @@ +/* + * Corona-Warn-App / cwa-testresult-server + * + * (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.testresult.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.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 TestResultConfig testResultConfig; + + @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() + .x509().x509PrincipalExtractor(new ThumbprintX509PrincipalExtractor()).userDetailsService(userDetailsService()) + .and().authorizeRequests() + .mvcMatchers("/actuator/**").permitAll() + .anyRequest().denyAll() + .and().csrf().disable(); + } + + @Override + public UserDetailsService userDetailsService() { + return hash -> { + + boolean allowed = Stream.of(testResultConfig.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/testresult/config/TestResultConfig.java b/src/main/java/app/coronawarn/testresult/config/TestResultConfig.java index 89df120..918359e 100644 --- a/src/main/java/app/coronawarn/testresult/config/TestResultConfig.java +++ b/src/main/java/app/coronawarn/testresult/config/TestResultConfig.java @@ -11,6 +11,8 @@ @ConfigurationProperties(prefix = "testresult") public class TestResultConfig { + private String allowedClientCertificates; + private Cleanup cleanup; @Getter diff --git a/src/main/resources/application-cloud.yml b/src/main/resources/application-cloud.yml index ad78041..9b80327 100644 --- a/src/main/resources/application-cloud.yml +++ b/src/main/resources/application-cloud.yml @@ -11,3 +11,5 @@ server: enabled: true key-store-password: ${SSL_TESTRESULT_KEYSTORE_PASSWORD} trust-store-password: ${SSL_TESTRESULT_TRUSTSTORE_PASSWORD} +testresult: + allowed-client-certificates: ${TESTRESULT_ALLOWEDCLIENTCERTIFICATES} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 65d5b79..3478596 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -52,3 +52,4 @@ testresult: delete: days: 60 rate: 3600000 + allowed-client-certificates: