Skip to content

Commit b2508eb

Browse files
authored
Added support for trusted certificates
1 parent c7ab0f3 commit b2508eb

4 files changed

Lines changed: 122 additions & 49 deletions

File tree

spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/EncryptionController.java

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.security.crypto.codec.Hex;
3333
import org.springframework.security.crypto.encrypt.TextEncryptor;
3434
import org.springframework.security.rsa.crypto.RsaKeyHolder;
35+
import org.springframework.security.rsa.crypto.RsaSecretEncryptor;
3536
import org.springframework.util.Base64Utils;
3637
import org.springframework.web.bind.annotation.ExceptionHandler;
3738
import org.springframework.web.bind.annotation.PathVariable;
@@ -43,6 +44,7 @@
4344

4445
/**
4546
* @author Dave Syer
47+
* @author Tim Ysewyn
4648
*
4749
*/
4850
@RestController
@@ -51,16 +53,16 @@ public class EncryptionController {
5153

5254
private static Log logger = LogFactory.getLog(EncryptionController.class);
5355

54-
volatile private TextEncryptorLocator encryptor;
56+
volatile private TextEncryptorLocator encryptorLocator;
5557

5658
private EnvironmentPrefixHelper helper = new EnvironmentPrefixHelper();
5759

5860
private String defaultApplicationName = "application";
5961

6062
private String defaultProfile = "default";
6163

62-
public EncryptionController(TextEncryptorLocator encryptor) {
63-
this.encryptor = encryptor;
64+
public EncryptionController(TextEncryptorLocator encryptorLocator) {
65+
this.encryptorLocator = encryptorLocator;
6466
}
6567

6668
public void setDefaultApplicationName(String defaultApplicationName) {
@@ -73,82 +75,61 @@ public void setDefaultProfile(String defaultProfile) {
7375

7476
@RequestMapping(value = "/key", method = RequestMethod.GET)
7577
public String getPublicKey() {
76-
TextEncryptor encryptor = this.encryptor
77-
.locate(this.helper.getEncryptorKeys("application", "default", ""));
78-
if (!(encryptor instanceof RsaKeyHolder)) {
79-
throw new KeyNotAvailableException();
80-
}
81-
return ((RsaKeyHolder) encryptor).getPublicKey();
78+
return getPublicKey(defaultApplicationName, defaultProfile);
8279
}
8380

8481
@RequestMapping(value = "/key/{name}/{profiles}", method = RequestMethod.GET)
8582
public String getPublicKey(@PathVariable String name, @PathVariable String profiles) {
86-
TextEncryptor encryptor = this.encryptor
87-
.locate(this.helper.getEncryptorKeys(name, profiles, ""));
83+
TextEncryptor encryptor = getEncryptor(name, profiles, "");
8884
if (!(encryptor instanceof RsaKeyHolder)) {
8985
throw new KeyNotAvailableException();
9086
}
9187
return ((RsaKeyHolder) encryptor).getPublicKey();
9288
}
9389

94-
@ExceptionHandler(KeyFormatException.class)
95-
public ResponseEntity<Map<String, Object>> keyFormat() {
96-
Map<String, Object> body = new HashMap<>();
97-
body.put("status", "BAD_REQUEST");
98-
body.put("description", "Key data not in correct format (PEM or jks keystore)");
99-
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
100-
}
101-
102-
@ExceptionHandler(KeyNotAvailableException.class)
103-
public ResponseEntity<Map<String, Object>> keyUnavailable() {
104-
Map<String, Object> body = new HashMap<>();
105-
body.put("status", "NOT_FOUND");
106-
body.put("description", "No public key available");
107-
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
108-
}
109-
11090
@RequestMapping(value = "encrypt/status", method = RequestMethod.GET)
11191
public Map<String, Object> status() {
112-
checkEncryptorInstalled("application", "default");
92+
TextEncryptor encryptor = getEncryptor(defaultApplicationName, defaultProfile,
93+
"");
94+
validateEncryptionWeakness(encryptor);
11395
return Collections.singletonMap("status", "OK");
11496
}
11597

11698
@RequestMapping(value = "encrypt", method = RequestMethod.POST)
11799
public String encrypt(@RequestBody String data,
118100
@RequestHeader("Content-Type") MediaType type) {
119-
120-
return encrypt(this.defaultApplicationName, this.defaultProfile, data, type);
101+
return encrypt(defaultApplicationName, defaultProfile, data, type);
121102
}
122103

123104
@RequestMapping(value = "/encrypt/{name}/{profiles}", method = RequestMethod.POST)
124105
public String encrypt(@PathVariable String name, @PathVariable String profiles,
125106
@RequestBody String data, @RequestHeader("Content-Type") MediaType type) {
126-
checkEncryptorInstalled(name, profiles);
107+
TextEncryptor encryptor = getEncryptor(name, profiles, "");
108+
validateEncryptionWeakness(encryptor);
127109
String input = stripFormData(data, type, false);
128-
Map<String, String> keys = this.helper.getEncryptorKeys(name, profiles, input);
129-
String textToEncrypt = this.helper.stripPrefix(input);
130-
String encrypted = this.helper.addPrefix(keys,
131-
this.encryptor.locate(keys).encrypt(textToEncrypt));
110+
Map<String, String> keys = helper.getEncryptorKeys(name, profiles, input);
111+
String textToEncrypt = helper.stripPrefix(input);
112+
String encrypted = helper.addPrefix(keys,
113+
encryptorLocator.locate(keys).encrypt(textToEncrypt));
132114
logger.info("Encrypted data");
133115
return encrypted;
134116
}
135117

136118
@RequestMapping(value = "decrypt", method = RequestMethod.POST)
137119
public String decrypt(@RequestBody String data,
138120
@RequestHeader("Content-Type") MediaType type) {
139-
140-
return decrypt(this.defaultApplicationName, this.defaultProfile, data, type);
121+
return decrypt(defaultApplicationName, defaultProfile, data, type);
141122
}
142123

143124
@RequestMapping(value = "/decrypt/{name}/{profiles}", method = RequestMethod.POST)
144125
public String decrypt(@PathVariable String name, @PathVariable String profiles,
145126
@RequestBody String data, @RequestHeader("Content-Type") MediaType type) {
146-
checkEncryptorInstalled(name, profiles);
127+
TextEncryptor encryptor = getEncryptor(name, profiles, "");
128+
checkDecryptionPossible(encryptor);
129+
validateEncryptionWeakness(encryptor);
147130
try {
148-
String input = stripFormData(this.helper.stripPrefix(data), type, true);
149-
Map<String, String> encryptorKeys = this.helper.getEncryptorKeys(name,
150-
profiles, data);
151-
TextEncryptor encryptor = this.encryptor.locate(encryptorKeys);
131+
encryptor = getEncryptor(name, profiles, data);
132+
String input = stripFormData(helper.stripPrefix(data), type, true);
152133
String decrypted = encryptor.decrypt(input);
153134
logger.info("Decrypted cipher data");
154135
return decrypted;
@@ -159,16 +140,31 @@ public String decrypt(@PathVariable String name, @PathVariable String profiles,
159140
}
160141
}
161142

162-
private void checkEncryptorInstalled(String name, String profiles) {
163-
if (this.encryptor == null) {
143+
private TextEncryptor getEncryptor(String name, String profiles, String data) {
144+
if (encryptorLocator == null) {
145+
throw new KeyNotInstalledException();
146+
}
147+
TextEncryptor encryptor = encryptorLocator
148+
.locate(helper.getEncryptorKeys(name, profiles, data));
149+
if (encryptor == null) {
164150
throw new KeyNotInstalledException();
165151
}
166-
if (this.encryptor.locate(this.helper.getEncryptorKeys(name, profiles, ""))
167-
.encrypt("FOO").equals("FOO")) {
152+
return encryptor;
153+
}
154+
155+
private void validateEncryptionWeakness(TextEncryptor textEncryptor) {
156+
if (textEncryptor.encrypt("FOO").equals("FOO")) {
168157
throw new EncryptionTooWeakException();
169158
}
170159
}
171160

161+
private void checkDecryptionPossible(TextEncryptor textEncryptor) {
162+
if (textEncryptor instanceof RsaSecretEncryptor
163+
&& !((RsaSecretEncryptor) textEncryptor).canDecrypt()) {
164+
throw new DecryptionNotSupportedException();
165+
}
166+
}
167+
172168
private String stripFormData(String data, MediaType type, boolean cipher) {
173169

174170
if (data.endsWith("=") && !type.equals(MediaType.TEXT_PLAIN)) {
@@ -209,6 +205,30 @@ private String stripFormData(String data, MediaType type, boolean cipher) {
209205

210206
}
211207

208+
@ExceptionHandler(KeyFormatException.class)
209+
public ResponseEntity<Map<String, Object>> keyFormat() {
210+
Map<String, Object> body = new HashMap<>();
211+
body.put("status", "BAD_REQUEST");
212+
body.put("description", "Key data not in correct format (PEM or jks keystore)");
213+
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
214+
}
215+
216+
@ExceptionHandler(KeyNotAvailableException.class)
217+
public ResponseEntity<Map<String, Object>> keyUnavailable() {
218+
Map<String, Object> body = new HashMap<>();
219+
body.put("status", "NOT_FOUND");
220+
body.put("description", "No public key available");
221+
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
222+
}
223+
224+
@ExceptionHandler(DecryptionNotSupportedException.class)
225+
public ResponseEntity<Map<String, Object>> decryptionDisabled() {
226+
Map<String, Object> body = new HashMap<>();
227+
body.put("status", "BAD_REQUEST");
228+
body.put("description", "Server-side decryption is not supported");
229+
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
230+
}
231+
212232
@ExceptionHandler(KeyNotInstalledException.class)
213233
public ResponseEntity<Map<String, Object>> notInstalled() {
214234
Map<String, Object> body = new HashMap<>();
@@ -254,3 +274,8 @@ class EncryptionTooWeakException extends RuntimeException {
254274
class InvalidCipherException extends RuntimeException {
255275

256276
}
277+
278+
@SuppressWarnings("serial")
279+
class DecryptionNotSupportedException extends RuntimeException {
280+
281+
}

spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/HttpClientConfigurableHttpConnectionFactory.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,10 @@ private HttpClientBuilder lookupHttpClientBuilder(final URL url) {
106106
}
107107
if (builderMap.size() > 1) {
108108
/*
109-
* Try to determine if there is an exact match URL or not. So if there is a placeholder in the URL, filter
110-
* it out. We should be left with only URLs which have no placeholders.
111-
* That is the one we want to use in the case there are multiple matches.
109+
* Try to determine if there is an exact match URL or not. So if there is a
110+
* placeholder in the URL, filter it out. We should be left with only URLs
111+
* which have no placeholders. That is the one we want to use in the case
112+
* there are multiple matches.
112113
*/
113114
List<String> keys = builderMap.keySet().stream().filter(key -> {
114115
String[] tokens = key.split(PLACEHOLDER_PATTERN);

spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/encryption/EncryptionIntegrationTests.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@
1616

1717
package org.springframework.cloud.config.server.encryption;
1818

19+
import org.junit.BeforeClass;
1920
import org.junit.Test;
2021
import org.junit.runner.RunWith;
2122

2223
import org.springframework.beans.factory.annotation.Autowired;
2324
import org.springframework.boot.test.context.SpringBootTest;
2425
import org.springframework.boot.test.web.client.TestRestTemplate;
2526
import org.springframework.cloud.config.server.ConfigServerApplication;
27+
import org.springframework.cloud.config.server.test.ConfigServerTestUtils;
28+
import org.springframework.http.HttpEntity;
29+
import org.springframework.http.HttpHeaders;
2630
import org.springframework.http.HttpStatus;
31+
import org.springframework.http.MediaType;
2732
import org.springframework.http.ResponseEntity;
2833
import org.springframework.test.annotation.DirtiesContext;
2934
import org.springframework.test.context.ActiveProfiles;
@@ -90,4 +95,46 @@ public void keystoreBootstrapConfig() throws Exception {
9095

9196
}
9297

98+
@RunWith(SpringRunner.class)
99+
@SpringBootTest(classes = { ConfigServerApplication.class }, properties = {
100+
"spring.cloud.bootstrap.name:keystore-bootstrap",
101+
"spring.cloud.config.server.encrypt.enabled=false",
102+
"encrypt.keyStore.alias=myencryptionkey" }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
103+
@ActiveProfiles({ "test", "git" })
104+
@DirtiesContext
105+
public static class KeystoreConfigurationEncryptionOnlyIntegrationTests {
106+
107+
@Autowired
108+
private TestRestTemplate testRestTemplate;
109+
110+
@BeforeClass
111+
public static void setupTest() throws Exception {
112+
ConfigServerTestUtils.prepareLocalRepo("./", "target/repos", "encrypt-repo",
113+
"target/config");
114+
}
115+
116+
@Test
117+
public void shouldOnlySupportEncryption() {
118+
ResponseEntity<String> entity = this.testRestTemplate
119+
.getForEntity("/keystore-bootstrap/encrypt", String.class);
120+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
121+
assertThat(entity.getBody()).contains(
122+
"{cipher}{key:mytestkey}AQCohs2V6P8/UiG6a4TF/CZTCBdt5Q7wvNvcyf6vs2ByK2ZYSM77Nu0sOAduxUpMbVwJ/syecmkIXR+hU3EfT2uqPieA7/v5n33ppqIQ9JAt5JggdYIGe+wX25zU3DTXOOJdAAMzNX+zjOVyCh0QtmJf/kFslg6NqQq0E+kSg3zBi3AnkKj5BLnLIxkjxzKA4mnDXpSm7ekLZZP2iQSYSW/82AC7UOLLzTqwInMI3tJLW1e9Ne+LDsjmSxA+nkK9zhidtXPwb/SPaNF74cJCEf9mgzzKYwJlwqChLzJt8UQ1jHwRc8B6FufmizUHSp27nxdtVB4HMqh3nNsMCy137Ces58T09ZS/y/cYNRxcFbp78MHFHUqAgbC0B/p5t6h4XbQ=");
123+
124+
HttpEntity<String> encryptionRequest = new HttpEntity<>("valueToBeEncrypted");
125+
entity = this.testRestTemplate.postForEntity("/encrypt", encryptionRequest,
126+
String.class);
127+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
128+
129+
HttpHeaders decryptionRequestHeaders = new HttpHeaders();
130+
decryptionRequestHeaders.setContentType(MediaType.TEXT_PLAIN);
131+
HttpEntity<String> decryptionRequest = new HttpEntity<>(entity.getBody(),
132+
decryptionRequestHeaders);
133+
entity = this.testRestTemplate.postForEntity("/decrypt", decryptionRequest,
134+
String.class);
135+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
136+
}
137+
138+
}
139+
93140
}
931 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)