From 1c25fe26c9d0241f14f28705ec585aa5c9791ae1 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 4 Feb 2019 11:49:21 -0700 Subject: [PATCH] Introduce Support for Reading RSA Keys Fixes: gh-6494 --- .../WebSecurityConfiguration.java | 10 +- .../WebFluxSecurityConfiguration.java | 9 +- .../RsaKeyConversionServicePostProcessor.java | 154 +++++++++++++++ ...eyConversionServicePostProcessorTests.java | 184 ++++++++++++++++++ .../server/OAuth2ResourceServerSpecTests.java | 34 ++++ .../annotation/web/configuration/simple.priv | 28 +++ .../annotation/web/configuration/simple.pub | 7 + .../OAuth2ResourceServerSpecTests-simple.pub | 3 + .../security/converter/RsaKeyConverters.java | 134 +++++++++++++ .../converter/RsaKeyConvertersTest.java | 123 ++++++++++++ 10 files changed, 683 insertions(+), 3 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java create mode 100644 config/src/test/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessorTests.java create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configuration/simple.priv create mode 100644 config/src/test/resources/org/springframework/security/config/annotation/web/configuration/simple.pub create mode 100644 config/src/test/resources/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests-simple.pub create mode 100644 core/src/main/java/org/springframework/security/converter/RsaKeyConverters.java create mode 100644 core/src/test/java/org/springframework/security/converter/RsaKeyConvertersTest.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index 0d2bf24e00..23bb6a6f27 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,12 @@ package org.springframework.security.config.annotation.web.configuration; import java.util.Collections; import java.util.List; import java.util.Map; - import javax.servlet.Filter; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -40,6 +40,7 @@ import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor; import org.springframework.security.context.DelegatingApplicationListener; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterInvocation; @@ -159,6 +160,11 @@ public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAwa this.webSecurityConfigurers = webSecurityConfigurers; } + @Bean + public static BeanFactoryPostProcessor conversionServicePostProcessor() { + return new RsaKeyConversionServicePostProcessor(); + } + @Bean public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents( ConfigurableListableBeanFactory beanFactory) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java index 029ca84dc5..26192f785b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,12 @@ import java.util.Arrays; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor; import org.springframework.security.web.server.SecurityWebFilterChain; @@ -66,6 +68,11 @@ class WebFluxSecurityConfiguration { return new CsrfRequestDataValueProcessor(); } + @Bean + public static BeanFactoryPostProcessor conversionServicePostProcessor() { + return new RsaKeyConversionServicePostProcessor(); + } + private List getSecurityWebFilterChains() { List result = this.securityWebFilterChains; if (ObjectUtils.isEmpty(result)) { diff --git a/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java b/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java new file mode 100644 index 0000000000..c28d8db346 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.security.config.crypto; + +import java.beans.PropertyEditorSupport; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.security.converter.RsaKeyConverters; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Adds {@link RsaKeyConverters} to the configured {@link ConversionService} or {@link PropertyEditor}s + * + * @author Josh Cummings + * @since 5.2 + */ +public class RsaKeyConversionServicePostProcessor implements BeanFactoryPostProcessor { + private static final String CONVERSION_SERVICE_BEAN_NAME = "conversionService"; + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + public void setResourceLoader(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "resourceLoader cannot be null"); + this.resourceLoader = resourceLoader; + } + + /** + * {@inheritDoc} + */ + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + if (hasUserDefinedConversionService(beanFactory)) { + return; + } + + Converter pkcs8 = pkcs8(); + Converter x509 = x509(); + + ConversionService service = beanFactory.getConversionService(); + if (service instanceof ConverterRegistry) { + ConverterRegistry registry = (ConverterRegistry) service; + registry.addConverter(String.class, RSAPrivateKey.class, pkcs8); + registry.addConverter(String.class, RSAPublicKey.class, x509); + } else { + beanFactory.addPropertyEditorRegistrar(registry -> { + registry.registerCustomEditor(RSAPublicKey.class, new ConverterPropertyEditorAdapter<>(x509)); + registry.registerCustomEditor(RSAPrivateKey.class, new ConverterPropertyEditorAdapter<>(pkcs8)); + }); + } + } + + private boolean hasUserDefinedConversionService(ConfigurableListableBeanFactory beanFactory) { + return beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) && + beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class); + } + + private Converter pkcs8() { + Converter pemInputStreamConverter = pemInputStreamConverter(); + Converter pkcs8KeyConverter = autoclose(RsaKeyConverters.pkcs8()); + return pair(pemInputStreamConverter, pkcs8KeyConverter); + } + + private Converter x509() { + Converter pemInputStreamConverter = pemInputStreamConverter(); + Converter x509KeyConverter = autoclose(RsaKeyConverters.x509()); + return pair(pemInputStreamConverter, x509KeyConverter); + } + + private Converter pemInputStreamConverter() { + return source -> source.startsWith("-----") ? + toInputStream(source) : toInputStream(this.resourceLoader.getResource(source)); + } + + private InputStream toInputStream(String raw) { + return new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream toInputStream(Resource resource) { + try { + return resource.getInputStream(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private Converter autoclose(Converter inputStreamKeyConverter) { + return inputStream -> { + try (InputStream is = inputStream) { + return inputStreamKeyConverter.convert(is); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + } + + private Converter pair(Converter one, Converter two) { + return source -> { + I intermediary = one.convert(source); + return two.convert(intermediary); + }; + } + + private static class ConverterPropertyEditorAdapter extends PropertyEditorSupport { + private final Converter converter; + + public ConverterPropertyEditorAdapter(Converter converter) { + this.converter = converter; + } + + @Override + public String getAsText() { + return null; + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + setValue(this.converter.convert(text)); + } else { + setValue(null); + } + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessorTests.java b/config/src/test/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessorTests.java new file mode 100644 index 0000000000..e4bb97ca8e --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessorTests.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.security.config.crypto; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestRule; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link RsaKeyConversionServicePostProcessor} + */ +public class RsaKeyConversionServicePostProcessorTests { + + private static final String PKCS8_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCMk7CKSTfu3QoV\n" + + "HoPVXxwZO+qweztd36cVWYqGOZinrOR2crWFu50AgR2CsdIH0+cqo7F4Vx7/3O8i\n" + + "RpYYZPe2VoO5sumzJt8P6fS80/TAKjhJDAqgZKRJTgGN8KxCM6p/aJli1ZeDBqiV\n" + + "v7vJJe+ZgJuPGRS+HMNa/wPxEkqqXsglcJcQV1ZEtfKXSHB7jizKpRL38185SyAC\n" + + "pwyjvBu6Cmm1URfhQo88mf239ONh4dZ2HoDfzN1q6Ssu4F4hgutxr9B0DVLDP5u+\n" + + "WFrm3nsJ76zf99uJ+ntMUHJ+bY+gOjSlVWIVBIZeAaEGKCNWRk/knjvjbijpvm3U\n" + + "acGlgdL3AgMBAAECggEACxxxS7zVyu91qI2s5eSKmAQAXMqgup6+2hUluc47nqUv\n" + + "uZz/c/6MPkn2Ryo+65d4IgqmMFjSfm68B/2ER5FTcvoLl1Xo2twrrVpUmcg3BClS\n" + + "IZPuExdhVNnxjYKEWwcyZrehyAoR261fDdcFxLRW588efIUC+rPTTRHzAc7sT+Ln\n" + + "t/uFeYNWJm3LaegOLoOmlMAhJ5puAWSN1F0FxtRf/RVgzbLA9QC975SKHJsfWCSr\n" + + "IZyPsdeaqomKaF65l8nfqlE0Ua2L35gIOGKjUwb7uUE8nI362RWMtYdoi3zDDyoY\n" + + "hSFbgjylCHDM0u6iSh6KfqOHtkYyJ8tUYgVWl787wQKBgQDYO3wL7xuDdD101Lyl\n" + + "AnaDdFB9fxp83FG1cWr+t7LYm9YxGfEUsKHAJXN6TIayDkOOoVwIl+Gz0T3Z06Bm\n" + + "eBGLrB9mrVA7+C7NJwu5gTMlzP6HxUR9zKJIQ/VB1NUGM77LSmvOFbHc9Q0+z8EH\n" + + "X5WO516a3Z7lNtZJcCoPOtu2rwKBgQCmbj41Fh+SSEUApCEKms5ETRpe7LXQlJgx\n" + + "yW7zcJNNuIb1C3vBLPxjiOTMgYKOeMg5rtHTGLT43URHLh9ArjawasjSAr4AM3J4\n" + + "xpoi/sKGDdiKOsuDWIGfzdYL8qyTHSdpZLQsCTMRiRYgAHZFPgNa7SLZRfZicGlr\n" + + "GHN1rJW6OQKBgEjiM/upyrJSWeypUDSmUeAZMpA6aWkwsfHgmtnkfUn5rQa74cDB\n" + + "kKO9e+D7LmOR3z+SL/1NhGwh2SE07dncGr3jdGodfO/ZxZyszozmeaECKcEFwwJM\n" + + "GV8WWPKplGwUwPiwywmZ0mvRxXcoe73KgBS88+xrSwWjqDL0tZiQlEJNAoGATkei\n" + + "GMQMG3jEg9Wu+NbxV6zQT3+U0MNjhl9RQU1c63x0dcNt9OFc4NAdlZcAulRTENaK\n" + + "OHjxffBM0hH+fySx8m53gFfr2BpaqDX5f6ZGBlly1SlsWZ4CchCVsc71nshipi7I\n" + + "k8HL9F5/OpQdDNprJ5RMBNfkWE65Nrcsb1e6oPkCgYAxwgdiSOtNg8PjDVDmAhwT\n" + + "Mxj0Dtwi2fAqQ76RVrrXpNp3uCOIAu4CfruIb5llcJ3uak0ZbnWri32AxSgk80y3\n" + + "EWiRX/WEDu5znejF+5O3pI02atWWcnxifEKGGlxwkcMbQdA67MlrJLFaSnnGpNXo\n" + + "yPfcul058SOqhafIZQMEKQ==\n" + + "-----END PRIVATE KEY-----"; + private static final String X509_PUBLIC_KEY_LOCATION = + "classpath:org/springframework/security/config/annotation/web/configuration/simple.pub"; + + private final RsaKeyConversionServicePostProcessor postProcessor = + new RsaKeyConversionServicePostProcessor(); + private ConversionService service; + + @Value("classpath:org/springframework/security/config/annotation/web/configuration/simple.pub") + RSAPublicKey publicKey; + + @Value("classpath:org/springframework/security/config/annotation/web/configuration/simple.priv") + RSAPrivateKey privateKey; + + @Value("custom:simple.pub") + RSAPublicKey samePublicKey; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Before + public void setUp() { + ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setConversionService(new GenericConversionService()); + this.postProcessor.postProcessBeanFactory(beanFactory); + this.service = beanFactory.getConversionService(); + } + + @Test + public void convertWhenUsingConversionServiceForRawKeyThenOk() { + RSAPrivateKey key = this.service.convert(PKCS8_PRIVATE_KEY, RSAPrivateKey.class); + assertThat(key.getModulus().bitLength()).isEqualTo(2048); + } + + @Test + public void convertWhenUsingConversionServiceForClasspathThenOk() { + RSAPublicKey key = this.service.convert(X509_PUBLIC_KEY_LOCATION, RSAPublicKey.class); + assertThat(key.getModulus().bitLength()).isEqualTo(1024); + } + + @Test + public void valueWhenReferringToClasspathPublicKeyThenConverts() { + this.spring.register(CustomResourceLoaderConfig.class, DefaultConfig.class).autowire(); + assertThat(this.publicKey.getModulus().bitLength()).isEqualTo(1024); + } + + @Test + public void valueWhenReferringToClasspathPrivateKeyThenConverts() { + this.spring.register(CustomResourceLoaderConfig.class, DefaultConfig.class).autowire(); + assertThat(this.privateKey.getModulus().bitLength()).isEqualTo(2048); + } + + @Test + public void valueWhenReferringToCustomResourceLoadedPublicKeyThenConverts() { + this.spring.register(CustomResourceLoaderConfig.class, DefaultConfig.class).autowire(); + assertThat(this.samePublicKey.getModulus().bitLength()).isEqualTo(1024); + } + + @Test + public void valueWhenOverridingConversionServiceThenUsed() { + assertThatCode(() -> + this.spring.register(OverrideConversionServiceConfig.class, DefaultConfig.class).autowire()) + .hasRootCauseInstanceOf(IllegalArgumentException.class); + } + + @EnableWebSecurity + static class DefaultConfig { } + + @Configuration + static class CustomResourceLoaderConfig { + @Bean + BeanFactoryPostProcessor conversionServiceCustomizer() { + return beanFactory -> beanFactory.getBean(RsaKeyConversionServicePostProcessor.class) + .setResourceLoader(new CustomResourceLoader()); + } + } + + @Configuration + static class OverrideConversionServiceConfig { + @Bean + ConversionService conversionService() { + GenericConversionService service = new GenericConversionService(); + service.addConverter(String.class, RSAPublicKey.class, source -> { + throw new IllegalArgumentException("unsupported"); + }); + return service; + } + } + + private static class CustomResourceLoader implements ResourceLoader { + private final ResourceLoader delegate = new DefaultResourceLoader(); + + @Override + public Resource getResource(String location) { + if (location.startsWith("classpath:")) { + return this.delegate.getResource(location); + } else if (location.startsWith("custom:")) { + String[] parts = location.split(":"); + return this.delegate.getResource( + "classpath:org/springframework/security/config/annotation/web/configuration/" + parts[1]); + } + throw new IllegalArgumentException("unsupported resource"); + } + + @Override + public ClassLoader getClassLoader() { + return this.delegate.getClassLoader(); + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java index a397425ffa..5edd88a005 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java @@ -45,6 +45,7 @@ import reactor.core.publisher.Mono; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.core.convert.converter.Converter; @@ -174,6 +175,16 @@ public class OAuth2ResourceServerSpecTests { .expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"invalid_token\"")); } + @Test + public void getWhenValidUsingPlaceholderThenReturnsOk() { + this.spring.register(PlaceholderConfig.class, RootController.class).autowire(); + + this.client.get() + .headers(headers -> headers.setBearerAuth(this.messageReadToken)) + .exchange() + .expectStatus().isOk(); + } + @Test public void getWhenCustomDecoderThenAuthenticatesAccordingly() { this.spring.register(CustomDecoderConfig.class, RootController.class).autowire(); @@ -383,6 +394,29 @@ public class OAuth2ResourceServerSpecTests { } } + @EnableWebFlux + @EnableWebFluxSecurity + static class PlaceholderConfig { + @Value("${classpath:org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests-simple.pub}") + RSAPublicKey key; + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeExchange() + .anyExchange().hasAuthority("SCOPE_message:read") + .and() + .oauth2ResourceServer() + .jwt() + .publicKey(this.key); + // @formatter:on + + + return http.build(); + } + } + @EnableWebFlux @EnableWebFluxSecurity static class JwkSetUriConfig { diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configuration/simple.priv b/config/src/test/resources/org/springframework/security/config/annotation/web/configuration/simple.priv new file mode 100644 index 0000000000..7177ea578d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configuration/simple.priv @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCMk7CKSTfu3QoV +HoPVXxwZO+qweztd36cVWYqGOZinrOR2crWFu50AgR2CsdIH0+cqo7F4Vx7/3O8i +RpYYZPe2VoO5sumzJt8P6fS80/TAKjhJDAqgZKRJTgGN8KxCM6p/aJli1ZeDBqiV +v7vJJe+ZgJuPGRS+HMNa/wPxEkqqXsglcJcQV1ZEtfKXSHB7jizKpRL38185SyAC +pwyjvBu6Cmm1URfhQo88mf239ONh4dZ2HoDfzN1q6Ssu4F4hgutxr9B0DVLDP5u+ +WFrm3nsJ76zf99uJ+ntMUHJ+bY+gOjSlVWIVBIZeAaEGKCNWRk/knjvjbijpvm3U +acGlgdL3AgMBAAECggEACxxxS7zVyu91qI2s5eSKmAQAXMqgup6+2hUluc47nqUv +uZz/c/6MPkn2Ryo+65d4IgqmMFjSfm68B/2ER5FTcvoLl1Xo2twrrVpUmcg3BClS +IZPuExdhVNnxjYKEWwcyZrehyAoR261fDdcFxLRW588efIUC+rPTTRHzAc7sT+Ln +t/uFeYNWJm3LaegOLoOmlMAhJ5puAWSN1F0FxtRf/RVgzbLA9QC975SKHJsfWCSr +IZyPsdeaqomKaF65l8nfqlE0Ua2L35gIOGKjUwb7uUE8nI362RWMtYdoi3zDDyoY +hSFbgjylCHDM0u6iSh6KfqOHtkYyJ8tUYgVWl787wQKBgQDYO3wL7xuDdD101Lyl +AnaDdFB9fxp83FG1cWr+t7LYm9YxGfEUsKHAJXN6TIayDkOOoVwIl+Gz0T3Z06Bm +eBGLrB9mrVA7+C7NJwu5gTMlzP6HxUR9zKJIQ/VB1NUGM77LSmvOFbHc9Q0+z8EH +X5WO516a3Z7lNtZJcCoPOtu2rwKBgQCmbj41Fh+SSEUApCEKms5ETRpe7LXQlJgx +yW7zcJNNuIb1C3vBLPxjiOTMgYKOeMg5rtHTGLT43URHLh9ArjawasjSAr4AM3J4 +xpoi/sKGDdiKOsuDWIGfzdYL8qyTHSdpZLQsCTMRiRYgAHZFPgNa7SLZRfZicGlr +GHN1rJW6OQKBgEjiM/upyrJSWeypUDSmUeAZMpA6aWkwsfHgmtnkfUn5rQa74cDB +kKO9e+D7LmOR3z+SL/1NhGwh2SE07dncGr3jdGodfO/ZxZyszozmeaECKcEFwwJM +GV8WWPKplGwUwPiwywmZ0mvRxXcoe73KgBS88+xrSwWjqDL0tZiQlEJNAoGATkei +GMQMG3jEg9Wu+NbxV6zQT3+U0MNjhl9RQU1c63x0dcNt9OFc4NAdlZcAulRTENaK +OHjxffBM0hH+fySx8m53gFfr2BpaqDX5f6ZGBlly1SlsWZ4CchCVsc71nshipi7I +k8HL9F5/OpQdDNprJ5RMBNfkWE65Nrcsb1e6oPkCgYAxwgdiSOtNg8PjDVDmAhwT +Mxj0Dtwi2fAqQ76RVrrXpNp3uCOIAu4CfruIb5llcJ3uak0ZbnWri32AxSgk80y3 +EWiRX/WEDu5znejF+5O3pI02atWWcnxifEKGGlxwkcMbQdA67MlrJLFaSnnGpNXo +yPfcul058SOqhafIZQMEKQ== +-----END PRIVATE KEY----- diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configuration/simple.pub b/config/src/test/resources/org/springframework/security/config/annotation/web/configuration/simple.pub new file mode 100644 index 0000000000..a25c08779e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configuration/simple.pub @@ -0,0 +1,7 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd +UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs +HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D +o2kQ+X5xK9cipRgEKwIDAQAB +-----END PUBLIC KEY----- + diff --git a/config/src/test/resources/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests-simple.pub b/config/src/test/resources/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests-simple.pub new file mode 100644 index 0000000000..3d01c1a2e0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests-simple.pub @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0IUjrPZDz+3z0UE4ppcKU36v7hnh8FJjhu3lbJYj0qj9eZiwEJxi9HHUfSK1DhUQG7mJBbYTK1tPYCgre5EkfKh+64VhYUa+vz17zYCmuB8fFj4XHE3MLkWIG+AUn8hNbPzYYmiBTjfGnMKxLHjsbdTiF4mtn+85w366916R6midnAuiPD4HjZaZ1PAsuY60gr8bhMEDtJ8unz81hoQrozpBZJ6r8aR1PrsWb1OqPMloK9kAIutJNvWYKacp8WYAp2WWy72PxQ7Fb0eIA1br3A5dnp+Cln6JROJcZUIRJ+QvS6QONWeS2407uQmS+i+lybsqaH0ldYC7NBEBA5inPQIDAQAB +-----END PUBLIC KEY----- diff --git a/core/src/main/java/org/springframework/security/converter/RsaKeyConverters.java b/core/src/main/java/org/springframework/security/converter/RsaKeyConverters.java new file mode 100644 index 0000000000..d499a8d19e --- /dev/null +++ b/core/src/main/java/org/springframework/security/converter/RsaKeyConverters.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.security.converter; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.Assert; + +/** + * Used for creating {@link java.security.Key} converter instances + * + * @author Josh Cummings + * @since 5.2 + */ +public class RsaKeyConverters { + private static final String DASHES = "-----"; + private static final String PKCS8_PEM_HEADER = DASHES + "BEGIN PRIVATE KEY" + DASHES; + private static final String PKCS8_PEM_FOOTER = DASHES + "END PRIVATE KEY" + DASHES; + private static final String X509_PEM_HEADER = DASHES + "BEGIN PUBLIC KEY" + DASHES; + private static final String X509_PEM_FOOTER = DASHES + "END PUBLIC KEY" + DASHES; + + /** + * Construct a {@link Converter} for converting a PEM-encoded PKCS#8 RSA Private Key + * into a {@link RSAPrivateKey}. + * + * Note that keys are often formatted in PKCS#1 and this can easily be identified by the header. + * If the key file begins with "-----BEGIN RSA PRIVATE KEY-----", then it is PKCS#1. If it is + * PKCS#8 formatted, then it begins with "-----BEGIN PRIVATE KEY-----". + * + * This converter does not close the {@link InputStream} in order to avoid making non-portable + * assumptions about the streams' origin and further use. + * + * @return A {@link Converter} that can read a PEM-encoded PKCS#8 RSA Private Key and return a + * {@link RSAPrivateKey}. + */ + public static Converter pkcs8() { + KeyFactory keyFactory = rsaFactory(); + return source -> { + List lines = readAllLines(source); + Assert.isTrue(!lines.isEmpty() && lines.get(0).startsWith(PKCS8_PEM_HEADER), + "Key is not in PEM-encoded PKCS#8 format, " + + "please check that the header begins with -----" + PKCS8_PEM_HEADER + "-----"); + String base64Encoded = lines.stream() + .filter(RsaKeyConverters::isNotPkcs8Wrapper) + .collect(Collectors.joining()); + byte[] pkcs8 = Base64.getDecoder().decode(base64Encoded); + + try { + return (RSAPrivateKey) keyFactory.generatePrivate( + new PKCS8EncodedKeySpec(pkcs8)); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + }; + } + + /** + * Construct a {@link Converter} for converting a PEM-encoded X.509 RSA Public Key + * into a {@link RSAPublicKey}. + * + * This converter does not close the {@link InputStream} in order to avoid making non-portable + * assumptions about the streams' origin and further use. + * + * @return A {@link Converter} that can read a PEM-encoded X.509 RSA Public Key and return a + * {@link RSAPublicKey}. + */ + public static Converter x509() { + KeyFactory keyFactory = rsaFactory(); + return source -> { + List lines = readAllLines(source); + Assert.isTrue(!lines.isEmpty() && lines.get(0).startsWith(X509_PEM_HEADER), + "Key is not in PEM-encoded X.509 format, " + + "please check that the header begins with -----" + X509_PEM_HEADER + "-----"); + String base64Encoded = lines.stream() + .filter(RsaKeyConverters::isNotX509Wrapper) + .collect(Collectors.joining()); + byte[] x509 = Base64.getDecoder().decode(base64Encoded); + + try { + return (RSAPublicKey) keyFactory.generatePublic( + new X509EncodedKeySpec(x509)); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + }; + } + + private static List readAllLines(InputStream source) { + BufferedReader reader = new BufferedReader(new InputStreamReader(source)); + return reader.lines().collect(Collectors.toList()); + } + + private static KeyFactory rsaFactory() { + try { + return KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + private static boolean isNotPkcs8Wrapper(String line) { + return !PKCS8_PEM_HEADER.equals(line) && !PKCS8_PEM_FOOTER.equals(line); + } + + private static boolean isNotX509Wrapper(String line) { + return !X509_PEM_HEADER.equals(line) && !X509_PEM_FOOTER.equals(line); + } +} diff --git a/core/src/test/java/org/springframework/security/converter/RsaKeyConvertersTest.java b/core/src/test/java/org/springframework/security/converter/RsaKeyConvertersTest.java new file mode 100644 index 0000000000..f1ba277e12 --- /dev/null +++ b/core/src/test/java/org/springframework/security/converter/RsaKeyConvertersTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.security.converter; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +import org.assertj.core.api.Assertions; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.Test; + +import org.springframework.core.convert.converter.Converter; + +/** + * Tests for {@link RsaKeyConverters} + */ +public class RsaKeyConvertersTest { + private static final String PKCS8_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCMk7CKSTfu3QoV\n" + + "HoPVXxwZO+qweztd36cVWYqGOZinrOR2crWFu50AgR2CsdIH0+cqo7F4Vx7/3O8i\n" + + "RpYYZPe2VoO5sumzJt8P6fS80/TAKjhJDAqgZKRJTgGN8KxCM6p/aJli1ZeDBqiV\n" + + "v7vJJe+ZgJuPGRS+HMNa/wPxEkqqXsglcJcQV1ZEtfKXSHB7jizKpRL38185SyAC\n" + + "pwyjvBu6Cmm1URfhQo88mf239ONh4dZ2HoDfzN1q6Ssu4F4hgutxr9B0DVLDP5u+\n" + + "WFrm3nsJ76zf99uJ+ntMUHJ+bY+gOjSlVWIVBIZeAaEGKCNWRk/knjvjbijpvm3U\n" + + "acGlgdL3AgMBAAECggEACxxxS7zVyu91qI2s5eSKmAQAXMqgup6+2hUluc47nqUv\n" + + "uZz/c/6MPkn2Ryo+65d4IgqmMFjSfm68B/2ER5FTcvoLl1Xo2twrrVpUmcg3BClS\n" + + "IZPuExdhVNnxjYKEWwcyZrehyAoR261fDdcFxLRW588efIUC+rPTTRHzAc7sT+Ln\n" + + "t/uFeYNWJm3LaegOLoOmlMAhJ5puAWSN1F0FxtRf/RVgzbLA9QC975SKHJsfWCSr\n" + + "IZyPsdeaqomKaF65l8nfqlE0Ua2L35gIOGKjUwb7uUE8nI362RWMtYdoi3zDDyoY\n" + + "hSFbgjylCHDM0u6iSh6KfqOHtkYyJ8tUYgVWl787wQKBgQDYO3wL7xuDdD101Lyl\n" + + "AnaDdFB9fxp83FG1cWr+t7LYm9YxGfEUsKHAJXN6TIayDkOOoVwIl+Gz0T3Z06Bm\n" + + "eBGLrB9mrVA7+C7NJwu5gTMlzP6HxUR9zKJIQ/VB1NUGM77LSmvOFbHc9Q0+z8EH\n" + + "X5WO516a3Z7lNtZJcCoPOtu2rwKBgQCmbj41Fh+SSEUApCEKms5ETRpe7LXQlJgx\n" + + "yW7zcJNNuIb1C3vBLPxjiOTMgYKOeMg5rtHTGLT43URHLh9ArjawasjSAr4AM3J4\n" + + "xpoi/sKGDdiKOsuDWIGfzdYL8qyTHSdpZLQsCTMRiRYgAHZFPgNa7SLZRfZicGlr\n" + + "GHN1rJW6OQKBgEjiM/upyrJSWeypUDSmUeAZMpA6aWkwsfHgmtnkfUn5rQa74cDB\n" + + "kKO9e+D7LmOR3z+SL/1NhGwh2SE07dncGr3jdGodfO/ZxZyszozmeaECKcEFwwJM\n" + + "GV8WWPKplGwUwPiwywmZ0mvRxXcoe73KgBS88+xrSwWjqDL0tZiQlEJNAoGATkei\n" + + "GMQMG3jEg9Wu+NbxV6zQT3+U0MNjhl9RQU1c63x0dcNt9OFc4NAdlZcAulRTENaK\n" + + "OHjxffBM0hH+fySx8m53gFfr2BpaqDX5f6ZGBlly1SlsWZ4CchCVsc71nshipi7I\n" + + "k8HL9F5/OpQdDNprJ5RMBNfkWE65Nrcsb1e6oPkCgYAxwgdiSOtNg8PjDVDmAhwT\n" + + "Mxj0Dtwi2fAqQ76RVrrXpNp3uCOIAu4CfruIb5llcJ3uak0ZbnWri32AxSgk80y3\n" + + "EWiRX/WEDu5znejF+5O3pI02atWWcnxifEKGGlxwkcMbQdA67MlrJLFaSnnGpNXo\n" + + "yPfcul058SOqhafIZQMEKQ==\n" + + "-----END PRIVATE KEY-----"; + + private static final String PKCS1_PRIVATE_KEY = + "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw\n" + + "33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW\n" + + "+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB\n" + + "AoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS\n" + + "3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5Cp\n" + + "uGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE\n" + + "2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0\n" + + "GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0K\n" + + "Su5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY\n" + + "6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5\n" + + "fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523\n" + + "Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aP\n" + + "FaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==\n" + + "-----END RSA PRIVATE KEY-----"; + + private static final String X509_PUBLIC_KEY = + "-----BEGIN PUBLIC KEY-----\n" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd\n" + + "UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs\n" + + "HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D\n" + + "o2kQ+X5xK9cipRgEKwIDAQAB\n" + + "-----END PUBLIC KEY-----"; + + private static final String MALFORMED_X509_KEY = "malformed"; + + private final Converter x509 = RsaKeyConverters.x509(); + private final Converter pkcs8 = RsaKeyConverters.pkcs8(); + + @Test + public void pkcs8WhenConvertingPkcs8PrivateKeyThenOk() { + RSAPrivateKey key = this.pkcs8.convert(toInputStream(PKCS8_PRIVATE_KEY)); + Assertions.assertThat(key).isInstanceOf(RSAPrivateCrtKey.class); + Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(2048); + } + + @Test + public void pkcs8WhenConvertingPkcs1PrivateKeyThenIllegalArgumentException() { + AssertionsForClassTypes.assertThatCode(() -> this.pkcs8.convert(toInputStream(PKCS1_PRIVATE_KEY))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void x509WhenConverteringX509PublicKeyThenOk() { + RSAPublicKey key = this.x509.convert(toInputStream(X509_PUBLIC_KEY)); + Assertions.assertThat(key.getModulus().bitLength()).isEqualTo(1024); + } + + @Test + public void x509WhenConvertingDerEncodedX509PublicKeyThenIllegalArgumentException() { + AssertionsForClassTypes.assertThatCode(() -> this.x509.convert(toInputStream(MALFORMED_X509_KEY))) + .isInstanceOf(IllegalArgumentException.class); + } + + private static InputStream toInputStream(String string) { + return new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8)); + } +}