Introduce Support for Reading RSA Keys

Fixes: gh-6494
This commit is contained in:
Josh Cummings 2019-02-04 11:49:21 -07:00
parent 22c8f63390
commit 1c25fe26c9
10 changed files with 683 additions and 3 deletions

View File

@ -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) {

View File

@ -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)) {

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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 {

View File

@ -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-----

View File

@ -0,0 +1,7 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd
UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs
HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D
o2kQ+X5xK9cipRgEKwIDAQAB
-----END PUBLIC KEY-----

View File

@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0IUjrPZDz+3z0UE4ppcKU36v7hnh8FJjhu3lbJYj0qj9eZiwEJxi9HHUfSK1DhUQG7mJBbYTK1tPYCgre5EkfKh+64VhYUa+vz17zYCmuB8fFj4XHE3MLkWIG+AUn8hNbPzYYmiBTjfGnMKxLHjsbdTiF4mtn+85w366916R6midnAuiPD4HjZaZ1PAsuY60gr8bhMEDtJ8unz81hoQrozpBZJ6r8aR1PrsWb1OqPMloK9kAIutJNvWYKacp8WYAp2WWy72PxQ7Fb0eIA1br3A5dnp+Cln6JROJcZUIRJ+QvS6QONWeS2407uQmS+i+lybsqaH0ldYC7NBEBA5inPQIDAQAB
-----END PUBLIC KEY-----

View File

@ -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);
}
}

View File

@ -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));
}
}