version 1.0
Some checks failed
CI / build (push) Failing after 1m21s

This commit is contained in:
2024-12-23 16:00:14 +08:00
parent 13f7ecc88a
commit 9394068fe2
29 changed files with 1940 additions and 432 deletions

View File

@@ -0,0 +1,7 @@
module net.woggioni.gbcs.test {
requires org.junit.jupiter.api;
requires net.woggioni.gbcs;
requires kotlin.stdlib;
requires java.xml;
requires java.naming;
}

View File

@@ -0,0 +1,227 @@
package net.woggioni.gbcs.utils;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectAltPublicKeyInfo;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.net.InetAddress;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
public class CertificateUtils {
public record X509Credentials(
KeyPair keyPair,
X509Certificate certificate
){ }
public static class CertificateAuthority {
private final PrivateKey privateKey;
private final X509Certificate certificate;
public CertificateAuthority(PrivateKey privateKey, X509Certificate certificate) {
this.privateKey = privateKey;
this.certificate = certificate;
}
public PrivateKey getPrivateKey() { return privateKey; }
public X509Certificate getCertificate() { return certificate; }
}
/**
* Creates a new Certificate Authority (CA)
* @param commonName The CA's common name
* @param validityDays How long the CA should be valid for
* @return The generated CA containing both private key and certificate
*/
public static X509Credentials createCertificateAuthority(String commonName, int validityDays)
throws Exception {
// Generate key pair
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(4096);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// Prepare certificate data
X500Name issuerName = new X500Name("CN=" + commonName);
BigInteger serialNumber = new BigInteger(160, new SecureRandom());
Instant now = Instant.now();
Date startDate = Date.from(now);
Date endDate = Date.from(now.plus(validityDays, ChronoUnit.DAYS));
// Create certificate builder
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuerName,
serialNumber,
startDate,
endDate,
issuerName,
keyPair.getPublic()
);
// Add CA extensions
certBuilder.addExtension(
Extension.basicConstraints,
true,
new BasicConstraints(true)
);
certBuilder.addExtension(
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)
);
// Sign the certificate
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.build(keyPair.getPrivate());
X509Certificate cert = new JcaX509CertificateConverter()
.getCertificate(certBuilder.build(signer));
return new X509Credentials(keyPair, cert);
}
/**
* Creates a server certificate signed by the CA
* @param ca The Certificate Authority to sign with
* @param subjectName The server's common name
* @param validityDays How long the certificate should be valid for
* @return KeyPair containing the server's private key and certificate
*/
public static X509Credentials createServerCertificate(X509Credentials ca, X500Name subjectName, int validityDays)
throws Exception {
// Generate server key pair
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair serverKeyPair = keyPairGenerator.generateKeyPair();
// Prepare certificate data
X500Name issuerName = new X500Name(ca.certificate().getSubjectX500Principal().getName());
BigInteger serialNumber = new BigInteger(160, new SecureRandom());
Instant now = Instant.now();
Date startDate = Date.from(now);
Date endDate = Date.from(now.plus(validityDays, ChronoUnit.DAYS));
// Create certificate builder
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuerName,
serialNumber,
startDate,
endDate,
subjectName,
serverKeyPair.getPublic()
);
// Add server certificate extensions
certBuilder.addExtension(
Extension.basicConstraints,
true,
new BasicConstraints(false)
);
certBuilder.addExtension(
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment)
);
certBuilder.addExtension(
Extension.extendedKeyUsage,
true,
new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth})
);
GeneralNames subjectAltNames = GeneralNames.getInstance(
new DERSequence(
new GeneralName[] {
new GeneralName(GeneralName.iPAddress, "127.0.0.1")
}
)
);
certBuilder.addExtension(
Extension.subjectAlternativeName,
true,
subjectAltNames
);
// Sign the certificate
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.build(ca.keyPair().getPrivate());
X509Certificate cert = new JcaX509CertificateConverter()
.getCertificate(certBuilder.build(signer));
return new X509Credentials(serverKeyPair, cert);
}
/**
* Creates a client certificate signed by the CA
* @param ca The Certificate Authority to sign with
* @param subjectName The client's common name
* @param validityDays How long the certificate should be valid for
* @return KeyPair containing the client's private key and certificate
*/
public static X509Credentials createClientCertificate(X509Credentials ca, X500Name subjectName, int validityDays)
throws Exception {
// Generate client key pair
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair clientKeyPair = keyPairGenerator.generateKeyPair();
// Prepare certificate data
X500Name issuerName = new X500Name(ca.certificate().getSubjectX500Principal().getName());
BigInteger serialNumber = new BigInteger(160, new SecureRandom());
Instant now = Instant.now();
Date startDate = Date.from(now);
Date endDate = Date.from(now.plus(validityDays, ChronoUnit.DAYS));
// Create certificate builder
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuerName,
serialNumber,
startDate,
endDate,
subjectName,
clientKeyPair.getPublic()
);
// Add client certificate extensions
certBuilder.addExtension(
Extension.basicConstraints,
true,
new BasicConstraints(false)
);
certBuilder.addExtension(
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.digitalSignature)
);
certBuilder.addExtension(
Extension.extendedKeyUsage,
true,
new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth})
);
// Sign the certificate
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.build(ca.keyPair().getPrivate());
X509Certificate cert = new JcaX509CertificateConverter()
.getCertificate(certBuilder.build(signer));
return new X509Credentials(clientKeyPair, cert);
}
}

