Skip to content

Log TLS signature algorithms - #13378

Open
bneradt wants to merge 1 commit into
apache:masterfrom
bneradt:tls-signature-algorithm-logging
Open

bneradt wants to merge 1 commit into
apache:masterfrom
bneradt:tls-signature-algorithm-logging

Conversation

@bneradt

@bneradt bneradt commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Certificate selection decisions require visibility into the signature
algorithms clients offer, but access logs currently expose only TLS
cipher and group details. This makes it difficult to identify clients
that depend on a particular certificate signature type.

This patch adds offered and negotiated signature-algorithm log fields
with precise GREASE filtering and a defensive guard for resumed
handshakes. It handles OpenSSL fallback ambiguity, documents derivation
semantics, and covers capture precedence, certificate disambiguation,
and TLS 1.2 and TLS 1.3 full and resumed handshakes.

Co-authored-by: GPT-6 Astra Medium

Copilot AI lite review requested due to automatic review settings July 13, 2026 21:36
@bneradt bneradt added this to the 11.0.0 milestone Jul 13, 2026
@bneradt bneradt self-assigned this Jul 13, 2026

Copilot AI left a comment

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.

Pull request overview

Adds new access-log fields to expose TLS signature-scheme information (client-offered list and ATS-selected scheme), improving visibility for certificate selection / client-compatibility analysis across TLS 1.2/1.3 and resumed sessions.

Changes:

  • Introduces two new logging fields (cqssig, cqssin) wired through TransactionLogData/LogAccess/Log and documented in the admin guide.
  • Captures and formats signature-scheme data (GREASE-filtered, dash-separated decimal code points) for both TCP-TLS and QUIC paths.
  • Adds a unit test for GREASE filtering/formatting and a new gold test validating the access-log output for full vs resumed TLS 1.2/1.3 handshakes.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/gold_tests/tls/tls_signature_algorithms.test.py New gold test driving OpenSSL handshakes and validating new log fields.
tests/gold_tests/tls/gold/tls_signature_algorithms.gold Expected access-log output for the new fields across handshake modes/versions.
src/proxy/logging/TransactionLogData.cc Exposes new TLS signature data from HttpUserAgent to logging.
src/proxy/logging/LogAccess.cc Adds marshalling for the new signature-algorithm log fields.
src/proxy/logging/Log.cc Registers cqssig / cqssin logging field symbols.
src/iocore/net/unit_tests/test_SSLUtils.cc Unit tests for GREASE omission and wire-order formatting.
src/iocore/net/TLSBasicSupport.cc Adds capture/cache plumbing for offered + negotiated signature scheme values.
src/iocore/net/SSLUtils.cc Implements extraction/formatting and hooks capture into TLS callbacks.
src/iocore/net/SSLNetVConnection.cc Provides SSLNetVConnection implementations for the new TLSBasicSupport virtuals.
src/iocore/net/QUICNetVConnection.cc Provides QUICNetVConnection implementations for the new TLSBasicSupport virtuals.
src/iocore/net/QUICMultiCertConfigLoader.cc Enables handshake info callback for QUIC to capture negotiated scheme.
src/iocore/net/P_SSLUtils.h Declares new signature-algorithm helper APIs.
src/iocore/net/P_SSLNetVConnection.h Declares new TLSBasicSupport virtual overrides for SSLNetVConnection.
src/iocore/net/P_QUICNetVConnection.h Declares new TLSBasicSupport virtual overrides for QUICNetVConnection.
src/iocore/net/CMakeLists.txt Adds the new SSLUtils unit test to the build.
include/tscore/ink_config.h.cmake.in Adds feature-detection macros for TLS signature API availability.
include/proxy/logging/TransactionLogData.h Declares new TransactionLogData accessors for signature fields.
include/proxy/logging/LogAccess.h Declares new marshal functions for signature fields.
include/proxy/http/HttpUserAgent.h Stores signature-scheme strings in ClientConnectionInfo and exposes getters.
include/iocore/net/TLSBasicSupport.h Adds APIs/virtuals and optional caches for signature scheme capture.
doc/admin-guide/logging/formatting.en.rst Documents cqssig/cqssin.
CMakeLists.txt Adds configure-time checks for OpenSSL/BoringSSL signature API symbols.

Comment thread src/iocore/net/TLSBasicSupport.cc
Copilot AI review requested due to automatic review settings July 13, 2026 21:52
@bneradt
bneradt force-pushed the tls-signature-algorithm-logging branch from f685eb6 to ee1564d Compare July 13, 2026 21:52

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.

@bryancall
bryancall requested a review from cmcfarlen July 13, 2026 22:07
Copilot AI review requested due to automatic review settings July 13, 2026 23:22
@bneradt
bneradt force-pushed the tls-signature-algorithm-logging branch from ee1564d to 8cfd649 Compare July 13, 2026 23:22

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.

