-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add client certificate authentication. * Fix tests. * Don't provide client certificate for mail updates. Pick up (optional) client certificate for syncupdates. * Added test for client certificate authentication. * More audit logging. * Properly handle PEM certificate. * Tidy up code, use X509CertificateWrapper for cert operations, added tests, handle Apache's SSL_CLIENT_CERT PEM format. * Added feature toggle for client certificate authentication. * Enable client certificate authentication in tests. * Client cert authentication should not be enabled by default. * Also accept FAILED SSL verify header (for self signed).
- Loading branch information
Showing
25 changed files
with
620 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
whois-api/src/main/java/net/ripe/db/whois/api/rest/ClientCertificateExtractor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package net.ripe.db.whois.api.rest; | ||
|
||
import net.ripe.db.whois.common.DateTimeProvider; | ||
import net.ripe.db.whois.update.keycert.X509CertificateWrapper; | ||
import org.apache.commons.lang.StringUtils; | ||
import org.apache.commons.net.util.Base64; | ||
import org.bouncycastle.x509.util.StreamParsingException; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
import java.util.Optional; | ||
|
||
public class ClientCertificateExtractor { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(ClientCertificateExtractor.class); | ||
|
||
final static String HEADER_SSL_CLIENT_CERT = "SSL_CLIENT_CERT"; | ||
final static String HEADER_SSL_CLIENT_VERIFY = "SSL_CLIENT_VERIFY"; | ||
|
||
public static Optional<X509CertificateWrapper> getClientCertificate(final HttpServletRequest request, | ||
final DateTimeProvider dateTimeProvider) { | ||
final String sslClientCert = request.getHeader(HEADER_SSL_CLIENT_CERT); | ||
|
||
if (StringUtils.isBlank(sslClientCert)) { | ||
return Optional.empty(); | ||
} | ||
|
||
if (!hasAcceptableVerifyHeader(request)) { | ||
return Optional.empty(); | ||
} | ||
|
||
return getX509Certificate(sslClientCert, dateTimeProvider); | ||
} | ||
|
||
private static boolean hasAcceptableVerifyHeader(final HttpServletRequest request) { | ||
final String sslClientVerify = request.getHeader(HEADER_SSL_CLIENT_VERIFY); | ||
|
||
return StringUtils.isNotEmpty(sslClientVerify) && | ||
("GENEROUS".equals(sslClientVerify) || | ||
"SUCCESS".equals(sslClientVerify) || | ||
sslClientVerify.startsWith("FAILED")); | ||
} | ||
|
||
private static Optional<X509CertificateWrapper> getX509Certificate(final String certificate, | ||
final DateTimeProvider dateTimeProvider) { | ||
String fingerprint; | ||
try { | ||
// the PEM cert provided by Apache in SSL_CLIENT_CERT has spaces that JCA doesn't like so we decode it ourselves: | ||
final byte[] bytes = Base64.decodeBase64( | ||
certificate | ||
.replace(X509CertificateWrapper.X509_HEADER, "") | ||
.replace(X509CertificateWrapper.X509_FOOTER, "") | ||
.replaceAll(" ", "") | ||
); | ||
// TODO we should probably let the servlet container handle this for us and just use javax.servlet.request.X509Certificate | ||
|
||
final X509CertificateWrapper x509CertificateWrapper = X509CertificateWrapper.parse(bytes); | ||
fingerprint = x509CertificateWrapper.getFingerprint(); | ||
|
||
if (x509CertificateWrapper.isNotYetValid(dateTimeProvider)) { | ||
LOGGER.info("Client certificate {} is not yet valid", fingerprint); | ||
return Optional.empty(); | ||
} | ||
|
||
if (x509CertificateWrapper.isExpired(dateTimeProvider)) { | ||
LOGGER.info("Client certificate {} has expired", fingerprint); | ||
return Optional.empty(); | ||
} | ||
|
||
return Optional.of(x509CertificateWrapper); | ||
} catch (StreamParsingException e) { | ||
LOGGER.info("Invalid X.509 certificate"); | ||
} | ||
|
||
return Optional.empty(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
whois-api/src/test/java/net/ripe/db/whois/api/rest/ClientCertificateExtractorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package net.ripe.db.whois.api.rest; | ||
|
||
import net.ripe.db.whois.common.ClockDateTimeProvider; | ||
import net.ripe.db.whois.common.TestDateTimeProvider; | ||
import net.ripe.db.whois.update.keycert.X509CertificateTestUtil; | ||
import org.junit.Test; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
import static net.ripe.db.whois.api.rest.ClientCertificateExtractor.HEADER_SSL_CLIENT_CERT; | ||
import static net.ripe.db.whois.api.rest.ClientCertificateExtractor.HEADER_SSL_CLIENT_VERIFY; | ||
import static org.hamcrest.CoreMatchers.is; | ||
import static org.junit.Assert.assertThat; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.when; | ||
|
||
public class ClientCertificateExtractorTest { | ||
|
||
static String cert; | ||
|
||
static { | ||
try { | ||
cert = X509CertificateTestUtil.asPem(X509CertificateTestUtil.generate("test-cn", new ClockDateTimeProvider())); | ||
} catch (Exception e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
@Test | ||
public void testValidCertificate() { | ||
final HttpServletRequest request = mock(HttpServletRequest.class); | ||
when(request.getHeader(HEADER_SSL_CLIENT_CERT)).thenReturn(cert); | ||
when(request.getHeader(HEADER_SSL_CLIENT_VERIFY)).thenReturn("GENEROUS"); | ||
|
||
assertThat(ClientCertificateExtractor.getClientCertificate(request, new ClockDateTimeProvider()).isPresent(), is(true)); | ||
} | ||
|
||
@Test | ||
public void testInvalidVerifyHeader() { | ||
final HttpServletRequest request = mock(HttpServletRequest.class); | ||
when(request.getHeader(HEADER_SSL_CLIENT_CERT)).thenReturn(cert); | ||
when(request.getHeader(HEADER_SSL_CLIENT_VERIFY)).thenReturn("NOT-ACCEPTED"); | ||
|
||
assertThat(ClientCertificateExtractor.getClientCertificate(request, new ClockDateTimeProvider()).isPresent(), is(false)); | ||
} | ||
|
||
@Test | ||
public void testNoCertificate() { | ||
final HttpServletRequest request = mock(HttpServletRequest.class); | ||
when(request.getHeader(HEADER_SSL_CLIENT_VERIFY)).thenReturn("GENEROUS"); | ||
|
||
assertThat(ClientCertificateExtractor.getClientCertificate(request, new ClockDateTimeProvider()).isPresent(), is(false)); | ||
} | ||
|
||
@Test | ||
public void testCertificateExpired() { | ||
final HttpServletRequest request = mock(HttpServletRequest.class); | ||
when(request.getHeader(HEADER_SSL_CLIENT_CERT)).thenReturn(cert); | ||
when(request.getHeader(HEADER_SSL_CLIENT_VERIFY)).thenReturn("GENEROUS"); | ||
|
||
final TestDateTimeProvider testDateTimeProvider = new TestDateTimeProvider(); | ||
testDateTimeProvider.setTime(LocalDateTime.now().plusYears(5)); | ||
|
||
assertThat(ClientCertificateExtractor.getClientCertificate(request, testDateTimeProvider).isPresent(), is(false)); | ||
} | ||
|
||
@Test | ||
public void testCertificateNotYetValid() { | ||
final HttpServletRequest request = mock(HttpServletRequest.class); | ||
when(request.getHeader(HEADER_SSL_CLIENT_CERT)).thenReturn(cert); | ||
when(request.getHeader(HEADER_SSL_CLIENT_VERIFY)).thenReturn("GENEROUS"); | ||
|
||
final TestDateTimeProvider testDateTimeProvider = new TestDateTimeProvider(); | ||
testDateTimeProvider.setTime(LocalDateTime.now().minusMonths(1)); | ||
|
||
assertThat(ClientCertificateExtractor.getClientCertificate(request, testDateTimeProvider).isPresent(), is(false)); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.