Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create secure connector for Client Certificate #1390

Merged
merged 30 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e4d0c14
feat: add new secure connector to the current server
MiguelAHM Jan 25, 2024
d43123d
feat: comment previous secure port
MiguelAHM Jan 25, 2024
f5ad290
feat: specify host
MiguelAHM Jan 25, 2024
5047ef7
feat: add host
MiguelAHM Jan 25, 2024
713d45e
feat: create client auth connector
MiguelAHM Jan 25, 2024
e5b579a
feat: enable https again in rest api
MiguelAHM Jan 25, 2024
8aa9428
feat: leave it as before
MiguelAHM Jan 25, 2024
ccd3f23
feat: specify the host for client auth
MiguelAHM Jan 26, 2024
e1bbd27
feat: remove host
MiguelAHM Jan 26, 2024
296e7e6
feat: create another secure connection with client auth enabled
MiguelAHM Jan 26, 2024
4c0814b
feat: put rest as before, just enable client auth cert
MiguelAHM Jan 26, 2024
b679831
feat: do not trust all cacert
MiguelAHM Jan 26, 2024
3475111
feat: enable client cert for both ports if cert auth enabled
MiguelAHM Jan 26, 2024
c360f30
feat: change the property in everyplace that was in use
MiguelAHM Jan 26, 2024
8d081ce
feat: put the correct port
MiguelAHM Jan 26, 2024
0b939eb
feat: server will validate agains truststore
MiguelAHM Jan 30, 2024
364274c
feat: the to false the server validation
MiguelAHM Jan 30, 2024
aa17931
feat: try with setWantClientAuth
MiguelAHM Jan 30, 2024
adf0be0
feat: put the correct configuration
MiguelAHM Jan 30, 2024
585c166
feat: configure the properties correctly
MiguelAHM Jan 31, 2024
5995c84
feat: set want client auth instead of needed
MiguelAHM Jan 31, 2024
1da52d4
feat: try with setNeed
MiguelAHM Jan 31, 2024
1c66a83
feat: use needed just when is client Certificate
MiguelAHM Feb 1, 2024
d5ee317
feat: fix tests
MiguelAHM Feb 2, 2024
0326d55
feat: do not accept self-signed certificates when client certificates…
MiguelAHM Feb 2, 2024
17f4e7e
feat: use the correct abstract test class
MiguelAHM Feb 2, 2024
c05a970
Merge branch 'master' into DB-5061-Client-Cert-Server-Name
eshryane Feb 2, 2024
b68dfc3
feat: revert abstractHttpsIntegration notes
MiguelAHM Feb 2, 2024
150be68
Merge branch 'master' into DB-5061-Client-Cert-Server-Name
MiguelAHM Feb 5, 2024
e4bc233
feat: fix tests
MiguelAHM Feb 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import io.netty.handler.ssl.util.TrustManagerFactoryWrapper;
import jakarta.servlet.DispatcherType;
import jakarta.ws.rs.HEAD;
import net.ripe.db.whois.common.ApplicationService;
import net.ripe.db.whois.common.aspects.RetryFor;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
Expand Down Expand Up @@ -107,6 +106,10 @@ public class JettyBootstrap implements ApplicationService {
private int port;
private final int idleTimeout;

private int clientAuthPort;

private final boolean clientCertEnabled;

@Autowired
public JettyBootstrap(final RemoteAddressFilter remoteAddressFilter,
final ExtensionOverridesAcceptHeaderFilter extensionOverridesAcceptHeaderFilter,
Expand All @@ -119,8 +122,10 @@ public JettyBootstrap(final RemoteAddressFilter remoteAddressFilter,
@Value("${dos.filter.enabled:false}") final boolean dosFilterEnabled,
@Value("${rewrite.engine.enabled:false}") final boolean rewriteEngineEnabled,
@Value("${port.api:0}") final int port,
@Value("${port.api.secure:-1}") final int securePort
) throws MalformedObjectNameException {
@Value("${port.api.secure:-1}") final int securePort,
@Value("${port.client.auth:-1}") final int clientAuthPort,
@Value("${client.auth.enabled:false}") final boolean clientCertEnabled
) throws MalformedObjectNameException {
this.remoteAddressFilter = remoteAddressFilter;
this.extensionOverridesAcceptHeaderFilter = extensionOverridesAcceptHeaderFilter;
this.servletDeployers = servletDeployers;
Expand All @@ -136,6 +141,8 @@ public JettyBootstrap(final RemoteAddressFilter remoteAddressFilter,
this.securePort = securePort;
this.port = port;
this.server = null;
this.clientAuthPort = clientAuthPort;
this.clientCertEnabled = clientCertEnabled;
}

@Override
Expand All @@ -158,6 +165,10 @@ public int getSecurePort() {
return this.securePort;
}

public int getClientAuthPort(){
return this.clientAuthPort;
}

public Server getServer() {
return this.server;
}
Expand All @@ -180,11 +191,15 @@ private Server createAndStartServer() {
private Server createServer() {
final Server server = new Server();

server.setConnectors(new Connector[]{createConnector(server)});

if (this.securePort >= 0) {
server.setConnectors(new Connector[]{createConnector(server), createSecureConnector(server)});
} else {
server.setConnectors(new Connector[]{createConnector(server)});
}
server.addConnector(createSecureConnector(server, this.securePort, false));
}

if (this.clientAuthPort >= 0 && this.clientCertEnabled) {
server.addConnector(createSecureConnector(server, this.clientAuthPort, true));
}

final WebAppContext context = new WebAppContext();
context.setContextPath("/");
Expand Down Expand Up @@ -234,6 +249,8 @@ private Connector createConnector(final Server server) {
return connector;
}



/**
* Use the DoSFilter from Jetty for rate limiting: https://www.eclipse.org/jetty/documentation/current/dos-filter.html.
* See {@link WhoisDoSFilter} for the customisations added.
Expand Down Expand Up @@ -265,7 +282,7 @@ private FilterHolder createDosFilter() throws JmxException, JMException {
return holder;
}

private Connector createSecureConnector(final Server server) {
private Connector createSecureConnector(final Server server, final int port, final boolean isClientCertificate) {
// allow (untrusted) self-signed certificates to connect
final SslContextFactory.Server sslContextFactory = new SslContextFactory.Server() {
@Override
Expand All @@ -288,11 +305,13 @@ protected TrustManagerFactory getTrustManagerFactoryInstance() {
sslContextFactory.setKeyStorePassword(whoisKeystore.getPassword());
sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR);

// enable optional client certificates
sslContextFactory.setWantClientAuth(true);
sslContextFactory.setValidateCerts(false);
sslContextFactory.setTrustAll(true);

if (isClientCertificate) {
// accept self-signed client certificates for authentication
sslContextFactory.setNeedClientAuth(true);
sslContextFactory.setValidateCerts(false);
sslContextFactory.setTrustAll(true);
}

// Exclude weak / insecure ciphers
// TODO CBC became weak, we need to skip them in the future https://support.kemptechnologies.com/hc/en-us/articles/9338043775757-CBC-ciphers-marked-as-weak-by-SSL-labs
// Check client compatability first
Expand All @@ -318,7 +337,7 @@ protected TrustManagerFactory getTrustManagerFactoryInstance() {
final SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, alpn.getProtocol());

final ServerConnector sslConnector = new ServerConnector(server, sslConnectionFactory, alpn, h2, new HttpConnectionFactory(httpsConfiguration));
sslConnector.setPort(this.securePort);
sslConnector.setPort(port);
return sslConnector;
}

Expand Down Expand Up @@ -408,11 +427,18 @@ private void logHttpsConfig() {
private void updatePorts() {
for (Connector connector : this.server.getConnectors()) {
final int localPort = ((NetworkConnector) connector).getLocalPort();
if (connector.getProtocols().contains("ssl")) {
this.securePort = localPort;
} else {
if(!connector.getProtocols().contains("ssl")) {
this.port = localPort;
continue;
}

if (this.clientCertEnabled) {
this.clientAuthPort = localPort;
continue;
}

this.securePort = localPort;

}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.ripe.db.whois.api.httpserver;

import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
Expand Down Expand Up @@ -27,14 +28,30 @@ public class RewriteEngine {
private final String source;
private final String nonAuthSource;

private String clientAuthHost;

private final boolean clientCertEnabled;

@Autowired
public RewriteEngine(final @Value("${api.rest.baseurl}") String baseUrl,
final @Value("${whois.source}") String source,
final @Value("${whois.nonauth.source}") String nonAuthSource) {
public RewriteEngine(@Value("${api.rest.baseurl}") final String baseUrl,
@Value("${whois.source}") final String source,
@Value("${whois.nonauth.source}") final String nonAuthSource,
@Value("${api.client.auth.baseurl:}") final String clientAuthBaseUrl,
@Value("${client.auth.enabled:false}") final boolean clientCertEnabled) {
this.source = source;
this.nonAuthSource = nonAuthSource;
URI uri = URI.create(baseUrl);
restVirtualHost = uri.getHost();

URI restBaseUri = URI.create(baseUrl);
restVirtualHost = restBaseUri.getHost();

this.clientCertEnabled = clientCertEnabled;

if (this.clientCertEnabled && StringUtils.isNotBlank(clientAuthBaseUrl)) {
URI clientAuthBaseUri = URI.create(clientAuthBaseUrl);
clientAuthHost = clientAuthBaseUri.getHost();
LOGGER.info("Client Auth virtual host: {}", clientAuthHost);
}

syncupdatesVirtualHost = restVirtualHost.replace("rest", "syncupdates");
rdapVirtualHost = restVirtualHost.replace("rest", "rdap");

Expand All @@ -54,6 +71,14 @@ public RewriteHandler getRewriteHandler() {
rewriteHandler.addRule(restVirtualHostRule);
restRedirectRules(restVirtualHostRule);

if (this.clientCertEnabled) {
// Client Auth
VirtualHostRuleContainer clientAuthVirtualHostRule = new VirtualHostRuleContainer();
clientAuthVirtualHostRule.addVirtualHost(clientAuthHost);
rewriteHandler.addRule(clientAuthVirtualHostRule);
restRedirectRules(clientAuthVirtualHostRule);
}

// rdap
VirtualHostRuleContainer rdapVirtualHostRule = new VirtualHostRuleContainer();
rdapVirtualHostRule.addVirtualHost(rdapVirtualHost);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import net.ripe.db.whois.common.rpsl.RpslObject;
import net.ripe.db.whois.common.rpsl.RpslObjectBuilder;
import net.ripe.db.whois.update.keycert.X509CertificateWrapper;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;

import javax.net.ssl.KeyManager;
Expand Down Expand Up @@ -45,7 +46,14 @@ public class AbstractClientCertificateIntegrationTest extends AbstractHttpsInteg

@BeforeAll
public static void enableClientAuth() {
System.setProperty("client.cert.auth.enabled", "true");
System.setProperty("client.auth.enabled", "true");
MiguelAHM marked this conversation as resolved.
Show resolved Hide resolved
System.setProperty("port.client.auth", "0");
}

@AfterAll
public static void disableClientAuth() {
System.clearProperty("client.auth.enabled");
System.clearProperty("port.client.auth");
}

public SSLContext getClientSSLContext() {
Expand Down Expand Up @@ -112,4 +120,7 @@ public static RpslObject createKeycertObject(final X509Certificate x509, final S
return builder.get();
}

public int getClientCertificatePort() {
return jettyBootstrap.getClientAuthPort();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package net.ripe.db.whois.api.httpserver;

import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.core.MediaType;
import net.ripe.db.whois.api.SecureRestTest;
import org.junit.jupiter.api.Tag;
Expand All @@ -16,7 +16,7 @@ public class ClientCertificateServiceTestIntegration extends AbstractClientCerti

@Test
public void client_certificate() {
final String response = SecureRestTest.target(getClientSSLContext(), getSecurePort(), "whois/client")
final String response = SecureRestTest.target(getClientSSLContext(), getClientCertificatePort(), "whois/client")
.request()
.get(String.class);

Expand All @@ -28,13 +28,13 @@ public void client_certificate() {
@Test
public void no_client_certificate() {
try {
SecureRestTest.target(getSecurePort(), "whois/client")
SecureRestTest.target(getClientCertificatePort(), "whois/client")
.request()
.accept(MediaType.TEXT_PLAIN)
.get(String.class);
fail();
} catch (BadRequestException e) {
assertThat(e.getResponse().readEntity(String.class), containsString("Bad Request"));
} catch (ProcessingException e) {
assertThat(e.getMessage(), containsString("javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate"));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public class WhoisRestBasicAuthTestIntegration extends AbstractHttpsIntegrationT
"mnt-by: OWNER-MNT\n" +
"source: TEST");

private static String TEST_ROLE_STRING = "" +
private static final String TEST_ROLE_STRING = "" +
"role: Test Role\n" +
"address: Singel 258\n" +
"phone: +31 6 12345678\n" +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.ripe.db.whois.api.rest;

import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import net.ripe.db.whois.api.RestTest;
Expand Down Expand Up @@ -129,7 +130,7 @@ public void update_person_with_client_cert_no_mntner_cert_unauthorised() {
"source: TEST");

try {
SecureRestTest.target(getClientSSLContext(), getSecurePort(), "whois/test/person/TP1-TEST")
SecureRestTest.target(getClientSSLContext(), getClientCertificatePort(), "whois/test/person/TP1-TEST")
.request()
.put(Entity.entity(map(updatedPerson), MediaType.APPLICATION_XML), WhoisResources.class);
fail();
Expand Down Expand Up @@ -159,7 +160,7 @@ public void update_person_with_client_cert_auth_different_mntner_cert_unauthoris
databaseHelper.updateObject(updatedMntner);

try {
SecureRestTest.target(getClientSSLContext(), getSecurePort(), "whois/test/person/TP1-TEST")
SecureRestTest.target(getClientSSLContext(), getClientCertificatePort(), "whois/test/person/TP1-TEST")
.request()
.put(Entity.entity(map(updatedPerson), MediaType.APPLICATION_XML), WhoisResources.class);
fail();
Expand Down Expand Up @@ -190,7 +191,7 @@ public void update_person_with_client_cert_and_mntner_cert_successful() {
databaseHelper.updateObject(updatedMntner);

// connect with mntner's client cert for authentication
final WhoisResources whoisResources = SecureRestTest.target(getClientSSLContext(), getSecurePort(), "whois/test/person/TP1-TEST")
final WhoisResources whoisResources = SecureRestTest.target(getClientSSLContext(), getClientCertificatePort(), "whois/test/person/TP1-TEST")
.request()
.put(Entity.entity(map(updatedPerson), MediaType.APPLICATION_XML), WhoisResources.class);

Expand Down Expand Up @@ -222,16 +223,12 @@ public void update_person_missing_private_key_unauthorised() throws Exception {
databaseHelper.updateObject(updatedMntner);

try {
SecureRestTest.target(sslContext, getSecurePort(), "whois/test/person/TP1-TEST")
SecureRestTest.target(sslContext, getClientCertificatePort(), "whois/test/person/TP1-TEST")
.request()
.put(Entity.entity(map(updatedPerson), MediaType.APPLICATION_XML), WhoisResources.class);
fail();
} catch (NotAuthorizedException e) {
final WhoisResources whoisResources = e.getResponse().readEntity(WhoisResources.class);
RestTest.assertErrorCount(whoisResources, 1);
RestTest.assertErrorMessage(whoisResources, 0, "Error",
"Authorisation for [%s] %s failed\nusing \"%s:\"\n" +
"not authenticated by: %s", "person", "TP1-TEST", "mnt-by", "OWNER-MNT");
} catch (ProcessingException e) {
assertThat(e.getMessage(), containsString("javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate"));
}
}

Expand All @@ -255,7 +252,7 @@ public void update_route6_with_client_cert_and_mntner_cert_successful() {
final RpslObject updatedRoute6 = new RpslObjectBuilder(route6).append(new RpslAttribute(AttributeType.REMARKS, "updated")).get();

// connect with mntner's client cert for authentication
final WhoisResources whoisResources = SecureRestTest.target(getClientSSLContext(), getSecurePort(), "whois/test/route6/2001::/32AS12726")
final WhoisResources whoisResources = SecureRestTest.target(getClientSSLContext(), getClientCertificatePort(), "whois/test/route6/2001::/32AS12726")
.request()
.put(Entity.entity(map(updatedRoute6), MediaType.APPLICATION_XML), WhoisResources.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class ClientCertificateCredentialValidator implements CredentialValidator
public ClientCertificateCredentialValidator(final RpslObjectDao rpslObjectDao,
final DateTimeProvider dateTimeProvider,
final LoggerContext loggerContext,
final @Value("${client.cert.auth.enabled:false}") boolean enabled) {
final @Value("${client.auth.enabled:false}") boolean enabled) {
eshryane marked this conversation as resolved.
Show resolved Hide resolved
this.rpslObjectDao = rpslObjectDao;
this.dateTimeProvider = dateTimeProvider;
this.loggerContext = loggerContext;
Expand Down