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

NRTMv4 Key Rotation #1484

Merged
merged 13 commits into from
Jul 4, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public Response nrtmFiles(
final String payload = updateNotificationFileSourceAwareDao.findLastNotification(getSource(source))
.orElseThrow(() -> new NotFoundException("update-notification-file.json does not exists for source " + source));

return fileName.endsWith(".sig") ? getResponse(signWithEd25519(payload.getBytes(), nrtmKeyConfigDao.getPrivateKey()))
return fileName.endsWith(".sig") ? getResponse(signWithEd25519(payload.getBytes(), nrtmKeyConfigDao.getActivePrivateKey()))
: getResponse(payload);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import net.ripe.db.nrtm4.generator.DeltaFileGenerator;
import net.ripe.db.nrtm4.generator.NrtmKeyPairService;
import net.ripe.db.nrtm4.generator.SnapshotFileGenerator;
import net.ripe.db.nrtm4.generator.UpdateNotificationFileGenerator;
import net.ripe.db.nrtm4.dao.NrtmKeyConfigDao;
Expand Down Expand Up @@ -47,6 +48,8 @@ public abstract class AbstractNrtmIntegrationTest extends AbstractIntegrationTes
@Autowired
protected DeltaFileGenerator deltaFileGenerator;

@Autowired
protected NrtmKeyPairService nrtmKeyPairService;

@BeforeEach
public void setup() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import net.ripe.db.nrtm4.domain.NrtmKeyRecord;
import net.ripe.db.nrtm4.generator.DeltaFileGenerator;
import net.ripe.db.nrtm4.generator.SnapshotFileGenerator;
import net.ripe.db.nrtm4.dao.DeltaFileDao;
Expand Down Expand Up @@ -286,7 +287,7 @@ public void should_get_signature_file() {

final String signature = response.readEntity(String.class);

assertThat(Ed25519Util.verifySignature(signature, nrtmKeyConfigDao.getPublicKey(), notificationFile.getBytes()), is(Boolean.TRUE));
assertThat(Ed25519Util.verifySignature(signature, nrtmKeyConfigDao.getActivePublicKey(), notificationFile.getBytes()), is(Boolean.TRUE));
}

@Test
Expand Down Expand Up @@ -497,6 +498,9 @@ private void generateAndSaveKeyPair() {
final byte[] privateKey =((Ed25519PrivateKeyParameters) asymmetricCipherKeyPair.getPrivate()).getEncoded();
final byte[] publicKey = ((Ed25519PublicKeyParameters) asymmetricCipherKeyPair.getPublic()).getEncoded();

nrtmKeyConfigDao.saveKeyPair(privateKey, publicKey);
final long createdTimestamp = dateTimeProvider.getCurrentDateTime().toEpochSecond(ZoneOffset.UTC);
final long expires = dateTimeProvider.getCurrentDateTime().plusYears(1).toEpochSecond(ZoneOffset.UTC);

nrtmKeyConfigDao.saveKeyPair(NrtmKeyRecord.of(privateKey, publicKey, true, createdTimestamp, expires));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package net.ripe.db.whois.api.nrtmv4;

import jakarta.xml.bind.DatatypeConverter;
import net.ripe.db.nrtm4.domain.NrtmKeyRecord;
import net.ripe.db.nrtm4.domain.UpdateNotificationFile;
import net.ripe.db.nrtm4.util.ByteArrayUtil;
import net.ripe.db.whois.api.AbstractNrtmIntegrationTest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.test.annotation.DirtiesContext;

import java.time.LocalDateTime;
import java.util.Base64;

import static net.ripe.db.whois.query.support.PatternMatcher.matchesPattern;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;

@Tag("IntegrationTest")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class NrtmKeyRotationTestIntegration extends AbstractNrtmIntegrationTest {

@Test
public void should_not_generate_next_signing_key_in_notification_file() {
setTime(LocalDateTime.now().minusDays(1));

snapshotFileGenerator.createSnapshot();

setTime(LocalDateTime.now().plusYears(1).minusDays(9));

updateNotificationFileGenerator.generateFile();

final UpdateNotificationFile testIteration = getNotificationFileBySource("TEST");
final UpdateNotificationFile testNonAuthIteration = getNotificationFileBySource("TEST-NONAUTH");

assertThat(testIteration.getSource().getName(), is("TEST"));
assertThat(testIteration.getNextSigningKey(), is(nullValue()));

assertThat(testNonAuthIteration.getSource().getName(), is("TEST-NONAUTH"));
assertThat(testNonAuthIteration.getNextSigningKey(), is(nullValue()));
}

@Test
public void should_add_next_signing_key_in_notification_file() {
setTime(LocalDateTime.now().minusDays(1));

snapshotFileGenerator.createSnapshot();

setTime(LocalDateTime.now().plusYears(1).minusDays(7));

nrtmKeyPairService.generateOrRotateNextKey();
updateNotificationFileGenerator.generateFile();

final UpdateNotificationFile testIteration = getNotificationFileBySource("TEST");
final UpdateNotificationFile testNonAuthIteration = getNotificationFileBySource("TEST-NONAUTH");

final String nextKey = Base64.getEncoder().encodeToString(nrtmKeyPairService.getNextkeyPair().publicKey());
assertThat(testIteration.getSource().getName(), is("TEST"));
assertThat(testIteration.getNextSigningKey(), is(nextKey));

assertThat(testNonAuthIteration.getSource().getName(), is("TEST-NONAUTH"));
assertThat(testNonAuthIteration.getNextSigningKey(), is(nextKey));
}

@Test
public void should_rotate_next_key_as_new_key() {

//No new signing next key till expiry is greater than 7 days
setTime(LocalDateTime.now());

snapshotFileGenerator.createSnapshot();
nrtmKeyPairService.generateOrRotateNextKey();

assertThat(nrtmKeyPairService.getNextkeyPair(), is(nullValue()));

//New signing next key when expiry is smaller than 7 days
setTime(LocalDateTime.now().plusYears(1).minusDays(7));

nrtmKeyPairService.generateOrRotateNextKey();

final String nextKey = ByteArrayUtil.byteArrayToHexString(nrtmKeyPairService.getNextkeyPair().publicKey());
assertThat(nrtmKeyPairService.getNextkeyPair(), is(not(nullValue())));

//New signing next key will be the active key now and no next signing key
setTime(LocalDateTime.now().plusYears(1));
nrtmKeyPairService.generateOrRotateNextKey();

final String newCurrentKey = ByteArrayUtil.byteArrayToHexString(nrtmKeyConfigDao.getActivePublicKey());
assertThat(nextKey, is(newCurrentKey));
assertThat(nrtmKeyPairService.getNextkeyPair(), is(nullValue()));
}

@Test
public void should_force_rotate() {

//No new signing next key till expiry is greater than 7 days
setTime(LocalDateTime.now());

snapshotFileGenerator.createSnapshot();
nrtmKeyPairService.generateOrRotateNextKey();

assertThat(nrtmKeyPairService.getNextkeyPair(), is(nullValue()));

//New signing next key when expiry is smaller than 7 days
setTime(LocalDateTime.now().plusYears(1).minusDays(7));

nrtmKeyPairService.generateOrRotateNextKey();

assertThat(nrtmKeyPairService.getNextkeyPair(), is(not(nullValue())));

final String currentActiveKey = ByteArrayUtil.byteArrayToHexString(nrtmKeyConfigDao.getActivePublicKey());

nrtmKeyPairService.deleteAndGenerateNewActiveKey();

final String newActiveKey = ByteArrayUtil.byteArrayToHexString(nrtmKeyConfigDao.getActivePublicKey());

assertThat(newActiveKey , is(not(currentActiveKey)));
assertThat(nrtmKeyPairService.getNextkeyPair(), is(nullValue()));

}

@Test
public void should_make_next_key_as_active() {

//No new signing next key till expiry is greater than 7 days
setTime(LocalDateTime.now());

snapshotFileGenerator.createSnapshot();
nrtmKeyPairService.generateOrRotateNextKey();

assertThat(nrtmKeyPairService.getNextkeyPair(), is(nullValue()));

//New signing next key when expiry is smaller than 7 days
setTime(LocalDateTime.now().plusYears(1).minusDays(7));

nrtmKeyPairService.generateOrRotateNextKey();

assertThat(nrtmKeyPairService.getNextkeyPair(), is(not(nullValue())));

final String currentActiveKey = ByteArrayUtil.byteArrayToHexString(nrtmKeyConfigDao.getActivePublicKey());
final String nextKey = ByteArrayUtil.byteArrayToHexString(nrtmKeyPairService.getNextkeyPair().publicKey());

nrtmKeyPairService.forceRotateKey();

final String newActiveKey = ByteArrayUtil.byteArrayToHexString(nrtmKeyConfigDao.getActivePublicKey());

assertThat(newActiveKey , is(nextKey));
assertThat(nrtmKeyPairService.getNextkeyPair(), is(nullValue()));

}

@Test
public void should_get_next_key_from_multiple_inactive_key() {
setTime(LocalDateTime.now());
nrtmKeyPairService.generateKeyRecord(false);

final NrtmKeyRecord oldestKey = nrtmKeyConfigDao.getAllKeyPair().get(0);
System.out.println(DatatypeConverter.printHexBinary(oldestKey.publicKey()));

nrtmKeyPairService.generateKeyRecord(true);

setTime(LocalDateTime.now().plusYears(1).plusMonths(10));
nrtmKeyPairService.generateKeyRecord(false);

setTime(LocalDateTime.now().plusYears(1).minusDays(7));

final NrtmKeyRecord expectedNextKey = nrtmKeyConfigDao.getAllKeyPair().stream().filter( nrtmKeyRecord -> nrtmKeyRecord.isActive() == false && nrtmKeyRecord.id() != oldestKey.id()).findFirst().get();
nrtmKeyPairService.generateOrRotateNextKey();

assertThat(expectedNextKey.id(), is(nrtmKeyPairService.getNextkeyPair().id()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.xml.bind.DatatypeConverter;
import net.ripe.db.nrtm4.domain.NrtmDocumentType;
import net.ripe.db.nrtm4.domain.NrtmKeyRecord;
import net.ripe.db.nrtm4.domain.UpdateNotificationFile;
import net.ripe.db.nrtm4.util.ByteArrayUtil;
import net.ripe.db.whois.api.AbstractNrtmIntegrationTest;
import net.ripe.db.whois.common.rpsl.RpslObject;
import org.junit.jupiter.api.Tag;
Expand All @@ -14,13 +17,16 @@
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Base64;
import java.util.UUID;

import static net.ripe.db.whois.query.support.PatternMatcher.matchesPattern;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;

@Tag("IntegrationTest")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
Expand Down Expand Up @@ -145,11 +151,12 @@ public void should_create_file_with_changes_both_sources() {

assertThat(testIteration.getSource().getName(), is("TEST"));
assertThat(testNonAuthIteration.getSource().getName(), is("TEST-NONAUTH"));
assertThat(testNonAuthIteration.getNextSigningKey(), is(nullValue()));
assertThat(testIteration.getNextSigningKey(), is(nullValue()));

assertThat(testIteration.getSessionID(), is(not(testNonAuthIteration.getSessionID())));

}

@Test
public void should_contain_snapshot_delta_url(){
final RpslObject rpslObject = RpslObject.parse("" +
Expand Down
1 change: 1 addition & 0 deletions whois-commons/src/main/resources/nrtm_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ CREATE TABLE `key_pair`
`public_key` VARBINARY(3000) NOT NULL,
`created` bigint unsigned NOT NULL,
`expires` bigint unsigned NOT NULL,
`is_active` bit(1) NOT NULL DEFAULT b'0',
UNIQUE KEY `private_key_name_uk` (`private_key`),
UNIQUE KEY `public_key_name_uk` (`public_key`),
PRIMARY KEY (`id`)
Expand Down
10 changes: 10 additions & 0 deletions whois-commons/src/main/resources/patch/nrtm-1.114.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- add isActive column in in kay_pair table

ALTER TABLE key_pair ADD is_active bit(1) NOT NULL DEFAULT b'0'

--There is only one active key entry
UPDATE key_pair SET isActive = b'1';

TRUNCATE version;
INSERT INTO version VALUES ('nrtm-1.114');

Original file line number Diff line number Diff line change
@@ -1,51 +1,78 @@
package net.ripe.db.nrtm4.dao;

import jakarta.ws.rs.InternalServerErrorException;
import net.ripe.db.whois.common.DateTimeProvider;
import net.ripe.db.nrtm4.domain.NrtmKeyRecord;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.time.ZoneOffset;
import java.util.List;

@Repository
public class NrtmKeyConfigDao {
maggarwal13 marked this conversation as resolved.
Show resolved Hide resolved

private final JdbcTemplate readTemplate;
private final JdbcTemplate writeTemplate;
private final DateTimeProvider dateTimeProvider;


NrtmKeyConfigDao(@Qualifier("nrtmSlaveDataSource") final DataSource readOnlyDataSource, @Qualifier("nrtmMasterDataSource") final DataSource writeDataSource, final DateTimeProvider dateTimeProvider ) {
NrtmKeyConfigDao(@Qualifier("nrtmSlaveDataSource") final DataSource readOnlyDataSource, @Qualifier("nrtmMasterDataSource") final DataSource writeDataSource) {
this.readTemplate = new JdbcTemplate(readOnlyDataSource);
this.dateTimeProvider = dateTimeProvider;
this.writeTemplate = new JdbcTemplate(writeDataSource);
}

public byte[] getPrivateKey() {
return readTemplate.queryForObject("SELECT private_key FROM key_pair", (rs, rowNum) -> rs.getBytes(1));
public byte[] getActivePrivateKey() {
MiguelAHM marked this conversation as resolved.
Show resolved Hide resolved
return readTemplate.queryForObject("SELECT private_key FROM key_pair where is_active= true", (rs, rowNum) -> rs.getBytes(1));
}

public byte[] getPublicKey() {
return readTemplate.queryForObject("SELECT public_key FROM key_pair", (rs, rowNum) -> rs.getBytes(1));
public byte[] getActivePublicKey() {
return readTemplate.queryForObject("SELECT public_key FROM key_pair where is_active= true", (rs, rowNum) -> rs.getBytes(1));
}

public void saveKeyPair( final byte[] privateKey, final byte[] publicKey) {
public void saveKeyPair(final NrtmKeyRecord nrtmKeyRecord) {
final String sql = """
INSERT INTO key_pair (private_key, public_key, created, expires)
VALUES (?, ?, ?, ?)
INSERT INTO key_pair (private_key, public_key, created, expires, is_active)
VALUES (?, ?, ?, ?, ?)
""";

final long createdTimestamp = dateTimeProvider.getCurrentDateTime().toEpochSecond(ZoneOffset.UTC);
final long expires = dateTimeProvider.getCurrentDateTime().plusYears(1).toEpochSecond(ZoneOffset.UTC);
writeTemplate.update(sql,privateKey, publicKey, createdTimestamp, expires);
writeTemplate.update(sql,nrtmKeyRecord.privateKey(), nrtmKeyRecord.publicKey(), nrtmKeyRecord.createdTimestamp(), nrtmKeyRecord.expires(), nrtmKeyRecord.isActive());
}

public void deleteKeyPair(final NrtmKeyRecord nrtmKeyRecord) {
writeTemplate.update("DELETE FROM key_pair WHERE id = ?", nrtmKeyRecord.id());
}

public void makeCurrentActiveKeyAsInActive() {
writeTemplate.update("UPDATE key_pair SET is_active = false WHERE is_active = true");
}

public NrtmKeyRecord getActiveKeyPair() {
return readTemplate.queryForObject(
"SELECT id, private_key, public_key, is_active, created, expires FROM key_pair WHERE is_active = true",
(rs, rn) -> new NrtmKeyRecord(rs.getLong(1),
rs.getBytes(2),
rs.getBytes(3),
rs.getBoolean(4),
rs.getLong(5),
rs.getLong(6))
);
}

public List<NrtmKeyRecord> getAllKeyPair() {
return readTemplate.query(
"SELECT id, private_key, public_key, is_active, created, expires FROM key_pair",
(rs, rn) -> new NrtmKeyRecord(rs.getLong(1),
rs.getBytes(2),
rs.getBytes(3),
rs.getBoolean(4),
rs.getLong(5),
rs.getLong(6))
);
}

public boolean isKeyPairExists() {
final int count = writeTemplate.queryForObject("SELECT count(*) FROM key_pair", Integer.class);
public boolean isActiveKeyPairExists() {
final int count = writeTemplate.queryForObject("SELECT count(*) FROM key_pair WHERE is_active=true", Integer.class);
if(count > 1) {
throw new InternalServerErrorException("More than one key pair exists");
throw new InternalServerErrorException("More than one active key pair exists");
}

return count == 1;
Expand Down
Loading