Computing FoundationsHow the Internet Works › Day 19

Hands-on lab — Day 19: HTTPS and TLS: Encryption on the Wire

Commands

Setup

cd labs/sections/computing-foundations/day-019-https-and-tls-encryption-on-the

Run

bash examples/inspect_tls.sh
bash examples/inspect_tls.sh example.com
bash starter/inspect_tls.sh

Test

bash tests/run_tests.sh

File tree

examples/inspect_tls.sh
expected-output/FIELDS.md
expected-output/sample-example-com.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/inspect_tls.sh
starter/tls-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 019 lab — Inspect a TLS Certificate

Lesson

Purpose

Day 19's lesson explains how HTTPS secures a connection: the handshake, the mix of symmetric and public-key encryption, and the certificate chain of trust. This lab makes it concrete. You use openssl and curl to inspect a real TLS certificate — its subject, issuer, and validity dates — and to watch the TLS version get negotiated, so the padlock stops being magic and becomes something you can read for yourself.

Learning objectives

  • Read a certificate's subject, issuer, and not-before / not-after dates from the command line.
  • Distinguish the certificate's subject (the domain) from its issuer (the CA that signed it).
  • Observe the negotiated TLS version and the "certificate verify ok" result in a real handshake.
  • Trace the certificate chain a server sends and explain why a missing intermediate breaks trust.
  • Complete a working shell script by filling in four well-specified exercises.

Prerequisites

  • The Day 19 lesson (read it first — it explains every concept this lab inspects).
  • Comfort running commands in a terminal (Days 8–14).
  • An internet connection (this lab opens a real HTTPS connection).

Supported operating systems

  • macOS — fully supported (tested on macOS with Apple Silicon, OpenSSL 3.x).
  • Linux — fully supported (any distribution with openssl and curl).
  • Windows — use WSL and follow the Linux path, or Git Bash (which bundles openssl and curl).

Hardware requirements

Any computer made in roughly the last 15 years. The lab only reads a public certificate over the network; it needs no special hardware.

Required software

  • openssl (LibreSSL on macOS or OpenSSL 3.x — both work; preinstalled).
  • curl (preinstalled on macOS and Linux).
  • Network access to reach the host being inspected.

Free and open-source options

Everything in this lab is free and open source: openssl and curl ship with your OS or install at no cost, and inspecting a public certificate costs nothing. Getting a certificate for a site you run is also free — the lesson covers Let's Encrypt, a non-profit CA that issues certificates at no charge.

Installation

None. Both tools are preinstalled. From the repository root:

cd labs/sections/computing-foundations/day-019-https-and-tls-encryption-on-the

File structure

day-019-https-and-tls-encryption-on-the/
├── README.md                       ← you are here
├── metadata.yml                    ← machine-readable lab metadata
├── starter/
│   ├── inspect_tls.sh              ← YOUR working file (4 exercises)
│   └── tls-worksheet.md            ← worksheet for the practice assignment
├── examples/
│   └── inspect_tls.sh              ← completed reference implementation
├── tests/
│   └── run_tests.sh                ← automated checks (skip gracefully offline)
├── expected-output/
│   ├── sample-example-com.txt      ← real captured run (macOS, example.com)
│   └── FIELDS.md                   ← required fields and platform differences
├── requirements/
│   └── README.md                   ← dependency statement (openssl, curl, network)
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished result first (defaults to example.com)
bash examples/inspect_tls.sh

## 2. Try another host of your choice
bash examples/inspect_tls.sh wikipedia.org

## 3. Your task: complete the four exercises in the starter, then run it
bash starter/inspect_tls.sh

## 4. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/inspect_tls.sh [host] — runs the reference script: it checks the host is reachable, then uses openssl s_client piped into openssl x509 to print the certificate's subject, issuer, and validity dates, and curl -vI to show the negotiated TLS version and the verification result.
  • bash starter/inspect_tls.sh — the same skeleton with four REPLACE ME lines; each exercise comment names the exact command to paste in. Edit the file in any text editor and replace each placeholder.
  • bash tests/run_tests.sh — runs structural checks always, and, when the network is available, verifies that openssl returns a subject, an issuer, and an expiry date and that the reference script completes. Offline, the certificate checks are skipped (not failed) and the script still exits 0.

