diff --git a/SimpleAPI/pom.xml b/SimpleAPI/pom.xml
index d768c4b..6aba52e 100644
--- a/SimpleAPI/pom.xml
+++ b/SimpleAPI/pom.xml
@@ -16,7 +16,6 @@
21
21
21
- 1.85
${project.name}
@@ -255,16 +254,6 @@
-
- org.bouncycastle
- bcpkix-jdk18on
- ${bouncycastle.version}
-
-
- org.bouncycastle
- bcprov-jdk18on
- ${bouncycastle.version}
-
org.spigotmc
spigot-api
diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/HttpClientCredentialStore.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/HttpClientCredentialStore.java
index 1ddd67f..ec8b7e7 100644
--- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/HttpClientCredentialStore.java
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/HttpClientCredentialStore.java
@@ -451,7 +451,7 @@ private static boolean matchesProfile(ClientCredential credential, HttpClientPro
credential.certificate().verify(credential.caCertificate().getPublicKey());
java.util.List usage = credential.certificate().getExtendedKeyUsage();
boolean[] keyUsage = credential.certificate().getKeyUsage();
- if (usage == null || !usage.contains(org.bouncycastle.asn1.x509.KeyPurposeId.id_kp_clientAuth.getId())
+ if (usage == null || !usage.contains("1.3.6.1.5.5.7.3.2")
|| keyUsage == null || !keyUsage[0]) return false;
String expected = "urn:votingplugin:http-backend:" + profile.serverId();
var names = credential.certificate().getSubjectAlternativeNames();
diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/HttpTlsIdentity.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/HttpTlsIdentity.java
index 7550430..1fc5927 100644
--- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/HttpTlsIdentity.java
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/HttpTlsIdentity.java
@@ -1,7 +1,6 @@
package com.bencodez.simpleapi.servercomm.http;
import java.io.IOException;
-import java.math.BigInteger;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
@@ -15,14 +14,12 @@
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Principal;
-import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.Clock;
import java.time.Duration;
import java.net.InetAddress;
-import java.util.Date;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -33,22 +30,6 @@
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
-import org.bouncycastle.asn1.x500.X500Name;
-import org.bouncycastle.asn1.x509.BasicConstraints;
-import org.bouncycastle.asn1.x509.Extension;
-import org.bouncycastle.asn1.x509.GeneralName;
-import org.bouncycastle.asn1.x509.GeneralNames;
-import org.bouncycastle.asn1.x509.KeyUsage;
-import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
-import org.bouncycastle.asn1.x509.KeyPurposeId;
-import org.bouncycastle.cert.X509CertificateHolder;
-import org.bouncycastle.cert.X509v3CertificateBuilder;
-import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
-import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
-import org.bouncycastle.util.IPAddress;
import com.bencodez.simpleapi.file.DurableFiles;
import com.bencodez.simpleapi.file.PrivateFilePermissions;
@@ -146,7 +127,6 @@ static HttpTlsIdentity loadOrCreate(Path directory, String advertisedHost, Clock
throw new IOException("HTTP TLS identity files are invalid");
boolean caRenewed = needsCaRenewal(caCertificate, clock);
if (caRenewed) {
- ensureBouncyCastle();
KeyPair caPair = new KeyPair(caCertificate.getPublicKey(), caKey);
caCertificate = certificate("CN=VotingPlugin HTTP private CA", caPair, null, null,
CertificateRole.CA, null, clock.instant());
@@ -156,7 +136,6 @@ static HttpTlsIdentity loadOrCreate(Path directory, String advertisedHost, Clock
writeStore(caFile, ca, password);
}
if (caRenewed || !hasServerName(serverCertificate, advertisedHost) || needsRenewal(serverCertificate, clock)) {
- ensureBouncyCastle();
KeyPair serverPair = keyPair();
serverCertificate = certificate("CN=" + certificateName(advertisedHost), serverPair, caCertificate, caKey,
CertificateRole.SERVER, advertisedHost, clock.instant());
@@ -177,7 +156,6 @@ static HttpTlsIdentity loadOrCreate(Path directory, String advertisedHost, Clock
if (persistentTransportState)
throw new IOException("HTTP TLS identity files are missing");
if (!initializing) writeInitializationMarker(initializingFile);
- ensureBouncyCastle();
char[] password = HttpTransportSecrets.randomToken().toCharArray();
try {
KeyPair caPair = keyPair();
@@ -265,7 +243,6 @@ private synchronized void renewIdentityIfNeeded() throws Exception {
serverCertificate = persistedServer;
renewCa = needsCaRenewal(caCertificate, clock);
if (!renewCa && !needsRenewal(serverCertificate, clock)) return;
- ensureBouncyCastle();
X509Certificate replacementCa = caCertificate;
if (renewCa) {
KeyPair caPair = new KeyPair(caCertificate.getPublicKey(), caKey);
@@ -297,7 +274,6 @@ public IssuedClientCertificate issueClientCertificate(String serverId) throws Ex
IssuedClientCertificate issueClientCertificate(String serverId, Instant issuedAt) throws Exception {
serverId = canonicalServerId(serverId);
if (issuedAt == null) throw new IllegalArgumentException("Certificate issuance time is required");
- ensureBouncyCastle();
KeyPair pair = keyPair();
X509Certificate certificate = certificate("CN=" + serverId, pair, caCertificate, caKey, CertificateRole.CLIENT,
"urn:votingplugin:http-backend:" + serverId, issuedAt);
@@ -334,12 +310,12 @@ public boolean validClientCertificate(String expectedServerId, X509Certificate c
try {
List usage = certificate.getExtendedKeyUsage();
boolean[] keyUsage = certificate.getKeyUsage();
- if (usage == null || !usage.contains(KeyPurposeId.id_kp_clientAuth.getId()) || keyUsage == null || !keyUsage[0]) return false;
+ if (usage == null || !usage.contains("1.3.6.1.5.5.7.3.2") || keyUsage == null || !keyUsage[0]) return false;
String expectedUri = "urn:votingplugin:http-backend:" + canonicalServerId(expectedServerId);
Collection> names = certificate.getSubjectAlternativeNames();
if (names == null) return false;
for (List> name : names) {
- if (name.size() == 2 && Integer.valueOf(GeneralName.uniformResourceIdentifier).equals(name.get(0))
+ if (name.size() == 2 && Integer.valueOf(6).equals(name.get(0))
&& expectedUri.equals(name.get(1))) return true;
}
return false;
@@ -363,30 +339,8 @@ private static KeyPair keyPair() throws Exception {
private static X509Certificate certificate(String subject, KeyPair subjectKey, X509Certificate issuer, PrivateKey issuerKey,
CertificateRole role, String subjectAlternativeName, Instant now) throws Exception {
- X500Name issuerName = issuer == null ? new X500Name(subject) : new X500Name(issuer.getSubjectX500Principal().getName());
- X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuerName,
- new BigInteger(160, new java.security.SecureRandom()).setBit(159), Date.from(now.minusSeconds(300)),
- Date.from(now.plusSeconds(role == CertificateRole.CA ? 315360000L : 31536000L)), new X500Name(subject), subjectKey.getPublic());
- builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(role == CertificateRole.CA));
- builder.addExtension(Extension.keyUsage, true, new KeyUsage(role == CertificateRole.CA ? KeyUsage.keyCertSign | KeyUsage.cRLSign
- : KeyUsage.digitalSignature));
- if (role == CertificateRole.SERVER) builder.addExtension(Extension.extendedKeyUsage, false,
- new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth));
- if (role == CertificateRole.CLIENT) builder.addExtension(Extension.extendedKeyUsage, false,
- new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth));
- if (role == CertificateRole.SERVER && subjectAlternativeName != null) {
- GeneralName name;
- if (subjectAlternativeName.matches("(?:\\d{1,3}\\.){3}\\d{1,3}") || subjectAlternativeName.indexOf(':') >= 0)
- name = new GeneralName(GeneralName.iPAddress, subjectAlternativeName);
- else name = new GeneralName(GeneralName.dNSName, subjectAlternativeName);
- builder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(name));
- }
- if (role == CertificateRole.CLIENT) builder.addExtension(Extension.subjectAlternativeName, false,
- new GeneralNames(new GeneralName(GeneralName.uniformResourceIdentifier, subjectAlternativeName)));
- ContentSigner signer = new JcaContentSignerBuilder("SHA256withECDSA").setProvider("BC")
- .build(issuerKey == null ? subjectKey.getPrivate() : issuerKey);
- X509CertificateHolder holder = builder.build(signer);
- return new JcaX509CertificateConverter().setProvider("BC").getCertificate(holder);
+ return JdkX509CertificateGenerator.create(subject, subjectKey, issuer, issuerKey, role == CertificateRole.CA,
+ role == CertificateRole.SERVER, subjectAlternativeName, now);
}
static boolean needsRenewal(X509Certificate certificate, Clock clock) {
@@ -397,9 +351,6 @@ static boolean needsCaRenewal(X509Certificate certificate, Clock clock) {
return certificate == null || !certificate.getNotAfter().toInstant().isAfter(clock.instant().plus(CA_RENEW_BEFORE));
}
- private static void ensureBouncyCastle() {
- if (Security.getProvider("BC") == null) Security.addProvider(new BouncyCastleProvider());
- }
private static String certificateName(String host) {
return host.replaceAll("[^A-Za-z0-9 ._-]", "_");
@@ -411,8 +362,8 @@ private static boolean hasServerName(X509Certificate certificate, String adverti
if (names == null) return false;
for (List> name : names) {
if (name.size() != 2 || !(name.get(1) instanceof String value)) continue;
- if (Integer.valueOf(GeneralName.dNSName).equals(name.get(0)) && advertisedHost.equalsIgnoreCase(value)) return true;
- if (Integer.valueOf(GeneralName.iPAddress).equals(name.get(0)) && sameIpAddress(advertisedHost, value)) return true;
+ if (Integer.valueOf(2).equals(name.get(0)) && advertisedHost.equalsIgnoreCase(value)) return true;
+ if (Integer.valueOf(7).equals(name.get(0)) && sameIpAddress(advertisedHost, value)) return true;
}
return false;
} catch (Exception failure) { return false; }
@@ -426,7 +377,7 @@ private static boolean sameIpAddress(String first, String second) {
}
private static boolean isIpLiteral(String value) {
- return IPAddress.isValid(value);
+ return JdkX509CertificateGenerator.isIpLiteral(value);
}
private static Path safe(Path file) throws IOException {
diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/JdkX509CertificateGenerator.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/JdkX509CertificateGenerator.java
new file mode 100644
index 0000000..f359e0b
--- /dev/null
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/JdkX509CertificateGenerator.java
@@ -0,0 +1,160 @@
+package com.bencodez.simpleapi.servercomm.http;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javax.security.auth.x500.X500Principal;
+
+/** Creates the private HTTP transport PKI using only standard JCA primitives. */
+final class JdkX509CertificateGenerator {
+ private static final byte[] ECDSA_SHA256 = sequence(oid("1.2.840.10045.4.3.2"));
+ private static final DateTimeFormatter UTC_TIME = DateTimeFormatter.ofPattern("yyMMddHHmmss'Z'")
+ .withZone(ZoneOffset.UTC);
+ private static final DateTimeFormatter GENERALIZED_TIME = DateTimeFormatter.ofPattern("yyyyMMddHHmmss'Z'")
+ .withZone(ZoneOffset.UTC);
+
+ private JdkX509CertificateGenerator() { }
+
+ static X509Certificate create(String subject, KeyPair subjectKey, X509Certificate issuer, PrivateKey issuerKey,
+ boolean certificateAuthority, boolean server, String subjectAlternativeName, Instant now) throws Exception {
+ byte[] issuerName = issuer == null ? new X500Principal(subject).getEncoded()
+ : issuer.getSubjectX500Principal().getEncoded();
+ List extensions = new ArrayList<>();
+ extensions.add(extension("2.5.29.19", true,
+ certificateAuthority ? sequence(bool(true)) : sequence()));
+ extensions.add(extension("2.5.29.15", true,
+ certificateAuthority ? bitString(1, new byte[] { 0x06 }) : bitString(7, new byte[] { (byte) 0x80 })));
+ if (!certificateAuthority) extensions.add(extension("2.5.29.37", false,
+ sequence(oid(server ? "1.3.6.1.5.5.7.3.1" : "1.3.6.1.5.5.7.3.2"))));
+ if (subjectAlternativeName != null) {
+ byte tag;
+ byte[] value;
+ byte[] ipAddress = server ? parseIpLiteral(subjectAlternativeName) : null;
+ if (ipAddress != null) {
+ tag = (byte) 0x87;
+ value = ipAddress;
+ } else {
+ if (server && (subjectAlternativeName.indexOf(':') >= 0
+ || subjectAlternativeName.matches("(?:\\d{1,3}\\.){3}\\d{1,3}")))
+ throw new IllegalArgumentException("Advertised HTTPS host is an invalid IP address");
+ if (!StandardCharsets.US_ASCII.newEncoder().canEncode(subjectAlternativeName))
+ throw new IllegalArgumentException("Certificate alternative name must be ASCII");
+ tag = server ? (byte) 0x82 : (byte) 0x86;
+ value = subjectAlternativeName.getBytes(StandardCharsets.US_ASCII);
+ }
+ extensions.add(extension("2.5.29.17", false, sequence(tagged(tag, value))));
+ }
+ byte[] tbs = sequence(
+ tagged((byte) 0xa0, integer(BigInteger.valueOf(2))),
+ integer(new BigInteger(160, new java.security.SecureRandom()).setBit(159)), ECDSA_SHA256, issuerName,
+ sequence(time(now.minusSeconds(300)),
+ time(now.plusSeconds(certificateAuthority ? 315360000L : 31536000L))),
+ new X500Principal(subject).getEncoded(), subjectKey.getPublic().getEncoded(),
+ tagged((byte) 0xa3, sequence(extensions.toArray(byte[][]::new))));
+ Signature signer = Signature.getInstance("SHA256withECDSA");
+ signer.initSign(issuerKey == null ? subjectKey.getPrivate() : issuerKey);
+ signer.update(tbs);
+ byte[] encoded = sequence(tbs, ECDSA_SHA256, bitString(0, signer.sign()));
+ try (ByteArrayInputStream input = new ByteArrayInputStream(encoded)) {
+ return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(input);
+ }
+ }
+
+ static boolean isIpLiteral(String value) {
+ return parseIpLiteral(value) != null;
+ }
+
+ private static byte[] parseIpLiteral(String value) {
+ if (value == null || value.isEmpty()) return null;
+ if (value.indexOf(':') >= 0) {
+ if (!value.matches("[0-9A-Fa-f:.]+")) return null;
+ try {
+ byte[] parsed = InetAddress.getByName(value).getAddress();
+ if (parsed.length == 16) return parsed;
+ if (parsed.length == 4) {
+ byte[] mapped = new byte[16];
+ mapped[10] = (byte) 0xff;
+ mapped[11] = (byte) 0xff;
+ System.arraycopy(parsed, 0, mapped, 12, parsed.length);
+ return mapped;
+ }
+ return null;
+ } catch (Exception invalid) { return null; }
+ }
+ String[] parts = value.split("\\.", -1);
+ if (parts.length != 4) return null;
+ for (String part : parts) {
+ if (part.isEmpty() || part.length() > 3 || !part.chars().allMatch(Character::isDigit)) return null;
+ try { if (Integer.parseInt(part) > 255) return null; }
+ catch (NumberFormatException invalid) { return null; }
+ }
+ try { return InetAddress.getByName(value).getAddress(); }
+ catch (Exception invalid) { return null; }
+ }
+
+ private static byte[] extension(String id, boolean critical, byte[] value) {
+ return critical ? sequence(oid(id), bool(true), octetString(value)) : sequence(oid(id), octetString(value));
+ }
+ private static byte[] time(Instant value) {
+ int year = value.atZone(ZoneOffset.UTC).getYear();
+ String encoded = year >= 1950 && year <= 2049 ? UTC_TIME.format(value) : GENERALIZED_TIME.format(value);
+ return tagged(year >= 1950 && year <= 2049 ? (byte) 0x17 : (byte) 0x18,
+ encoded.getBytes(StandardCharsets.US_ASCII));
+ }
+ private static byte[] oid(String value) {
+ String[] components = value.split("\\.");
+ ByteArrayOutputStream body = new ByteArrayOutputStream();
+ writeBase128(body, Long.parseLong(components[0]) * 40L + Long.parseLong(components[1]));
+ for (int index = 2; index < components.length; index++) writeBase128(body, Long.parseLong(components[index]));
+ return tagged((byte) 0x06, body.toByteArray());
+ }
+ private static void writeBase128(ByteArrayOutputStream output, long value) {
+ byte[] encoded = new byte[10];
+ int position = encoded.length;
+ encoded[--position] = (byte) (value & 0x7f);
+ while ((value >>>= 7) != 0) encoded[--position] = (byte) ((value & 0x7f) | 0x80);
+ output.writeBytes(Arrays.copyOfRange(encoded, position, encoded.length));
+ }
+ private static byte[] integer(BigInteger value) { return tagged((byte) 0x02, value.toByteArray()); }
+ private static byte[] bool(boolean value) { return tagged((byte) 0x01, new byte[] { value ? (byte) 0xff : 0 }); }
+ private static byte[] octetString(byte[] value) { return tagged((byte) 0x04, value); }
+ private static byte[] bitString(int unusedBits, byte[] value) {
+ byte[] body = new byte[value.length + 1];
+ body[0] = (byte) unusedBits;
+ System.arraycopy(value, 0, body, 1, value.length);
+ return tagged((byte) 0x03, body);
+ }
+ private static byte[] sequence(byte[]... values) { return tagged((byte) 0x30, concatenate(values)); }
+ private static byte[] tagged(byte tag, byte[] value) {
+ ByteArrayOutputStream output = new ByteArrayOutputStream(value.length + 6);
+ output.write(tag);
+ writeLength(output, value.length);
+ output.writeBytes(value);
+ return output.toByteArray();
+ }
+ private static void writeLength(ByteArrayOutputStream output, int length) {
+ if (length < 128) { output.write(length); return; }
+ int bytes = 0;
+ for (int remaining = length; remaining != 0; remaining >>>= 8) bytes++;
+ output.write(0x80 | bytes);
+ for (int shift = (bytes - 1) * 8; shift >= 0; shift -= 8) output.write(length >>> shift);
+ }
+ private static byte[] concatenate(byte[]... values) {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ for (byte[] value : values) output.writeBytes(value);
+ return output.toByteArray();
+ }
+}
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTransportSecurityTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTransportSecurityTest.java
index e86dc80..b1ee09b 100644
--- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTransportSecurityTest.java
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTransportSecurityTest.java
@@ -58,6 +58,27 @@ void connectionCodeRejectsExplicitZeroPort() {
@TempDir Path directory;
+ @Test
+ void tlsIdentityRejectsInvalidIpAndNonAsciiAlternativeNames() {
+ assertThrows(IllegalArgumentException.class,
+ () -> HttpTlsIdentity.loadOrCreate(directory.resolve("invalid-ip"), "999.1.1.1"));
+ assertThrows(IllegalArgumentException.class,
+ () -> HttpTlsIdentity.loadOrCreate(directory.resolve("invalid-ipv6"), "not:an:ip"));
+ assertThrows(IllegalArgumentException.class,
+ () -> HttpTlsIdentity.loadOrCreate(directory.resolve("non-ascii"), "tést.example"));
+ }
+
+ @Test
+ void tlsIdentityAcceptsIpv4MappedIpv6AlternativeNames() {
+ assertTrue(JdkX509CertificateGenerator.isIpLiteral("::ffff:192.0.2.1"));
+ assertTrue(JdkX509CertificateGenerator.isIpLiteral("::ffff:c000:201"));
+ assertTrue(JdkX509CertificateGenerator.isIpLiteral("0:0:0:0:0:ffff:c000:201"));
+ assertDoesNotThrow(() -> HttpTlsIdentity.loadOrCreate(directory.resolve("mapped-ipv6-dotted"),
+ "::ffff:192.0.2.1"));
+ assertDoesNotThrow(() -> HttpTlsIdentity.loadOrCreate(directory.resolve("mapped-ipv6-hex"),
+ "::ffff:c000:201"));
+ }
+
@Test
void backendResponseReaderRejectsBodiesBeyondTheWireLimit() throws Exception {
byte[] maximum = new byte[HttpTransportProtocol.MAX_BODY_BYTES];
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/PackagedTlsSmoke.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/PackagedTlsSmoke.java
index 4e44cca..ea1ff6b 100644
--- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/PackagedTlsSmoke.java
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/PackagedTlsSmoke.java
@@ -6,7 +6,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
-import java.security.Security;
import java.security.cert.X509Certificate;
import java.time.Clock;
import java.time.Duration;
@@ -32,8 +31,6 @@ public static void main(String[] args) throws Exception {
Path full = Path.of(args[1]);
requireFromJar(HttpTlsIdentity.class, full);
HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(state.resolve("trusted"), "127.0.0.1");
- require(Security.getProvider("BC") != null, "BC provider was not registered");
- requireFromJar(Security.getProvider("BC").getClass(), full);
X509Certificate ca = identity.caCertificate();
X509Certificate server = identity.serverCertificate();
ca.verify(ca.getPublicKey());
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/packaging/FullArtifactTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/packaging/FullArtifactTest.java
index d978742..ba1c179 100644
--- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/packaging/FullArtifactTest.java
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/packaging/FullArtifactTest.java
@@ -6,14 +6,10 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
-import org.bouncycastle.asn1.cms.ContentInfo;
-import org.bouncycastle.cert.X509CertificateHolder;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -23,43 +19,16 @@
public class FullArtifactTest {
@TempDir Path temporary;
- @Test void retainsBaseCryptoClassesWithoutUnusableVersionedPayload() throws Exception {
+ @Test void omitsExternalCryptoProvider() throws Exception {
Path full = fullJar();
- long removedEntries = 0;
- long removedCompressedBytes = 0;
- long retainedEntries = 0;
try (JarFile output = new JarFile(full.toFile())) {
assertNotNull(output.getManifest(), "Full artifact must have a manifest");
- assertFalse(Boolean.parseBoolean(output.getManifest().getMainAttributes()
- .getValue(Attributes.Name.MULTI_RELEASE)),
- "Revisit the BC filter before making the full artifact multi-release");
String classPath = output.getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
assertTrue(classPath == null || classPath.isBlank(), "Smoke test must not load external manifest dependencies");
- assertFalse(output.stream().anyMatch(entry -> entry.getName().startsWith("META-INF/versions/")),
- "A non-multi-release artifact must not bundle unreachable versioned implementations");
-
- // Resolve all three original libraries from Maven, without a pinned version or ~/.m2 path.
- for (Class> anchor : List.of(BouncyCastleProvider.class, X509CertificateHolder.class, ContentInfo.class)) {
- Path source = Path.of(anchor.getProtectionDomain().getCodeSource().getLocation().toURI());
- assertFalse(Files.isSameFile(source, full), "Expected Maven's original dependency for comparison");
- try (JarFile dependency = new JarFile(source.toFile())) {
- for (var entry : dependency.stream().filter(entry -> !entry.isDirectory()).toList()) {
- if (entry.getName().startsWith("META-INF/versions/")) {
- removedEntries++;
- removedCompressedBytes += entry.getCompressedSize();
- } else if (entry.getName().startsWith("org/bouncycastle/")) {
- assertNotNull(output.getEntry(entry.getName()), "Lost base dependency entry: " + entry.getName());
- retainedEntries++;
- }
- }
- }
- }
+ assertFalse(output.stream().anyMatch(entry -> entry.getName().startsWith("org/bouncycastle/")),
+ "The JDK-only TLS implementation must not package Bouncy Castle");
}
- assertTrue(retainedEntries > 0, "No base crypto entries were checked");
- // This is input ZIP payload, not an invented before/after output-JAR size.
- System.out.printf("Full artifact: %,d bytes; retained %,d base BC entries; omitted %,d versioned entries "
- + "(%,d compressed bytes in upstream dependency JARs)%n",
- Files.size(full), retainedEntries, removedEntries, removedCompressedBytes);
+ System.out.printf("Full artifact: %,d bytes; no external crypto provider packaged%n", Files.size(full));
}
@Test void packagedTlsWorksWithoutMavenDependencies() throws Exception {