Skip to content

Add HTTP TLS peer cert tracing via CertificateTrace (Phase 1)#21599

Merged
jheysel-r7 merged 6 commits into
rapid7:masterfrom
Pushpenderrathore:feature/ssl-peer-cert-trace
Jul 15, 2026
Merged

Add HTTP TLS peer cert tracing via CertificateTrace (Phase 1)#21599
jheysel-r7 merged 6 commits into
rapid7:masterfrom
Pushpenderrathore:feature/ssl-peer-cert-trace

Conversation

@Pushpenderrathore

@Pushpenderrathore Pushpenderrathore commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends CertificateTrace (the existing mixin for issued cert and CSR tracing) to also surface the server's TLS peer certificate when an HTTP module connects over HTTPS. No new operator-facing option is introduced -- operators use the same CertificateTrace enum (off/metadata/full) they already know.

Key design points:

  • New public method #certificate_peer_cert_trace(cert, host, port) added to Msf::Exploit::Remote::CertificateTrace
  • HttpClient calls it after send_recv when a raw peer cert is available
  • Dedup per session: DB-backed (ssl.peer_cert_seen note, update: :unique) with in-memory @_peer_cert_seen fallback when no DB
  • Plain HTTP connections (no peer_cert) are silently ignored
  • Removed the previous standalone SslPeerCertTrace mixin and its SSLPeerCertTrace datastore option

Test plan

