From 973a06353d3e5f07146114963e53bb8f141def04 Mon Sep 17 00:00:00 2001 From: Tigran Mkrtchyan Date: Wed, 31 Jan 2024 15:32:26 +0100 Subject: [PATCH] xroot: switch to xrootd4j-4.6.0 New major release with bug fixes and enhancements. Highlight: - reload TLS certificate Changelog for v4.5.0..v4.6.0 * [2861259b] [maven-release-plugin] prepare for next development iteration * [fd22f60c] xrootd4j: do not release buffer of unconsumed message in AuthenticationHandler * [7924276f] xrootd4j: correct regression in NoAuthenticationProvider * [f2090745] Motivation: * [7d03e489] xrootd4j: support kXR_prefname in kXR_locate request * [c76a766d] build(deps): bump netty-codec-haproxy from 4.1.5.Final to 4.1.86.Final * [6551be9a] xrootd4j: fix bug in goToTLS value * [1ac4ee31] build: update Copyright (C) 2011-2022 to Copyright (C) 2011-2023 * [88922f05] xrootd4j: fix source TLS check for TPC client * [e09debe9] xrootd4j: turn off TLS and ZTN for pre-5+ clients * [555a7500] xrootd4j: fix byte buffer leak in ZTNCredentialUtils * [7abf228d] xrootd4j: provide better info on SSLExceptions * [b62cb25d] core: don't manually update writeIndex in ReadResponse#writeTo * [75ae03b8] standalone: don't use ByteBuffer wrapper for read file chunk * [9f27e279] core: don't allocate AuthenticationRequest buffer from memory pool * [422d1a72] tls: update SSLHandlerFactory to use SSLContext supplier * [8af2965a] [maven-release-plugin] prepare branch 4.6 * [99c727fb] pom: fix license year * [e84edf92] [maven-release-plugin] prepare release v4.6.0 Ticket: #10562 Acked-by: Lea Morschel Target: master Require-book: no Require-notes: yes (cherry picked from commit 030d5ce33b072079782fd08a12bbb172761e81bc) Signed-off-by: Tigran Mkrtchyan --- .../plugins/tls/CDCCanlTLSHandlerFactory.java | 31 ++- .../tls/CDCCanlTLSHandlerFactoryTest.java | 183 ++++++++++++++++++ pom.xml | 2 +- 3 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 modules/dcache-xrootd/src/test/java/org/dcache/xrootd/plugins/tls/CDCCanlTLSHandlerFactoryTest.java diff --git a/modules/dcache-xrootd/src/main/java/org/dcache/xrootd/plugins/tls/CDCCanlTLSHandlerFactory.java b/modules/dcache-xrootd/src/main/java/org/dcache/xrootd/plugins/tls/CDCCanlTLSHandlerFactory.java index a038941bec6..6bc1361d1ab 100644 --- a/modules/dcache-xrootd/src/main/java/org/dcache/xrootd/plugins/tls/CDCCanlTLSHandlerFactory.java +++ b/modules/dcache-xrootd/src/main/java/org/dcache/xrootd/plugins/tls/CDCCanlTLSHandlerFactory.java @@ -17,12 +17,15 @@ */ package org.dcache.xrootd.plugins.tls; +import com.google.common.base.Throwables; import eu.emi.security.authn.x509.CrlCheckingMode; import eu.emi.security.authn.x509.NamespaceCheckingMode; import eu.emi.security.authn.x509.OCSPCheckingMode; import io.netty.handler.ssl.SslContext; import java.io.File; import java.util.Properties; +import java.util.function.Supplier; + import org.dcache.ssl.CanlContextFactory; /** @@ -34,15 +37,15 @@ */ public class CDCCanlTLSHandlerFactory extends SSLHandlerFactory { - private static final String SERVICE_KEY = "xrootd.security.tls.hostcert.key"; - private static final String SERVICE_CERT = "xrootd.security.tls.hostcert.cert"; - private static final String SERVICE_CACERTS = "xrootd.security.tls.ca.path"; - private static final String NAMESPACE_MODE = "xrootd.security.tls.ca.namespace-mode"; - private static final String CRL_MODE = "xrootd.security.tls.ca.crl-mode"; - private static final String OCSP_MODE = "xrootd.security.tls.ca.ocsp-mode"; + public static final String SERVICE_KEY = "xrootd.security.tls.hostcert.key"; + public static final String SERVICE_CERT = "xrootd.security.tls.hostcert.cert"; + public static final String SERVICE_CACERTS = "xrootd.security.tls.ca.path"; + public static final String NAMESPACE_MODE = "xrootd.security.tls.ca.namespace-mode"; + public static final String CRL_MODE = "xrootd.security.tls.ca.crl-mode"; + public static final String OCSP_MODE = "xrootd.security.tls.ca.ocsp-mode"; @Override - protected SslContext buildContext(Properties properties) throws Exception { + protected Supplier buildContextSupplier(Properties properties) throws Exception { File serviceKey = new File(properties.getProperty(SERVICE_KEY)); File serviceCert = new File(properties.getProperty(SERVICE_CERT)); File serviceCaCerts = new File(properties.getProperty(SERVICE_CACERTS)); @@ -53,7 +56,7 @@ protected SslContext buildContext(Properties properties) throws Exception { OCSPCheckingMode ocspMode = OCSPCheckingMode.valueOf(properties.getProperty(OCSP_MODE)); - return CanlContextFactory.custom() + var builder = CanlContextFactory.custom() .withCertificatePath(serviceCert.toPath()) .withKeyPath(serviceKey.toPath()) .withCertificateAuthorityPath(serviceCaCerts.toPath()) @@ -62,7 +65,15 @@ protected SslContext buildContext(Properties properties) throws Exception { .withNamespaceMode(namespaceMode) .withLazy(false) .startTls(startTls) - .buildWithCaching(SslContext.class) - .call(); + .buildWithCaching(SslContext.class); + + return () -> { + try { + return builder.call(); + } catch (Exception e) { + Throwables.throwIfUnchecked(e); + throw new RuntimeException(e); + } + }; } } diff --git a/modules/dcache-xrootd/src/test/java/org/dcache/xrootd/plugins/tls/CDCCanlTLSHandlerFactoryTest.java b/modules/dcache-xrootd/src/test/java/org/dcache/xrootd/plugins/tls/CDCCanlTLSHandlerFactoryTest.java new file mode 100644 index 00000000000..f56262e7e45 --- /dev/null +++ b/modules/dcache-xrootd/src/test/java/org/dcache/xrootd/plugins/tls/CDCCanlTLSHandlerFactoryTest.java @@ -0,0 +1,183 @@ +package org.dcache.xrootd.plugins.tls; + +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.nio.file.StandardOpenOption.WRITE; +import static org.dcache.xrootd.plugins.tls.CDCCanlTLSHandlerFactory.*; +import static org.dcache.xrootd.plugins.tls.CDCCanlTLSHandlerFactory.OCSP_MODE; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeTrue; +import eu.emi.security.authn.x509.impl.CertificateUtils; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.nio.file.Files; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.security.Security; +import java.util.Date; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +public class CDCCanlTLSHandlerFactoryTest { + + + // whatever slow test should run + private static final boolean RUN_SLOW_TESTS = System.getProperty("run.slow.tests") != null; + + @BeforeClass + public static void setupClass() { + Security.addProvider(new BouncyCastleProvider()); + } + private File keyFile; + private File certFile; + private File ca; + + private Properties configProperties; + + @Before + public void setUp() throws Exception { + keyFile = File.createTempFile("hostkey-", ".pem"); + certFile = File.createTempFile("hostcert-", ".pem"); + ca = Files.createTempDirectory("ca-").toFile(); + + configProperties = new Properties(); + configProperties.setProperty(SERVICE_KEY, keyFile.getAbsolutePath()); + configProperties.setProperty(SERVICE_CERT, certFile.getAbsolutePath()); + configProperties.setProperty(SERVICE_CACERTS, ca.getAbsolutePath()); + configProperties.setProperty(NAMESPACE_MODE, "IGNORE"); + configProperties.setProperty(OCSP_MODE, "IGNORE"); + configProperties.setProperty(CRL_MODE, "IGNORE"); + } + + @After + public void tearDown() throws Exception { + keyFile.delete(); + certFile.delete(); + ca.delete(); + } + + @Test + public void shouldReturnCachedContextIfFilesAreNotChanged() throws Exception { + + assumeTrue("Skip slow running test", RUN_SLOW_TESTS); + + generateSelfSignedCert(); + + var contextSupplierFactory = new CDCCanlTLSHandlerFactory(); + var contextSupplier = contextSupplierFactory.buildContextSupplier(configProperties); + + var c1 = contextSupplier.get(); + + TimeUnit.SECONDS.sleep(62); // 1 min is hard coded in org.dcache.ssl.CanlContextFactory.Builder.Builder + + var c2 = contextSupplier.get(); + assertSame(c1, c2); + } + + @Test + public void shouldReturnNewContextIfFilesChanged() throws Exception { + + assumeTrue("Skip slow running test", RUN_SLOW_TESTS); + + generateSelfSignedCert(); + var contextSupplierFactory = new CDCCanlTLSHandlerFactory(); + var contextSupplier = contextSupplierFactory.buildContextSupplier(configProperties); + + var c1 = contextSupplier.get(); + + TimeUnit.SECONDS.sleep(62); // 1 min is hard coded in org.dcache.ssl.CanlContextFactory.Builder.Builder + + generateSelfSignedCert(); + var c2 = contextSupplier.get(); + + assertNotSame(c1, c2); + } + + // ensue that files are re-read + @Test//(expected = FileNotFoundException.class) + public void shouldFailToCreateContextIfFilesRemoved() throws Exception { + + assumeTrue("Skip slow running test", RUN_SLOW_TESTS); + + generateSelfSignedCert(); + var contextSupplierFactory = new CDCCanlTLSHandlerFactory(); + var contextSupplier = contextSupplierFactory.buildContextSupplier(configProperties); + + contextSupplier.get(); + + TimeUnit.SECONDS.sleep(62); // 1 min is hard coded in org.dcache.ssl.CanlContextFactory.Builder.Builder + + keyFile.delete(); + certFile.delete(); + + var t = assertThrows(RuntimeException.class, contextSupplier::get); + assertThat(t.getCause(), new org.hamcrest.core.IsInstanceOf(FileNotFoundException.class)); + } + + private void generateSelfSignedCert() + throws GeneralSecurityException, OperatorCreationException, IOException { + + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(2048, new SecureRandom()); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + long notBefore = System.currentTimeMillis(); + long notAfter = notBefore + TimeUnit.DAYS.toMillis(1); + + X500Name subjectDN = new X500Name("CN=localhost, O=dCache.org"); + // explicit self-signed certificate + X500Name issuerDN = subjectDN; + + SubjectPublicKeyInfo subjectPublicKeyInfo = + SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); + + X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(issuerDN, + BigInteger.ONE, + new Date(notBefore), + new Date(notAfter), subjectDN, + subjectPublicKeyInfo); + + String signatureAlgorithm = "SHA256WithRSA"; + + // sign with own key + ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm) + .build(keyPair.getPrivate()); + + X509CertificateHolder certificateHolder = certificateBuilder.build(contentSigner); + var cert = new JcaX509CertificateConverter().getCertificate(certificateHolder); + + try (OutputStream certOut = Files.newOutputStream( + certFile.toPath(), CREATE, TRUNCATE_EXISTING, + WRITE); OutputStream keyOut = Files.newOutputStream(keyFile.toPath(), CREATE, + TRUNCATE_EXISTING, WRITE)) { + + CertificateUtils.saveCertificate(certOut, cert, CertificateUtils.Encoding.PEM); + CertificateUtils.savePrivateKey(keyOut, keyPair.getPrivate(), CertificateUtils.Encoding.PEM, null, null); + } + } + +} + diff --git a/pom.xml b/pom.xml index 5cfd8f5f759..f9cc21097aa 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,7 @@ 6.6.0 2.12.0 9.4.51.v20230217 - 4.5.8 + 4.6.0 2.28 2.1.0 4.1.94.Final