Skip to content

Commit aad1ec6

Browse files
authored
Merge pull request #563 from govuk-one-login/OJ-3158-decrypt-request-kms-or-jwks-endpoint
OJ-3158: Decrypt request from kms or jwks endpoint depending of feature-flag
2 parents 4c23697 + d4bb665 commit aad1ec6

24 files changed

Lines changed: 1252 additions & 278 deletions

.secrets.baseline

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@
113113
{
114114
"path": "detect_secrets.filters.regex.should_exclude_secret",
115115
"pattern": [
116+
"jfDZSCq6Z7Hu22uWaNEtDfFfv-RZot58oxhTAwNoGT3aMvWUiZBIzqm0b9f2xkxMBEky3oix9xC5_KRL2Xv-OO9DdTw7sfLMUs7BidEXWRIAq7PgiD1rdkQ5ElZHM1TPYoREXhJyqtXMgup8lD_B85m-xBOgaZQvuG_cxc0lNerLBgu1f23jcy0S8G3P8L-Cl056Kv6QV-WGFOQW0Vurwd_f432Ho1W1STYrSat22YNkX2_A0SJZGVcxF_wKKfNAUw4n7sVdYZOfl62x7Cz2Rt2HX36U6vLhI8ZLNGROCsNKI-LYJA2ET1_li150DMgMNlfYfwHrO3jFi_j1XcK_oA",
117+
"cT7gnhBT0VT7jY5gEAsafuZi-o6BP8DI-aaH97mJ4e6q0E1pAgWkWAHc-qvmRWYHLUfbMlTOpH5AlQNhQ-ZWsfm40eM0sIV3OZCk4KcAbSoz4v-9aqleBTVhr_YhZqk_lZ9I9566SzLnOuPkWQr6J5F6F19Ol7Ob0j7-a2zHgXlxQizp1hjXiWAhJ0aFFRfP4hxcohn7h5EKeMw8ZT8jv1kqc0PwRoZOt83SgBcdlLcIz9LDPIUWuXXtw9Xi5FrfAc2SXFv4sv7BEo70-ICT9sC1jTpkMsqJlofqu5R3L2Kf51HFOJe2C1SRy_MQGID9FnQGgrDburfSpcmH_DPxdLS8SJ9X7LyyrPWzrdTwgUDdUCWmsoYbvgZQC1KhRiu7GjKLDU2uQgo0NSiaNIcyS6qllDXPqJUTkz0snmMUjcIN7ZTzA29ngxJh5OhI444qChQrB-2hU769giX00UEyqb--MpTWybGReoC0nF-BzaZrrQkWMB2vFWiDg5dUUD6778b4YvmryINCP5H4NteK8JHnIsqMMbY6wxtZFqVhsvVAR6thM9JBKJrN5nSMkKlwSAEpf2vbUyec2x_AZQ6d66lrneZe3VHWmHAo42d6if2P-yaL2vLrr9g73vr7CfU9WiTYTYtFOJ0aWodFwnSeZq-Bek1RXTNsEl4G8K3ved97W1YlEW4359V6OWpSCfFouDJv-yLxaedRvzXjcBH0Ssx6D8Njs4cOduQ-PE22mUcpHd5URsUsU19F59jgXpk",
118+
".*okVaj3BYY8FfaPef4nzV9dr\\+ziueibf2hofYDQ=",
116119
"eyJraWQiOiJpcHYtY29yZS1zdHViIiwiYWxnIjoiUlMyNTYifQ\\.eyJzdWIiOiJpcHYtY29yZS1zdHViIiwiYXVkIjoiaHR0cHM6XC9cL2Rldi5hZGRyZXNzLmNyaS5hY2NvdW50Lmdvdi51ayIsIm5iZiI6MTY1MDU0MTg0MCwic2hhcmVkX2NsY.*",
117120
"eyJraWQiOiJpcHYtY29yZS1zdHViIiwiYWxnIjoiUlMyNTYifQ\\.eyJzdWIiOiJpcHYtY29yZS1zdHViIiwiYXVkIjoiaHR0cHM6XC9cL2Rldi5hZGRyZXNzLmNyaS5hY2NvdW50Lmdvdi51ayIsIm5iZiI6MTY1MDU0MDkyNSwic2hhcmVkX2NsY.*",
118121
"MIIDJDCCAgwCCQD3oEU83RePojANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJHQjEXMBUGA1UECgwOQ2FiaW5ldCBPZmZpY2UxDDAKBgNVBAsMA0dEUzEeMBwGA1UEAwwVSVBWIENvcmUgU3R1YiBTaWduaW5nMB4XDTIyMDIwNDE3NDg1NFoXDT.*",

RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Credential Issuer common libraries Release Notes
22

3+
# 6.2.0
4+
- Added steps to decrypt and verify session request in WellKnownJwksSteps
5+
that have been encrypted by the stub for the CRI to decrypt using the corresponding kms key alias
6+
if `./well-known` is used the ENV_VAR_FEATURE_FLAG_KEY_ROTATION needs to be set to true
7+
when the flag is not enabled then encryption is done using the former approach through a shared public base64 key
38
# 6.1.0
49
- Retrieves JWKS endpoint for JWT verification from SSM params instead of an ENV variable
510
- Caches JWKS per endpoint instead of a single cache to allow for different clients

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ plugins {
1111
}
1212

1313
// please update RELEASE_NOTES.md when you update this version
14-
def buildVersion = "6.1.0"
14+
def buildVersion = "6.2.0"
1515

1616
defaultTasks 'clean', 'spotlessApply', 'build'
1717

@@ -154,6 +154,7 @@ dependencies {
154154

155155
testRuntimeOnly configurations.test_runtime
156156

157+
testFixturesApi configurations.kms
157158
testFixturesApi configurations.aws
158159
testFixturesApi configurations.sqs
159160
testFixturesApi configurations.aws_crt_client
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package uk.gov.di.ipv.cri.common.library.service;
2+
3+
import com.nimbusds.jose.JOSEException;
4+
import com.nimbusds.jose.JWEObject;
5+
import com.nimbusds.jwt.SignedJWT;
6+
7+
import java.text.ParseException;
8+
9+
public class JWTDecrypter {
10+
private final KMSRSADecrypter decrypter;
11+
12+
public JWTDecrypter(KMSRSADecrypter decrypter) {
13+
this.decrypter = decrypter;
14+
}
15+
16+
public SignedJWT decrypt(String serialisedJweObj) throws ParseException, JOSEException {
17+
JWEObject requestJweObj = JWEObject.parse(serialisedJweObj);
18+
requestJweObj.decrypt(this.decrypter);
19+
return requestJweObj.getPayload().toSignedJWT();
20+
}
21+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package uk.gov.di.ipv.cri.common.library.service;
2+
3+
import com.nimbusds.jose.EncryptionMethod;
4+
import com.nimbusds.jose.JOSEException;
5+
import com.nimbusds.jose.JWEAlgorithm;
6+
import com.nimbusds.jose.JWEDecrypter;
7+
import com.nimbusds.jose.JWEHeader;
8+
import com.nimbusds.jose.crypto.impl.AlgorithmSupportMessage;
9+
import com.nimbusds.jose.crypto.impl.ContentCryptoProvider;
10+
import com.nimbusds.jose.jca.JWEJCAContext;
11+
import com.nimbusds.jose.util.Base64URL;
12+
import org.apache.logging.log4j.LogManager;
13+
import org.apache.logging.log4j.Logger;
14+
import software.amazon.awssdk.core.SdkBytes;
15+
import software.amazon.awssdk.services.kms.KmsClient;
16+
import software.amazon.awssdk.services.kms.model.DecryptRequest;
17+
import software.amazon.awssdk.services.kms.model.DecryptResponse;
18+
import software.amazon.awssdk.services.kms.model.EncryptionAlgorithmSpec;
19+
import uk.gov.di.ipv.cri.common.library.util.EventProbe;
20+
21+
import javax.crypto.SecretKey;
22+
import javax.crypto.spec.SecretKeySpec;
23+
24+
import java.util.Objects;
25+
import java.util.Set;
26+
27+
import static software.amazon.awssdk.services.kms.model.EncryptionAlgorithmSpec.RSAES_OAEP_SHA_256;
28+
29+
public class KMSRSADecrypter implements JWEDecrypter {
30+
private static final Set<JWEAlgorithm> SUPPORTED_ALGORITHMS = Set.of(JWEAlgorithm.RSA_OAEP_256);
31+
private static final Set<EncryptionMethod> SUPPORTED_ENCRYPTION_METHODS =
32+
Set.of(EncryptionMethod.A256GCM);
33+
private static final Logger LOGGER = LogManager.getLogger();
34+
private static final String SESSION_DECRYPTION_KEY_PRIMARY_ALIAS =
35+
"session_decryption_key_active_alias";
36+
private static final String SESSION_DECRYPTION_KEY_SECONDARY_ALIAS =
37+
"session_decryption_key_inactive_alias";
38+
private static final String SESSION_DECRYPTION_KEY_PREVIOUS_ALIAS =
39+
"session_decryption_key_previous_alias";
40+
private static final String ALL_ALIASES_UNAVAILABLE = "all_aliases_unavailable_for_decryption";
41+
private boolean keyRotationEnabled = false;
42+
private final JWEJCAContext jcaContext;
43+
private final KmsClient kmsClient;
44+
private final EventProbe eventProbe;
45+
private final String keyId;
46+
47+
public KMSRSADecrypter(String keyId, KmsClient kmsClient, EventProbe eventProbe) {
48+
this(
49+
kmsClient,
50+
eventProbe,
51+
keyId,
52+
Boolean.parseBoolean(System.getenv("ENV_VAR_FEATURE_FLAG_KEY_ROTATION")));
53+
}
54+
55+
public KMSRSADecrypter(
56+
KmsClient kmsClient, EventProbe eventProbe, String keyId, Boolean keyRotationEnabled) {
57+
this.jcaContext = new JWEJCAContext();
58+
this.kmsClient = kmsClient;
59+
this.eventProbe = eventProbe;
60+
this.keyId = keyId;
61+
this.keyRotationEnabled = keyRotationEnabled;
62+
}
63+
64+
@Override
65+
public byte[] decrypt(
66+
JWEHeader header,
67+
Base64URL encryptedKey,
68+
Base64URL iv,
69+
Base64URL cipherText,
70+
Base64URL authTag,
71+
byte[] aad)
72+
throws JOSEException {
73+
// Validate required JWE parts
74+
if (Objects.isNull(encryptedKey)) {
75+
throw new JOSEException("Missing JWE encrypted key");
76+
}
77+
78+
if (Objects.isNull(iv)) {
79+
throw new JOSEException("Missing JWE initialization vector (IV)");
80+
}
81+
82+
if (Objects.isNull(authTag)) {
83+
throw new JOSEException("Missing JWE authentication tag");
84+
}
85+
86+
JWEAlgorithm alg = header.getAlgorithm();
87+
88+
if (!SUPPORTED_ALGORITHMS.contains(alg)) {
89+
throw new JOSEException(
90+
AlgorithmSupportMessage.unsupportedJWEAlgorithm(alg, supportedJWEAlgorithms()));
91+
}
92+
DecryptResponse decryptResponse;
93+
if (isKeyRotationEnabled()) {
94+
LOGGER.info("Key rotation enabled. Attempting to decrypt with key aliases.");
95+
// During a key rotation we might receive JWTs encrypted with either the old or new key.
96+
decryptResponse = decryptWithKeyAliases(encryptedKey);
97+
98+
if (decryptResponse == null) {
99+
String message = "Failed to decrypt with all available key aliases.";
100+
LOGGER.error(message);
101+
throw new JOSEException(message);
102+
}
103+
} else {
104+
DecryptRequest decryptRequest =
105+
DecryptRequest.builder()
106+
.encryptionAlgorithm(EncryptionAlgorithmSpec.RSAES_OAEP_SHA_256)
107+
.ciphertextBlob(SdkBytes.fromByteArray(encryptedKey.decode()))
108+
.keyId(this.keyId)
109+
.build();
110+
decryptResponse = this.kmsClient.decrypt(decryptRequest);
111+
}
112+
113+
SecretKey cek = new SecretKeySpec(decryptResponse.plaintext().asByteArray(), "AES");
114+
return ContentCryptoProvider.decrypt(
115+
header, null, encryptedKey, iv, cipherText, authTag, cek, getJCAContext());
116+
}
117+
118+
private DecryptResponse decryptWithKeyAliases(Base64URL encryptedKey) {
119+
String[] keyAliases = {
120+
SESSION_DECRYPTION_KEY_PRIMARY_ALIAS,
121+
SESSION_DECRYPTION_KEY_SECONDARY_ALIAS,
122+
SESSION_DECRYPTION_KEY_PREVIOUS_ALIAS
123+
};
124+
125+
DecryptResponse decryptResponse = null;
126+
for (String alias : keyAliases) {
127+
try {
128+
decryptResponse = kmsClient.decrypt(buildDecryptRequest(alias, encryptedKey));
129+
LOGGER.info("Decryption successful with key alias: {}", alias);
130+
return decryptResponse;
131+
} catch (Exception e) {
132+
LOGGER.warn(
133+
"Failed to decrypt with key alias: {}. Error: {}", alias, e.getMessage());
134+
}
135+
}
136+
137+
eventProbe.counterMetric(ALL_ALIASES_UNAVAILABLE);
138+
139+
return decryptResponse;
140+
}
141+
142+
public boolean isKeyRotationEnabled() {
143+
return keyRotationEnabled;
144+
}
145+
146+
private DecryptRequest buildDecryptRequest(String keyAlias, Base64URL encryptedKey) {
147+
return DecryptRequest.builder()
148+
.ciphertextBlob(SdkBytes.fromByteArray(encryptedKey.decode()))
149+
.encryptionAlgorithm(RSAES_OAEP_SHA_256)
150+
.keyId("alias/" + keyAlias)
151+
.build();
152+
}
153+
154+
@Override
155+
public Set<JWEAlgorithm> supportedJWEAlgorithms() {
156+
return SUPPORTED_ALGORITHMS;
157+
}
158+
159+
@Override
160+
public Set<EncryptionMethod> supportedEncryptionMethods() {
161+
return SUPPORTED_ENCRYPTION_METHODS;
162+
}
163+
164+
@Override
165+
public JWEJCAContext getJCAContext() {
166+
return jcaContext;
167+
}
168+
}

src/main/java/uk/gov/di/ipv/cri/common/library/util/EventProbe.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@
1616
public class EventProbe {
1717
private static final String GOVUK_SIGNIN_JOURNEY_ID = "govuk_signin_journey_id";
1818
private static final Logger LOGGER = LogManager.getLogger();
19-
private static final MetricsLogger METRICS_LOGGER = MetricsUtils.metricsLogger();
19+
private final MetricsLogger metricsLogger;
20+
21+
public EventProbe() {
22+
this(MetricsUtils.metricsLogger());
23+
}
24+
25+
public EventProbe(MetricsLogger metricsLogger) {
26+
this.metricsLogger = metricsLogger;
27+
}
2028

2129
public EventProbe log(Level level, Throwable throwable) {
2230
LOGGER.log(level, throwable.getMessage(), throwable);
@@ -42,17 +50,17 @@ private void logErrorCause(Throwable throwable) {
4250
}
4351

4452
public EventProbe counterMetric(String key) {
45-
METRICS_LOGGER.putMetric(key, 1d);
53+
metricsLogger.putMetric(key, 1d);
4654
return this;
4755
}
4856

4957
public EventProbe counterMetric(String key, double value) {
50-
METRICS_LOGGER.putMetric(key, value);
58+
metricsLogger.putMetric(key, value);
5159
return this;
5260
}
5361

5462
public EventProbe counterMetric(String key, double value, Unit unit) {
55-
METRICS_LOGGER.putMetric(key, value, unit);
63+
metricsLogger.putMetric(key, value, unit);
5664
return this;
5765
}
5866

@@ -77,7 +85,7 @@ public void addDimensions(Map<String, String> dimensions) {
7785
if (dimensions != null) {
7886
DimensionSet dimensionSet = new DimensionSet();
7987
dimensions.forEach(dimensionSet::addDimension);
80-
METRICS_LOGGER.putDimensions(dimensionSet);
88+
metricsLogger.putDimensions(dimensionSet);
8189
}
8290
}
8391
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package uk.gov.di.ipv.cri.common.library.service;
2+
3+
import com.nimbusds.jose.EncryptionMethod;
4+
import com.nimbusds.jose.JOSEException;
5+
import com.nimbusds.jose.JWEAlgorithm;
6+
import com.nimbusds.jose.JWEHeader;
7+
import com.nimbusds.jose.util.Base64URL;
8+
import com.nimbusds.jwt.SignedJWT;
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.ExtendWith;
12+
import org.mockito.ArgumentCaptor;
13+
import org.mockito.Mock;
14+
import org.mockito.junit.jupiter.MockitoExtension;
15+
16+
import java.nio.charset.StandardCharsets;
17+
import java.text.ParseException;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
import static org.mockito.ArgumentMatchers.any;
22+
import static org.mockito.Mockito.verify;
23+
import static org.mockito.Mockito.when;
24+
25+
@ExtendWith(MockitoExtension.class)
26+
class JWTDecrypterTest {
27+
@Mock private KMSRSADecrypter mockDecrypter;
28+
private JWTDecrypter jwtDecrypter;
29+
30+
@BeforeEach
31+
void setup() {
32+
this.jwtDecrypter = new JWTDecrypter(mockDecrypter);
33+
}
34+
35+
@Test
36+
void shouldDecrypt() throws ParseException, JOSEException {
37+
String encryptedJWT =
38+
"eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.jfDZSCq6Z7Hu22uWaNEtDfFfv-RZot58oxhTAwNoGT3aMvWUiZBIzqm0b9f2xkxMBEky3oix9xC5_KRL2Xv-OO9DdTw7sfLMUs7BidEXWRIAq7PgiD1rdkQ5ElZHM1TPYoREXhJyqtXMgup8lD_B85m-xBOgaZQvuG_cxc0lNerLBgu1f23jcy0S8G3P8L-Cl056Kv6QV-WGFOQW0Vurwd_f432Ho1W1STYrSat22YNkX2_A0SJZGVcxF_wKKfNAUw4n7sVdYZOfl62x7Cz2Rt2HX36U6vLhI8ZLNGROCsNKI-LYJA2ET1_li150DMgMNlfYfwHrO3jFi_j1XcK_oA.esSJbN3jlduupMFy.cT7gnhBT0VT7jY5gEAsafuZi-o6BP8DI-aaH97mJ4e6q0E1pAgWkWAHc-qvmRWYHLUfbMlTOpH5AlQNhQ-ZWsfm40eM0sIV3OZCk4KcAbSoz4v-9aqleBTVhr_YhZqk_lZ9I9566SzLnOuPkWQr6J5F6F19Ol7Ob0j7-a2zHgXlxQizp1hjXiWAhJ0aFFRfP4hxcohn7h5EKeMw8ZT8jv1kqc0PwRoZOt83SgBcdlLcIz9LDPIUWuXXtw9Xi5FrfAc2SXFv4sv7BEo70-ICT9sC1jTpkMsqJlofqu5R3L2Kf51HFOJe2C1SRy_MQGID9FnQGgrDburfSpcmH_DPxdLS8SJ9X7LyyrPWzrdTwgUDdUCWmsoYbvgZQC1KhRiu7GjKLDU2uQgo0NSiaNIcyS6qllDXPqJUTkz0snmMUjcIN7ZTzA29ngxJh5OhI444qChQrB-2hU769giX00UEyqb--MpTWybGReoC0nF-BzaZrrQkWMB2vFWiDg5dUUD6778b4YvmryINCP5H4NteK8JHnIsqMMbY6wxtZFqVhsvVAR6thM9JBKJrN5nSMkKlwSAEpf2vbUyec2x_AZQ6d66lrneZe3VHWmHAo42d6if2P-yaL2vLrr9g73vr7CfU9WiTYTYtFOJ0aWodFwnSeZq-Bek1RXTNsEl4G8K3ved97W1YlEW4359V6OWpSCfFouDJv-yLxaedRvzXjcBH0Ssx6D8Njs4cOduQ-PE22mUcpHd5URsUsU19F59jgXpk.I48OP5ZO-bl9nqunO4VX6w";
39+
byte[] decryptedJWT =
40+
new SignedJWTBuilder().build().serialize().getBytes(StandardCharsets.UTF_8);
41+
when(mockDecrypter.decrypt(
42+
any(JWEHeader.class),
43+
any(Base64URL.class),
44+
any(Base64URL.class),
45+
any(Base64URL.class),
46+
any(Base64URL.class),
47+
any(byte[].class)))
48+
.thenReturn(decryptedJWT);
49+
50+
SignedJWT decryptedSignedJWT = this.jwtDecrypter.decrypt(encryptedJWT);
51+
52+
ArgumentCaptor<JWEHeader> headerArgumentCaptor = ArgumentCaptor.forClass(JWEHeader.class);
53+
verify(mockDecrypter)
54+
.decrypt(
55+
headerArgumentCaptor.capture(),
56+
any(Base64URL.class),
57+
any(Base64URL.class),
58+
any(Base64URL.class),
59+
any(Base64URL.class),
60+
any(byte[].class));
61+
JWEHeader actualHeader = headerArgumentCaptor.getValue();
62+
assertEquals(JWEAlgorithm.RSA_OAEP_256.getName(), actualHeader.getAlgorithm().getName());
63+
assertEquals(
64+
EncryptionMethod.A256GCM.getName(), actualHeader.getEncryptionMethod().getName());
65+
assertEquals("JWT", actualHeader.getContentType());
66+
assertNotNull(decryptedSignedJWT);
67+
}
68+
}

0 commit comments

Comments
 (0)