Summary

  • In TLS 1.3, CertificateVerify signs the transcript with the certificate private key; Finished separately authenticates the transcript with a key derived from the sender's handshake traffic secret.
  • Completion is directional and bounded. A valid server Finished does not prove receipt of the client's, acceptance of early data, an upstream proxy handshake, application authorization or durable business outcome.

The alert arrived after the success metric

The incident began with two correct observations. The certificate chain matched the service policy, and the server's CertificateVerify signature validated. The monitoring adapter therefore emitted handshake_authenticated=true.

The next encrypted handshake message was Finished. Its verify_data did not match the client's transcript and derived key. RFC 9846 requires the recipient to terminate with decrypt_error. The client obeyed. No ordinary protected conversation began.

The metric had not detected a weak signature or a bad certificate. It had ended the authentication block one message too early. A proof of certificate-key possession had been treated as proof that the peer also held the negotiated handshake secret and agreed on the same transcript.

Two proofs, two keys, two boundaries

CertificateVerify and Finished stand next to each other on the wire but answer different questions.

CertificateVerify covers a domain-separated construction containing the transcript hash through the Certificate message. Its signature proves that the sender controls the private key corresponding to the credential it presented. Certificate-path and service-identity checks remain separate: a mathematically valid signature can still belong to a certificate that policy rejects.

Finished is not another certificate signature. TLS 1.3 derives a finished_key from the sender's handshake traffic secret, then computes an HMAC over the transcript hash through CertificateVerify when that message is present. A valid result confirms knowledge of the current handshake key material and agreement on the authenticated history at that boundary.

That separation is visible in PSK mode. A resumed or externally provisioned PSK handshake can omit Certificate and CertificateVerify entirely, yet both parties still send Finished. Conversely, a valid CertificateVerify followed by an invalid Finished is a failed handshake, not a mostly successful one.

The transcript is precise, not a packet-shaped memory

The transcript hash covers handshake messages in order, including each handshake message's type and length header. It does not hash TLS record headers, alerts or arbitrary application records. Records can fragment or combine handshake messages without changing the logical transcript.

HelloRetryRequest adds a special rule: the first ClientHello is represented by a synthetic message_hash before the retry sequence continues. That is why a packet collector cannot safely reconstruct Finished input by concatenating visible record payloads. It must implement the protocol transcript rules and, for encrypted TLS 1.3 messages, possess endpoint-derived diagnostic secrets or equivalent implementation evidence.

The negotiated cipher suite selects the hash used by both the transcript and HKDF. In TLS 1.3 the Finished verify_data length equals that hash's output length. Importing the old TLS 1.2 assumption of a fixed 12-byte value creates another plausible but false control.

Completion has a direction

Each endpoint derives separate client and server handshake traffic secrets. Each side therefore creates its own Finished value and verifies the peer's value at a different moment.

After a server sends its Finished, it switches its sending side to application traffic keys. It may send application data before receiving the client's Finished. The standard is explicit about the missing assurance: at that point the server does not yet know the client's identity or even its liveness, because the ClientHello could have been replayed.

The client's position is different. It verifies the server Finished, sends any required client Certificate and CertificateVerify, then sends its own Finished. A single field called tls_complete erases which endpoint sent what, which peer value was verified and whether client authentication was still pending.

The narrow shared invariant is better: for each direction record finished_sent, peer_finished_verified and the implementation state that made ordinary application data eligible. Only the application can then decide which combination is sufficient for a particular action.

Early data is an explicit exception, not evidence of completion

TLS 1.3 permits a client to send 0-RTT data before it receives the server Finished. That traffic is deliberately weaker: it is linked to a PSK from an earlier session and may be replayed. Its presence proves neither that the server accepted it nor that the current handshake later completed.

Operational records therefore need at least three states: early data offered, accepted or rejected; server Finished verified; and ordinary post-handshake traffic established. Combining them into “encrypted request received” allows a replayable request to inherit the authority of a completed fresh handshake.

EAP-TLS provides a useful integration lesson. A surrounding protocol may define its own connected or tunnel-ready state around the TLS handshake. The outer state is not allowed to move earlier merely because one inner authentication message looked valid.

A proxy creates two cryptographic histories

At a TLS terminator, the downstream client and proxy share one transcript and one pair of Finished values. The proxy and origin share another. Even if both legs use TLS 1.3, their random values, negotiated parameters, secrets, credentials and completion times differ.

Copying peer_finished_verified=true from the edge connection onto the upstream request invents continuity. The upstream leg must establish its own evidence, and the proxy must add a separately authenticated application correlation if the origin needs to know anything about the downstream identity.

The same rule applies to retries and pools. A request moved to a second upstream connection cannot inherit the first connection's Finished state. Correlation may explain the move; it cannot merge cryptographic histories.

Runtime APIs are witnesses with limited vocabularies

OpenSSL's SSL_is_init_finished() reports whether its state machine has reached a point where fully protected application data can be transferred. The documentation also warns that early-data states do not fit a simple before/in-init/finished partition. SSL_get_verify_result() reports certificate verification, not Finished verification or application authorization.

Key-log callbacks can support controlled decryption and transcript reconstruction, but the output is sensitive key material. Enabling it across production merely to enrich a dashboard creates a new authority leak. A safer default is to retain state transitions, negotiated identifiers, error classes and bounded trace correlation, reserving raw secrets for isolated conformance tests.

Implementation names are not portable proof. BoringSSL's current headers document that its Finished accessors return zero for TLS 1.3. GnuTLS exposes handshake hooks and a dedicated Finished-packet error, while warning that input observed before handshake completion cannot yet be treated as protected from an active intermediary. A control must be defined by the protocol state it needs, then mapped and tested separately for each library.

The evidence ledger

A reviewable connection record should preserve the endpoint role, direction, negotiated version, cipher suite, transcript hash, authentication mode, certificate-policy result, CertificateVerify result, server Finished state, client Finished state, early-data disposition and TLS termination leg.

It should also preserve failure. A decrypt_error following a valid CertificateVerify is not noise to be collapsed into “certificate success”. It is the evidence that the later boundary rejected the handshake.

The strongest conformance test changes one thing at a time. Mutate one transcript byte or one Finished byte and require a terminal failure. Keep the certificate and signature valid while corrupting Finished. Withhold the client Finished after accepting the server's. Exercise a PSK handshake with no certificate messages. Force HelloRetryRequest. Repeat through a proxy whose upstream parameters differ.

Those tests prove that the running implementation respects the boundary. An RFC citation, API symbol or configured callback only proves that a rule or capability exists.