@bneradt bneradt removed this from ATS v10.2.x Jul 30, 2026
Copilot AI review requested due to automatic review settings August 17, 2026 14:16

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@bneradt
bneradt force-pushed the tls-signature-algorithm-logging branch from b104445 to 52ea149 Compare August 18, 2026 22:10
Copilot AI review requested due to automatic review settings August 18, 2026 22:10

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maskit
maskit requested a lite review from Copilot August 19, 2026 17:29

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

src/iocore/net/SSLUtils.cc:231

  • ssl_client_offered_signature_algorithms() runs on every TLS handshake (ClientHello) but currently allocates and populates a std::vector<uint16_t> just to immediately re-walk it for formatting. Consider formatting directly while iterating the extension bytes (including GREASE filtering) to avoid the extra allocation and pass over the data. This keeps per-handshake overhead lower on busy TLS termination nodes.
static std::string
ssl_client_offered_signature_algorithms(TLSSNISupport::ClientHello &client_hello)
{
  uint8_t const *extension = nullptr;
  size_t         length    = 0;

  if (client_hello.getExtension(TLSEXT_TYPE_signature_algorithms, &extension, &length) != 1 || length < 2) {
    return {};
  }

  size_t const algorithms_length = static_cast<size_t>(extension[0]) << 8 | extension[1];
  if (algorithms_length != length - 2 || algorithms_length % 2 != 0) {
    return {};
  }

  std::vector<uint16_t> algorithms;
  algorithms.reserve(algorithms_length / 2);
  for (size_t offset = 2; offset < length; offset += 2) {
    algorithms.push_back(static_cast<uint16_t>(extension[offset]) << 8 | extension[offset + 1]);
  }

  return SSLFormatSignatureAlgorithms(algorithms);
}