View File

@@ -1,35 +0,0 @@
package net.woggioni.gbcs
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.cert.CertPathValidator
import java.security.cert.CertificateFactory
import java.security.cert.PKIXParameters
import org.junit.jupiter.api.Test
class CertificateValidationTest {
@Test
fun test() {
val keystore = KeyStore.getInstance("PKCS12")
val keystorePath = Path.of("/home/woggioni/ssl/woggioni@f6aa5663ef26.pfx")
Files.newInputStream(keystorePath).use {
keystore.load(it, System.getenv("KEYPASS").toCharArray())
}
val pkix = CertPathValidator.getInstance("PKIX")
val trustStore = KeyStore.getInstance("PKCS12")
val trustStorePath = Path.of("/home/woggioni/ssl/truststore.pfx")
Files.newInputStream(trustStorePath).use {
trustStore.load(it, "123456".toCharArray())
}
val certificateFactory = CertificateFactory.getInstance("X.509")
val cert = keystore.getCertificateChain("woggioni@f6aa5663ef26").toList()
.let(certificateFactory::generateCertPath)
val params = PKIXParameters(trustStore)
params.isRevocationEnabled = false
pkix.validate(cert, params)
}
}

View File

@@ -1,22 +0,0 @@
package net.woggioni.gbcs
import java.net.URI
import java.net.URL
import org.junit.jupiter.api.Test
class ConfigurationTest {
@Test
fun test() {
GradleBuildCacheServer.registerUrlProtocolHandler()
val schemaUrl = this::class.java.getResource("/net/woggioni/gbcs/gbcs.xsd")
val dbf = Xml.newDocumentBuilderFactory()
dbf.schema = Xml.getSchema(schemaUrl)
val db = dbf.newDocumentBuilder().apply {
setErrorHandler(Xml.ErrorHandler(schemaUrl))
}
val configurationUrl = this::class.java.getResource("/net/woggioni/gbcs/gbcs-default.xml")
val doc = configurationUrl.openStream().use(db::parse)
Configuration.parse(doc.documentElement)
}
}

View File

@@ -0,0 +1,51 @@
package net.woggioni.gbcs.test
import net.woggioni.gbcs.GradleBuildCacheServer
import net.woggioni.gbcs.configuration.Configuration
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.ClassOrderer
import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestMethodOrder
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
abstract class AbstractServerTest {
protected lateinit var cfg : Configuration
protected lateinit var testDir : Path
private var serverHandle : GradleBuildCacheServer.ServerHandle? = null
@BeforeAll
fun setUp0(@TempDir tmpDir : Path) {
this.testDir = tmpDir
setUp()
startServer(cfg)
}
@AfterAll
fun tearDown0() {
tearDown()
stopServer()
}
abstract fun setUp()
abstract fun tearDown()
private fun startServer(cfg : Configuration) {
this.serverHandle = GradleBuildCacheServer(cfg).run()
}
private fun stopServer() {
this.serverHandle?.use {
it.shutdown()
}
}
}

