diff options
author | Bjørn Christian Seime <bjorncs@vespa.ai> | 2024-01-04 16:21:14 +0100 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@vespa.ai> | 2024-01-05 09:44:02 +0100 |
commit | 0b091e54c3e5ff1c9afa0cc7ba6e8a4c73783fd9 (patch) | |
tree | b3e7c9a2769015958478f68c4f77b99308681254 /config-model | |
parent | 690db20944acf2fa551c4b648959aae51496b40e (diff) |
Add validator detecting applications that will fail on BouncyCastle v1.77
Diffstat (limited to 'config-model')
5 files changed, 125 insertions, 0 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/CloudClientsValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/CloudClientsValidator.java new file mode 100644 index 00000000000..2882489ef0a --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/CloudClientsValidator.java @@ -0,0 +1,56 @@ +package com.yahoo.vespa.model.application.validation; + +import com.yahoo.config.model.deploy.DeployState; +import org.bouncycastle.asn1.x509.TBSCertificate; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.logging.Level; + +/** + * Validates that trusted data plane certificates are valid + * + * @author bjorncs + */ +public class CloudClientsValidator implements Validator { + + @Override + public void validate(Validation.Context ctx) { + if (!ctx.deployState().isHosted()) return; + ctx.model().getContainerClusters().forEach((clusterName, cluster) -> { + for (var client : cluster.getClients()) { + client.certificates().forEach(cert -> validateCertificate(clusterName, client.id(), cert, ctx.deployState())); + } + }); + } + + static void validateCertificate(String clusterName, String clientId, X509Certificate cert, DeployState state) { + try { + var extensions = TBSCertificate.getInstance(cert.getTBSCertificate()).getExtensions(); + if (extensions == null) return; // Certificate without any extensions is okay + if (extensions.getExtensionOIDs().length == 0) { + /* + BouncyCastle 1.77 no longer accepts certificates having an empty sequence of extensions. + Earlier releases violated the ASN.1 specification as the specification forbids empty extension sequence. + See https://github.com/bcgit/bc-java/issues/1479. + + Detect such certificates and issue a warning for now. + Validation will be implicitly enforced once we upgrade BouncyCastle past 1.76. + */ + var message = "The certificate's ASN.1 structure contains an empty sequence of extensions, " + + "which is a violation of the ASN.1 specification. " + + "Please update the application package with a new certificate, " + + "e.g by generating a new one using the Vespa CLI `$ vespa auth cert`. " + + "Such certificate will no longer be accepted in near future."; + state.getDeployLogger().log(Level.WARNING, errorMessage(clusterName, clientId, message)); + } + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException(errorMessage(clusterName, clientId, e.getMessage()), e); + } + } + + private static String errorMessage(String clusterName, String clientId, String message) { + return "Client **%s** defined for cluster **%s** contains an invalid certificate: %s" + .formatted(clientId, clusterName, message); + } +} diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java index edaaf7b206d..dc7a2651e1f 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java @@ -113,6 +113,7 @@ public class Validation { new JvmHeapSizeValidator().validate(execution); new InfrastructureDeploymentValidator().validate(execution); new EndpointCertificateSecretsValidator().validate(execution); + new CloudClientsValidator().validate(execution); } private static void validateFirstTimeDeployment(Execution execution) { diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/CloudClientsValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/CloudClientsValidatorTest.java new file mode 100644 index 00000000000..cc6cb4e7f88 --- /dev/null +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/CloudClientsValidatorTest.java @@ -0,0 +1,50 @@ +package com.yahoo.vespa.model.application.validation; + +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.vespa.model.test.utils.DeployLoggerStub; +import org.junit.jupiter.api.Test; + +import java.security.cert.X509Certificate; + +import static com.yahoo.yolean.Exceptions.uncheck; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author bjorncs + */ +class CloudClientsValidatorTest { + + @Test + void logs_deployment_warning_on_certificate_with_empty_sequence_of_extensions() { + // Test should fail on BouncyCastle 1.77 or later + + var logger = new DeployLoggerStub(); + var state = new DeployState.Builder().deployLogger(logger).build(); + var cert = readTestCertificate("cert-with-empty-sequence-of-extensions.pem"); + CloudClientsValidator.validateCertificate("default", "my-feed-client", cert, state); + var expected = "Client **my-feed-client** defined for cluster **default** contains an invalid certificate: " + + "The certificate's ASN.1 structure contains an empty sequence of extensions, " + + "which is a violation of the ASN.1 specification. " + + "Please update the application package with a new certificate, " + + "e.g by generating a new one using the Vespa CLI `$ vespa auth cert`. " + + "Such certificate will no longer be accepted in near future."; + assertEquals(expected, logger.getLast().message); + } + + @Test + void accepts_valid_certificate() { + var logger = new DeployLoggerStub(); + var state = new DeployState.Builder().deployLogger(logger).build(); + var cert = readTestCertificate("valid-cert.pem"); + assertDoesNotThrow(() -> CloudClientsValidator.validateCertificate("default", "my-feed-client", cert, state)); + assertEquals(0, logger.entries.size()); + } + + private static X509Certificate readTestCertificate(String filename) { + return X509CertificateUtils.fromPem(new String(uncheck( + () -> CloudClientsValidatorTest.class.getResourceAsStream( + "/cloud-clients-validator/%s".formatted(filename)).readAllBytes()))); + } +} diff --git a/config-model/src/test/resources/cloud-clients-validator/cert-with-empty-sequence-of-extensions.pem b/config-model/src/test/resources/cloud-clients-validator/cert-with-empty-sequence-of-extensions.pem new file mode 100644 index 00000000000..1942c12b28e --- /dev/null +++ b/config-model/src/test/resources/cloud-clients-validator/cert-with-empty-sequence-of-extensions.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBPDCB46ADAgECAhEAhdEB3eHnsQxTdcYcClVpkzAKBggqhkjOPQQDAjAeMRww +GgYDVQQDExNjbG91ZC52ZXNwYS5leGFtcGxlMB4XDTIyMTIyMTE4NTg0MloXDTMy +MTIxODE4NTg0MlowHjEcMBoGA1UEAxMTY2xvdWQudmVzcGEuZXhhbXBsZTBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABOPzpGlb+4HvgsRT5Ic6gmzYqAE2GQrgfi5z +txf8yzoi5YqgEG6utFhjleQ5bUusDhMtrfOJoBL5VZxrQccmwsCjAjAAMAoGCCqG +SM49BAMCA0gAMEUCIQCuNXMk5lsb9lF2IloYZB2wAHme/xAOyQ2arWzZf6BH2wIg +dEsmbGhel9MLlfPVQjeUwCJha/XD7xfWW6IaL+hI5TQ= +-----END CERTIFICATE----- diff --git a/config-model/src/test/resources/cloud-clients-validator/valid-cert.pem b/config-model/src/test/resources/cloud-clients-validator/valid-cert.pem new file mode 100644 index 00000000000..aebec508772 --- /dev/null +++ b/config-model/src/test/resources/cloud-clients-validator/valid-cert.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBNzCB3qADAgECAhB00FXOPRoixiJghCsT2FVOMAoGCCqGSM49BAMCMB4xHDAa +BgNVBAMTE2Nsb3VkLnZlc3BhLmV4YW1wbGUwHhcNMjQwMTA0MTM0MTMwWhcNMzQw +MTAxMTM0MTMwWjAeMRwwGgYDVQQDExNjbG91ZC52ZXNwYS5leGFtcGxlMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEts/fYf1H+aOW4xZHtcxX2YvMWojzU4HvHw1b +9Zc+7OoUcoqv9dTZMVaYj3J8Z3A73wNn5rhjPrI4sKtI5KN6sjAKBggqhkjOPQQD +AgNIADBFAiEAqgs4QouJOf6ny48o5c6EZSTB3+iNyZr+23JXKwnYuUkCIFRtE736 +BJ5KdCPpI4jS611HgeLLlJmgF2524Gz4EpjH +-----END CERTIFICATE----- |