Expected output

See expected-output/sample-example-com.txt — a real captured run:

=== TLS inspection for example.com ===
Generated on: 2026-07-12

--- Certificate (openssl) ---
subject=CN=example.com
issuer=C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
notBefore=May 31 21:39:12 2026 GMT
notAfter=Aug 29 21:41:26 2026 GMT

--- Handshake summary (curl) ---
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
*  subject: CN=example.com
*  expire date: Aug 29 21:41:26 2026 GMT
*  issuer: C=US; O=SSL Corporation; CN=Cloudflare TLS Issuing ECC CA 3
*  SSL certificate verify ok.

=== End of inspection ===

Your issuer, dates, and cipher will differ as certificates are renewed — the shape is what stays the same. expected-output/FIELDS.md lists the required fields and describes platform differences.

Validation steps

  1. Run bash examples/inspect_tls.sh — it should print a subject, issuer, and dates, then a SSL certificate verify ok. line.
  2. Confirm you can identify which line is the domain (subject) and which is the CA (issuer).
  3. Confirm you can read the expiry date and the negotiated TLS version.
  4. Complete the four exercises in starter/inspect_tls.sh and re-run it — its output should match the reference for the same host.
  5. Run the tests (next section) — all checks must pass (or skip cleanly offline).

Tests

bash tests/run_tests.sh

Expected final line online: 10 checks, 0 failure(s), 0 skipped. Offline the certificate checks are skipped and the line reports a non-zero skip count, but the command still exits 0 as long as nothing genuinely failed, so it is safe in CI.

Cleanup

Nothing to clean up: the scripts read a public certificate and print it, writing nothing outside their own console output. To reset your work, restore the starter from git: git checkout -- starter/inspect_tls.sh.

Troubleshooting

See troubleshooting.md for the full list (s_client appearing to hang, OpenSSL/LibreSSL flag differences, offline behavior, and curl verbose formatting).

Security notes

See security.md. Short version: this is read-only inspection of public certificates. The scripts send no data, need no elevated privileges, and never disable certificate verification.

Extension exercises

  1. Add -showcerts to the openssl command and keep the s: / i: lines to see the full chain the server sends; count the certificates and match each issuer to the next subject up.
  2. Inspect a site you run or your school's site and record its issuer and expiry; note whether it uses a free CA (such as Let's Encrypt) or a commercial one.
  3. Deliberately find a certificate problem: try https://expired.badssl.com or https://self-signed.badssl.com and read exactly which check fails and how curl reports it.
  • Previous day: Day 18 — HTTP: Requests, Responses, and Methods (labs/sections/computing-foundations/day-018-http-requests-responses-and-methods/).
  • Next day: Day 20 — How Browsers Render: HTML, CSS, and JavaScript (labs/sections/computing-foundations/day-020-how-browsers-render-html-css-and/, to be written).

Expected output

FIELDS.md

# Required output fields (all platforms)

A correct online run of `inspect_tls.sh` prints, in order:

1. `=== TLS inspection for <host> ===`
2. `Generated on: YYYY-MM-DD`
3. `--- Certificate (openssl) ---`
4. `subject=...` — the domain the certificate is for
5. `issuer=...` — the CA that signed the certificate
6. `notBefore=...` — start of the validity window
7. `notAfter=...` — expiry of the validity window
8. `--- Handshake summary (curl) ---`
9. A `SSL connection using TLSv1.x ...` line — the negotiated TLS version and cipher
10. `subject:`, `issuer:`, and `expire date:` lines from curl
11. `SSL certificate verify ok.` — chain of trust verified
12. `=== End of inspection ===`

`sample-example-com.txt` in this directory is a real captured run against
`example.com` (macOS, OpenSSL 3.x, 2026-07-12). The exact issuer, dates, and
cipher change whenever the site renews its certificate — the *shape* is what
stays constant.

