File Coverage

blib/lib/Crypt/MagicSignatures/Envelope.pm
Criterion Covered Total %
statement 191 218 87.6
branch 88 148 59.4
condition 28 44 63.6
subroutine 25 25 100.0
pod 14 14 100.0
total 346 449 77.0


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