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   944685 use strict;
  14         46  
  14         424  
3 14     14   93 use warnings;
  14         42  
  14         492  
4 14     14   95 use Scalar::Util qw/blessed/;
  14         38  
  14         1465  
5 14     14   103 use Carp 'carp';
  14         41  
  14         805  
6            
7 14     14   236 use v5.10.1;
  14         63  
8            
9             our @CARP_NOT;
10             our $VERSION = '0.20';
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   2349 use overload '""' => sub { $_[0]->to_string }, fallback => 1;
  14     12   2286  
  14         197  
  12         569  
27 14     14   9513 use Digest::SHA qw/sha256 sha256_hex/;
  14         50124  
  14         1501  
28 14     14   7432 use MIME::Base64 qw(decode_base64 encode_base64);
  14         9104  
  14         1051  
29            
30             # Implement with GMP or PARI if existent
31 14     14   4447 use Math::BigInt try => 'GMP,Pari';
  14         96625  
  14         216  
32            
33             # Export functions on request
34 14     14   95416 use Exporter 'import';
  14         43  
  14         1322  
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   994 if (eval q{use Math::Prime::Util 'random_nbit_prime'; 1;}) {
  14     14   15414  
  14         203228  
  14         89  
45 14         34325 our $GENERATOR = 1;
46             };
47             };
48            
49            
50             # Construct a new object
51             sub new {
52 32     32 1 32965 my $class = shift;
53 32         81 my $self;
54            
55             # Message to pass to carp on parameter failure
56 32         88 state $INVALID_MSG = 'Invalid parameters for MagicKey construction';
57            
58             # Check if the passed argument is an object
59 32 100       307 if (my $bl = blessed($_[0])) {
60            
61             # It's already a MagicKey object - fine!
62 4 100       60 return $_[0] if $bl->isa(__PACKAGE__);
63             }
64            
65             # Do not support references
66 30 100       139 if (ref $_[0]) {
67            
68             # Passed object or reference is invalid
69 3         556 carp $INVALID_MSG;
70 3         248 return;
71             };
72            
73             # MagicKey in string notation
74 27 100 100     292 if (@_ == 1 && index($_[0], 'RSA.') >= 0) {
    100          
75            
76 22         71 my $string = shift;
77            
78             # New object from parent class
79 22         79 $self = bless [], $class;
80            
81             # Delete whitespace
82 22         113 $string =~ tr{\t-\x0d }{}d;
83            
84             # Ignore mime-type prolog if given
85 22         147 $string =~ s{^data\:application\/magic(?:\-public|\-private)?\-key[,;:]}{}i;
86            
87             # Split MagicKey
88 22         178 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       110 unless ($mod = _b64url_to_hex( $mod )) {
94 2         218 carp $INVALID_MSG;
95 2         20 return;
96             };
97            
98 20 100       1534546 $exp = _b64url_to_hex( $exp ) if $exp;
99 20 100       4010 $private_exp = _b64url_to_hex( $private_exp ) if $private_exp;
100            
101             # Set modulus
102 20         81865 $self->n( $mod );
103            
104             # Set exponent
105 20 100       80 $self->e( $exp ) if $exp;
106            
107             # Set private key
108 20 100       110 $self->d( $private_exp ) if $private_exp;
109             }
110            
111             # MagicKey defined by parameters
112             elsif (@_ % 2 == 0) {
113 3         18 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         12 foreach (qw/n e d/) {
122 9 100       57 $self->$_( $param{$_} ) if exists $param{$_};
123             };
124            
125             # Modulus was not defined
126 3 100       13 unless ($self->n) {
127 2         275 carp $INVALID_MSG;
128 2         73 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         224 carp $INVALID_MSG;
144 2         15 return;
145             };
146            
147             # Get size (bitsize length of modulus)
148 21         164 my $size = $self->size;
149            
150             # Size is to small
151 21 100 100     1660494 if ($size < 512 || $size > 4096) {
152 2         522 carp 'Keysize is out of range';
153 2         44 return;
154             };
155            
156             # Set emLen (octet length of modulus)
157 19         124 $self->_emLen( _octet_len( $self->n ) );
158            
159 19         158547 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 2189272 my $self = shift;
272            
273             # Get value
274 155 100       1047 return $self->[0] unless $_[0];
275            
276             # Set value
277 22         868 my $n = Math::BigInt->new( shift );
278            
279             # n is not a number
280 22 100       1471 if ($n->is_nan) {
281 1         120 carp 'n is not a number';
282 1         58 return;
283             };
284            
285             # Delete precalculated emLen and size
286 21         244 $#{$self} = 2;
  21         513  
287            
288 21         91 $self->[0] = $n;
289             };
290            
291            
292             # Get or set public exponent
293             sub e {
294 58     58 1 2925 my $self = shift;
295            
296             # Get value
297 58 100 66     393 return ($self->[1] //= Math::BigInt->new('65537')) unless $_[0];
298            
299             # Set value
300 22         598 my $e = Math::BigInt->new( shift );
301            
302             # e is not a number
303 22 100       1094 if ($e->is_nan) {
304 1         142 carp 'e is not a number';
305 1         88 return;
306             };
307            
308 21         220 $self->[1] = $e;
309             };
310            
311            
312             # Get or set private exponent
313             sub d {
314 34     34 1 4469 my $self = shift;
315            
316             # Get value
317 34 100 100     249 return ($self->[2] // undef) unless $_[0];
318            
319             # Set value
320 10         299 my $d = Math::BigInt->new( shift );
321            
322             # d is not a number
323 10 100       490 if ($d->is_nan) {
324 1         114 carp 'd is not a number';
325 1         66 return;
326             };
327            
328 9         146 $self->[2] = $d;
329             };
330            
331            
332             # Get key size
333             sub size {
334             # return unless $_[0]->n;
335 31   66 31 1 12578 $_[0]->[3] // ($_[0]->[3] = _bitsize($_[0]->n));
336             };
337            
338            
339             # Sign a message
340             sub sign {
341 6     6 1 1103 my ($self, $message) = @_;
342            
343 6 100       29 unless ($self->d) {
344 1         166 carp 'Unable to sign without a private key';
345 1         125 return;
346             };
347            
348 5         300 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 2847 my ($self,
357             $message,
358             $encoded_message) = @_;
359            
360             # Missing parameters
361 15 100 100     141 unless ($encoded_message && $message) {
362 2         158 carp 'No signature or message given';
363 2         119 return;
364             };
365            
366             # Delete whitespace and padding
367 13         47 $encoded_message =~ tr{=\t-\x0d }{}d;
368            
369             # Invalid message
370 13 100       56 unless ($encoded_message) {
371 1         99 carp 'No signature given';
372 1         69 return;
373             };
374            
375             # No modulus
376             # return unless $self->n;
377            
378             # Verify message
379 12         64 _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 1759 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         81 my @array = ('RSA', _hex_to_b64url($self->n), _hex_to_b64url($self->e));
397            
398 18 100 100     122 if ($_[0] && $self->d) {
399 1         73 push(@array, _hex_to_b64url($self->d));
400             };
401            
402             # Specification is not clear about $mkey =~ s/=+//g;
403 18         199 join('.', @array);
404             };
405            
406            
407             # Returns the b64 urlsafe encoding of a string
408             sub b64url_encode {
409 82 100   82 1 19448 return '' unless $_[0];
410            
411 81         266 my $v = $_[0];
412            
413 81 100       419 utf8::encode $v if utf8::is_utf8 $v;
414 81         531 $v = encode_base64($v, '');
415 81         554 $v =~ tr{+/\t-\x0d }{-_}d;
416            
417             # Trim padding or not
418 81 100       408 $v =~ s/\=+$// unless (defined $_[1] ? $_[1] : 1);
    100          
419 81         502 $v;
420             };
421            
422            
423             # Returns the b64 urlsafe decoded string
424             sub b64url_decode {
425 93     93 1 1761 my $v = shift;
426 93 100       393 return '' unless $v;
427            
428 91         318 $v =~ tr{-_}{+/};
429            
430 91         298 my $padding;
431            
432             # Add padding
433 91 100       396 if ($padding = (length($v) % 4)) {
434 27         131 $v .= chr(61) x (4 - $padding);
435             };
436            
437 91         807 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   14806734 ($_[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   34 my ($K, $M) = @_;
454            
455             # octet length of n
456 6         34 my $k = $K->_emLen;
457            
458             # encode message (Hash digest is always 'sha-256')
459 6 50       144 my $EM = _emsa_encode($M, $k) or return;
460            
461 6         2902 _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   47 my ($K, $M, $S) = @_;
471            
472 12         62 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       355 if (length($S) != $k) {
478 2         762 carp 'Invalid signature';
479 2         162 return;
480             };
481            
482 10         1483 my $s = _os2ip($S);
483 10 50       62 my $m = _rsavp1($K, $s) or return;
484 10 50       1847393 my $EM = _emsa_encode($M, $k) or return;
485            
486 10         5905 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   1221 my ($K, $m) = @_;
496            
497 10 100 66     55 if ($m >= $K->n || $m < 0) {
498 2         409 carp 'Message representative out of range';
499 2         129 return;
500             };
501            
502 8         2045 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   44 my ($K, $s) = @_;
512            
513             # Is signature in range?
514 10 50 33     88 if ($s > $K->n || $s < 0) {
515 0         0 carp 'Signature representative out of range';
516 0         0 return;
517             };
518            
519 10         2875 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   153 my ($M, $emLen) = @_;
528            
529             # No message given
530 18 50       84 return unless $M;
531            
532             # Hash digest is always 'sha-256'
533            
534             # Create Hash with DER padding
535 18         413 my $H = sha256($M);
536 18         87 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         57 my $tLen = length( $T );
539            
540 18 50       93 if ($emLen < $tLen + 11) {
541 0         0 carp 'Intended encoded message length too short';
542 0         0 return;
543             };
544            
545 18         3446 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   6442 my $os = shift;
554            
555 58         156 my $l = length $os;
556 58 100       245 return if $l > 30_000;
557            
558 57         187 state $base = Math::BigInt->new(256);
559 57         825 my $result = Math::BigInt->bzero;
560 57         1698 for (0 .. $l - 1) {
561             # Maybe optimizable
562 5515         10132504 $result->badd(
563             int(ord(unpack "x$_ a", $os)) * ($base ** int($l - $_ - 1))
564             );
565             };
566 57         30832 $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   80306657 my $num = Math::BigInt->new(shift);
576            
577 57 100 100     9002 return if $num->is_nan || $num->length > 30_000;
578            
579 55   100     2413 my $l = shift || 0;
580 55         901 state $base = Math::BigInt->new(256);
581            
582 55         734 my $result = '';
583            
584 55 100 100     260 if ($l && $num > ( $base ** $l )) {
585 1         431 carp 'i2osp error - Integer is to short';
586 1         10 return;
587             };
588            
589 54         46478 do {
590 5183         923887 my $r = $num % 256;
591 5183         1270067 $num = ($num - $r) / 256;
592 5183         2694790 $result = chr($r) . $result;
593             } until ($num < 256);
594            
595 54 100       9112 $result = chr($num) . $result if $num != 0;
596            
597 54 100       14089 if (length($result) < $l) {
598 14         997 $result = chr(0) x ($l - length($result)) . $result;
599             };
600            
601 54         3792 $result;
602             };
603            
604            
605             # Returns the octet length of a given integer
606             sub _octet_len {
607 40     40   2872 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   11012 my $int = Math::BigInt->new( $_[0] );
617 65 100       4304 return 0 unless $int;
618             # Trim leading '0b'
619 64         2479 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   2205 my $b64 = b64url_decode( shift ) or return;
631 52         603 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   8291 my $num = substr(Math::BigInt->new( shift )->as_hex, 2);
642            
643             # Add leading zero padding
644 40 100       165687 $num = ( ( ( length $num ) % 2 ) > 0 ) ? '0' . $num : $num;
645            
646             # Encode number using b64url
647 40         296 b64url_encode( pack( 'H*', $num ) );
648             };
649            
650            
651             1;
652            
653            
654             __END__