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

Add JWS Support To Fix Race Condition in NRTMv4 #1564

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,17 @@
<version>${netty.version}</version>
</dependency>

<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.40</version>
</dependency>
<dependency>
<groupId>com.google.crypto.tink</groupId>
<artifactId>tink</artifactId>
<version>1.13.0</version>
</dependency>

<!-- Jakarta Mail (formerly JavaMail) TODO: [ES] depend on angus-mail -->
<dependency>
<groupId>org.eclipse.angus</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package net.ripe.db.whois.api.nrtm4;

import com.google.common.net.HttpHeaders;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import net.ripe.db.nrtm4.dao.DeltaFileSourceAwareDao;
Expand All @@ -27,6 +25,7 @@
import java.io.ByteArrayInputStream;

import static net.ripe.db.nrtm4.util.Ed25519Util.signWithEd25519;
import static net.ripe.db.nrtm4.util.JWSUtil.signWithJWS;

@Component
@Path("/")
Expand Down Expand Up @@ -75,17 +74,21 @@ public String sourcesLinkAsHtml() {
@GET
@Path("{source}/{filename}")
@Produces({MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON})
public Response nrtmFiles(
@Context final HttpServletRequest httpServletRequest,
@PathParam("source") final String source,
public Response nrtmFiles(@PathParam("source") final String source,
@PathParam("filename") final String fileName) {

if(isNotificationFile(fileName)) {

final String payload = updateNotificationFileSourceAwareDao.findLastNotification(getSource(source))
.orElseThrow(() -> new NotFoundException("update-notification-file.json does not exists for source " + source));
.orElseThrow(() -> new NotFoundException("update-notification-file does not exists for source " + source));

if(fileName.endsWith(".jose")) {
return getResponseForJWS(signWithJWS(payload, nrtmKeyConfigDao.getActivePrivateKey()));
}

//TODO: remove once client is also shifted to JWS
return fileName.endsWith(".sig") ? getResponse(signWithEd25519(payload.getBytes(), nrtmKeyConfigDao.getActivePrivateKey()))
: getResponse(payload);
: getResponse(payload);
}

validateSource(source, fileName);
Expand Down Expand Up @@ -133,6 +136,13 @@ private Response getResponse(final String payload) {
.build();
}

private Response getResponseForJWS(final String payload) {
return Response.ok(payload)
.header(HttpHeaders.CONTENT_TYPE, "application/jose+json")
.build();
}


private Response getResponseForDelta(final String payload) {
return Response.ok(payload)
.header(HttpHeaders.CONTENT_TYPE, "application/json-seq")
Expand All @@ -151,7 +161,7 @@ private boolean isNotificationFile(final String fileName) {
}

final String fileExtension = StringUtils.substringAfter(fileName, NrtmDocumentType.NOTIFICATION.getFileNamePrefix());
if(!fileExtension.equals(".json.sig")) {
if(!fileExtension.equals(".json.sig") && !fileExtension.equals(".jose") ) {
throw new NotFoundException("Notification file does not exists");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.nimbusds.jose.JWSObject;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
Expand All @@ -29,6 +30,8 @@
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Nullable;
import java.io.IOException;
import java.text.ParseException;
import java.util.List;

import static org.hamcrest.MatcherAssert.assertThat;
Expand Down Expand Up @@ -196,29 +199,29 @@ public void setup() {
"source: TEST-NONAUTH");
}

protected WebTarget createResource(final String path) {
return RestTest.target(getPort(), String.format("nrtmv4/%s", path));
}

protected UpdateNotificationFile getNotificationFileBySource(final String sourceName) {
return getResponseFromHttpsRequest(sourceName + "/update-notification-file.json", MediaType.APPLICATION_JSON).readEntity(UpdateNotificationFile.class);
try {
final Response response = getResponseFromHttpsRequest(sourceName + "/update-notification-file.jose", MediaType.APPLICATION_JSON);
final JWSObject jwsObjectParsed = JWSObject.parse(response.readEntity(String.class));

return new ObjectMapper().readValue(jwsObjectParsed.getPayload().toString(), UpdateNotificationFile.class);
} catch (IOException | ParseException e) {
throw new RuntimeException(e);
}
}

protected String getSnapshotNameFromUpdateNotification(final UpdateNotificationFile notificationFile) {
return notificationFile.getSnapshot().getUrl().split("/")[4];
}

protected Response getSnapshotFromUpdateNotificationBySource(final String sourceName) throws JsonProcessingException {
final Response updateNotificationResponse = getResponseFromHttpsRequest(sourceName + "/update-notification-file.json", MediaType.APPLICATION_JSON);
final UpdateNotificationFile notificationFile = new ObjectMapper().readValue(updateNotificationResponse.readEntity(String.class),
UpdateNotificationFile.class);
protected Response getSnapshotFromUpdateNotificationBySource(final String sourceName) {
final UpdateNotificationFile notificationFile = getNotificationFileBySource(sourceName);
return getResponseFromHttpsRequest(sourceName + "/" + getSnapshotNameFromUpdateNotification(notificationFile)
, MediaType.APPLICATION_JSON);
}

protected String[] getDeltasFromUpdateNotificationBySource(final String sourceName, final int deltaPosition) {
final UpdateNotificationFile updateNotificationResponse = getResponseFromHttpsRequest(sourceName +
"/update-notification-file.json", MediaType.APPLICATION_JSON).readEntity(UpdateNotificationFile.class);
final UpdateNotificationFile updateNotificationResponse = getNotificationFileBySource(sourceName);

final String response = getResponseFromHttpsRequest(sourceName + "/" + getDeltaNameFromUpdateNotification(updateNotificationResponse, deltaPosition), "application/json-seq").readEntity(String.class);
return StringUtils.split( response, NrtmFileUtil.RECORD_SEPERATOR);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
package net.ripe.db.whois.api.nrtmv4;

import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.Lists;
import com.google.common.net.HttpHeaders;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
Expand All @@ -21,6 +35,7 @@
import net.ripe.db.nrtm4.domain.NrtmVersionInfo;
import net.ripe.db.nrtm4.domain.SnapshotFile;
import net.ripe.db.nrtm4.util.Ed25519Util;
import net.ripe.db.nrtm4.util.JWSUtil;
import net.ripe.db.nrtm4.util.NrtmFileUtil;
import net.ripe.db.whois.api.AbstractNrtmIntegrationTest;
import net.ripe.db.whois.common.TestDateTimeProvider;
Expand All @@ -44,6 +59,7 @@

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.text.ParseException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
Expand All @@ -54,6 +70,7 @@
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Tag("IntegrationTest")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
Expand Down Expand Up @@ -239,33 +256,34 @@ public void should_get_snapshot_file() throws IOException, JSONException {

assertThat(rpslKeys.size(), is(7));
assertThat(rpslKeys, containsInAnyOrder("::/0",
"0.0.0.0 - 255.255.255.255",
"AS100 - AS200",
"AS102",
"31.12.202.in-addr.arpa",
"OWNER-MNT",
"ORG-TEST1-TEST"));
"0.0.0.0 - 255.255.255.255",
"AS100 - AS200",
"AS102",
"31.12.202.in-addr.arpa",
"OWNER-MNT",
"ORG-TEST1-TEST"));
}

@Test
public void should_get_all_source_links() {
databaseHelper.getNrtmTemplate().update("INSERT INTO source (id, name) VALUES (?,?)", 1, "TEST");
databaseHelper.getNrtmTemplate().update("INSERT INTO source (id, name) VALUES (?,?)", 2, "TEST-NONAUTH");

final Response response = getResponseFromHttpsRequest( null, MediaType.TEXT_HTML);
final Response response = getResponseFromHttpsRequest(null, MediaType.TEXT_HTML);

assertThat(response.readEntity(String.class), is("<html><header><title>NRTM Version 4</title></header><body><a " +
"href='https://nrtm" +
".db.ripe.net/TEST/update-notification-file.json'>TEST</a><br/><a href='https://nrtm.db.ripe.net/TEST-NONAUTH/update-notification-file.json'>TEST-NONAUTH</a><br/><body></html>"));
}

@Test
public void should_get_update_notification_file() {
public void should_get_update_notification_file_old_method() {
insertUpdateNotificationFile();
final Response response = getResponseFromHttpsRequest("TEST/update-notification-file.json", MediaType.APPLICATION_JSON);

assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode()));
assertThat(response.getHeaderString(HttpHeaders.CACHE_CONTROL), is("public, max-age=60"));
assertThat(response.getHeaderString(HttpHeaders.CONTENT_TYPE), is(MediaType.APPLICATION_JSON));
assertThat(response.readEntity(String.class), containsString("\"source\":\"TEST\""));

final Response responseNonAuth = getResponseFromHttpsRequest("TEST-NONAUTH/update-notification-file.json", MediaType.APPLICATION_JSON);
Expand All @@ -276,9 +294,9 @@ public void should_get_update_notification_file() {
}

@Test
public void should_get_signature_file() {
public void should_get_signature_file_old_method() {
insertUpdateNotificationFile();
generateAndSaveKeyPair();
generateAndSaveKeyPairEd25519();

final String notificationFile = getResponseFromHttpsRequest("TEST/update-notification-file.json", MediaType.APPLICATION_JSON).readEntity(String.class);

Expand All @@ -302,29 +320,68 @@ public void should_get_base64_signature(){
}

@Test
public void should_fail_to_verify_signature_file() {
public void should_get_notification_file_for_each_source() throws ParseException {
insertUpdateNotificationFile();
generateAndSaveKeyPair();

final String notificationFile = getResponseFromHttpsRequest("TEST/update-notification-file.json", MediaType.APPLICATION_JSON).readEntity(String.class);
final Response response = getResponseFromHttpsRequest("TEST/update-notification-file.jose", MediaType.APPLICATION_JSON);

final Response response = getResponseFromHttpsRequest("TEST/update-notification-file.json.sig", MediaType.APPLICATION_JSON);
assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode()));
assertThat(response.getHeaderString(HttpHeaders.CACHE_CONTROL), is("public, max-age=60"));
assertThat(response.getHeaderString(HttpHeaders.CONTENT_TYPE), is("application/jose+json"));

final String signature = response.readEntity(String.class);
final byte[] publicKey = ((Ed25519PublicKeyParameters) Ed25519Util.generateEd25519KeyPair().getPublic()).getEncoded();
assertThat(Ed25519Util.verifySignature(signature, publicKey, notificationFile.getBytes()), is(Boolean.FALSE));
final JWSObject jwsObjectParsed = JWSObject.parse(response.readEntity(String.class));
assertTrue(JWSUtil.verifySignature(jwsObjectParsed, nrtmKeyConfigDao.getActivePublicKey()));

assertThat(jwsObjectParsed.getPayload().toString(), containsString("\"source\":\"TEST\""));

final Response responseNonAuth = getResponseFromHttpsRequest("TEST-NONAUTH/update-notification-file.jose", MediaType.APPLICATION_JSON);
final JWSObject jwsObjectParsedNonAuth = JWSObject.parse(responseNonAuth.readEntity(String.class));

assertTrue(JWSUtil.verifySignature(jwsObjectParsedNonAuth, nrtmKeyConfigDao.getActivePublicKey()));

assertThat(responseNonAuth.getStatus(), is(Response.Status.OK.getStatusCode()));
assertThat(responseNonAuth.getHeaderString(HttpHeaders.CACHE_CONTROL), is("public, max-age=60"));
assertThat(jwsObjectParsedNonAuth.getPayload().toString(), containsString("\"source\":\"TEST-NONAUTH\""));
}

@Test
public void should_get_jws_signature_notification_file() throws ParseException {
insertUpdateNotificationFile();
generateAndSaveKeyPair();

final String notificationFile = getResponseFromHttpsRequest("TEST/update-notification-file.jose", MediaType.APPLICATION_JSON).readEntity(String.class);

final JWSObject jwsObjectParsed = JWSObject.parse(notificationFile);

assertTrue(JWSUtil.verifySignature(jwsObjectParsed, nrtmKeyConfigDao.getActivePublicKey()));
assertThat(jwsObjectParsed.getPayload().toString(), containsString("https://nrtm.ripe.net//4e0c9366-0eb2-42be-bc20-f66d11791d49/nrtm-snapshot.1.RIPE.abb5672a6f3f533ce8caf76b0a3fe995.json.gz"));
}

@Test
public void should_fail_to_verify_signature_file() throws ParseException {
insertUpdateNotificationFile();
nrtmKeyPairService.generateKeyRecord(true);

final String notificationFile = getResponseFromHttpsRequest("TEST/update-notification-file.jose", MediaType.APPLICATION_JSON).readEntity(String.class);

final JWSObject jwsObjectParsed = JWSObject.parse(notificationFile);
assertTrue(JWSUtil.verifySignature(jwsObjectParsed, nrtmKeyConfigDao.getActivePublicKey()));

testDateTimeProvider.setTime(testDateTimeProvider.getCurrentDateTime().plusDays(4));
nrtmKeyPairService.forceRotateKey();

assertFalse(JWSUtil.verifySignature(jwsObjectParsed, nrtmKeyConfigDao.getActivePublicKey()));
}

@Test
public void should_throw_exeption_no_key_exists() {
insertUpdateNotificationFile();

assertThrows(InternalServerErrorException.class, () -> getWebTarget("TEST/update-notification-file.json.sig")
.request(MediaType.APPLICATION_JSON)
.header(HttpHeader.X_FORWARDED_PROTO.asString(), HttpScheme.HTTPS.asString())
.get(String.class));
assertThrows(InternalServerErrorException.class, () -> getWebTarget("TEST/update-notification-file.jose")
.request(MediaType.APPLICATION_JSON)
.header(HttpHeader.X_FORWARDED_PROTO.asString(), HttpScheme.HTTPS.asString())
.get(String.class));
}

@Test
Expand Down Expand Up @@ -389,13 +446,6 @@ public void should_throw_exception_invalid_notification_filename() {
assertThat(response.readEntity(String.class), is("Notification file does not exists"));
}

@Test
public void should_throw_exception_invalid_notification_sig_filename() {
final Response response = getResponseFromHttpsRequest("TEST/update-notification-file.aaaaa.sig", MediaType.APPLICATION_OCTET_STREAM);
assertThat(response.getStatus(), is(404));
assertThat(response.readEntity(String.class), is("Notification file does not exists"));
}

@Test
public void should_throw_exception_invalid_source_filename_combo() {
final Response response = getResponseFromHttpsRequest("TEST/nrtm-snapshot.1.TEST-NONAUTH.4e9e8c4e4891411be.json" +
Expand All @@ -406,7 +456,7 @@ public void should_throw_exception_invalid_source_filename_combo() {

@Test
public void should_throw_exception_invalid_source_notification_file() {
final Response response = getResponseFromHttpsRequest("TEST/update-notification-file.json", MediaType.APPLICATION_JSON);
final Response response = getResponseFromHttpsRequest("TEST/update-notification-file.jose", MediaType.APPLICATION_JSON);
assertThat(response.getStatus(), is(400));
assertThat(response.readEntity(String.class), is("Invalid source"));
}
Expand Down Expand Up @@ -494,6 +544,10 @@ INSERT INTO version_info (id, source_id, version, session_id, type, last_serial_
}

private void generateAndSaveKeyPair() {
nrtmKeyPairService.generateActiveKeyPair();
}

private void generateAndSaveKeyPairEd25519() {
final AsymmetricCipherKeyPair asymmetricCipherKeyPair = Ed25519Util.generateEd25519KeyPair();
final byte[] privateKey =((Ed25519PrivateKeyParameters) asymmetricCipherKeyPair.getPrivate()).getEncoded();
final byte[] publicKey = ((Ed25519PublicKeyParameters) asymmetricCipherKeyPair.getPublic()).getEncoded();
Expand Down
Loading