diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d815a81b..c8b756ad5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,7 +55,7 @@ jobs: - name: Update build environment run: sudo apt-get update --fix-missing -y - name: Install prerequisites - run: sudo apt-get install -y avahi-daemon libavahi-client-dev libgnutls28-dev libpam-dev libusb-1.0-0-dev zlib1g-dev + run: sudo apt-get install -y avahi-daemon libavahi-client-dev libgnutls28-dev python3 openssl libpam-dev libusb-1.0-0-dev zlib1g-dev - name: Configure CUPS env: CC: /usr/bin/gcc @@ -65,6 +65,8 @@ jobs: run: make - name: Test CUPS run: make test + - name: Test GnuTLS SSLOptions + run: python3 test/testssloptions.py - name: Upload Test Results uses: actions/upload-artifact@v7 if: ${{ !cancelled() }} diff --git a/CHANGES.md b/CHANGES.md index a86893ce3..41d553933 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ CHANGES - OpenPrinting CUPS v2.5b1 - YYYY-MM-DD ------------------- +- Fixed ignored GnuTLS `SSLOptions` when no system priority is configured + (Issue #1677). - Added multiple language support for IPP Everywhere. - Added `cupsConcatString`, `cupsCopyString`, and `cupsFormatString` string APIs. diff --git a/cups/tls-gnutls.c b/cups/tls-gnutls.c index 5fa9011a4..09d440a0e 100644 --- a/cups/tls-gnutls.c +++ b/cups/tls-gnutls.c @@ -1927,10 +1927,26 @@ _httpTLSStart(http_t *http) // I - Connection to server return (false); } - if (tls_options & _HTTP_TLS_NO_SYSTEM) - priority_string[0] = '\0'; - else - cupsCopyString(priority_string, "@SYSTEM,", sizeof(priority_string)); + priority_string[0] = '\0'; + + if (!(tls_options & _HTTP_TLS_NO_SYSTEM)) + { + // Named system priorities are not configured on every system; only use + // them when available so the options below are not discarded... +#ifdef HAVE_GNUTLS_PRIORITY_SET_DIRECT + if (!gnutls_priority_set_direct(http->tls, "@SYSTEM", NULL)) + cupsCopyString(priority_string, "@SYSTEM,", sizeof(priority_string)); + +#else + gnutls_priority_t system_priority; // System priority + + if (!gnutls_priority_init(&system_priority, "@SYSTEM", NULL)) + { + gnutls_priority_deinit(system_priority); + cupsCopyString(priority_string, "@SYSTEM,", sizeof(priority_string)); + } +#endif // HAVE_GNUTLS_PRIORITY_SET_DIRECT + } cupsConcatString(priority_string, "NORMAL", sizeof(priority_string)); @@ -1971,16 +1987,34 @@ _httpTLSStart(http_t *http) // I - Connection to server cupsConcatString(priority_string, ":!AES-128-CBC:!AES-256-CBC:!CAMELLIA-128-CBC:!CAMELLIA-256-CBC:!3DES-CBC", sizeof(priority_string)); #ifdef HAVE_GNUTLS_PRIORITY_SET_DIRECT - gnutls_priority_set_direct(http->tls, priority_string, NULL); + status = gnutls_priority_set_direct(http->tls, priority_string, NULL); #else gnutls_priority_t priority; // Priority - gnutls_priority_init(&priority, priority_string, NULL); - gnutls_priority_set(http->tls, priority); - gnutls_priority_deinit(priority); + status = gnutls_priority_init(&priority, priority_string, NULL); + if (!status) + { + status = gnutls_priority_set(http->tls, priority); + gnutls_priority_deinit(priority); + } #endif // HAVE_GNUTLS_PRIORITY_SET_DIRECT + if (status) + { + http->error = EIO; + http->status = HTTP_STATUS_ERROR; + + DEBUG_printf("4_httpTLSStart: Unable to set TLS priorities: %s", gnutls_strerror(status)); + _cupsSetError(IPP_STATUS_ERROR_CUPS_PKI, gnutls_strerror(status), 0); + + gnutls_deinit(http->tls); + _httpFreeCredentials(credentials); + http->tls = NULL; + + return (false); + } + gnutls_transport_set_ptr(http->tls, (gnutls_transport_ptr_t)http); gnutls_transport_set_pull_function(http->tls, gnutls_http_read); #ifdef HAVE_GNUTLS_TRANSPORT_SET_PULL_TIMEOUT_FUNCTION diff --git a/test/testssloptions.py b/test/testssloptions.py new file mode 100644 index 000000000..441b5233b --- /dev/null +++ b/test/testssloptions.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# +# GnuTLS SSLOptions integration tests for CUPS. +# Copyright (c) 2026 by OpenPrinting. +# Licensed under Apache License v2.0. See LICENSE for details. +# +# Run after building with --with-tls=gnutls: python3 test/testssloptions.py +# Requires a C compiler, Python 3 with TLS 1.3 support, and the openssl command. +# All certificates, configuration, and connections are local to this test. + +import os +import shlex +import socket +import ssl +import subprocess +import tempfile +import threading +from pathlib import Path + +root = Path(__file__).resolve().parent.parent +if "#define HAVE_GNUTLS 1" not in (root / "config.h").read_text(): + raise SystemExit("Configure CUPS with --with-tls=gnutls before running this test.") +failures = 0 +with tempfile.TemporaryDirectory(prefix="cups-ssloptions-") as d: + p = Path(d) + (p / "client.c").write_text(r""" +#include "cups/cups.h" +#include +#include + +int +main(int argc, char *argv[]) +{ + char security[1024]; /* Negotiated TLS settings */ + http_t *http; /* Connection to the test server */ + + if (argc != 2) + return (2); + + http = httpConnect2("localhost", atoi(argv[1]), NULL, AF_INET, + HTTP_ENCRYPTION_ALWAYS, 1, 2000, NULL); + if (!http) + { + fprintf(stderr, "%s\n", cupsGetErrorString()); + return (1); + } + + puts(httpGetSecurity(http, security, sizeof(security))); + httpClose(http); + return (0); +} +""") + subprocess.run( + shlex.split(os.environ.get("CC", "cc")) + + [ + "-I" + str(root), + str(p / "client.c"), + "-L" + str(root / "cups"), + "-Wl,-rpath," + str(root / "cups"), + "-lcups", + "-o", + str(p / "client"), + ], + check=True, + ) + subprocess.run( + [ + os.environ.get("OPENSSL", "openssl"), + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + str(p / "key.pem"), + "-out", + str(p / "cert.pem"), + "-days", + "1", + "-subj", + "/CN=localhost", + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + cases = [ + ( + "missing SYSTEM honors MaxTLS1.2", + "", + "MinTLS1.2 MaxTLS1.2", + ssl.TLSVersion.TLSv1_2, + ssl.TLSVersion.TLSv1_3, + True, + "TLS/1.2", + ), + ( + "missing SYSTEM rejects TLS1.3-only server", + "", + "MinTLS1.2 MaxTLS1.2", + ssl.TLSVersion.TLSv1_3, + ssl.TLSVersion.TLSv1_3, + False, + None, + ), + ( + "missing SYSTEM honors MinTLS1.3", + "", + "MinTLS1.3", + ssl.TLSVersion.TLSv1_2, + ssl.TLSVersion.TLSv1_2, + False, + None, + ), + ( + "NoSystem remains functional", + "", + "NoSystem MinTLS1.2 MaxTLS1.2", + ssl.TLSVersion.TLSv1_2, + ssl.TLSVersion.TLSv1_3, + True, + "TLS/1.2", + ), + ( + "configured SYSTEM remains functional", + "[priorities]\nSYSTEM = NORMAL\n", + "MinTLS1.2 MaxTLS1.2", + ssl.TLSVersion.TLSv1_2, + ssl.TLSVersion.TLSv1_3, + True, + "TLS/1.2", + ), + ( + "configured SYSTEM keeps cipher restriction", + "[priorities]\nSYSTEM = NORMAL:-AES-128-GCM\n", + "MinTLS1.2 MaxTLS1.2", + ssl.TLSVersion.TLSv1_2, + ssl.TLSVersion.TLSv1_2, + False, + "GCM", + ), + ( + "NoSystem bypasses named cipher restriction", + "[priorities]\nSYSTEM = NORMAL:-AES-128-GCM\n", + "NoSystem MinTLS1.2 MaxTLS1.2", + ssl.TLSVersion.TLSv1_2, + ssl.TLSVersion.TLSv1_2, + True, + "GCM", + ), + ( + "missing SYSTEM honors DenyCBC", + "", + "MinTLS1.2 MaxTLS1.2 DenyCBC", + ssl.TLSVersion.TLSv1_2, + ssl.TLSVersion.TLSv1_2, + False, + "CBC", + ), + ] + for name, policy, options, minimum, maximum, success, expected in cases: + (p / "gnutls.conf").write_text(policy) + (p / "client.conf").write_text("SSLOptions " + options + "\n") + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.minimum_version = minimum + ctx.maximum_version = maximum + ctx.load_cert_chain(p / "cert.pem", p / "key.pem") + if expected == "CBC": + ctx.set_ciphers("ECDHE-RSA-AES128-SHA256") + if expected == "GCM": + ctx.set_ciphers("ECDHE-RSA-AES128-GCM-SHA256") + listener = socket.socket() + listener.bind(("127.0.0.1", 0)) + listener.listen() + listener.settimeout(5) + port = listener.getsockname()[1] + + def serve(listener, ctx): + try: + conn, _ = listener.accept() + with conn: + conn.settimeout(4) + with ctx.wrap_socket(conn, server_side=True) as tls: + tls.recv(1) + except (ssl.SSLError, OSError): + pass + finally: + listener.close() + + thread = threading.Thread(target=serve, args=(listener, ctx)) + thread.start() + env = dict( + os.environ, + GNUTLS_SYSTEM_PRIORITY_FILE=str(p / "gnutls.conf"), + CUPS_SYSCONFIG=d, + CUPS_USERCONFIG=d, + ) + r = subprocess.run( + [str(p / "client"), str(port)], + env=env, + check=False, + capture_output=True, + text=True, + timeout=8, + ) + thread.join(6) + if success: + passed = r.returncode == 0 and expected in r.stdout + else: + passed = r.returncode == 1 and "TLS" in r.stderr + failures += not passed + print( + ("PASS" if passed else "FAIL"), + name, + "=>", + (r.stdout + r.stderr).strip(), + flush=True, + ) +print("Failures:", failures) +raise SystemExit(bool(failures))