File Coverage

blib/lib/Mail/DKIM/Iterator.pm
Criterion Covered Total %
statement 353 427 82.6
branch 178 260 68.4
condition 77 163 47.2
subroutine 34 43 79.0
pod 9 9 100.0
total 651 902 72.1


line stmt bran cond sub pod time code
1             package Mail::DKIM::Iterator;
2 2     2   493020 use v5.10.0;
  2         11  
3              
4             our $VERSION = '1.011';
5              
6 2     2   13 use strict;
  2         5  
  2         77  
7 2     2   18 use warnings;
  2         4  
  2         164  
8 2     2   1408 use Crypt::OpenSSL::RSA;
  2         11837  
  2         124  
9 2     2   24 use Scalar::Util 'dualvar';
  2         6  
  2         311  
10              
11             # critical header fields which should be well protected
12             my @critical_headers = qw(from subject content-type content-transfer-encoding);
13             my $critical_headers_rx = do {
14             my $rx = join("|",@critical_headers);
15             qr{$rx}i;
16             };
17             # all header fields which should be included in the signature
18             my @sign_headers = (@critical_headers, 'to', 'cc', 'date');
19              
20 2     2   19 use Exporter 'import';
  2         3  
  2         444  
21             our @EXPORT = qw(
22             DKIM_POLICY
23             DKIM_PERMERROR
24             DKIM_NEUTRAL
25             DKIM_TEMPERROR
26             DKIM_FAIL
27             DKIM_PASS
28             );
29              
30             use constant {
31 2         532 DKIM_POLICY => dualvar(-4,'policy'),
32             DKIM_PERMERROR => dualvar(-3,'permerror'),
33             DKIM_NEUTRAL => dualvar(-2,'neutral'),
34             DKIM_TEMPERROR => dualvar(-1,'temperror'),
35             DKIM_FAIL => dualvar( 0,'fail'),
36             DKIM_PASS => dualvar( 1,'pass'),
37 2     2   17 };
  2         5  
38              
39             # compability to versions 1.003 and lower
40             push @EXPORT, qw(
41             DKIM_INVALID_HDR
42             DKIM_TEMPFAIL
43             DKIM_SOFTFAIL
44             DKIM_PERMFAIL
45             DKIM_SUCCESS
46             );
47              
48             use constant {
49 2         12943 DKIM_INVALID_HDR => DKIM_PERMERROR,
50             DKIM_TEMPFAIL => DKIM_TEMPERROR,
51             DKIM_SOFTFAIL => DKIM_NEUTRAL,
52             DKIM_PERMFAIL => DKIM_FAIL,
53             DKIM_SUCCESS => DKIM_PASS,
54 2     2   23 };
  2         4  
