File Coverage

blib/lib/Crypt/MagicSignatures/Envelope.pm
Criterion Covered Total %
statement 229 237 96.6
branch 125 154 81.1
condition 36 41 87.8
subroutine 25 25 100.0
pod 14 14 100.0
total 429 471 91.0


line stmt bran cond sub pod time code
1             package Crypt::MagicSignatures::Envelope;
2 9     9   1180292 use Crypt::MagicSignatures::Key qw/b64url_encode b64url_decode/;
  9         648137  
  9         797  
3 9     9   104 use Carp 'carp';
  9         24  
  9         416  
4 9     9   61 use strict;
  9         21  
  9         225  
5 9     9   49 use warnings;
  9         24  
  9         290  
6 9     9   6643 use Mojo::DOM;
  9         1654247  
  9         507  
7 9     9   4645 use Mojo::JSON qw/encode_json decode_json/;
  9         168960  
  9         885  
8 9     9   89 use Mojo::Util qw/trim/;
  9         24  
  9         406  
9              
10 9     9   139 use v5.10.1;
  9         38  
11              
12             our $VERSION = '0.15';
13              
14             our @CARP_NOT;
15              
16             # MagicEnvelope namespace
17             our $ME_NS = 'http://salmon-protocol.org/ns/magic-env';
18              
19             sub _trim_all;
20              
21             # Constructor
22             sub new {
23 34     34 1 204654 my $class = shift;
24              
25 34         84 my $self;
26              
27             # Bless object with parameters
28 34 100       153 if (@_ > 1) {
29              
30 11 100       57 if (@_ % 2) {
31 1         317 carp 'Wrong number of arguments';
32 1         14 return;
33             };
34              
35 10         50 my %param = @_;
36              
37             # Given algorithm is wrong
38 10 100 100     60 if ($param{alg} &&
39             uc($param{alg}) ne 'RSA-SHA256') {
40 1         228 carp 'Algorithm is not supported';
41 1         29 return;
42             };
43              
44             # Given encoding is wrong
45 9 100 100     56 if ($param{encoding} &&
46             lc($param{encoding}) ne 'base64url') {
47 1         211 carp 'Encoding is not supported';
48 1         12 return;
49             };
50              
51             # No payload is given
52 8 100       36 unless (defined $param{data}) {
53 1         209 carp 'No data payload defined';
54 1         13 return;
55             };
56              
57             # Create object
58 7         21 $self = bless {}, $class;
59              
60             # Set data
61 7 100       66 $self->data( delete $param{data} ) if $param{data};
62              
63             # Set data type if defined
64             $self->data_type( delete $param{data_type} )
65 7 100       38 if $param{data_type};
66              
67             # Append all defined signatures
68 7         17 foreach ( @{$param{sigs}} ) {
  7         40  
69              
70             # No value is given
71 4 100       10 next unless $_->{value};
72              
73             # Create new array reference if not already existing
74 2   100     10 $self->{sigs} //= [];
75              
76             # Start new signature value
77 2         6 my %sig = ( value => $_->{value} );
78 2 100       7 $sig{key_id} = $_->{key_id} if exists $_->{key_id};
79              
80             # Add signature to signature array
81 2         5 push(@{$self->{sigs}}, \%sig);
  2         7  
82             };
83             }
84              
85             # Envelope is defined as a string
86             else {
87 23         65 my $string = shift;
88              
89             # Construct object
90 23         101 $self = bless { sigs => [] }, $class;
91              
92             # Create empty object
93 23 100       110 return $self unless $string;
94              
95 22         170 $string = trim $string;
96              
97             # Message is me-xml
98 22 100       667 if (index($string, '<') == 0) {
    100          
    100          
99              
100             # Parse xml string
101 17         151 my $dom = Mojo::DOM->new(xml => 1)->parse($string);
102              
103             # Extract envelope from env or provenance
104 17         14859 my $env = $dom->at('env');
105 17 100       6262 $env = $dom->at('provenance') unless $env;
106              
107             # Envelope doesn't exist or is in wrong namespace
108 17 100 100     477 if (!$env || $env->namespace ne $ME_NS) {
109 2         594 carp 'Invalid envelope data';
110 2         48 return;
111             };
112              
113             # Retrieve and edit data
114 15         906 my $data = $env->at('data');
115              
116             # The envelope is empty
117 15 100       3107 unless (defined $data) {
118 1         251 carp 'No data payload defined';
119 1         24 return;
120             };
121              
122 14         36 my $temp;
123              
124             # Add data type if given
125 14 100       72 $self->data_type( $temp ) if $temp = $data->attr->{type};
126              
127             # Add decoded data
128 14         84 $self->data( b64url_decode( $data->text ) );
129              
130             # The envelope is empty
131 14 100       58 unless ($self->data) {
132 1         288 carp 'No data payload defined';
133 1         28 return;
134             };
135              
136             # Check algorithm
137 13 100 66     54 if (($temp = $env->at('alg')) &&
138             (uc $temp->text ne 'RSA-SHA256')) {
139 2         953 carp 'Algorithm is not supported';
140 2         51 return;
141             };
142              
143             # Check encoding
144 11 100 66     3223 if (($temp = $env->at('encoding')) &&
145             (lc $temp->text ne 'base64url')) {
146 1 50       437 carp 'Encoding is not supported' and return;
147             };
148              
149             # Find signatures
150             $env->find('sig')->each(
151             sub {
152 10 50   10   3214 my $sig_text = $_->text or return;
153              
154 10         357 my %sig = ( value => _trim_all $sig_text );
155              
156 10 100       37 if ($temp = $_->attr->{key_id}) {
157 5         85 $sig{key_id} = $temp;
158             };
159              
160             # Add sig to array
161 10         107 push( @{ $self->{sigs} }, \%sig );
  10         46  
162 10         2567 });
163             }
164              
165             # Message is me-json
166             elsif (index($string, '{') == 0) {
167 2         5 my $env;
168              
169             # Parse json object
170 2         28 $env = decode_json $string;
171              
172 2 50       2631 unless (defined $env) {
173 0         0 return;
174             };
175              
176             # Clone datastructure
177 2         8 foreach (qw/data data_type encoding alg sigs/) {
178 10 100       42 $self->{$_} = delete $env->{$_} if exists $env->{$_};
179             };
180              
181 2         12 $self->data( b64url_decode( $self->data ));
182              
183             # Envelope is empty
184 2 50       9 return unless $self->data;
185              
186             # Unknown parameters
187 2 50       16 carp 'Unknown parameters: ' . join(',', %$env)
188             if keys %$env;
189             }
190              
191             # Message is me as a compact string
192             elsif (index((my $me_c = _trim_all $string), '.YmFzZTY0dXJs.') > 0) {
193              
194             # Parse me compact string
195 1         2 my $value = [];
196 1         9 foreach (@$value = split(/\./, $me_c) ) {
197 6 50       67 $_ = b64url_decode( $_ ) if $_;
198             };
199              
200             # Given encoding is wrong
201 1 50       14 unless (lc $value->[4] eq 'base64url') {
202 0 0       0 carp 'Encoding is not supported' and return;
203             };
204              
205             # Given algorithm is wrong
206 1 50       8 unless (uc $value->[5] eq 'RSA-SHA256') {
207 0 0       0 carp 'Algorithm is not supported' and return;
208             };
209              
210             # Store sig to data structure
211 1         4 for ($self->{sigs}->[0]) {
212 1 50       3 next unless $value->[1];
213 1 50       6 $_->{key_id} = $value->[0] if defined $value->[0];
214 1         3 $_->{value} = $value->[1];
215             };
216              
217             # ME is empty
218 1 50       5 return unless $value->[2];
219 1         7 $self->data( $value->[2] );
220 1 50       6 $self->data_type( $value->[3] ) if $value->[3];
221             }
222              
223             # String is no valid envelope
224             else {
225 2         463 carp 'Invalid envelope data';
226 2         34 return;
227             };
228             };
229              
230             # The envelope is signed
231 20 100       307 $self->{signed} = 1 if $self->{sigs}->[0];
232              
233 20         166 return $self;
234             };
235              
236              
237             # Signature algorithm
238 21     21 1 208 sub alg { 'RSA-SHA256' };
239              
240              
241             # Encoding of the MagicEnvelope
242 21     21 1 197 sub encoding { 'base64url' };
243              
244              
245             # Data of the MagicEnvelope
246             sub data {
247              
248             # Return data
249 98 100 100 98 1 552234 return shift->{data} // '' unless defined $_[1];
250              
251 25         57 my $self = shift;
252              
253             # Delete calculated signature base string
254 25         65 delete $self->{sig_base};
255              
256             # Delete DOM tree
257 25         93 delete $self->{dom};
258              
259 25         135 return ($self->{data} = join ' ', map { $_ } @_);
  25         149  
260             };
261              
262              
263             # Datatype of the MagicEnvelope's content
264             sub data_type {
265              
266             # Return data type
267 41 100 100 41 1 818 return shift->{data_type} // 'text/plain' unless defined $_[1];
268              
269 18         42 my $self = shift;
270              
271             # Delete calculated signature base string
272 18         63 delete $self->{sig_base};
273              
274             # Delete DOM tree
275 18         43 delete $self->{dom};
276              
277 18         57 return ($self->{data_type} = shift);
278             };
279              
280              
281             # Sign MagicEnvelope instance following the spec
282             sub sign {
283 6     6 1 175425294 my $self = shift;
284              
285 6 100       31 return unless @_;
286              
287 5 100       25 return unless $self->data;
288              
289             # Get key and signature information
290 4         20 my ($key_id, $mkey, $flag) = _key_array(@_);
291              
292             # Choose data to sign
293 4 100       33 my $data = $flag eq '-data' ?
294             b64url_encode($self->data) :
295             $self->signature_base;
296              
297             # Regarding key id:
298             # "If the signer does not maintain individual key_ids,
299             # it SHOULD output the base64url encoded representation
300             # of the SHA-256 hash of public key's application/magic-key
301             # representation."
302              
303             # A valid key needs to be available
304 4 50       51 return unless $mkey;
305              
306             # No valid private key
307 4 100       24194 unless ($mkey->d) {
308 1 50       253 carp 'Unable to sign without private exponent' and return;
309             };
310              
311             # Compute signature for base string
312 3         129 my $msig = $mkey->sign( $data );
313              
314             # No valid signature
315 3 50       6391753 return unless $msig;
316              
317             # Sign envelope
318 3         22 my %msig = ( value => $msig );
319 3 100       23 $msig{key_id} = $key_id if defined $key_id;
320              
321             # Push signature
322 3         8 push( @{ $self->{sigs} }, \%msig );
  3         21  
323              
324             # Declare envelope as signed
325 3         12 $self->{signed} = 1;
326              
327             # Return envelope for piping
328 3         51 return $self;
329             };
330              
331              
332             # Verify Signature
333             sub verify {
334 22     22 1 1359516 my $self = shift;
335              
336             # Regarding key id:
337             # "If the signer does not maintain individual key_ids,
338             # it SHOULD output the base64url encoded representation
339             # of the SHA-256 hash of public key's application/magic-key
340             # representation."
341              
342             # Not signed - not verifiable
343 22 50       111 return unless $self->signed;
344              
345 22         62 my $verified = 0;
346 22         82 foreach (@_) {
347              
348 22 100 100     278 my ($key_id, $mkey, $flag) = _key_array(
349             ref $_ && ref $_ eq 'ARRAY' ? @$_ : $_
350             );
351              
352             # No key given
353 22 50       120 next unless $mkey;
354              
355             # Get signature
356 22         309444 my $sig = $self->signature($key_id);
357              
358             # Found key/sig pair
359 22 50       83 if ($sig) {
360              
361 22 100       101 if ($flag ne '-data') {
362 15         71 $verified = $mkey->verify($self->signature_base => $sig->{value});
363 15 100       5276494 last if $verified;
364             };
365              
366             # Verify against data
367 20 100 100     180 if ($flag eq '-data' || $flag eq '-compatible') {
368              
369             # Verify with b64url data
370 13         100 $verified = $mkey->verify(b64url_encode($self->data) => $sig->{value});
371 13 50       4937820 last if $verified;
372             };
373             };
374             };
375              
376 22         273 return $verified;
377             };
378              
379              
380             # Retrieve MagicEnvelope signatures
381             sub signature {
382 38     38 1 16134625 my $self = shift;
383 38         97 my $key_id = shift;
384              
385             # MagicEnvelope has no signature
386 38 100       169 return unless $self->signed;
387              
388 37         122 my @sigs = @{ $self->{sigs} };
  37         168  
389              
390             # No key_id given
391 37 100       143 unless ($key_id) {
392              
393             # Search sigs for necessary default key
394 33         115 foreach (@sigs) {
395 34 100       252 return $_ unless exists $_->{key_id};
396             };
397              
398             # Return first sig
399 3         20 return $sigs[0];
400             }
401              
402             # Key is given
403             else {
404 4         8 my $default;
405              
406             # Search sigs for necessary specific key
407 4         13 foreach (@sigs) {
408              
409             # sig specifies key
410 7 50       18 if (defined $_->{key_id}) {
411              
412             # Found wanted key
413 7 100       38 return $_ if $_->{key_id} eq $key_id;
414             }
415              
416             # sig needs default key
417             else {
418 0         0 $default = $_;
419             };
420             };
421              
422             # Return sig for default key
423 0         0 return $default;
424             };
425              
426             # No matching sig found
427 0         0 return;
428             };
429              
430              
431             # Is the MagicEnvelope signed?
432             sub signed {
433              
434             # There is no specific key_id requested
435 71 100   71 1 1318 return $_[0]->{signed} unless defined $_[1];
436              
437             # Check for specific key_id
438 2         5 foreach my $sig (@{ $_[0]->{sigs} }) {
  2         7  
439 3 100 100     21 return 1 if $sig->{key_id} && $sig->{key_id} eq $_[1];
440             };
441              
442             # Envelope is not signed
443 1         5 return 0;
444             };
445              
446              
447             # Generate and return signature base
448             sub signature_base {
449 24     24 1 19488 my $self = shift;
450              
451 24 100       176 return $self->{sig_base} if $self->{sig_base};
452              
453             return $self->{sig_base} =
454 8         48 join('.',
455             b64url_encode( $self->data, 0 ),
456             b64url_encode( $self->data_type ),
457             b64url_encode( $self->encoding ),
458             b64url_encode( $self->alg )
459             );
460             };
461              
462              
463             # Return the data as a Mojo::DOM if it is xml
464             sub dom {
465 8     8 1 3504 my $self = shift;
466              
467             # Already computed
468 8 100       38 return $self->{dom} if $self->{dom};
469              
470             # No data defined to parse
471 4 100       20 return unless $self->{data};
472              
473             # Create new DOM instantiation
474 3 100       8 if (index($self->data_type, 'xml') >= 0) {
475 2         12 my $dom = Mojo::DOM->new(xml => 1);
476 2         208 $dom->parse( $self->{data} );
477              
478             # Return DOM instantiation (Maybe empty)
479 2         1507 return ($self->{dom} = $dom);
480             };
481              
482 1         5 return;
483             };
484              
485              
486             # Return em-xml string
487             sub to_xml {
488 5     5 1 1583 my $self = shift;
489 5         11 my $embed = shift;
490              
491 5         13 my $xml = '';
492              
493 5         12 my $start_tag = 'env';
494              
495             # Is a provenance me
496 5 100       21 if ($embed) {
497 1         3 $start_tag = 'provenance';
498             }
499              
500             # Is a full document
501             else {
502 4         9 $xml = qq{\n};
503             };
504              
505             # Start document
506 5         18 $xml .= qq{\n};
507              
508             # Data payload
509 5         12 $xml .= '
510 5 100       29 $xml .= ' type="' . $self->data_type . '"' if exists $self->{data_type};
511 5         24 $xml .= ">" . b64url_encode($self->data, 0) . "\n";
512              
513             # Encoding
514 5         137 $xml .= ' ' . $self->encoding . "\n";
515              
516             # Algorithm
517 5         19 $xml .= ' ' . $self->alg . "\n";
518              
519             # Signatures
520 5         12 foreach my $sig (@{$self->{sigs}}) {
  5         18  
521 5         35 $xml .= '
522 5 100       18 $xml .= ' key_id="' . $sig->{key_id} . '"' if $sig->{key_id};
523 5         16 $xml .= '>' . b64url_encode($sig->{value}) . "\n"
524             };
525              
526             # End document
527 5         83 $xml .= "";
528              
529 5         32 return $xml;
530             };
531              
532              
533             # Return em-compact string
534             sub to_compact {
535 4     4 1 772 my $self = shift;
536              
537             # The me has to be signed
538 4 100       22 return unless $self->signed;
539              
540             # Use default signature for serialization
541 2         8 my $sig = $self->signature;
542              
543             return
544             join(
545             '.',
546             b64url_encode( $sig->{key_id} ) || '',
547 2   50     11 b64url_encode( $sig->{value} ),
548             $self->signature_base
549             );
550             };
551              
552              
553             # Return em-json string
554             sub to_json {
555 5     5 1 498 my $self = shift;
556              
557             # Empty envelope
558 5 100       14 return '{}' unless $self->data;
559              
560             # Create new datastructure
561 3         9 my %new_em = (
562             alg => $self->alg,
563             encoding => $self->encoding,
564             data_type => $self->data_type,
565             data => b64url_encode( $self->data, 0),
566             sigs => []
567             );
568              
569             # Loop through signatures
570 3         80 foreach my $sig ( @{ $self->{sigs} } ) {
  3         12  
571 2         7 my %msig = ( value => b64url_encode( $sig->{value} ) );
572 2 100       30 $msig{key_id} = $sig->{key_id} if defined $sig->{key_id};
573 2         4 push( @{ $new_em{sigs} }, \%msig );
  2         8  
574             };
575              
576             # Return json-string
577 3         18 return encode_json \%new_em;
578             };
579              
580              
581             # Delete all whitespaces
582             sub _trim_all {
583 13     13   35 my $string = shift;
584 13         49 $string =~ tr{\t-\x0d }{}d;
585 13         59 $string;
586             };
587              
588              
589             sub _key_array {
590 26 50   26   111 return () unless @_;
591              
592 26         72 my $flag = '-base';
593              
594 26 100 100     178 if ($_[-1] eq '-data' || $_[-1] eq '-compatible' || $_[-1] eq '-base') {
      66        
595 14         40 $flag = pop;
596             };
597              
598 26         165601 my $key = pop;
599 26         69 my $key_id = shift;
600              
601 26 50       140 return () unless $key;
602              
603 26         305695 my @param;
604              
605             # Hash reference
606 26 50 66     241 if (ref $key && $key eq 'HASH') {
607 0 0       0 return () unless $key->{n};
608 0         0 @param = %$key;
609             }
610              
611             # String or object
612             else {
613 26         152919 @param = ($key);
614             };
615              
616             # Create MagicKey from parameter
617 26         184 my $mkey = Crypt::MagicSignatures::Key->new(@param);
618              
619 26         67565 return ($key_id, $mkey, $flag);
620             };
621              
622              
623             1;
624              
625              
626             __END__