The browser shows a padlock and hides the rest. curl and openssl show all of it: the protocol version, the key exchange, the certificates, and how many milliseconds each part takes. Below are eleven checks, each one a command and the output I got.
Setup: macOS 26.6, Homebrew curl 8.13 and OpenSSL 3.6. The curl that ships with macOS is built against a different TLS library and did not give me TLS 1.3 details, so install the Homebrew one and put it first in PATH:
brew install curl openssl@3
export PATH="$(brew --prefix curl)/bin:$(brew --prefix openssl@3)/bin:$PATH"
Most output was captured on 2 October 2026. Google rotates certificates and servers, so your dates, addresses and timings will differ.
The checks, in one table
| Question | What I saw |
|---|---|
| Which TLS version and key exchange does Google use? | TLS 1.3, AES-256-GCM, X25519MLKEM768 (post-quantum hybrid) |
| What does the same server pick with TLS 1.2? | Plain X25519, no post-quantum part |
| Who signed google.com’s certificate? | WR2, signed by GTS Root R1, cross-signed by GlobalSign Root CA |
| How long is the certificate valid? | 84 days |
| Which names does it cover? | google.com, youtube.com, youtu.be, android.com, goo.gl and more |
| Can I sign my own certificate? | Yes, in four openssl commands |
| What does curl say about it? | Error 60 until I pass my CA with --cacert |
| What do broken certificates look like? | Three different error 60 messages on badssl.com |
| Is TLS 1.3 faster? | About 14 to 18 ms against about 22 to 24 ms for TLS 1.2 |
| Where do the 107 ms of one request go? | Mostly into waiting for the first byte |
| Does a second request pay for the handshake again? | No, curl reuses the connection |
Check 1: one real connection
curl -v prints the handshake details on stderr. grep keeps the lines that matter.
curl -v -o /dev/null https://www.google.com 2>&1 | grep -E 'ALPN|SSL connection|^> (GET|Host)|^< HTTP'
* ALPN: curl offers h2,http/1.1
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / id-ecPublicKey
* ALPN: server accepted h2
> GET / HTTP/2
> Host: www.google.com
< HTTP/2 200 output
The SSL connection line has four parts. TLSv1.3 is the protocol. TLS_AES_256_GCM_SHA384 is the cipher suite that encrypts the data. X25519MLKEM768 is the key exchange: classic X25519 and ML-KEM-768 (NIST FIPS 203) combined, so an attacker has to break both. id-ecPublicKey means the certificate holds an elliptic-curve key.
The ALPN lines are the negotiation of the HTTP version, done inside the TLS handshake: curl offers HTTP/2 and HTTP/1.1, Google picks h2.
openssl s_client reports the same connection with different labels:
openssl s_client -connect google.com:443 -servername google.com </dev/null 2>/dev/null \
| grep -E 'Protocol|Cipher is|Negotiated TLS1.3 group|Peer signature type'
Peer signature type: ecdsa_secp256r1_sha256
Negotiated TLS1.3 group: X25519MLKEM768
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3output
Peer signature type is how the server proved it owns the certificate’s private key: an ECDSA signature on the P-256 curve.
Check 2: the same server over TLS 1.2
Force TLS 1.2 and ask openssl which temporary key the server used.
openssl s_client -connect google.com:443 -servername google.com -tls1_2 </dev/null 2>&1 \
| grep -E 'Protocol|Cipher is|Peer Temp Key'
New, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
Protocol: TLSv1.2
Peer Temp Key: X25519, 253 bitsoutput
Same server, same day (16 October), and the key exchange is plain X25519. In my tests the post-quantum hybrid only appeared on TLS 1.3 connections. The TLS 1.2 cipher name also spells out the whole recipe in one string: ECDHE key exchange, ECDSA signature, ChaCha20-Poly1305 for the data. TLS 1.3 moved the key exchange and signature out of the cipher suite name, which is why its names are shorter.
Check 3: the certificate chain
-showcerts prints every certificate the server sends. s: is the subject, i: the issuer.
openssl s_client -connect google.com:443 -servername google.com -showcerts </dev/null 2>/dev/null \
| grep -E '^ *[0-9] s:|^ *i:'
0 s:CN=*.google.com
i:C=US, O=Google Trust Services, CN=WR2
1 s:C=US, O=Google Trust Services, CN=WR2
i:C=US, O=Google Trust Services LLC, CN=GTS Root R1
2 s:C=US, O=Google Trust Services LLC, CN=GTS Root R1
i:C=BE, O=GlobalSign nv-sa, OU=Root CA, CN=GlobalSign Root CAoutput
Read it as links: each issuer on one line is the subject of the next certificate. Certificate 2 is a version of Google’s root signed by GlobalSign, an older root. A client whose trust store already has GTS Root R1 stops at certificate 1. A client that only has GlobalSign can still finish the path through certificate 2.
GlobalSign Root CA itself is not in the list. A root is trusted because the client already has it in its own trust store, so a server has no reason to send it.
Check 4: the dates
openssl s_client -connect google.com:443 -servername google.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
subject=CN=*.google.com
issuer=C=US, O=Google Trust Services, CN=WR2
notBefore=Sep 10 19:21:53 2026 GMT
notAfter=Dec 3 19:21:52 2026 GMToutput
84 days. When I ran it again on 16 October I got a different certificate, valid from 18 September to 11 December: also 84 days, and also issued by WR2. Two weeks apart, the same name was served with two different certificates.
Public certificates are getting shorter for everyone. Under CA/Browser Forum ballot SC-081 the cap went from 398 to 200 days in March 2026. It drops to 100 days in March 2027 and to 47 in March 2029. Renewing by hand stops being practical well before the end of that schedule, which is why ACME clients such as certbot exist.
For a monitoring script, -checkend answers “does this expire within N seconds?” and sets the exit code:
openssl s_client -connect google.com:443 -servername google.com </dev/null 2>/dev/null \
| openssl x509 -noout -enddate -checkend 2592000
notAfter=Dec 11 20:00:10 2026 GMT
Certificate will not expireoutput
2592000 is 30 days. Exit code 0 means more than 30 days are left, 1 means it expires sooner.
Check 5: which names the certificate covers
The Subject Alternative Name extension is the list browsers actually check. The common name (CN=) is ignored for matching by modern clients.
openssl s_client -connect google.com:443 -servername google.com </dev/null 2>/dev/null \
| openssl x509 -noout -ext subjectAltName | tr ',' '\n' \
| grep -E 'DNS:(google\.com|youtube\.com|android\.com|goo\.gl|youtu\.be)$'
DNS:android.com
DNS:goo.gl
DNS:google.com
DNS:youtu.be
DNS:youtube.comoutput
The grep keeps five names to keep the output short. Without it, the certificate I got on 16 October lists 65 names.
Check 6: a certificate authority in four commands
Create a root key and a self-signed root certificate, then a key and a signing request for the server, then sign it. P-256 keys keep everything small.
mkdir tls-demo && cd tls-demo
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout ca.key -out ca.crt -days 365 -subj "/CN=Demo Root CA"
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout server.key -out server.csr -subj "/CN=localhost"
echo "subjectAltName = DNS:localhost" > san.ext
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 30 -extfile san.ext -out server.crt
openssl verify -CAfile ca.crt server.crt
Certificate request self-signature ok
subject=CN=localhost
server.crt: OKoutput
san.ext is the step people skip. Without it the certificate has only CN=localhost, and curl and browsers reject it for a name mismatch even though the chain is fine.
Check 7: curl and an unknown CA
Serve the certificate with openssl’s built-in test server and call it.
openssl s_server -accept 4443 -cert server.crt -key server.key -www &
curl -sS https://localhost:4443/
curl: (60) SSL certificate problem: unable to get local issuer certificateoutput
The chain is valid, but it ends at a root curl does not have. Pass the root explicitly:
curl --cacert ca.crt -s -o /dev/null -w '%{http_code} verify=%{ssl_verify_result}\n' https://localhost:4443/
kill %1
200 verify=0output
ssl_verify_result 0 means no verification error. --cacert replaces the trust store for this one call. Adding ca.crt to the system keychain would make every program on the machine trust it, which is the same thing company proxies do with their own root.
Check 8: three broken certificates
badssl.com keeps broken certificates online for testing clients.
curl -sS https://expired.badssl.com/
curl -sS https://wrong.host.badssl.com/
curl -sS https://self-signed.badssl.com/
curl: (60) SSL certificate problem: certificate has expired
curl: (60) SSL: no alternative certificate subject name matches target hostname 'wrong.host.badssl.com'
curl: (60) SSL certificate problem: self-signed certificateoutput
All three are exit code 60, so a script that only checks the exit code cannot tell them apart. The message can: expired dates, a missing SAN entry, or a chain that ends at a certificate nobody trusts. -k would make all three “work”, and it disables exactly the check that failed.
Check 9: TLS 1.2 against TLS 1.3
curl’s time_connect is when TCP was ready and time_appconnect is when TLS finished, both counted from the start, so subtracting one from the other gives the handshake time.
for i in 1 2 3; do curl --tls-max 1.2 -s -o /dev/null -w 'TLS 1.2 tcp=%{time_connect} tls=%{time_appconnect}\n' https://www.google.com; done
for i in 1 2 3; do curl --tlsv1.3 -s -o /dev/null -w 'TLS 1.3 tcp=%{time_connect} tls=%{time_appconnect}\n' https://www.google.com; done
TLS 1.2 tcp=0.011102 tls=0.117446
TLS 1.2 tcp=0.010225 tls=0.034364
TLS 1.2 tcp=0.010840 tls=0.032984
TLS 1.3 tcp=0.009499 tls=0.023786
TLS 1.3 tcp=0.010548 tls=0.028259
TLS 1.3 tcp=0.011232 tls=0.026321output
| Run | TLS 1.2 handshake | TLS 1.3 handshake |
|---|---|---|
| 1 | 106.3 ms (network hiccup, ignored) | 14.3 ms |
| 2 | 24.1 ms | 17.7 ms |
| 3 | 22.1 ms | 15.1 ms |
TCP took about 10 ms, which is one round trip to this Google server. TLS 1.2 needs about two round trips before data can flow, TLS 1.3 about one, and the numbers line up with that. Three runs are a small sample, so run it ten times on your own network before quoting a number.
Check 10: where the time of one request goes
dig google.com | grep -E 'IN\s+A\s|Query time'
curl -s -o /dev/null -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} first-byte=%{time_starttransfer} total=%{time_total}\n' https://www.google.com
google.com. 72 IN A 172.217.171.110
;; Query time: 4 msec
dns=0.002775 tcp=0.012095 tls=0.028537 first-byte=0.086848 total=0.107017output
| Step | Ends at | Takes |
|---|---|---|
| DNS lookup | 2.8 ms | 2.8 ms |
| TCP handshake | 12.1 ms | 9.3 ms |
| TLS handshake | 28.5 ms | 16.4 ms |
| Waiting for the first byte | 86.8 ms | 58.3 ms |
| Rest of the response | 107.0 ms | 20.2 ms |
DNS was fast because the answer was cached nearby (the 72 in the dig output is the remaining TTL in seconds). The biggest slice is the wait between sending the request and getting the first byte, which is mostly the server building the page. Connection setup, DNS through TLS, is under 30 ms of the 107.
The response also tells the client about HTTP/3:
curl -sI https://www.google.com | grep -i alt-svc
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000output
h3=":443" means HTTP/3 over QUIC on UDP port 443, and ma=2592000 says the hint is valid for 30 days. My Homebrew curl is built without HTTP/3 support, so it stays on HTTP/2; a browser would switch on its next connection.
Check 11: the second request
Give curl the same URL twice in one call. It keeps the connection open between them.
curl -s -o /dev/null -o /dev/null \
-w 'tcp=%{time_connect} tls=%{time_appconnect} new-connections=%{num_connects}\n' \
https://www.google.com https://www.google.com
tcp=0.009827 tls=0.025541 new-connections=1
tcp=0.000000 tls=0.000000 new-connections=0output
The second request opened no new connection, so it paid nothing for TCP or TLS. This is why keep-alive and connection pools matter more than handshake tuning for most services: a pooled client pays the 25 ms once, not on every call.
What I would take from this
curl -vandopenssl s_clientanswer most “what is this connection using” questions in one line.- The post-quantum key exchange is already live on TLS 1.3 to Google. On TLS 1.2 I only saw classic X25519.
- Read a chain issuer to subject. The root is never sent; it has to be in the client’s trust store.
- Put names in the SAN. The common name is not checked.
- Error 60 covers many different problems. Read the message, and never fix it with
-k. - TLS 1.3 saves about one round trip per new connection. Reusing connections saves the whole handshake.