## Platform differences (not fabricated — described)

- **Linux** produces the same fields. The `curl` verbose lines may be
  prefixed with connection markers like `* (IN)` / `* (OUT)`; the field
  names (`subject:`, `issuer:`, `expire date:`) are identical.
- **OpenSSL vs LibreSSL.** macOS has historically shipped LibreSSL as
  `/usr/bin/openssl` and Homebrew installs OpenSSL 3.x. Both support the
  `s_client`, `x509 -subject -issuer -dates`, and `-showcerts` flags used
  here; if a flag is rejected, run `openssl x509 -help` for your build's
  exact spelling. The capture above is from OpenSSL 3.x.
- **Offline.** With no network, the script prints the `OFFLINE or host
  unreachable` message and exits 0 instead of the certificate fields.
- **Windows.** Use WSL and follow the Linux path, or use Git Bash which
  bundles both `openssl` and `curl`.

sample-example-com.txt

=== TLS inspection for example.com ===
Generated on: 2026-07-12

--- Certificate (openssl) ---
subject=CN=example.com
issuer=C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
notBefore=May 31 21:39:12 2026 GMT
notAfter=Aug 29 21:41:26 2026 GMT

--- Handshake summary (curl) ---
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
*  subject: CN=example.com
*  expire date: Aug 29 21:41:26 2026 GMT
*  issuer: C=US; O=SSL Corporation; CN=Cloudflare TLS Issuing ECC CA 3
*  SSL certificate verify ok.

=== End of inspection ===

Source files

examples/inspect_tls.sh (1969 bytes)
#!/usr/bin/env bash
# Day 019 lab — completed reference implementation.
# Inspects the TLS certificate and handshake for a public HTTPS host,
# showing the certificate subject, issuer, and validity dates, and the
# negotiated TLS version. Read-only: it opens a connection and reads the
# public certificate, sends no data, and writes no files.
#
# Usage:  bash examples/inspect_tls.sh [host]
# Default host is example.com — a stable, well-known test domain.
set -uo pipefail

host="${1:-example.com}"

echo "=== TLS inspection for ${host} ==="
echo "Generated on: $(date '+%Y-%m-%d')"
echo

# --- Offline guard -----------------------------------------------------------
# This lab needs the network. If we cannot reach the host, say so clearly
# and exit 0 so a reader offline still gets a graceful result.
if ! curl -sI --max-time 15 "https://${host}" >/dev/null 2>&1; then
  echo "OFFLINE or host unreachable: could not open https://${host}"
  echo "Connect to the internet and re-run. (This lab requires network access.)"
  echo "=== End of inspection ==="
  exit 0
fi

# --- 1. Certificate identity via openssl s_client + x509 ---------------------
# s_client opens the TLS connection; </dev/null feeds empty input so it does
# not hang waiting for an HTTP request. x509 prints the fields we care about.
echo "--- Certificate (openssl) ---"
openssl s_client -connect "${host}:443" -servername "${host}" </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates
echo

# --- 2. Handshake summary via curl -v ----------------------------------------
# curl -vI makes a verbose, headers-only request; we keep just the TLS lines:
# the negotiated protocol/cipher, the certificate subject/issuer/expiry, and
# whether verification succeeded.
echo "--- Handshake summary (curl) ---"
curl -vI --max-time 15 "https://${host}" 2>&1 \
  | grep -Ei "SSL connection|subject:|issuer:|expire date|SSL certificate verify"
echo

echo "=== End of inspection ==="
metadata.yml (643 bytes)
lesson_id: D019
day: 19
kind: command-line-inspection
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-019-https-and-tls-encryption-on-the
run_commands:
  - bash examples/inspect_tls.sh
  - bash examples/inspect_tls.sh example.com
  - bash starter/inspect_tls.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/inspect_tls.sh  # optional: reset your work'
