Skip to content

Commit

Permalink
feat: add certificate chain config (#198)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nelson Ochoa authored Nov 10, 2022
1 parent 8a7a8e8 commit 96065e6
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.aws.greengrass.clientdevices.auth.configuration.CAConfiguration;
import com.aws.greengrass.clientdevices.auth.connectivity.CISShadowMonitor;
import com.aws.greengrass.clientdevices.auth.connectivity.ConnectivityInformation;
import com.aws.greengrass.clientdevices.auth.exception.CertificateChainLoadingException;
import com.aws.greengrass.clientdevices.auth.exception.CertificateGenerationException;
import com.aws.greengrass.clientdevices.auth.exception.CloudServiceInteractionException;
import com.aws.greengrass.clientdevices.auth.exception.InvalidCertificateAuthorityException;
Expand All @@ -28,6 +29,7 @@
import com.aws.greengrass.logging.impl.LogManager;
import com.aws.greengrass.security.SecurityService;
import com.aws.greengrass.security.exceptions.ServiceUnavailableException;
import com.aws.greengrass.util.EncryptionUtils;
import com.aws.greengrass.util.GreengrassServiceClientFactory;
import com.aws.greengrass.util.RetryUtils;
import lombok.NonNull;
Expand All @@ -38,6 +40,7 @@

import java.io.IOException;
import java.net.URI;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
Expand All @@ -50,6 +53,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import javax.inject.Inject;
Expand Down Expand Up @@ -255,6 +259,33 @@ private void removeCGFromMonitors(CertificateGenerator gen) {
caConfigurationMonitor.removeFromMonitor(gen);
}

@SuppressWarnings("PMD.AvoidCatchingGenericException")
private X509Certificate[] getCertificateChainFromConfiguration(CAConfiguration configuration) throws
CertificateChainLoadingException {
try {
Optional<URI> certificateChainUri = configuration.getCertificateChainUri();

if (certificateChainUri.isPresent()) {
List<X509Certificate> certificateChain =
EncryptionUtils.loadX509Certificates(Paths.get(certificateChainUri.get()));
return certificateChain.toArray(new X509Certificate[0]);
}

URI certificateUri = configuration.getCertificateUri().get();
URI privateKeyUri = configuration.getPrivateKeyUri().get();

RetryUtils.RetryConfig retryConfig =
RetryUtils.RetryConfig.builder().initialRetryInterval(Duration.ofSeconds(2)).maxAttempt(3)
.retryableExceptions(Collections.singletonList(ServiceUnavailableException.class)).build();

return RetryUtils.runWithRetry(retryConfig,
() -> certificateStore.loadCaCertificateChain(privateKeyUri, certificateUri),
"get-certificate-chain", logger);
} catch (Exception e) {
throw new CertificateChainLoadingException("Failed to load certificate chain", e);
}
}

/**
* Configures the KeyStore to use a certificates provided from the CA configuration.
*
Expand Down Expand Up @@ -283,9 +314,7 @@ public void configureCustomCA(CAConfiguration configuration) throws InvalidConfi
KeyPair keyPair = RetryUtils.runWithRetry(retryConfig,
() -> securityService.getKeyPair(privateKeyUri, certificateUri), "get-key-pair", logger);

X509Certificate[] certificateChain = RetryUtils.runWithRetry(retryConfig,
() -> certificateStore.loadCaCertificateChain(privateKeyUri, certificateUri),
"get-certificate-chain", logger);
X509Certificate[] certificateChain = getCertificateChainFromConfiguration(configuration);

CertificateHelper.ProviderType providerType =
privateKeyUri.getScheme().contains(pkcs11Scheme) ? CertificateHelper.ProviderType.HSM
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* <p>
* |---- configuration
* | |---- certificateAuthority:
* | |---- certificateChainUri "..."
* | |---- privateKeyUri: "..."
* | |---- certificateUri: "..."
* | |---- caType: [...]
Expand All @@ -36,6 +37,7 @@
public final class CAConfiguration {
public static final String CA_CERTIFICATE_URI = "certificateUri";
public static final String CA_PRIVATE_KEY_URI = "privateKeyUri";
public static final String CA_CERTIFICATE_CHAIN_URI = "certificateChainUri";
public static final String DEPRECATED_CA_TYPE_KEY = "ca_type";
public static final String CA_TYPE_KEY = "caType";
public static final String CERTIFICATE_AUTHORITY_TOPIC = "certificateAuthority";
Expand All @@ -45,14 +47,17 @@ public final class CAConfiguration {
private List<String> caTypeList;
private Optional<URI> privateKeyUri;
private Optional<URI> certificateUri;
private Optional<URI> certificateChainUri;



private CAConfiguration(List<String> caTypes, CertificateStore.CAType caType, Optional<URI> privateKeyUri,
Optional<URI> certificateUri) {
Optional<URI> certificateUri, Optional<URI> certificateChainUri) {
this.caType = caType;
this.caTypeList = caTypes;
this.privateKeyUri = privateKeyUri;
this.certificateUri = certificateUri;
this.certificateChainUri = certificateChainUri;
}

/**
Expand All @@ -67,7 +72,9 @@ public static CAConfiguration from(Topics configurationTopics) throws URISyntaxE
return new CAConfiguration(getCaTypeListFromConfiguration(configurationTopics),
getCaTypeFromConfiguration(configurationTopics),
getCaPrivateKeyUriFromConfiguration(certAuthorityTopic),
getCaCertificateUriFromConfiguration(certAuthorityTopic));
getCaCertificateUriFromConfiguration(certAuthorityTopic),
getCaCertificateChainUriFromConfiguration(certAuthorityTopic)
);
}

/**
Expand Down Expand Up @@ -132,9 +139,20 @@ private static URI getUri(String rawUri) throws URISyntaxException {
return uri;
}

private static Optional<URI> getCaCertificateChainUriFromConfiguration(Topics certAuthorityTopic)
throws URISyntaxException {
String certificateChainUri = Coerce.toString(certAuthorityTopic.find(CA_CERTIFICATE_CHAIN_URI));

if (Utils.isEmpty(certificateChainUri)) {
return Optional.empty();
}

return Optional.of(getUri(certificateChainUri));
}

private static Optional<URI> getCaPrivateKeyUriFromConfiguration(Topics certAuthorityTopic)
throws URISyntaxException {
String privateKeyUri = Coerce.toString(certAuthorityTopic.findOrDefault("", CA_PRIVATE_KEY_URI));
String privateKeyUri = Coerce.toString(certAuthorityTopic.find(CA_PRIVATE_KEY_URI));

if (Utils.isEmpty(privateKeyUri)) {
return Optional.empty();
Expand All @@ -145,7 +163,7 @@ private static Optional<URI> getCaPrivateKeyUriFromConfiguration(Topics certAuth

private static Optional<URI> getCaCertificateUriFromConfiguration(Topics certAuthorityTopic)
throws URISyntaxException {
String certificateUri = Coerce.toString(certAuthorityTopic.findOrDefault("", CA_CERTIFICATE_URI));
String certificateUri = Coerce.toString(certAuthorityTopic.find(CA_CERTIFICATE_URI));

if (Utils.isEmpty(certificateUri)) {
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,19 @@
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
Expand All @@ -64,6 +68,9 @@
import java.util.stream.Stream;

import static com.aws.greengrass.clientdevices.auth.ClientDevicesAuthService.CLIENT_DEVICES_AUTH_SERVICE_NAME;
import static com.aws.greengrass.clientdevices.auth.certificate.CertificateHelper.PEM_BOUNDARY_CERTIFICATE;
import static com.aws.greengrass.clientdevices.auth.certificate.CertificateHelper.PEM_BOUNDARY_PRIVATE_KEY;
import static com.aws.greengrass.clientdevices.auth.configuration.CAConfiguration.CA_CERTIFICATE_CHAIN_URI;
import static com.aws.greengrass.clientdevices.auth.configuration.CAConfiguration.CA_CERTIFICATE_URI;
import static com.aws.greengrass.clientdevices.auth.configuration.CAConfiguration.CA_PRIVATE_KEY_URI;
import static com.aws.greengrass.clientdevices.auth.configuration.CAConfiguration.CERTIFICATE_AUTHORITY_TOPIC;
Expand Down Expand Up @@ -91,6 +98,8 @@ public class CertificateManagerTest {
GreengrassServiceClientFactory clientFactoryMock;
@Mock
SecurityService securityServiceMock;
@TempDir
Path rootDir;


@TempDir
Expand Down Expand Up @@ -122,6 +131,28 @@ void afterEach() {
reset(securityServiceMock);
}

private Map<String, URI> storeCAOnFileSystem(PrivateKey pk, X509Certificate... chain)
throws IOException, CertificateEncodingException {
Path pkPath = rootDir.resolve("private.key").toAbsolutePath();
CertificateTestHelpers.writeToPath(pkPath, PEM_BOUNDARY_PRIVATE_KEY,
Collections.singletonList(pk.getEncoded()));

List<byte[]> encodings = new ArrayList<>();
for (X509Certificate x509Certificate : chain) {
byte[] encoded = x509Certificate.getEncoded();
encodings.add(encoded);
}

Path chainPath = rootDir.resolve("certificate.pem").toAbsolutePath();
CertificateTestHelpers.writeToPath(chainPath.toAbsolutePath(), PEM_BOUNDARY_CERTIFICATE, encodings);

return new HashMap<String, URI>() {{
put("privateKey", pkPath.toUri());
put("certificateAuthority", chainPath.toUri());
}};
}


@Test
void GIVEN_customCAConfiguration_WHEN_configureCustomCA_THEN_returnsCustomCA() throws Exception {
// Given
Expand Down Expand Up @@ -159,6 +190,52 @@ void GIVEN_customCAConfiguration_WHEN_configureCustomCA_THEN_returnsCustomCA() t
assertEquals(caPem, CertificateHelper.toPem(caCertificate));
}

@Test
void GIVEN_certificateChainConfiguration_WHEN_configureCustomCA_THEN_certificateChainFetchedFromCertificateChainUri() throws
Exception {
// Given
certificateManager.generateCA("", CertificateStore.CAType.RSA_2048);

Instant now = Instant.now();
KeyPair rootKeyPair = CertificateStore.newRSAKeyPair(2048);
X509Certificate rootCA =
CertificateHelper.createCACertificate(rootKeyPair, Date.from(now), Date.from(now.plusSeconds(10)),
"Custom Core CA", CertificateHelper.ProviderType.DEFAULT);
KeyPair intermediateKeyPair = CertificateStore.newRSAKeyPair(2048);
X509Certificate intermediateCA =
CertificateTestHelpers.createIntermediateCertificateAuthority(rootCA, "intermediate",
intermediateKeyPair.getPublic(), rootKeyPair.getPrivate());


X509Certificate[] certificateChain = {intermediateCA, rootCA};
storeCAOnFileSystem(intermediateKeyPair.getPrivate(), certificateChain);

URI certificateChainUri = rootDir.resolve("certificate.pem").toFile().toURI();
URI privateKeyUri = new URI("pkcs11:object=CustomerIntermediateCA;type=private");
URI certificateUri = new URI("pkcs11:object=CustomerIntermediateCA;type=cert");

Topics configurationTopics = Topics.of(new Context(), CLIENT_DEVICES_AUTH_SERVICE_NAME, null);
configurationTopics.lookup(CONFIGURATION_CONFIG_KEY, CERTIFICATE_AUTHORITY_TOPIC, CA_PRIVATE_KEY_URI)
.withValue(privateKeyUri.toString());
configurationTopics.lookup(CONFIGURATION_CONFIG_KEY, CERTIFICATE_AUTHORITY_TOPIC, CA_CERTIFICATE_URI)
.withValue(certificateUri.toString());
configurationTopics.lookup(CONFIGURATION_CONFIG_KEY, CERTIFICATE_AUTHORITY_TOPIC, CA_CERTIFICATE_CHAIN_URI)
.withValue(certificateChainUri.toString());

when(securityServiceMock.getKeyPair(privateKeyUri, certificateUri)).thenReturn(intermediateKeyPair);


// When
CDAConfiguration cdaConfiguration = CDAConfiguration.from(configurationTopics);
certificateManager.configureCustomCA(cdaConfiguration.getCertificateAuthorityConfiguration());

// Then
assertEquals(CertificateHelper.toPem(certificateStore.getCACertificate()),
CertificateHelper.toPem(intermediateCA));
assertEquals(CertificateHelper.toPem(certificateStore.getCaCertificateChain()),
CertificateHelper.toPem(certificateChain));
}

@Test
void Given_defaultCAConfiguration_THEN_returnsAutoGeneratedCA() throws Exception {
// Given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Arrays;
import java.util.Collections;

import static com.aws.greengrass.clientdevices.auth.configuration.CAConfiguration.CA_CERTIFICATE_CHAIN_URI;
import static com.aws.greengrass.clientdevices.auth.configuration.CAConfiguration.CA_CERTIFICATE_URI;
import static com.aws.greengrass.clientdevices.auth.configuration.CAConfiguration.CA_PRIVATE_KEY_URI;
import static com.aws.greengrass.clientdevices.auth.configuration.CAConfiguration.CA_TYPE_KEY;
Expand All @@ -46,6 +47,7 @@ void afterEach() throws IOException {
configurationTopics.getContext().close();
}


@Test
public void GIVEN_cdaDefaultConfiguration_WHEN_getCATypeList_THEN_returnsEmptyList() throws URISyntaxException {
CAConfiguration caConfiguration = CAConfiguration.from(configurationTopics);
Expand Down Expand Up @@ -94,7 +96,7 @@ public void GIVEN_hsmCdaConfiguration_WHEN_getCAKeyUri_THEN_returnsCACertUri() t
}

@Test
public void GIVEN_pathInsteadOfUri_WHEN_configurationLoaded_THEN_itRaisesAnException() throws URISyntaxException {
public void GIVEN_pathInsteadOfUri_WHEN_configurationLoaded_THEN_itRaisesAnException() {
configurationTopics.lookup(CERTIFICATE_AUTHORITY_TOPIC, CA_PRIVATE_KEY_URI)
.withValue("/root/tls/intermediate/certs/intermediate.cacert.pem");

Expand All @@ -121,7 +123,6 @@ public void GIVEN_oldCdaConfiguration_WHEN_reading_ca_type_THEN_returns_ca_type(
CAConfiguration caConfiguration = CAConfiguration.from(configurationTopics);
assertThat(caConfiguration.getCaType(), is(CertificateStore.CAType.ECDSA_P256));
assertThat(caConfiguration.getCaTypeList(), is(Arrays.asList("ECDSA_P256")));

}

@Test
Expand All @@ -134,6 +135,21 @@ public void GIVEN_oldCdaConfiguration_and_newCdaConfiguration_WHEN_reading_caTyp
CAConfiguration caConfiguration = CAConfiguration.from(configurationTopics);
assertThat(caConfiguration.getCaType(), is(CertificateStore.CAType.RSA_2048));
assertThat(caConfiguration.getCaTypeList(), is(Arrays.asList("RSA_2048")));
}

@Test
public void GIVEN_certificateChainConfiguration_WHEN_configuringCustomCA_THEN_certificateChainConfigApplied() throws
URISyntaxException {
CAConfiguration caConfiguration = CAConfiguration.from(configurationTopics);
configurationTopics.lookup(CERTIFICATE_AUTHORITY_TOPIC, CA_PRIVATE_KEY_URI)
.withValue("pkcs11:object=test;CustomerIntermediateCA;type=private");
configurationTopics.lookup(CERTIFICATE_AUTHORITY_TOPIC, CA_CERTIFICATE_URI)
.withValue("pkcs11:object=test;CustomerIntermediateCA;type=cert");
configurationTopics.lookup(CERTIFICATE_AUTHORITY_TOPIC, CA_CERTIFICATE_CHAIN_URI)
.withValue("file:///root/tls/intermediate/certs/certificateChain.pem");

caConfiguration = CAConfiguration.from(configurationTopics);
assertThat(caConfiguration.getCertificateChainUri().get(), is(new URI("file:///root/tls/intermediate/certs" +
"/certificateChain.pem")));
}
}

0 comments on commit 96065e6

Please sign in to comment.