Skip to content

nqtlnew(3)

nqtlnew, nqtlct, nqtlhs, nqtlwr, nqtlrd, nqtlend, nqtlwhy, nqtlfree — a TLS 1.3 client over the caller's transport

libnoqtis, -lnoqtis

#include <noqtis.h>

typedef struct {
  int (*wr)(void *user, const uint8_t *b, size_t n);
  int (*rd)(void *user, uint8_t *b, size_t cap, size_t *got);
  void *user;
} nqtlio;

int
nqtlnew(nqtls **t, const char *host, const uint8_t *ca, size_t calen, const nqtlio *io);

int
nqtlct(nqtls *t, const uint8_t *json, size_t len);

int
nqtlhs(nqtls *t);

int
nqtlwr(nqtls *t, const uint8_t *b, size_t n);

int
nqtlrd(nqtls *t, uint8_t *b, size_t cap, size_t *got);

int
nqtlend(nqtls *t);

const char *
nqtlwhy(const nqtls *t);

void
nqtlfree(nqtls *t);

These functions are a TLS 1.3 client (RFC 8446), so that a program can fetch over HTTPS with noqtis alone. They exist only when noqtis was built with the CMake option NQTLS; otherwise every one returns NQENOSUP and () says so. The library opens no connection: the caller connects a socket and passes two callbacks. wr sends all n bytes and returns NQOK, or NQEIO. rd reads 1 to cap bytes, sets *got, and returns NQOK (with *got 0 at the end of the stream) or NQEIO. Deadlines are the callbacks' job.

() makes a client for host: a DNS name (ASCII letters, digits and hyphens, at most 253 bytes; one trailing dot is dropped), sent as the server name and matched against the certificate, or an IPv4 or IPv6 address literal, matched against the certificate's addresses and not sent. ca is the trust anchors: a PEM bundle (every BEGIN CERTIFICATE block; a block noqtis cannot read is left out) or one DER certificate, calen bytes, copied.

() runs the handshake: one ClientHello offering the groups X25519MLKEM768 (the ML-KEM-768 hybrid of draft-ietf-tls-ecdhe-mlkem) and X25519 with a key share for each, then secp256r1 and secp384r1 (P-256 and P-384) without one, the suites TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384 and TLS_CHACHA20_POLY1305_SHA256, and the signature schemes ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, rsa_pss_rsae_sha256/384/512 and, for certificates only, rsa_pkcs1; at most one HelloRetryRequest (with a fresh key share for the group it asks for); the server's certificate path to one of the anchors at the current time (names, signatures, validity, basicConstraints, keyUsage, the serverAuth purpose, name constraints on DNS names, addresses, directory names and mail addresses), the host name (subjectAltName only; a wildcard only as the whole leftmost label), the CertificateVerify signature and the Finished MAC. A CertificateRequest is answered with an empty Certificate.

The ClientHello asks for OCSP stapling (status_request, RFC 6066). An OCSP response the server staples to its certificate (RFC 8446 section 4.4.2.1) must be about that certificate, signed by its issuer or by a responder the issuer named, and current; a revoked certificate ends the handshake with the alert certificate_revoked, a response that does not check with bad_certificate_status_response. No response is fine, except when the certificate carries the TLS Feature extension with status_request (must-staple, RFC 7633).

(), called before nqtlhs(), turns on Certificate Transparency (RFC 6962). json is a log list of len bytes, copied, in the form of Chrome's log_list.json, version 3: operators, each with logs, each with its log_id, key and state. The ClientHello then offers signed_certificate_timestamp, and the server's certificate must carry SCTs, embedded in it or sent in TLS, that come from logs of at least two operators. An SCT counts when its log is in the list and was qualified, usable or read-only (or retired, but after the SCT), the certificate's notAfter is in the log's temporal interval if it has one, the SCT is not from the future, and its signature (ECDSA P-256 or RSA, SHA-256) verifies over the certificate, or for an embedded SCT over the precertificate the log saw. Without nqtlct() no SCT is looked at: a log list goes stale, so the caller keeps it current.

() sends application data; () reads it, 1 to cap bytes at a time, or *got 0 once the server has sent close_notify. A stream that ends without close_notify is NQEIO: the data may have been cut short. Session tickets are read and dropped; a KeyUpdate is followed, and answered when the server asks. () sends close_notify.

Any error is final: the client sends the alert RFC 8446 names, records a sentence that () returns, and every later call returns the same code. () wipes every secret and frees the client; NULL is allowed.

NQOK, or:

NQEVRFY
a certificate, signature or MAC that does not check out
NQEDEC
a message TLS 1.3 does not allow at that point, a bundle with no certificate, or a log list that does not parse, holds no log, or a log whose log_id is not the hash of its key
NQENOSUP
a server that speaks nothing the client offers, or a build without NQTLS
NQEIO
the transport failed, the server ended the connection, or sent an alert
NQESTATE
a call out of order (data before the handshake, after close_notify; nqtlct() after nqtlhs())
NQEARG
a bad argument, or a host name the client will not send
NQELIM, NQEMEM
a message over its limit (256 KiB, ten certificates), or no memory

noqtis(3)

RFC 8446 (TLS 1.3), RFC 8448 (its example traces, which the tests reproduce byte for byte), draft-ietf-tls-ecdhe-mlkem, RFC 5280, RFC 6125, RFC 6066 and RFC 6960 (OCSP stapling), RFC 7633 (must-staple), RFC 6962 (Certificate Transparency).

Not done: CRLs and OCSP queries (only a stapled response is checked, and only for the server's own certificate), policy constraints (a certificate that carries them as a critical extension is refused), name constraints on other name forms (a certificate with a name of such a form below one is refused), SCTs in an OCSP response, the rule of Chrome's policy on the number of SCTs by certificate lifetime, finite-field groups (a server with none of the offered groups fails), Ed25519 and ML-DSA server keys, TLS 1.2, resumption, 0-RTT, client certificates.