Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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() }}
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
50 changes: 42 additions & 8 deletions cups/tls-gnutls.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it would be better to have the check for @System before we assign it into priority string, instead of handling it here, although it adds new set of HAVE_GNUTLS_PRIORITY_SET_DIRECT ifdef.

Just do not forget to initialize the string with NULL terminator in case of error, as I did :( .

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in db6e871: @SYSTEM is now probed before the priority string is built (with set_direct or priority_init/deinit depending on HAVE_GNUTLS_PRIORITY_SET_DIRECT), priority_string is initialized to an empty string first, and the retry after failure is removed. test/testssloptions.py passes all 8 cases with GnuTLS 3.7.3 on both the set_direct and the priority_init code paths.


#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
Expand Down
220 changes: 220 additions & 0 deletions test/testssloptions.py
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
#include <stdlib.h>

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(
[
"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))