requires_network: true
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon, OpenSSL 3.6.3), bash tests/run_tests.sh → 10 checks, 0 failure(s), 0 skipped'
requirements/README.md (1045 bytes)
# Dependencies — Day 019 lab

**Two standard tools, both preinstalled on macOS and Linux:**

- `openssl` — the TLS/certificate toolkit. Ships with macOS (as LibreSSL) and
  every mainstream Linux distribution; Homebrew's `openssl@3` is a fine
  alternative. Used here only to *read* a public certificate.
- `curl` — the HTTP(S) client. Preinstalled on macOS and Linux; used with
  `-v` to show the TLS handshake.

**Network access is required.** Unlike most labs in this course, this one
opens a real HTTPS connection to a public host (`example.com` by default) to
read its certificate. It reads only the public certificate a server presents
to every visitor and sends no data of its own. With no network, every script
here degrades gracefully: it prints an "offline" message and exits cleanly.

There is deliberately no `requirements.txt` / `package.json`: the lab must
run on a stock machine with nothing installed.

## Windows

Use WSL (`wsl --install`, then follow the Linux path) or Git Bash, which
bundles both `openssl` and `curl`.
starter/inspect_tls.sh (2406 bytes)
#!/usr/bin/env bash
# Day 019 lab — inspect a TLS certificate yourself.
#
# This starter already handles the host argument and the offline guard and
# prints the report skeleton. Your job (README, "How to run") is to fill in
# the four exercises below, each of which names the exact command to use.
# The completed reference version is in examples/inspect_tls.sh — try to
# write yours first, then compare.
set -uo pipefail

host="${1:-example.com}"

echo "=== TLS inspection for ${host} ==="
echo "Generated on: $(date '+%Y-%m-%d')"
echo

# Offline guard — leave this as is. It exits cleanly if there is no network.
if ! curl -sI --max-time 15 "https://${host}" >/dev/null 2>&1; then
  echo "OFFLINE or host unreachable: could not open https://${host}"
  echo "Connect to the internet and re-run. (This lab requires network access.)"
  echo "=== End of inspection ==="
  exit 0
fi

echo "--- Certificate (openssl) ---"
# Exercise 1: print the certificate SUBJECT, ISSUER, and DATES.
#   Pipe an s_client connection into x509. Use exactly:
#   openssl s_client -connect "${host}:443" -servername "${host}" </dev/null 2>/dev/null \
#     | openssl x509 -noout -subject -issuer -dates
echo "REPLACE ME: exercise 1 (openssl subject/issuer/dates)"
echo

echo "--- Handshake summary (curl) ---"
# Exercise 2: show the negotiated TLS VERSION and the certificate summary.
#   Run a verbose headers-only request and keep the TLS lines. Use exactly:
#   curl -vI --max-time 15 "https://${host}" 2>&1 \
#     | grep -Ei "SSL connection|subject:|issuer:|expire date|SSL certificate verify"
echo "REPLACE ME: exercise 2 (curl handshake summary)"
echo

echo "--- Just the expiry date (openssl) ---"
# Exercise 3: print ONLY the not-after (expiry) date of the certificate.
#   Use the -enddate flag. Use exactly:
#   openssl s_client -connect "${host}:443" -servername "${host}" </dev/null 2>/dev/null \
#     | openssl x509 -noout -enddate
echo "REPLACE ME: exercise 3 (openssl expiry date only)"
echo

echo "--- The certificate chain (openssl) ---"
# Exercise 4: show the chain of subjects (s:) and issuers (i:) the server sends.
#   Add -showcerts and keep the s: / i: lines. Use exactly:
#   openssl s_client -connect "${host}:443" -servername "${host}" -showcerts </dev/null 2>/dev/null \
#     | grep -E "s:|i:"
echo "REPLACE ME: exercise 4 (openssl -showcerts chain)"
echo

echo "=== End of inspection ==="
starter/tls-worksheet.md (1875 bytes)
# TLS certificate worksheet

Fill this in for **two** different public HTTPS sites of your choice, using
the commands from the Day 19 lab and lesson. Replace the bracketed blanks.

