Bläddra i källkod

Introduce Support for Reading RSA Keys

Fixes: gh-6494
Josh Cummings 6 år sedan
förälder
incheckning
1c25fe26c9

+ 8 - 2
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) {

+ 8 - 1
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<SecurityWebFilterChain> getSecurityWebFilterChains() {
 		List<SecurityWebFilterChain> result = this.securityWebFilterChains;
 		if (ObjectUtils.isEmpty(result)) {

+ 154 - 0
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<String, RSAPrivateKey> pkcs8 = pkcs8();
+		Converter<String, RSAPublicKey> 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<String, RSAPrivateKey> pkcs8() {
+		Converter<String, InputStream> pemInputStreamConverter = pemInputStreamConverter();
+		Converter<InputStream, RSAPrivateKey> pkcs8KeyConverter = autoclose(RsaKeyConverters.pkcs8());
+		return pair(pemInputStreamConverter, pkcs8KeyConverter);
+	}
+
+	private Converter<String, RSAPublicKey> x509() {
+		Converter<String, InputStream> pemInputStreamConverter = pemInputStreamConverter();
+		Converter<InputStream, RSAPublicKey> x509KeyConverter = autoclose(RsaKeyConverters.x509());
+		return pair(pemInputStreamConverter, x509KeyConverter);
+	}
+
+	private Converter<String, InputStream> 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 <T> Converter<InputStream, T> autoclose(Converter<InputStream, T> inputStreamKeyConverter) {
+		return inputStream -> {
+			try (InputStream is = inputStream) {
+				return inputStreamKeyConverter.convert(is);
+			} catch (IOException e) {
+				throw new UncheckedIOException(e);
+			}
+		};
+	}
+
+	private <S, T, I> Converter<S, T> pair(Converter<S, I> one, Converter<I, T> two) {
+		return source -> {
+			I intermediary = one.convert(source);
+			return two.convert(intermediary);
+		};
+	}
+
+	private static class ConverterPropertyEditorAdapter<T> extends PropertyEditorSupport {
+		private final Converter<String, T> converter;
+
+		public ConverterPropertyEditorAdapter(Converter<String, T> 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);
+			}
+		}
+	}
+}

+ 184 - 0
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();
+		}
+	}
+}

+ 34 - 0
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 {

+ 28 - 0
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-----

+ 7 - 0
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-----
+

+ 3 - 0
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-----

+ 134 - 0
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<InputStream, RSAPrivateKey> pkcs8() {
+		KeyFactory keyFactory = rsaFactory();
+		return source -> {
+			List<String> 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<InputStream, RSAPublicKey> x509() {
+		KeyFactory keyFactory = rsaFactory();
+		return source -> {
+			List<String> 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<String> 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);
+	}
+}

+ 123 - 0
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<InputStream, RSAPublicKey> x509 = RsaKeyConverters.x509();
+	private final Converter<InputStream, RSAPrivateKey> 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));
+	}
+}