55              
56              
57             # create new object
58             sub new {
59 46     46 1 253211 my ($class,%args) = @_;
60             my $self = bless {
61             records => $args{dns} || {}, # mapping (dnsname,dkim_key)
62             extract_sig => 1, # extract signatures from mail header
63             filter => $args{filter}, # filter which signatures are relevant
64             # sig => [...], # list of signatures from header or to sign
65 46   50     335 _hdrbuf => '', # used while collecting the mail header
66             }, $class;
67              
68 46 100       141 if (my $sig = delete $args{sign}) {
69             # signatures given for signing, either as [{},...] or {}
70             # add to self.sig
71 24 50       103 $sig = [$sig] if ref($sig) ne 'ARRAY';
72 24         53 $self->{extract_sig} = delete $args{sign_and_verify};
73 24         41 my $error;
74 24         48 for(@$sig) {
75 24 50 100     160 $_->{h} //= 'from' if ref($_); # minimal
76 24         66 my $s = parse_signature($_,\$error,1);
77 24 50       53 die "bad signature '$_': $error" if !$s;
78 24   100     95 $s->{h_auto} //= 1; # secure version will be detected based on mail
79 24         38 push @{$self->{sig}}, $s
  24         83  
80             }
81             }
82 46         126 return $self;
83             }
84              
85             # Iterator: feed object with information and get back what to do next
86             sub next {
87 108     108 1 1002 my $self = shift;
88 108         118 my $rv;
89 108         201 while (@_) {
90 108         151 my $arg = shift;
91 108 100       170 if (ref($arg)) {
92             # ref: mapping (host,dkim_key)
93 19         57 while (my ($k,$v) = each %$arg) {
94 19         62 $self->{records}{$k} = $v;
95             }
96 19         26 $rv = _compute_result($self);
97             } else {
98             # string: append data from mail
99 89 100       183 if (defined $self->{_hdrbuf}) {
100             # header not fully read: append and try to find end of header
101 46         158 $self->{_hdrbuf} .= $arg;
102 46 50       557 $self->{_hdrbuf} =~m{(\r?\n)\1}g or last; # no end of header
103              
104             # Extract header into self.header and look for DKIM signatures
105             # inside. The rest of _hdrbuf will be appended as part of the
106             # body and the attribute _hdrbuf itself is no longer needed
107 46         371 $self->{header} = substr($self->{_hdrbuf},0,$+[0],'');
108 46 100 66     163 if ($self->{extract_sig}
109             and my @sig = _parse_header($self->{header})) {
110 22 50       66 if (my $f = $self->{filter}) {
111 0         0 @sig = grep { $f->($_,$self->{header}) } @sig;
  0         0  
112             }
113 22 50 50     35 push @{$self->{sig} ||= []}, @sig if @sig;
  22         164  
114             }
115 46         129 $arg = delete $self->{_hdrbuf};
116 46 100       176 _append_body($self,$arg) if $arg ne '';
117              
118             } else {
119             # header already read: append as part of body
120 43         102 _append_body($self,$arg);
121             }
122              
123 89 50       186 if (!$self->{sig}) {
124             # No signatures found in body -> empty return list
125 0         0 $rv = [];
126             } else {
127 89         144 $rv = _compute_result($self);
128             }
129             }
130             }
131 108 50       251 $rv = _compute_result($self) if ! @_;
132              
133             # If we have no results yet just return that we need more data
134 108 50       177 $rv or return ([],\'');
135              
136             # Extract the DNS names for the partial results where the DKIM key is needed
137             # and return the as todo. If the body hash could not yet computed for a
138             # signature mark also that we need more data
139 108         161 my (%need_dns,$need_more_data);
140 108         166 for(@$rv) {
141 108 100       177 $_->status and next;
142 62         104 my $sig = $_->sig;
143              
144             # Need more data to compute the body hash?
145 62 100       118 $need_more_data = 1 if !$sig->{'bh:computed'};
146              
147             # Need to get DKIM key to validate signature?
148             # Only if we have sig.b, i.e. an extracted signature from the header.
149 62 100       122 if ($sig->{b}) {
150 38         62 my $name = $_->dnsname;
151 38 50       107 $need_dns{$name}++ if ! $self->{records}{$name};
152             }
153             }
154              
155             # return preliminary results and @todo
156 108 100       682 return ($rv,$need_more_data ? (\''):(),sort keys %need_dns);
157             }
158              
159             sub filter {
160 0     0 1 0 my ($self,$filter) = @_;
161 0         0 $self->{filter} = $filter;
162 0         0 @{$self->{sig}} = grep { $filter->($_,$self->{header}) } @{$self->{sig}}
  0         0  
  0         0  
163 0 0 0     0 if $self->{header} && $self->{sig};
164             }
165              
166             sub result {
167 0     0 1 0 my $self = shift;
168 0         0 return $self->{_last_result};
169             }
170              
171             sub authentication_results {
172             return join(";\n",map {
173 0         0 my $ar = $_->authentication_results;
174 0 0       0 $ar ? (' '.$ar) : (),
175 0 0   0 1 0 } @{shift->result || []});
  0         0  
176             }
177              
178             # Compute result based on current data.
179             # This might add more DKIM records to validate signatures.
180             sub _compute_result {
181 216     216   253 my $self = shift;
182 216 50       351 return if defined $self->{_hdrbuf}; # need more header
183 216 50       353 return [] if !$self->{sig}; # nothing to verify
184              
185 216         237 my @rv;
186 216         222 for my $sig (@{$self->{sig}}) {
  216         368  
187              
188             # use final result if we have one already
189 216 100       345 if ($sig->{':result'}) {
190 46         97 push @rv, $sig->{':result'};
191 46         73 next;
192             }
193              
194 170 100       262 if ($sig->{error}) {
195             # something wrong with the DKIM-Signature header, return error
196             push @rv, $sig->{':result'} =
197             Mail::DKIM::Iterator::VerifyRecord->new(
198             $sig,
199             ($sig->{s}//'UNKNOWN')."_domainkey".($sig->{d}//'UNKNOWN'),
200             DKIM_PERMERROR,
201             $sig->{error}
202 1   50     11 );
      50        
203 1         2 next;
204             }
205              
206 169 100       313 if (!$sig->{b}) {
207             # sig is not for verification but for signing
208 72 100       104 if (!$sig->{'bh:computed'}) {
209             # incomplete: still need more data to compute signature
210 48         161 push @rv, Mail::DKIM::Iterator::SignRecord->new($sig);
211             } else {
212             # complete: compute signature and save it in :result
213 24         27 my $err;
214 24         59 my $dkim_sig = sign($sig,$sig->{':key'},$self->{header},\$err);
215 24 50       138 push @rv, $sig->{':result'} =
216             Mail::DKIM::Iterator::SignRecord->new(
217             $dkim_sig ? ($sig,$dkim_sig,DKIM_PASS)
218             : ($sig,undef,DKIM_FAIL,$err)
219             );
220             }
221 72         138 next;
222             }
223              
224 97         172 my $dns = "$sig->{s}._domainkey.$sig->{d}";
225              
226 97 100 66     178 if ($sig->{x} && $sig->{x} < time()) {
227 1         4 push @rv, $sig->{':result'} = Mail::DKIM::Iterator::VerifyRecord
228             ->new($sig,$dns, DKIM_POLICY, "signature e[x]pired");
229 1         1 next;
230             }
231              
232 96         131 my $txt = $self->{records}{$dns};
233 96 100 33     288 if ($txt and !ref($txt) || ref($txt) eq 'ARRAY') {
    100 66        
      66        
234             # Take the first syntactically valid DKIM key from the list of
235             # TXT records.
236 18         23 my $error = "no TXT records";
237 18 50       37 for(ref($txt) ? @$txt:$txt) {
238 18 100       38 if (my $r = parse_dkimkey($_,\$error)) {
239 17         34 $self->{records}{$dns} = $txt = $r;
240 17         28 $error = undef;
241 17         62 last;
242             }
243             }
244 18 100       42 $self->{records}{$dns} = $txt = { permfail => $error }
245             if $error;
246             } elsif (exists $self->{records}{$dns} && ! $txt) {
247 1         3 $self->{records}{$dns} = $txt = { tempfail => "dns lookup failed" }
248             }
249              
250 96         157 my @v = _verify_sig($sig,$txt);
251 96         209 push @rv, Mail::DKIM::Iterator::VerifyRecord->new($sig,$dns,@v);
252 96 100       250 $sig->{':result'} = $rv[-1] if @v; # we got a final result
253             }
254 216         687 return ($self->{_last_result} = \@rv);
255             }
256              
257             # Parse DKIM-Signature value into hash and fill in necessary default values.
258             # Input can be string or hash.
259             sub parse_signature {
260 70     70 1 180 my ($v,$error,$for_signing) = @_;
261 70 100 50     163 $v = parse_taglist($v,$error) or return if !ref($v);
262              
263 70 100       135 if ($for_signing) {
264             # some defaults
265 48   100     166 $v->{v} //= '1';
266             }
267              
268 70 100 50     406 if (($v->{v}//'') ne '1') {
    50 66        
    50          
    50          
    50          
    50          
269 1   50     3 $$error = "bad DKIM signature version: ".($v->{v}||'');
270             } elsif (!$v->{d}) {
271 0         0 $$error = "required [d]omain not given";
272             } elsif (!$v->{s}) {
273 0         0 $$error = "required [s]elector not given";
274             } elsif (!$v->{h}) {
275 0         0 $$error = "required [h]eader fields not given";
276             } elsif ($v->{l} && $v->{l} !~m{^\d{1,76}\z}) {
277 0         0 $$error = "invalid body [l]ength";
278             } elsif (do {
279 69   100     215 $v->{q} = lc($v->{q}//'dns/txt');
280 69         175 $v->{q} ne 'dns/txt'
281             }) {
282 0         0 $$error = "unsupported query method $v->{q}";
283             }
284 70 100       148 return if $$error;
285              
286 69         151 $v->{d} = lc($v->{d});
287 69   100     181 $v->{a} = lc($v->{a}//'rsa-sha256');
288 69   100     150 $v->{c} = lc($v->{c}//'simple/simple');
289              
290 69         668 my @h = split(/\s*:\s*/,lc($v->{h}));
291             $$error = "'from' missing from [h]eader fields"
292 69 50       146 if ! grep { $_ eq 'from' } @h;
  497         759  
293 69         161 $v->{'h:list'} = \@h;
294              
295 69 100       126 if ($for_signing) {
296 48         65 delete $v->{b};
297 48         58 delete $v->{bh};
298 48 50       98 $v->{t} = undef if exists $v->{t};
299 48 50 66     125 if (defined $v->{x} && $v->{x} !~m{^\+?\d{1,12}\z}) {
300 0         0 $$error = "invalid e[x]piration time";
301             }
302             } else {
303 21 50 33     82 if (!$v->{b} or not $v->{'b:bin'} = _decode64($v->{b})) {
    50 33        
    50 33        
    100          
304 0   0     0 $$error = "invalid body signature: ".($v->{b}||'');
305             } elsif (!$v->{bh} or not $v->{'bh:bin'} = _decode64($v->{bh})) {
306 0   0     0 $$error = "invalid header signature: ".($v->{bh}||'');
307             } elsif ($v->{t} && $v->{t} !~m{^\d{1,12}\z}) {
308 0         0 $$error = "invalid [t]imestamp";
309             } elsif ($v->{x}) {
310 1 50 33     12 if ($v->{x} !~m{^\d{1,12}\z}) {
    50          
311 0         0 $$error = "invalid e[x]piration time";
312             } elsif ($v->{t} && $v->{x} < $v->{t}) {
313 0         0 $$error = "expiration precedes timestamp";
314             }
315             }
316              
317 21 50       40 if ($v->{i}) {
318 0         0 $v->{i} = _decodeQP($v->{i});
319 0 0       0 if (lc($v->{i}) =~m{\@([^@]+)$}) {
320 0         0 $v->{'i:domain'} = $1;
321             $$error ||= "[i]dentity does not match [d]omain"
322 0 0 0     0 if $v->{'i:domain'} !~m{^(.+\.)?\Q$v->{d}\E\z};
323             } else {
324 0         0 $$error = "no domain in identity";
325             }
326             } else {
327 21         58 $v->{i} = '@'.$v->{d};
328             }
329             }
330              
331             my ($hdrc,$bodyc) = $v->{c}
332 69 50       494 =~m{^(relaxed|simple)(?:/(relaxed|simple))?$} or do {
333 0   0     0 $$error ||= "invalid canonicalization $v->{c}";
334             };
335 69   100     155 $bodyc ||= 'simple';
336 69 50       331 my ($kalgo,$halgo) = $v->{a} =~m{^(rsa)-(sha(?:1|256))$} or do {
337 0   0     0 $$error ||= "unsupported algorithm $v->{a}";
338             };
339 69 50       123 return if $$error;
340              
341 69         154 $v->{'c:hdr'} = $hdrc;
342 69         109 $v->{'c:body'} = $bodyc;
343 69         136 $v->{'a:key'} = $kalgo;
344 69         120 $v->{'a:hash'} = $halgo;
345              
346             # ignore: z
347 69         198 return $v;
348             }
349              
350             # Parse DKIM key into hash and fill in necessary default values.
351             # Input can be string or hash.
352             sub parse_dkimkey {
353 18     18 1 31 my ($v,$error) = @_;
354 18 50 50     40 $v = parse_taglist($v,$error) or return if !ref($v);
355 18 100 66     67 if (!$v || !%$v) {
356 1         1 $$error = "invalid or empty DKIM record";
357 1         4 return;
358             }
359              
360 17 50 50     97 if (($v->{v}||='DKIM1') ne 'DKIM1') {
    50 50        
361 0         0 $$error = "bad DKIM record version: $v->{v}";
362             } elsif (($v->{k}//='rsa') ne 'rsa') {
363 0         0 $$error = "unsupported key type $v->{k}";
364             } else {
365 17 50       38 if (exists $v->{g}) {
366             # g is deprecated in RFC 6376
367 0         0 if (1) {
368             delete $v->{g}
369 0         0 } else {
370             $v->{g} = ($v->{g}//'') =~m{^(.*)\*(.*)$}
371             ? qr{^\Q$1\E.*\Q$2\E\@[^@]+\z}
372             : qr{^\Q$v->{g}\E\@[^@]+\z};
373             }
374             }
375 17   50     91 $v->{t} = { map { $_ => 1 } split(':',lc($v->{t} || '')) };
  0         0  
376 17   50     68 $v->{h} = { map { $_ => 1 } split(':',lc($v->{h} || 'sha1:sha256')) };
  34         97  
377 17   50     69 $v->{s} = { map { $_ => 1 } split(':',lc($v->{s} || '*')) };
  17         43  
378 17 0 33     40 if (!$v->{s}{'*'} && !$v->{s}{email}) {
379 0         0 $$error = "service type " . join(':',keys %{$v->{s}})
  0         0  
380             . " does not match";
381 0         0 return;
382             }
383 17         48 return $v;
384             }
385 0         0 return;
386             }
387              
388             # Finalize signature, i.e add the 'b' parameter.
389             # Input is signature (hash or string), the private key (PEM or
390             # Crypto::OpenSSL::RSA object) and the mail header.
391             # Output is "DKIM-Signature: .... " string with proper line length so that it
392             # can be inserted into the mail header.
393             sub sign {
394 24     24 1 54 my ($sig,$key,$hdr,$error) = @_;
395 24 100 66     88 if (ref($sig) && $sig->{h_auto}) {
396             # add a useful default based on the header which makes sure that no all
397             # relevant headers are covered and no additional important headers can
398             # be added
399 22         28 my (%oh,@nh);
400 22   50     148 $oh{lc($_)}++ for split(':',$sig->{h} ||'');
401 22         46 for my $k (@sign_headers) {
402 154         2242 for($hdr =~m{^($k):}mgi) {
403 88         181 push @nh,$k; # cover each instance in header
404             }
405 154         221 push @nh,$k; # cover non-existance so that no instance can be added
406 154 100 66     335 delete $oh{$k} if exists $oh{$k} and --$oh{$k} == 0;
407             }
408 22         42 push @nh,($_) x $oh{$_} for keys %oh;
409 22         117 $sig->{h} = join(':',@nh);
410             }
411 24 50       49 $sig = parse_signature($sig,$error,1) or return;
412              
413              
414 24         243 my %sig = %$sig;
415 24 50 33     110 $sig{t} = time() if !$sig{t} && exists $sig{t};
416             $sig{x} = ($sig{t} || time()) + $1
417 24 50 0     56 if $sig{x} && $sig{x} =~m{^\+(\d+)$};
      66        
418 24 50       48 $sig{'a:key'} eq 'rsa' or do {
419 0         0 $$error = "unsupported algorithm ".$sig{'a:key'};
420 0         0 return;
421             };
422 24         31 delete $sig{b};
423 24 50       63 $sig{i} = _encodeQP($sig{':i'}) if $sig{':i'};
424 24 50       39 $sig{z} = _encodeQP($sig{':z'}) if $sig{':z'};
425 24   33     65 $sig{bh} = _encode64($sig{'bh:computed'} || $sig{'bh:bin'});
426 24         30 $sig{h} = join(':',@{$sig{'h:list'}});
  24         69  
427              
428 24         33 my @v;
429 24         48 for (qw(v a c d q s t x h l i z bh)) {
430 312   100     555 my $v = delete $sig{$_} // next;
431 194         367 push @v, "$_=$v"
432             }
433 24         115 for(sort keys %sig) {
434 192 100       295 m{:} and next;
435 24   50     61 my $v = _encodeQP(delete $sig{$_} // next);
436 24         52 push @v, "$_=$v"
437             }
438              
439 24         52 my @lines = shift(@v);
440 24         36 for(@v,"b=") {
441 218         221 $lines[-1] .= ';';
442 218         210 my $append = " $_";
443 218 100       290 my $x80 = (@lines == 1 ? 64 : 80) - length($lines[-1]);
444 218 100       286 if (length($append)<=$x80) {
    100          
445 173         264 $lines[-1] .= $append;
446             } elsif (length($append)<=80) {
447 23         35 push @lines,$append;
448             } else {
449 22         23 while (1) {
450 48 100       69 if ( $x80>10) {
451 44         70 $lines[-1] .= substr($append,0,$x80,'');
452 44 100       115 $append eq '' and last;
453             }
454 26         30 push @lines,' ';
455 26         27 $x80 = 80;
456             }
457             }
458             }
459              
460 24         55 my $dkh = 'DKIM-Signature: '.join("\r\n",@lines);
461 24 50       49 $sig->{'a:key'} eq 'rsa' or do {
462 0         0 $$error = "unsupported signature algorithm $sig->{'a:key'}";
463 0         0 return;
464             };
465             my $hash = _compute_hdrhash($hdr,
466 24         77 $sig{'h:list'},$sig->{'a:hash'},$sig->{'c:hdr'},$dkh);
467              
468 24 50       30478 my $priv = ref($key) ? $key : Crypt::OpenSSL::RSA->new_private_key($key);
469 24 50       123 $priv or do {
470 0         0 $$error = "using private key failed";
471 0         0 return;
472             };
473 24         108 $priv->use_no_padding;
474              
475             my $data = _encode64($priv->decrypt(
476 24         121 _emsa_pkcs1_v15($sig->{'a:hash'},$hash,$priv->size)));
477              
478 24   33     261 my $x80 = 80 - ($dkh =~m{\n([^\n]+)\z} && length($1));
479 24         53 while ($data ne '') {
480 85 100       182 $dkh .= substr($data,0,$x80,'') if $x80>10;
481 85 100       141 $dkh .= "\r\n " if $data ne '';
482 85         113 $x80 = 80;
483             }
484 24         31 $dkh .= "\r\n";
485 24         375 return $dkh;
486             }
487              
488             # Verify a DKIM signature (hash from parse_signature) using a DKIM key (hash
489             # from parse_dkimkey). Output is (error_code,error_string) or simply
490             # (DKIM_PASS) in case of no error or () if no final result can be computed yet.
491             sub _verify_sig {
492 96     96   144 my ($sig,$param) = @_;
493              
494             # check pre-computed hash over body if body done
495 96 100 100     248 if (defined $sig->{'bh:computed'}
496             and $sig->{'bh:computed'} ne $sig->{'bh:bin'}) {
497 1         3 return (DKIM_FAIL, 'body hash mismatch');
498             }
499 95 100       169 return if ! $param;
500              
501 19 50       31 return (DKIM_PERMERROR,"none or invalid dkim record") if ! %$param;
502 19 100       67 return (DKIM_TEMPERROR,$param->{tempfail}) if $param->{tempfail};
503 18 100       34 return (DKIM_PERMERROR,$param->{permfail}) if $param->{permfail};
504              
505 17 50       57 my $FAIL = $param->{t}{y} ? DKIM_NEUTRAL : DKIM_FAIL;
506 17 50       34 return ($FAIL,"key revoked") if ! $param->{p};
507              
508             return ($FAIL,"hash algorithm not allowed")
509 17 50       41 if ! $param->{h}{$sig->{'a:hash'}};
510              
511             return ($FAIL,"identity does not match domain") if $param->{t}{s}
512 17 0 33     34 && $sig->{'i:domain'} && $sig->{'i:domain'} ne $sig->{d};
      0        
513              
514             return ($FAIL,"identity does not match granularity")
515 17 50 33     40 if $param->{g} && $sig->{i} !~ $param->{g};
516              
517             # needs bh:computed to continue
518 17 50       31 return if ! defined $sig->{'bh:computed'};
519              
520 17 100       24 if (!eval {
521 17 50       31 my $rsa = Crypt::OpenSSL::RSA->new_public_key(do {
522 17         25 local $_ = $param->{p};
523 17         65 s{\s+}{}g;
524 17         177 s{(.{1,64})}{$1\n}g;
525 17         94 "-----BEGIN PUBLIC KEY-----\n$_-----END PUBLIC KEY-----\n";
526             }) or die [DKIM_PERMERROR,"using public key failed"];
527 16         16408 $rsa->use_no_padding;
528 16 100       25 my $bencrypt = eval { $rsa->encrypt($sig->{'b:bin'}) }
  16         16100  
529             or die [DKIM_PERMERROR,"header sig corrupt"];
530             my $expect = _emsa_pkcs1_v15(
531 15         143 $sig->{'a:hash'},$sig->{'h:hash'},$rsa->size);
532 15 100       36 if ($expect ne $bencrypt) {
533             # warn "expect= "._encode64($expect)."\n";
534             # warn "encrypt="._encode64($bencrypt)."\n";
535 1         11 die [$FAIL,"header sig mismatch"];
536             }
537 14         128 1;
538             }) {
539 3 100       876 return @{$@} if ref($@);
  2         8  
540 1         5 return (DKIM_PERMERROR,"using public key failed");
541             }
542 14 100       22 return (DKIM_PASS, join(' + ', @{$sig->{':warning'} || []}));
  14         103  
543             }
544              
545             # parse the header and extract
546             sub _parse_header {
547 22     22   30 my $hdr = shift;
548 22         48 my %all_critical = map { $_ => 0 } @critical_headers;
  88         205  
549 22         411 $all_critical{lc($_)}-- for $hdr =~m{^($critical_headers_rx):}mig;
550 22         37 my @sig;
551 22         151 while ( $hdr =~m{^(DKIM-Signature:\s*(.*\n(?:[ \t].*\n)*))}mig ) {
552 22         52 my $dkh = $1; # original value to exclude it when computing hash
553              
554 22         25 my $error;
555 22         45 my $sig = parse_signature($2,\$error);
556 22 100       38 if ($sig) {
557             $sig->{'h:hash'} = _compute_hdrhash($hdr,
558 21         59 $sig->{'h:list'},$sig->{'a:hash'},$sig->{'c:hdr'},$dkh);
559              
560 21         78 my %critical = %all_critical;
561 21         41 $critical{$_}++ for @{$sig->{'h:list'}};
  21         118  
562 21 100       76 if (my @h = grep { $critical{$_} < 0 } keys %critical) {
  155         269  
563 2         3 push @{$sig->{':warning'}},
  2         10  
564             "unprotected critical header ".join(",",sort @h);
565             }
566             } else {
567 1         3 $sig = { error => "invalid DKIM-Signature header: $error" };
568             }
569              
570 22         116 push @sig,$sig;
571             }
572 22         103 return @sig;
573             }
574              
575             {
576             # EMSA-PKCS1-v1_5 encapsulation, see RFC 3447 9.2
577             my %sig_prefix = (
578             'sha1' => pack("H*","3021300906052B0E03021A05000414"),
579             'sha256' => pack("H*","3031300d060960864801650304020105000420"),
580             );
581             sub _emsa_pkcs1_v15 {
582 39     39   102 my ($algo,$hash,$len) = @_;
583 39   50     131 my $t = ($sig_prefix{$algo} || die "unsupport digest $algo") . $hash;
584 39         72 my $pad = $len - length($t) -3;
585 39 50       82 $pad < 8 and die;
586 39         20105 return "\x00\x01" . ("\xff" x $pad) . "\x00" . $t;
587             }
588             }
589              
590             {
591              
592             # simple header canonicalization:
593             my $simple_hdrc = sub {
594             my $line = shift;
595             $line =~s{(?
596             return $line;
597             };
598              
599             # relaxed header canonicalization:
600             my $relaxed_hdrc = sub {
601             my ($k,$v) = shift() =~m{\A([^:]+:[ \t]*)?(.*)\z}s;
602             $v =~s{\r?\n([ \t])}{$1}g; # unfold lines
603             $v =~s{[ \t]+}{ }g; # WSP+ -> SP
604             $v =~s{\s+\z}{\r\n}; # eliminate all WS from end, normalize line end
605             $k = lc($k||''); # lower case key
606             $k=~s{[ \t]*:[ \t]*}{:}; # remove white-space around colon
607             return $k.$v;
608             };
609              
610             my %hdrc = (
611             simple => $simple_hdrc,
612             relaxed => $relaxed_hdrc,
613             );
614              
615 2     2   1419 use Digest::SHA;
  2         7194  
  2         5349  
616             my %digest = (
617             sha1 => sub { Digest::SHA->new(1) },
618             sha256 => sub { Digest::SHA->new(256) },
619             );
620              
621             # compute the hash over the header
622             sub _compute_hdrhash {
623 45     45   114 my ($hdr,$headers,$hash,$canon,$dkh) = @_;
624             #warn "XXX $hash | $canon";
625 45         99 $hash = $digest{$hash}();
626 45         575 $canon = $hdrc{$canon};
627 45         59 my @hdr;
628             my %kv;
629 45         74 for my $k (@$headers) {
630 467 50       617 if ($k eq 'dkim-signature') {
631 0         0 for($hdr =~m{^($k:[^\n]*\n(?:[ \t][^\n]*\n)*)}mig) {
632 0 0       0 $_ eq $dkh and next;
633 0         0 push @hdr,$_;
634             }
635             } else {
636 467   100     8311 my $v = $kv{$k} ||=
637             [ $hdr =~m{^($k:[^\n]*\n(?:[ \t][^\n]*\n)*)}mig ];
638             # take last matching kv in mail header
639 467   100     1404 push @hdr, pop(@$v) // '';
640             }
641             }
642 45         182 $dkh =~s{([ \t;:]b=)([a-zA-Z0-9/+= \t\r\n]+)}{$1};
643 45         163 $dkh =~s{[\r\n]+\z}{};
644 45         62 push @hdr,$dkh;
645 45         136 $_ = $canon->($_) for (@hdr);
646             #warn Dumper(\@hdr); use Data::Dumper;
647 45         315 $hash->add(@hdr);
648 45         440 return $hash->digest;
649             }
650              
651             # simple body canonicalization:
652             # - normalize to \r\n line end
653             # - remove all empty lines at the end
654             # - make sure that body consists at least of a single empty line
655             # relaxed body canonicalization:
656             # - like simple, but additionally...
657             # - remove any white-space at the end of a line (excluding \r\n)
658             # - compact any white-space inside the line to a single space
659             # - also, empty body will result in '', not \r\n
660              
661             my $bodyc = sub {
662             my $relaxed = shift;
663             my $empty = my $no_line_yet = '';
664             my $realdata;
665             sub {
666             my $data = shift;
667             if ($data eq '') {
668             return $no_line_yet if $realdata;
669             return $relaxed ? "" : "\r\n";
670             }
671             my $nl = rindex($data,"\n");
672             if ($nl == -1) {
673             $no_line_yet .= $data;
674             return '';
675             }
676              
677             if ($nl == length($data)-1) {
678             # newline at end of data
679             $data = $no_line_yet . $data if $no_line_yet ne '';
680             $no_line_yet = '';
681             } else {
682             # newline somewhere inside
683             $no_line_yet .= substr($data,0,$nl+1,'');
684             ($data,$no_line_yet) = ($no_line_yet,$data);
685             }
686              
687             $data =~s{(?
688             if ($relaxed) {
689             $data =~s{[ \t]+}{ }g; # compact WSP+ to SP
690             $data =~s{ \r\n}{\r\n}g; # remove WSP+ at eol
691             }
692              
693             if ($data =~m{(^|\n)(?:\r\n)+\z}) {
694             if (!$+[1]) {
695             # everything empty
696             $empty .= $data;
697             return '';
698             } else {
699             # part empty
700             $empty .= substr($data,0,$+[1],'');
701             ($empty,$data) = ($data,$empty);
702             }
703             } else {
704             # nothing empty
705             if ($empty ne '') {
706             $data = $empty . $data;
707             $empty = '';
708             }
709             }
710             $realdata = 1;
711             return $data;
712             };
713             };
714              
715             my %bodyc = (
716             simple => sub { $bodyc->(0) },
717             relaxed => sub { $bodyc->(1) },
718             );
719              
720             # add data to the body
721             sub _append_body {
722 87     87   164 my ($self,$buf) = @_;
723 87         109 for my $sig (@{$self->{sig}}) {
  87         161  
724 87 50       169 $sig->{'bh:computed'} and next;
725 87   66     218 my $bh = $sig->{'bh:collecting'} ||= do {
726 46 100 66     174 if (!$sig->{error} and
      66        
727             my $digest = $digest{$sig->{'a:hash'}}() and
728             my $transform = $bodyc{$sig->{'c:body'}}()
729             ) {
730             {
731             digest => $digest,
732             transform => $transform,
733             $sig->{l} ? (l => $sig->{l}) :
734 45 100       243 defined($sig->{l}) ? (l => \$sig->{l}) : # capture l
    100          
735             (),
736             };
737             } else {
738 1         2 { done => 1 };
739             }
740             };
741              
742 87 100       150 $bh->{done} and next;
743 86         141 my $tbuf = $bh->{transform}($buf);
744 86 100       150 if ($buf eq '') {
745 43         84 $bh->{done} = 1;
746 43         197 goto add_tbuf;
747             }
748 43 50       66 $tbuf eq '' and next;
749             {
750 43 100       59 defined $bh->{l} or last;
  43         97  
751 2 100       6 if (ref $bh->{l}) {
752 1         1 ${$bh->{l}} += length($tbuf);
  1         2  
753 1         2 next;
754             }
755 1 50       4 if ($bh->{l} > 0) {
756 1 50       4 last if ($bh->{l} -= length($tbuf))>0;
757             $bh->{_data_after_l} ||=
758 1   33     33 substr($tbuf,$bh->{l},-$bh->{l},'') =~m{\S} & 1;
759 1         3 $bh->{l} = 0;
760             } else {
761 0   0     0 $bh->{_data_after_l} ||= $tbuf =~m{\S} & 1;
762 0         0 $tbuf = '';
763             }
764 1         4 $bh->{done} = 1;
765             }
766              
767             add_tbuf:
768 86 100       268 $bh->{digest}->add($tbuf) if $tbuf ne '';
769 86 100       193 $bh->{done} or next;
770              
771 44         68 delete $sig->{'bh:collecting'};
772 44         258 $sig->{'bh:computed'} = $bh->{digest}->digest;
773 1         18 push @{$sig->{':warning'}}, 'data after signed body'
774 44 100       574 if $bh->{_data_after_l};
775             }
776             }
777             }
778              
779             {
780              
781             # parse_taglist($val,\$error)
782             # Parse a tag-list, like in the DKIM signature and in the DKIM key.
783             # Returns a hash of the parsed list. If error occur $error will be set and
784             # undef will be returned.
785              
786             my $fws = qr{
787             [ \t]+ (?:\r?\n[ \t]+)? |
788             \r?\n[ \t]+
789             }x;
790             my $tagname = qr{[a-z]\w*}i;
791             my $tval = qr{[\x21-\x3a\x3c-\x7e]+};
792             my $tagval = qr{$tval(?:$fws$tval)*};
793             my $end = qr{(?:\r?\n)?\z};
794             my $delim_or_end = qr{ $fws? (?: $end | ; (?: $fws?$end|)) }x;
795              
796             sub parse_taglist {
797 40     40 1 62 my ($v,$error) = @_;
798 40         49 my %v;
799 40         1249 while ( $v =~m{\G $fws? (?:
800             ($tagname) $fws?=$fws? ($tagval?) $delim_or_end |
801             | (.+)
802             )}xgcs) {
803 295 50       522 if (defined $3) {
804 0         0 $$error = "invalid data at end: '$3'";
805 0         0 return;
806             }
807 295 100       495 last if ! defined $1;
808 255 50       457 exists($v{$1}) && do {
809 0         0 $$error = "duplicate key $1";
810 0         0 return;
811             };
812 255         2450 $v{$1} = $2;
813             }
814             #warn Dumper(\%v); use Data::Dumper;
815 40         131 return \%v;
816             }
817             }
818              
819             sub _encode64 {
820 48     48   127 my $data = shift;
821 48         94 my $pad = ( 3 - length($data) % 3 ) % 3;
822 48         199 $data = pack('u',$data);
823 48         893 $data =~s{(^.|\n)}{}mg;
824 48         123 $data =~tr{` -_}{AA-Za-z0-9+/};
825 48 50       144 substr($data,-$pad) = '=' x $pad if $pad;
826 48         104 return $data;
827             }
828              
829             sub _decode64 {
830 42     42   63 my $data = shift;
831 42         154 $data =~s{\s+}{}g;
832 42         146 $data =~s{=+$}{};
833 42         73 $data =~tr{A-Za-z0-9+/}{`!-_};
834 42         125 $data =~s{(.{1,60})}{ chr(32 + length($1)*3/4) . $1 . "\n" }eg;
  82         351  
835 42         358 return unpack("u",$data);
836             }
837              
838             sub _encodeQP {
839 24     24   105 (my $data = shift)
840 0         0 =~s{([^\x21-\x3a\x3c\x3e-\x7e])}{ sprintf('=%02X',ord($1)) }esg;
841 24         37 return $data;
842             }
843              
844             sub _decodeQP {
845 0     0   0 my $data = shift;
846 0         0 $data =~s{\s+}{}g;
847 0         0 $data =~s{=([0-9A-F][0-9A-F])}{ chr(hex($1)) }esg;
  0         0  
848 0         0 return $data;
849             }
850              
851              
852             # ResultRecord for verification.
853             package Mail::DKIM::Iterator::VerifyRecord;
854             sub new {
855 98     98   144 my $class = shift;
856 98         258 bless [@_],$class;
857             }
858 38     38   53 sub sig { shift->[0] }
859 0     0   0 sub domain { shift->[0]{d} }
860 38     38   58 sub dnsname { shift->[1] }
861 90     90   324 sub status { shift->[2] }
862 8 50   8   27 sub error { $_[0]->[2] >0 ? undef : $_[0]->[3] }
863 16 50   16   106 sub warning { $_[0]->[2] >0 ? $_[0]->[3] : undef }
864              
865             sub authentication_results {
866 0     0   0 my $self = shift;
867 0 0       0 return if ! $self->[2];
868 0         0 my $ar = "dkim=$self->[2]";
869 0 0 0     0 $ar .= " ($self->[3])" if defined $self->[3] and $self->[3] ne '';
870 0   0     0 $ar .= " header.d=".( $self->[0]{d} // 'unknown');
871 0         0 return $ar;
872             }
873              
874             # ResultRecord for signing.
875             package Mail::DKIM::Iterator::SignRecord;
876             sub new {
877 72     72   105 my $class = shift;
878 72         238 bless [@_],$class;
879             }
880 24     24   102 sub sig { shift->[0] }
881 0     0   0 sub domain { shift->[0]{d} }
882             sub dnsname {
883 0     0   0 my $sig = shift->[0];
884 0   0     0 return ($sig->{s} || 'UNKNOWN').'_domainkey'.($sig->{d} || 'UNKNOWN');
      0        
885             }
886 24     24   104 sub signature { shift->[1] }
887 72     72   301 sub status { shift->[2] }
888 0     0     sub error { shift->[3] }
889              
890             1;
891              
892             __END__