File Coverage

blib/lib/Mail/DKIM/Signer.pm
Criterion Covered Total %
statement 149 172 86.6
branch 54 78 69.2
condition 13 28 46.4
subroutine 19 21 90.4
pod 10 15 66.6
total 245 314 78.0


line stmt bran cond sub pod time code
1             package Mail::DKIM::Signer;
2 5     5   320650 use strict;
  5         65  
  5         151  
3 5     5   29 use warnings;
  5         9  
  5         201  
4             our $VERSION = '1.20230212'; # VERSION
5             # ABSTRACT: generates a DKIM signature for a message
6              
7             # Copyright 2005-2007 Messiah College. All rights reserved.
8             # Jason Long
9              
10             # Copyright (c) 2004 Anthony D. Urso. All rights reserved.
11             # This program is free software; you can redistribute it and/or
12             # modify it under the same terms as Perl itself.
13              
14 5     5   2069 use Mail::DKIM::PrivateKey;
  5         15  
  5         160  
15 5     5   2593 use Mail::DKIM::Signature;
  5         74  
  5         439  
16              
17              
18 5     5   37 use base 'Mail::DKIM::Common';
  5         11  
  5         2183  
19 5     5   38 use Carp;
  5         11  
  5         9074  
20              
21             # PROPERTIES
22             #
23             # public:
24             #
25             # $dkim->{Algorithm}
26             # identifies what algorithm to use when signing the message
27             # default is "rsa-sha1"
28             #
29             # $dkim->{Domain}
30             # identifies what domain the message is signed for
31             #
32             # $dkim->{KeyFile}
33             # name of the file containing the private key used to sign
34             #
35             # $dkim->{Method}
36             # identifies what canonicalization method to use when signing
37             # the message. default is "relaxed"
38             #
39             # $dkim->{Policy}
40             # a signing policy (of type Mail::DKIM::SigningPolicy)
41             #
42             # $dkim->{Selector}
43             # identifies name of the selector identifying the key
44             #
45             # $dkim->{Key}
46             # the loaded private key
47             #
48             # private:
49             #
50             # $dkim->{algorithms} = []
51             # an array of algorithm objects... an algorithm object is created for
52             # each signature being added to the message
53             #
54             # $dkim->{result}
55             # result of the signing policy: "signed" or "skipped"
56             #
57             # $dkim->{signature}
58             # the created signature (of type Mail::DKIM::Signature)
59              
60             sub init {
61 22     22 0 43 my $self = shift;
62 22         90 $self->SUPER::init;
63              
64 22 100       75 if ( defined $self->{KeyFile} ) {
65             $self->{Key} ||=
66 20   33     164 Mail::DKIM::PrivateKey->load( File => $self->{KeyFile} );
67             }
68              
69 20 100       117 unless ( $self->{'Algorithm'} ) {
70              
71             # use default algorithm
72 10         25 $self->{'Algorithm'} = 'rsa-sha1';
73             }
74 20 100       88 unless ( $self->{'Method'} ) {
75              
76             # use default canonicalization method
77 10         21 $self->{'Method'} = 'relaxed';
78             }
79 20 100       49 unless ( $self->{'Domain'} ) {
80              
81             # use default domain
82 10         20 $self->{'Domain'} = 'example.org';
83             }
84 20 100       57 unless ( $self->{'Selector'} ) {
85              
86             # use default selector
87 10         29 $self->{'Selector'} = 'unknown';
88             }
89             }
90              
91             sub finish_header {
92 20     20 0 32 my $self = shift;
93              
94 20         47 $self->{algorithms} = [];
95              
96 20         36 my $policy = $self->{Policy};
97 20 100 66     118 if ( UNIVERSAL::isa( $policy, 'CODE' ) ) {
    100          
98              
99             # policy is a subroutine ref
100 8         21 my $default_sig = $policy->($self);
101 8 100 100     18 unless ( @{ $self->{algorithms} } || $default_sig ) {
  8         31  
102 1         2 $self->{'result'} = 'skipped';
103 1         3 return;
104             }
105             }
106             elsif ( $policy && $policy->can('apply') ) {
107              
108             # policy is a Perl object or class
109 2         8 my $default_sig = $policy->apply($self);
110 2 50 33     7 unless ( @{ $self->{algorithms} } || $default_sig ) {
  2         14  
111 0         0 $self->{'result'} = 'skipped';
112 0         0 return;
113             }
114             }
115              
116 19 100       33 unless ( @{ $self->{algorithms} } ) {
  19         63  
117              
118             # no algorithms were created yet, so construct a signature
119             # using the current signature properties
120              
121             # check properties
122 13 50       31 unless ( $self->{'Algorithm'} ) {
123 0         0 die 'invalid algorithm property';
124             }
125 13 50       30 unless ( $self->{'Method'} ) {
126 0         0 die 'invalid method property';
127             }
128 13 50       31 unless ( $self->{'Domain'} ) {
129 0         0 die 'invalid header property';
130             }
131 13 50       35 unless ( $self->{'Selector'} ) {
132 0         0 die 'invalid selector property';
133             }
134              
135             $self->add_signature(
136             Mail::DKIM::Signature->new(
137             Algorithm => $self->{'Algorithm'},
138             Method => $self->{'Method'},
139             Headers => $self->headers,
140             Domain => $self->{'Domain'},
141             Selector => $self->{'Selector'},
142             Key => $self->{'Key'},
143             KeyFile => $self->{'KeyFile'},
144             (
145             $self->{'Identity'} ? ( Identity => $self->{'Identity'} )
146             : ()
147             ),
148             (
149             $self->{'Timestamp'} ? ( Timestamp => $self->{'Timestamp'} )
150             : ()
151             ),
152             (
153 13 100       44 $self->{'Expiration'} ? ( Expiration => $self->{'Expiration'} )
    100          
    100          
154             : ()
155             ),
156             )
157             );
158             }
159              
160 19         35 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  19         47  
161              
162             # output header as received so far into canonicalization
163 20         33 foreach my $header ( @{ $self->{headers} } ) {
  20         49  
164 61         141 $algorithm->add_header($header);
165             }
166 20         74 $algorithm->finish_header( Headers => $self->{headers} );
167             }
168             }
169              
170             sub finish_body {
171 20     20 0 37 my $self = shift;
172              
173 20         32 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  20         59  
174              
175             # finished canonicalizing
176 20         87 $algorithm->finish_body;
177              
178             # load the private key file if necessary
179 20         82 my $signature = $algorithm->signature;
180             my $key =
181             $signature->{Key}
182             || $signature->{KeyFile}
183             || $self->{Key}
184 20   66     94 || $self->{KeyFile};
185 20 100 66     99 if ( defined($key) && !ref($key) ) {
186 1         6 $key = Mail::DKIM::PrivateKey->load( File => $key );
187             }
188             $key
189 20 50       62 or die "no key available to sign with\n";
190              
191             # compute signature value
192 20         82 my $signb64 = $algorithm->sign($key);
193 20         92 $signature->data($signb64);
194              
195             # insert linebreaks in signature data, if desired
196 20         88 $signature->prettify_safe();
197              
198 20         73 $self->{signature} = $signature;
199 20         113 $self->{result} = 'signed';
200             }
201             }
202              
203              
204             sub add_signature {
205 20     20 1 36 my $self = shift;
206 20         35 my $signature = shift;
207              
208             # create a canonicalization filter and algorithm
209 20 50 0     48 my $algorithm_class =
210             $signature->get_algorithm_class( $signature->algorithm )
211             or die 'unsupported algorithm ' . ( $signature->algorithm || '' ) . "\n";
212             my $algorithm = $algorithm_class->new(
213             Signature => $signature,
214             Debug_Canonicalization => $self->{Debug_Canonicalization},
215 20         227 );
216 20         46 push @{ $self->{algorithms} }, $algorithm;
  20         59  
217 20         44 return;
218             }
219              
220              
221             sub algorithm {
222 3     3 1 12 my $self = shift;
223 3 50       8 if ( @_ == 1 ) {
224 3         5 $self->{Algorithm} = shift;
225             }
226 3         7 return $self->{Algorithm};
227             }
228              
229              
230             sub domain {
231 3     3 1 16 my $self = shift;
232 3 50       10 if ( @_ == 1 ) {
233 3         6 $self->{Domain} = shift;
234             }
235 3         7 return $self->{Domain};
236             }
237              
238              
239              
240             # these are headers that "should" be included in the signature,
241             # according to the DKIM spec.
242             my @DEFAULT_HEADERS = qw(From Sender Reply-To Subject Date
243             Message-ID To Cc MIME-Version
244             Content-Type Content-Transfer-Encoding Content-ID Content-Description
245             Resent-Date Resent-From Resent-Sender Resent-To Resent-cc
246             Resent-Message-ID
247             In-Reply-To References
248             List-Id List-Help List-Unsubscribe List-Subscribe
249             List-Post List-Owner List-Archive);
250              
251             sub process_headers_hash {
252 1     1 0 10 my $self = shift;
253              
254 1         2 my @headers;
255              
256             # these are the header fields we found in the message we're signing
257 1         3 my @found_headers = @{ $self->{header_field_names} };
  1         5  
258              
259             # Convert all keys to lower case
260 1         2 foreach my $header ( keys %{ $self->{'ExtendedHeaders'} } ) {
  1         6  
261 4 50       10 next if $header eq lc $header;
262 4 50       11 if ( exists $self->{'ExtendedHeaders'}->{ lc $header } ) {
263              
264             # Merge
265 0         0 my $first = $self->{'ExtendedHeaders'}->{ lc $header };
266 0         0 my $second = $self->{'ExtendedHeaders'}->{$header};
267 0 0 0     0 if ( $first eq '+' || $second eq '+' ) {
    0 0        
268 0         0 $self->{'ExtendedHeaders'}->{ lc $header } = '+';
269             }
270             elsif ( $first eq '*' || $second eq '*' ) {
271 0         0 $self->{'ExtendedHeaders'}->{ lc $header } = '*';
272             }
273             else {
274 0         0 $self->{'ExtendedHeaders'}->{ lc $header } = $first + $second;
275             }
276             }
277             else {
278             # Rename
279             $self->{'ExtendedHeaders'}->{ lc $header } =
280 4         9 $self->{'ExtendedHeaders'}->{$header};
281             }
282 4         10 delete $self->{'ExtendedHeaders'}->{$header};
283             }
284              
285             # Add the default headers
286 1         16 foreach my $default (@DEFAULT_HEADERS) {
287 28 100       64 if ( !exists $self->{'ExtendedHeaders'}->{ lc $default } ) {
288 26         60 $self->{'ExtendedHeaders'}->{ lc $default } = '*';
289             }
290             }
291              
292             # Build a count of found headers
293 1         2 my $header_counts = {};
294 1         3 foreach my $header (@found_headers) {
295 10 100       19 if ( !exists $header_counts->{ lc $header } ) {
296 7         15 $header_counts->{ lc $header } = 1;
297             }
298             else {
299 3         11 $header_counts->{ lc $header } = $header_counts->{ lc $header } + 1;
300             }
301             }
302              
303 1         10 foreach my $header ( sort keys %{ $self->{'ExtendedHeaders'} } ) {
  1         21  
304 30         60 my $want_count = $self->{'ExtendedHeaders'}->{$header};
305 30   100     77 my $have_count = $header_counts->{ lc $header } || 0;
306 30         53 my $add_count = 0;
307 30 100       57 if ( $want_count eq '+' ) {
    100          
308 1         3 $add_count = $have_count + 1;
309             }
310             elsif ( $want_count eq '*' ) {
311 27         29 $add_count = $have_count;
312             }
313             else {
314 2 50       6 if ( $want_count > $have_count ) {
315 0         0 $add_count = $have_count;
316             }
317             else {
318 2         3 $add_count = $want_count;
319             }
320             }
321 30         54 for ( 1 .. $add_count ) {
322 6         14 push @headers, $header;
323             }
324             }
325 1         18 return join( ':', @headers );
326             }
327              
328             sub extended_headers {
329 1     1 1 302 my $self = shift;
330 1         4 $self->{'ExtendedHeaders'} = shift;
331 1         3 return;
332             }
333              
334             sub headers {
335 18     18 1 54 my $self = shift;
336 18 50       44 croak 'unexpected argument' if @_;
337              
338 18 100       47 if ( exists $self->{'ExtendedHeaders'} ) {
339 1         5 return $self->process_headers_hash();
340             }
341              
342             # these are the header fields we found in the message we're signing
343 17         24 my @found_headers = @{ $self->{header_field_names} };
  17         57  
344              
345             # these are the headers we actually want to sign
346 17         93 my @wanted_headers = @DEFAULT_HEADERS;
347 17 50       44 if ( $self->{Headers} ) {
348 0         0 push @wanted_headers, split /:/, $self->{Headers};
349             }
350              
351             my @headers =
352             grep {
353 17         40 my $a = $_;
  44         64  
354 44         65 scalar grep { lc($a) eq lc($_) } @wanted_headers
  1232         2073  
355             } @found_headers;
356 17         200 return join( ':', @headers );
357             }
358              
359             # return nonzero if this is header we should sign
360             sub want_header {
361 0     0 0 0 my $self = shift;
362 0         0 my ($header_name) = @_;
363              
364             #TODO- provide a way for user to specify which headers to sign
365 0         0 return scalar grep { lc($_) eq lc($header_name) } @DEFAULT_HEADERS;
  0         0  
366             }
367              
368              
369             sub key {
370 0     0 1 0 my $self = shift;
371 0 0       0 if (@_) {
372 0         0 $self->{Key} = shift;
373 0         0 $self->{KeyFile} = undef;
374             }
375 0         0 return $self->{Key};
376             }
377              
378              
379             sub key_file {
380 1     1 1 5 my $self = shift;
381 1 50       3 if (@_) {
382 1         3 $self->{Key} = undef;
383 1         3 $self->{KeyFile} = shift;
384             }
385 1         2 return $self->{KeyFile};
386             }
387              
388              
389             sub method {
390 3     3 1 12 my $self = shift;
391 3 50       7 if ( @_ == 1 ) {
392 3         6 $self->{Method} = shift;
393             }
394 3         5 return $self->{Method};
395             }
396              
397              
398              
399             sub selector {
400 3     3 1 10 my $self = shift;
401 3 50       7 if ( @_ == 1 ) {
402 3         5 $self->{Selector} = shift;
403             }
404 3         5 return $self->{Selector};
405             }
406              
407              
408             sub signatures {
409 1     1 1 3 my $self = shift;
410 1 50       5 croak 'no arguments allowed' if @_;
411 1         2 return map { $_->signature } @{ $self->{algorithms} };
  2         6  
  1         3  
412             }
413              
414              
415             1;
416              
417             __END__