1570
|
1 #!/usr/bin/perl
|
|
2
|
|
3 # (C) Sergey Kandaurov
|
|
4 # (C) Nginx, Inc.
|
|
5
|
|
6 # Tests for OCSP with client certificates.
|
|
7
|
|
8 ###############################################################################
|
|
9
|
|
10 use warnings;
|
|
11 use strict;
|
|
12
|
|
13 use Test::More;
|
|
14
|
|
15 use MIME::Base64 qw/ decode_base64 /;
|
|
16
|
|
17 BEGIN { use FindBin; chdir($FindBin::Bin); }
|
|
18
|
|
19 use lib 'lib';
|
|
20 use Test::Nginx;
|
|
21
|
|
22 ###############################################################################
|
|
23
|
|
24 select STDERR; $| = 1;
|
|
25 select STDOUT; $| = 1;
|
|
26
|
|
27 eval {
|
|
28 require Net::SSLeay;
|
|
29 Net::SSLeay::load_error_strings();
|
|
30 Net::SSLeay::SSLeay_add_ssl_algorithms();
|
|
31 Net::SSLeay::randomize();
|
|
32 Net::SSLeay::SSLeay();
|
|
33 defined &Net::SSLeay::set_tlsext_status_type or die;
|
|
34 };
|
|
35 plan(skip_all => 'Net::SSLeay not installed or too old') if $@;
|
|
36
|
|
37 eval {
|
|
38 my $ctx = Net::SSLeay::CTX_new() or die;
|
|
39 my $ssl = Net::SSLeay::new($ctx) or die;
|
|
40 Net::SSLeay::set_tlsext_host_name($ssl, 'example.org') == 1 or die;
|
|
41 };
|
|
42 plan(skip_all => 'Net::SSLeay with OpenSSL SNI support required') if $@;
|
|
43
|
|
44 my $t = Test::Nginx->new()->has(qw/http http_ssl sni/)->has_daemon('openssl');
|
|
45
|
|
46 plan(skip_all => 'no OCSP stapling') if $t->has_module('BoringSSL');
|
|
47
|
|
48 $t->write_file_expand('nginx.conf', <<'EOF');
|
|
49
|
|
50 %%TEST_GLOBALS%%
|
|
51
|
|
52 daemon off;
|
|
53
|
|
54 events {
|
|
55 }
|
|
56
|
|
57 http {
|
|
58 %%TEST_GLOBALS_HTTP%%
|
|
59
|
|
60 ssl_ocsp leaf;
|
|
61 ssl_verify_client on;
|
|
62 ssl_verify_depth 2;
|
|
63 ssl_client_certificate trusted.crt;
|
|
64
|
|
65 ssl_ciphers DEFAULT:ECCdraft;
|
|
66
|
|
67 ssl_certificate_key ec.key;
|
|
68 ssl_certificate ec.crt;
|
|
69
|
|
70 ssl_certificate_key rsa.key;
|
|
71 ssl_certificate rsa.crt;
|
|
72
|
|
73 ssl_session_cache shared:SSL:1m;
|
|
74 ssl_session_tickets off;
|
|
75
|
|
76 add_header X-Verify x${ssl_client_verify}:${ssl_session_reused}x always;
|
|
77
|
|
78 server {
|
|
79 listen 127.0.0.1:8443 ssl;
|
|
80 server_name localhost;
|
|
81 }
|
|
82
|
|
83 server {
|
|
84 listen 127.0.0.1:8443 ssl;
|
|
85 server_name sni;
|
|
86
|
|
87 ssl_ocsp_responder http://127.0.0.1:8082;
|
|
88 }
|
|
89
|
|
90 server {
|
|
91 listen 127.0.0.1:8444 ssl;
|
|
92 server_name localhost;
|
|
93
|
|
94 ssl_ocsp on;
|
|
95 }
|
|
96
|
|
97 server {
|
|
98 listen 127.0.0.1:8445 ssl;
|
|
99 server_name localhost;
|
|
100
|
|
101 ssl_ocsp_responder http://127.0.0.1:8082;
|
|
102 }
|
|
103
|
|
104 server {
|
|
105 listen 127.0.0.1:8446 ssl;
|
|
106 server_name localhost;
|
|
107
|
|
108 ssl_ocsp_cache shared:OCSP:1m;
|
|
109 }
|
|
110
|
|
111 server {
|
|
112 listen 127.0.0.1:8447 ssl;
|
|
113 server_name localhost;
|
|
114
|
|
115 ssl_ocsp_responder http://127.0.0.1:8082;
|
|
116 ssl_client_certificate root.crt;
|
|
117 }
|
|
118 }
|
|
119
|
|
120 EOF
|
|
121
|
|
122 my $d = $t->testdir();
|
|
123 my $p = port(8081);
|
|
124
|
|
125 $t->write_file('openssl.conf', <<EOF);
|
|
126 [ req ]
|
|
127 default_bits = 2048
|
|
128 encrypt_key = no
|
|
129 distinguished_name = req_distinguished_name
|
|
130 [ req_distinguished_name ]
|
|
131 EOF
|
|
132
|
|
133 $t->write_file('ca.conf', <<EOF);
|
|
134 [ ca ]
|
|
135 default_ca = myca
|
|
136
|
|
137 [ myca ]
|
|
138 new_certs_dir = $d
|
|
139 database = $d/certindex
|
|
140 default_md = sha256
|
|
141 policy = myca_policy
|
|
142 serial = $d/certserial
|
|
143 default_days = 1
|
|
144 x509_extensions = myca_extensions
|
|
145
|
|
146 [ myca_policy ]
|
|
147 commonName = supplied
|
|
148
|
|
149 [ myca_extensions ]
|
|
150 basicConstraints = critical,CA:TRUE
|
|
151 authorityInfoAccess = OCSP;URI:http://127.0.0.1:$p
|
|
152 EOF
|
|
153
|
|
154 foreach my $name ('root') {
|
|
155 system('openssl req -x509 -new '
|
|
156 . "-config $d/openssl.conf -subj /CN=$name/ "
|
|
157 . "-out $d/$name.crt -keyout $d/$name.key "
|
|
158 . ">>$d/openssl.out 2>&1") == 0
|
|
159 or die "Can't create certificate for $name: $!\n";
|
|
160 }
|
|
161
|
|
162 foreach my $name ('int', 'end') {
|
|
163 system("openssl req -new "
|
|
164 . "-config $d/openssl.conf -subj /CN=$name/ "
|
|
165 . "-out $d/$name.csr -keyout $d/$name.key "
|
|
166 . ">>$d/openssl.out 2>&1") == 0
|
|
167 or die "Can't create certificate for $name: $!\n";
|
|
168 }
|
|
169
|
|
170 foreach my $name ('ec-end') {
|
|
171 system("openssl ecparam -genkey -out $d/$name.key -name prime256v1 "
|
|
172 . ">>$d/openssl.out 2>&1") == 0
|
|
173 or die "Can't create EC param: $!\n";
|
|
174 system("openssl req -new -key $d/$name.key "
|
|
175 . "-config $d/openssl.conf -subj /CN=$name/ "
|
|
176 . "-out $d/$name.csr "
|
|
177 . ">>$d/openssl.out 2>&1") == 0
|
|
178 or die "Can't create certificate for $name: $!\n";
|
|
179 }
|
|
180
|
|
181 $t->write_file('certserial', '1000');
|
|
182 $t->write_file('certindex', '');
|
|
183
|
|
184 system("openssl ca -batch -config $d/ca.conf "
|
|
185 . "-keyfile $d/root.key -cert $d/root.crt "
|
|
186 . "-subj /CN=int/ -in $d/int.csr -out $d/int.crt "
|
|
187 . ">>$d/openssl.out 2>&1") == 0
|
|
188 or die "Can't sign certificate for int: $!\n";
|
|
189
|
|
190 system("openssl ca -batch -config $d/ca.conf "
|
|
191 . "-keyfile $d/int.key -cert $d/int.crt "
|
|
192 . "-subj /CN=ec-end/ -in $d/ec-end.csr -out $d/ec-end.crt "
|
|
193 . ">>$d/openssl.out 2>&1") == 0
|
|
194 or die "Can't sign certificate for ec-end: $!\n";
|
|
195
|
|
196 system("openssl ca -batch -config $d/ca.conf "
|
|
197 . "-keyfile $d/int.key -cert $d/int.crt "
|
|
198 . "-subj /CN=end/ -in $d/end.csr -out $d/end.crt "
|
|
199 . ">>$d/openssl.out 2>&1") == 0
|
|
200 or die "Can't sign certificate for end: $!\n";
|
|
201
|
|
202 # RFC 6960, serialNumber
|
|
203
|
|
204 system("openssl x509 -in $d/int.crt -serial -noout "
|
|
205 . ">>$d/serial_int 2>>$d/openssl.out") == 0
|
|
206 or die "Can't obtain serial for end: $!\n";
|
|
207
|
|
208 my $serial_int = pack("n2", 0x0202, hex $1)
|
|
209 if $t->read_file('serial_int') =~ /(\d+)/;
|
|
210
|
|
211 system("openssl x509 -in $d/end.crt -serial -noout "
|
|
212 . ">>$d/serial 2>>$d/openssl.out") == 0
|
|
213 or die "Can't obtain serial for end: $!\n";
|
|
214
|
|
215 my $serial = pack("n2", 0x0202, hex $1) if $t->read_file('serial') =~ /(\d+)/;
|
|
216
|
|
217 # ocsp end
|
|
218
|
|
219 system("openssl ocsp -issuer $d/int.crt -cert $d/end.crt "
|
|
220 . "-reqout $d/req.der >>$d/openssl.out 2>&1") == 0
|
|
221 or die "Can't create OCSP request: $!\n";
|
|
222
|
|
223 system("openssl ocsp -index $d/certindex -CA $d/int.crt "
|
|
224 . "-rsigner $d/int.crt -rkey $d/int.key "
|
|
225 . "-reqin $d/req.der -respout $d/resp.der -ndays 1 "
|
|
226 . ">>$d/openssl.out 2>&1") == 0
|
|
227 or die "Can't create OCSP response: $!\n";
|
|
228
|
|
229 system("openssl ocsp -issuer $d/int.crt -cert $d/ec-end.crt "
|
|
230 . "-reqout $d/ec-req.der >>$d/openssl.out 2>&1") == 0
|
|
231 or die "Can't create EC OCSP request: $!\n";
|
|
232
|
|
233 system("openssl ocsp -index $d/certindex -CA $d/int.crt "
|
|
234 . "-rsigner $d/root.crt -rkey $d/root.key "
|
|
235 . "-reqin $d/ec-req.der -respout $d/ec-resp.der -ndays 1 "
|
|
236 . ">>$d/openssl.out 2>&1") == 0
|
|
237 or die "Can't create EC OCSP response: $!\n";
|
|
238
|
|
239 $t->write_file('trusted.crt',
|
|
240 $t->read_file('int.crt') . $t->read_file('root.crt'));
|
|
241
|
|
242 # server cert/key
|
|
243
|
|
244 system("openssl ecparam -genkey -out $d/ec.key -name prime256v1 "
|
|
245 . ">>$d/openssl.out 2>&1") == 0 or die "Can't create EC pem: $!\n";
|
|
246 system("openssl genrsa -out $d/rsa.key 2048 >>$d/openssl.out 2>&1") == 0
|
|
247 or die "Can't create RSA pem: $!\n";
|
|
248
|
|
249 foreach my $name ('ec', 'rsa') {
|
|
250 system("openssl req -x509 -new -key $d/$name.key "
|
|
251 . "-config $d/openssl.conf -subj /CN=$name/ "
|
|
252 . "-out $d/$name.crt -keyout $d/$name.key "
|
|
253 . ">>$d/openssl.out 2>&1") == 0
|
|
254 or die "Can't create certificate for $name: $!\n";
|
|
255 }
|
|
256
|
|
257 $t->run_daemon(\&http_daemon, $t, port(8081));
|
|
258 $t->run_daemon(\&http_daemon, $t, port(8082));
|
|
259 $t->try_run('no ssl_ocsp')->plan(13);
|
|
260
|
|
261 $t->waitforsocket("127.0.0.1:" . port(8081));
|
|
262 $t->waitforsocket("127.0.0.1:" . port(8082));
|
|
263
|
|
264 my $version = get_version();
|
|
265
|
|
266 ###############################################################################
|
|
267
|
|
268 like(get('RSA', 'end'), qr/200 OK.*SUCCESS/s, 'ocsp leaf');
|
|
269
|
|
270 # demonstrate that ocsp int request is actually made by failing ocsp response
|
|
271
|
|
272 like(get('RSA', 'end', port => 8444),
|
|
273 qr/400 Bad.*FAILED:certificate status request failed/s,
|
|
274 'ocsp many failed');
|
|
275
|
|
276 # now prepare valid ocsp int response
|
|
277
|
|
278 system("openssl ocsp -issuer $d/root.crt -cert $d/int.crt "
|
|
279 . "-reqout $d/int-req.der >>$d/openssl.out 2>&1") == 0
|
|
280 or die "Can't create OCSP request: $!\n";
|
|
281
|
|
282 system("openssl ocsp -index $d/certindex -CA $d/root.crt "
|
|
283 . "-rsigner $d/root.crt -rkey $d/root.key "
|
|
284 . "-reqin $d/int-req.der -respout $d/int-resp.der -ndays 1 "
|
|
285 . ">>$d/openssl.out 2>&1") == 0
|
|
286 or die "Can't create OCSP response: $!\n";
|
|
287
|
|
288 like(get('RSA', 'end', port => 8444), qr/200 OK.*SUCCESS/s, 'ocsp many');
|
|
289
|
|
290 # store into ssl_ocsp_cache
|
|
291
|
|
292 like(get('RSA', 'end', port => 8446), qr/200 OK.*SUCCESS/s, 'cache store');
|
|
293
|
|
294 # revoke
|
|
295
|
|
296 system("openssl ca -config $d/ca.conf -revoke $d/end.crt "
|
|
297 . "-keyfile $d/root.key -cert $d/root.crt "
|
|
298 . ">>$d/openssl.out 2>&1") == 0
|
|
299 or die "Can't revoke end.crt: $!\n";
|
|
300
|
|
301 system("openssl ocsp -issuer $d/int.crt -cert $d/end.crt "
|
|
302 . "-reqout $d/req.der >>$d/openssl.out 2>&1") == 0
|
|
303 or die "Can't create OCSP request: $!\n";
|
|
304
|
|
305 system("openssl ocsp -index $d/certindex -CA $d/int.crt "
|
|
306 . "-rsigner $d/int.crt -rkey $d/int.key "
|
|
307 . "-reqin $d/req.der -respout $d/revoked.der -ndays 1 "
|
|
308 . ">>$d/openssl.out 2>&1") == 0
|
|
309 or die "Can't create OCSP response: $!\n";
|
|
310
|
|
311 like(get('RSA', 'end'), qr/400 Bad.*FAILED:certificate revoked/s, 'revoked');
|
|
312
|
|
313 # with different responder where it's still valid
|
|
314
|
|
315 like(get('RSA', 'end', port => 8445), qr/200 OK.*SUCCESS/s, 'ocsp responder');
|
|
316
|
|
317 # with different context to responder where it's still valid
|
|
318
|
|
319 like(get('RSA', 'end', sni => 'sni'), qr/200 OK.*SUCCESS/s, 'ocsp context');
|
|
320
|
|
321 # with cached ocsp response it's still valid
|
|
322
|
|
323 like(get('RSA', 'end', port => 8446), qr/200 OK.*SUCCESS/s, 'cache lookup');
|
|
324
|
|
325 # ocsp end response signed with invalid (root) cert, expect HTTP 400
|
|
326
|
|
327 like(get('ECDSA', 'ec-end'),
|
|
328 qr/400 Bad.*FAILED:certificate status request failed/s,
|
|
329 'root ca not trusted');
|
|
330
|
|
331 # now sign ocsp end response with valid int cert
|
|
332
|
|
333 system("openssl ocsp -index $d/certindex -CA $d/int.crt "
|
|
334 . "-rsigner $d/int.crt -rkey $d/int.key "
|
|
335 . "-reqin $d/ec-req.der -respout $d/ec-resp.der -ndays 1 "
|
|
336 . ">>$d/openssl.out 2>&1") == 0
|
|
337 or die "Can't create EC OCSP response: $!\n";
|
|
338
|
|
339 like(get('ECDSA', 'ec-end'), qr/200 OK.*SUCCESS/s, 'ocsp ecdsa');
|
|
340
|
|
341 my ($s, $ssl) = get('ECDSA', 'ec-end');
|
|
342 my $ses = Net::SSLeay::get_session($ssl);
|
|
343
|
|
344 like(get('ECDSA', 'ec-end', ses => $ses),
|
|
345 qr/200 OK.*SUCCESS:r/s, 'session reused');
|
|
346
|
|
347 # revoke with saved session
|
|
348
|
|
349 system("openssl ca -config $d/ca.conf -revoke $d/ec-end.crt "
|
|
350 . "-keyfile $d/root.key -cert $d/root.crt "
|
|
351 . ">>$d/openssl.out 2>&1") == 0
|
|
352 or die "Can't revoke end.crt: $!\n";
|
|
353
|
|
354 system("openssl ocsp -issuer $d/int.crt -cert $d/ec-end.crt "
|
|
355 . "-reqout $d/ec-req.der >>$d/openssl.out 2>&1") == 0
|
|
356 or die "Can't create OCSP request: $!\n";
|
|
357
|
|
358 system("openssl ocsp -index $d/certindex -CA $d/int.crt "
|
|
359 . "-rsigner $d/int.crt -rkey $d/int.key "
|
|
360 . "-reqin $d/ec-req.der -respout $d/ec-resp.der -ndays 1 "
|
|
361 . ">>$d/openssl.out 2>&1") == 0
|
|
362 or die "Can't create OCSP response: $!\n";
|
|
363
|
|
364 # reusing session with revoked certificate
|
|
365
|
|
366 like(get('ECDSA', 'ec-end', ses => $ses),
|
|
367 qr/400 Bad.*FAILED:certificate revoked:r/s, 'session reused - revoked');
|
|
368
|
|
369 # regression test for self-signed
|
|
370
|
|
371 like(get('RSA', 'root', port => 8447), qr/200 OK.*SUCCESS/s, 'ocsp one');
|
|
372
|
|
373 ###############################################################################
|
|
374
|
|
375 sub get {
|
|
376 my ($type, $cert, %extra) = @_;
|
|
377 $type = 'PSS' if $type eq 'RSA' && $version > 0x0303;
|
|
378 my ($s, $ssl) = get_ssl_socket($type, $cert, %extra);
|
|
379 my $cipher = Net::SSLeay::get_cipher($ssl);
|
|
380 Test::Nginx::log_core('||', "cipher: $cipher");
|
|
381 my $host = $extra{sni} ? $extra{sni} : 'localhost';
|
|
382 Net::SSLeay::write($ssl, "GET /serial HTTP/1.0\nHost: $host\n\n");
|
|
383 my $r = Net::SSLeay::read($ssl);
|
|
384 Test::Nginx::log_core($r);
|
|
385 $s->close();
|
|
386 return $r unless wantarray();
|
|
387 return ($s, $ssl);
|
|
388 }
|
|
389
|
|
390 sub get_ssl_socket {
|
|
391 my ($type, $cert, %extra) = @_;
|
|
392 my $ses = $extra{ses};
|
|
393 my $sni = $extra{sni};
|
|
394 my $port = $extra{port} || 8443;
|
|
395 my $s;
|
|
396
|
|
397 eval {
|
|
398 local $SIG{ALRM} = sub { die "timeout\n" };
|
|
399 local $SIG{PIPE} = sub { die "sigpipe\n" };
|
|
400 alarm(8);
|
|
401 $s = IO::Socket::INET->new('127.0.0.1:' . port($port));
|
|
402 alarm(0);
|
|
403 };
|
|
404 alarm(0);
|
|
405
|
|
406 if ($@) {
|
|
407 log_in("died: $@");
|
|
408 return undef;
|
|
409 }
|
|
410
|
|
411 my $ctx = Net::SSLeay::CTX_new() or die("Failed to create SSL_CTX $!");
|
|
412
|
|
413 if (defined $type) {
|
|
414 my $ssleay = Net::SSLeay::SSLeay();
|
|
415 if ($ssleay < 0x1000200f || $ssleay == 0x20000000) {
|
|
416 Net::SSLeay::CTX_set_cipher_list($ctx, $type)
|
|
417 or die("Failed to set cipher list");
|
|
418 } else {
|
|
419 # SSL_CTRL_SET_SIGALGS_LIST
|
|
420 Net::SSLeay::CTX_ctrl($ctx, 98, 0, $type . '+SHA256')
|
|
421 or die("Failed to set sigalgs");
|
|
422 }
|
|
423 }
|
|
424
|
|
425 Net::SSLeay::set_cert_and_key($ctx, "$d/$cert.crt", "$d/$cert.key")
|
|
426 or die if $cert;
|
|
427 my $ssl = Net::SSLeay::new($ctx) or die("Failed to create SSL $!");
|
|
428 Net::SSLeay::set_session($ssl, $ses) if defined $ses;
|
|
429 Net::SSLeay::set_tlsext_host_name($ssl, $sni) if $sni;
|
|
430 Net::SSLeay::set_fd($ssl, fileno($s));
|
|
431 Net::SSLeay::connect($ssl) or die("ssl connect");
|
|
432 return ($s, $ssl);
|
|
433 }
|
|
434
|
|
435 sub get_version {
|
|
436 my ($s, $ssl) = get_ssl_socket();
|
|
437 return Net::SSLeay::version($ssl);
|
|
438 }
|
|
439
|
|
440 ###############################################################################
|
|
441
|
|
442 sub http_daemon {
|
|
443 my ($t, $port) = @_;
|
|
444 my $server = IO::Socket::INET->new(
|
|
445 Proto => 'tcp',
|
|
446 LocalHost => "127.0.0.1:$port",
|
|
447 Listen => 5,
|
|
448 Reuse => 1
|
|
449 )
|
|
450 or die "Can't create listening socket: $!\n";
|
|
451
|
|
452 local $SIG{PIPE} = 'IGNORE';
|
|
453
|
|
454 while (my $client = $server->accept()) {
|
|
455 $client->autoflush(1);
|
|
456
|
|
457 my $headers = '';
|
|
458 my $uri = '';
|
|
459 my $resp;
|
|
460
|
|
461 while (<$client>) {
|
|
462 $headers .= $_;
|
|
463 last if (/^\x0d?\x0a?$/);
|
|
464 }
|
|
465
|
|
466 $uri = $1 if $headers =~ /^\S+\s+\/([^ ]+)\s+HTTP/i;
|
|
467 next unless $uri;
|
|
468
|
|
469 $uri =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
|
|
470 my $req = decode_base64($uri);
|
|
471
|
|
472 if (index($req, $serial_int) > 0) {
|
|
473 $resp = 'int-resp';
|
|
474
|
|
475 } elsif (index($req, $serial) > 0) {
|
|
476 $resp = 'resp';
|
|
477
|
|
478 # used to differentiate ssl_ocsp_responder
|
|
479
|
|
480 if ($port == port(8081) && -e "$d/revoked.der") {
|
|
481 $resp = 'revoked';
|
|
482 }
|
|
483
|
|
484 } else {
|
|
485 $resp = 'ec-resp';
|
|
486 }
|
|
487
|
|
488 # ocsp dummy handler
|
|
489
|
|
490 select undef, undef, undef, 0.02;
|
|
491
|
|
492 $headers = <<"EOF";
|
|
493 HTTP/1.1 200 OK
|
|
494 Connection: close
|
|
495 Content-Type: application/ocsp-response
|
|
496
|
|
497 EOF
|
|
498
|
|
499 print $client $headers . $t->read_file("$resp.der")
|
|
500 if -e "$d/$resp.der";
|
|
501 }
|
|
502 }
|
|
503
|
|
504 ###############################################################################
|