## Commands you will use

```bash
# Subject, issuer, and validity dates:
openssl s_client -connect HOST:443 -servername HOST </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

# Negotiated TLS version (read the "SSL connection using ..." line):
curl -vI https://HOST 2>&1 | grep -Ei "SSL connection|verify"
```

## Site 1

- Host (domain): `[ e.g. example.com ]`
- Certificate **subject** (the domain the cert is for): `[ ______ ]`
- Certificate **issuer** (the CA that signed it): `[ ______ ]`
- **notBefore** date: `[ ______ ]`
- **notAfter** (expiry) date: `[ ______ ]`
- Currently valid? (is today between the two dates?): `[ yes / no ]`
- Negotiated **TLS version** (from curl): `[ e.g. TLSv1.3 ]`
- Certificate verification result: `[ SSL certificate verify ok / error ]`

## Site 2

- Host (domain): `[ ______ ]`
- Certificate **subject**: `[ ______ ]`
- Certificate **issuer**: `[ ______ ]`
- **notBefore** date: `[ ______ ]`
- **notAfter** (expiry) date: `[ ______ ]`
- Currently valid?: `[ yes / no ]`
- Negotiated **TLS version**: `[ ______ ]`
- Certificate verification result: `[ ______ ]`

## Short written answer (4–6 sentences)

Pick one of your two sites. Explain what would happen to a visitor if that
certificate's `notAfter` date passed with no renewal:

- Which of TLS's three protections (confidentiality, integrity,
  authentication) would still technically function, and which trust decision
  breaks?
- What warning would the visitor see, and why does the browser refuse to
  proceed silently?
- Why is "just click through the warning" dangerous advice — what real attack
  does that warning exist to catch?

`[ Write your paragraph here. ]`
tests/run_tests.sh (2963 bytes)
#!/usr/bin/env bash
# Tests for the Day 019 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Structural checks always run. The certificate checks require network:
# if the host is unreachable, those checks are SKIPPED (not failed) with a
# clear message. The script exits 0 when nothing genuinely failed, so it is
# safe to run offline and in CI.
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
host="example.com"
failures=0
checks=0
skipped=0

check() {
  local label="$1" ok="$2"
  checks=$((checks + 1))
  if [ "${ok}" = "yes" ]; then
    echo "  ok: ${label}"
  else
    echo "  FAIL: ${label}"
    failures=$((failures + 1))
  fi
}

skip() {
  echo "  SKIP: $1"
  skipped=$((skipped + 1))
}

echo "Structural checks ..."
# Required tools present.
command -v openssl >/dev/null 2>&1 && check "openssl is installed" "yes" || check "openssl is installed" "no"
command -v curl >/dev/null 2>&1 && check "curl is installed" "yes" || check "curl is installed" "no"
# Required files present.
[ -f "${lab_dir}/examples/inspect_tls.sh" ] && check "examples/inspect_tls.sh exists" "yes" || check "examples/inspect_tls.sh exists" "no"
[ -f "${lab_dir}/starter/inspect_tls.sh" ] && check "starter/inspect_tls.sh exists" "yes" || check "starter/inspect_tls.sh exists" "no"
[ -f "${lab_dir}/starter/tls-worksheet.md" ] && check "starter/tls-worksheet.md exists" "yes" || check "starter/tls-worksheet.md exists" "no"
# The example script is valid bash (parses without executing).
if bash -n "${lab_dir}/examples/inspect_tls.sh" 2>/dev/null; then
  check "examples/inspect_tls.sh is valid bash" "yes"
else
  check "examples/inspect_tls.sh is valid bash" "no"
fi

echo
echo "Network-dependent checks ..."
if ! curl -sI --max-time 15 "https://${host}" >/dev/null 2>&1; then
  skip "no network / ${host} unreachable — certificate checks skipped"
  skip "run again with an internet connection to exercise the full lab"