Rebased onto latest master; the conflicts with the merged CSR trace work (#21580) were resolved (both certificate_peer_cert_trace and certificate_csr_trace kept as sibling methods). All checks below re-run after the merge.

Peer cert labeling: peer certs now print under a Peer Cert header (a label: keyword was added to CertificateTracePresenter#to_s_metadata/#to_s_full, default x.509), so a server's TLS certificate is no longer visually indistinguishable from an issued/client certificate. Issued-cert (x.509) and CSR output are unchanged.

Unit specs (this branch, post-merge):

  • bundle exec rspec spec/lib/msf/core/exploit/remote/certificate_trace_spec.rb - 54 examples, 0 failures

Live test - HTTP peer cert trace against a real HTTPS server (auxiliary/scanner/http/http_version vs example.com:443, CertificateTrace=full):

[CertificateTrace] Peer Cert ----------------------------
  Subject    : /CN=example.com
  Issuer     : /C=US/O=SSL Corporation/CN=Cloudflare TLS Issuing ECC CA 3
  Not Before : 2026-05-31 21:39:12 UTC
  Not After  : 2026-08-29 21:41:26 UTC
  SHA-256    : beab14cf39678fda0ef1606eedb818c2298ba2cc7a00886e7dc2d2410f24cd35
  Serial     : 35428337808578903465180920265426569102
  Version    : v3
  Public Key : EC-256
  Identity   : example.com (Subject CN)
  • Dedup confirmed: one trace per distinct host:port (DB-backed ssl.peer_cert_seen note when a database is connected, in-memory fallback otherwise).
  • Plain HTTP connections (no peer cert) produce no output.

Phasing

This PR covers HTTP only (Phase 1). Follow-up PR covers LDAP over TLS and RDP (Phase 2), with Postgres and SMB over TLS pending TLS support in those mixins.

Depends on: #21198 (already merged)

Introduces Msf::Exploit::Remote::SslPeerCertTrace with an SSLPeerCertTrace
enum datastore option (off/metadata/full). HttpClient includes the mixin and
calls ssl_peer_cert_trace after each send_recv; the helper reads conn.peer_cert,
deduplicates per host:port, and formats the server certificate through the
existing CertificateTracePresenter.
Removes the standalone SSLPeerCertTrace mixin and SSLPeerCertTrace datastore
option. Peer SSL certificate tracing now lives in CertificateTrace as
#certificate_peer_cert_trace, reusing the existing CertificateTrace enum and
CertificateTraceColors option so all cert output (issued, CSR, peer) is
controlled by a single operator-facing option.

Deduplication now uses the database when active (ssl.peer_cert_seen note per
host:port, update: :unique) so the same server certificate is not reprinted
across module runs. Falls back to in-memory dedup when no DB is connected.

HttpClient is updated to include CertificateTrace instead of SslPeerCertTrace
and calls certificate_peer_cert_trace after send_recv. The ssl_peer_cert_trace
mixin file and its spec are removed; the 20 new peer cert examples are added
to certificate_trace_spec.rb (46 examples, 0 failures).
@Pushpenderrathore Pushpenderrathore changed the title Add SSLPeerCertTrace mixin for HTTP TLS peer certificate tracing Add HTTP TLS peer cert tracing via CertificateTrace (Phase 1) Jun 25, 2026
@Pushpenderrathore
Pushpenderrathore marked this pull request as ready for review June 25, 2026 17:57
certificate_trace already tested the invalid-cert no-raise case;
add the same coverage for certificate_peer_cert_trace.
Resolve conflicts from the merged CSR trace work (rapid7#21580):
- certificate_trace.rb: keep both certificate_peer_cert_trace and
  certificate_csr_trace as sibling methods
- certificate_trace_spec.rb: keep both describe blocks as siblings

Presenter takes master's trace_separator version; peer cert method
uses it transparently via the presenter public API.
@Pushpenderrathore

Copy link
Copy Markdown
Contributor Author

Rebased/merged latest master and resolved the conflicts.

The conflicts were both-add collisions with the CSR trace work (#21580) that landed on master after this branch was cut. Both PRs inserted a new method into certificate_trace.rb and a new describe block into the spec at the same anchor, so git flagged them even though the additions are independent.

Resolution:

  • certificate_trace.rb: kept both certificate_peer_cert_trace and certificate_csr_trace as sibling methods.
  • certificate_trace_spec.rb: kept both describe blocks as siblings.
  • The presenter took master's trace_separator version; the peer cert method uses it transparently via the presenter API (no stale SEPARATOR reference).

Spec passes locally: 52 examples, 0 failures. Branch is now mergeable.

Addresses review feedback that a server's TLS peer certificate printed
under the same '[CertificateTrace] x.509' header as an issued/client
certificate, making the two indistinguishable.

Add an optional label: keyword to CertificateTracePresenter#to_s_metadata
and #to_s_full (default 'x.509', backward compatible). certificate_peer_cert_trace
now renders peer certs under a 'Peer Cert' header. Issued-cert and CSR
output are unchanged.
@Pushpenderrathore

Pushpenderrathore commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the peer-cert labeling feedback: a server's TLS peer certificate was printing under the same [CertificateTrace] x.509 header as an issued/client cert, making the two indistinguishable.

Added an optional label: keyword to CertificateTracePresenter#to_s_metadata/#to_s_full (default x.509, backward compatible). certificate_peer_cert_trace now renders peer certs under a Peer Cert header; issued-cert and CSR output are unchanged. Specs updated to lock in both the new Peer Cert label and the unchanged x.509 issued-cert label.

Verified live with http_version vs example.com:443 in full mode:

[CertificateTrace] Peer Cert ----------------------------
  Subject    : /CN=example.com
  Issuer     : /C=US/O=SSL Corporation/CN=Cloudflare TLS Issuing ECC CA 3
  Not Before : 2026-05-31 21:39:12 UTC
  Not After  : 2026-08-29 21:41:26 UTC
  SHA-256    : beab14cf39678fda0ef1606eedb818c2298ba2cc7a00886e7dc2d2410f24cd35
  Serial     : 35428337808578903465180920265426569102
  Version    : v3
  Public Key : EC-256
  Identity   : example.com (Subject CN)
  SAN        : DNS:example.com, DNS:*.example.com
  EKU        : TLS Web Server Authentication
  Key Usage  : Digital Signature
  Extensions :
    basicConstraints : CA:FALSE
    authorityKeyIdentifier : 83:03:FD:E7:F6:F5:4A:4D:15:41:F4:ED:22:16:D3:32:0A:3E:CA:66
    authorityInfoAccess : CA Issuers - URI:http://i.cf-i.ssl.com/Cloudflare-TLS-I-E3.cer
OCSP - URI:http://o.cf-i.ssl.com
    certificatePolicies : Policy: 2.23.140.1.2.1
Policy: 1.3.6.1.4.1.38064.1.3.1.1
    crlDistributionPoints : Full Name:
  URI:http://c.cf-i.ssl.com/ae801ed1c55bb579d79208b0d772acfb8cc3a208.crl

    1.3.6.1.4.1.44363.44 : 05:00
    ct_precert_scts : 04:81:F2:00:F0:00:76:00:94:4E:43:87:FA:EC:C1:EF:81:F3:19:24:26:A8:18:65:01:C7:D3:5F:38:02:01:3F:72:67:7D:55:37:2E:19:D8:00:00:01:9E:80:03:31:89:00:00:04:03:00:47:30:45:02:21:00:81:BF:42:4C:D2:65:DA:8A:24:3D:79:FB:80:AE:49:37:3B:6B:7F:45:0D:FD:AB:B3:B7:A0:AB:58:0A:F3:94:85:02:20:76:7D:AE:B7:9A:E7:F8:05:B3:06:A6:7D:9B:55:4E:94:C2:E9:4F:F8:F2:D8:CF:76:22:CE:A2:B4:CC:BD:AD:1F:00:76:00:C8:A3:C4:7F:C7:B3:AD:B9:35:6B:01:3F:6A:7A:12:6D:E3:3A:4E:43:A5:C6:46:F9:97:AD:39:75:99:1D:CF:9A:00:00:01:9E:80:03:31:A0:00:00:04:03:00:47:30:45:02:21:00:A8:73:CA:F4:2D:58:A0:6B:3E:A9:BC:6F:2C:BF:4F:AE:06:AC:DF:F8:B3:CD:3F:1C:18:C6:D8:24:B9:57:4D:33:02:20:01:B2:BF:A9:13:49:21:69:63:21:5A:77:D0:F1:56:8E:63:3E:4A:7A:13:F3:44:A1:68:F2:4C:19:D6:ED:34:B3

Only the header line differs from an issued or client cert (Peer Cert vs x.509); every field comes from the same to_s_full path.

@jheysel-r7 jheysel-r7 added the GSoC Google Summer of Code project PRs label Jul 15, 2026
@jheysel-r7 jheysel-r7 self-assigned this Jul 15, 2026
@jheysel-r7
jheysel-r7 requested a review from Copilot July 15, 2026 17:53

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

This PR extends Metasploit’s existing Msf::Exploit::Remote::CertificateTrace facility to optionally trace an HTTPS server’s TLS peer certificate from HttpClient connections, using the existing CertificateTrace verbosity enum (off/metadata/full) and presenter formatting.

Changes:

  • Add #certificate_peer_cert_trace(cert, host, port) to the CertificateTrace mixin with per-host:port dedup (DB-backed when available, in-memory fallback otherwise).
  • Wire HttpClient#send_request_raw to invoke peer-cert tracing when an underlying connection exposes peer_cert.
  • Update CertificateTracePresenter#to_s_metadata / #to_s_full to accept a label: so peer cert output can be distinguished from issued cert output (Peer Cert vs x.509), and add unit specs covering the new behavior.

Impact Analysis:

  • Blast radius: high — Msf::Exploit::Remote::HttpClient is widely mixed into HTTP modules, so tracing behavior and option registration can affect many modules.
  • Data and contract effects: adds a new public mixin method and causes optional console output; introduces DB notes of type ssl.peer_cert_seen for dedup when the DB is active.
  • Rollback and test focus: focus validation on common HTTPS modules using send_request_raw, ensuring (1) no output when CertificateTrace=off, (2) single trace per host:port, (3) no DB query/write regressions on repeated requests, and (4) consistent behavior on invalid CertificateTrace values.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
spec/lib/msf/core/exploit/remote/certificate_trace_spec.rb Adds specs for peer-cert tracing modes, labeling, dedup, and color wrapping.
lib/msf/core/trace/certificate_trace_presenter.rb Adds label: to metadata/full rendering to distinguish peer cert headers.
lib/msf/core/exploit/remote/http_client.rb Includes CertificateTrace and invokes peer-cert tracing after send_recv when available.
lib/msf/core/exploit/remote/certificate_trace.rb Implements certificate_peer_cert_trace with dedup and presenter dispatch.

Comment on lines +79 to +97
@_peer_cert_seen ||= {}
key = "#{host}:#{port}"
return if @_peer_cert_seen[key]

if respond_to?(:framework) && framework&.db&.active
already_noted = framework.db.notes(ntype: 'ssl.peer_cert_seen').any? do |n|
n.host&.address == host && n.service&.port == port
end
if already_noted
@_peer_cert_seen[key] = true
return
end
framework.db.report_note(
host: host, port: port, proto: 'tcp',
ntype: 'ssl.peer_cert_seen',
data: {},
update: :unique
)
end
Comment thread lib/msf/core/exploit/remote/certificate_trace.rb
Mirror the metadata/full/else dispatch used by certificate_trace and
certificate_csr_trace so an unrecognized CertificateTrace value warns
via vprint_warning and produces no output instead of silently falling
through the metadata branch. Add matching unknown-mode spec coverage.
@Pushpenderrathore

Copy link
Copy Markdown
Contributor Author

Hey Jack, pushed a small follow-up from the Copilot review.

Addressed (Copilot #2): certificate_peer_cert_trace was using a ternary that silently treated any non-full value as metadata. I switched it to the same metadata / full / else case dispatch that certificate_trace and certificate_csr_trace already use, so an unrecognized CertificateTrace value now warns via vprint_warning and prints nothing instead of falling through. Added matching unknown-mode spec coverage. Full mixin spec is green (55 examples, 0 failures).

Left as-is (Copilot #1, the notes lookup / N+1): I looked at this one and decided not to touch it in this PR, wanted to flag why. The lookup does load the ssl.peer_cert_seen notes and filter in Ruby, and n.service&.port is an extra query per note since db.notes only eager-loads :host. But the in-memory cache means the DB is only hit once per host:port per module run (covered by a spec), so it fires once on first sighting over a small note set. Fixing it properly would mean either a scoped query that needs a change to core db_manager/note.rb, or stashing the host:port key in the note data and reworking the dedup specs. Both feel like more churn than the issue warrants on a trace path. Happy to do it in a separate change if you'd rather, but figured keeping this PR tight was the better call. Let me know if you disagree.

@jheysel-r7 jheysel-r7 added enhancement rn-enhancement release notes enhancement labels Jul 15, 2026

@jheysel-r7 jheysel-r7 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.

Looks great, thanks @Pushpenderrathore

Testing

msf auxiliary(scanner/http/http_version) > run
[CertificateTrace] Peer Cert ----------------------------
  Subject    : /CN=example.local
  Issuer     : /CN=example.local
  Not Before : 2026-07-15 14:56:00 UTC
  Not After  : 2027-07-15 14:56:00 UTC
  SHA-256    : 3099b8eb5c278d4bda0d6a4567d3b6e2777160deca9efd2fa538bbce66522e2c
  Serial     : 146406233325429125722816363610311164214444770560
  Version    : v3
  Public Key : RSA-2048
  Identity   : example.local (Subject CN)
  Extensions :
    subjectKeyIdentifier : 84:45:D1:67:08:B6:57:5A:87:5C:9C:A9:5C:45:7E:F2:5B:E8:F4:BF
    authorityKeyIdentifier : 84:45:D1:67:08:B6:57:5A:87:5C:9C:A9:5C:45:7E:F2:5B:E8:F4:BF
    basicConstraints : CA:TRUE
[+] 172.16.199.136:8443
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf auxiliary(scanner/http/http_version) > run
[+] 172.16.199.136:8443
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf auxiliary(scanner/http/http_version) > notes

Notes
=====

 Time                     Host            Service  Port  Protocol  Type                Data
 ----                     ----            -------  ----  --------  ----                ----
 2026-07-15 17:23:21 UTC  172.16.199.136  https    8443  tcp       ssl.peer_cert_seen  {}
msf auxiliary(scanner/http/http_version) > db_disconnect
Successfully disconnected from the data service: local_db_service.
msf auxiliary(scanner/http/http_version) > run
[CertificateTrace] Peer Cert ----------------------------
  Subject    : /CN=example.local
  Issuer     : /CN=example.local
  Not Before : 2026-07-15 14:56:00 UTC
  Not After  : 2027-07-15 14:56:00 UTC
  SHA-256    : 3099b8eb5c278d4bda0d6a4567d3b6e2777160deca9efd2fa538bbce66522e2c
  Serial     : 146406233325429125722816363610311164214444770560
  Version    : v3
  Public Key : RSA-2048
  Identity   : example.local (Subject CN)
  Extensions :
    subjectKeyIdentifier : 84:45:D1:67:08:B6:57:5A:87:5C:9C:A9:5C:45:7E:F2:5B:E8:F4:BF
    authorityKeyIdentifier : 84:45:D1:67:08:B6:57:5A:87:5C:9C:A9:5C:45:7E:F2:5B:E8:F4:BF
    basicConstraints : CA:TRUE
[+] 172.16.199.136:8443
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf auxiliary(scanner/http/http_version) > run
[CertificateTrace] Peer Cert ----------------------------
  Subject    : /CN=example.local
  Issuer     : /CN=example.local
  Not Before : 2026-07-15 14:56:00 UTC
  Not After  : 2027-07-15 14:56:00 UTC
  SHA-256    : 3099b8eb5c278d4bda0d6a4567d3b6e2777160deca9efd2fa538bbce66522e2c
  Serial     : 146406233325429125722816363610311164214444770560
  Version    : v3
  Public Key : RSA-2048
  Identity   : example.local (Subject CN)
  Extensions :
    subjectKeyIdentifier : 84:45:D1:67:08:B6:57:5A:87:5C:9C:A9:5C:45:7E:F2:5B:E8:F4:BF
    authorityKeyIdentifier : 84:45:D1:67:08:B6:57:5A:87:5C:9C:A9:5C:45:7E:F2:5B:E8:F4:BF
    basicConstraints : CA:TRUE
[+] 172.16.199.136:8443
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

@github-project-automation github-project-automation Bot moved this from Todo to In Progress in Metasploit Kanban Jul 15, 2026
@jheysel-r7
jheysel-r7 merged commit 67c4478 into rapid7:master Jul 15, 2026
44 of 45 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Metasploit Kanban Jul 15, 2026
@jheysel-r7

Copy link
Copy Markdown
Contributor

Release Notes

This extends CertificateTrace functionality to also surface the server's TLS peer certificate when an HTTP module connects over HTTPS. This makes use of the same CertificateTrace enum (off/metadata/full) operators are already familiar with.

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

Labels

enhancement GSoC Google Summer of Code project PRs rn-enhancement release notes enhancement

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants