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   323955 use strict;
  5         59  
  5         147  
3 5     5   26 use warnings;
  5         10  
  5         203  
4             our $VERSION = '1.20230630'; # 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   2166 use Mail::DKIM::PrivateKey;
  5         21  
  5         180  
15 5     5   2720 use Mail::DKIM::Signature;
  5         48  
  5         396  
16              
17              
18 5     5   41 use base 'Mail::DKIM::Common';
  5         9  
  5         2237  
19 5     5   43 use Carp;
  5         11  
  5         9844  
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 46 my $self = shift;
62 24         89 $self->SUPER::init;
63              
64 24 100       72 unless ( $self->{'Algorithm'} ) {
65              
66             # use default algorithm
67 10         28 $self->{'Algorithm'} = 'rsa-sha1';
68             }
69              
70 24         45 my $type = 'rsa'; # default
71 24 100       86 $type = 'ed25519' if ( $self->{'Algorithm'} =~ /^ed25519/ );
72              
73 24 100       61 if ( defined $self->{KeyFile} ) {
74             $self->{Key} ||=
75             Mail::DKIM::PrivateKey->load( File => $self->{KeyFile},
76 22   33     158 Type => $type );
77             }
78              
79 22 100       76 unless ( $self->{'Method'} ) {
80              
81             # use default canonicalization method
82 10         22 $self->{'Method'} = 'relaxed';
83             }
84 22 100       59 unless ( $self->{'Domain'} ) {
85              
86             # use default domain
87 10         18 $self->{'Domain'} = 'example.org';
88             }
89 22 100       68 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 40 my $self = shift;
99              
100 22         52 $self->{algorithms} = [];
101              
102 22         40 my $policy = $self->{Policy};
103 22 100 66     133 if ( UNIVERSAL::isa( $policy, 'CODE' ) ) {
    100          
104              
105             # policy is a subroutine ref
106 8         95 my $default_sig = $policy->($self);
107 8 100 100     18 unless ( @{ $self->{algorithms} } || $default_sig ) {
  8         33  
108 1         3 $self->{'result'} = 'skipped';
109 1         3 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     8 unless ( @{ $self->{algorithms} } || $default_sig ) {
  2         13  
117 0         0 $self->{'result'} = 'skipped';
118 0         0 return;
119             }
120             }
121              
122 21 100       34 unless ( @{ $self->{algorithms} } ) {
  21         73  
123              
124             # no algorithms were created yet, so construct a signature
125             # using the current signature properties
126              
127             # check properties
128 15 50       38 unless ( $self->{'Algorithm'} ) {
129 0         0 die 'invalid algorithm property';
130             }
131 15 50       37 unless ( $self->{'Method'} ) {
132 0         0 die 'invalid method property';
133             }
134 15 50       41 unless ( $self->{'Domain'} ) {
135 0         0 die 'invalid header property';
136             }
137 15 50       36 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       57 $self->{'Tags'} ? ( Tags => $self->{'Tags'} )
    100          
    100          
    50          
164             : ()
165             ),
166             )
167             );
168             }
169              
170 21         40 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  21         53  
171              
172             # output header as received so far into canonicalization
173 22         34 foreach my $header ( @{ $self->{headers} } ) {
  22         43  
174 67         153 $algorithm->add_header($header);
175             }
176 22         102 $algorithm->finish_header( Headers => $self->{headers} );
177             }
178             }
179              
180             sub finish_body {
181 22     22 0 39 my $self = shift;
182              
183 22         29 foreach my $algorithm ( @{ $self->{algorithms} } ) {
  22         72  
184              
185             # finished canonicalizing
186 22         99 $algorithm->finish_body;
187              
188 22         49 my $type = 'rsa'; # default
189 22 100       77 $type = 'ed25519' if ( $self->{'Algorithm'} =~ /^ed25519/ );
190              
191             # load the private key file if necessary
192 22         71 my $signature = $algorithm->signature;
193             my $key =
194             $signature->{Key}
195             || $signature->{KeyFile}
196             || $self->{Key}
197 22   66     98 || $self->{KeyFile};
198 22 100 66     104 if ( defined($key) && !ref($key) ) {
199 1         7 $key = Mail::DKIM::PrivateKey->load( File => $key,
200             Type => $type );
201             }
202             $key
203 22 50       64 or die "no key available to sign with\n";
204              
205             # compute signature value
206 22         86 my $signb64 = $algorithm->sign($key);
207 22         105 $signature->data($signb64);
208              
209             # insert linebreaks in signature data, if desired
210 22         71 $signature->prettify_safe();
211              
212 22         75 $self->{signature} = $signature;
213 22         99 $self->{result} = 'signed';
214             }
215             }
216              
217              
218             sub add_signature {
219 22     22 1 38 my $self = shift;
220 22         35 my $signature = shift;
221              
222             # create a canonicalization filter and algorithm
223 22 50 0     58 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         225 );
230 22         53 push @{ $self->{algorithms} }, $algorithm;
  22         58  
231 22         45 return;
232             }
233              
234              
235             sub algorithm {
236 3     3 1 11 my $self = shift;
237 3 50       7 if ( @_ == 1 ) {
238 3         7 $self->{Algorithm} = shift;
239             }
240 3         4 return $self->{Algorithm};
241             }
242              
243              
244             sub domain {
245 3     3 1 20 my $self = shift;
246 3 50       7 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         2 my @headers;
269              
270             # these are the header fields we found in the message we're signing
271 1         2 my @found_headers = @{ $self->{header_field_names} };
  1         5  
272              
273             # Convert all keys to lower case
274 1         3 foreach my $header ( keys %{ $self->{'ExtendedHeaders'} } ) {
  1         7  
275 4 50       12 next if $header eq lc $header;
276 4 50       10 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         10 $self->{'ExtendedHeaders'}->{$header};
295             }
296 4         8 delete $self->{'ExtendedHeaders'}->{$header};
297             }
298              
299             # Add the default headers
300 1         3 foreach my $default (@DEFAULT_HEADERS) {
301 28 100       51 if ( !exists $self->{'ExtendedHeaders'}->{ lc $default } ) {
302 26         65 $self->{'ExtendedHeaders'}->{ lc $default } = '*';
303             }
304             }
305              
306             # Build a count of found headers
307 1         3 my $header_counts = {};
308 1         3 foreach my $header (@found_headers) {
309 10 100       20 if ( !exists $header_counts->{ lc $header } ) {
310 7         13 $header_counts->{ lc $header } = 1;
311             }
312             else {
313 3         9 $header_counts->{ lc $header } = $header_counts->{ lc $header } + 1;
314             }
315             }
316              
317 1         4 foreach my $header ( sort keys %{ $self->{'ExtendedHeaders'} } ) {
  1         16  
318 30         44 my $want_count = $self->{'ExtendedHeaders'}->{$header};
319 30   100     75 my $have_count = $header_counts->{ lc $header } || 0;
320 30         38 my $add_count = 0;
321 30 100       85 if ( $want_count eq '+' ) {
    100          
322 1         3 $add_count = $have_count + 1;
323             }
324             elsif ( $want_count eq '*' ) {
325 27         35 $add_count = $have_count;
326             }
327             else {
328 2 50       5 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         19 push @headers, $header;
337             }
338             }
339 1         17 return join( ':', @headers );
340             }
341              
342             sub extended_headers {
343 1     1 1 433 my $self = shift;
344 1         5 $self->{'ExtendedHeaders'} = shift;
345 1         3 return;
346             }
347              
348             sub headers {
349 20     20 1 63 my $self = shift;
350 20 50       43 croak 'unexpected argument' if @_;
351              
352 20 100       53 if ( exists $self->{'ExtendedHeaders'} ) {
353 1         10 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         57  
358              
359             # these are the headers we actually want to sign
360 19         105 my @wanted_headers = @DEFAULT_HEADERS;
361 19 50       46 if ( $self->{Headers} ) {
362 0         0 push @wanted_headers, split /:/, $self->{Headers};
363             }
364              
365             my @headers =
366             grep {
367 19         44 my $a = $_;
  50         74  
368 50         77 scalar grep { lc($a) eq lc($_) } @wanted_headers
  1400         2299  
369             } @found_headers;
370 19         230 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 5 my $self = shift;
395 1 50       4 if (@_) {
396 1         68 $self->{Key} = undef;
397 1         7 $self->{KeyFile} = shift;
398             }
399 1         3 return $self->{KeyFile};
400             }
401              
402              
403             sub method {
404 3     3 1 11 my $self = shift;
405 3 50       8 if ( @_ == 1 ) {
406 3         5 $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         9 $self->{Selector} = shift;
417             }
418 3         4 return $self->{Selector};
419             }
420              
421              
422             sub signatures {
423 1     1 1 4 my $self = shift;
424 1 50       5 croak 'no arguments allowed' if @_;
425 1         3 return map { $_->signature } @{ $self->{algorithms} };
  2         6  
  1         3  
426             }
427              
428              
429             1;
430              
431             __END__