Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ public static TlsKeyPair ofSelfSigned(String hostname) {

/**
* Generates a self-signed certificate for the local hostname.
*
* <p>Note that if the local hostname exceeds 64 characters, it is truncated to satisfy the
* RFC 5280 common name length limit.
*/
public static TlsKeyPair ofSelfSigned() {
return ofSelfSigned(SystemInfo.hostname());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableList;

/**
Expand All @@ -53,6 +56,10 @@
*/
public final class SelfSignedCertificate extends SignedCertificate {

private static final Logger logger = LoggerFactory.getLogger(SelfSignedCertificate.class);

private static final int MAX_COMMON_NAME_LENGTH = 64;

// Forked from:
// https://github.com/netty/netty/blob/11e6a77fba9ec7184a558d869373d0ce506d7236/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java
// https://github.com/netty/netty/blob/11e6a77fba9ec7184a558d869373d0ce506d7236/handler/src/main/java/io/netty/handler/ssl/util/BouncyCastleSelfSignedCertGenerator.java
Expand Down Expand Up @@ -235,7 +242,17 @@ public SelfSignedCertificate(String fqdn, Random random, int bits, Date notBefor
public SelfSignedCertificate(String fqdn, Random random, int bits, Date notBefore, Date notAfter,
String algorithm, Iterable<String> subjectAlternativeNames, boolean isCA)
throws CertificateException {
super(new CertificateParams(fqdn, random, bits, notBefore, notAfter, algorithm,
subjectAlternativeNames, isCA));
super(new CertificateParams(truncateToCommonNameLength(fqdn), random, bits, notBefore, notAfter,
algorithm, subjectAlternativeNames, isCA));
Comment on lines +245 to +246

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

root=core/src/main/java/com/linecorp/armeria/internal/common/util

fd -0 -t f -e java '^(CertificateParams|SignedCertificate)\.java$' "$root" |
  xargs -0 -r rg -n -C 8 \
    'params\.fqdn\(\)|ownerName\(\)|subjectAlternativeName|dNSName|new GeneralName'

Repository: line/armeria

Length of output: 25781


🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -0 -t f -e java '^(SelfSignedCertificate|SelfSignedCertificateTest)\.java$' \
  core/src/main/java/com/linecorp/armeria/internal/common/util/ \
  core/src/test/java/com/linecorp/armeria/internal/common/util/ |
  sort -z | xargs -0 -r wc -l

fd -0 -t f -e java '^(SelfSignedCertificate|SelfSignedCertificateTest)\.java$' \
  core/src/main/java/com/linecorp/armeria/internal/common/util/ \
  core/src/test/java/com/linecorp/armeria/internal/common/util/ |
  sort -z | xargs -0 -r sed -n '1,380p'

# Extract SelfSignedCertificate constructor context around the truncated fqdn line.
root=core/src/main/java/com/linecorp/armeria/internal/common/util
sed -n '220,260p' "$root/SelfSignedCertificate.java" | cat -n

# Programmatically inspect whether OwnerName is truncated on SelfSignedCertificate contruction.
# This uses the source text as data; it does not compile or execute repository code.
python3 - <<'PY'
from pathlib import Path
p = Path('core/src/main/java/com/linecorp/armeria/internal/common/util/SelfSignedCertificate.java')
src = p.read_text()
start = src.index('public SelfSignedCertificate(String fqdn')
end = src.index('}', start) + 2
body = src[start:end]
print("--- SelfSignedCertificate constructor body ---")
print(body)
print("passes truncated fqdn to super:", 'new CertificateParams(truncateToCommonNameLength(fqdn)' in body)
print("passes original fqdn to super:", 'new CertificateParams(fqdn' in body)
PY

Repository: line/armeria

Length of output: 18420


Preserve the original FQDN in the DNS SAN.

truncationToCommonNameLength(fqdn) is passed to CertificateParams, and SignedCertificate.generate() uses params.fqdn() as the automatic DNS SAN. A FQDN over 64 characters therefore produces a SAN that is also truncated, so hostname validation against the requested FQDN can fail. Store the original FQDN separately or pass it to SAN generation, and add a test that asserts the full FQDN in the SAN when the common name is truncated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/linecorp/armeria/internal/common/util/SelfSignedCertificate.java`
around lines 245 - 246, Update SelfSignedCertificate’s CertificateParams
construction and SignedCertificate.generate() flow so the original fqdn is
retained for automatic DNS SAN generation while only the common name is
truncated to its permitted length. Ensure the generated certificate contains the
full requested FQDN in its DNS SAN, and add coverage for an over-64-character
FQDN.

}

private static String truncateToCommonNameLength(String fqdn) {
if (fqdn.length() <= MAX_COMMON_NAME_LENGTH) {
Comment on lines +249 to +250

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate fqdn before measuring its length.

truncateToCommonNameLength(fqdn) calls fqdn.length() without validation. A null FQDN produces an NPE without a useful message. Validate fqdn with "fqdn" before this call.

As per path instructions, validation must use meaningful validation and exception messages.

Proposed fix
 private static String truncateToCommonNameLength(String fqdn) {
+    requireNonNull(fqdn, "fqdn");
     if (fqdn.length() <= MAX_COMMON_NAME_LENGTH) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static String truncateToCommonNameLength(String fqdn) {
if (fqdn.length() <= MAX_COMMON_NAME_LENGTH) {
private static String truncateToCommonNameLength(String fqdn) {
requireNonNull(fqdn, "fqdn");
if (fqdn.length() <= MAX_COMMON_NAME_LENGTH) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/linecorp/armeria/internal/common/util/SelfSignedCertificate.java`
around lines 249 - 250, Update truncateToCommonNameLength to validate fqdn with
the existing meaningful validation utility and the parameter name "fqdn" before
calling fqdn.length(), ensuring null input produces the standard validation
exception and message.

Source: Path instructions

return fqdn;
}
final String truncated = fqdn.substring(0, MAX_COMMON_NAME_LENGTH);
logger.debug("Truncating the fqdn '{}' to '{}' to satisfy " +
"the RFC 5280 common name length limit (64).", fqdn, truncated);
return truncated;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.junit.jupiter.api.Test;

import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.util.SystemInfo;
import com.linecorp.armeria.internal.common.util.SelfSignedCertificate;

class TlsKeyPairTest {
Expand All @@ -45,7 +46,14 @@ void selfSignedIsAValidPair() throws CertificateException {

@Test
void ofSelfSignedIsAValidPair() {
assertThat(TlsKeyPair.ofSelfSigned().privateKey()).isNotNull();
// Must not fail even on a machine whose hostname exceeds the 64-character common name limit
// of RFC 5280, such as a GitHub Actions macOS runner.
final TlsKeyPair keyPair = TlsKeyPair.ofSelfSigned();
assertThat(keyPair.privateKey()).isNotNull();
final String hostname = SystemInfo.hostname();
final String expectedCommonName = hostname.length() <= 64 ? hostname : hostname.substring(0, 64);
assertThat(keyPair.certificateChain().get(0).getSubjectX500Principal().getName())
.isEqualTo("CN=" + expectedCommonName);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import org.junit.jupiter.api.Test;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;

class SelfSignedCertificateTest {
Expand All @@ -42,6 +43,18 @@ void fqdnAsteriskFileNameTest() throws CertificateException {
assertThat(ssc.privateKey().getName()).doesNotContain("*");
}

@Test
void fqdnLongerThanCommonNameLimitTest() throws Exception {
// The hostname of some machines, such as a GitHub Actions macOS runner, exceeds the
// 64-character common name limit of RFC 5280.
final String fqdn = "very-long-hostname-" + Strings.repeat("a", 42) + ".local";
assertThat(fqdn).hasSize(67);

final SelfSignedCertificate ssc = new SelfSignedCertificate(fqdn);
assertThat(ssc.cert().getSubjectX500Principal().getName())
.isEqualTo("CN=" + fqdn.substring(0, 64));
}

@Test
void subjectAlternativeNamesWithUriTest() throws Exception {
final List<String> additionalSans = ImmutableList.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ void handshakeSuccess(SessionProtocol sessionProtocol, String tlsProtocol, Strin
final Counter counter =
meterRegistry.find("armeria.server.tls.handshakes")
.tag("cipher.suite", value -> value.startsWith("TLS_"))
.tag("hostname", server.server().defaultHostname())
.tag("hostname", certificateHostname())
.tag("protocol", expectedProtocol)
.tag("result", "success")
.tag("tls.protocol", tlsProtocol)
Expand Down Expand Up @@ -134,7 +134,7 @@ void handshakeFailure() {
final Counter counter =
meterRegistry.find("armeria.server.tls.handshakes")
.tag("cipher.suite", "")
.tag("hostname", server.server().defaultHostname())
.tag("hostname", certificateHostname())
.tag("protocol", "")
.tag("result", "failure")
.tag("tls.protocol", "")
Expand All @@ -145,4 +145,11 @@ void handshakeFailure() {
.isNotNull();
assertThat(counter.count()).isOne();
}

private static String certificateHostname() {
// The hostname tag is derived from the self-signed certificate, whose common name is
// truncated to the 64-character limit of RFC 5280.
final String hostname = server.server().defaultHostname();
return hostname.length() <= 64 ? hostname : hostname.substring(0, 64);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.junit.jupiter.params.provider.CsvSource;
import org.slf4j.LoggerFactory;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;

import com.linecorp.armeria.common.Flags;
Expand Down Expand Up @@ -89,6 +90,19 @@ void defaultVirtualHostSetDefaultHostname() {
assertThat(virtualHost.defaultHostname()).isEqualTo("foo");
}

@Test
void tlsSelfSignedWithLongDefaultHostname() {
// The hostname of some machines exceeds the 64-character common name limit of RFC 5280.
final String hostname = "very-long-hostname-" + Strings.repeat("a", 42) + ".local";
final Server server = Server.builder()
.defaultHostname(hostname)
.tlsSelfSigned()
.service("/test", (ctx, req) -> HttpResponse.of(OK))
.build();

assertThat(server.config().defaultVirtualHost().defaultHostname()).isEqualTo(hostname);
}

@Test
void defaultVirtualHostWithImplicitStyle() {
final ServerBuilder sb = Server.builder();
Expand Down
Loading