From 6c9ddc6d0175eabcc5affdab0b5c19d8f34cbaca Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Wed, 7 Jan 2026 09:03:37 +0100 Subject: [PATCH] Fix y10k problem in certificate signing (#24604) * fix y10k problem in certificate signing * added changelog --- changelog/unreleased/pr-24604.toml | 5 +++ .../security/certutil/csr/CsrSigner.java | 16 +++++++-- .../CertificateRenewalPolicyResource.java | 14 ++++++-- .../security/certutil/csr/CsrSignerTest.java | 18 ++++++++++ .../CertificateRenewalPolicyResourceTest.java | 36 +++++++++++++++++++ 5 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 changelog/unreleased/pr-24604.toml create mode 100644 graylog2-server/src/test/java/org/graylog2/bootstrap/preflight/web/resources/CertificateRenewalPolicyResourceTest.java diff --git a/changelog/unreleased/pr-24604.toml b/changelog/unreleased/pr-24604.toml new file mode 100644 index 0000000000..d6a01f5849 --- /dev/null +++ b/changelog/unreleased/pr-24604.toml @@ -0,0 +1,5 @@ +type = "f" +message = "Fix y10k problem in certificates signing" + +pulls = ["24604"] +issues = [] diff --git a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrSigner.java b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrSigner.java index f28eaff899..83c972b48e 100644 --- a/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrSigner.java +++ b/graylog2-server/src/main/java/org/graylog/security/certutil/csr/CsrSigner.java @@ -40,6 +40,8 @@ import java.security.cert.X509Certificate; import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.Date; import java.util.Optional; @@ -58,6 +60,9 @@ public class CsrSigner { new GeneralName(iPAddress, "127.0.0.1"), new GeneralName(iPAddress, "0:0:0:0:0:0:0:1") ); + public static final Instant Y10K = LocalDate.of(9999, 1, 1) // Let's not try 31.12, who wants to deal with timezones and DST in year 9999?! + .atStartOfDay(ZoneOffset.UTC) + .toInstant(); private final Clock clock; @@ -83,7 +88,7 @@ public class CsrSigner { public X509Certificate sign(PrivateKey caPrivateKey, X509Certificate caCertificate, PKCS10CertificationRequest csr, @NotNull Duration certificateLifetime) throws Exception { final boolean keysMatching = KeystoreUtils.matchingKeys(caPrivateKey, caCertificate.getPublicKey()); - if(!keysMatching) { + if (!keysMatching) { throw new IllegalArgumentException("Provided CA private key doesn't correspond to provided CA certificate!"); } @@ -109,7 +114,7 @@ public class CsrSigner { var builder = new X509v3CertificateBuilder( issuerName, serialNumber, - Date.from(validFrom), Date.from(validUntil), + Date.from(validFrom), fixY10kProblem(validUntil), csr.getSubject(), csr.getSubjectPublicKeyInfo()); var altNames = Optional.ofNullable(csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest)) @@ -133,6 +138,13 @@ public class CsrSigner { return new JcaX509CertificateConverter().getCertificate(certHolder); } + private Date fixY10kProblem(Instant validUntil) { + if (validUntil.isAfter(Y10K)) { + return Date.from(Y10K); + } + return Date.from(validUntil); + } + private Stream resolveDNSName(GeneralName name) { final var hostname = name.getName().toString(); try { diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/CertificateRenewalPolicyResource.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/CertificateRenewalPolicyResource.java index 00f7b79ad3..aadb40939f 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/CertificateRenewalPolicyResource.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/resources/CertificateRenewalPolicyResource.java @@ -17,6 +17,7 @@ package org.graylog2.bootstrap.preflight.web.resources; import jakarta.inject.Inject; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -49,7 +50,16 @@ public class CertificateRenewalPolicyResource { @POST @RequiresPermissions(PreflightWebModule.PERMISSION_PREFLIGHT_ONLY) @NoAuditEvent("No Auditing during preflight") - public void set(@NotNull RenewalPolicy renewalPolicy) { - this.clusterConfigService.write(renewalPolicy); + public void set(@NotNull @Valid RenewalPolicy renewalPolicy) { + this.clusterConfigService.write(validatePolicy(renewalPolicy)); + } + + private RenewalPolicy validatePolicy(@NotNull @Valid RenewalPolicy renewalPolicy) { + try { + renewalPolicy.parsedCertificateLifetime(); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid certificate lifetime value: " + renewalPolicy.certificateLifetime(), e); + } + return renewalPolicy; } } diff --git a/graylog2-server/src/test/java/org/graylog/security/certutil/csr/CsrSignerTest.java b/graylog2-server/src/test/java/org/graylog/security/certutil/csr/CsrSignerTest.java index 06491a7d3f..dd40e7df05 100644 --- a/graylog2-server/src/test/java/org/graylog/security/certutil/csr/CsrSignerTest.java +++ b/graylog2-server/src/test/java/org/graylog/security/certutil/csr/CsrSignerTest.java @@ -44,6 +44,7 @@ import java.security.cert.X509Certificate; import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.Date; @@ -109,6 +110,23 @@ class CsrSignerTest { assertThat(result.getNotAfter()).isEqualTo(fixedInstant.plus(180, ChronoUnit.DAYS)); } + /** + * X509 certificates don't handle Y10K problem correctly, failing to parse + * any date after 9999-12-31. Let's make sure we limit cert validity in a way + * that prevents this. + */ + @Test + void testSigningCertY10k() throws Exception { + var result = sign("P9999999D"); + assertThat(result).isNotNull(); + + final Instant y10k = LocalDate.of(10000, 1, 1) + .atStartOfDay(UTC) + .toInstant(); + + assertThat(result.getNotAfter()).isBefore(y10k); + } + private PKCS10CertificationRequest createCSR(KeyPair keyPair) throws OperatorCreationException { var contentSigner = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); var pkcs10Builder = new PKCS10CertificationRequestBuilder(subjectName, SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())); diff --git a/graylog2-server/src/test/java/org/graylog2/bootstrap/preflight/web/resources/CertificateRenewalPolicyResourceTest.java b/graylog2-server/src/test/java/org/graylog2/bootstrap/preflight/web/resources/CertificateRenewalPolicyResourceTest.java new file mode 100644 index 0000000000..b3a5f001f7 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog2/bootstrap/preflight/web/resources/CertificateRenewalPolicyResourceTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.bootstrap.preflight.web.resources; + +import org.assertj.core.api.Assertions; +import org.graylog.security.certutil.InMemoryClusterConfigService; +import org.graylog2.plugin.certificates.RenewalPolicy; +import org.junit.jupiter.api.Test; + +class CertificateRenewalPolicyResourceTest { + + @Test + void testCertificateLifetimeValidation() { + final CertificateRenewalPolicyResource resource = new CertificateRenewalPolicyResource(new InMemoryClusterConfigService()); + Assertions.assertThatThrownBy(() -> resource.set(new RenewalPolicy(RenewalPolicy.Mode.AUTOMATIC, "10nonsense"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid certificate lifetime value: 10nonsense"); + + Assertions.assertThatNoException() + .isThrownBy(() -> resource.set(new RenewalPolicy(RenewalPolicy.Mode.AUTOMATIC, "P30D"))); + } +}