File Coverage

blib/lib/Mail/DKIM/PublicKey.pm
Criterion Covered Total %
statement 155 195 79.4
branch 60 92 65.2
condition 20 27 74.0
subroutine 19 24 79.1
pod 2 17 11.7
total 256 355 72.1


line stmt bran cond sub pod time code
1             package Mail::DKIM::PublicKey;
2 14     14   91 use strict;
  14         35  
  14         389  
3 14     14   88 use warnings;
  14         27  
  14         665  
4             our $VERSION = '1.20230212'; # VERSION
5             # ABSTRACT: Represents a DKIM key
6              
7             # Copyright 2005 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 14     14   108 use base ( 'Mail::DKIM::KeyValueList', 'Mail::DKIM::Key' );
  14         30  
  14         6309  
15             *calculate_EM = \&Mail::DKIM::Key::calculate_EM;
16              
17 14     14   6118 use Mail::DKIM::DNS;
  14         148  
  14         21712  
18              
19             sub new {
20 358     358 0 542 my $type = shift;
21 358         640 my %prms = @_;
22              
23 358         616 my $self = {};
24              
25 358         801 $self->{'GRAN'} = $prms{'Granularity'};
26 358         618 $self->{'NOTE'} = $prms{'Note'};
27 358         604 $self->{'TEST'} = $prms{'Testing'};
28 358   50     1321 $self->{'TYPE'} = ( $prms{'Type'} or 'rsa' );
29 358         597 $self->{'DATA'} = $prms{'Data'};
30              
31 358         1009 bless $self, $type;
32             }
33              
34              
35             sub fetch {
36 2     2 1 591 my $class = shift;
37 2         7 my $waiter = $class->fetch_async(@_);
38 2         5 my $self = $waiter->();
39 2         59 return $self;
40             }
41              
42             # fetch_async() - asynchronously tries fetching a specific public key
43             # using a specific protocol.
44             #
45             # Usage:
46             # my $fut = Mail::DKIM::PublicKey->fetch_async(
47             # Protocol => 'dns/txt',
48             # Selector => 'selector1',
49             # Domain => 'example.org',
50             # Callbacks => { ... }, #optional
51             # );
52             #
53             # # some later time
54             # my $pubkey = $fut->(); # blocks until the public key is returned
55             #
56             sub fetch_async {
57 419     419 0 783 my $class = shift;
58 419         1675 my %prms = @_;
59              
60 419 50 33     2853 defined( $prms{Protocol} ) && $prms{Protocol} =~ m{^dns(/txt)?$}s
61             or die "invalid/missing Protocol\n";
62              
63             defined( $prms{Selector} ) && length( $prms{Selector} )
64 419 100 66     1547 or die "invalid/missing Selector\n";
65              
66             defined( $prms{Domain} ) && length( $prms{Domain} )
67 417 50 33     1300 or die "invalid/missing Domain\n";
68              
69 417 100       615 my %callbacks = %{ $prms{Callbacks} || {} };
  417         1954  
70 417   100 2   1163 my $on_success = $callbacks{Success} || sub { $_[0] };
  2         18  
71             $callbacks{Success} = sub {
72 363     363   743 my @resp = @_;
73 363 100       786 unless (@resp) {
74              
75             # no requested resource records or NXDOMAIN,
76 5         23 return $on_success->();
77             }
78              
79 358         565 my $strn;
80 358         786 foreach my $rr (@resp) {
81 358 50       827 next unless $rr->type eq 'TXT';
82              
83             # join with no intervening spaces, RFC 6376
84 358 50       6280 if ( Net::DNS->VERSION >= 0.69 ) {
85              
86             # must call txtdata() in a list context
87 358         1101 $strn = join '', $rr->txtdata;
88             }
89             else {
90             # char_str_list method is 'historical'
91 0         0 $strn = join '', $rr->char_str_list;
92             }
93 358         11094 last;
94             }
95              
96             $strn
97 358 50       812 or return $on_success->();
98              
99 358         1142 my $self = $class->parse($strn);
100 356         1131 $self->{Selector} = $prms{'Selector'};
101 356         785 $self->{Domain} = $prms{'Domain'};
102 356         1183 $self->check;
103 352         953 return $on_success->($self);
104 417         1749 };
105              
106             #
107             # perform DNS query for public key...
108             #
109 417         1199 my $host = $prms{Selector} . '._domainkey.' . $prms{Domain};
110 417         1339 my $waiter =
111             Mail::DKIM::DNS::query_async( $host, 'TXT', Callbacks => \%callbacks, );
112 417         1157 return $waiter;
113             }
114              
115              
116             # check syntax of the public key
117             # throw an error if any errors are detected
118             sub check {
119 356     356 0 549 my $self = shift;
120              
121             # check public key version tag
122 356 100       1211 if ( my $v = $self->get_tag('v') ) {
123 335 100       804 unless ( $v eq 'DKIM1' ) {
124 1         20 die "unsupported version\n";
125             }
126             }
127              
128             # check public key granularity
129 355         895 my $g = $self->granularity;
130              
131             # check key type
132 355 100       789 if ( my $k = $self->get_tag('k') ) {
133 330 100       749 unless ( $k eq 'rsa' ) {
134 1         10 die "unsupported key type\n";
135             }
136             }
137              
138             # check public-key data
139 354         820 my $p = $self->data;
140 354 50       761 if ( not defined $p ) {
141 0         0 die "missing p= tag\n";
142             }
143 354 100       799 if ( $p eq '' ) {
144 1         9 die "revoked\n";
145             }
146 353 50       1489 unless ( $p =~ /^[A-Za-z0-9\+\/\=]+$/ ) {
147 0         0 die "invalid data\n";
148             }
149              
150             # have OpenSSL load the key
151             eval {
152 353         1142 local $SIG{__DIE__};
153 353         1303 $self->cork;
154 353         1417 1
155 353 50       564 } || do {
156              
157             # see also finish_body
158 0         0 chomp( my $E = $@ );
159 0 0       0 if ( $E =~ /(OpenSSL error: .*?) at / ) {
    0          
160 0         0 $E = "$1";
161             }
162             elsif ( $E =~ /^(panic:.*?) at / ) {
163 0         0 $E = "OpenSSL $1";
164             }
165 0         0 die "$E\n";
166             };
167              
168             # check service type
169 353 100       963 if ( my $s = $self->get_tag('s') ) {
170 22         77 my @list = split( /:/, $s );
171 22 100       50 unless ( grep { $_ eq '*' || $_ eq 'email' } @list ) {
  26 100       139  
172 1         15 die "does not support email\n";
173             }
174             }
175              
176 352         712 return 1;
177             }
178              
179             # check_granularity() - check whether this key matches signature identity
180             #
181             # a public key record can restrict what identities it may sign with,
182             # g=, granularity, restricts the local part of the identity
183             # t=s, restricts whether subdomains can be used
184             #
185             # This method returns true if the given identity is allowed by this
186             # public key; it returns false otherwise.
187             # If false is returned, you can check C<$@> for an explanation of
188             # why.
189             #
190             sub check_granularity {
191 349     349 0 588 my $self = shift;
192 349         668 my ( $identity, $empty_g_means_wildcard ) = @_;
193              
194             # check granularity
195 349         675 my $g = $self->granularity;
196              
197             # yuck- what is this $empty_g_means_wildcard parameter?
198             # well, it turns out that with DomainKeys signatures,
199             # an empty g= is the same as g=*
200 349 100 100     894 if ( $g eq '' && $empty_g_means_wildcard ) {
201 1         4 $g = '*';
202             }
203              
204             # split i= value into a "local part" and a "domain part"
205 349         545 my ( $local_part, $domain_part );
206 349 100       1115 if ( $identity =~ /^(.*)\@([^@]*)$/ ) {
207 54         127 $local_part = $1;
208 54         117 $domain_part = $2;
209             }
210             else {
211 295         420 $local_part = '';
212 295         406 $domain_part = $identity;
213             }
214              
215 349         1060 my ( $begins, $ends ) = split /\*/, $g, 2;
216 349 100       728 if ( defined $ends ) {
217              
218             # the g= tag contains an asterisk
219              
220             # the local part must be at least as long as the pattern
221 348 100 66     2050 if (
      100        
      100        
222             length($local_part) < length($begins) + length($ends)
223             or
224              
225             # the local part must begin with $begins
226             substr( $local_part, 0, length($begins) ) ne $begins
227             or
228              
229             # the local part must end with $ends
230             ( length($ends) && substr( $local_part, -length($ends) ) ne $ends )
231             )
232             {
233 3         8 $@ = "granularity mismatch\n";
234 3         12 return;
235             }
236             }
237             else {
238 1 50       8 if ( $g eq '' ) {
239 1         4 $@ = "granularity is empty\n";
240 1         4 return;
241             }
242 0 0       0 unless ( $local_part eq $begins ) {
243 0         0 $@ = "granularity mismatch\n";
244 0         0 return;
245             }
246             }
247              
248             # check subdomains
249 345 100       776 if ( $self->subdomain_flag ) {
250 2 100       11 unless ( lc( $domain_part ) eq lc( $self->{'Domain'} ) ) {
251 1         2 $@ = "does not support signing subdomains\n";
252 1         6 return;
253             }
254             }
255              
256 344         1248 return 1;
257             }
258              
259             # returns true if the actual hash algorithm used is listed by this
260             # public key; dies otherwise
261             #
262             sub check_hash_algorithm {
263 351     351 0 543 my $self = shift;
264 351         623 my ($hash_algorithm) = @_;
265              
266             # check hash algorithm
267 351 100       703 if ( my $h = $self->get_tag('h') ) {
268 14         48 my @list = split( /:/, $h );
269 14 100       35 unless ( grep { $_ eq $hash_algorithm } @list ) {
  38         97  
270 2         18 die "does not support hash algorithm '$hash_algorithm'\n";
271             }
272             }
273 349         739 return 1;
274             }
275              
276             # Create an OpenSSL public key object from the Base64-encoded data
277             # found in this public key's DNS record. The OpenSSL object is saved
278             # in the "cork" property.
279             sub convert {
280 14     14   3363 use Crypt::OpenSSL::RSA;
  14         42736  
  14         13772  
281              
282 353     353 0 491 my $self = shift;
283              
284 353 50       610 $self->data
285             or return;
286              
287             # have to PKCS1ify the pubkey because openssl is too finicky...
288 353         696 my $cert = "-----BEGIN PUBLIC KEY-----\n";
289              
290 353         755 for ( my $i = 0 ; $i < length $self->data ; $i += 64 ) {
291 1355         2236 $cert .= substr $self->data, $i, 64;
292 1355         3140 $cert .= "\n";
293             }
294              
295 353         657 $cert .= "-----END PUBLIC KEY-----\n";
296              
297 353 50       10553 my $cork = Crypt::OpenSSL::RSA->new_public_key($cert)
298             or die 'unable to generate public key object';
299              
300             # segfaults on my machine
301             # $cork->check_key or
302             # return;
303              
304 353         17232 $self->cork($cork);
305              
306 353         700 return 1;
307             }
308              
309             sub verify {
310 0     0 0 0 my $self = shift;
311 0         0 my %prms = @_;
312              
313 0         0 my $rtrn;
314              
315             eval {
316 0         0 local $SIG{__DIE__};
317 0         0 $rtrn = $self->cork->verify( $prms{'Text'}, $prms{'Signature'} );
318 0         0 1
319 0 0       0 } || do {
320 0         0 $self->errorstr($@);
321 0         0 return;
322             };
323              
324 0         0 return $rtrn;
325             }
326              
327              
328             sub granularity {
329 704     704 1 1040 my $self = shift;
330              
331             # set new granularity if provided
332 704 50       1523 (@_)
333             and $self->set_tag( 'g', shift );
334              
335 704         1510 my $g = $self->get_tag('g');
336 704 100       1347 if ( defined $g ) {
337 44         92 return $g;
338             }
339             else {
340 660         1346 return '*';
341             }
342             }
343              
344             sub notes {
345 0     0 0 0 my $self = shift;
346              
347 0 0       0 (@_)
348             and $self->set_tag( 'n', shift );
349              
350 0         0 return $self->get_tag('n');
351             }
352              
353             sub data {
354 3770     3770 0 5066 my $self = shift;
355              
356 3770 50       6455 (@_)
357             and $self->set_tag( 'p', shift );
358              
359 3770         6908 my $p = $self->get_tag('p');
360              
361             # remove whitespace (actually only LWSP is allowed) and double quote (long string delimiter)
362 3770 50       10312 $p =~ tr/\015\012 \t"//d if defined $p;
363 3770         8724 return $p;
364             }
365              
366             sub flags {
367 345     345 0 570 my $self = shift;
368              
369 345 50       731 (@_)
370             and $self->set_tag( 't', shift );
371              
372 345   100     752 return $self->get_tag('t') || '';
373             }
374              
375             # subdomain_flag() - checks whether "s" is specified in flags
376             #
377             # returns true if "s" is found, false otherwise
378             #
379             sub subdomain_flag {
380 345     345 0 549 my $self = shift;
381 345         699 my @flags = split /:/, $self->flags;
382 345         1018 return grep { $_ eq 's' } @flags;
  20         80  
383             }
384              
385             sub revoked {
386 0     0 0 0 my $self = shift;
387              
388 0 0       0 $self->data
389             or return 1;
390              
391 0         0 return;
392             }
393              
394             sub testing {
395 0     0 0 0 my $self = shift;
396              
397 0         0 my $flags = $self->flags;
398 0         0 my @flaglist = split( /:/, $flags );
399 0 0       0 if ( grep { $_ eq 'y' } @flaglist ) {
  0         0  
400 0         0 return 1;
401             }
402 0         0 return undef;
403             }
404              
405             sub verify_sha1_digest {
406 0     0 0 0 my $self = shift;
407 0         0 my ( $digest, $signature ) = @_;
408 0         0 return $self->verify_digest( 'SHA-1', $digest, $signature );
409             }
410              
411             # verify_digest() - returns true if the digest verifies, false otherwise
412             #
413             # if false, $@ is set to a description of the problem
414             #
415             sub verify_digest {
416 341     341 0 520 my $self = shift;
417 341         741 my ( $digest_algorithm, $digest, $signature ) = @_;
418              
419 341         866 my $rsa_pub = $self->cork;
420 341 50       723 if ( !$rsa_pub ) {
421 0 0       0 $@ = $@ ne '' ? "RSA failed: $@" : 'RSA unknown problem';
422 0         0 $@ .= ", s=$self->{Selector} d=$self->{Domain}";
423 0         0 return;
424             }
425              
426 341         980 $rsa_pub->use_no_padding;
427 341         14383 my $verify_result = $rsa_pub->encrypt($signature);
428              
429 333         1066 my $k = $rsa_pub->size;
430 333         881 my $expected = calculate_EM( $digest_algorithm, $digest, $k );
431 333 100       1297 return 1 if ( $verify_result eq $expected );
432              
433             # well, the RSA verification failed; I wonder if the RSA signing
434             # was performed on a different digest value? I think we can check...
435              
436             # basically, if the $verify_result has the same prefix as $expected,
437             # then only the digest was different
438              
439 63         109 my $digest_len = length $digest;
440 63         117 my $prefix_len = length($expected) - $digest_len;
441 63 100       235 if (
442             substr( $verify_result, 0, $prefix_len ) eq
443             substr( $expected, 0, $prefix_len ) )
444             {
445 60         102 $@ = 'message has been altered';
446 60         336 return;
447             }
448              
449 3         6 $@ = 'bad RSA signature';
450 3         16 return;
451             }
452              
453             1;
454              
455             __END__