File Coverage

blib/lib/Protocol/Matrix.pm
Criterion Covered Total %
statement 44 44 100.0
branch 12 20 60.0
condition n/a
subroutine 12 12 100.0
pod 4 4 100.0
total 72 80 90.0


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2015 -- leonerd@leonerd.org.uk
5              
6             package Protocol::Matrix;
7              
8 3     3   39074 use strict;
  3         5  
  3         111  
9 3     3   10 use warnings;
  3         4  
  3         78  
10 3     3   64 use 5.014; # s///r
  3         13  
  3         120  
11              
12             our $VERSION = '0.01';
13              
14 3     3   13 use Carp;
  3         4  
  3         218  
15              
16 3     3   1417 use Crypt::NaCl::Sodium;
  3         37293  
  3         18  
17 3     3   2317 use JSON;
  3         21021  
  3         22  
18 3     3   2210 use MIME::Base64 qw( encode_base64 decode_base64 );
  3         1787  
  3         245  
19              
20 3     3   19 use Exporter 'import';
  3         6  
  3         1142  
21             our @EXPORT_OK = qw(
22             encode_json_for_signing
23             encode_base64_unpadded
24              
25             sign_json
26             verify_json_signature
27             );
28              
29             my $sign = Crypt::NaCl::Sodium->sign;
30              
31             my $json_canon = JSON->new
32             ->convert_blessed
33             ->canonical
34             ->utf8;
35              
36             =head1 NAME
37              
38             C - Helper functions for the Matrix protocol
39              
40             =head1 DESCRIPTION
41              
42             This module provides some helper functions for implementing a F client
43             or server. Currently it only contains a few base-level functions to assist
44             with signing and verifying signatures on federation-level events.
45              
46             =cut
47              
48             =head1 FUNCTIONS
49              
50             =cut
51              
52             =head2 encode_json_for_signing
53              
54             $json = encode_json_for_signing( $data )
55              
56             Encodes a given HASH reference as Canonical JSON, having removed the
57             C and C keys if present. This is the first step
58             towards signing it or verifying an embedded signature in it. The hash
59             referred to by C<$data> remains unmodified by this function.
60              
61             =cut
62              
63             sub encode_json_for_signing
64             {
65 30     30 1 8919 my ( $d ) = @_;
66              
67             # Remove keys that don't get signed
68 30         104 my %to_sign = %$d;
69 30         52 delete $to_sign{signatures};
70 30         32 delete $to_sign{unsigned};
71              
72 30         1398 return $json_canon->encode( \%to_sign );
73             }
74              
75             =head2 encode_base64_unpadded
76              
77             $base64 = encode_base64( $bytes )
78              
79             Returns a character string containing the Base-64 encoding of the given bytes,
80             with no internal linebreaks and no trailing padding.
81              
82             =cut
83              
84             sub encode_base64_unpadded
85             {
86 5     5 1 423 return encode_base64( $_[0], "" ) =~ s/=+$//r;
87             }
88              
89             =head2 sign_json
90              
91             sign_json( $data, secret_key => $key, origin => $name, key_id => $id )
92              
93             Modifies the given HASH reference in-place to add a signature. This signature
94             is created from the given key, and annotated as being from the given origin
95             name and key ID. Existing signatures already in the hash are not disturbed.
96              
97             The C<$key> should be a plain byte string or L object obtained
98             from L's C method.
99              
100             =cut
101              
102             sub sign_json
103             {
104 3     3 1 1608 my ( $data, %args ) = @_;
105              
106 3 50       71 my $key = $args{secret_key} or croak "Require a 'secret_key'";
107              
108 3 50       11 my $origin = $args{origin} or croak "Require an 'origin'";
109 3 50       12 my $key_id = $args{key_id} or croak "Require a 'key_id'";
110              
111 3         9 my $signature = $sign->mac( encode_json_for_signing( $data ), $key );
112              
113 3         18 $data->{signatures}{$origin}{$key_id} = encode_base64_unpadded( $signature );
114             }
115              
116             =head2 verify_json_signature
117              
118             verify_json_signature( $data, public_key => $key, origin => $name, key_id => $id )
119              
120             Inspects the given HASH reference to check that it contains a signature from
121             the named origin, with the given key ID, and that it is actually valid.
122              
123             This function does not return an interesting value; all failures are indicated
124             by thrown exceptions. If no exception is thrown, it can be presumed valid.
125              
126             =cut
127              
128             sub verify_json_signature
129             {
130 4     4 1 2110 my ( $data, %args ) = @_;
131              
132 4 50       24 my $key = $args{public_key} or croak "Require a 'public_key'";
133              
134 4 50       12 my $origin = $args{origin} or croak "Require an 'origin'";
135 4 50       8 my $key_id = $args{key_id} or croak "Require a 'key_id'";
136              
137 4 50       9 $data->{signatures} or
138             croak "No 'signatures'";
139 4 100       234 $data->{signatures}{$origin} or
140             croak "No signatures from '$origin'";
141              
142 3 100       88 my $signature = $data->{signatures}{$origin}{$key_id} or
143             croak "No signature from '$origin' using key '$key_id'";
144              
145 2 50       10 $sign->verify( decode_base64( $signature ), encode_json_for_signing( $data ), $key ) or
146             croak "Signature verification failed";
147             }
148              
149             =head1 AUTHOR
150              
151             Paul Evans
152              
153             =cut
154              
155             0x55AA;