Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;

/**
Expand Down Expand Up @@ -123,6 +126,23 @@ void storeAuthorityEntry(final AuthorityEntry authorityEntry) throws KeyStoreExc
this.flush();
}

void deleteEntriesByAliasPrefix(final String aliasPrefix) throws KeyStoreException {
List<String> aliasesToDelete = new ArrayList<>();
Enumeration<String> aliases = javaKeyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (alias.startsWith(aliasPrefix)) {
aliasesToDelete.add(alias);
}
}

for (String alias : aliasesToDelete) {
javaKeyStore.deleteEntry(alias);
}

this.flush();
}

// Flush KeyStore to disk, to the configured keyStoreFilePath
private void flush() throws KeyStoreException {
try (OutputStream outputStream = Files.newOutputStream(keyStoreFilePath)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
import java.io.IOException;
import java.nio.file.Path;
import java.security.KeyStoreException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
Expand Down Expand Up @@ -206,18 +211,44 @@ private void storeX509ContextUpdate(final X509Context update) throws KeyStoreExc
}

private void storeBundle(TrustDomain trustDomain, X509Bundle bundle) throws KeyStoreException {
List<X509Certificate> authorities = sortedAuthorities(bundle);
trustStore.deleteEntriesByAliasPrefix(generateAliasPrefix(trustDomain));

int index = 0;
for (X509Certificate certificate : bundle.getX509Authorities()) {
for (X509Certificate certificate : authorities) {
final AuthorityEntry authorityEntry = AuthorityEntry.builder()
.alias(generateAlias(trustDomain, index))
.certificate(certificate)
.build();
trustStore.storeAuthorityEntry(authorityEntry);
index++;
}
}

private List<X509Certificate> sortedAuthorities(X509Bundle bundle) throws KeyStoreException {
List<X509Certificate> authorities = new ArrayList<>(bundle.getX509Authorities());
try {
authorities.sort(Comparator.comparing(this::certificateSortKey));
} catch (IllegalArgumentException e) {
throw new KeyStoreException("X.509 authority cannot be encoded", e);
}
return authorities;
}

private String certificateSortKey(final X509Certificate certificate) {
try {
return Base64.getEncoder().encodeToString(certificate.getEncoded());
} catch (CertificateEncodingException e) {
throw new IllegalArgumentException(e);
}
}

private String generateAliasPrefix(final TrustDomain trustDomain) {
return trustDomain.getName().concat(".");
}

private String generateAlias(final TrustDomain trustDomain, int index) {
return trustDomain.getName().concat(".").concat(String.valueOf(index));
return generateAliasPrefix(trustDomain).concat(String.valueOf(index));
}

private boolean isClosed() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package io.spiffe.helper.keystore;

import io.spiffe.bundle.x509bundle.X509Bundle;
import io.spiffe.bundle.x509bundle.X509BundleSet;
import io.spiffe.exception.SocketEndpointAddressException;
import io.spiffe.helper.exception.KeyStoreHelperException;
import io.spiffe.internal.CertificateUtils;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
import io.spiffe.utils.X509CertificateTestUtils;
import io.spiffe.workloadapi.Address;
import io.spiffe.workloadapi.Watcher;
import io.spiffe.workloadapi.WorkloadApiClient;
import io.spiffe.workloadapi.X509Context;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;

import java.io.InputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -24,9 +31,13 @@
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;

class KeyStoreHelperTest {
Expand Down Expand Up @@ -121,6 +132,134 @@ void testNewHelper_use_default_type_and_alias() throws KeyStoreException, Socket
checkBundleEntries(trustStoreFilePath, trustStorePass, KeyStoreType.getDefaultType(), authority1Alias);
}

@Test
void testNewHelper_storesMultipleAuthoritiesForSameTrustDomain() throws Exception {
String keyStorefileName = RandomStringUtils.randomAlphabetic(10);
keyStoreFilePath = Paths.get(keyStorefileName);

String trustStoreFileName = RandomStringUtils.randomAlphabetic(10);
trustStoreFilePath = Paths.get(trustStoreFileName);

String trustStorePass = "truststore123";
String keyStorePass = "keystore123";
String keyPass = "keypass123";
KeyStoreType keyStoreType = KeyStoreType.JKS;

Set<X509Certificate> authorities = new HashSet<>();
authorities.add(X509CertificateTestUtils.createRootCA("CN=Root CA 1", "spiffe://example.org").getCertificate());
authorities.add(X509CertificateTestUtils.createRootCA("CN=Root CA 2", "spiffe://example.org").getCertificate());

WorkloadApiClient client = new WorkloadApiClientStub() {
@Override
public void watchX509Context(Watcher<X509Context> watcher) {
X509Context context = fetchX509Context();
X509Bundle bundle = new X509Bundle(TrustDomain.parse("example.org"), authorities);
X509BundleSet bundleSet = X509BundleSet.of(Collections.singleton(bundle));
watcher.onUpdate(X509Context.of(context.getX509Svids(), bundleSet));
}
};

final KeyStoreHelper.KeyStoreOptions options = KeyStoreHelper.KeyStoreOptions
.builder()
.keyStoreType(keyStoreType)
.keyStorePath(keyStoreFilePath)
.keyStorePass(keyStorePass)
.trustStorePath(trustStoreFilePath)
.trustStorePass(trustStorePass)
.keyPass(keyPass)
.workloadApiClient(client)
.build();

try (KeyStoreHelper keystoreHelper = KeyStoreHelper.create(options)) {
keystoreHelper.run(false);
} catch (KeyStoreHelperException e) {
fail(e);
}

KeyStore trustStore = java.security.KeyStore.getInstance(keyStoreType.value());
try (InputStream trustStoreInputStream = Files.newInputStream(trustStoreFilePath)) {
trustStore.load(trustStoreInputStream, trustStorePass.toCharArray());
}

Certificate authority1 = trustStore.getCertificate("example.org.0");
Certificate authority2 = trustStore.getCertificate("example.org.1");
assertNotNull(authority1);
assertNotNull(authority2);
assertEquals(2, trustStore.size());
Set<X509Certificate> storedAuthorities = new HashSet<>();
storedAuthorities.add((X509Certificate) authority1);
storedAuthorities.add((X509Certificate) authority2);
assertEquals(authorities, storedAuthorities);
}

@Test
void testNewHelper_removesStaleAuthoritiesForSameTrustDomain() throws Exception {
String keyStorefileName = RandomStringUtils.randomAlphabetic(10);
keyStoreFilePath = Paths.get(keyStorefileName);

String trustStoreFileName = RandomStringUtils.randomAlphabetic(10);
trustStoreFilePath = Paths.get(trustStoreFileName);

String trustStorePass = "truststore123";
String keyStorePass = "keystore123";
String keyPass = "keypass123";
KeyStoreType keyStoreType = KeyStoreType.JKS;

X509Certificate authority1 = X509CertificateTestUtils.createRootCA("CN=Root CA 1", "spiffe://example.org").getCertificate();
X509Certificate authority2 = X509CertificateTestUtils.createRootCA("CN=Root CA 2", "spiffe://example.org").getCertificate();

Set<X509Certificate> initialAuthorities = new HashSet<>();
initialAuthorities.add(authority1);
initialAuthorities.add(authority2);

Set<X509Certificate> rotatedAuthorities = new HashSet<>();
rotatedAuthorities.add(authority1);

WorkloadApiClient client = new WorkloadApiClientStub() {
@Override
public void watchX509Context(Watcher<X509Context> watcher) {
X509Context context = fetchX509Context();
X509Bundle initialBundle = new X509Bundle(TrustDomain.parse("example.org"), initialAuthorities);
watcher.onUpdate(X509Context.of(
context.getX509Svids(),
X509BundleSet.of(Collections.singleton(initialBundle))
));

X509Bundle rotatedBundle = new X509Bundle(TrustDomain.parse("example.org"), rotatedAuthorities);
watcher.onUpdate(X509Context.of(
context.getX509Svids(),
X509BundleSet.of(Collections.singleton(rotatedBundle))
));
}
};

final KeyStoreHelper.KeyStoreOptions options = KeyStoreHelper.KeyStoreOptions
.builder()
.keyStoreType(keyStoreType)
.keyStorePath(keyStoreFilePath)
.keyStorePass(keyStorePass)
.trustStorePath(trustStoreFilePath)
.trustStorePass(trustStorePass)
.keyPass(keyPass)
.workloadApiClient(client)
.build();

try (KeyStoreHelper keystoreHelper = KeyStoreHelper.create(options)) {
keystoreHelper.run(false);
} catch (KeyStoreHelperException e) {
fail(e);
}

KeyStore trustStore = java.security.KeyStore.getInstance(keyStoreType.value());
try (InputStream trustStoreInputStream = Files.newInputStream(trustStoreFilePath)) {
trustStore.load(trustStoreInputStream, trustStorePass.toCharArray());
}

assertEquals(authority1, trustStore.getCertificate("example.org.0"));
assertNull(trustStore.getCertificate("example.org.1"));
assertEquals(1, trustStore.size());
}

@Test
void testCreateHelper_keyStore_trustStore_same_file_throwsException() throws SocketEndpointAddressException {

Expand Down Expand Up @@ -337,7 +476,9 @@ private void checkPrivateKeyEntry(Path keyStoreFilePath,

KeyStore keyStore = java.security.KeyStore.getInstance(keyStoreType.value());

keyStore.load(Files.newInputStream(keyStoreFilePath), keyStorePassword.toCharArray());
try (InputStream keyStoreInputStream = Files.newInputStream(keyStoreFilePath)) {
keyStore.load(keyStoreInputStream, keyStorePassword.toCharArray());
}
Certificate[] chain = keyStore.getCertificateChain(alias);
SpiffeId spiffeId = CertificateUtils.getSpiffeId((X509Certificate) chain[0]);
PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, privateKeyPassword.toCharArray());
Expand All @@ -354,7 +495,9 @@ private void checkBundleEntries(Path keyStoreFilePath,
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {

KeyStore keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
keyStore.load(Files.newInputStream(keyStoreFilePath), keyStorePassword.toCharArray());
try (InputStream keyStoreInputStream = Files.newInputStream(keyStoreFilePath)) {
keyStore.load(keyStoreInputStream, keyStorePassword.toCharArray());
}
Certificate certificate = keyStore.getCertificate(alias);
assertNotNull(certificate);

Expand Down