diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a0733a4b0..162da2f2259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Fix #5584: Fix CRD generation when EnumMap is used * Fix #5626: Prevent memory accumulation from informer usage * Fix #5527: Unable to transfer file to pod if `/tmp` is read-only +* Fix #5656: Enable EC private key usage for mTLS auth #### Improvements * Fix #5429: moved crd generator annotations to generator-annotations instead of crd-generator-api. Using generator-annotations introduces no transitive dependencies. diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/CertUtils.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/CertUtils.java index 25d23196206..d6fb0a52690 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/CertUtils.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/CertUtils.java @@ -17,6 +17,8 @@ import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.utils.Utils; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; @@ -180,22 +182,27 @@ private static PrivateKey handleECKey(InputStream keyInputStream) { try { return new Callable() { @Override - public PrivateKey call() { - try { - if (Security.getProvider("BC") == null && Security.getProvider("BCFIPS") == null) { - Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); - } - PEMKeyPair keys = (PEMKeyPair) new PEMParser(new InputStreamReader(keyInputStream)).readObject(); - return new JcaPEMKeyConverter().getKeyPair(keys).getPrivate(); - } catch (IOException exception) { - exception.printStackTrace(); + public PrivateKey call() throws IOException { + if (Security.getProvider("BC") == null && Security.getProvider("BCFIPS") == null) { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + Object pemObject = new PEMParser(new InputStreamReader(keyInputStream)).readObject(); + if (pemObject == null) { + throw new KubernetesClientException("Got null PEM object from EC key's input stream."); + } else if (pemObject instanceof PEMKeyPair) { + return new JcaPEMKeyConverter().getKeyPair((PEMKeyPair) pemObject).getPrivate(); + } else if (pemObject instanceof PrivateKeyInfo) { + return BouncyCastleProvider.getPrivateKey((PrivateKeyInfo) pemObject); + } else { + throw new KubernetesClientException("Don't know what to do with a " + pemObject.getClass().getName()); } - return null; } }.call(); } catch (NoClassDefFoundError e) { throw new KubernetesClientException( "JcaPEMKeyConverter is provided by BouncyCastle, an optional dependency. To use support for EC Keys you must explicitly add this dependency to classpath."); + } catch (IOException e) { + throw new KubernetesClientException(e.getMessage()); } } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/CertUtilsTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/CertUtilsTest.java index 4f42b9632be..efe680301d4 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/CertUtilsTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/CertUtilsTest.java @@ -15,6 +15,7 @@ */ package io.fabric8.kubernetes.client.internal; +import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.utils.IOHelpers; import io.fabric8.kubernetes.client.utils.Utils; import org.junit.jupiter.api.AfterEach; @@ -31,12 +32,14 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; import java.util.Base64; import java.util.Collections; import java.util.Objects; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.Assert.assertNotSame; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -184,6 +187,48 @@ void getInputStreamFromDataOrFileShouldDecodeBase64EncodedString() throws IOExce assertEquals(inputStr, certDataReadFromInputStream); } + @Test + void loadECkeys() + throws InvalidKeySpecException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + String privateKeyPath = Utils.filePath(getClass().getResource("/ssl-test/fabric8-ec.paired.key")); + String certPath = Utils.filePath(getClass().getResource("/ssl-test/fabric8-ec.cert")); + + KeyStore trustStore = CertUtils.createKeyStore(null, certPath, null, privateKeyPath, "EC", "foo", null, null); + + assertEquals(1, trustStore.size()); + } + + @Test + void loadECPrivateOnlyKey() + throws InvalidKeySpecException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + String privateKeyPath = Utils.filePath(getClass().getResource("/ssl-test/fabric8-ec.private-only.key")); + String certPath = Utils.filePath(getClass().getResource("/ssl-test/fabric8-ec.cert")); + + KeyStore trustStore = CertUtils.createKeyStore(null, certPath, null, privateKeyPath, "EC", "foo", null, null); + + assertEquals(1, trustStore.size()); + } + + @Test + void loadNothingError() { + String privateKeyPath = Utils.filePath(getClass().getResource("/ssl-test/empty")); + String certPath = Utils.filePath(getClass().getResource("/ssl-test/empty")); + + assertThatExceptionOfType(KubernetesClientException.class) + .isThrownBy(() -> CertUtils.createKeyStore(null, certPath, null, privateKeyPath, "EC", "foo", null, null)) + .withMessage("Got null PEM object from EC key's input stream."); + } + + @Test + void loadUnknownError() { + String privateKeyPath = Utils.filePath(getClass().getResource("/ssl-test/multiple-certs.p7b")); + String certPath = Utils.filePath(getClass().getResource("/ssl-test/multiple-certs.p7b")); + + assertThatExceptionOfType(KubernetesClientException.class) + .isThrownBy(() -> CertUtils.createKeyStore(null, certPath, null, privateKeyPath, "EC", "foo", null, null)) + .withMessageContaining("Don't know what to do with a"); + } + @Test void storeKeyFallbacksToDefault() throws Exception { // When diff --git a/kubernetes-client-api/src/test/resources/ssl-test/empty b/kubernetes-client-api/src/test/resources/ssl-test/empty new file mode 100644 index 00000000000..e69de29bb2d diff --git a/kubernetes-client-api/src/test/resources/ssl-test/fabric8-ec.cert b/kubernetes-client-api/src/test/resources/ssl-test/fabric8-ec.cert new file mode 100644 index 00000000000..43c17dfea0a --- /dev/null +++ b/kubernetes-client-api/src/test/resources/ssl-test/fabric8-ec.cert @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC7TCCAdWgAwIBAgIUD0Q+lZUpqmvTUxmCbhsPx93hYI4wDQYJKoZIhvcNAQEL +BQAwczELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDVNhbiBGcmFuY2lzY28xCzAJBgNV +BAcTAkNBMRgwFgYDVQQKEw9NeSBDb21wYW55IE5hbWUxEzARBgNVBAsTCk9yZyBV +bml0IDIxEDAOBgNVBAMTB0t1YmUgQ0EwHhcNMTgwMjEzMDc1MzAwWhcNMjMwMjEy +MDc1MzAwWjBCMQswCQYDVQQGEwJVUzEWMBQGA1UECBMNU2FuIEZyYW5jaXNjbzEL +MAkGA1UEBxMCQ0ExDjAMBgNVBAMTBWFkbWluMFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEUHBg7OvKkSprAljQcCcpXFns/pMNDkQJZuooj97A0063ipBrZzbdxTcu +VcBjFNJC/Tn2keNSQP+m9QbQmQfmM6N1MHMwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud +JQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFMBq0KJaRKoC ++a8R5la/B8tJahD3MB8GA1UdIwQYMBaAFOuZHF+ex0WLX3UX9LJoFHbI9cPNMA0G +CSqGSIb3DQEBCwUAA4IBAQC47D3zYMTg/C9gOYu20xEmw6eTEhJ1wWG9jSdHM9G8 +0F3mD4+bG3skx5kaCgtJ3KqbMSGPJ234Ju9VHCNiyiasZS41a8wuagJ6v5pTItLL +BmQ3OhT2HJZz+lDbsb3jLDzesQ5UCct08/e8ST4hnZUcSrz2geD1hSYQEhadsI87 +A+wVAvGfhCyiDUiClA2vwosWfomshkWphRzy+s5zFLuxlBAxj8g2oAbCJfhfJS5A +O8W5Nu+ddO5+7PB+y28Bue1GWABIjytmyLdvic0vkKzZkSzPOwqCT1ofZ9HU56h7 +jKOjtBacCcl/7pLgvaA8Ng1qfjQTiveDVfI8rNWiEhRm +-----END CERTIFICATE----- diff --git a/kubernetes-client-api/src/test/resources/ssl-test/fabric8-ec.paired.key b/kubernetes-client-api/src/test/resources/ssl-test/fabric8-ec.paired.key new file mode 100644 index 00000000000..f375231bb44 --- /dev/null +++ b/kubernetes-client-api/src/test/resources/ssl-test/fabric8-ec.paired.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAPhomYs9rdnNgEtr2FIB1rBDYnuKqV4QVAYBX4yRqAEoAoGCCqGSM49 +AwEHoUQDQgAEUHBg7OvKkSprAljQcCcpXFns/pMNDkQJZuooj97A0063ipBrZzbd +xTcuVcBjFNJC/Tn2keNSQP+m9QbQmQfmMw== +-----END EC PRIVATE KEY----- diff --git a/kubernetes-client-api/src/test/resources/ssl-test/fabric8-ec.private-only.key b/kubernetes-client-api/src/test/resources/ssl-test/fabric8-ec.private-only.key new file mode 100644 index 00000000000..14a8b57f96e --- /dev/null +++ b/kubernetes-client-api/src/test/resources/ssl-test/fabric8-ec.private-only.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQglAqmnWCvaPD+4fCx +rIAuhPoZcToa25FT3pKinym+9w6hRANCAAQr1Kmy52b6tabokBe3M8gO8Vi9N+Bc +BkTWaRt9NijEmB9uJNsJ835nVmvQEdjffk5nErGMcp0ytPJak27040+K +-----END PRIVATE KEY----- diff --git a/kubernetes-client-api/src/test/resources/ssl-test/multiple-certs.p7b b/kubernetes-client-api/src/test/resources/ssl-test/multiple-certs.p7b new file mode 100644 index 00000000000..d5c5fab3321 --- /dev/null +++ b/kubernetes-client-api/src/test/resources/ssl-test/multiple-certs.p7b @@ -0,0 +1,35 @@ +-----BEGIN PKCS7----- +MIIGKQYJKoZIhvcNAQcCoIIGGjCCBhYCAQExADALBgkqhkiG9w0BBwGgggX8MIIC +6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVuc2hp +ZnQtc2lnbmVyQDE0ODkwNTI0ODEwHhcNMTcwMzA5MDk0MTIxWhcNMjIwMzA4MDk0 +MTIyWjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE0ODkwNTI0ODEwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHvEtorBUHnUErPhKKbRrpEd/3 +Kl1UeMMowkHNxq9FMHcm6Mczuu7I0O+qtKbuW7arAZyaoSLO2ezW1lLRuGsuMSJ7 +qUqiRus+RUCQWDxLeZhu4OXvcebR3T+drzd35lOwkZBqoNysmT6Uqs7RVZ05+jKK +VJpKDya99Zbr/9zTsYH+gzSAg4LOyMs9fd2tZ75pMHVS0MWMAhXLAPtP/JDMBXOx +iBoFMymsnKeHKThztjoA7dzS1XQTq4cGcCJqyj3wdvey76HBaLzMfu9ZaoA10J20 +eRnk7K4oQEK9jE1rBof4huFa5vvBumSv8Ds/yMzpOA0qOjXPcmJzx0xwQQunAgMB +AAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQA3hfVp3yf8k0dPr6p/H/xgDKxedWOUTbdfl3v+AWGgSUvDT30H +ruYqaC34FXCvoUIia4kxzZlDBDjMg9/9zOOCQdwQimgQM2tOujNUFR3HBEfgYLxt +Y106hxzJpLCO+HsYPX9rFOTAsvt2og/jflP10EHAodj40FSymL6HIV671kzbykoq +fM3Hp+fPfUmvldxZSkzCGJAlDylE8pF4NOMd7B0LKPJuQWM2wgFH1uWQqqq0gu6o +q6GoX+39CSlx5bfydd2D660+Xt0G4k2u6z4SFxqb1hKTpi6cGtB8TERa6sxbl/Kl +cfE/R05F+L34hITDjoKYSZUWe5r4weAdMtwpMIIDCjCCAfKgAwIBAgIBATANBgkq +hkiG9w0BAQsFADA2MTQwMgYDVQQDDCtvcGVuc2hpZnQtc2VydmljZS1zZXJ2aW5n +LXNpZ25lckAxNDg5MDUyNDgyMB4XDTE3MDMwOTA5NDEyMVoXDTIyMDMwODA5NDEy +MlowNjE0MDIGA1UEAwwrb3BlbnNoaWZ0LXNlcnZpY2Utc2VydmluZy1zaWduZXJA +MTQ4OTA1MjQ4MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKvcK+2 +kKix24UR5U9en6IJ5yTYBS1ozLCTsKAKWrD4Ttpec3ON/SycoEfUXY6ao+pN2Q0Q +bH0/hi1+fFQgdHoQxo1yG4/SAwEu+n4uUse5ApCtNWLAv8jO+Y1o33CSIIM4Sm3P +uw7w/qzHAGmad6Nvg5v+ZOxdGNm7UU399zWzY6+igFo/rg8oacZVerchrnLE2cev +pjafguqy6kiuKFbfdIcYGfdmmypyC/SbpwgRABPWLBqk/AUfkNClKVi91Ysbaj5t +lFSXEu2i2z6aAhJCHddjGeq17cQybiYVLhqXPYXtdXH+khYfXY9cupqVftrXgV6c +/xWRzmRxDQyTqdMCAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBADA5X9iNqR8JmXbHCFMovs5PIOExK6uR +1xDBMmD6op0giDXB4ljnIa4R7euo1BCIOjC9uX6YT0OEXl31IXPAzapZNHzp2sW9 +8pyxDXBd78KD/WEeWfeO4rwL4CMACgqUT9LwNCdL01s74NEruNzWIfx74Az1WtlU +8YqE7RroCpcBaqZgLJih9jcBdOVuZN5gK/xJm2P1QminD5Z+YzU2yeCFOnRzueA9 +tFveiPVRk454bflfsW8dfixTNeU9MuG3PbtZ9nqR0hBKWtPst1tJwLTOdNcAijt1 +T4r6qoZAmGJcEp/EQ5b0beARM1STqNeO3GPOOg1Ec33+jUHYySkQ3JGhADEA +-----END PKCS7-----