File Coverage

blib/lib/Mail/Milter/Authentication/Handler/TLS.pm
Criterion Covered Total %
statement 70 97 72.1
branch 14 30 46.6
condition n/a
subroutine 14 16 87.5
pod 1 9 11.1
total 99 152 65.1


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::TLS;
2 13     13   7360 use strict;
  13         47  
  13         393  
3 13     13   73 use warnings;
  13         31  
  13         387  
4 13     13   101 use base 'Mail::Milter::Authentication::Handler';
  13         32  
  13         1689  
5             our $VERSION = '20191206'; # VERSION
6              
7 13     13   87 use Sys::Syslog qw{:standard :macros};
  13         31  
  13         4949  
8 13     13   99 use Mail::AuthenticationResults::Header::Entry;
  13         39  
  13         413  
9 13     13   77 use Mail::AuthenticationResults::Header::SubEntry;
  13         29  
  13         347  
10 13     13   67 use Mail::AuthenticationResults::Header::Comment;
  13         33  
  13         11938  
11              
12             sub default_config {
13 0     0 0 0 return {};
14             }
15              
16             sub grafana_rows {
17 0     0 0 0 my ( $self ) = @_;
18 0         0 my @rows;
19 0         0 push @rows, $self->get_json( 'TLS_metrics' );
20 0         0 return \@rows;
21             }
22              
23             sub register_metrics {
24             return {
25 12     12 1 102 'tls_connect_total' => 'The number of connections which were enctypted',
26             };
27             }
28              
29             sub pre_loop_setup {
30 12     12 0 39 my ( $self ) = @_;
31 12         54 my $protocol = Mail::Milter::Authentication::Config::get_config()->{'protocol'};
32 12 50       60 if ( $protocol eq 'smtp' ) {
33 12         336 warn 'When in smtp mode, the TLS handler requires the MTA to write TLS data into the first Received header.';
34             }
35 12         54 return;
36             }
37              
38             sub connect_callback {
39 35     35 0 115 my ( $self ) = @_;
40             # Reset state on a connection
41 35         131 delete $self->{'is_encrypted'};
42 35         108 return;
43             }
44              
45             sub envfrom_callback {
46 35     35 0 145 my ( $self, $env_from ) = @_;
47              
48 35         100 delete $self->{'first_header_read'};
49              
50 35         234 my $protocol = Mail::Milter::Authentication::Config::get_config()->{'protocol'};
51 35 50       169 return if $protocol ne 'milter';
52              
53 0         0 my $version = $self->get_symbol('{tls_version}');
54 0         0 my $cipher = $self->get_symbol('{cipher}');
55 0         0 my $bits = $self->get_symbol('{cipher_bits}');
56             # on postfix the macro is empty on untrusted connections
57 0 0       0 my $trusted = $self->get_symbol('{cert_issuer}') ? ', trusted' : '';
58              
59 0 0       0 if ($version) {
60 0         0 $self->dbgout( 'EncryptedAs', "$version, $cipher, $bits bits$trusted", LOG_INFO );
61 0         0 $self->{'is_encrypted'} = 1;
62              
63 0         0 my $metric_data = {};
64 0         0 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-tls' )->safe_set_value( 'pass' );
65 0         0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'smtp.version' )->safe_set_value( $version ) );
66 0         0 $metric_data->{ 'version' } = $version;
67              
68 0 0       0 if ( $cipher ) {
69 0         0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'smtp.cipher' )->safe_set_value( $cipher ) );
70 0         0 $metric_data->{ 'cipher' } = $cipher;
71             }
72 0 0       0 if ( $bits ) {
73 0         0 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'smtp.bits' )->safe_set_value( $bits ) );
74 0         0 $metric_data->{ 'bits' } = $bits;
75             }
76 0 0       0 $metric_data->{ 'trusted' } = $trusted ? 1 : 0;
77              
78 0         0 $self->metric_count( 'tls_connect_total', $metric_data );
79              
80 0         0 $self->add_auth_header( $header );
81             }
82             else {
83 0         0 $self->{'is_encrypted'} = 0;
84             }
85 0         0 return;
86             }
87              
88             sub header_callback {
89 383     383 0 1241 my ( $self, $header, $value ) = @_;
90              
91 383 100       1566 return if lc $header ne 'received';
92 60 100       356 return if ( exists( $self->{'first_header_read'} ) );
93 24         96 $self->{'first_header_read'} = 1;
94              
95              
96 24         150 my $protocol = Mail::Milter::Authentication::Config::get_config()->{'protocol'};
97 24 50       108 return if $protocol ne 'smtp';
98              
99             # Try and parse the first received header, this should be something like...
100             # Received: from mail-ua0-f173.google.com (mail-ua0-f173.google.com [209.85.217.173])
101             # (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
102             # (No client certificate requested)
103             # by mx5.messagingengine.com (Postfix) with ESMTPS
104             # for <marcmctest@fastmail.com>; Thu, 1 Dec 2016 22:35:06 -0500 (EST)
105              
106             # Future, extend to check for client certificates
107              
108 24         204 $value =~ m/using ([^ ]*) with cipher ([^ ]+) \(([^ ]+) bits\)/;
109 24         102 my $version = $1;
110 24         86 my $cipher = $2;
111 24         73 my $bits = $3;
112              
113 24 100       91 if ($version) {
114 13         154 $self->dbgout( 'EncryptedAs', "$version, $cipher, $bits bits", LOG_INFO );
115 13         74 $self->{'is_encrypted'} = 1;
116              
117 13         63 my $metric_data = {};
118 13         120 my $header = Mail::AuthenticationResults::Header::Entry->new()->set_key( 'x-tls' )->safe_set_value( 'pass' );
119 13         1293 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'smtp.version' )->safe_set_value( $version ) );
120 13         1567 $metric_data->{ 'version' } = $version;
121              
122 13 50       56 if ( $cipher ) {
123 13         74 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'smtp.cipher' )->safe_set_value( $cipher ) );
124 13         1221 $metric_data->{ 'cipher' } = $cipher;
125             }
126 13 50       53 if ( $bits ) {
127 13         60 $header->add_child( Mail::AuthenticationResults::Header::SubEntry->new()->set_key( 'smtp.bits' )->safe_set_value( $bits ) );
128 13         1156 $metric_data->{ 'bits' } = $bits;
129             }
130              
131 13         79 $self->metric_count( 'tls_connect_total', $metric_data );
132              
133 13         100 $self->add_auth_header( $header );
134             }
135              
136 24         106 return;
137             }
138              
139             sub eoh_callback {
140 35     35 0 130 my ( $self ) = @_;
141 35         245 my $protocol = Mail::Milter::Authentication::Config::get_config()->{'protocol'};
142 35 50       182 return if $protocol ne 'smtp';
143 35 100       142 return if defined $self->{'is_encrypted'};
144 22         118 $self->{'is_encrypted'} = 0;
145 22         66 return;
146             }
147              
148             sub close_callback {
149 71     71 0 194 my ( $self ) = @_;
150 71         192 delete $self->{'first_header_read'};
151 71         190 delete $self->{'is_encrypted'};
152 71         220 return;
153             }
154              
155             1;
156              
157             __END__
158              
159             =pod
160              
161             =encoding UTF-8
162              
163             =head1 NAME
164              
165             Mail::Milter::Authentication::Handler::TLS
166              
167             =head1 VERSION
168              
169             version 20191206
170              
171             =head1 DESCRIPTION
172              
173             Identify TLS protected connections.
174              
175             =head1 CONFIGURATION
176              
177             No configuration options exist for this handler.
178              
179             =head1 AUTHOR
180              
181             Marc Bradshaw <marc@marcbradshaw.net>
182              
183             =head1 COPYRIGHT AND LICENSE
184              
185             This software is copyright (c) 2018 by Marc Bradshaw.
186              
187             This is free software; you can redistribute it and/or modify it under
188             the same terms as the Perl 5 programming language system itself.
189              
190             =cut