File Coverage

blib/lib/Crypt/MagicSignatures/Key.pm
Criterion Covered Total %
statement 192 236 81.3
branch 86 110 78.1
condition 31 43 72.0
subroutine 35 36 97.2
pod 11 11 100.0
total 355 436 81.4


line stmt bran cond sub pod time code
1             package Crypt::MagicSignatures::Key;
2 14     14   1528667 use strict;
  14         99  
  14         428  
3 14     14   77 use warnings;
  14         29  
  14         417  
4 14     14   79 use Scalar::Util qw/blessed/;
  14         100  
  14         975  
5 14     14   88 use Carp 'carp';
  14         27  
  14         632  
6            
7 14     14   238 use v5.10.1;
  14         65  
8            
9             our @CARP_NOT;
10             our $VERSION = '0.22';
11            
12             our $DEFAULT_KEY_SIZE = 512;
13             our $MAX_GEN_ROUNDS = 100;
14            
15             # Maximum number of tests for random prime generation = 100
16             # Range of valid key sizes = 512 - 4096
17             # Maximum number length for i2osp and os2ip = 30000
18            
19             # This implementation uses a blessed array for speed.
20             # The array order is [n, e, d, size, emLen].
21            
22             # TODO: Implement a common tech based alternative,
23             # e.g. openssl. LibSodium unfortunately will not work.
24             # TODO: Check extreme values for d, e, n, sign, verify
25             # TODO: Improve tests for _bitsize, b64url_encode, b64url_decode,
26             # hex2b64url, b64url2hex
27             # TODO: Add some notes regarding
28             # http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html
29             # TODO: use Encode for utf8 in Crypt::Key
30            
31 14     14   2612 use overload '""' => sub { $_[0]->to_string }, fallback => 1;
  14     12   2133  
  14         381  
  12         1877  
32 14     14   9166 use Digest::SHA qw/sha256 sha256_hex/;
  14         40331  
  14         1461  
33 14     14   7062 use MIME::Base64 qw(decode_base64 encode_base64);
  14         9184  
  14         917  
34            
35             # Implement with GMP or PARI if existent
36 14     14   4879 use Math::BigInt try => 'GMP,Pari';
  14         112899  
  14         116  
37            
38             # Export functions on request
39 14     14   111639 use Exporter 'import';
  14         38  
  14         1406  