View File

@@ -0,0 +1,189 @@
package net.woggioni.gbcs.test
import io.netty.handler.codec.Headers
import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.AbstractNettyHttpAuthenticator.Companion.hashPassword
import net.woggioni.gbcs.Authorizer
import net.woggioni.gbcs.Role
import net.woggioni.gbcs.Xml
import net.woggioni.gbcs.configuration.Configuration
import net.woggioni.gbcs.configuration.Serializer
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import java.net.ServerSocket
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.time.Duration
import java.util.Base64
import kotlin.random.Random
class BasicAuthServerTest : AbstractServerTest() {
companion object {
private const val PASSWORD = "password"
}
private lateinit var cacheDir : Path
private val random = Random(101325)
private val keyValuePair = newEntry(random)
override fun setUp() {
this.cacheDir = testDir.resolve("cache")
val readersGroup = Configuration.Group("readers", setOf(Role.Reader))
val writersGroup = Configuration.Group("writers", setOf(Role.Writer))
cfg = Configuration.of(
cache = Configuration.FileSystemCache(this.cacheDir, maxAge = Duration.ofSeconds(3600 * 24)),
host = "127.0.0.1",
port = ServerSocket(0).localPort + 1,
users = listOf(
Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup)),
Configuration.User("user2", hashPassword(PASSWORD), setOf(writersGroup)),
Configuration.User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup))
).asSequence().map { it.name to it}.toMap(),
groups = sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(),
authentication = Configuration.BasicAuthentication(),
useVirtualThread = true,
tls = null,
serverPath = "/"
)
Xml.write(Serializer.serialize(cfg), System.out)
}
override fun tearDown() {
}
fun buildAuthorizationHeader(user : Configuration.User, password : String) : String {
val b64 = Base64.getEncoder().encode("${user.name}:${password}".toByteArray(Charsets.UTF_8)).let{
String(it, StandardCharsets.UTF_8)
}
return "Basic $b64"
}
fun newRequestBuilder(key : String) = HttpRequest.newBuilder()
.uri(URI.create("http://${cfg.host}:${cfg.port}/$key"))
fun newEntry(random : Random) : Pair<String, ByteArray> {
val key = ByteArray(0x10).let {
random.nextBytes(it)
Base64.getUrlEncoder().encodeToString(it)
}
val value = ByteArray(0x1000).also {
random.nextBytes(it)
}
return key to value
}
@Test
@Order(1)
fun putWithNoAuthorizationHeader() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, value) = keyValuePair
val requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode())
}
@Test
@Order(2)
fun putAsAReaderUser() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, value) = keyValuePair
val user = cfg.users.values.find {
Role.Reader in it.roles && Role.Writer !in it.roles
} ?: throw RuntimeException("Reader user not found")
val requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode())
}
@Test
@Order(3)
fun getAsAWriterUser() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, _) = keyValuePair
val user = cfg.users.values.find {
Role.Writer in it.roles
} ?: throw RuntimeException("Reader user not found")
val requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET()
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode())
}
@Test
@Order(4)
fun putAsAWriterUser() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, value) = keyValuePair
val user = cfg.users.values.find {
Role.Writer in it.roles
} ?: throw RuntimeException("Reader user not found")
val requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
Assertions.assertEquals(HttpResponseStatus.CREATED.code(), response.statusCode())
}
@Test
@Order(5)
fun getAsAReaderUser() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, value) = keyValuePair
val user = cfg.users.values.find {
Role.Reader in it.roles
} ?: throw RuntimeException("Reader user not found")
val requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET()
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
Assertions.assertArrayEquals(value, response.body())
}
@Test
@Order(6)
fun getMissingKeyAsAReaderUser() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, _) = newEntry(random)
val user = cfg.users.values.find {
Role.Reader in it.roles
} ?: throw RuntimeException("Reader user not found")
val requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET()
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
}
}

