File Coverage

blib/lib/Crypt/MagicSignatures/Envelope.pm
Criterion Covered Total %
statement 192 219 87.6
branch 90 150 60.0
condition 28 44 63.6
subroutine 25 25 100.0
pod 14 14 100.0
total 349 452 77.2


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