File Coverage

blib/lib/Mail/DKIM/Signer.pm
Criterion Covered Total %
statement 153 176 86.9
branch 59 84 70.2
condition 13 28 46.4
subroutine 19 21 90.4
pod 10 15 66.6
total 254 324 78.4


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