tests/gold_tests/tls/tls_signature_algorithms.test.py:118

  • Using the HTTP request string as the printf format (printf \"{request}\") can be fragile: any % characters in the request (now or in future edits) would be treated as format directives, and escape handling for \\r/\\n can vary across shells. Prefer passing the request as an argument to printf (so it’s not a format string) and using an explicit format (e.g., one that interprets backslash escapes) to make the command more robust/portable.
    def _s_client_command(self, path: str, tls_option: str, sigalgs: str, session_option: str) -> str:

tests/gold_tests/tls/tls_signature_algorithms.test.py:129

  • Using the HTTP request string as the printf format (printf \"{request}\") can be fragile: any % characters in the request (now or in future edits) would be treated as format directives, and escape handling for \\r/\\n can vary across shells. Prefer passing the request as an argument to printf (so it’s not a format string) and using an explicit format (e.g., one that interprets backslash escapes) to make the command more robust/portable.
        request = f'GET {path} HTTP/1.1\\r\\nHost: example.com\\r\\nConnection: close\\r\\n\\r\\n'
        return (
            f'printf "{request}" | openssl s_client -quiet -connect 127.0.0.1:{self._ts.Variables.ssl_port} '
            f'-servername example.com {tls_option} -sigalgs {sigalgs} {session_option}')

Comment thread src/iocore/net/SSLUtils.cc

@cmcfarlen cmcfarlen left a comment

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.

Review

Two new log fields (cqssig offered list, cqssin used algorithm) captured from the ClientHello, the cert callback, and a handshake-done info callback.

What's right

  • The GREASE filter is correct, which is the easiest thing here to get wrong. (a & 0x0f0f) == 0x0a0a && (a >> 12) == ((a >> 4) & 0x0f) matches exactly RFC 8701's 0xNANA family — I checked 0x0a0a/0x1a1a/0xfafa (filtered) against 0x1a2a and 0xffff (retained), and the unit tests pin all of those, including the non-obvious reserved-but-not-GREASE case.
  • Wire order is preserved, and the TLS 1.2 code-point reconstruction hash << 8 | signature matches the SignatureAndHashAlgorithm encoding, so SSL_get_sigalgs's raw out-params compose correctly.
  • Resumption semantics are coherent: no CertificateVerify → empty → -, documented, and the gold file exercises full and resumed at both 1.2 and 1.3.

Main finding: on OpenSSL, cqssin is a heuristic, and that's the path nearly everyone runs

SSL_get_signature_algorithm_used and SSL_get0_peer_verify_algorithms are BoringSSL APIs. I checked the OpenSSL 3.6 headers — both are absent; only SSL_get_sigalgs, SSL_get_shared_sigalgs, SSL_get_signature_nid and SSL_get_signature_type_nid exist. So every OpenSSL build takes the #elif branch, which reconstructs the code point by scanning shared sigalgs for a (signature-type NID, hash NID) match and disambiguating via signature_algorithm_matches_private_key().

The logic is coherent — the RSA vs RSA-PSS ambiguity is genuinely why the key-type check has to exist — but it is inference rather than the value the library actually used, and it returns the first match. Two consequences I'd like addressed:

  • The docs promise "the TLS signature scheme used by |TS|" with no hint that on the common build it is derived. An operator comparing an OpenSSL box against a BoringSSL box could see different values for identical handshakes. A sentence in formatting.en.rst and a comment above the #elif would cover it.
  • The exact path is the one CI's OpenSSL jobs never execute, so the primary implementation is only covered wherever BoringSSL/quiche builds run.

Introduces the tree's only deprecated EC_KEY usage

signature_algorithm_matches_private_key() uses EVP_PKEY_get0_EC_KEY, EC_KEY_get0_group and EC_GROUP_get_curve_name, and that block compiles precisely on the OpenSSL path. Grepping src/, include/ and plugins/, these appear nowhere else in the tree. It builds today only because of OPENSSL_API_COMPAT=10002, and it lands right after #13476 ("Resolve OpenSSL 4.0 build issues"). EVP_PKEY_get_utf8_string_param(pkey, OSSL_PKEY_PARAM_GROUP_NAME, ...) gets the same answer without the deprecated handle.

Test coverage gaps

  • tests/gold_tests/tls/ssl/server.pem is rsaEncryption, so the entire EC branch — including the six-entry ECDSA/brainpool table — is never executed by any test, unit or auto. That branch is where the reconstruction is most likely to be wrong, so an ECDSA cert case, or a unit test over signature_algorithm_matches_private_key, would be worth more than the four existing gold lines.
  • Log-flush flake risk: the gold compares a four-line log with no max_secs_per_buffer/flush record and no wait run, relying on the shutdown flush and on the four entries landing in request order. There's a run of recent commits doing nothing but fixing that exact class of flake (stale_response, jax_fingerprint, slice_prefetch, pqsi-pqsp log order, plus "Harden timing-sensitive AuTests"). Cheap to harden now.

Undocumented behavior change: QUIC info callback

QUICMultiCertConfigLoader::_set_info_callback went from an explicit no-op (// Disabled for now / TODO Check if we need this for QUIC) to installing SSLHandshakeInfoCallback. That's a reasonable prerequisite, but it enables an info callback on QUIC contexts where there was none, isn't mentioned in the description, and no test covers QUIC for these fields. Worth a line in the description at minimum.

Smaller items

  • The field symbols break the established shape. The existing TLS fields are five characters — cqssl, cqssv, cqssc, cqssu, cqssg, cqssa — and these add six-character cqssig/cqssin, where cqssin reads like "SSL in" rather than "signature negotiated". Log symbols are permanent once shipped, so one round of bikeshedding now seems worth it.
  • Two more std::strings per transaction in ClientConnectionInfo. A realistic offered list (8 schemes, roughly 40 characters) exceeds SSO, so that's an extra heap allocation per TLS transaction. Consistent with how security_group already works, so not a blocker — just noting this list is longer than its neighbors.
  • The two capture_tls_offered_signature_algorithms overloads differ subtly: the argument-taking one overwrites, the no-arg one preserves a non-empty earlier value. Tested, but distinct names would document themselves.
  • m_conn_info.offered_signature_algorithms = '-'; assigns a char. It matches the adjacent security_group line so it's fine, though "-" reads better.

Requested changes

Requesting changes on three items: the doc caveat plus code comment for the derived cqssin, replacing the deprecated EC_KEY calls, and either an ECDSA test case or a unit test over the reconstruction. The rest is discretionary.

Copilot AI review requested due to automatic review settings August 25, 2026 22:03
@bneradt
bneradt force-pushed the tls-signature-algorithm-logging branch from 52ea149 to c39fc35 Compare August 25, 2026 22:03

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@bneradt
bneradt force-pushed the tls-signature-algorithm-logging branch from c39fc35 to 7397e16 Compare August 25, 2026 22:05
@bneradt

bneradt commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Updated cqssin documentation to explain its derived behavior. OpenSSL 3 now uses provider-based curve lookup, the fallback requires a unique compatible scheme, and the AuTest includes a P-256 ECDSA TLS 1.3 handshake.

Copilot AI review requested due to automatic review settings September 17, 2026 17:40
@bneradt
bneradt force-pushed the tls-signature-algorithm-logging branch from 7397e16 to 76ae3f1 Compare September 17, 2026 17:40

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Certificate selection decisions require visibility into the signature
algorithms clients offer, but access logs currently expose only TLS
cipher and group details. This makes it difficult to identify clients
that depend on a particular certificate signature type.

This patch adds offered and negotiated signature-algorithm log fields
with precise GREASE filtering and a defensive guard for resumed
handshakes. It handles OpenSSL fallback ambiguity, documents derivation
semantics, and covers capture precedence, certificate disambiguation,
and TLS 1.2 and TLS 1.3 full and resumed handshakes.

Co-authored-by: GPT-6 Astra Medium
Copilot AI review requested due to automatic review settings September 17, 2026 21:17
@bneradt
bneradt force-pushed the tls-signature-algorithm-logging branch from 76ae3f1 to ca54c63 Compare September 17, 2026 21:17

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants