Commit fa1d46d1cc for openssl.org
commit fa1d46d1cc32dfe1b824d5617c48a2c92950db03
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date: Fri Aug 28 10:04:06 2026 +0200
apps: test ciphers app output modes and cipher selection
Add a new test recipe covering the ciphers app output: the non-verbose
colon-separated list including rejection of an unknown cipher string,
-v cipher descriptions, -V cipher code prefixes and -stdname standard
cipher names, including their combination and a check that AES128-SHA
maps to the TLS_RSA_WITH_AES_128_CBC_SHA standard name. Also check
that -psk makes PSK ciphers supported and that -ciphersuites configures
the TLSv1.3 ciphersuites and rejects an unknown one.
Assisted-by: Claude:claude-fable-5
Reviewed-by: Daniel Kubec <kubec@openssl.foundation>
Reviewed-by: Richard Levitte <levitte@openssl.org>
Merge-date: Tue Sep 15 15:17:09 2026
Merged-from: https://github.com/openssl/openssl/pull/32569
diff --git a/test/recipes/20-test_app_ciphers.t b/test/recipes/20-test_app_ciphers.t
new file mode 100644
index 0000000000..e605c9925d
--- /dev/null
+++ b/test/recipes/20-test_app_ciphers.t
@@ -0,0 +1,165 @@
+#! /usr/bin/env perl
+# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use strict;
+use warnings;
+
+use OpenSSL::Test qw/:DEFAULT with/;
+use OpenSSL::Test::Utils;
+
+setup("test_app_ciphers");
+
+plan skip_all => "The ciphers app is not available in a no-sock build"
+ if disabled("sock");
+plan skip_all => "No TLS protocols are supported by this OpenSSL build"
+ if alldisabled(available_protocols("tls"));
+
+plan tests => 7;
+
+my $base_status;
+my @base = run(app(["openssl", "ciphers"]),
+ capture => 1, statusvar => \$base_status);
+my @names = map { s|\R||; split(/:/, $_) } @base;
+
+subtest "ciphers lists cipher names in non-verbose mode" => sub {
+ plan tests => 5;
+
+ ok($base_status, "ciphers with no options runs successfully");
+ is(scalar @base, 1, "the whole list is printed on a single line");
+ ok(@names > 0, "the list is not empty");
+ is(join(" ", grep { !/^[A-Za-z0-9_-]+$/ } @names), "",
+ "the list holds colon-separated cipher names");
+ with({ exit_checker => sub { return shift == 1; } },
+ sub {
+ ok(run(app(["openssl", "ciphers", "NOSUCHCIPHER"])),
+ "an unknown cipher string is rejected");
+ });
+};
+
+# Each verbose line is a SSL_CIPHER_description() of one listed cipher.
+my $desc = qr/\S+\s+Kx=\S+\s+Au=\S+\s+Enc=\S+\s+Mac=\S+/;
+
+subtest "ciphers -v describes each cipher" => sub {
+ plan tests => 3;
+
+ my $status;
+ my @out = run(app(["openssl", "ciphers", "-v"]),
+ capture => 1, statusvar => \$status);
+ chomp @out;
+ ok($status, "ciphers -v runs successfully");
+ is(scalar @out, scalar @names, "one description line per cipher");
+ my @got = map { /^(\S+)\s+$desc$/ ? $1 : "BAD LINE: $_" } @out;
+ is(join(":", @got), join(":", @names),
+ "each line holds a full description of the listed cipher");
+};
+
+subtest "ciphers -V prefixes descriptions with cipher codes" => sub {
+ plan tests => 3;
+
+ my $status;
+ my @out = run(app(["openssl", "ciphers", "-V"]),
+ capture => 1, statusvar => \$status);
+ chomp @out;
+ ok($status, "ciphers -V runs successfully");
+ is(scalar @out, scalar @names, "one description line per cipher");
+ my $code = qr/(?:0x[0-9A-F]{2},){1,3}0x[0-9A-F]{2}/;
+ my @got = map { /^\s*$code - (\S+)\s+$desc$/ ? $1 : "BAD LINE: $_" } @out;
+ is(join(":", @got), join(":", @names),
+ "each line holds the cipher code and a full description");
+};
+
+subtest "ciphers -stdname prefixes descriptions with standard names" => sub {
+ plan tests => 4;
+
+ my $status;
+ my @out = run(app(["openssl", "ciphers", "-stdname"]),
+ capture => 1, statusvar => \$status);
+ chomp @out;
+ ok($status, "ciphers -stdname runs successfully");
+ is(scalar @out, scalar @names, "one description line per cipher");
+ my @got = map { /^\S+\s+- (\S+)\s+$desc$/ ? $1 : "BAD LINE: $_" } @out;
+ is(join(":", @got), join(":", @names),
+ "each line holds the standard name and a full description");
+ my @badstd = grep { !/^(?:TLS_\S+|UNKNOWN)\s/ } @out;
+ is(join("\n", @badstd), "", "standard names are TLS_* or UNKNOWN");
+};
+
+subtest "ciphers -V -stdname combines codes and standard names" => sub {
+ plan tests => 3;
+
+ my $status;
+ my @out = run(app(["openssl", "ciphers", "-V", "-stdname"]),
+ capture => 1, statusvar => \$status);
+ chomp @out;
+ ok($status, "ciphers -V -stdname runs successfully");
+ my $code = qr/(?:0x[0-9A-F]{2},){1,3}0x[0-9A-F]{2}/;
+ my @got = map { /^\s*$code - \S+\s+- (\S+)\s+$desc$/ ? $1 : "BAD LINE: $_" }
+ @out;
+ is(join(":", @got), join(":", @names),
+ "each line holds the code, the standard name and a description");
+
+ SKIP: {
+ my @all = run(app(["openssl", "ciphers", "ALL"]), capture => 1);
+ skip "AES128-SHA is not available", 1
+ unless grep { /(?:^|:)AES128-SHA(?::|$)/ } map { s|\R||r } @all;
+
+ # The TLSv1.3 ciphersuites are always prepended to the list.
+ my @line = grep { / - AES128-SHA\s/ }
+ run(app(["openssl", "ciphers", "-stdname", "AES128-SHA"]),
+ capture => 1, statusvar => \$status);
+ ok($status && @line == 1
+ && $line[0] =~ /^TLS_RSA_WITH_AES_128_CBC_SHA\s+- AES128-SHA\s/,
+ "AES128-SHA maps to the TLS_RSA_WITH_AES_128_CBC_SHA standard name");
+ }
+};
+
+subtest "ciphers -psk includes PSK ciphers among the supported ones" => sub {
+ plan skip_all => "PSK is not supported by this OpenSSL build"
+ if disabled("psk");
+ plan skip_all => "TLSv1.2 is not supported by this OpenSSL build"
+ if disabled("tls1_2");
+
+ plan tests => 3;
+
+ my $status;
+ my @psk = grep { /PSK/ }
+ map { s|\R||; split(/:/, $_) }
+ run(app(["openssl", "ciphers", "-s", "-psk", "PSK"]),
+ capture => 1, statusvar => \$status);
+ ok($status, "ciphers -s -psk runs successfully");
+ ok(@psk > 0, "PSK ciphers are supported with -psk");
+
+ @psk = grep { /PSK/ }
+ map { s|\R||; split(/:/, $_) }
+ run(app(["openssl", "ciphers", "-s", "PSK"]),
+ capture => 1, statusvar => \$status);
+ ok($status && @psk == 0, "PSK ciphers are not supported without -psk");
+};
+
+subtest "ciphers -ciphersuites configures the TLSv1.3 ciphersuites" => sub {
+ plan skip_all => "TLSv1.3 is not supported by this OpenSSL build"
+ if disabled("tls1_3");
+
+ plan tests => 3;
+
+ my $status;
+ my @suites = grep { /^TLS_/ }
+ map { s|\R||; split(/:/, $_) }
+ run(app(["openssl", "ciphers", "-ciphersuites",
+ "TLS_AES_128_GCM_SHA256"]),
+ capture => 1, statusvar => \$status);
+ ok($status, "ciphers -ciphersuites runs successfully");
+ is(join(":", @suites), "TLS_AES_128_GCM_SHA256",
+ "only the configured TLSv1.3 ciphersuite is listed");
+ with({ exit_checker => sub { return shift == 1; } },
+ sub {
+ ok(run(app(["openssl", "ciphers", "-ciphersuites",
+ "NOSUCHSUITE"])),
+ "an unknown TLSv1.3 ciphersuite is rejected");
+ });
+};