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