aboutsummaryrefslogtreecommitdiffstats
path: root/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java')
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java162
1 files changed, 162 insertions, 0 deletions
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java
new file mode 100644
index 00000000000..f9f8b230154
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java
@@ -0,0 +1,162 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.configserver.certificate;
+
+import com.yahoo.test.ManualClock;
+import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerApi;
+import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.Date;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author freva
+ */
+public class ConfigServerKeyStoreRefresherTest {
+
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private final ManualClock clock = new ManualClock();
+ private final String commonName = "CertificateRefresherTest";
+ private final Duration certificateExpiration = Duration.ofDays(6);
+ private final ConfigServerApi configServerApi = mock(ConfigServerApi.class);
+ private final Runnable keyStoreUpdatedCallback = mock(Runnable.class);
+ private final ScheduledExecutorService executor = mock(ScheduledExecutorService.class);
+ private KeyStoreOptions keyStoreOptions;
+
+ @Before
+ public void setup() {
+ keyStoreOptions = new KeyStoreOptions(
+ tempFolder.getRoot().toPath().resolve("some/path/keystore.p12"), new char[0], "PKCS12", null);
+ }
+
+ @Test
+ public void manually_trigger_certificate_refresh() throws Exception {
+ X509Certificate firstCertificate = mockConfigServerCertificateSigning(1);
+
+ ConfigServerKeyStoreRefresher keyStoreRefresher = new ConfigServerKeyStoreRefresher(
+ keyStoreOptions, keyStoreUpdatedCallback, configServerApi, executor, clock, commonName);
+
+ // No keystore previously existed, so a new one should be written
+ assertTrue(keyStoreRefresher.refreshKeyStoreIfNeeded());
+ assertEquals(firstCertificate, keyStoreRefresher.getConfigServerCertificate());
+
+ // Calling it again before a third of certificate lifetime has passed has no effect
+ assertFalse(keyStoreRefresher.refreshKeyStoreIfNeeded());
+ assertEquals(firstCertificate, keyStoreRefresher.getConfigServerCertificate());
+
+ // After a third of the expiration time passes, we should refresh the certificate
+ clock.advance(certificateExpiration.dividedBy(3).plusSeconds(1));
+ X509Certificate secondCertificate = mockConfigServerCertificateSigning(2);
+ assertTrue(keyStoreRefresher.refreshKeyStoreIfNeeded());
+ assertEquals(secondCertificate, keyStoreRefresher.getConfigServerCertificate());
+
+ verify(configServerApi, times(2))
+ .post(eq(ConfigServerKeyStoreRefresher.CONFIG_SERVER_CERTIFICATE_SIGNING_PATH), any(), any());
+
+ // We're just triggering refresh manually, so callback and executor should not have been touched
+ verifyZeroInteractions(keyStoreUpdatedCallback);
+ verifyZeroInteractions(executor);
+ }
+
+ @Test
+ public void certificate_refresh_schedule_test() throws Exception {
+ ConfigServerKeyStoreRefresher keyStoreRefresher = new ConfigServerKeyStoreRefresher(
+ keyStoreOptions, keyStoreUpdatedCallback, configServerApi, executor, clock, commonName);
+
+ // No keystore exist, so refresh once
+ mockConfigServerCertificateSigning(1);
+ assertTrue(keyStoreRefresher.refreshKeyStoreIfNeeded());
+
+ // Start automatic refreshment, since keystore was just written, next check should be in 1/3rd of
+ // certificate lifetime, which is in 2 days.
+ keyStoreRefresher.start();
+ Duration nextExpectedExecution = Duration.ofDays(2);
+ verify(executor, times(1)).schedule(any(Runnable.class), eq(nextExpectedExecution.getSeconds()), eq(TimeUnit.SECONDS));
+
+ // First automatic refreshment goes without any problems
+ clock.advance(nextExpectedExecution);
+ mockConfigServerCertificateSigning(2);
+ keyStoreRefresher.refresh();
+ verify(executor, times(2)).schedule(any(Runnable.class), eq(nextExpectedExecution.getSeconds()), eq(TimeUnit.SECONDS));
+ verify(keyStoreUpdatedCallback).run();
+
+ // We fail to refresh the certificate, wait minimum amount of time and try again
+ clock.advance(nextExpectedExecution);
+ mockConfigServerCertificateSigningFailure(new RuntimeException());
+ keyStoreRefresher.refresh();
+ nextExpectedExecution = Duration.ofSeconds(ConfigServerKeyStoreRefresher.MINIMUM_SECONDS_BETWEEN_REFRESH_RETRY);
+ verify(executor, times(1)).schedule(any(Runnable.class), eq(nextExpectedExecution.getSeconds()), eq(TimeUnit.SECONDS));
+
+ clock.advance(nextExpectedExecution);
+ keyStoreRefresher.refresh();
+ verify(executor, times(2)).schedule(any(Runnable.class), eq(nextExpectedExecution.getSeconds()), eq(TimeUnit.SECONDS));
+ verifyNoMoreInteractions(keyStoreUpdatedCallback); // Callback not called after the last 2 failures
+
+ clock.advance(nextExpectedExecution);
+ mockConfigServerCertificateSigning(3);
+ keyStoreRefresher.refresh();
+ nextExpectedExecution = Duration.ofDays(2);
+ verify(executor, times(3)).schedule(any(Runnable.class), eq(nextExpectedExecution.getSeconds()), eq(TimeUnit.SECONDS));
+ verify(keyStoreUpdatedCallback, times(2)).run();
+ }
+
+ private X509Certificate mockConfigServerCertificateSigning(int serial) throws Exception {
+ X509Certificate certificate = makeCertificate(serial);
+
+ when(configServerApi.post(eq(ConfigServerKeyStoreRefresher.CONFIG_SERVER_CERTIFICATE_SIGNING_PATH), any(), any()))
+ .thenReturn(new CertificateSerializedPayload(certificate));
+ return certificate;
+ }
+
+ private void mockConfigServerCertificateSigningFailure(Exception exception) throws Exception {
+ when(configServerApi.post(eq(ConfigServerKeyStoreRefresher.CONFIG_SERVER_CERTIFICATE_SIGNING_PATH), any(), any()))
+ .thenThrow(exception);
+ }
+
+ private X509Certificate makeCertificate(int serial) throws Exception {
+ try {
+ KeyPair keyPair = ConfigServerKeyStoreRefresher.generateKeyPair();
+ X500Name subject = new X500Name("CN=" + commonName);
+ Date notBefore = Date.from(clock.instant());
+ Date notAfter = Date.from(clock.instant().plus(certificateExpiration));
+
+ JcaX509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(subject,
+ BigInteger.valueOf(serial), notBefore, notAfter, subject, keyPair.getPublic());
+ ContentSigner sigGen = new JcaContentSignerBuilder(ConfigServerKeyStoreRefresher.SIGNER_ALGORITHM)
+ .build(keyPair.getPrivate());
+ return new JcaX509CertificateConverter()
+ .setProvider(new BouncyCastleProvider())
+ .getCertificate(certGen.build(sigGen));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+} \ No newline at end of file