Skip to content

Commit 1efd6ac

Browse files
committed
test: Cover PemSslContext error paths
1 parent 088d972 commit 1efd6ac

3 files changed

Lines changed: 77 additions & 3 deletions

File tree

docs/superpowers/plans/2026-05-21-https-support.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ SKIP=commitlint git commit -m "feat: Support EC private keys in PemSslContext"
414414

415415
The error messages are already produced by Task 2/3's implementation. This task just asserts them.
416416

417-
- [ ] **Step 1: Add the failing missing-file test**
417+
- [x] **Step 1: Add the failing missing-file test**
418418

419419
Append to `PemSslContextTest`:
420420

@@ -467,7 +467,7 @@ Add the import at the top of the file:
467467
import static org.assertj.core.api.Assertions.assertThatThrownBy;
468468
```
469469

470-
- [ ] **Step 2: Run, confirm all five pass**
470+
- [x] **Step 2: Run, confirm all five pass**
471471

472472
```bash
473473
mvn test -Dtest=PemSslContextTest
@@ -477,7 +477,7 @@ Expected: 7 tests pass (2 happy + 5 negative).
477477

478478
If the mismatched-key test fails because the key parsed cleanly but the cert→key binding wasn't detected, that's a real bug in `buildSslContext``KeyManagerFactory.init` is what surfaces the mismatch, and it does. Verify by reading the actual message: it should contain "do not match" via the chained cause. If `KeyManagerFactory` accepts the pair, the test will fail; do NOT relax the assertion — debug instead.
479479

480-
- [ ] **Step 3: Commit**
480+
- [x] **Step 3: Commit**
481481

482482
```bash
483483
git add src/test/java/com/retailsvc/http/internal/PemSslContextTest.java

src/main/java/com/retailsvc/http/internal/PemSslContext.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.security.KeyFactory;
99
import java.security.KeyStore;
1010
import java.security.PrivateKey;
11+
import java.security.Signature;
1112
import java.security.cert.Certificate;
1213
import java.security.cert.CertificateFactory;
1314
import java.security.spec.InvalidKeySpecException;
@@ -79,6 +80,7 @@ private static PrivateKey readPrivateKey(Path path) {
7980
}
8081

8182
private static SSLContext buildSslContext(Certificate[] chain, PrivateKey key) {
83+
verifyKeyMatchesCert(key, chain[0]);
8284
try {
8385
KeyStore ks = KeyStore.getInstance("PKCS12");
8486
ks.load(null, null);
@@ -92,4 +94,35 @@ private static SSLContext buildSslContext(Certificate[] chain, PrivateKey key) {
9294
throw new IllegalStateException("TLS certificate and private key do not match", e);
9395
}
9496
}
97+
98+
private static void verifyKeyMatchesCert(PrivateKey key, Certificate cert) {
99+
String algorithm =
100+
switch (key.getAlgorithm()) {
101+
case "RSA" -> "SHA256withRSA";
102+
case "EC" -> "SHA256withECDSA";
103+
default ->
104+
throw new IllegalStateException(
105+
"Unsupported TLS private key algorithm: " + key.getAlgorithm());
106+
};
107+
byte[] probe = {1, 2, 3, 4, 5, 6, 7, 8};
108+
byte[] signature;
109+
try {
110+
Signature signer = Signature.getInstance(algorithm);
111+
signer.initSign(key);
112+
signer.update(probe);
113+
signature = signer.sign();
114+
} catch (GeneralSecurityException e) {
115+
throw new IllegalStateException("TLS certificate and private key do not match", e);
116+
}
117+
try {
118+
Signature verifier = Signature.getInstance(algorithm);
119+
verifier.initVerify(cert.getPublicKey());
120+
verifier.update(probe);
121+
if (!verifier.verify(signature)) {
122+
throw new IllegalStateException("TLS certificate and private key do not match");
123+
}
124+
} catch (GeneralSecurityException e) {
125+
throw new IllegalStateException("TLS certificate and private key do not match", e);
126+
}
127+
}
95128
}

src/test/java/com/retailsvc/http/internal/PemSslContextTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.retailsvc.http.internal;
22

33
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
45

56
import java.nio.file.Path;
67
import javax.net.ssl.SSLContext;
@@ -12,6 +13,9 @@ class PemSslContextTest {
1213
private static final Path RSA_KEY = Path.of("src/test/resources/tls/rsa-key.pem");
1314
private static final Path EC_CERT = Path.of("src/test/resources/tls/ec-cert.pem");
1415
private static final Path EC_KEY = Path.of("src/test/resources/tls/ec-key.pem");
16+
private static final Path MISMATCHED_KEY = Path.of("src/test/resources/tls/mismatched-key.pem");
17+
private static final Path GARBAGE = Path.of("src/test/resources/tls/garbage.pem");
18+
private static final Path MISSING = Path.of("src/test/resources/tls/does-not-exist.pem");
1519

1620
@Test
1721
void loadsRsaPemPair() throws Exception {
@@ -29,4 +33,41 @@ void loadsEcPemPair() throws Exception {
2933
assertThat(context).isNotNull();
3034
assertThat(context.getServerSocketFactory()).isNotNull();
3135
}
36+
37+
@Test
38+
void rejectsMissingCertFile() {
39+
assertThatThrownBy(() -> PemSslContext.load(MISSING, RSA_KEY))
40+
.isInstanceOf(IllegalStateException.class)
41+
.hasMessageContaining("Cannot read TLS certificate chain")
42+
.hasMessageContaining("does-not-exist.pem");
43+
}
44+
45+
@Test
46+
void rejectsMissingKeyFile() {
47+
assertThatThrownBy(() -> PemSslContext.load(RSA_CERT, MISSING))
48+
.isInstanceOf(IllegalStateException.class)
49+
.hasMessageContaining("Cannot read TLS private key")
50+
.hasMessageContaining("does-not-exist.pem");
51+
}
52+
53+
@Test
54+
void rejectsGarbageCertPem() {
55+
assertThatThrownBy(() -> PemSslContext.load(GARBAGE, RSA_KEY))
56+
.isInstanceOf(IllegalStateException.class)
57+
.hasMessageContaining("Failed to parse TLS certificate chain");
58+
}
59+
60+
@Test
61+
void rejectsGarbageKeyPem() {
62+
assertThatThrownBy(() -> PemSslContext.load(RSA_CERT, GARBAGE))
63+
.isInstanceOf(IllegalStateException.class)
64+
.hasMessageContaining("Failed to parse TLS private key");
65+
}
66+
67+
@Test
68+
void rejectsMismatchedCertAndKey() {
69+
assertThatThrownBy(() -> PemSslContext.load(RSA_CERT, MISMATCHED_KEY))
70+
.isInstanceOf(IllegalStateException.class)
71+
.hasMessageContaining("do not match");
72+
}
3273
}

0 commit comments

Comments
 (0)