diff --git a/mail_imap_ssl_partial_chain.t b/mail_imap_ssl_partial_chain.t new file mode 100644 index 00000000..42f68462 --- /dev/null +++ b/mail_imap_ssl_partial_chain.t @@ -0,0 +1,272 @@ +#!/usr/bin/perl + +# (C) Nginx, Inc. + +# Tests for nginx mail imap module with ssl_verify_partial_chain. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use MIME::Base64; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::IMAP; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +local $SIG{PIPE} = 'IGNORE'; + +my $t = Test::Nginx->new() + ->has(qw/mail mail_ssl imap http rewrite socket_ssl_sslversion/) + ->has_daemon('openssl')->plan(12) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +mail { + proxy_pass_error_message on; + proxy_timeout 15s; + auth_http http://127.0.0.1:18083/mail/auth; + auth_http_pass_client_cert on; + + ssl_certificate_key localhost.key; + ssl_certificate localhost.crt; + + server { + listen 127.0.0.1:18080 ssl; + protocol imap; + + ssl_verify_client on; + ssl_verify_partial_chain on; + ssl_client_certificate int.crt; + } + + server { + listen 127.0.0.1:18081 ssl; + protocol imap; + + ssl_verify_client on; + ssl_client_certificate int.crt; + } + + server { + listen 127.0.0.1:18082 ssl; + protocol imap; + + ssl_verify_client on; + ssl_verify_partial_chain on; + ssl_trusted_certificate int.crt; + } + + server { + listen 127.0.0.1:18085 ssl; + protocol imap; + + ssl_verify_client optional; + ssl_verify_partial_chain on; + ssl_client_certificate int.crt; + } +} + +http { + %%TEST_GLOBALS_HTTP%% + + log_format test '$http_auth_ssl_verify:$http_auth_ssl_subject:' + '$http_auth_pass'; + + server { + listen 127.0.0.1:18083; + server_name localhost; + + location = /mail/auth { + access_log auth.log test; + + add_header Auth-Status OK; + add_header Auth-Server 127.0.0.1; + add_header Auth-Port 18084; + add_header Auth-Wait 1; + return 204; + } + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +$t->write_file('ca.conf', <>$d/openssl.out 2>&1") == 0 + or die "openssl command failed: $_[0]\n"; +} + +openssl("openssl req -x509 -new " + . "-config $d/openssl.conf -subj /CN=localhost/ " + . "-out $d/localhost.crt -keyout $d/localhost.key"); + +openssl("openssl req -x509 -new " + . "-config $d/openssl.conf -subj /CN=root/ " + . "-out $d/root.crt -keyout $d/root.key"); + +foreach my $name ('int', 'end') { + openssl("openssl req -new " + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.csr -keyout $d/$name.key"); +} + +openssl("openssl req -x509 -new " + . "-config $d/openssl.conf -subj /CN=other/ " + . "-out $d/other.crt -keyout $d/other.key"); + +$t->write_file('certserial', '1000'); +$t->write_file('certindex', ''); + +openssl("openssl ca -batch -config $d/ca.conf " + . "-keyfile $d/root.key -cert $d/root.crt " + . "-extensions myca_extensions " + . "-subj /CN=int/ -in $d/int.csr -out $d/int.crt"); + +openssl("openssl ca -batch -config $d/ca.conf " + . "-keyfile $d/int.key -cert $d/int.crt " + . "-extensions client_extensions " + . "-subj /CN=end/ -in $d/end.csr -out $d/end.crt"); + +$t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon, 18084); +$t->run()->waitforsocket('127.0.0.1:18084'); + +############################################################################### + +my $cred = sub { encode_base64("\0test\@example.com\0$_[0]", '') }; + +my $s = Test::Nginx::IMAP->new(PeerAddr => '127.0.0.1:18080', SSL => 1); +$s->check(qr/BYE No required SSL certificate/, 'partial_chain - no cert'); + +$s = Test::Nginx::IMAP->new( + PeerAddr => '127.0.0.1:18080', + SSL => 1, + SSL_cert_file => "$d/end.crt", + SSL_key_file => "$d/end.key" +); +$s->ok('partial_chain - leaf cert accepted'); +$s->send('1 AUTHENTICATE PLAIN ' . $cred->("p1")); + +$s = Test::Nginx::IMAP->new( + PeerAddr => '127.0.0.1:18080', + SSL => 1, + SSL_cert_file => "$d/other.crt", + SSL_key_file => "$d/other.key" +); +$s->check(qr/BYE SSL certificate error/, 'partial_chain - unknown cert rejected'); + +$s = Test::Nginx::IMAP->new( + PeerAddr => '127.0.0.1:18081', + SSL => 1, + SSL_cert_file => "$d/end.crt", + SSL_key_file => "$d/end.key" +); +$s->check(qr/BYE SSL certificate error/, + 'on \(control\) - leaf cert rejected without root CA'); + +$s = Test::Nginx::IMAP->new( + PeerAddr => '127.0.0.1:18082', + SSL => 1, + SSL_cert_file => "$d/end.crt", + SSL_key_file => "$d/end.key" +); +$s->ok('partial_chain trusted_certificate - leaf cert accepted'); +$s->send('1 AUTHENTICATE PLAIN ' . $cred->("p2")); + +$s = Test::Nginx::IMAP->new(PeerAddr => '127.0.0.1:18082', SSL => 1); +$s->check(qr/BYE No required SSL certificate/, + 'partial_chain trusted_certificate - no cert'); + +$s = Test::Nginx::IMAP->new( + PeerAddr => '127.0.0.1:18082', + SSL => 1, + SSL_cert_file => "$d/other.crt", + SSL_key_file => "$d/other.key" +); +$s->check(qr/BYE SSL certificate error/, + 'partial_chain trusted_certificate - unknown cert rejected'); + +$s = Test::Nginx::IMAP->new(PeerAddr => '127.0.0.1:18085', SSL => 1); +$s->ok('partial_chain optional - no cert'); +$s->send('1 AUTHENTICATE PLAIN ' . $cred->("p3")); + +$s = Test::Nginx::IMAP->new( + PeerAddr => '127.0.0.1:18085', + SSL => 1, + SSL_cert_file => "$d/end.crt", + SSL_key_file => "$d/end.key" +); +$s->ok('partial_chain optional - leaf cert accepted'); +$s->send('1 AUTHENTICATE PLAIN ' . $cred->("p4")); + +$s = Test::Nginx::IMAP->new( + PeerAddr => '127.0.0.1:18085', + SSL => 1, + SSL_cert_file => "$d/other.crt", + SSL_key_file => "$d/other.key" +); +$s->check(qr/BYE SSL certificate error/, + 'partial_chain optional - unknown cert rejected'); + +$t->stop(); + +my $f = $t->read_file('auth.log'); + +like($f, qr!^SUCCESS:(/?CN=end):p1$!m, + 'partial_chain auth log - leaf cert via client_certificate'); +like($f, qr!^SUCCESS:(/?CN=end):p2$!m, + 'partial_chain auth log - leaf cert via trusted_certificate'); + +############################################################################### diff --git a/ssl_verify_client_partial_chain.t b/ssl_verify_client_partial_chain.t new file mode 100644 index 00000000..b8a866ec --- /dev/null +++ b/ssl_verify_client_partial_chain.t @@ -0,0 +1,239 @@ +#!/usr/bin/perl + +# (C) Nginx, Inc. + +# Tests for http ssl module, ssl_verify_partial_chain. +# +# Validates that ssl_verify_partial_chain accepts a client certificate whose +# chain terminates at a trusted intermediate CA, without requiring the full +# chain of trust up to a root CA. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx qw/ :DEFAULT /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_ssl socket_ssl/) + ->has_daemon('openssl')->plan(10); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + ssl_certificate localhost.crt; + ssl_certificate_key localhost.key; + + add_header X-Verify $ssl_client_verify always; + + # partial_chain: accepts leaf cert signed by trusted intermediate, + # root CA is not in the bundle and not required + server { + listen 127.0.0.1:8080 ssl; + server_name localhost; + + ssl_verify_client on; + ssl_verify_partial_chain on; + ssl_client_certificate int.crt; + } + + # control: same intermediate-only bundle but regular "on" mode; + # leaf cert must fail because root CA is absent from the store + server { + listen 127.0.0.1:8081 ssl; + server_name localhost; + + ssl_verify_client on; + ssl_client_certificate int.crt; + } + + # partial_chain via ssl_trusted_certificate (not ssl_client_certificate) + server { + listen 127.0.0.1:8082 ssl; + server_name localhost; + + ssl_verify_client on; + ssl_verify_partial_chain on; + ssl_trusted_certificate int.crt; + } + + server { + listen 127.0.0.1:8083 ssl; + server_name localhost; + + ssl_verify_client optional; + ssl_verify_partial_chain on; + ssl_client_certificate int.crt; + } +} + +EOF + +my $d = $t->testdir(); + +$t->write_file('openssl.conf', <write_file('ca.conf', <>$d/openssl.out 2>&1") == 0 + or die "Can't create localhost certificate: $!\n"; + +# root CA (self-signed) - intentionally NOT added to nginx trust store +system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=root/ " + . "-out $d/root.crt -keyout $d/root.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create root certificate: $!\n"; + +# intermediate CA and leaf CSRs +foreach my $name ('int', 'end') { + system('openssl req -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.csr -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create CSR for $name: $!\n"; +} + +# unrelated self-signed cert (not in any chain) +system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=other/ " + . "-out $d/other.crt -keyout $d/other.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create other certificate: $!\n"; + +$t->write_file('certserial', '1000'); +$t->write_file('certindex', ''); + +# intermediate CA signed by root +system("openssl ca -batch -config $d/ca.conf " + . "-keyfile $d/root.key -cert $d/root.crt " + . "-extensions myca_extensions " + . "-subj /CN=int/ -in $d/int.csr -out $d/int.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign intermediate certificate: $!\n"; + +# leaf cert signed by intermediate (has clientAuth extension) +system("openssl ca -batch -config $d/ca.conf " + . "-keyfile $d/int.key -cert $d/int.crt " + . "-extensions client_extensions " + . "-subj /CN=end/ -in $d/end.csr -out $d/end.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign end certificate: $!\n"; + +$t->write_file('t', ''); + +$t->run(); + +############################################################################### + +# --- partial_chain via ssl_client_certificate --- + +# no cert: partial_chain with verify_client on requires a cert +like(get(8080), qr/400 Bad Request/, 'partial_chain - no cert'); + +# leaf cert signed by trusted intermediate: should be accepted +like(get(8080, 'end'), qr/SUCCESS/, 'partial_chain - leaf cert accepted'); + +# unrelated self-signed cert: should be rejected +like(get(8080, 'other'), qr/400 Bad Request/, 'partial_chain - unknown cert rejected'); + +# --- control: regular "on" with same intermediate-only bundle --- + +# leaf cert fails with regular "on" because root CA is not trusted +like(get(8081, 'end'), qr/400 Bad Request/, + 'on (control) - leaf cert rejected without root CA'); + +# --- partial_chain via ssl_trusted_certificate --- + +# leaf cert signed by trusted intermediate: should be accepted +like(get(8082, 'end'), qr/SUCCESS/, + 'partial_chain trusted_certificate - leaf cert accepted'); + +# no cert: partial_chain with verify_client on requires a cert +like(get(8082), qr/400 Bad Request/, + 'partial_chain trusted_certificate - no cert'); + +# unrelated cert: should be rejected +like(get(8082, 'other'), qr/400 Bad Request/, + 'partial_chain trusted_certificate - unknown cert rejected'); + +# --- partial_chain with optional client verification --- + +like(get(8083), qr/NONE/, 'partial_chain optional - no cert'); +like(get(8083, 'end'), qr/SUCCESS/, + 'partial_chain optional - leaf cert accepted'); +like(get(8083, 'other'), qr/400 Bad Request/, + 'partial_chain optional - unknown cert rejected'); + +############################################################################### + +sub get { + my ($port, $cert) = @_; + http_get( + '/t', + PeerAddr => '127.0.0.1:' . port($port), + SSL => 1, + $cert ? ( + SSL_cert_file => "$d/$cert.crt", + SSL_key_file => "$d/$cert.key", + ) : () + ); +} + +############################################################################### diff --git a/stream_ssl_verify_client_partial_chain.t b/stream_ssl_verify_client_partial_chain.t new file mode 100644 index 00000000..c32049b0 --- /dev/null +++ b/stream_ssl_verify_client_partial_chain.t @@ -0,0 +1,224 @@ +#!/usr/bin/perl + +# (C) Nginx, Inc. + +# Tests for stream ssl module, ssl_verify_partial_chain. +# +# Validates that ssl_verify_partial_chain accepts a client certificate whose +# chain terminates at a trusted intermediate CA, without requiring the full +# chain of trust up to a root CA. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_ssl stream_return socket_ssl/) + ->has_daemon('openssl')->plan(9); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + ssl_certificate localhost.crt; + ssl_certificate_key localhost.key; + + # partial_chain: accepts leaf cert signed by trusted intermediate + server { + listen 127.0.0.1:8080 ssl; + return $ssl_client_verify:$ssl_client_s_dn; + + ssl_verify_client on; + ssl_verify_partial_chain on; + ssl_client_certificate int.crt; + } + + # control: same intermediate-only bundle, regular "on" mode + server { + listen 127.0.0.1:8081 ssl; + return $ssl_client_verify:$ssl_client_s_dn; + + ssl_verify_client on; + ssl_client_certificate int.crt; + } + + # partial_chain via ssl_trusted_certificate + server { + listen 127.0.0.1:8082 ssl; + return $ssl_client_verify:$ssl_client_s_dn; + + ssl_verify_client on; + ssl_verify_partial_chain on; + ssl_trusted_certificate int.crt; + } + + server { + listen 127.0.0.1:8083 ssl; + return $ssl_client_verify:$ssl_client_s_dn; + + ssl_verify_client optional; + ssl_verify_partial_chain on; + ssl_client_certificate int.crt; + } +} + +EOF + +my $d = $t->testdir(); + +$t->write_file('openssl.conf', <write_file('ca.conf', <>$d/openssl.out 2>&1") == 0 + or die "Can't create localhost certificate: $!\n"; + +# root CA (not trusted by nginx) +system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=root/ " + . "-out $d/root.crt -keyout $d/root.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create root certificate: $!\n"; + +foreach my $name ('int', 'end') { + system('openssl req -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.csr -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create CSR for $name: $!\n"; +} + +# unrelated cert +system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=other/ " + . "-out $d/other.crt -keyout $d/other.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create other certificate: $!\n"; + +$t->write_file('certserial', '1000'); +$t->write_file('certindex', ''); + +system("openssl ca -batch -config $d/ca.conf " + . "-keyfile $d/root.key -cert $d/root.crt " + . "-extensions myca_extensions " + . "-subj /CN=int/ -in $d/int.csr -out $d/int.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign intermediate certificate: $!\n"; + +system("openssl ca -batch -config $d/ca.conf " + . "-keyfile $d/int.key -cert $d/int.crt " + . "-extensions client_extensions " + . "-subj /CN=end/ -in $d/end.csr -out $d/end.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign end certificate: $!\n"; + +$t->run(); + +############################################################################### + +# --- partial_chain via ssl_client_certificate --- + +# no cert: connection is dropped (cert required with verify_client on) +is(get(8080), '', 'partial_chain - no cert'); + +# leaf cert signed by trusted intermediate: accepted +like(get(8080, 'end'), qr/SUCCESS/, 'partial_chain - leaf cert accepted'); + +# unrelated cert: connection is dropped +is(get(8080, 'other'), '', 'partial_chain - unknown cert rejected'); + +# --- control: regular "on" with same intermediate-only bundle --- + +# leaf cert rejected because root CA is absent from the store +is(get(8081, 'end'), '', 'on (control) - leaf cert rejected without root CA'); + +# --- partial_chain via ssl_trusted_certificate --- + +# leaf cert accepted +like(get(8082, 'end'), qr/SUCCESS/, + 'partial_chain trusted_certificate - leaf cert accepted'); + +# unrelated cert: connection is dropped +is(get(8082, 'other'), '', + 'partial_chain trusted_certificate - unknown cert rejected'); + +# --- partial_chain with optional client verification --- + +like(get(8083), qr/NONE:/, 'partial_chain optional - no cert'); +like(get(8083, 'end'), qr/SUCCESS/, 'partial_chain optional - leaf cert accepted'); +is(get(8083, 'other'), '', 'partial_chain optional - unknown cert rejected'); + +############################################################################### + +sub get { + my ($port, $cert) = @_; + + my $s = stream( + PeerAddr => '127.0.0.1:' . port($port), + SSL => 1, + $cert ? ( + SSL_cert_file => "$d/$cert.crt", + SSL_key_file => "$d/$cert.key", + ) : () + ); + + return $s->read(); +} + +###############################################################################