View File

@@ -0,0 +1,32 @@
package net.woggioni.gbcs.test
import net.woggioni.gbcs.configuration.Configuration
import net.woggioni.gbcs.GradleBuildCacheServer
import net.woggioni.gbcs.Xml
import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
class ConfigurationTest {
@Test
fun test(@TempDir testDir : Path) {
URL.setURLStreamHandlerFactory(ClasspathUrlStreamHandlerFactoryProvider())
val dbf = Xml.newDocumentBuilderFactory(GradleBuildCacheServer.CONFIGURATION_SCHEMA_URL)
val db = dbf.newDocumentBuilder()
val configurationUrl = GradleBuildCacheServer.DEFAULT_CONFIGURATION_URL
val doc = configurationUrl.openStream().use(db::parse)
val cfg = Configuration.parse(doc)
val configFile = testDir.resolve("gbcs.xml")
Files.newOutputStream(configFile).use {
Xml.write(Serializer.serialize(cfg), it)
}
val parsed = Configuration.parse(Xml.parseXml(configFile.toUri().toURL()))
Assertions.assertEquals(cfg, parsed)
}
}

View File

@@ -0,0 +1,98 @@
package net.woggioni.gbcs.test
import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.Xml
import net.woggioni.gbcs.configuration.Configuration
import net.woggioni.gbcs.configuration.Serializer
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import java.net.ServerSocket
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Path
import java.time.Duration
import java.util.Base64
import kotlin.random.Random
class NoAuthServerTest : AbstractServerTest() {
private lateinit var cacheDir : Path
private val random = Random(101325)
private val keyValuePair = newEntry(random)
override fun setUp() {
this.cacheDir = testDir.resolve("cache")
cfg = Configuration.of(
cache = Configuration.FileSystemCache(this.cacheDir, maxAge = Duration.ofSeconds(3600 * 24)),
host = "127.0.0.1",
port = ServerSocket(0).localPort + 1,
users = emptyMap(),
groups = emptyMap(),
authentication = null,
useVirtualThread = true,
tls = null,
serverPath = "/"
)
Xml.write(Serializer.serialize(cfg), System.out)
}
override fun tearDown() {
}
fun newRequestBuilder(key : String) = HttpRequest.newBuilder()
.uri(URI.create("http://${cfg.host}:${cfg.port}/$key"))
fun newEntry(random : Random) : Pair<String, ByteArray> {
val key = ByteArray(0x10).let {
random.nextBytes(it)
Base64.getUrlEncoder().encodeToString(it)
}
val value = ByteArray(0x1000).also {
random.nextBytes(it)
}
return key to value
}
@Test
@Order(1)
fun putWithNoAuthorizationHeader() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, value) = keyValuePair
val requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
Assertions.assertEquals(HttpResponseStatus.CREATED.code(), response.statusCode())
}
@Test
@Order(2)
fun getWithNoAuthorizationHeader() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, value ) = keyValuePair
val requestBuilder = newRequestBuilder(key)
.GET()
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
Assertions.assertArrayEquals(value, response.body())
}
@Test
@Order(3)
fun getMissingKey() {
val client: HttpClient = HttpClient.newHttpClient()
val (key, _) = newEntry(random)
val requestBuilder = newRequestBuilder(key).GET()
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
}
}

View File

