NAME
HTTPS::Handy - A tiny HTTPS/1.0 server with TLS written in pure Perl
VERSION
Version 1.01
SYNOPSIS
use HTTPS::Handy;
my $app = sub {
my ($env) = @_;
return [200,
['Content-Type', 'text/html; charset=utf-8'],
['<h1>Hello, HTTPS World!</h1>']];
};
# Starts on https://0.0.0.0:8443/ with a self-signed certificate
# that the module generates by itself, in Perl, in about a second.
HTTPS::Handy->run(app => $app);
# With your own certificate
HTTPS::Handy->run(
app => $app,
port => 8443,
ssl_cert_file => 'server-cert.pem',
ssl_key_file => 'server-key.pem',
);
TABLE OF CONTENTS
"HOW TLS IS IMPLEMENTED" -- the packages inside this file
"PSGI SUBSET SPECIFICATION" --
$envkeys, response format, psgi.input"SERVER STARTUP" --
run(), access log"CERTIFICATES" -- how the certificate and key are found or made
"METHODS" --
serve_static,url_decode,parse_query,mime_type,is_htmx, and theresponse_*builders"DIAGNOSTICS" -- error messages and runtime warnings
DESCRIPTION
The shortest way in
perl lib/HTTPS/Handy.pm
That starts a demonstration server. The first run spends about a second making a key and a certificate, then prints the address to open. The browser will warn that the certificate vouches for itself; say to continue, and the page appears. Nothing had to be installed, and no other program ran.
What it is
HTTPS::Handy is a small HTTPS/1.0 server for teaching, for reading, and for local development. It speaks a subset of PSGI, so an application is a single code reference that takes $env and returns a three element array reference.
What makes this version unusual is that the TLS layer is not borrowed from anywhere. The whole of it -- big integer arithmetic, the P-256 elliptic curve, SHA-256, HMAC, the TLS pseudo random function, ChaCha20-Poly1305, DER and PEM encoding, X.509 certificate generation, the record layer and the handshake -- is written in Perl inside this one file. The distribution contains no binary file, no XS code, and no compiled component of any kind; the module loads nothing outside the Perl core and runs no external program.
Two things follow from that, and they are the reason the module exists.
The first is that an ordinary browser opens the pages it serves. The cipher suites here are the ones Chrome, Firefox, Safari and Edge still accept: ECDHE for key agreement, ChaCha20-Poly1305 for the data, and an ECDSA certificate. A lesson can therefore start with a real https:// URL in a real browser rather than with a command line tool.
The second is that every step from the first byte of a ClientHello to the HTML the browser displays can be read, printed out, and traced with a print statement. Nothing is hidden behind a library.
If no certificate is supplied, the module generates an elliptic curve key and a self-signed certificate itself, in about a second, and caches them on disk. There is nothing to install and nothing to prepare.
REQUIREMENTS
Perl : 5.005_03 or later
Modules : Core only (IO::Socket, POSIX, Carp)
Platform : Windows, UNIX/Linux, and anywhere else Perl runs
External : Nothing. No OpenSSL, no certbot, no compiler.
Clients : Current browsers, curl, and openssl s_client
DIFFERENCES FROM HTTP::HANDY
Transport is HTTPS instead of HTTP.
psgi.url_schemeis"https"instead of"http".psgi.sslis set to1in the PSGI environment.Default port is 8443 instead of 8080.
A certificate and a private key are needed, and are generated automatically when they are not supplied.
The $env keys, the response format, and every utility method are the same in both modules, so an application written for one runs unchanged on the other.
SUPPORTED PROTOCOL
HTTPS/1.0 only (no Keep-Alive)
Methods: GET and POST only
Connection is closed immediately after each response
TLS 1.2 only
Key agreement: ECDHE on the P-256 curve, which gives forward secrecy
Cipher suites:
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256(0xCCA9) with a generated certificate, andTLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256(0xCCA8) when the supplied certificate holds an RSA keyCertificates are signed with SHA-256
Session resumption by session id, so that only the first connection of a session pays for the public key arithmetic
One certificate per server. The name a client asks for through the Server Name Indication extension is not looked at.
These are the suites current browsers still accept, which is the point: https://localhost:8443/ opens in Chrome, Firefox, Safari and Edge, as well as in curl and openssl s_client, with no special options on either side. A self-signed certificate still produces the usual warning page, where the visitor chooses to continue.
HOW TLS IS IMPLEMENTED
Where to start reading
The file holds eight packages, laid out in the order they are meant to be read: each one uses the one below it, so reading downwards is reading from the protocol towards the arithmetic.
Package Lines What it does
----------------------- ------- --------------------------------------
HTTPS::Handy 923 the web server, and the demo at the end
HTTPS::Handy::Input 109 an in memory handle for psgi.input
HTTPS::Handy::TLS 896 the record layer and the handshake
HTTPS::Handy::EC 332 the P-256 curve: ECDH and ECDSA
HTTPS::Handy::ChaCha 211 ChaCha20-Poly1305, the record cipher
HTTPS::Handy::Crypt 187 SHA-256, HMAC, the PRF, randomness
HTTPS::Handy::X509 447 DER, PEM, self-signed certificates
HTTPS::Handy::RSA 77 signing with a certificate from elsewhere
HTTPS::Handy::BigInt 465 multiple precision integers
Comments and blank lines are counted too; about a third of each figure is prose. A test in the distribution compares this table with the file, so it cannot quietly go out of date.
The core is one subroutine: HTTPS::Handy::TLS::server_handshake. It is about a hundred and fifty lines and it contains the whole protocol. Everything else in the file is a part it calls. A reader in a hurry should read that one subroutine, then follow whichever part looks interesting.
What a handshake does
client -> ClientHello which versions and ciphers it knows
server -> ServerHello the version and cipher it picked
server -> Certificate the server public key, signed
server -> ServerKeyExchange a one time curve point, signed
server -> ServerHelloDone
client -> ClientKeyExchange its own one time curve point
client -> ChangeCipherSpec everything after this is encrypted
client -> Finished a hash of the whole handshake
server -> ChangeCipherSpec
server -> Finished
Each side multiplies its own secret number by the other side's point. The two results are equal, neither side ever sends it, and nobody watching can work it out. That shared value becomes the pre-master secret; the PRF expands it into two keys and two nonces; and every record after that is sealed with ChaCha20-Poly1305.
Because each side throws its secret number away when the connection ends, traffic recorded today cannot be decrypted later even if the server key is stolen afterwards. That property is called forward secrecy.
Why these algorithms
Every choice here was made twice: once for what browsers accept, and once for what fits in a file a student can read.
ECDHE on P-256 is the only key agreement current browsers offer that this module could implement in a few hundred lines. It also brings forward secrecy, which RSA key transport does not.
ChaCha20-Poly1305 is the fastest AEAD to write in pure Perl: ChaCha20 is nothing but 32 bit addition, exclusive or and rotation, and Poly1305 is one multiplication modulo 2**130 - 5 per 16 bytes, which the big integer package already provides. AES-GCM would need an S-box, a key schedule and multiplication in GF(2**128), and would run slower.
An ECDSA certificate can be generated in about a second. An RSA key of comparable strength takes minutes of pure Perl, because it has to search for primes.
PSGI SUBSET SPECIFICATION
PSGI is the agreed shape of a Perl web application: a subroutine that takes one hash of request information and returns one array of response information. Writing to that shape means the same application runs under this module, under HTTP::Handy, and under a full server such as Plack, unchanged. Only the part of PSGI described here is implemented.
Application Interface
A HTTPS::Handy application is a plain code reference that receives a request environment hash and returns a three-element response arrayref:
my $app = sub {
my ($env) = @_;
return [$status, \@headers, \@body];
};
Request Environment -- $env
The following keys are provided in the environment hashref passed to the app:
Key Description
------------------ ----------------------------------------------
REQUEST_METHOD "GET" or "POST"
PATH_INFO URL path (e.g. "/index.html")
QUERY_STRING Query string ("key=val&..."), without leading "?"
SERVER_NAME Server hostname
SERVER_PORT Port number (integer)
CONTENT_TYPE Content-Type header of POST request
CONTENT_LENGTH Content-Length of POST body (integer)
HTTP_* Request headers, uppercased, hyphens as underscores
psgi.input Object with read() for the POST body (see below)
psgi.errors \*STDERR
psgi.url_scheme Always "https"
psgi.ssl Always 1 (indicates TLS is active)
psgix.tls_cipher Name of the cipher suite this connection uses
psgix.tls_resumed 1 if the handshake was resumed, 0 if it was full
psgi.input Object
The psgi.input value is a HTTPS::Handy::Input object. It provides:
$env->{'psgi.input'}->read($buf, $length) # read up to $length bytes
$env->{'psgi.input'}->read($buf, $len, $off) # read with offset
$env->{'psgi.input'}->seek($pos, $whence) # reposition
$env->{'psgi.input'}->tell() # current position
$env->{'psgi.input'}->getline() # read one line
$env->{'psgi.input'}->getlines() # read all lines
This object works on Perl 5.5.3, which does not support open my $fh, '<', \$scalar.
The two psgix. Keys
psgix. is the conventional prefix for a key that a particular PSGI server adds, and these two are here to be looked at rather than acted on. psgix.tls_cipher is the name of the cipher suite in use. psgix.tls_resumed is 0 on a connection that did the full handshake and 1 on one that resumed a remembered session.
The demo page at /info prints the whole environment, so reloading it shows psgix.tls_resumed turning from 0 into 1 as the session cache starts doing its work. That is the fastest way to see, rather than read about, what session resumption is for.
Response Format
The application must return an arrayref of exactly three elements:
[$status_code, \@headers, \@body]
$status_code-
An integer HTTP status code (e.g. 200, 404, 500).
\@headers-
A flat arrayref of header name/value pairs, alternating:
['Content-Type', 'text/html', 'X-Custom', 'value'] \@body-
An arrayref of strings. All elements are joined and sent as the response body.
['<html>', '<body>Hello</body>', '</html>']
Example:
return [200,
['Content-Type', 'text/html; charset=utf-8'],
['<h1>Hello HTTPS::Handy</h1>']];
SERVER STARTUP
run(%args)
Starts the HTTPS server. This call blocks indefinitely (until the process is killed). In order, it
finds a certificate and a private key, generating them if it must;
listens on a plain TCP socket;
for each connection in turn: runs the TLS handshake, reads one HTTP request, calls the application, writes the response, and closes the connection.
One connection is handled at a time. The next client waits.
HTTPS::Handy->run(
app => $app, # required: PSGI app code reference
host => '127.0.0.1', # optional: bind address (default: 0.0.0.0)
port => 8443, # optional: port number (default: 8443)
log => 1, # optional: messages to STDERR (default: 1)
max_post_size => 10485760, # optional: max POST bytes (default: 10MB)
# TLS certificate options
ssl_cert_file => 'server-cert.pem', # optional
ssl_key_file => 'server-key.pem', # optional
domains => 'example.com', # optional
cert_dir => '.https_handy_certs', # optional
);
log-
When true, which it is by default, this module writes to STDERR: the two lines it prints at startup, one access log line per request, and a line for each certificate step, failed handshake or application error. When false it writes nothing at all, and an application that still wants to record something can write to
psgi.errorsitself. ssl_cert_file/ssl_key_file-
Paths to an existing TLS certificate and private key, in PEM form. When both are given, no certificate is generated. The key may be an elliptic curve key on the P-256 curve (
BEGIN EC PRIVATE KEY) or an RSA key (BEGIN RSA PRIVATE KEY), and either may arrive in the PKCS #8 wrapper that saysBEGIN PRIVATE KEY. An encrypted key is not supported. The certificate file may hold a chain, and every certificate in it is sent. domains-
Host name, or an arrayref of host names, that this server answers for. If a Let's Encrypt certificate for the first name is already installed below /etc/letsencrypt/live/, it is used. Otherwise the names go into the self-signed certificate that is generated instead.
cert_dir-
Directory for the generated certificate and key. Default: .https_handy_certs
Access Log Format (LTSV)
When log is enabled, each request is written to STDERR as a single LTSV (Labeled Tab-separated Values) line:
time:2026-01-01T12:00:00\tmethod:GET\tpath:/index.html\tstatus:200\tsize:1234\tua:Mozilla/5.0\treferer:
Fields:
time ISO 8601 local timestamp (YYYY-MM-DDTHH:MM:SS)
method HTTP method (GET or POST)
path Request path (PATH_INFO, without query string)
status HTTP status code
size Response body size in bytes
ua User-Agent header value (empty string if absent)
referer Referer header value (empty string if absent)
LTSV can be parsed line by line with split /\t/ and each field with split /:/, $field, 2. It is directly compatible with LTSV::LINQ.
CERTIFICATES
How the certificate is chosen
ssl_cert_fileandssl_key_file, when both are given.An existing Let's Encrypt certificate for the first name in
domains, when one is installed on this machine.A self-signed certificate generated by this module and cached in
cert_diras selfsigned-cert.pem and selfsigned-key.pem. Later runs find those files and start at once.
The generated certificate
The certificate is a version 3 X.509 certificate holding a P-256 public key, signed with ECDSA and SHA-256 by the matching private key. It is valid for 397 days, because browsers refuse a certificate whose lifetime is much longer than that. It carries the host name as the common name and as a subjectAltName, together with localhost and 127.0.0.1, so that a browser matches it for local use.
Browsers still warn about it, because nothing vouches for it but itself. In Chrome the warning is ERR_CERT_AUTHORITY_INVALID and the way past it is "Advanced", then "Proceed"; Firefox and Safari have the same door under different labels. That warning is the only obstacle between a class and a working https:// page.
Using a certificate from elsewhere
Certificates made by other tools work, as long as the key is a P-256 elliptic curve key or an RSA key, and the file is not encrypted. For example:
openssl ecparam -genkey -name prime256v1 -out server-key.pem
openssl req -x509 -new -key server-key.pem -days 397 \
-subj /CN=localhost -out server-cert.pem
The same files are then passed to ssl_cert_file and ssl_key_file. Obtaining a certificate from a certificate authority over ACME is not implemented here; it needs a client of its own.
METHODS
Everything below is called as a class method, with an arrow and the module name in front:
HTTPS::Handy->response_text('hello');
They are helpers for writing the application, and none of them touch the network. run(), which does, is described under "SERVER STARTUP" above.
serve_static($env, $docroot [, %opts])
Serve a static file from $docroot using PATH_INFO as the file path. Returns a complete PSGI response arrayref.
my $res = HTTPS::Handy->serve_static($env, './htdocs');
# With cache control (e.g. for htmx apps: cache JS/CSS, never cache HTML)
my $res = HTTPS::Handy->serve_static($env, './htdocs', cache_max_age => 3600);
Options:
cache_max_age-
Sets the
Cache-Controlheader.cache_max_age => 3600 # Cache-Control: public, max-age=3600 cache_max_age => 0 # Cache-Control: no-cache (not specified) # Cache-Control: no-cache (default)For htmx applications, setting a positive
cache_max_agefor static assets (CSS, JS, images) while leaving HTML fragments at the defaultno-cacheprevents stale scripts from being reused after a partial page update.
Behaviour:
MIME type is detected automatically from the file extension
Supported types: html, htm, txt, css, js, json, xml, png, jpg, jpeg, gif, ico, svg, pdf, zip, gz, ltsv, csv, tsv
Directory access attempts to serve
index.htmlReturns 404 if the file does not exist
Returns 403 if the file cannot be opened
Path traversal (
..) is blocked with a 403 response
url_decode($str)
Decode a percent-encoded URL string. + is decoded as a space.
my $str = HTTPS::Handy->url_decode('hello+world%21');
# returns: "hello world!"
parse_query($query_string)
Parse a URL query string into a hash. When the same key appears more than once, its value becomes an arrayref.
my %p = HTTPS::Handy->parse_query('name=ina&tag=perl&tag=cpan');
# $p{name} eq 'ina'
# $p{tag} is ['perl', 'cpan']
mime_type($ext)
Return the MIME type string for a given file extension. The leading dot is optional.
HTTPS::Handy->mime_type('html'); # 'text/html; charset=utf-8'
HTTPS::Handy->mime_type('.json'); # 'application/json'
HTTPS::Handy->mime_type('xyz'); # 'application/octet-stream'
is_htmx($env)
Returns 1 if the request was made by htmx (i.e. the HX-Request: true header is present), or 0 otherwise.
if (HTTPS::Handy->is_htmx($env)) { ... }
response_redirect($location, [$code])
Build a redirect response. Default status is 302.
HTTPS::Handy->response_redirect('/new/path');
HTTPS::Handy->response_redirect('/new/path', 301);
response_json($json_str, [$code])
Build a JSON response. The caller must provide already-encoded JSON.
HTTPS::Handy->response_json('{"ok":true}');
HTTPS::Handy->response_json('{"error":"bad"}', 400);
response_html($html, [$code])
Build an HTML response.
HTTPS::Handy->response_html('<h1>Hello</h1>');
response_text($text, [$code])
Build a plain text response.
HTTPS::Handy->response_text('Hello, World!');
SECURITY
Do not use this module to protect anything of value. It is written to be read, and a cryptographic implementation that is easy to read is not the same thing as one that is safe to rely on. Use it on a private network, on a development machine, or in a classroom, and put a mature server such as nginx or Apache in front of anything that matters.
The rest of this section says exactly what is weak about it, because a list of specific weaknesses teaches more than a general warning does.
The implementation is not hardened. Comparisons, the authentication tag check and the curve arithmetic are written for clarity, so they take different amounts of time for different inputs. An attacker who can measure those times on the same network can learn things a constant time implementation would hide.
Randomness depends on the platform. Keys, nonces and the per signature value k come from /dev/urandom when it can be read. Where it cannot -- on Windows, for instance -- the module falls back to hashing the process id, the clock and
rand, which is far weaker. For ECDSA that is worse than it sounds: two signatures made with the same k give away the private key. Supply your own certificate on such systems if the key matters.Self-signed certificates make browsers warn. That warning is correct: nothing vouches for the certificate. It is fine for local work and wrong for the public internet.
Certificates are not verified by the built-in client. The client side exists for the test suite, and it accepts whatever certificate it is shown. It is not a general purpose TLS client.
One process, one connection at a time. A slow or hostile client blocks every other client while it is connected.
The session cache is in memory and not shared. It holds at most sixty-four sessions for an hour each, and it disappears when the process does.
On the other hand, the protocol itself is the current one: TLS 1.2 with forward secrecy and an authenticated cipher. A record that is altered in transit is refused rather than decrypted, and traffic recorded today stays unreadable even if the server key is taken tomorrow.
PERFORMANCE
All cryptography here runs in Perl. On a current machine:
Generating the key and the self-signed certificate takes about a second, once, and the result is cached in
cert_dir.A full handshake costs three elliptic curve multiplications, which is between one and two seconds.
A resumed handshake costs none of them and is effectively instant. Since a browser opens several connections and makes several requests per page, this is what makes a demonstration usable: the first connection is slow and the rest are not.
Bulk data goes through ChaCha20-Poly1305 at roughly two hundred kilobytes per second. Pages of a few kilobytes are comfortable; large downloads are not.
Because HTTP/1.0 closes the connection after every response, every request needs a new handshake, though a resumed one. Keeping demonstration pages self-contained, with no external images or scripts, makes them appear at once.
LIMITATIONS
Single-process, single-thread. Requests are handled one at a time. Not suitable for production or for high load.
One curve and one cipher. P-256 and ChaCha20-Poly1305 only. A client that offers neither cannot connect; every current browser offers both.
TLS 1.2 only. TLS 1.3 is a different handshake and is not implemented. Browsers fall back to 1.2 without complaint.
No ALPN, no renegotiation, no client certificates, no OCSP, no session tickets (session ids only).
No Server Name Indication. One certificate is served to every client, whatever host name was asked for.
The built-in client does not resume sessions and does not check certificates. It exists for the test suite.
No ACME client. Certificates from a certificate authority must be obtained by other means and passed in.
POST body fully buffered. The entire POST body is read into memory before the application runs.
No Keep-Alive. Every request closes the connection.
No cookie or session management (implement in the application layer).
Headers a client sends more than once keep only the last value.
None of this affects the application: it sees an ordinary PSGI environment and returns an ordinary PSGI response, with the encryption and the certificate entirely behind it.
DEMO
Run directly to start a self-contained demo server:
perl lib/HTTPS/Handy.pm # from the distribution directory
perl lib/HTTPS/Handy.pm 9443 # on port 9443
The first run generates a key and a self-signed certificate and stores them in .https_handy_certs, which takes about a second; later runs start at once. Then open https://localhost:8443/ (or the port you specified) in a browser, and click past the certificate warning. The demo provides three built-in pages:
/-
Top page with a GET query form and a POST form.
/echo-
Echoes GET query parameters or POST form fields in a table. Demonstrates
parse_queryfor both methods. /info-
Displays the full PSGI
$envhash for the current request, includingpsgi.url_schemeandpsgi.ssl.
DIAGNOSTICS
Startup errors
HTTPS::Handy->run: 'app' is required-
run()was called without anappargument. HTTPS::Handy->run: 'app' must be a code reference-
The
appargument was not a code reference. HTTPS::Handy->run: 'port' must be a number-
The
portargument contained something other than digits. HTTPS::Handy->run: 'max_post_size' must be a number-
The
max_post_sizeargument contained something other than digits. HTTPS::Handy: Cannot bind to <host>:<port> - <reason>-
The listening socket could not be created. The port may be in use, or below 1024 without the privilege to bind it.
HTTPS::Handy: Cannot read certificate file <file>: <reason>-
ssl_cert_filecould not be opened. HTTPS::Handy: No CERTIFICATE block found in <file>-
The certificate file contains no
-----BEGIN CERTIFICATE-----block. A DER file must be converted to PEM first. HTTPS::Handy: Cannot read private key file <file>: <reason>-
ssl_key_filecould not be opened. HTTPS::Handy: No PRIVATE KEY block found in <file>-
The key file contains none of
BEGIN EC PRIVATE KEY,BEGIN RSA PRIVATE KEYorBEGIN PRIVATE KEY. HTTPS::Handy: <file> is not a P-256 or RSA private key this module can read-
The key is encrypted, or it is on a curve other than P-256. Decrypt it, or make a key this module understands, with
openssl ec -in old-key.pem -out server-key.pem HTTPS::Handy: Cannot write <file>: <reason>-
The generated certificate or key could not be saved. Check that
cert_direxists and is writable.
Internal errors
These come from the cryptographic packages and indicate a bug or a damaged key file rather than a configuration mistake.
HTTPS::Handy::BigInt: division by zero-
A modulus of zero reached the arithmetic, which a valid key never does.
Runtime messages (STDERR)
These are written only when the log option is true, which is the default.
[TIMESTAMP] App error: MESSAGE-
The application died. A 500 response was sent.
[TIMESTAMP] Accept failed: MESSAGE-
acceptfailed. The server continues. [TIMESTAMP] TLS handshake failed: MESSAGE-
A client connected but the handshake did not finish. The connection is closed and the server continues. MESSAGE is one of:
not a TLS record (is the client speaking plain HTTP?)-
The first bytes were not a TLS record. Almost always an
http://URL aimed at the HTTPS port. client offered an older protocol than TLS 1.2-
The client is older than this module supports.
no cipher suite in common (this server speaks NAME over the P-256 curve)-
The client offered neither of the two suites here, or does not support the P-256 curve.
the ClientHello was too short to read-
The first handshake message was truncated or malformed.
the client sent a curve point this server cannot readthe client sent a point that is not on the P-256 curve-
The ClientKeyExchange did not hold a valid P-256 point. A correct client does not do this.
client Finished did not verify-
The two sides did not arrive at the same keys, or the handshake was tampered with in transit.
record failed its authentication tag-
An encrypted record was altered, reordered or replayed.
alert from peer: LEVEL/DESCRIPTION-
The client gave up and said why, in the numbers of RFC 5246 section 7.2. The commonest is
2/48, which means the client would not accept the certificate.
[TIMESTAMP] Generating a P-256 key and a self-signed certificate for HOST ...-
No cached certificate was found in
cert_dir, so one is being made. It takes about a second, and the next line appears when it is done.
BUGS AND LIMITATIONS
Please report any bugs or feature requests by e-mail to <ina.cpan@gmail.com>.
When reporting a bug, please include:
A minimal, self-contained test script that reproduces the problem.
The version of HTTPS::Handy:
perl -MHTTPS::Handy -e 'print HTTPS::Handy->VERSION, "\n"'Your Perl version:
perl -VYour operating system, and, for a handshake problem, the client you used and the output of
openssl s_client -connect localhost:8443 -tls1_2
See "LIMITATIONS" above for known design limitations.
SEE ALSO
Related Modules
HTTP::Handy -- the plain-HTTP sibling of this module, by the same author. HTTPS::Handy is HTTP::Handy plus TLS; the $env hash, response format, and all utility methods are identical.
IO::Socket::SSL -- the usual way to add TLS to a Perl socket, backed by OpenSSL. Faster, safer and far larger than the TLS layer here; the right choice whenever the goal is to protect something rather than to study the protocol.
Plack -- a full-featured PSGI toolkit and server collection. Requires Perl 5.8+. For production use or more demanding workloads, migrating from HTTPS::Handy to Plack (with a reverse proxy such as nginx terminating TLS) is straightforward.
Standards implemented here
RFC 5246 (TLS 1.2), RFC 8422 (elliptic curves in TLS), RFC 7905 (ChaCha20-Poly1305 in TLS), RFC 8439 (ChaCha20 and Poly1305), RFC 5746 (renegotiation indication, the empty extension only), RFC 8017 (PKCS #1 v1.5), RFC 5280 (X.509), SEC 1 (elliptic curve keys), RFC 2104 (HMAC), RFC 4648 (Base64), FIPS 180-4 (SHA-256), FIPS 186-4 (the P-256 curve and ECDSA).
AUTHOR
INABA Hitoshi <ina.cpan@gmail.com>
COPYRIGHT AND LICENSE
This software is free software; you can redistribute it and/or modify it under the same terms as Perl itself.