40             our @EXPORT_OK = qw(b64url_encode b64url_decode);
41            
42             # Primitive for Math::Prime::Util
43             sub random_nbit_prime ($);
44            
45             our $GENERATOR;
46            
47             # Load Math::Prime::Util and Math::Random::Secure
48             BEGIN {
49 14 50   14   1297 if (eval q{use Math::Prime::Util 'random_nbit_prime'; 1;}) {
  14     14   17529  
  14         158806  
  14         72  
50 14         36618 our $GENERATOR = 1;
51             };
52             };
53            
54            
55             # Construct a new object
56             sub new {
57 32     32 1 27175 my $class = shift;
58 32         68 my $self;
59            
60             # Message to pass to carp on parameter failure
61 32         74 state $INVALID_MSG = 'Invalid parameters for MagicKey construction';
62            
63             # Check if the passed argument is an object
64 32 100       190 if (my $bl = blessed($_[0])) {
65            
66             # It's already a MagicKey object - fine!
67 4 100       50 return $_[0] if $bl->isa(__PACKAGE__);
68             }
69            
70             # Do not support references
71 30 100       107 if (ref $_[0]) {
72            
73             # Passed object or reference is invalid
74 3         479 carp $INVALID_MSG;
75 3         254 return;
76             };
77            
78             # MagicKey in string notation
79 27 100 100     224 if (@_ == 1 && index($_[0], 'RSA.') >= 0) {
    100          
80            
81 22         57 my $string = shift;
82            
83             # New object from parent class
84 22         63 $self = bless [], $class;
85            
86             # Delete whitespace
87 22         103 $string =~ tr{\t-\x0d }{}d;
88            
89             # Ignore mime-type prolog if given
90 22         115 $string =~ s{^data\:application\/magic(?:\-public|\-private)?\-key[,;:]}{}i;
91            
92             # Split MagicKey
93 22         150 my ($type, $mod, $exp, $private_exp) = split(/\./, $string);
94            
95             # RSA.modulus(n).exponent(e).private_exponent(d)?
96            
97             # MagicKey modulus is missing or invalid
98 22 100       91 unless ($mod = _b64url_to_hex( $mod )) {
99 2         287 carp $INVALID_MSG;
100 2         24 return;
101             };
102            
103 20 100       1262231 $exp = _b64url_to_hex( $exp ) if $exp;
104 20 100       3473 $private_exp = _b64url_to_hex( $private_exp ) if $private_exp;
105            
106             # Set modulus
107 20         57669 $self->n( $mod );
108            
109             # Set exponent
110 20 100       60 $self->e( $exp ) if $exp;
111            
112             # Set private key
113 20 100       126 $self->d( $private_exp ) if $private_exp;
114             }
115            
116             # MagicKey defined by parameters
117             elsif (@_ % 2 == 0) {
118 3         17 my %param = @_;
119            
120             # RSA complete description
121 3 50       14 if (defined $param{n}) {
122            
123 3         7 $self = bless [], $class;
124            
125             # Set attributes
126 3         11 foreach (qw/n e d/) {
127 9 100       47 $self->$_( $param{$_} ) if exists $param{$_};
128             };
129            
130             # Modulus was not defined
131 3 100       11 unless ($self->n) {
132 2         225 carp $INVALID_MSG;
133 2         58 return;
134             };
135             }
136            
137             # Generate new key
138             else {
139            
140 0         0 carp $INVALID_MSG . ' - did you mean generate()?';
141 0         0 return;
142             };
143             }
144            
145             # Invalid request
146             else {
147 2         236 carp $INVALID_MSG;
148 2         16 return;
149             };
150            
151             # Get size (bitsize length of modulus)
152 21         121 my $size = $self->size;
153            
154             # Size is to small
155 21 100 100     1606217 if ($size < 512 || $size > 4096) {
156 2         473 carp 'Keysize is out of range';
157 2         46 return;
158             };
159            
160             # Set emLen (octet length of modulus)
161 19         88 $self->_emLen( _octet_len( $self->n ) );
162            
163 19         133490 return $self;
164             };
165            
166            
167             # Generate a new MagicKey
168             sub generate {
169            
170             # Generator not installed
171 0 0   0 1 0 unless ($GENERATOR) {
172 0         0 carp 'No Math::Prime::Util installed';
173 0         0 return;
174             };
175            
176             # Check for passing of
177 0         0 my ($class, %param) = @_;
178            
179             # Define key size
180 0         0 my $size = $param{size};
181            
182             # Size is given
183 0 0       0 if ($size) {
184            
185             # Key size is too short or impractical
186 0 0 0     0 if ($size < 512 || $size > 4096 || $size % 2) {
      0        
187 0         0 carp "Key size $size is invalid";
188 0         0 return;
189             };
190             }
191            
192             # Default size
193             else {
194 0         0 $size = $DEFAULT_KEY_SIZE;
195             };
196            
197             # Public exponent
198 0         0 my $e = $param{e};
199            
200             # Partial size
201 0         0 my $psize = int( $size / 2 );
202            
203 0         0 my $n;
204            
205             # Maximum number of rounds
206 0         0 my $m = $MAX_GEN_ROUNDS;
207            
208 0         0 my ($p, $q);
209            
210             # Start calculation of combining primes
211             CALC_KEY:
212            
213             # Run as long as allowed
214 0         0 while ($m > 0) {
215            
216             # Fetch random primes p and q
217             # Uses Bytes::Random::Secure by default
218 0         0 $p = random_nbit_prime($psize);
219 0         0 $q = random_nbit_prime($psize);
220            
221             # Fetch a new prime if both are equal
222 0         0 while ($p == $q) {
223 0         0 $q = random_nbit_prime($psize);
224 0 0       0 unless (--$m > 0) {
225 0         0 $p = $q = Math::BigInt->bzero;
226 0         0 last;
227             };
228             };
229            
230             # Calculate modulus
231 0         0 $n = $p * $q;
232            
233             # Bitsize is correct based on given size
234 0 0       0 last if _bitsize($n) == $size;
235            
236 0         0 $m--;
237             };
238            
239 0 0       0 unless ($m > 0) {
240 0         0 carp 'Maximum rounds for key generation is reached';
241 0         0 return;
242             };
243            
244             # Bless object
245 0         0 my $self = bless [], $class;
246            
247             # Set e
248 0 0       0 $self->e($e) if $e;
249            
250             # Calculate phi
251 0         0 my $phi = ($p - 1) * ($q - 1);
252            
253             # Calculate multiplicative inverse of e modulo phi
254 0         0 my $d = $self->e->copy->bmodinv($phi);
255            
256             # $d is too short
257 0 0       0 goto CALC_KEY if _bitsize($d) < $size / 4;
258            
259             # Store d
260 0         0 $self->d($d);
261            
262             # Store n
263 0         0 $self->n($n);
264            
265             # Set emLen (octet length of modulus)
266 0         0 $self->_emLen( _octet_len( $self->n ) );
267            
268 0         0 return $self;
269             };
270            
271            
272            
273             # Get or set modulus
274             sub n {
275 155     155 1 2147438 my $self = shift;
276            
277             # Get value
278 155 100       875 return $self->[0] unless $_[0];
279            
280             # Set value
281 22         659 my $n = Math::BigInt->new( shift );
282            
283             # n is not a number
284 22 100       1199 if ($n->is_nan) {
285 1         140 carp 'n is not a number';
286 1         59 return;
287             };
288            
289             # Delete precalculated emLen and size
290 21         190 $#{$self} = 2;
  21         474  
291            
292 21         70 $self->[0] = $n;
293             };
294            
295            
296             # Get or set public exponent
297             sub e {
298 58     58 1 3025 my $self = shift;
299            
300             # Get value
301 58 100 66     303 return ($self->[1] //= Math::BigInt->new('65537')) unless $_[0];
302            
303             # Set value
304 22         496 my $e = Math::BigInt->new( shift );
305            
306             # e is not a number
307 22 100       949 if ($e->is_nan) {
308 1         170 carp 'e is not a number';
309 1         109 return;
310             };
311            
312 21         179 $self->[1] = $e;
313             };
314            
315            
316             # Get or set private exponent
317             sub d {
318 34     34 1 4186 my $self = shift;
319            
320             # Get value
321 34 100 100     207 return ($self->[2] // undef) unless $_[0];
322            
323             # Set value
324 10         254 my $d = Math::BigInt->new( shift );
325            
326             # d is not a number
327 10 100       397 if ($d->is_nan) {
328 1         85 carp 'd is not a number';
329 1         71 return;
330             };
331            
332 9         136 $self->[2] = $d;
333             };
334            
335            
336             # Get key size
337             sub size {
338             # return unless $_[0]->n;
339 31   66 31 1 11593 $_[0]->[3] // ($_[0]->[3] = _bitsize($_[0]->n));
340             };
341            
342            
343             # Sign a message
344             sub sign {
345 6     6 1 1481 my ($self, $message) = @_;
346            
347 6 100       38 unless ($self->d) {
348 1         173 carp 'Unable to sign without a private key';
349 1         122 return;
350             };
351            
352 5         241 b64url_encode(
353             _sign_emsa_pkcs1_v1_5($self, $message)
354             );
355             };
356            
357            
358             # Verify a signature for a message (sig base)
359             sub verify {
360 15     15 1 3186 my ($self,
361             $message,
362             $encoded_message) = @_;
363            
364             # Missing parameters
365 15 100 100     116 unless ($encoded_message && $message) {
366 2         166 carp 'No signature or message given';
367 2         138 return;
368             };
369            
370             # Delete whitespace and padding
371 13         46 $encoded_message =~ tr{=\t-\x0d }{}d;
372            
373             # Invalid message
374 13 100       42 unless ($encoded_message) {
375 1         83 carp 'No signature given';
376 1         72 return;
377             };
378            
379             # No modulus
380             # return unless $self->n;
381            
382             # Verify message
383 12         52 _verify_emsa_pkcs1_v1_5(
384             $self,
385             $message,
386             # _b64url_to_hex( $encoded_message )
387             b64url_decode($encoded_message)
388             );
389             };
390            
391            
392            
393             # Return MagicKey-String (public only)
394             sub to_string {
395 18     18 1 1581 my $self = shift;
396            
397             # return '' unless $n; # Shouldn't be possible
398            
399             # Convert modulus and exponent and add to component array
400 18         65 my @array = ('RSA', _hex_to_b64url($self->n), _hex_to_b64url($self->e));
401            
402 18 100 100     86 if ($_[0] && $self->d) {
403 1         52 push(@array, _hex_to_b64url($self->d));
404             };
405            
406             # Specification is not clear about $mkey =~ s/=+//g;
407 18         168 join('.', @array);
408             };
409            
410            
411             # Returns the b64 urlsafe encoding of a string
412             sub b64url_encode {
413 82 100   82 1 14084 return '' unless $_[0];
414            
415 81         232 my $v = $_[0];
416            
417 81 100       344 utf8::encode $v if utf8::is_utf8 $v;
418 81         426 $v = encode_base64($v, '');
419 81         428 $v =~ tr{+/\t-\x0d }{-_}d;
420            
421             # Trim padding or not
422 81 100       329 $v =~ s/\=+$// unless (defined $_[1] ? $_[1] : 1);
    100          
423 81         426 $v;
424             };
425            
426            
427             # Returns the b64 urlsafe decoded string
428             sub b64url_decode {
429 93     93 1 1146 my $v = shift;
430 93 100       288 return '' unless $v;
431            
432 91         280 $v =~ tr{-_}{+/};
433            
434 91         167 my $padding;
435            
436             # Add padding
437 91 100       331 if ($padding = (length($v) % 4)) {
438 27         96 $v .= chr(61) x (4 - $padding);
439             };
440            
441 91         633 decode_base64($v);
442             };
443            
444            
445             # Get octet length of n
446             sub _emLen {
447             # return 0 unless $_[0]->n;
448 43   66 43   13559988 ($_[0]->[4] // ($_[0]->[4] = _octet_len( $_[0]->n )));
449             };
450            
451            
452             # Sign with emsa padding
453             sub _sign_emsa_pkcs1_v1_5 {
454             # http://www.ietf.org/rfc/rfc3447.txt [Ch. 8.1.1]
455            
456             # key, message
457 6     6   32 my ($K, $M) = @_;
458            
459             # octet length of n
460 6         25 my $k = $K->_emLen;
461            
462             # encode message (Hash digest is always 'sha-256')
463 6 50       124 my $EM = _emsa_encode($M, $k) or return;
464            
465 6         2586 _i2osp(_rsasp1($K, _os2ip($EM)), $k);
466             };
467            
468            
469             # Verify with emsa padding
470             sub _verify_emsa_pkcs1_v1_5 {
471             # http://www.ietf.org/rfc/rfc3447.txt [Ch. 8.2.2]
472            
473             # key, message, signature
474 12     12   43 my ($K, $M, $S) = @_;
475            
476 12         53 my $k = $K->_emLen;
477            
478             # The length of the signature is not
479             # equivalent to the length of the RSA modulus
480             # TODO: This probably needs to check octetlength
481 12 100       280 if (length($S) != $k) {
482 2         680 carp 'Invalid signature';
483 2         186 return;
484             };
485            
486 10         1231 my $s = _os2ip($S);
487 10 50       58 my $m = _rsavp1($K, $s) or return;
488 10 50       1498182 my $EM = _emsa_encode($M, $k) or return;
489            
490 10         4864 return $EM eq _i2osp($m, $k);
491             };
492            
493            
494             # RSA signing
495             sub _rsasp1 {
496             # http://www.ietf.org/rfc/rfc3447.txt [Ch. 5.2.1]
497            
498             # Key, message
499 10     10   1300 my ($K, $m) = @_;
500            
501 10 100 66     55 if ($m >= $K->n || $m < 0) {
502 2         362 carp 'Message representative out of range';
503 2         141 return;
504             };
505            
506 8         1992 return $m->bmodpow($K->d, $K->n);
507             };
508            
509            
510             # RSA verification
511             sub _rsavp1 {
512             # http://www.ietf.org/rfc/rfc3447.txt [Ch. 5.2.2]
513            
514             # Key, signature
515 10     10   37 my ($K, $s) = @_;
516            
517             # Is signature in range?
518 10 50 33     69 if ($s > $K->n || $s < 0) {
519 0         0 carp 'Signature representative out of range';
520 0         0 return;
521             };
522            
523 10         2468 return $s->bmodpow($K->e, $K->n);
524             };
525            
526            
527             # Create code with emsa padding (only sha-256 support)
528             sub _emsa_encode {
529             # http://www.ietf.org/rfc/rfc3447.txt [Ch. 9.2]
530            
531 18     18   242 my ($M, $emLen) = @_;
532            
533             # No message given
534 18 50       73 return unless $M;
535            
536             # Hash digest is always 'sha-256'
537            
538             # Create Hash with DER padding
539 18         335 my $H = sha256($M);
540 18         77 my $T = "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01" .
541             "\x65\x03\x04\x02\x01\x05\x00\x04\x20" . $H;
542 18         48 my $tLen = length( $T );
543            
544 18 50       84 if ($emLen < $tLen + 11) {
545 0         0 carp 'Intended encoded message length too short';
546 0         0 return;
547             };
548            
549 18         3113 return "\x00\x01" . ("\xFF" x ($emLen - $tLen - 3)) . "\x00" . $T;
550             };
551            
552            
553             # Convert from octet string to bigint
554             sub _os2ip {
555             # Based on Crypt::RSA::DataFormat
556             # See also Convert::ASN1
557 58     58   5977 my $os = shift;
558            
559 58         160 my $l = length $os;
560 58 100       206 return if $l > 30_000;
561            
562 57         163 state $base = Math::BigInt->new(256);
563 57         769 my $result = Math::BigInt->bzero;
564 57         2706 for (0 .. $l - 1) {
565             # Maybe optimizable
566 5515         8264954 $result->badd(
567             int(ord(unpack "x$_ a", $os)) * ($base ** int($l - $_ - 1))
568             );
569             };
570 57         29430 $result;
571             };
572            
573            
574             # Convert from bigint to octet string
575             sub _i2osp {
576             # Based on Crypt::RSA::DataFormat
577             # See also Convert::ASN1
578            
579 57     57   69923792 my $num = Math::BigInt->new(shift);
580            
581 57 100 100     9557 return if $num->is_nan || $num->length > 30_000;
582            
583 55   100     1893 my $l = shift || 0;
584 55         662 state $base = Math::BigInt->new(256);
585            
586 55         603 my $result = '';
587            
588 55 100 100     203 if ($l && $num > ( $base ** $l )) {
589 1         698 carp 'i2osp error - Integer is to short';
590 1         13 return;
591             };
592            
593 54         40481 do {
594 5183         686820 my $r = $num % 256;
595 5183         943882 $num = ($num - $r) / 256;
596 5183         2119148 $result = chr($r) . $result;
597             } until ($num < 256);
598            
599 54 100       7231 $result = chr($num) . $result if $num != 0;
600            
601 54 100       11767 if (length($result) < $l) {
602 14         802 $result = chr(0) x ($l - length($result)) . $result;
603             };
604            
605 54         3116 $result;
606             };
607            
608            
609             # Returns the octet length of a given integer
610             sub _octet_len {
611 40     40   1760 return Math::BigInt->new( _bitsize( shift ))
612             ->badd(7)
613             ->bdiv(8)
614             ->bfloor;
615             };
616            
617            
618             # Returns the bitlength of the integer
619             sub _bitsize {
620 65     65   9836 my $int = Math::BigInt->new( $_[0] );
621 65 100       3036 return 0 unless $int;
622             # Trim leading '0b'
623 64         1980 length( $int->as_bin ) - 2;
624             };
625            
626            
627             # base64url to hex number
628             sub _b64url_to_hex {
629             # Based on
630             # https://github.com/sivy/Salmon/blob/master/lib/Salmon/
631             # MagicSignatures/SignatureAlgRsaSha256.pm
632            
633             # Decode and convert b64url encoded hex number
634 55 100   55   1634 my $b64 = b64url_decode( shift ) or return;
635 52         468 Math::BigInt->new('0x' . unpack( 'H*', $b64));
636             };
637            
638            
639             # hex number to base64url
640             sub _hex_to_b64url {
641             # https://github.com/sivy/Salmon/blob/master/lib/Salmon/
642             # MagicSignatures/SignatureAlgRsaSha256.pm
643            
644             # Trim leading '0x'
645 40     40   5431 my $num = substr(Math::BigInt->new( shift )->as_hex, 2);
646            
647             # Add leading zero padding
648 40 100       147427 $num = ( ( ( length $num ) % 2 ) > 0 ) ? '0' . $num : $num;
649            
650             # Encode number using b64url
651 40         278 b64url_encode( pack( 'H*', $num ) );
652             };
653            
654            
655             1;
656            
657            
658             __END__