@@ -0,0 +1,292 @@
package net.woggioni.gbcs.test
import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.Role
import net.woggioni.gbcs.Xml
import net.woggioni.gbcs.configuration.Configuration
import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.utils.CertificateUtils
import net.woggioni.gbcs.utils.CertificateUtils.X509Credentials
import org.bouncycastle.asn1.x500.X500Name
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import java.net.ServerSocket
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.KeyStore.PasswordProtection
import java.time.Duration
import java.util.Base64
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import kotlin.random.Random
class TlsServerTest : AbstractServerTest() {
companion object {
private const val CA_CERTIFICATE_ENTRY = "gbcs-ca"
private const val CLIENT_CERTIFICATE_ENTRY = "gbcs-client"
private const val SERVER_CERTIFICATE_ENTRY = "gbcs-server"
private const val PASSWORD = "password"
}
private lateinit var cacheDir: Path
private lateinit var serverKeyStoreFile: Path
private lateinit var clientKeyStoreFile: Path
private lateinit var trustStoreFile: Path
private lateinit var serverKeyStore: KeyStore
private lateinit var clientKeyStore: KeyStore
private lateinit var trustStore: KeyStore
private lateinit var ca: X509Credentials
private val readersGroup = Configuration.Group("readers", setOf(Role.Reader))
private val writersGroup = Configuration.Group("writers", setOf(Role.Writer))
private val random = Random(101325)
private val keyValuePair = newEntry(random)
private val users = listOf(
Configuration.User("user1", null, setOf(readersGroup)),
Configuration.User("user2", null, setOf(writersGroup)),
Configuration.User("user3", null, setOf(readersGroup, writersGroup))
)
fun createKeyStoreAndTrustStore() {
ca = CertificateUtils.createCertificateAuthority(CA_CERTIFICATE_ENTRY, 30)
val serverCert = CertificateUtils.createServerCertificate(ca, X500Name("CN=$SERVER_CERTIFICATE_ENTRY"), 30)
val clientCert = CertificateUtils.createClientCertificate(ca, X500Name("CN=$CLIENT_CERTIFICATE_ENTRY"), 30)
serverKeyStore = KeyStore.getInstance("PKCS12").apply {
load(null, null)
setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null))
setEntry(
SERVER_CERTIFICATE_ENTRY,
KeyStore.PrivateKeyEntry(
serverCert.keyPair().private,
arrayOf(serverCert.certificate(), ca.certificate)
),
PasswordProtection(PASSWORD.toCharArray())
)
}
Files.newOutputStream(this.serverKeyStoreFile).use {
serverKeyStore.store(it, null)
}
clientKeyStore = KeyStore.getInstance("PKCS12").apply {
load(null, null)
setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null))
setEntry(
CLIENT_CERTIFICATE_ENTRY,
KeyStore.PrivateKeyEntry(
clientCert.keyPair().private,
arrayOf(clientCert.certificate(), ca.certificate)
),
PasswordProtection(PASSWORD.toCharArray())
)
}
Files.newOutputStream(this.clientKeyStoreFile).use {
clientKeyStore.store(it, null)
}
trustStore = KeyStore.getInstance("PKCS12").apply {
load(null, null)
setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null))
}
Files.newOutputStream(this.trustStoreFile).use {
trustStore.store(it, null)
}
}
fun getClientKeyStore(ca : X509Credentials, subject: X500Name) = KeyStore.getInstance("PKCS12").apply {
val clientCert = CertificateUtils.createClientCertificate(ca, subject, 30)
load(null, null)
setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null))
setEntry(
CLIENT_CERTIFICATE_ENTRY,
KeyStore.PrivateKeyEntry(clientCert.keyPair().private, arrayOf(clientCert.certificate(), ca.certificate)),
PasswordProtection(PASSWORD.toCharArray())
)
}
fun getHttpClient(clientKeyStore : KeyStore?): HttpClient {
val kmf = clientKeyStore?.let {
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply {
init(it, PASSWORD.toCharArray())
}
}
// Set up trust manager factory with the truststore
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
tmf.init(trustStore)
// Create SSL context with the key and trust managers
val sslContext = SSLContext.getInstance("TLS").apply {
init(kmf?.keyManagers ?: emptyArray(), tmf.trustManagers, null)
}
return HttpClient.newBuilder().sslContext(sslContext).build()
}
override fun setUp() {
this.clientKeyStoreFile = testDir.resolve("client-keystore.p12")
this.serverKeyStoreFile = testDir.resolve("server-keystore.p12")
this.trustStoreFile = testDir.resolve("truststore.p12")
this.cacheDir = testDir.resolve("cache")
createKeyStoreAndTrustStore()
cfg = Configuration.of(
cache = Configuration.FileSystemCache(this.cacheDir, maxAge = Duration.ofSeconds(3600 * 24)),
host = "127.0.0.1",
port = ServerSocket(0).localPort + 1,
users = users.asSequence().map { it.name to it }.toMap(),
groups = sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
authentication = Configuration.ClientCertificateAuthentication(
userExtractor = Configuration.TlsCertificateExtractor("CN", "(.*)"),
groupExtractor = null
),
useVirtualThread = true,
tls = Configuration.Tls(
Configuration.KeyStore(this.serverKeyStoreFile, null, SERVER_CERTIFICATE_ENTRY, PASSWORD),
Configuration.TrustStore(this.trustStoreFile, null, false),
true
),
serverPath = "/"
)
Xml.write(Serializer.serialize(cfg), System.out)
}
override fun tearDown() {
}
fun newRequestBuilder(key: String) = HttpRequest.newBuilder()
.uri(URI.create("https://${cfg.host}:${cfg.port}/$key"))
fun buildAuthorizationHeader(user: Configuration.User, password: String): String {
val b64 = Base64.getEncoder().encode("${user.name}:${password}".toByteArray(Charsets.UTF_8)).let {
String(it, StandardCharsets.UTF_8)
}
return "Basic $b64"
}
fun newEntry(random: Random): Pair<String, ByteArray> {
val key = ByteArray(0x10).let {
random.nextBytes(it)
Base64.getUrlEncoder().encodeToString(it)
}
val value = ByteArray(0x1000).also {
random.nextBytes(it)
}
return key to value
}
@Test
@Order(1)
fun putWithNoClientCertificate() {
val client: HttpClient = getHttpClient(null)
val (key, value) = keyValuePair
val requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode())
}
@Test
@Order(2)
fun putAsAReaderUser() {
val (key, value) = keyValuePair
val user = cfg.users.values.find {
Role.Reader in it.roles && Role.Writer !in it.roles
} ?: throw RuntimeException("Reader user not found")
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
val requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode())
}
@Test
@Order(3)
fun getAsAWriterUser() {
val (key, _) = keyValuePair
val user = cfg.users.values.find {
Role.Writer in it.roles
} ?: throw RuntimeException("Reader user not found")
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
val requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET()
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode())
}
@Test
@Order(4)
fun putAsAWriterUser() {
val (key, value) = keyValuePair
val user = cfg.users.values.find {
Role.Writer in it.roles
} ?: throw RuntimeException("Reader user not found")
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
val requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
Assertions.assertEquals(HttpResponseStatus.CREATED.code(), response.statusCode())
}
@Test
@Order(5)
fun getAsAReaderUser() {
val (key, value) = keyValuePair
val user = cfg.users.values.find {
Role.Reader in it.roles
} ?: throw RuntimeException("Reader user not found")
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
val requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET()
val response: HttpResponse<ByteArray> =
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
Assertions.assertArrayEquals(value, response.body())
}
@Test
@Order(6)
fun getMissingKeyAsAReaderUser() {
val (key, _) = newEntry(random)
val user = cfg.users.values.find {
Role.Reader in it.roles
} ?: throw RuntimeException("Reader user not found")
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
val requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET()
val response: HttpResponse<ByteArray> =
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
}
}

View File

@@ -0,0 +1,19 @@
package net.woggioni.gbcs.test
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import javax.naming.ldap.LdapName
class X500NameTest {
@Test
fun test() {
val name =
"C=SG, L=Bugis, CN=woggioni@f6aa5663ef26, emailAddress=oggioni.walter@gmail.com, street=1 Fraser Street\\, Duo Residences #23-05, postalCode=189350, GN=Walter, SN=Oggioni, pseudonym=woggioni"
val ldapName = LdapName(name)
val value = ldapName.rdns.asSequence().find {
it.type == "CN"
}!!.value
Assertions.assertEquals("woggioni@f6aa5663ef26", value)
}
}