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 {