Fix y10k problem in certificate signing (#24604)

* fix y10k problem in certificate signing

* added changelog
This commit is contained in:
Tomas Dvorak
2026-01-07 09:03:37 +01:00
committed by GitHub
parent 97e046041b
commit 6c9ddc6d01
5 changed files with 85 additions and 4 deletions

View File

@@ -0,0 +1,5 @@
type = "f"
message = "Fix y10k problem in certificates signing"
pulls = ["24604"]
issues = []

View File

@@ -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<? extends GeneralName> resolveDNSName(GeneralName name) {
final var hostname = name.getName().toString();
try {

View File

@@ -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;
}
}

View File

@@ -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()));

View File

@@ -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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
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")));
}
}