else
  cert="$(openssl s_client -connect "${host}:443" -servername "${host}" </dev/null 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates 2>/dev/null)"
  echo "${cert}" | grep -qi "subject=" && check "openssl returns a certificate subject" "yes" || check "openssl returns a certificate subject" "no"
  echo "${cert}" | grep -qi "issuer=" && check "openssl returns a certificate issuer" "yes" || check "openssl returns a certificate issuer" "no"
  echo "${cert}" | grep -qi "notAfter=" && check "openssl returns an expiry (notAfter) date" "yes" || check "openssl returns an expiry (notAfter) date" "no"
  # The reference script runs end to end and prints its footer.
  if bash "${lab_dir}/examples/inspect_tls.sh" "${host}" 2>&1 | grep -q "=== End of inspection ==="; then
    check "reference script runs and prints its footer" "yes"
  else
    check "reference script runs and prints its footer" "no"
  fi
fi

echo
echo "${checks} checks, ${failures} failure(s), ${skipped} skipped."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 019 lab

openssl s_client seems to hang and never returns

s_client opens the connection and then waits for you to type an HTTP request, so it looks frozen. The fix is the </dev/null at the end of the command, which feeds it empty input so it disconnects immediately. Every command in this lab already includes it — make sure you did not drop it.

openssl s_client syntax varies by version

macOS has shipped LibreSSL as /usr/bin/openssl, while Homebrew installs OpenSSL 3.x, and their flags differ slightly. The flags used here (-connect, -servername, -showcerts, and x509 -noout -subject -issuer -dates -enddate) work on both current builds. If your build rejects a flag, run openssl version to see what you have and openssl x509 -help / openssl s_client -help for the exact spelling it expects.

command not found: openssl or curl

Both are normally preinstalled. On a minimal Linux container, install them with your package manager (for example apt install openssl curl). On Windows, use WSL or Git Bash, both of which include them.

Empty output / unable to load certificate

The connection failed before a certificate arrived — usually no network, a captive-portal Wi-Fi login, a proxy, or a firewall. First confirm plain connectivity with curl -I https://example.com; if that fails too, the problem is the network, not the lab. The scripts detect this and print an OFFLINE or host unreachable message.

curl prints no TLS lines after the grep

Your curl build may word the verbose lines slightly differently, so the grep filter misses them. Drop the pipe and read the whole handshake: curl -vI https://example.com. Look for the TLS handshake, SSL connection using ..., and Server certificate: lines.

The certificate expiry is only a few weeks away

That is normal. Modern certificates are deliberately short-lived — Let's Encrypt issues 90-day certificates — and are renewed automatically. A near expiry is only a problem when nobody automated the renewal.

I get a certificate error against a site I chose

That is a real finding, not a bug in the lab. It usually means an expired certificate, a missing intermediate in the chain, a clock skew on your machine, or a self-signed certificate. Note which site and which message — diagnosing it is exactly the skill this lesson builds.

Security notes

Security notes — Day 019 lab

  • What the scripts do: open a TLS connection to a public HTTPS host and read the certificate that server presents to every visitor, then print its subject, issuer, dates, and the negotiated TLS version. This is read-only inspection of public information.
  • What they do not do: they send no request body, no credentials, and no personal data; they log in nowhere; they write no files outside their own console output (and the one capture you choose to redirect). There are no private keys involved anywhere in this lab — you only ever read the server's public certificate.
  • Network: the lab requires an outbound HTTPS connection (port 443) to the host you inspect. It makes no other connections. On an offline machine the scripts detect the lack of network and exit cleanly.
  • Privileges: everything runs as your normal user. Nothing here needs sudo; if any tutorial ever tells you to sudo a script you have not read, stop and read it first.
  • Choosing a host: inspecting a public site's certificate is a normal, unremarkable act — it is the same data your browser reads on every visit. Prefer well-known public sites; avoid probing hosts you do not have permission to interact with, as a courtesy and to keep your intent clear.
  • A real caution the lesson teaches: never disable certificate verification (for example curl -k / --insecure) to "make an error go away." That silently reopens the man-in-the-middle hole TLS exists to close. This lab never uses those flags, and neither should your real code.